lib/jabber/
lib/minify/
lib/flowplayer/
-lib/password_compat/
lib/pear/Auth/RADIUS.php
lib/pear/Crypt/CHAP.php
lib/pear/HTML/Common.php
lib/amd/src/mustache.js
lib/graphlib.php
lib/spout/
+lib/amd/src/chartjs-lazy.js
mod/assign/feedback/editpdf/fpdi/
repository/s3/S3.php
theme/bootstrapbase/less/bootstrap/
Example:
\$sudo -u www-data /usr/bin/php admin/cli/reset_password.php
\$sudo -u www-data /usr/bin/php admin/cli/reset_password.php --username=rosaura --password=jiu3jiu --ignore-password-policy
-"; //TODO: localize - to be translated later when everything is finished
+";
echo $help;
die;
}
if ($options['username'] == '' ) {
- cli_heading('Password reset'); // TODO: localize.
- $prompt = "enter username (manual authentication only)"; // TODO: localize.
+ cli_heading('Password reset');
+ $prompt = "Enter username (manual authentication only)";
$username = cli_input($prompt);
} else {
$username = $options['username'];
}
if ($options['password'] == '' ) {
- $prompt = "Enter new password"; // TODO: localize.
+ $prompt = "Enter new password";
$password = cli_input($prompt);
} else {
$password = $options['password'];
$errmsg = '';//prevent eclipse warning
if (!$options['ignore-password-policy'] ) {
if (!check_password_policy($password, $errmsg)) {
- cli_error($errmsg);
+ cli_error(html_to_text($errmsg, 0));
}
}
}
if (!empty($searchmanagererror)) {
- $errorstr = get_string($searchmanagererror->errorcode, $searchmanagererror->module);
+ $errorstr = get_string($searchmanagererror->errorcode, $searchmanagererror->module, $searchmanagererror->a);
echo $OUTPUT->notification($errorstr, \core\output\notification::NOTIFY_ERROR);
} else {
echo $OUTPUT->notification(get_string('indexinginfo', 'admin'), \core\output\notification::NOTIFY_INFO);
'debug' => empty($CFG->debugimap) ? null : fopen('php://stderr', 'w'),
);
+ if (strpos($configuration['hostspec'], ':')) {
+ $hostdata = explode(':', $configuration['hostspec']);
+ if (count($hostdata) === 2) {
+ // A hostname in the format hostname:port has been provided.
+ $configuration['hostspec'] = $hostdata[0];
+ $configuration['port'] = $hostdata[1];
+ }
+ }
+
$this->client = new \Horde_Imap_Client_Socket($configuration);
try {
require_once("$CFG->libdir/cronlib.php");
list($options, $unrecognized) = cli_get_params(
- array('help' => false, 'list' => false, 'execute' => false),
+ array('help' => false, 'list' => false, 'execute' => false, 'showsql' => false, 'showdebugging' => false),
array('h' => 'help')
);
Options:
--execute=\\\\some\\\\task Execute scheduled task manually
--list List all scheduled tasks
+--showsql Show sql queries before they are executed
+--showdebugging Show developer level debugging information
-h, --help Print out this help
Example:
die;
}
+if ($options['showdebugging']) {
+ set_debugging(DEBUG_DEVELOPER, true);
+}
+
+if ($options['showsql']) {
+ $DB->set_debug(true);
+}
if ($options['list']) {
cli_heading("List of scheduled tasks ($CFG->wwwroot)");
} else if ($this->config->passtype === 'sha1') {
return (strtolower($fromdb) == sha1($extpassword));
} else if ($this->config->passtype === 'saltedcrypt') {
- require_once($CFG->libdir.'/password_compat/lib/password.php');
return password_verify($extpassword, $fromdb);
} else {
return false;
$DB->update_record('auth_db_users', $user3);
$this->assertTrue($auth->user_login('u3', 'heslo'));
- require_once($CFG->libdir.'/password_compat/lib/password.php');
set_config('passtype', 'saltedcrypt', 'auth/db');
$auth->config->passtype = 'saltedcrypt';
$user3->pass = password_hash('heslo', PASSWORD_BCRYPT);
continue;
}
- $id = $item->id ? $item->id : uniqid();
+ $id = $item->id ? $item->id : html_writer::random_id();
$content = $item->get_content();
$title = $item->get_title();
$ulattr = ['id' => $id . '_group', 'role' => 'group'];
}
$content = $this->output->render($item);
- $id = $item->id ? $item->id : uniqid();
+ $id = $item->id ? $item->id : html_writer::random_id();
$ulattr = ['id' => $id . '_group', 'role' => 'group'];
$liattr = ['class' => [$item->get_css_type(), 'depth_'.$depth], 'tabindex' => '-1'];
$pattr = ['class' => ['tree_item'], 'role' => 'treeitem'];
Given I follow "This month"
And I click on "Export calendar" "button"
And I set the field "All events" to "1"
- And I set the field "This week" to "1"
+ And I set the field "Recent and next 60 days" to "1"
When I click on "Get calendar URL" "button"
Then I should see "&preset_what=all&"
Given I follow "This month"
And I click on "Export calendar" "button"
And I set the field "Events related to courses" to "1"
- And I set the field "This week" to "1"
+ And I set the field "Recent and next 60 days" to "1"
When I click on "Get calendar URL" "button"
Then I should see "&preset_what=courses&"
Given I follow "This month"
And I click on "Export calendar" "button"
And I set the field "Events related to groups" to "1"
- And I set the field "This week" to "1"
+ And I set the field "Recent and next 60 days" to "1"
When I click on "Get calendar URL" "button"
Then I should see "&preset_what=groups&"
Given I follow "This month"
And I click on "Export calendar" "button"
And I set the field "My personal events" to "1"
- And I set the field "This week" to "1"
+ And I set the field "Recent and next 60 days" to "1"
When I click on "Get calendar URL" "button"
Then I should see "&preset_what=user&"
return self::get_course_module_returns();
}
+ /**
+ * Returns description of method parameters
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.2
+ */
+ public static function get_activities_overview_parameters() {
+ return new external_function_parameters(
+ array(
+ 'courseids' => new external_multiple_structure(new external_value(PARAM_INT, 'Course id.')),
+ )
+ );
+ }
+
+ /**
+ * Return activities overview for the given courses.
+ *
+ * @param array $courseids a list of course ids
+ * @return array of warnings and the activities overview
+ * @since Moodle 3.2
+ * @throws moodle_exception
+ */
+ public static function get_activities_overview($courseids) {
+ global $USER;
+
+ // Parameter validation.
+ $params = self::validate_parameters(self::get_activities_overview_parameters(), array('courseids' => $courseids));
+ $courseoverviews = array();
+
+ list($courses, $warnings) = external_util::validate_courses($params['courseids']);
+
+ if (!empty($courses)) {
+ // Add lastaccess to each course (required by print_overview function).
+ // We need the complete user data, the ws server does not load a complete one.
+ $user = get_complete_user_data('id', $USER->id);
+ foreach ($courses as $course) {
+ if (isset($user->lastcourseaccess[$course->id])) {
+ $course->lastaccess = $user->lastcourseaccess[$course->id];
+ } else {
+ $course->lastaccess = 0;
+ }
+ }
+
+ $overviews = array();
+ if ($modules = get_plugin_list_with_function('mod', 'print_overview')) {
+ foreach ($modules as $fname) {
+ $fname($courses, $overviews);
+ }
+ }
+
+ // Format output.
+ foreach ($overviews as $courseid => $modules) {
+ $courseoverviews[$courseid]['id'] = $courseid;
+ $courseoverviews[$courseid]['overviews'] = array();
+
+ foreach ($modules as $modname => $overviewtext) {
+ $courseoverviews[$courseid]['overviews'][] = array(
+ 'module' => $modname,
+ 'overviewtext' => $overviewtext // This text doesn't need formatting.
+ );
+ }
+ }
+ }
+
+ $result = array(
+ 'courses' => $courseoverviews,
+ 'warnings' => $warnings
+ );
+ return $result;
+ }
+
+ /**
+ * Returns description of method result value
+ *
+ * @return external_description
+ * @since Moodle 3.2
+ */
+ public static function get_activities_overview_returns() {
+ return new external_single_structure(
+ array(
+ 'courses' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'id' => new external_value(PARAM_INT, 'Course id'),
+ 'overviews' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'module' => new external_value(PARAM_PLUGIN, 'Module name'),
+ 'overviewtext' => new external_value(PARAM_RAW, 'Overview text'),
+ )
+ )
+ )
+ )
+ ), 'List of courses'
+ ),
+ 'warnings' => new external_warnings()
+ )
+ );
+ }
+
}
ob_end_clean();
// Create the XML file we want to use.
+ $course->category = (array)$course->category;
$imstestcase = new enrol_imsenterprise_testcase();
$imstestcase->imsplugin = enrol_get_plugin('imsenterprise');
$imstestcase->set_test_config();
$imstestcase->imsplugin->cron();
$events = $sink->get_events();
$sink->close();
- $event = $events[0];
+ $event = null;
+ foreach ($events as $eventinfo) {
+ if ($eventinfo instanceof \core\event\course_created ) {
+ $event = $eventinfo;
+ break;
+ }
+ }
// Validate the event triggered is \core\event\course_created. There is no need to validate the other values
// as they have already been validated in the previous steps. Here we only want to make sure that when the
$context = context_course::instance($course->id);
$roleid = $this->assignUserCapability('moodle/course:view', $context->id);
$this->assignUserCapability('moodle/course:update', $context->id, $roleid);
+ $this->assignUserCapability('mod/data:view', $context->id, $roleid);
$conditions = array('course' => $course->id, 'section' => 2);
$DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
}
}
+
+ /**
+ * Test get_activities_overview
+ */
+ public function test_get_activities_overview() {
+ global $USER;
+
+ $this->resetAfterTest();
+ $course1 = self::getDataGenerator()->create_course();
+ $course2 = self::getDataGenerator()->create_course();
+
+ // Create a viewer user.
+ $viewer = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
+ $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
+ $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
+
+ // Create two forums - one in each course.
+ $record = new stdClass();
+ $record->course = $course1->id;
+ $forum1 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course1->id));
+ $forum2 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course2->id));
+
+ $this->setAdminUser();
+ // A standard post in the forum.
+ $record = new stdClass();
+ $record->course = $course1->id;
+ $record->userid = $USER->id;
+ $record->forum = $forum1->id;
+ $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+ $this->setUser($viewer->id);
+ $courses = array($course1->id , $course2->id);
+
+ $result = core_course_external::get_activities_overview($courses);
+ $result = external_api::clean_returnvalue(core_course_external::get_activities_overview_returns(), $result);
+
+ // There should be one entry for course1, and no others.
+ $this->assertCount(1, $result['courses']);
+ $this->assertEquals($course1->id, $result['courses'][0]['id']);
+ // Check expected overview data for the module.
+ $this->assertEquals('forum', $result['courses'][0]['overviews'][0]['module']);
+ $this->assertContains('1 total unread', $result['courses'][0]['overviews'][0]['overviewtext']);
+ }
}
$string['allowunenrol'] = 'Allow the IMS data to <strong>unenrol</strong> students/teachers';
$string['allowunenrol_desc'] = 'If enabled, course enrolments will be removed when specified in the Enterprise data.';
$string['basicsettings'] = 'Basic settings';
+$string['categoryidnumber'] = 'Allow category idnumber';
+$string['categoryidnumber_desc'] = 'If enabled IMS Enterprise will create category with idnumber';
+$string['categoryseparator'] = 'Category Separator Character';
+$string['categoryseparator_desc'] = 'Required when "Category idnumber" is enabled. Character to separate the category name and idnumber.';
$string['coursesettings'] = 'Course data options';
$string['createnewcategories'] = 'Create new (hidden) course categories if not found in Moodle';
$string['createnewcategories_desc'] = 'If the <org><orgunit> element is present in a course\'s incoming data, its content will be used to specify a category if the course is to be created from scratch. The plugin will NOT re-categorise existing courses.
$string['mailusers'] = 'Notify users by email';
$string['messageprovider:imsenterprise_enrolment'] = 'IMS Enterprise enrolment messages';
$string['miscsettings'] = 'Miscellaneous';
+$string['nestedcategories'] = 'Allow nested categories';
+$string['nestedcategories_desc'] = 'If enabled IMS Enterprise will create nested categories';
$string['pluginname'] = 'IMS Enterprise file';
$string['pluginname_desc'] = 'This method will repeatedly check for and process a specially-formatted text file in the location that you specify. The file must follow the IMS Enterprise specifications containing person, group, and membership XML elements.';
$string['processphoto'] = 'Add user photo data to profile';
Some student information systems fail to output the <userid> field. If this is the case, you should enable this setting to allow for using the <sourcedid> as the Moodle user ID. Otherwise, leave this setting disabled.';
$string['truncatecoursecodes'] = 'Truncate course codes to this length';
$string['truncatecoursecodes_desc'] = 'In some situations you may have course codes which you wish to truncate to a specified length before processing. If so, enter the number of characters in this box. Otherwise, leave the box blank and no truncation will occur.';
+$string['updatecourses'] = 'Update course';
+$string['updatecourses_desc'] = 'If enabled, the IMS Enterprise enrolment plugin can update course full and short names (if the "recstatus" flag is set to 2, which represents an update).';
+$string['updateusers'] = 'Update user accounts when specified in IMS data';
+$string['updateusers_desc'] = 'If enabled, IMS Enterprise enrolment data can specify changes to user accounts (if the "recstatus" flag is set to 2, which represents an update).';
$string['usecapitafix'] = 'Tick this box if using "Capita" (their XML format is slightly wrong)';
$string['usecapitafix_desc'] = 'The student data system produced by Capita has been found to have one slight error in its XML output. If you are using Capita you should enable this setting - otherwise leave it un-ticked.';
$string['usersettings'] = 'User data options';
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/group/lib.php');
-
+require_once($CFG->dirroot.'/lib/coursecatlib.php');
/**
* IMS Enterprise file enrolment plugin.
*/
class enrol_imsenterprise_plugin extends enrol_plugin {
+ /**
+ * @var IMSENTERPRISE_ADD imsenterprise add action.
+ */
+ const IMSENTERPRISE_ADD = 1;
+
+ /**
+ * @var IMSENTERPRISE_UPDATE imsenterprise update action.
+ */
+ const IMSENTERPRISE_UPDATE = 2;
+
+ /**
+ * @var IMSENTERPRISE_DELETE imsenterprise delete action.
+ */
+ const IMSENTERPRISE_DELETE = 3;
+
/**
* @var $logfp resource file pointer for writing log data to.
*/
*/
protected $rolemappings;
+ /**
+ * @var $defaultcategoryid id of default category.
+ */
+ protected $defaultcategoryid;
+
/**
* Read in an IMS Enterprise file.
* Originally designed to handle v1.1 files but should be able to handle
$this->logfp = fopen($logtolocation, 'a');
}
+ $this->defaultcategoryid = null;
+
$fileisnew = false;
if ( file_exists($filename) ) {
core_php_time_limit::raise();
$this->log_line('Found file '.$filename);
$this->xmlcache = '';
+ $categoryseparator = trim($this->get_config('categoryseparator'));
+ $categoryidnumber = $this->get_config('categoryidnumber');
+
// Make sure we understand how to map the IMS-E roles to Moodle roles.
$this->load_role_mappings();
// Make sure we understand how to map the IMS-E course names to Moodle course names.
// Decide if we want to process the file (based on filepath, modification time, and MD5 hash)
// This is so we avoid wasting the server's efforts processing a file unnecessarily.
- if (empty($prevpath) || ($filename != $prevpath)) {
+ if ($categoryidnumber && empty($categoryseparator)) {
+ $this->log_line('Category idnumber is enabled but category separator is not defined - skipping processing.');
+ } else if (empty($prevpath) || ($filename != $prevpath)) {
$fileisnew = true;
} else if (isset($prevtime) && ($filemtime <= $prevtime)) {
$this->log_line('File modification time is not more recent than last update - skipping processing.');
// Get configs.
$truncatecoursecodes = $this->get_config('truncatecoursecodes');
$createnewcourses = $this->get_config('createnewcourses');
- $createnewcategories = $this->get_config('createnewcategories');
+ $updatecourses = $this->get_config('updatecourses');
if ($createnewcourses) {
require_once("$CFG->dirroot/course/lib.php");
$group->coursecode = trim($matches[1]);
}
+ $matches = array();
if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)) {
$group->long = trim($matches[1]);
}
+
+ $matches = array();
if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {
$group->short = trim($matches[1]);
}
+
+ $matches = array();
if (preg_match('{<description>.*?<full>(.*?)</full>.*?</description>}is', $tagcontents, $matches)) {
$group->full = trim($matches[1]);
}
- if (preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)) {
- $group->category = trim($matches[1]);
+ if (preg_match('{<org>(.*?)</org>}is', $tagcontents, $matchesorg)) {
+ if (preg_match_all('{<orgunit>(.*?)</orgunit>}is', $matchesorg[1], $matchesorgunit)) {
+ $group->categories = array_map('trim', $matchesorgunit[1]);
+ }
}
$recstatus = ($this->get_recstatus($tagcontents, 'group'));
// Third, check if the course(s) exist.
foreach ($group->coursecode as $coursecode) {
$coursecode = trim($coursecode);
- if (!$DB->get_field('course', 'id', array('idnumber' => $coursecode))) {
+ $dbcourse = $DB->get_record('course', array('idnumber' => $coursecode));
+ if (!$dbcourse) {
if (!$createnewcourses) {
$this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
} else {
- // Create the (hidden) course(s) if not found
+ // Create the (hidden) course(s) if not found.
$courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults.
// New course.
$course->enablecompletion = $courseconfig->enablecompletion;
// Insert default names for teachers/students, from the current language.
- // Handle course categorisation (taken from the group.org.orgunit field if present).
- if (!empty($group->category)) {
- // If the category is defined and exists in Moodle, we want to store it in that one.
- if ($catid = $DB->get_field('course_categories', 'id', array('name' => $group->category))) {
- $course->category = $catid;
- } else if ($createnewcategories) {
- // Else if we're allowed to create new categories, let's create this one.
- $newcat = new stdClass();
- $newcat->name = $group->category;
- $newcat->visible = 0;
- $catid = $DB->insert_record('course_categories', $newcat);
- $course->category = $catid;
- $this->log_line("Created new (hidden) category, #$catid: $newcat->name");
- } else {
- // If not found and not allowed to create, stick with default.
- $this->log_line('Category '.$group->category.' not found in Moodle database, so using '.
- 'default category instead.');
- $course->category = $this->get_default_category_id();
- }
- } else {
- $course->category = $this->get_default_category_id();
- }
+ // Handle course categorisation (taken from the group.org.orgunit or group.org.id fields if present).
+ $course->category = $this->get_category_from_group($group->categories);
+
$course->startdate = time();
// Choose a sort order that puts us at the start of the list!
$course->sortorder = 0;
$this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
}
- } else if ($recstatus == 3 && ($courseid = $DB->get_field('course', 'id', array('idnumber' => $coursecode)))) {
+ } else if (($recstatus == self::IMSENTERPRISE_UPDATE) && $dbcourse) {
+ if ($updatecourses) {
+ // Update course. Allowed fields to be updated are:
+ // Short Name, and Full Name.
+ $hasupdates = false;
+ if (!empty($group->short)) {
+ if ($group->short != $dbcourse->shortname) {
+ $dbcourse->shortname = $group->short;
+ $hasupdates = true;
+ }
+ }
+ if (!empty($group->full)) {
+ if ($group->full != $dbcourse->fullname) {
+ $dbcourse->fullname = $group->full;
+ $hasupdates = true;
+ }
+ }
+ if ($hasupdates) {
+ update_course($dbcourse);
+ $courseid = $dbcourse->id;
+ $this->log_line("Updated course $coursecode in Moodle (Moodle ID is $courseid)");
+ }
+ } else {
+ // Update courses option is not enabled. Ignore.
+ $this->log_line("Ignoring update to course $coursecode");
+ }
+ } else if (($recstatus == self::IMSENTERPRISE_DELETE) && $dbcourse) {
// If course does exist, but recstatus==3 (delete), then set the course as hidden.
- $DB->set_field('course', 'visible', '0', array('id' => $courseid));
+ $courseid = $dbcourse->id;
+ $show = false;
+ course_change_visibility($courseid, $show);
+ $this->log_line("Updated (set to hidden) course $coursecode in Moodle (Moodle ID is $courseid)");
}
}
}
$fixcasepersonalnames = $this->get_config('fixcasepersonalnames');
$imsdeleteusers = $this->get_config('imsdeleteusers');
$createnewusers = $this->get_config('createnewusers');
+ $imsupdateusers = $this->get_config('imsupdateusers');
$person = new stdClass();
if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
$person->idnumber = trim($matches[1]);
}
+
+ $matches = array();
if (preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
$person->firstname = trim($matches[1]);
}
+
+ $matches = array();
if (preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
$person->lastname = trim($matches[1]);
}
- if (preg_match('{<userid>(.*?)</userid>}is', $tagcontents, $matches)) {
+
+ $matches = array();
+ if (preg_match('{<userid.*?>(.*?)</userid>}is', $tagcontents, $matches)) {
$person->username = trim($matches[1]);
}
+
+ $matches = array();
+ if (preg_match('{<userid\s+authenticationtype\s*=\s*"*(.+?)"*>.*?</userid>}is', $tagcontents, $matches)) {
+ $person->auth = trim($matches[1]);
+ }
+
if ($imssourcedidfallback && trim($person->username) == '') {
- // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied
+ // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied.
// NB We don't use an "elseif" because the tag may be supplied-but-empty.
$person->username = $person->idnumber;
}
+
+ $matches = array();
if (preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)) {
$person->email = trim($matches[1]);
}
+
+ $matches = array();
if (preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)) {
$person->url = trim($matches[1]);
}
+
+ $matches = array();
if (preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)) {
$person->city = trim($matches[1]);
}
+
+ $matches = array();
if (preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)) {
$person->country = trim($matches[1]);
}
$recstatus = ($this->get_recstatus($tagcontents, 'person'));
// Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on.
- if ($recstatus == 3) {
+ if ($recstatus == self::IMSENTERPRISE_DELETE) {
if ($imsdeleteusers) { // If we're allowed to delete user records.
// Do not dare to hack the user.deleted field directly in database!!!
} else {
$this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
}
+ } else if ($recstatus == self::IMSENTERPRISE_UPDATE) { // Update user.
+ if ($imsupdateusers) {
+ if ($id = $DB->get_field('user', 'id', array('idnumber' => $person->idnumber))) {
+ $person->id = $id;
+ $DB->update_record('user', $person);
+ $this->log_line("Updated user $person->username");
+ } else {
+ $this->log_line("Ignoring update request for non-existent user $person->username");
+ }
+ } else {
+ $this->log_line("Ignoring update request for user $person->username");
+ }
} else { // Add or update record.
// If they don't exist and they have a defined username, and $createnewusers == true, we create them.
$person->lang = $CFG->lang;
// TODO: MDL-15863 this needs more work due to multiauth changes, use first auth for now.
- $auth = explode(',', $CFG->auth);
- $auth = reset($auth);
- $person->auth = $auth;
+ if (empty($person->auth)) {
+ $auth = explode(',', $CFG->auth);
+ $auth = reset($auth);
+ $person->auth = $auth;
+ }
$person->confirmed = 1;
$person->timemodified = time();
$person->mnethostid = $CFG->mnet_localhost_id;
foreach ($membermatches as $mmatch) {
$member = new stdClass();
$memberstoreobj = new stdClass();
+ $matches = array();
if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)) {
$member->idnumber = trim($matches[1]);
}
+
+ $matches = array();
if (preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)) {
// 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides.
$member->roletype = trim($matches[1]);
// and there are more besides.
$member->roletype = trim($matches[1]);
}
+
+ $matches = array();
if (preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)) {
// 1 means active, 0 means inactive - treat this as enrol vs unenrol.
$member->status = trim($matches[1]);
}
$recstatus = ($this->get_recstatus($mmatch[1], 'role'));
- if ($recstatus == 3) {
+ if ($recstatus == self::IMSENTERPRISE_DELETE) {
// See above - recstatus of 3 (==delete) is treated the same as status of 0.
$member->status = 0;
}
$timeframe = new stdClass();
$timeframe->begin = 0;
$timeframe->end = 0;
+ $matches = array();
if (preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)) {
$timeframe = $this->decode_timeframe($matches[1]);
}
+
+ $matches = array();
if (preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is',
$mmatch[1], $matches)) {
$member->groupname = trim($matches[1]);
if (preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)) {
$ret->begin = mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);
}
+
+ $matches = array();
if (preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)) {
$ret->end = mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);
}
global $CFG;
require_once($CFG->libdir.'/coursecatlib.php');
- static $defaultcategoryid = null;
-
- if ($defaultcategoryid === null) {
+ if ($this->defaultcategoryid === null) {
$category = coursecat::get_default();
- $defaultcategoryid = $category->id;
+ $this->defaultcategoryid = $category->id;
+ }
+
+ return $this->defaultcategoryid;
+ }
+
+ /**
+ * Find the category using idnumber or name.
+ *
+ * @param array $categories List of categories
+ *
+ * @return int id of category found.
+ */
+ private function get_category_from_group($categories) {
+ global $DB;
+
+ if (empty($categories)) {
+ $catid = $this->get_default_category_id();
+ } else {
+ $createnewcategories = $this->get_config('createnewcategories');
+ $categoryseparator = trim($this->get_config('categoryseparator'));
+ $nestedcategories = trim($this->get_config('nestedcategories'));
+ $searchbyidnumber = trim($this->get_config('categoryidnumber'));
+
+ if (!empty($categoryseparator)) {
+ $sep = '{\\'.$categoryseparator.'}';
+ }
+
+ $catid = 0;
+ $fullnestedcatname = '';
+
+ foreach ($categories as $categoryinfo) {
+ if ($searchbyidnumber) {
+ $values = preg_split($sep, $categoryinfo, -1, PREG_SPLIT_NO_EMPTY);
+ if (count($values) < 2) {
+ $this->log_line('Category ' . $categoryinfo . ' missing name or idnumber. Using default category instead.');
+ $catid = $this->get_default_category_id();
+ break;
+ }
+ $categoryname = $values[0];
+ $categoryidnumber = $values[1];
+ } else {
+ $categoryname = $categoryinfo;
+ $categoryidnumber = null;
+ if (empty($categoryname)) {
+ $this->log_line('Category ' . $categoryinfo . ' missing name. Using default category instead.');
+ $catid = $this->get_default_category_id();
+ break;
+ }
+ }
+
+ if (!empty($fullnestedcatname)) {
+ $fullnestedcatname .= ' / ';
+ }
+
+ $fullnestedcatname .= $categoryname;
+ $parentid = $catid;
+
+ // Check if category exist.
+ $params = array();
+ if ($searchbyidnumber) {
+ $params['idnumber'] = $categoryidnumber;
+ } else {
+ $params['name'] = $categoryname;
+ }
+ if ($nestedcategories) {
+ $params['parent'] = $parentid;
+ }
+
+ if ($catid = $DB->get_field('course_categories', 'id', $params)) {
+ continue; // This category already exists.
+ }
+
+ // If we're allowed to create new categories, let's create this one.
+ if ($createnewcategories) {
+ $newcat = new stdClass();
+ $newcat->name = $categoryname;
+ $newcat->visible = 0;
+ $newcat->parent = $parentid;
+ $newcat->idnumber = $categoryidnumber;
+ $newcat = coursecat::create($newcat);
+ $catid = $newcat->id;
+ $this->log_line("Created new (hidden) category '$fullnestedcatname'");
+ } else {
+ // If not found and not allowed to create, stick with default.
+ $this->log_line('Category ' . $categoryinfo . ' not found in Moodle database. Using default category instead.');
+ $catid = $this->get_default_category_id();
+ break;
+ }
+ }
}
- return $defaultcategoryid;
+ return $catid;
}
/**
$settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/createnewusers',
get_string('createnewusers', 'enrol_imsenterprise'), get_string('createnewusers_desc', 'enrol_imsenterprise'), 0));
+ $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/imsupdateusers',
+ get_string('updateusers', 'enrol_imsenterprise'), get_string('updateusers_desc', 'enrol_imsenterprise'), 0));
+
$settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/imsdeleteusers',
get_string('deleteusers', 'enrol_imsenterprise'), get_string('deleteusers_desc', 'enrol_imsenterprise'), 0));
$settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/createnewcourses',
get_string('createnewcourses', 'enrol_imsenterprise'), get_string('createnewcourses_desc', 'enrol_imsenterprise'), 0));
+ $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/updatecourses',
+ get_string('updatecourses', 'enrol_imsenterprise'), get_string('updatecourses_desc', 'enrol_imsenterprise'), 0));
+
$settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/createnewcategories',
get_string('createnewcategories', 'enrol_imsenterprise'), get_string('createnewcategories_desc', 'enrol_imsenterprise'),
0));
+ $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/nestedcategories',
+ get_string('nestedcategories', 'enrol_imsenterprise'), get_string('nestedcategories_desc', 'enrol_imsenterprise'), 0));
+
+ $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/categoryidnumber',
+ get_string('categoryidnumber', 'enrol_imsenterprise'), get_string('categoryidnumber_desc', 'enrol_imsenterprise'), 0));
+
+ $settings->add(new admin_setting_configtext('enrol_imsenterprise/categoryseparator',
+ get_string('categoryseparator', 'enrol_imsenterprise'), get_string('categoryseparator_desc', 'enrol_imsenterprise'), '',
+ PARAM_TEXT, 3));
+
$settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/imsunenrol',
get_string('allowunenrol', 'enrol_imsenterprise'), get_string('allowunenrol_desc', 'enrol_imsenterprise'), 0));
$prevnusers = $DB->count_records('user');
$user1 = new StdClass();
+ $user1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
$user1->username = 'u1';
$user1->email = 'u1@example.com';
$user1->firstname = 'U';
$this->assertEquals(($prevnusers + 1), $DB->count_records('user'));
}
+ /**
+ * Add new users and set an auth type
+ */
+ public function test_users_add_with_auth() {
+ global $DB;
+
+ $prevnusers = $DB->count_records('user');
+
+ $user2 = new StdClass();
+ $user2->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $user2->username = 'u2';
+ $user2->auth = 'cas';
+ $user2->email = 'u2@u2.org';
+ $user2->firstname = 'U';
+ $user2->lastname = '2';
+
+ $users = array($user2);
+ $this->set_xml_file($users);
+ $this->imsplugin->cron();
+
+ $dbuser = $DB->get_record('user', array('username' => $user2->username));
+ // TODO: MDL-15863 this needs more work due to multiauth changes, use first auth for now.
+ $dbauth = explode(',', $dbuser->auth);
+ $dbauth = reset($dbauth);
+
+ $this->assertEquals(($prevnusers + 1), $DB->count_records('user'));
+ $this->assertEquals($dbauth, $user2->auth);
+ }
+
+
+ /**
+ * Update user
+ */
+ public function test_user_update() {
+ global $DB;
+
+ $user = $this->getDataGenerator()->create_user(array('idnumber' => 'test-update-user'));
+ $imsuser = new stdClass();
+ $imsuser->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_UPDATE;
+ // THIS SHOULD WORK, surely?: $imsuser->username = $user->username;
+ // But this is required...
+ $imsuser->username = $user->idnumber;
+ $imsuser->email = 'u3@u3.org';
+ $imsuser->firstname = 'U';
+ $imsuser->lastname = '3';
+
+ $this->set_xml_file(array($imsuser));
+ $this->imsplugin->cron();
+ $dbuser = $DB->get_record('user', array('id' => $user->id), '*', MUST_EXIST);
+ $this->assertEquals($imsuser->email, $dbuser->email);
+ $this->assertEquals($imsuser->firstname, $dbuser->firstname);
+ $this->assertEquals($imsuser->lastname, $dbuser->lastname);
+ }
+
+ public function test_user_update_disabled() {
+ global $DB;
+
+ $this->imsplugin->set_config('imsupdateusers', false);
+
+ $user = $this->getDataGenerator()->create_user(array('idnumber' => 'test-update-user'));
+ $imsuser = new stdClass();
+ $imsuser->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_UPDATE;
+ // THIS SHOULD WORK, surely?: $imsuser->username = $user->username;
+ // But this is required...
+ $imsuser->username = $user->idnumber;
+ $imsuser->email = 'u3@u3.org';
+ $imsuser->firstname = 'U';
+ $imsuser->lastname = '3';
+
+ $this->set_xml_file(array($imsuser));
+ $this->imsplugin->cron();
+
+ // Verify no changes have been made.
+ $dbuser = $DB->get_record('user', array('id' => $user->id), '*', MUST_EXIST);
+ $this->assertEquals($user->email, $dbuser->email);
+ $this->assertEquals($user->firstname, $dbuser->firstname);
+ $this->assertEquals($user->lastname, $dbuser->lastname);
+ }
+
+ /**
+ * Delete user
+ */
+ public function test_user_delete() {
+ global $DB;
+
+ $this->imsplugin->set_config('imsdeleteusers', true);
+ $user = $this->getDataGenerator()->create_user(array('idnumber' => 'test-update-user'));
+
+ $imsuser = new stdClass();
+ $imsuser->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_DELETE;
+ $imsuser->username = $user->username;
+ $imsuser->firstname = $user->firstname;
+ $imsuser->lastname = $user->lastname;
+ $imsuser->email = $user->email;
+ $this->set_xml_file(array($imsuser));
+
+ $this->imsplugin->cron();
+ $this->assertEquals(1, $DB->get_field('user', 'deleted', array('id' => $user->id), '*', MUST_EXIST));
+ }
+
+ /**
+ * Delete user disabled
+ */
+ public function test_user_delete_disabled() {
+ global $DB;
+
+ $this->imsplugin->set_config('imsdeleteusers', false);
+ $user = $this->getDataGenerator()->create_user(array('idnumber' => 'test-update-user'));
+
+ $imsuser = new stdClass();
+ $imsuser->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_DELETE;
+ $imsuser->username = $user->username;
+ $imsuser->firstname = $user->firstname;
+ $imsuser->lastname = $user->lastname;
+ $imsuser->email = $user->email;
+ $this->set_xml_file(array($imsuser));
+
+ $this->imsplugin->cron();
+ $this->assertEquals(0, $DB->get_field('user', 'deleted', array('id' => $user->id), '*', MUST_EXIST));
+ }
+
/**
* Existing courses are not created again
*/
// Default mapping according to default course attributes - IMS description tags mapping.
$course1->imsshort = $course1->fullname;
$course2->imsshort = $course2->fullname;
+ unset($course1->category);
+ unset($course2->category);
$prevncourses = $DB->count_records('course');
$prevncourses = $DB->count_records('course');
$course1 = new StdClass();
+ $course1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
$course1->idnumber = 'id1';
$course1->imsshort = 'id1';
- $course1->category = 'DEFAULT CATNAME';
+ $course1->category[] = 'DEFAULT CATNAME';
$course2 = new StdClass();
+ $course2->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
$course2->idnumber = 'id2';
$course2->imsshort = 'id2';
- $course2->category = 'DEFAULT CATNAME';
+ $course2->category[] = 'DEFAULT CATNAME';
$courses = array($course1, $course2);
$this->set_xml_file(false, $courses);
$this->imsplugin->cron();
$this->assertEquals(($prevncourses + 2), $DB->count_records('course'));
+ $this->assertTrue($DB->record_exists('course', array('idnumber' => $course1->idnumber)));
+ $this->assertTrue($DB->record_exists('course', array('idnumber' => $course2->idnumber)));
+ }
+
+ /**
+ * Verify that courses are not created when createnewcourses
+ * option is diabled.
+ */
+ public function test_courses_add_createnewcourses_disabled() {
+ global $DB;
+
+ $this->imsplugin->set_config('createnewcourses', false);
+ $prevncourses = $DB->count_records('course');
+
+ $course1 = new StdClass();
+ $course1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course1->idnumber = 'id1';
+ $course1->imsshort = 'id1';
+ $course1->category[] = 'DEFAULT CATNAME';
+
+ $course2 = new StdClass();
+ $course2->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course2->idnumber = 'id2';
+ $course2->imsshort = 'id2';
+ $course2->category[] = 'DEFAULT CATNAME';
+
+ $courses = array($course1, $course2);
+ $this->set_xml_file(false, $courses);
+ $this->imsplugin->cron();
+
+ $courses = array($course1, $course2);
+ $this->set_xml_file(false, $courses);
+ $this->imsplugin->cron();
+
+ // Verify the courses have not ben creased.
+ $this->assertEquals($prevncourses , $DB->count_records('course'));
+ $this->assertFalse($DB->record_exists('course', array('idnumber' => $course1->idnumber)));
+ $this->assertFalse($DB->record_exists('course', array('idnumber' => $course2->idnumber)));
+ }
+
+ /**
+ * Test adding a course with no idnumber.
+ */
+ public function test_courses_no_idnumber() {
+ global $DB;
+
+ $prevncourses = $DB->count_records('course');
+
+ $course1 = new StdClass();
+ $course1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course1->idnumber = '';
+ $course1->imsshort = 'id1';
+ $course1->category[] = 'DEFAULT CATNAME';
+
+ $this->set_xml_file(false, array($course1));
+ $this->imsplugin->cron();
+
+ // Verify no action.
+ $this->assertEquals($prevncourses, $DB->count_records('course'));
+ }
+
+ /**
+ * Add new course with the truncateidnumber setting.
+ */
+ public function test_courses_add_truncate_idnumber() {
+ global $DB;
+
+ $truncatelength = 4;
+
+ $this->imsplugin->set_config('truncatecoursecodes', $truncatelength);
+ $prevncourses = $DB->count_records('course');
+
+ $course1 = new StdClass();
+ $course1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course1->idnumber = '123456789';
+ $course1->imsshort = 'id1';
+ $course1->category[] = 'DEFAULT CATNAME';
+
+ $this->set_xml_file(false, array($course1));
+ $this->imsplugin->cron();
+
+ // Verify the new course has been added.
+ $this->assertEquals(($prevncourses + 1), $DB->count_records('course'));
+
+ $truncatedidnumber = substr($course1->idnumber, 0, $truncatelength);
+
+ $this->assertTrue($DB->record_exists('course', array('idnumber' => $truncatedidnumber)));
}
/**
$course1 = new stdClass();
$course1->idnumber = 'id1';
$course1->imsshort = 'id1';
- $course1->category = '';
+ $course1->category[] = '';
$this->set_xml_file(false, array($course1));
$this->imsplugin->cron();
$this->imsplugin->set_config('imscoursemapsummary', 'coursecode');
$course1 = new StdClass();
+ $course1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
$course1->idnumber = 'id1';
$course1->imsshort = 'description_short1';
$course1->imslong = 'description_long';
$course1->imsfull = 'description_full';
- $course1->category = 'DEFAULT CATNAME';
+ $course1->category[] = 'DEFAULT CATNAME';
$this->set_xml_file(false, array($course1));
$this->imsplugin->cron();
$this->imsplugin->set_config('imscoursemapsummary', 'full');
$course2 = new StdClass();
+ $course2->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
$course2->idnumber = 'id2';
$course2->imsshort = 'description_short2';
$course2->imslong = 'description_long';
$course2->imsfull = 'description_full';
- $course2->category = 'DEFAULT CATNAME';
+ $course2->category[] = 'DEFAULT CATNAME';
$this->set_xml_file(false, array($course2));
$this->imsplugin->cron();
$this->imsplugin->set_config('imscoursemapsummary', 'full');
$course3 = new StdClass();
+ $course3->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
$course3->idnumber = 'id3';
$course3->imsshort = 'description_short3';
- $course3->category = 'DEFAULT CATNAME';
+ $course3->category[] = 'DEFAULT CATNAME';
$this->set_xml_file(false, array($course3));
$this->imsplugin->cron();
}
+ /**
+ * Course updates
+ */
+ public function test_course_update() {
+ global $DB;
+
+ $course4 = new StdClass();
+ $course4->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course4->idnumber = 'id4';
+ $course4->imsshort = 'id4';
+ $course4->imsfull = 'id4';
+ $course4->category[] = 'DEFAULT CATNAME';
+
+ $this->set_xml_file(false, array($course4));
+ $this->imsplugin->cron();
+
+ $course4u = $DB->get_record('course', array('idnumber' => $course4->idnumber));
+
+ $course4u->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_UPDATE;
+ $course4u->imsshort = 'description_short_updated';
+ $course4u->imsfull = 'description_full_updated';
+ unset($course4u->category);
+
+ $this->set_xml_file(false, array($course4u));
+ $this->imsplugin->cron();
+
+ $dbcourse = $DB->get_record('course', array('idnumber' => $course4->idnumber));
+ $this->assertFalse(!$dbcourse);
+ $this->assertEquals($dbcourse->shortname, $course4u->imsshort);
+ $this->assertEquals($dbcourse->fullname, $course4u->imsfull);
+ }
+
+ /**
+ * Course delete. Make it hidden.
+ */
+ public function test_course_delete() {
+ global $DB;
+
+ $course8 = new StdClass();
+ $course8->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course8->idnumber = 'id8';
+ $course8->imsshort = 'id8';
+ $course8->imsfull = 'id8';
+ $course8->category[] = 'DEFAULT CATNAME';
+
+ $this->set_xml_file(false, array($course8));
+ $this->imsplugin->cron();
+
+ $course8d = $DB->get_record('course', array('idnumber' => $course8->idnumber));
+ $this->assertEquals($course8d->visible, 1);
+
+ $course8d->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_DELETE;
+ unset($course8d->category);
+
+ $this->set_xml_file(false, array($course8d));
+ $this->imsplugin->cron();
+
+ $dbcourse = $DB->get_record('course', array('idnumber' => $course8d->idnumber));
+ $this->assertFalse(!$dbcourse);
+ $this->assertEquals($dbcourse->visible, 0);
+ }
+
+
+ /**
+ * Nested categories with name during course creation
+ */
+ public function test_nested_categories() {
+ global $DB;
+
+ $this->imsplugin->set_config('nestedcategories', true);
+
+ $topcat = 'DEFAULT CATNAME';
+ $subcat = 'DEFAULT SUB CATNAME';
+
+ $course5 = new StdClass();
+ $course5->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course5->idnumber = 'id5';
+ $course5->imsshort = 'description_short';
+ $course5->imslong = 'description_long';
+ $course5->imsfull = 'description_full';
+ $course5->category = array();
+ $course5->category[] = $topcat;
+ $course5->category[] = $subcat;
+
+ $this->set_xml_file(false, array($course5));
+ $this->imsplugin->cron();
+
+ $parentcatid = $DB->get_field('course_categories', 'id', array('name' => $topcat));
+ $subcatid = $DB->get_field('course_categories', 'id', array('name' => $subcat, 'parent' => $parentcatid));
+
+ $this->assertTrue(isset($subcatid));
+ $this->assertTrue($subcatid > 0);
+
+ $topcat = 'DEFAULT CATNAME';
+ $subcat = 'DEFAULT SUB CATNAME TEST2';
+
+ $course6 = new StdClass();
+ $course6->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course6->idnumber = 'id6';
+ $course6->imsshort = 'description_short';
+ $course6->imslong = 'description_long';
+ $course6->imsfull = 'description_full';
+ $course6->category = array();
+ $course6->category[] = $topcat;
+ $course6->category[] = $subcat;
+
+ $this->set_xml_file(false, array($course6));
+ $this->imsplugin->cron();
+
+ $parentcatid = $DB->get_field('course_categories', 'id', array('name' => $topcat));
+ $subcatid = $DB->get_field('course_categories', 'id', array('name' => $subcat, 'parent' => $parentcatid));
+
+ $this->assertTrue(isset($subcatid));
+ $this->assertTrue($subcatid > 0);
+ }
+
+
+ /**
+ * Test that duplicate nested categories with name are not created
+ */
+ public function test_nested_categories_for_dups() {
+ global $DB;
+
+ $this->imsplugin->set_config('nestedcategories', true);
+
+ $topcat = 'DEFAULT CATNAME';
+ $subcat = 'DEFAULT SUB CATNAME DUPTEST';
+
+ $course7 = new StdClass();
+ $course7->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course7->idnumber = 'id7';
+ $course7->imsshort = 'description_short';
+ $course7->imslong = 'description_long';
+ $course7->imsfull = 'description_full';
+ $course7->category[] = $topcat;
+ $course7->category[] = $subcat;
+
+ $this->set_xml_file(false, array($course7));
+ $this->imsplugin->cron();
+
+ $prevncategories = $DB->count_records('course_categories');
+
+ $course8 = new StdClass();
+ $course8->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course8->idnumber = 'id8';
+ $course8->imsshort = 'description_short';
+ $course8->imslong = 'description_long';
+ $course8->imsfull = 'description_full';
+ $course8->category[] = $topcat;
+ $course8->category[] = $subcat;
+
+ $this->set_xml_file(false, array($course8));
+ $this->imsplugin->cron();
+
+ $this->assertEquals($prevncategories, $DB->count_records('course_categories'));
+ }
+
+ /**
+ * Nested categories with idnumber during course creation
+ */
+ public function test_nested_categories_idnumber() {
+ global $DB;
+
+ $this->imsplugin->set_config('nestedcategories', true);
+ $this->imsplugin->set_config('categoryidnumber', true);
+ $this->imsplugin->set_config('categoryseparator', '|');
+
+ $catsep = trim($this->imsplugin->get_config('categoryseparator'));
+
+ $topcatname = 'DEFAULT CATNAME';
+ $subcatname = 'DEFAULT SUB CATNAME';
+ $topcatidnumber = '01';
+ $subcatidnumber = '0101';
+
+ $topcat = $topcatname.$catsep.$topcatidnumber;
+ $subcat = $subcatname.$catsep.$subcatidnumber;
+
+ $course1 = new StdClass();
+ $course1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course1->idnumber = 'id5';
+ $course1->imsshort = 'description_short';
+ $course1->imslong = 'description_long';
+ $course1->imsfull = 'description_full';
+ $course1->category[] = $topcat;
+ $course1->category[] = $subcat;
+
+ $this->set_xml_file(false, array($course1));
+ $this->imsplugin->cron();
+
+ $parentcatid = $DB->get_field('course_categories', 'id', array('idnumber' => $topcatidnumber));
+ $subcatid = $DB->get_field('course_categories', 'id', array('idnumber' => $subcatidnumber, 'parent' => $parentcatid));
+
+ $this->assertTrue(isset($subcatid));
+ $this->assertTrue($subcatid > 0);
+
+ // Change the category separator character.
+ $this->imsplugin->set_config('categoryseparator', ':');
+
+ $catsep = trim($this->imsplugin->get_config('categoryseparator'));
+
+ $topcatname = 'DEFAULT CATNAME';
+ $subcatname = 'DEFAULT SUB CATNAME TEST2';
+ $topcatidnumber = '01';
+ $subcatidnumber = '0102';
+
+ $topcat = $topcatname.$catsep.$topcatidnumber;
+ $subcat = $subcatname.$catsep.$subcatidnumber;
+
+ $course2 = new StdClass();
+ $course2->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course2->idnumber = 'id6';
+ $course2->imsshort = 'description_short';
+ $course2->imslong = 'description_long';
+ $course2->imsfull = 'description_full';
+ $course2->category[] = $topcat;
+ $course2->category[] = $subcat;
+
+ $this->set_xml_file(false, array($course2));
+ $this->imsplugin->cron();
+
+ $parentcatid = $DB->get_field('course_categories', 'id', array('idnumber' => $topcatidnumber));
+ $subcatid = $DB->get_field('course_categories', 'id', array('idnumber' => $subcatidnumber, 'parent' => $parentcatid));
+
+ $this->assertTrue(isset($subcatid));
+ $this->assertTrue($subcatid > 0);
+ }
+
+ /**
+ * Test that duplicate nested categories with idnumber are not created
+ */
+ public function test_nested_categories_idnumber_for_dups() {
+ global $DB;
+
+ $this->imsplugin->set_config('nestedcategories', true);
+ $this->imsplugin->set_config('categoryidnumber', true);
+ $this->imsplugin->set_config('categoryseparator', '|');
+
+ $catsep = trim($this->imsplugin->get_config('categoryseparator'));
+
+ $topcatname = 'DEFAULT CATNAME';
+ $subcatname = 'DEFAULT SUB CATNAME';
+ $topcatidnumber = '01';
+ $subcatidnumber = '0101';
+
+ $topcat = $topcatname.$catsep.$topcatidnumber;
+ $subcat = $subcatname.$catsep.$subcatidnumber;
+
+ $course1 = new StdClass();
+ $course1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course1->idnumber = 'id1';
+ $course1->imsshort = 'description_short';
+ $course1->imslong = 'description_long';
+ $course1->imsfull = 'description_full';
+ $course1->category[] = $topcat;
+ $course1->category[] = $subcat;
+
+ $this->set_xml_file(false, array($course1));
+ $this->imsplugin->cron();
+
+ $prevncategories = $DB->count_records('course_categories');
+
+ $course2 = new StdClass();
+ $course2->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course2->idnumber = 'id2';
+ $course2->imsshort = 'description_short';
+ $course2->imslong = 'description_long';
+ $course2->imsfull = 'description_full';
+ $course2->category[] = $topcat;
+ $course2->category[] = $subcat;
+
+ $this->set_xml_file(false, array($course2));
+ $this->imsplugin->cron();
+
+ $this->assertEquals($prevncategories, $DB->count_records('course_categories'));
+ }
+
+ /**
+ * Test that nested categories with idnumber is not created if name is missing
+ */
+ public function test_categories_idnumber_missing_name() {
+ global $DB, $CFG;
+
+ $this->imsplugin->set_config('nestedcategories', true);
+ $this->imsplugin->set_config('categoryidnumber', true);
+ $this->imsplugin->set_config('categoryseparator', '|');
+ $catsep = trim($this->imsplugin->get_config('categoryseparator'));
+
+ $topcatname = 'DEFAULT CATNAME';
+ $subcatname = '';
+ $topcatidnumber = '01';
+ $subcatidnumber = '0101';
+
+ $topcat = $topcatname.$catsep.$topcatidnumber;
+ $subcat = $subcatname.$catsep.$subcatidnumber;
+
+ $course1 = new StdClass();
+ $course1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course1->idnumber = 'id1';
+ $course1->imsshort = 'description_short';
+ $course1->imslong = 'description_long';
+ $course1->imsfull = 'description_full';
+ $course1->category[] = $topcat;
+ $course1->category[] = $subcat;
+
+ $this->set_xml_file(false, array($course1));
+ $this->imsplugin->cron();
+
+ // Check all categories except the last subcategory was created.
+ $parentcatid = $DB->get_field('course_categories', 'id', array('idnumber' => $topcatidnumber));
+ $this->assertTrue((boolean)$parentcatid);
+ $subcatid = $DB->get_field('course_categories', 'id', array('idnumber' => $subcatidnumber, 'parent' => $parentcatid));
+ $this->assertFalse((boolean)$subcatid);
+
+ // Check course was put in default category.
+ $defaultcat = coursecat::get_default();
+ $dbcourse = $DB->get_record('course', array('idnumber' => $course1->idnumber), '*', MUST_EXIST);
+ $this->assertEquals($dbcourse->category, $defaultcat->id);
+
+ }
+
+ /**
+ * Create category with name (nested categories not activated).
+ */
+ public function test_create_category_name_no_nested() {
+ global $DB;
+
+ $course = new StdClass();
+ $course->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course->idnumber = 'id';
+ $course->imsshort = 'description_short';
+ $course->imslong = 'description_long';
+ $course->imsfull = 'description_full';
+ $course->category[] = 'CATNAME';
+
+ $this->set_xml_file(false, array($course));
+ $this->imsplugin->cron();
+
+ $dbcat = $DB->get_record('course_categories', array('name' => $course->category[0]));
+ $this->assertFalse(!$dbcat);
+ $this->assertEquals($dbcat->parent, 0);
+
+ $dbcourse = $DB->get_record('course', array('idnumber' => $course->idnumber));
+ $this->assertFalse(!$dbcourse);
+ $this->assertEquals($dbcourse->category, $dbcat->id);
+
+ }
+
+ /**
+ * Find a category with name (nested categories not activated).
+ */
+ public function test_find_category_name_no_nested() {
+ global $DB;
+
+ $cattop = $this->getDataGenerator()->create_category(array('name' => 'CAT-TOP'));
+ $catsub = $this->getDataGenerator()->create_category(array('name' => 'CAT-SUB', 'parent' => $cattop->id));
+ $prevcats = $DB->count_records('course_categories');
+
+ $course = new StdClass();
+ $course->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course->idnumber = 'id';
+ $course->imsshort = 'description_short';
+ $course->imslong = 'description_long';
+ $course->imsfull = 'description_full';
+ $course->category[] = 'CAT-SUB';
+
+ $this->set_xml_file(false, array($course));
+ $this->imsplugin->cron();
+
+ $newcats = $DB->count_records('course_categories');
+
+ // Check that no new category was not created.
+ $this->assertEquals($prevcats, $newcats);
+
+ // Check course is associated to CAT-SUB.
+ $dbcourse = $DB->get_record('course', array('idnumber' => $course->idnumber));
+ $this->assertFalse(!$dbcourse);
+ $this->assertEquals($dbcourse->category, $catsub->id);
+
+ }
+
+ /**
+ * Create category with idnumber (nested categories not activated).
+ */
+ public function test_create_category_idnumber_no_nested() {
+ global $DB;
+
+ $this->imsplugin->set_config('categoryidnumber', true);
+ $this->imsplugin->set_config('categoryseparator', '|');
+ $catsep = trim($this->imsplugin->get_config('categoryseparator'));
+
+ $course = new StdClass();
+ $course->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course->idnumber = 'id';
+ $course->imsshort = 'description_short';
+ $course->imslong = 'description_long';
+ $course->imsfull = 'description_full';
+ $course->category[] = 'CATNAME'. $catsep . 'CATIDNUMBER';
+
+ $this->set_xml_file(false, array($course));
+ $this->imsplugin->cron();
+
+ $dbcat = $DB->get_record('course_categories', array('idnumber' => 'CATIDNUMBER'));
+ $this->assertFalse(!$dbcat);
+ $this->assertEquals($dbcat->parent, 0);
+ $this->assertEquals($dbcat->name, 'CATNAME');
+
+ $dbcourse = $DB->get_record('course', array('idnumber' => $course->idnumber));
+ $this->assertFalse(!$dbcourse);
+ $this->assertEquals($dbcourse->category, $dbcat->id);
+
+ }
+
+ /**
+ * Find a category with idnumber (nested categories not activated).
+ */
+ public function test_find_category_idnumber_no_nested() {
+ global $DB;
+
+ $this->imsplugin->set_config('categoryidnumber', true);
+ $this->imsplugin->set_config('categoryseparator', '|');
+ $catsep = trim($this->imsplugin->get_config('categoryseparator'));
+
+ $topcatname = 'CAT-TOP';
+ $subcatname = 'CAT-SUB';
+ $topcatidnumber = 'ID-TOP';
+ $subcatidnumber = 'ID-SUB';
+
+ $cattop = $this->getDataGenerator()->create_category(array('name' => $topcatname, 'idnumber' => $topcatidnumber));
+ $catsub = $this->getDataGenerator()->create_category(array('name' => $subcatname, 'idnumber' => $subcatidnumber,
+ 'parent' => $cattop->id));
+ $prevcats = $DB->count_records('course_categories');
+
+ $course = new StdClass();
+ $course->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course->idnumber = 'id';
+ $course->imsshort = 'description_short';
+ $course->imslong = 'description_long';
+ $course->imsfull = 'description_full';
+ $course->category[] = $subcatname . $catsep . $subcatidnumber;
+
+ $this->set_xml_file(false, array($course));
+ $this->imsplugin->cron();
+
+ $newcats = $DB->count_records('course_categories');
+
+ // Check that no new category was not created.
+ $this->assertEquals($prevcats, $newcats);
+
+ $dbcourse = $DB->get_record('course', array('idnumber' => $course->idnumber));
+ $this->assertFalse(!$dbcourse);
+ $this->assertEquals($dbcourse->category, $catsub->id);
+
+ }
+
+ /**
+ * Test that category with idnumber is not created if name is missing (nested categories not activated).
+ */
+ public function test_category_idnumber_missing_name_no_nested() {
+ global $DB;
+
+ $this->imsplugin->set_config('categoryidnumber', true);
+ $this->imsplugin->set_config('categoryseparator', '|');
+ $catsep = trim($this->imsplugin->get_config('categoryseparator'));
+
+ $catidnumber = '01';
+
+ $course = new StdClass();
+ $course->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD;
+ $course->idnumber = 'id1';
+ $course->imsshort = 'description_short';
+ $course->imslong = 'description_long';
+ $course->imsfull = 'description_full';
+ $course->category[] = '' . $catsep . $catidnumber;
+
+ $this->set_xml_file(false, array($course));
+ $this->imsplugin->cron();
+
+ // Check category was not created.
+ $catid = $DB->get_record('course_categories', array('idnumber' => $catidnumber));
+ $this->assertFalse($catid);
+
+ // Check course was put in default category.
+ $defaultcat = coursecat::get_default();
+ $dbcourse = $DB->get_record('course', array('idnumber' => $course->idnumber), '*', MUST_EXIST);
+ $this->assertEquals($dbcourse->category, $defaultcat->id);
+
+ }
+
/**
* Sets the plugin configuration for testing
*/
$this->imsplugin->set_config('mailadmins', false);
$this->imsplugin->set_config('prev_path', '');
$this->imsplugin->set_config('createnewusers', true);
+ $this->imsplugin->set_config('imsupdateusers', true);
$this->imsplugin->set_config('createnewcourses', true);
+ $this->imsplugin->set_config('updatecourses', true);
$this->imsplugin->set_config('createnewcategories', true);
+ $this->imsplugin->set_config('categoryseparator', '');
+ $this->imsplugin->set_config('categoryidnumber', false);
+ $this->imsplugin->set_config('nestedcategories', false);
}
/**
if (!empty($users)) {
foreach ($users as $user) {
$xmlcontent .= '
- <person>
+ <person';
+
+ // Optional recstatus (1=add, 2=update, 3=delete).
+ if (!empty($user->recstatus)) {
+ $xmlcontent .= ' recstatus="'.$user->recstatus.'"';
+ }
+
+ $xmlcontent .= '>
<sourcedid>
<source>TestSource</source>
<id>'.$user->username.'</id>
</sourcedid>
- <userid>'.$user->username.'</userid>
+ <userid';
+
+ // Optional authentication type.
+ if (!empty($user->auth)) {
+ $xmlcontent .= ' authenticationtype="'.$user->auth.'"';
+ }
+
+ $xmlcontent .= '>'.$user->username.'</userid>
<name>
<fn>'.$user->firstname.' '.$user->lastname.'</fn>
<n>
foreach ($courses as $course) {
$xmlcontent .= '
- <group>
+ <group';
+
+ // Optional recstatus (1=add, 2=update, 3=delete).
+ if (!empty($course->recstatus)) {
+ $xmlcontent .= ' recstatus="'.$course->recstatus.'"';
+ }
+
+ $xmlcontent .= '>
<sourcedid>
<source>TestSource</source>
<id>'.$course->idnumber.'</id>
// The orgunit tag value is used by moodle as category name.
$xmlcontent .= '
</description>
- <org>
- <orgunit>'.$course->category.'</orgunit>
+ <org>';
+ // Optional category name.
+ if (isset($course->category) && !empty($course->category)) {
+ foreach ($course->category as $category) {
+ $xmlcontent .= '
+ <orgunit>'.$category.'</orgunit>';
+ }
+ }
+
+ $xmlcontent .= '
</org>
</group>';
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2016052300; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version = 2016070600; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2016051900; // Requires this Moodle version.
$plugin->component = 'enrol_imsenterprise';
$string['cannotcreatedboninstall'] = '<p>No és pot crear la base de dades.</p> <p>La base de dades especificada no existeix i l\'usuari que heu proporcionat no té permÃs per a crear-la.</p>
<p>L\'administrador del lloc hauria de verificar la configuració de la base de dades.</p>';
-$string['cannotcreatelangdir'] = 'No s\'ha pogut crear el directori d\'idiomes.';
+$string['cannotcreatelangdir'] = 'No s\'ha pogut crear el directori d\'idiomes';
$string['cannotcreatetempdir'] = 'No s\'ha pogut crear el directori temporal';
$string['cannotdownloadcomponents'] = 'No s\'han pogut baixar components';
-$string['cannotdownloadzipfile'] = 'No s\'ha pogut baixar el fitxer zip';
+$string['cannotdownloadzipfile'] = 'No s\'ha pogut baixar el fitxer ZIP';
$string['cannotfindcomponent'] = 'No s\'ha pogut trobar el component';
$string['cannotsavemd5file'] = 'No s\'ha pogut desar el fitxer md5';
-$string['cannotsavezipfile'] = 'No s\'ha pogut desar el fitxer zip';
+$string['cannotsavezipfile'] = 'No s\'ha pogut desar el fitxer ZIP';
$string['cannotunzipfile'] = 'No s\'ha pogut descomprimir el fitxer';
-$string['componentisuptodate'] = 'El component està al dia';
+$string['componentisuptodate'] = 'El component està actualitzat';
$string['dmlexceptiononinstall'] = '<p>S\'ha produït un error de la base de dades [{$a->errorcode}].<br />{$a->debuginfo}</p>';
$string['downloadedfilecheckfailed'] = 'Ha fallat la comprovació del fitxer baixat';
$string['invalidmd5'] = 'L\'md5 no és và lid. Torneu-ho a provar';
$string['missingrequiredfield'] = 'Falta algun camp necessari';
-$string['remotedownloaderror'] = 'No s\'ha pogut baixar el component al vostre servidor. Verifiqueu els parà metres de servidor intermediari. Es recomana l\'extensió cURL.<br /><br />Haureu de baixar manualment el fitxer <a href="{$a->url}">{$a->url}</a>, copiar-lo a la ubicació "{$a->dest}" del vostre servidor i descomprimir-lo allÃ.';
+$string['remotedownloaderror'] = '<p>No s\'ha pogut baixar el component al vostre servidor. Verifiqueu els parà metres del servidor intermediari. Es recomana vivament l\'extensió cURL de PHP.</p>
+<p>Haureu de baixar manualment el fitxer <a href="{$a->url}">{$a->url}</a>, copiar-lo a la ubicació «{$a->dest}» del vostre servidor i descomprimir-lo allÃ.</p>';
$string['wrongdestpath'] = 'El camà de destinació és erroni';
$string['wrongsourcebase'] = 'L\'adreça (URL) base de la font és errònia';
$string['wrongzipfilename'] = 'El nom del fitxer ZIP és erroni';
defined('MOODLE_INTERNAL') || die();
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Català ';
$string['invaliduserchangeme'] = 'Username "changeme" is reserved -- you cannot create an account with it.';
$string['ipblocked'] = 'This site is not available currently.';
$string['ipblocker'] = 'IP blocker';
-$string['ipblockersyntax'] = 'Put every entry on one line. Valid entries are either full IP address (such as <b>192.168.10.1</b>) which matches a single host; or partial address (such as <b>192.168.</b>) which matches any address starting with those numbers; or CIDR notation (such as <b>231.54.211.0/20</b>); or a range of IP addresses (such as <b>231.3.56.10-20</b>) where the range applies to the last part of the address. Text domain names (like \'example.com\') are not supported. Blank lines are ignored.';
+$string['ipblockersyntax'] = 'Put every entry on one line. Valid entries are either full IP address (such as <b>192.168.10.1</b>) which matches a single host; or partial address (such as <b>192.168</b>) which matches any address starting with those numbers; or CIDR notation (such as <b>231.54.211.0/20</b>); or a range of IP addresses (such as <b>231.3.56.10-20</b>) where the range applies to the last part of the address. Text domain names (like \'example.com\') are not supported. Blank lines are ignored.';
$string['iplookup'] = 'IP address lookup';
$string['iplookupgeoplugin'] = '<a href="http://www.geoplugin.com">geoPlugin</a> service is currently being used to look up geographical information. For more accurate results we recommend installing a local copy of the MaxMind GeoLite database.';
$string['iplookupinfo'] = 'By default Moodle uses the free online NetGeo (The Internet Geographic Database) server to lookup location of IP addresses, unfortunately this database is not maintained anymore and may return <em>wildly incorrect</em> data.
$string['usesitenameforsitepages'] = 'Use site name for site pages';
$string['usetags'] = 'Enable tags functionality';
$string['validateerror'] = 'This value is not valid';
+$string['validateiperror'] = 'These IP addresses are invalid: {$a}';
$string['verifychangedemail'] = 'Restrict domains when changing email';
$string['warningcurrentsetting'] = 'Invalid current value: {$a}';
$string['warningiconvbuggy'] = 'Your version of the iconv library does not support the //IGNORE modifier. You should install the mbstring extension which can be used instead for cleaning strings containing invalid UTF-8 characters.';
$string['eventcoursemoduleinstancelistviewed'] = 'Course module instance list viewed';
$string['eventcourseuserreportviewed'] = 'Course user report viewed';
$string['eventcourseviewed'] = 'Course viewed';
+$string['eventdashboardreset'] = 'Dashboard reset';
+$string['eventdashboardsreset'] = 'Dashboards reset';
+$string['eventdashboardviewed'] = 'Dashboard viewed';
$string['eventemailfailed'] = 'Email failed to send';
$string['eventname'] = 'Event name';
$string['eventrecentactivityviewed'] = 'Recent activity viewed';
$string['showallusers'] = 'Show all users';
$string['showblockcourse'] = 'Show list of courses containing block';
$string['showcategory'] = 'Show {$a}';
+$string['showchartdata'] = 'Show chart data';
$string['showcomments'] = 'Show/hide comments';
$string['showcommentsnonjs'] = 'Show comments';
$string['showdescription'] = 'Display description on course page';
$string['duration'] = 'Duration';
$string['emptydatabaseerror'] = 'Database table is not present, or contains no index records.';
$string['enginenotfound'] = 'Engine {$a} not found.';
-$string['enginenotinstalled'] = '{$a} not installed.';
+$string['enginenotinstalled'] = 'Engine {$a} not installed.';
+$string['enginenotselected'] = 'You have not selected any search engine.';
$string['engineserverstatus'] = 'The search engine is not available. Please contact your administrator.';
$string['enteryoursearchquery'] = 'Enter your search query';
$string['errors'] = 'Errors';
return true;
}
$result = true;
+ $badips = array();
foreach($ips as $ip) {
$ip = trim($ip);
+ if (empty($ip)) {
+ continue;
+ }
if (preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}$#', $ip, $match) ||
preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}(\/\d{1,2})$#', $ip, $match) ||
preg_match('#^(\d{1,3})(\.\d{1,3}){3}(-\d{1,3})$#', $ip, $match)) {
- $result = true;
} else {
$result = false;
- break;
+ $badips[] = $ip;
}
}
if($result) {
return true;
} else {
- return get_string('validateerror', 'admin');
+ return get_string('validateiperror', 'admin', join(', ', $badips));
}
}
}
// 10% speedup to move MoveNext to child class
function MoveNext()
{
- if(@OCIfetchinto($this->_queryID,$this->fields,$this->fetchMode)) {
+ $ret = @oci_fetch_array($this->_queryID,$this->fetchMode);
+ if($ret !== false) {
global $ADODB_ANSI_PADDING_OFF;
+ $this->fields = $ret;
$this->_currentRow++;
$this->_updatefields();
$arr = array();
return $arr;
}
- if (!@OCIfetchinto($this->_queryID,$this->fields,$this->fetchMode)) {
+ $ret = @oci_fetch_array($this->_queryID,$this->fetchMode);
+ if ($ret === false) {
$arr = array();
return $arr;
}
+ $this->fields = $ret;
$this->_updatefields();
$results = array();
$cnt = 0;
{
global $ADODB_ANSI_PADDING_OFF;
- $ret = @OCIfetchinto($this->_queryID,$this->fields,$this->fetchMode);
+ $ret = @oci_fetch_array($this->_queryID,$this->fetchMode);
if ($ret) {
+ $this->fields = $ret;
$this->_updatefields();
if (!empty($ADODB_ANSI_PADDING_OFF)) {
}
}
}
- return $ret;
+ return $ret !== false;
}
}
* Removed random seed initialization from lib/adodb/adodb.inc.php:216 (see 038f546 and MDL-41198).
* MDL-52286 Added muting erros in ADORecordSet::__destruct().
Check if fixed upstream during the next upgrade and remove this note.
+ * MDL-52544 Pull upstream patch for php7 and ocipo.
skodak, iarenaza, moodler, stronk7, abgreeve
--- /dev/null
+// 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/>.
+
+/**
+ * Chart axis.
+ *
+ * @package core
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @module core/chart_axis
+ */
+define([], function() {
+
+ /**
+ * Chart axis class.
+ *
+ * This is used to represent an axis, whether X or Y.
+ *
+ * @alias module:core/chart_axis
+ * @class
+ */
+ function Axis() {
+ // Please eslint no-empty-function.
+ }
+
+ /**
+ * Default axis position.
+ * @const {Null}
+ */
+ Axis.prototype.POS_DEFAULT = null;
+
+ /**
+ * Bottom axis position.
+ * @const {String}
+ */
+ Axis.prototype.POS_BOTTOM = 'bottom';
+
+ /**
+ * Left axis position.
+ * @const {String}
+ */
+ Axis.prototype.POS_LEFT = 'left';
+
+ /**
+ * Right axis position.
+ * @const {String}
+ */
+ Axis.prototype.POS_RIGHT = 'right';
+
+ /**
+ * Top axis position.
+ * @const {String}
+ */
+ Axis.prototype.POS_TOP = 'top';
+
+ /**
+ * Label of the axis.
+ * @type {String}
+ * @protected
+ */
+ Axis.prototype._label = null;
+
+ /**
+ * Labels of the ticks.
+ * @type {String[]}
+ * @protected
+ */
+ Axis.prototype._labels = null;
+
+ /**
+ * Maximum value of the axis.
+ * @type {Number}
+ * @protected
+ */
+ Axis.prototype._max = null;
+
+ /**
+ * Minimum value of the axis.
+ * @type {Number}
+ * @protected
+ */
+ Axis.prototype._min = null;
+
+ /**
+ * Position of the axis.
+ * @type {String}
+ * @protected
+ */
+ Axis.prototype._position = null;
+
+ /**
+ * Steps on the axis.
+ * @type {Number}
+ * @protected
+ */
+ Axis.prototype._stepSize = null;
+
+ /**
+ * Create a new instance of an axis from serialised data.
+ *
+ * @static
+ * @method create
+ * @param {Object} obj The data of the axis.
+ * @return {module:core/chart_axis}
+ */
+ Axis.prototype.create = function(obj) {
+ var s = new Axis();
+ s.setPosition(obj.position);
+ s.setLabel(obj.label);
+ s.setStepSize(obj.stepSize);
+ s.setMax(obj.max);
+ s.setMin(obj.min);
+ s.setLabels(obj.labels);
+ return s;
+ };
+
+ /**
+ * Get the label of the axis.
+ *
+ * @method getLabel
+ * @return {String}
+ */
+ Axis.prototype.getLabel = function() {
+ return this._label;
+ };
+
+ /**
+ * Get the labels of the ticks of the axis.
+ *
+ * @method getLabels
+ * @return {String[]}
+ */
+ Axis.prototype.getLabels = function() {
+ return this._labels;
+ };
+
+ /**
+ * Get the maximum value of the axis.
+ *
+ * @method getMax
+ * @return {Number}
+ */
+ Axis.prototype.getMax = function() {
+ return this._max;
+ };
+
+ /**
+ * Get the minimum value of the axis.
+ *
+ * @method getMin
+ * @return {Number}
+ */
+ Axis.prototype.getMin = function() {
+ return this._min;
+ };
+
+ /**
+ * Get the position of the axis.
+ *
+ * @method getPosition
+ * @return {String}
+ */
+ Axis.prototype.getPosition = function() {
+ return this._position;
+ };
+
+ /**
+ * Get the step size of the axis.
+ *
+ * @method getStepSize
+ * @return {Number}
+ */
+ Axis.prototype.getStepSize = function() {
+ return this._stepSize;
+ };
+
+ /**
+ * Set the label of the axis.
+ *
+ * @method setLabel
+ * @param {String} label The label.
+ */
+ Axis.prototype.setLabel = function(label) {
+ this._label = label || null;
+ };
+
+ /**
+ * Set the labels of the values on the axis.
+ *
+ * This automatically sets the [_stepSize]{@link module:core/chart_axis#_stepSize},
+ * [_min]{@link module:core/chart_axis#_min} and [_max]{@link module:core/chart_axis#_max}
+ * to define a scale from 0 to the number of labels when none of the previously
+ * mentioned values have been modified.
+ *
+ * You can use other values so long that your values in a series are mapped
+ * to the values represented by your _min, _max and _stepSize.
+ *
+ * @method setLabels
+ * @param {String[]} labels The labels.
+ */
+ Axis.prototype.setLabels = function(labels) {
+ this._labels = labels || null;
+
+ // By default we set the grid according to the labels.
+ if (this._labels !== null
+ && this._stepSize === null
+ && (this._min === null || this._min === 0)
+ && this._max === null) {
+ this.setStepSize(1);
+ this.setMin(0);
+ this.setMax(labels.length - 1);
+ }
+ };
+
+ /**
+ * Set the maximum value on the axis.
+ *
+ * When this is not set (or set to null) it is left for the output
+ * library to best guess what should be used.
+ *
+ * @method setMax
+ * @param {Number} max The value.
+ */
+ Axis.prototype.setMax = function(max) {
+ this._max = typeof max !== 'undefined' ? max : null;
+ };
+
+ /**
+ * Set the minimum value on the axis.
+ *
+ * When this is not set (or set to null) it is left for the output
+ * library to best guess what should be used.
+ *
+ * @method setMin
+ * @param {Number} min The value.
+ */
+ Axis.prototype.setMin = function(min) {
+ this._min = typeof min !== 'undefined' ? min : null;
+ };
+
+ /**
+ * Set the position of the axis.
+ *
+ * This does not validate whether or not the constant used is valid
+ * as the axis itself is not aware whether it represents the X or Y axis.
+ *
+ * The output library has to have a fallback in case the values are incorrect.
+ * When this is not set to {@link module:core/chart_axis#POS_DEFAULT} it is up
+ * to the output library to choose what position fits best.
+ *
+ * @method setPosition
+ * @param {String} position The value.
+ */
+ Axis.prototype.setPosition = function(position) {
+ if (position != this.POS_DEFAULT
+ && position != this.POS_BOTTOM
+ && position != this.POS_LEFT
+ && position != this.POS_RIGHT
+ && position != this.POS_TOP) {
+ throw new Error('Invalid axis position.');
+ }
+ this._position = position;
+ };
+
+ /**
+ * Set the stepSize on the axis.
+ *
+ * This is used to determine where ticks are displayed on the axis between min and max.
+ *
+ * @method setStepSize
+ * @param {Number} stepSize The value.
+ */
+ Axis.prototype.setStepSize = function(stepSize) {
+ if (typeof stepSize === 'undefined' || stepSize === null) {
+ stepSize = null;
+ } else if (isNaN(Number(stepSize))) {
+ throw new Error('Value for stepSize is not a number.');
+ } else {
+ stepSize = Number(stepSize);
+ }
+
+ this._stepSize = stepSize;
+ };
+
+ return Axis;
+
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Chart bar.
+ *
+ * @package core
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @module core/chart_bar
+ */
+define(['core/chart_base'], function(Base) {
+
+ /**
+ * Bar chart.
+ *
+ * @alias module:core/chart_bar
+ * @extends {module:core/chart_base}
+ * @class
+ */
+ function Bar() {
+ Base.prototype.constructor.apply(this, arguments);
+ }
+ Bar.prototype = Object.create(Base.prototype);
+
+ /**
+ * Whether the bars should be displayed horizontally or not.
+ *
+ * @type {Bool}
+ * @protected
+ */
+ Bar.prototype._horizontal = false;
+
+ /** @override */
+ Bar.prototype.TYPE = 'bar';
+
+ /** @override */
+ Bar.prototype.create = function(Klass, data) {
+ var chart = Base.prototype.create.apply(this, arguments);
+ chart.setHorizontal(data.horizontal);
+ return chart;
+ };
+
+ /** @override */
+ Bar.prototype._setDefaults = function() {
+ Base.prototype._setDefaults.apply(this, arguments);
+ var axis = this.getYAxis(0, true);
+ axis.setMin(0);
+ };
+
+ /**
+ * Get whether the bars should be displayed horizontally or not.
+ *
+ * @returns {Bool}
+ */
+ Bar.prototype.getHorizontal = function() {
+ return this._horizontal;
+ };
+
+ /**
+ * Set whether the bars should be displayed horizontally or not.
+ *
+ * It sets the X Axis to zero if the min value is null.
+ *
+ * @param {Bool} horizontal True if the bars should be displayed horizontally, false otherwise.
+ */
+ Bar.prototype.setHorizontal = function(horizontal) {
+ var axis = this.getXAxis(0, true);
+ if (axis.getMin() === null) {
+ axis.setMin(0);
+ }
+ this._horizontal = Boolean(horizontal);
+ };
+
+ return Bar;
+
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Chart base.
+ *
+ * @package core
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @module core/chart_base
+ */
+define(['core/chart_series', 'core/chart_axis'], function(Series, Axis) {
+
+ /**
+ * Chart base.
+ *
+ * The constructor of a chart must never take any argument.
+ *
+ * {@link module:core/chart_base#_setDefault} to set the defaults on instantiation.
+ *
+ * @alias module:core/chart_base
+ * @class
+ */
+ function Base() {
+ this._series = [];
+ this._labels = [];
+ this._xaxes = [];
+ this._yaxes = [];
+
+ this._setDefaults();
+ }
+
+ /**
+ * The series constituting this chart.
+ *
+ * @protected
+ * @type {module:core/chart_series[]}
+ */
+ Base.prototype._series = null;
+
+ /**
+ * The labels of the X axis when categorised.
+ *
+ * @protected
+ * @type {String[]}
+ */
+ Base.prototype._labels = null;
+
+ /**
+ * The title of the chart.
+ *
+ * @protected
+ * @type {String}
+ */
+ Base.prototype._title = null;
+
+ /**
+ * The X axes.
+ *
+ * @protected
+ * @type {module:core/chart_axis[]}
+ */
+ Base.prototype._xaxes = null;
+
+ /**
+ * The Y axes.
+ *
+ * @protected
+ * @type {module:core/chart_axis[]}
+ */
+ Base.prototype._yaxes = null;
+
+ /**
+ * Colours to pick from when automatically assigning them.
+ *
+ * @const
+ * @type {String[]}
+ */
+ Base.prototype.COLORSET = ['#f3c300', '#875692', '#f38400', '#a1caf1', '#be0032', '#c2b280', '#7f180d', '#008856',
+ '#e68fac', '#0067a5'];
+
+ /**
+ * The type of chart.
+ *
+ * @abstract
+ * @type {String}
+ * @const
+ */
+ Base.prototype.TYPE = null;
+
+ /**
+ * Add a series to the chart.
+ *
+ * This will automatically assign a color to the series if it does not have one.
+ *
+ * @param {module:core/chart_series} series The series to add.
+ */
+ Base.prototype.addSeries = function(series) {
+ this._validateSeries(series);
+ this._series.push(series);
+
+ // Give a default color from the set.
+ if (series.getColor() === null) {
+ series.setColor(Base.prototype.COLORSET[this._series.length % Base.prototype.COLORSET.length]);
+ }
+ };
+
+ /**
+ * Create a new instance of a chart from serialised data.
+ *
+ * the serialised attributes they offer and support.
+ *
+ * @static
+ * @method create
+ * @param {module:core/chart_base} Klass The class oject representing the type of chart to instantiate.
+ * @param {Object} data The data of the chart.
+ * @return {module:core/chart_base}
+ */
+ Base.prototype.create = function(Klass, data) {
+ // TODO Not convinced about the usage of Klass here but I can't figure out a way
+ // to have a reference to the class in the sub classes, in PHP I'd do new self().
+ var Chart = new Klass();
+
+ Chart.setLabels(data.labels);
+ Chart.setTitle(data.title);
+ data.series.forEach(function(seriesData) {
+ Chart.addSeries(Series.prototype.create(seriesData));
+ });
+ data.axes.x.forEach(function(axisData, i) {
+ Chart.setXAxis(Axis.prototype.create(axisData), i);
+ });
+ data.axes.y.forEach(function(axisData, i) {
+ Chart.setYAxis(Axis.prototype.create(axisData), i);
+ });
+ return Chart;
+ };
+
+ /**
+ * Get an axis.
+ *
+ * @private
+ * @param {String} xy Accepts the values 'x' or 'y'.
+ * @param {Number} [index=0] The index of the axis of its type.
+ * @param {Bool} [createIfNotExists=false] When true, create an instance if it does not exist.
+ * @return {module:core/chart_axis}
+ */
+ Base.prototype.__getAxis = function(xy, index, createIfNotExists) {
+ var axes = xy === 'x' ? this._xaxes : this._yaxes,
+ setAxis = (xy === 'x' ? this.setXAxis : this.setYAxis).bind(this),
+ axis;
+
+ index = typeof index === 'undefined' ? 0 : index;
+ createIfNotExists = typeof createIfNotExists === 'undefined' ? false : createIfNotExists;
+ axis = axes[index];
+
+ if (typeof axis === 'undefined') {
+ if (!createIfNotExists) {
+ throw new Error('Unknown axis.');
+ }
+ axis = new Axis();
+ setAxis(axis, index);
+ }
+
+ return axis;
+ };
+
+ /**
+ * Get the labels of the X axis.
+ *
+ * @return {String[]}
+ */
+ Base.prototype.getLabels = function() {
+ return this._labels;
+ };
+
+ /**
+ * Get the series.
+ *
+ * @return {module:core/chart_series[]}
+ */
+ Base.prototype.getSeries = function() {
+ return this._series;
+ };
+
+ /**
+ * Get the title of the chart.
+ *
+ * @return {String}
+ */
+ Base.prototype.getTitle = function() {
+ return this._title;
+ };
+
+ /**
+ * Get the type of chart.
+ *
+ * @see module:core/chart_base#TYPE
+ * @return {String}
+ */
+ Base.prototype.getType = function() {
+ if (!this.TYPE) {
+ throw new Error('The TYPE property has not been set.');
+ }
+ return this.TYPE;
+ };
+
+ /**
+ * Get the X axes.
+ *
+ * @return {module:core/chart_axis[]}
+ */
+ Base.prototype.getXAxes = function() {
+ return this._xaxes;
+ };
+
+ /**
+ * Get an X axis.
+ *
+ * @param {Number} [index=0] The index of the axis.
+ * @param {Bool} [createIfNotExists=false] Create the instance of it does not exist at index.
+ * @return {module:core/chart_axis}
+ */
+ Base.prototype.getXAxis = function(index, createIfNotExists) {
+ return this.__getAxis('x', index, createIfNotExists);
+ };
+
+ /**
+ * Get the Y axes.
+ *
+ * @return {module:core/chart_axis[]}
+ */
+ Base.prototype.getYAxes = function() {
+ return this._yaxes;
+ };
+
+ /**
+ * Get an Y axis.
+ *
+ * @param {Number} [index=0] The index of the axis.
+ * @param {Bool} [createIfNotExists=false] Create the instance of it does not exist at index.
+ * @return {module:core/chart_axis}
+ */
+ Base.prototype.getYAxis = function(index, createIfNotExists) {
+ return this.__getAxis('y', index, createIfNotExists);
+ };
+
+ /**
+ * Set the defaults for this chart type.
+ *
+ * Child classes can extend this to set defaults values on instantiation.
+ *
+ * emphasize and self-document the defaults values set by the chart type.
+ *
+ * @protected
+ */
+ Base.prototype._setDefaults = function() {
+ // For the children to extend.
+ };
+
+ /**
+ * Set the labels of the X axis.
+ *
+ * This requires for each series to contain strictly as many values as there
+ * are labels.
+ *
+ * @param {String[]} labels The labels.
+ */
+ Base.prototype.setLabels = function(labels) {
+ if (labels.length && this._series.length && this._series[0].length != labels.length) {
+ throw new Error('Series must match label values.');
+ }
+ this._labels = labels;
+ };
+
+ /**
+ * Set the title of the chart.
+ *
+ * @param {String} title The title.
+ */
+ Base.prototype.setTitle = function(title) {
+ this._title = title;
+ };
+
+ /**
+ * Set an X axis.
+ *
+ * Note that this will override any predefined axis without warning.
+ *
+ * @param {module:core/chart_axis} axis The axis.
+ * @param {Number} [index=0] The index of the axis.
+ */
+ Base.prototype.setXAxis = function(axis, index) {
+ index = typeof index === 'undefined' ? 0 : index;
+ this._validateAxis('x', axis, index);
+ this._xaxes[index] = axis;
+ };
+
+ /**
+ * Set a Y axis.
+ *
+ * Note that this will override any predefined axis without warning.
+ *
+ * @param {module:core/chart_axis} axis The axis.
+ * @param {Number} [index=0] The index of the axis.
+ */
+ Base.prototype.setYAxis = function(axis, index) {
+ index = typeof index === 'undefined' ? 0 : index;
+ this._validateAxis('y', axis, index);
+ this._yaxes[index] = axis;
+ };
+
+ /**
+ * Validate an axis.
+ *
+ * @protected
+ * @param {String} xy X or Y axis.
+ * @param {module:core/chart_axis} axis The axis to validate.
+ * @param {Number} [index=0] The index of the axis.
+ */
+ Base.prototype._validateAxis = function(xy, axis, index) {
+ index = typeof index === 'undefined' ? 0 : index;
+ if (index > 0) {
+ var axes = xy == 'x' ? this._xaxes : this._yaxes;
+ if (typeof axes[index - 1] === 'undefined') {
+ throw new Error('Missing ' + xy + ' axis at index lower than ' + index);
+ }
+ }
+ };
+
+ /**
+ * Validate a series.
+ *
+ * @protected
+ * @param {module:core/chart_series} series The series to validate.
+ */
+ Base.prototype._validateSeries = function(series) {
+ if (this._series.length && this._series[0].getCount() != series.getCount()) {
+ throw new Error('Series do not have an equal number of values.');
+
+ } else if (this._labels.length && this._labels.length != series.getCount()) {
+ throw new Error('Series must match label values.');
+ }
+ };
+
+ return Base;
+
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Chart builder.
+ *
+ * @package core
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery'], function($) {
+
+ /**
+ * Chart builder.
+ *
+ * @exports core/chart_builder
+ */
+ var module = {
+
+ /**
+ * Make a chart instance.
+ *
+ * This takes data, most likely generated in PHP, and creates a chart instance from it
+ * deferring most of the logic to {@link module:core/chart_base.create}.
+ *
+ * @param {Object} data The data.
+ * @return {Promise} A promise resolved with the chart instance.
+ */
+ make: function(data) {
+ var deferred = $.Deferred();
+ require(['core/chart_' + data.type], function(Klass) {
+ var instance = Klass.prototype.create(Klass, data);
+ deferred.resolve(instance);
+ });
+ return deferred.promise();
+ }
+ };
+
+ return module;
+
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Chart line.
+ *
+ * @package core
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @module core/chart_line
+ */
+define(['core/chart_base'], function(Base) {
+
+ /**
+ * Line chart.
+ *
+ * @alias module:core/chart_line
+ * @extends {module:core/chart_base}
+ * @class
+ */
+ function Line() {
+ Base.prototype.constructor.apply(this, arguments);
+ }
+ Line.prototype = Object.create(Base.prototype);
+
+ /** @override */
+ Line.prototype.TYPE = 'line';
+
+ /**
+ * Whether the line should be smooth or not.
+ *
+ * By default the chart lines are not smooth.
+ *
+ * @type {Bool}
+ * @protected
+ */
+ Line.prototype._smooth = false;
+
+ /** @override */
+ Line.prototype.create = function(Klass, data) {
+ var chart = Base.prototype.create.apply(this, arguments);
+ chart.setSmooth(data.smooth);
+ return chart;
+ };
+
+ /**
+ * Get whether the line should be smooth or not.
+ *
+ * @method getSmooth
+ * @returns {Bool}
+ */
+ Line.prototype.getSmooth = function() {
+ return this._smooth;
+ };
+
+ /**
+ * Set whether the line should be smooth or not.
+ *
+ * @method setSmooth
+ * @param {Bool} smooth True if the line chart should be smooth, false otherwise.
+ */
+ Line.prototype.setSmooth = function(smooth) {
+ this._smooth = Boolean(smooth);
+ };
+
+ return Line;
+
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Chart output.
+ *
+ * Proxy to the default output module.
+ *
+ * @package core
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['core/chart_output_chartjs'], function(Output) {
+
+ /**
+ * @exports module:core/chart_output
+ * @extends {module:core/chart_output_chartjs}
+ */
+ var defaultModule = Output;
+
+ return defaultModule;
+
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Chart output base.
+ *
+ * This takes a chart object and draws it.
+ *
+ * @package core
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @module core/chart_output_base
+ */
+define(['jquery'], function($) {
+
+ /**
+ * Chart output base.
+ *
+ * The constructor of an output class must instantly generate and display the
+ * chart. It is also the responsability of the output module to check that
+ * the node received is of the appropriate type, if not a new node can be
+ * added within.
+ *
+ * The output module has total control over the content of the node and can
+ * clear it or output anything to it at will. A node should not be shared by
+ * two simultaneous output modules.
+ *
+ * @class
+ * @alias module:core/chart_output_base
+ * @param {Node} node The node to output with/in.
+ * @param {Chart} chart A chart object.
+ */
+ function Base(node, chart) {
+ this._node = $(node);
+ this._chart = chart;
+ }
+
+ /**
+ * Update method.
+ *
+ * This is the public method through which an output instance in informed
+ * that the chart instance has been updated and they need to update the
+ * chart rendering.
+ *
+ * @abstract
+ * @return {Void}
+ */
+ Base.prototype.update = function() {
+ throw new Error('Not supported.');
+ };
+
+ return Base;
+
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Chart output for chart.js.
+ *
+ * @package core
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @module core/chart_output_chartjs
+ */
+define([
+ 'jquery',
+ 'core/chartjs',
+ 'core/chart_axis',
+ 'core/chart_bar',
+ 'core/chart_output_base',
+ 'core/chart_line',
+ 'core/chart_pie',
+ 'core/chart_series'
+], function($, Chartjs, Axis, Bar, Base, Line, Pie, Series) {
+
+ /**
+ * Makes an axis ID.
+ *
+ * @param {String} xy Accepts 'x' and 'y'.
+ * @param {Number} index The axis index.
+ * @return {String}
+ */
+ var makeAxisId = function(xy, index) {
+ return 'axis-' + xy + '-' + index;
+ };
+
+ /**
+ * Chart output for Chart.js.
+ *
+ * @class
+ * @alias module:core/chart_output_chartjs
+ * @extends {module:core/chart_output_base}
+ */
+ function Output() {
+ Base.prototype.constructor.apply(this, arguments);
+
+ // Make sure that we've got a canvas tag.
+ this._canvas = this._node;
+ if (this._canvas.prop('tagName') != 'CANVAS') {
+ this._canvas = $('<canvas>');
+ this._node.append(this._canvas);
+ }
+
+ this._build();
+ }
+ Output.prototype = Object.create(Base.prototype);
+
+ /**
+ * Reference to the chart config object.
+ *
+ * @type {Object}
+ * @protected
+ */
+ Output.prototype._config = null;
+
+ /**
+ * Reference to the instance of chart.js.
+ *
+ * @type {Object}
+ * @protected
+ */
+ Output.prototype._chartjs = null;
+
+ /**
+ * Reference to the canvas node.
+ *
+ * @type {Jquery}
+ * @protected
+ */
+ Output.prototype._canvas = null;
+
+ /**
+ * Builds the config and the chart.
+ *
+ * @protected
+ */
+ Output.prototype._build = function() {
+ this._config = this._makeConfig();
+ this._chartjs = new Chartjs(this._canvas[0], this._config);
+ };
+
+ /**
+ * Get the chart type.
+ *
+ * It also handles the bar charts positioning, deciding if the bars should be displayed horizontally.
+ * Otherwise, get the chart TYPE value.
+ *
+ * @returns {String} the chart type.
+ * @protected
+ */
+ Output.prototype._getChartType = function() {
+ var type = this._chart.getType();
+
+ // Bars can be displayed vertically and horizontally, defining horizontalBar type.
+ if (this._chart.getType() === Bar.prototype.TYPE && this._chart.getHorizontal() === true) {
+ type = 'horizontalBar';
+ }
+
+ return type;
+ };
+
+ /**
+ * Make the axis config.
+ *
+ * @protected
+ * @param {module:core/chart_axis} axis The axis.
+ * @param {String} xy Accepts 'x' or 'y'.
+ * @param {Number} index The axis index.
+ * @return {Object} The axis config.
+ */
+ Output.prototype._makeAxisConfig = function(axis, xy, index) {
+ var scaleData = {
+ id: makeAxisId(xy, index)
+ };
+
+ if (axis.getPosition() !== Axis.prototype.POS_DEFAULT) {
+ scaleData.position = axis.getPosition();
+ }
+
+ if (axis.getLabel() !== null) {
+ scaleData.scaleLabel = {
+ display: true,
+ labelString: axis.getLabel()
+ };
+ }
+
+ if (axis.getStepSize() !== null) {
+ scaleData.ticks = scaleData.ticks || {};
+ scaleData.ticks.stepSize = axis.getStepSize();
+ }
+
+ if (axis.getMax() !== null) {
+ scaleData.ticks = scaleData.ticks || {};
+ scaleData.ticks.max = axis.getMax();
+ }
+
+ if (axis.getMin() !== null) {
+ scaleData.ticks = scaleData.ticks || {};
+ scaleData.ticks.min = axis.getMin();
+ }
+
+ return scaleData;
+ };
+
+ /**
+ * Make the config config.
+ *
+ * @protected
+ * @param {module:core/chart_axis} axis The axis.
+ * @return {Object} The axis config.
+ */
+ Output.prototype._makeConfig = function() {
+ var config = {
+ type: this._getChartType(),
+ data: {
+ labels: this._chart.getLabels(),
+ datasets: this._makeDatasetsConfig()
+ },
+ options: {
+ title: {
+ display: this._chart.getTitle() !== null,
+ text: this._chart.getTitle()
+ }
+ }
+ };
+
+ this._chart.getXAxes().forEach(function(axis, i) {
+ var axisLabels = axis.getLabels();
+
+ config.options.scales = config.options.scales || {};
+ config.options.scales.xAxes = config.options.scales.xAxes || [];
+ config.options.scales.xAxes[i] = this._makeAxisConfig(axis, 'x', i);
+
+ if (axisLabels !== null) {
+ config.options.scales.xAxes[i].ticks.callback = function(value, index) {
+ return axisLabels[index] || '';
+ };
+ }
+ }.bind(this));
+
+ this._chart.getYAxes().forEach(function(axis, i) {
+ var axisLabels = axis.getLabels();
+
+ config.options.scales = config.options.scales || {};
+ config.options.scales.yAxes = config.options.scales.yAxes || [];
+ config.options.scales.yAxes[i] = this._makeAxisConfig(axis, 'y', i);
+
+ if (axisLabels !== null) {
+ config.options.scales.yAxes[i].ticks.callback = function(value) {
+ return axisLabels[parseInt(value, 10)] || '';
+ };
+ }
+ }.bind(this));
+
+ config.options.tooltips = {
+ callbacks: {
+ label: this._makeTooltip.bind(this)
+ }
+ };
+
+ return config;
+ };
+
+ /**
+ * Get the datasets configurations.
+ *
+ * @protected
+ * @return {Object[]}
+ */
+ Output.prototype._makeDatasetsConfig = function() {
+ var sets = this._chart.getSeries().map(function(series) {
+ var colors = series.hasColoredValues() ? series.getColors() : series.getColor();
+ var dataset = {
+ label: series.getLabel(),
+ data: series.getValues(),
+ type: series.getType(),
+ fill: false,
+ backgroundColor: colors,
+ // Pie charts look better without borders.
+ borderColor: this._chart.getType() == Pie.prototype.TYPE ? null : colors,
+ lineTension: this._isSmooth(series) ? 0.3 : 0
+ };
+
+ if (series.getXAxis() !== null) {
+ dataset.xAxisID = makeAxisId('x', series.getXAxis());
+ }
+ if (series.getYAxis() !== null) {
+ dataset.yAxisID = makeAxisId('y', series.getYAxis());
+ }
+
+ return dataset;
+ }.bind(this));
+ return sets;
+ };
+
+ /**
+ * Get the chart data, add labels and rebuild the tooltip.
+ *
+ * @param {Object[]} tooltipItem The tooltip item data.
+ * @param {Object[]} data The chart data.
+ * @returns {String}
+ * @protected
+ */
+ Output.prototype._makeTooltip = function(tooltipItem, data) {
+
+ // Get series and chart data to rebuild the tooltip and add labels.
+ var series = this._chart.getSeries()[tooltipItem.datasetIndex];
+ var serieLabel = series.getLabel();
+ var serieLabels = series.getLabels();
+ var chartData = data.datasets[tooltipItem.datasetIndex].data;
+ var tooltipData = chartData[tooltipItem.index];
+
+ // Build default tooltip.
+ var tooltip = serieLabel + ': ' + tooltipData;
+
+ // Add serie labels to the tooltip if any.
+ if (serieLabels !== null) {
+ tooltip += ' ' + serieLabels[tooltipItem.index];
+ }
+
+ return tooltip;
+ };
+
+ /**
+ * Verify if the chart line is smooth or not.
+ *
+ * @protected
+ * @param {module:core/chart_series} series The series.
+ * @returns {Bool}
+ */
+ Output.prototype._isSmooth = function(series) {
+ var smooth = false;
+ if (this._chart.getType() === Line.prototype.TYPE) {
+ smooth = series.getSmooth();
+ if (smooth === null) {
+ smooth = this._chart.getSmooth();
+ }
+ } else if (series.getType() === Series.prototype.TYPE_LINE) {
+ smooth = series.getSmooth();
+ }
+
+ return smooth;
+ };
+
+ /** @override */
+ Output.prototype.update = function() {
+ $.extend(true, this._config, this._makeConfig());
+ this._chartjs.update();
+ };
+
+ return Output;
+
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Chart output for HTML table.
+ *
+ * @package core
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @module core/chart_output_htmltable
+ */
+define([
+ 'jquery',
+ 'core/chart_output_base',
+], function($, Base) {
+
+ /**
+ * Render a chart as an HTML table.
+ *
+ * @class
+ * @extends {module:core/chart_output_base}
+ * @alias module:core/chart_output_htmltable
+ */
+ function Output() {
+ Base.prototype.constructor.apply(this, arguments);
+ this._build();
+ }
+ Output.prototype = Object.create(Base.prototype);
+
+ /**
+ * Attach the table to the document.
+ *
+ * @protected
+ */
+ Output.prototype._build = function() {
+ this._node.empty();
+ this._node.append(this._makeTable());
+ };
+
+ /**
+ * Builds the table node.
+ *
+ * @protected
+ * @return {Jquery}
+ */
+ Output.prototype._makeTable = function() {
+ var tbl = $('<table>'),
+ c = this._chart,
+ node,
+ value,
+ labels = c.getLabels(),
+ hasLabel = labels.length > 0,
+ series = c.getSeries(),
+ seriesLabels,
+ rowCount = series[0].getCount();
+
+ // Identify the table.
+ tbl.addClass('chart-output-htmltable');
+
+ // Set the caption.
+ if (c.getTitle() !== null) {
+ tbl.append($('<caption>').text(c.getTitle()));
+ }
+
+ // Write the column headers.
+ node = $('<tr>');
+ if (hasLabel) {
+ node.append($('<td>'));
+ }
+ series.forEach(function(serie) {
+ node.append(
+ $('<th>')
+ .text(serie.getLabel())
+ .attr('scope', 'col')
+ );
+ });
+ tbl.append(node);
+
+ // Write rows.
+ for (var rowId = 0; rowId < rowCount; rowId++) {
+ node = $('<tr>');
+ if (labels.length > 0) {
+ node.append(
+ $('<th>')
+ .text(labels[rowId])
+ .attr('scope', 'row')
+ );
+ }
+ for (var serieId = 0; serieId < series.length; serieId++) {
+ value = series[serieId].getValues()[rowId];
+ seriesLabels = series[serieId].getLabels();
+ if (seriesLabels !== null) {
+ value += ' ' + series[serieId].getLabels()[rowId];
+ }
+ node.append($('<td>').text(value));
+ }
+ tbl.append(node);
+ }
+
+ return tbl;
+ };
+
+ /** @override */
+ Output.prototype.update = function() {
+ this._build();
+ };
+
+ return Output;
+
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Chart pie.
+ *
+ * @package core
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @module core/chart_pie
+ */
+define(['core/chart_base'], function(Base) {
+
+ /**
+ * Pie chart.
+ *
+ * @class
+ * @alias module:core/chart_pie
+ * @extends {module:core/chart_base}
+ */
+ function Pie() {
+ Base.prototype.constructor.apply(this, arguments);
+ }
+ Pie.prototype = Object.create(Base.prototype);
+
+ /** @override */
+ Pie.prototype.TYPE = 'pie';
+
+ /**
+ * Overridden to add appropriate colors to the series.
+ *
+ * @override
+ */
+ Pie.prototype.addSeries = function(series) {
+ if (series.getColor() === null) {
+ var colors = [];
+ for (var i = 0; i < series.getCount(); i++) {
+ colors.push(this.COLORSET[i % Base.prototype.COLORSET.length]);
+ }
+ series.setColors(colors);
+ }
+ return Base.prototype.addSeries.apply(this, arguments);
+ };
+
+ /**
+ * Validate a series.
+ *
+ * Overrides parent implementation to validate that there is only
+ * one series per chart instance.
+ *
+ * @override
+ */
+ Pie.prototype._validateSeries = function() {
+ if (this._series.length >= 1) {
+ throw new Error('Pie charts only support one serie.');
+ }
+ return Base.prototype._validateSeries.apply(this, arguments);
+ };
+
+ return Pie;
+
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Chart series.
+ *
+ * @package core
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @module core/chart_series
+ */
+define([], function() {
+
+ /**
+ * Chart data series.
+ *
+ * @class
+ * @alias module:core/chart_series
+ * @param {String} label The series label.
+ * @param {Number[]} values The values.
+ */
+ function Series(label, values) {
+ if (typeof label !== 'string') {
+ throw new Error('Invalid label for series.');
+
+ } else if (typeof values !== 'object') {
+ throw new Error('Values for a series must be an array.');
+
+ } else if (values.length < 1) {
+ throw new Error('Invalid values received for series.');
+ }
+
+ this._colors = [];
+ this._label = label;
+ this._values = values;
+ }
+
+ /**
+ * The default type of series.
+ *
+ * @type {Null}
+ * @const
+ */
+ Series.prototype.TYPE_DEFAULT = null;
+
+ /**
+ * Type of series 'line'.
+ *
+ * @type {String}
+ * @const
+ */
+ Series.prototype.TYPE_LINE = 'line';
+
+ /**
+ * The colors of the series.
+ *
+ * @type {String[]}
+ * @protected
+ */
+ Series.prototype._colors = null;
+
+ /**
+ * The label of the series.
+ *
+ * @type {String}
+ * @protected
+ */
+ Series.prototype._label = null;
+
+ /**
+ * The labels for the values of the series.
+ *
+ * @type {String[]}
+ * @protected
+ */
+ Series.prototype._labels = null;
+
+ /**
+ * Whether the line of the serie should be smooth or not.
+ *
+ * @type {Bool}
+ * @protected
+ */
+ Series.prototype._smooth = false;
+
+ /**
+ * The type of the series.
+ *
+ * @type {String}
+ * @protected
+ */
+ Series.prototype._type = Series.prototype.TYPE_DEFAULT;
+
+ /**
+ * The values in the series.
+ *
+ * @type {Number[]}
+ * @protected
+ */
+ Series.prototype._values = null;
+
+ /**
+ * The index of the X axis.
+ *
+ * @type {Number[]}
+ * @protected
+ */
+ Series.prototype._xaxis = null;
+
+ /**
+ * The index of the Y axis.
+ *
+ * @type {Number[]}
+ * @protected
+ */
+ Series.prototype._yaxis = null;
+
+ /**
+ * Create a new instance of a series from serialised data.
+ *
+ * @static
+ * @method create
+ * @param {Object} obj The data of the series.
+ * @return {module:core/chart_series}
+ */
+ Series.prototype.create = function(obj) {
+ var s = new Series(obj.label, obj.values);
+ s.setType(obj.type);
+ s.setXAxis(obj.axes.x);
+ s.setYAxis(obj.axes.y);
+ s.setLabels(obj.labels);
+
+ // Colors are exported as an array with 1, or n values.
+ if (obj.colors && obj.colors.length > 1) {
+ s.setColors(obj.colors);
+ } else {
+ s.setColor(obj.colors[0]);
+ }
+
+ s.setSmooth(obj.smooth);
+ return s;
+ };
+
+ /**
+ * Get the color.
+ *
+ * @return {String}
+ */
+ Series.prototype.getColor = function() {
+ return this._colors[0] || null;
+ };
+
+ /**
+ * Get the colors for each value in the series.
+ *
+ * @return {String[]}
+ */
+ Series.prototype.getColors = function() {
+ return this._colors;
+ };
+
+ /**
+ * Get the number of values in the series.
+ *
+ * @return {Number}
+ */
+ Series.prototype.getCount = function() {
+ return this._values.length;
+ };
+
+ /**
+ * Get the series label.
+ *
+ * @return {String}
+ */
+ Series.prototype.getLabel = function() {
+ return this._label;
+ };
+
+ /**
+ * Get labels for the values of the series.
+ *
+ * @return {String[]}
+ */
+ Series.prototype.getLabels = function() {
+ return this._labels;
+ };
+
+ /**
+ * Get whether the line of the serie should be smooth or not.
+ *
+ * @returns {Bool}
+ */
+ Series.prototype.getSmooth = function() {
+ return this._smooth;
+ };
+
+ /**
+ * Get the series type.
+ *
+ * @return {String}
+ */
+ Series.prototype.getType = function() {
+ return this._type;
+ };
+
+ /**
+ * Get the series values.
+ *
+ * @return {Number[]}
+ */
+ Series.prototype.getValues = function() {
+ return this._values;
+ };
+
+ /**
+ * Get the index of the X axis.
+ *
+ * @return {Number}
+ */
+ Series.prototype.getXAxis = function() {
+ return this._xaxis;
+ };
+
+ /**
+ * Get the index of the Y axis.
+ *
+ * @return {Number}
+ */
+ Series.prototype.getYAxis = function() {
+ return this._yaxis;
+ };
+
+ /**
+ * Whether there is a color per value.
+ *
+ * @return {Bool}
+ */
+ Series.prototype.hasColoredValues = function() {
+ return this._colors.length == this.getCount();
+ };
+
+ /**
+ * Set the series color.
+ *
+ * @param {String} color A CSS-compatible color.
+ */
+ Series.prototype.setColor = function(color) {
+ this._colors = [color];
+ };
+
+ /**
+ * Set a color for each value in the series.
+ *
+ * @param {String[]} colors CSS-compatible colors.
+ */
+ Series.prototype.setColors = function(colors) {
+ if (colors && colors.length != this.getCount()) {
+ throw new Error('When setting multiple colors there must be one per value.');
+ }
+ this._colors = colors || [];
+ };
+
+ /**
+ * Set the labels for the values of the series.
+ *
+ * @param {String[]} labels the labels of the series values.
+ */
+ Series.prototype.setLabels = function(labels) {
+ this._validateLabels(labels);
+ labels = typeof labels === 'undefined' ? null : labels;
+ this._labels = labels;
+ };
+
+ /**
+ * Set Whether the line of the serie should be smooth or not.
+ *
+ * Only applicable for line chart or a line series, if null it assumes the chart default (not smooth).
+ *
+ * @param {Bool} smooth True if the lines should be smooth, false for tensioned lines.
+ */
+ Series.prototype.setSmooth = function(smooth) {
+ smooth = typeof smooth === 'undefined' ? null : smooth;
+ this._smooth = smooth;
+ };
+
+ /**
+ * Set the type of the series.
+ *
+ * @param {String} type A type constant value.
+ */
+ Series.prototype.setType = function(type) {
+ if (type != this.TYPE_DEFAULT && type != this.TYPE_LINE) {
+ throw new Error('Invalid serie type.');
+ }
+ this._type = type || null;
+ };
+
+ /**
+ * Set the index of the X axis.
+ *
+ * @param {Number} index The index.
+ */
+ Series.prototype.setXAxis = function(index) {
+ this._xaxis = index || null;
+ };
+
+
+ /**
+ * Set the index of the Y axis.
+ *
+ * @param {Number} index The index.
+ */
+ Series.prototype.setYAxis = function(index) {
+ this._yaxis = index || null;
+ };
+
+ /**
+ * Validate series labels.
+ *
+ * @protected
+ * @param {String[]} labels The labels of the serie.
+ */
+ Series.prototype._validateLabels = function(labels) {
+ if (labels && labels.length > 0 && labels.length != this.getCount()) {
+ throw new Error('Series labels must match series values.');
+ }
+ };
+
+ return Series;
+
+});
--- /dev/null
+/*!\r
+ * Chart.js\r
+ * http://chartjs.org/\r
+ * Version: 2.1.6\r
+ *\r
+ * Copyright 2016 Nick Downie\r
+ * Released under the MIT license\r
+ * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\r
+ */\r
+\r
+/**\r
+ * Description of import into Moodle:\r
+ *\r
+ * - Download from http://www.chartjs.org/docs/#getting-started-download-chart-js.\r
+ * - Copy Chart.js to lib/amd/src/chartjs.js.\r
+ * - Add these instructions to the file.\r
+ * - Add the jshint ignore rules.\r
+ */\r
+\r
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Chart = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){\r
+\r
+},{}],2:[function(require,module,exports){\r
+/* MIT license */\r
+var colorNames = require(6);\r
+\r
+module.exports = {\r
+ getRgba: getRgba,\r
+ getHsla: getHsla,\r
+ getRgb: getRgb,\r
+ getHsl: getHsl,\r
+ getHwb: getHwb,\r
+ getAlpha: getAlpha,\r
+\r
+ hexString: hexString,\r
+ rgbString: rgbString,\r
+ rgbaString: rgbaString,\r
+ percentString: percentString,\r
+ percentaString: percentaString,\r
+ hslString: hslString,\r
+ hslaString: hslaString,\r
+ hwbString: hwbString,\r
+ keyword: keyword\r
+}\r
+\r
+function getRgba(string) {\r
+ if (!string) {\r
+ return;\r
+ }\r
+ var abbr = /^#([a-fA-F0-9]{3})$/,\r
+ hex = /^#([a-fA-F0-9]{6})$/,\r
+ rgba = /^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/,\r
+ per = /^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/,\r
+ keyword = /(\w+)/;\r
+\r
+ var rgb = [0, 0, 0],\r
+ a = 1,\r
+ match = string.match(abbr);\r
+ if (match) {\r
+ match = match[1];\r
+ for (var i = 0; i < rgb.length; i++) {\r
+ rgb[i] = parseInt(match[i] + match[i], 16);\r
+ }\r
+ }\r
+ else if (match = string.match(hex)) {\r
+ match = match[1];\r
+ for (var i = 0; i < rgb.length; i++) {\r
+ rgb[i] = parseInt(match.slice(i * 2, i * 2 + 2), 16);\r
+ }\r
+ }\r
+ else if (match = string.match(rgba)) {\r
+ for (var i = 0; i < rgb.length; i++) {\r
+ rgb[i] = parseInt(match[i + 1]);\r
+ }\r
+ a = parseFloat(match[4]);\r
+ }\r
+ else if (match = string.match(per)) {\r
+ for (var i = 0; i < rgb.length; i++) {\r
+ rgb[i] = Math.round(parseFloat(match[i + 1]) * 2.55);\r
+ }\r
+ a = parseFloat(match[4]);\r
+ }\r
+ else if (match = string.match(keyword)) {\r
+ if (match[1] == "transparent") {\r
+ return [0, 0, 0, 0];\r
+ }\r
+ rgb = colorNames[match[1]];\r
+ if (!rgb) {\r
+ return;\r
+ }\r
+ }\r
+\r
+ for (var i = 0; i < rgb.length; i++) {\r
+ rgb[i] = scale(rgb[i], 0, 255);\r
+ }\r
+ if (!a && a != 0) {\r
+ a = 1;\r
+ }\r
+ else {\r
+ a = scale(a, 0, 1);\r
+ }\r
+ rgb[3] = a;\r
+ return rgb;\r
+}\r
+\r
+function getHsla(string) {\r
+ if (!string) {\r
+ return;\r
+ }\r
+ var hsl = /^hsla?\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/;\r
+ var match = string.match(hsl);\r
+ if (match) {\r
+ var alpha = parseFloat(match[4]);\r
+ var h = scale(parseInt(match[1]), 0, 360),\r
+ s = scale(parseFloat(match[2]), 0, 100),\r
+ l = scale(parseFloat(match[3]), 0, 100),\r
+ a = scale(isNaN(alpha) ? 1 : alpha, 0, 1);\r
+ return [h, s, l, a];\r
+ }\r
+}\r
+\r
+function getHwb(string) {\r
+ if (!string) {\r
+ return;\r
+ }\r
+ var hwb = /^hwb\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/;\r
+ var match = string.match(hwb);\r
+ if (match) {\r
+ var alpha = parseFloat(match[4]);\r
+ var h = scale(parseInt(match[1]), 0, 360),\r
+ w = scale(parseFloat(match[2]), 0, 100),\r
+ b = scale(parseFloat(match[3]), 0, 100),\r
+ a = scale(isNaN(alpha) ? 1 : alpha, 0, 1);\r
+ return [h, w, b, a];\r
+ }\r
+}\r
+\r
+function getRgb(string) {\r
+ var rgba = getRgba(string);\r
+ return rgba && rgba.slice(0, 3);\r
+}\r
+\r
+function getHsl(string) {\r
+ var hsla = getHsla(string);\r
+ return hsla && hsla.slice(0, 3);\r
+}\r
+\r
+function getAlpha(string) {\r
+ var vals = getRgba(string);\r
+ if (vals) {\r
+ return vals[3];\r
+ }\r
+ else if (vals = getHsla(string)) {\r
+ return vals[3];\r
+ }\r
+ else if (vals = getHwb(string)) {\r
+ return vals[3];\r
+ }\r
+}\r
+\r
+// generators\r
+function hexString(rgb) {\r
+ return "#" + hexDouble(rgb[0]) + hexDouble(rgb[1])\r
+ + hexDouble(rgb[2]);\r
+}\r
+\r
+function rgbString(rgba, alpha) {\r
+ if (alpha < 1 || (rgba[3] && rgba[3] < 1)) {\r
+ return rgbaString(rgba, alpha);\r
+ }\r
+ return "rgb(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ")";\r
+}\r
+\r
+function rgbaString(rgba, alpha) {\r
+ if (alpha === undefined) {\r
+ alpha = (rgba[3] !== undefined ? rgba[3] : 1);\r
+ }\r
+ return "rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2]\r
+ + ", " + alpha + ")";\r
+}\r
+\r
+function percentString(rgba, alpha) {\r
+ if (alpha < 1 || (rgba[3] && rgba[3] < 1)) {\r
+ return percentaString(rgba, alpha);\r
+ }\r
+ var r = Math.round(rgba[0]/255 * 100),\r
+ g = Math.round(rgba[1]/255 * 100),\r
+ b = Math.round(rgba[2]/255 * 100);\r
+\r
+ return "rgb(" + r + "%, " + g + "%, " + b + "%)";\r
+}\r
+\r
+function percentaString(rgba, alpha) {\r
+ var r = Math.round(rgba[0]/255 * 100),\r
+ g = Math.round(rgba[1]/255 * 100),\r
+ b = Math.round(rgba[2]/255 * 100);\r
+ return "rgba(" + r + "%, " + g + "%, " + b + "%, " + (alpha || rgba[3] || 1) + ")";\r
+}\r
+\r
+function hslString(hsla, alpha) {\r
+ if (alpha < 1 || (hsla[3] && hsla[3] < 1)) {\r
+ return hslaString(hsla, alpha);\r
+ }\r
+ return "hsl(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%)";\r
+}\r
+\r
+function hslaString(hsla, alpha) {\r
+ if (alpha === undefined) {\r
+ alpha = (hsla[3] !== undefined ? hsla[3] : 1);\r
+ }\r
+ return "hsla(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%, "\r
+ + alpha + ")";\r
+}\r
+\r
+// hwb is a bit different than rgb(a) & hsl(a) since there is no alpha specific syntax\r
+// (hwb have alpha optional & 1 is default value)\r
+function hwbString(hwb, alpha) {\r
+ if (alpha === undefined) {\r
+ alpha = (hwb[3] !== undefined ? hwb[3] : 1);\r
+ }\r
+ return "hwb(" + hwb[0] + ", " + hwb[1] + "%, " + hwb[2] + "%"\r
+ + (alpha !== undefined && alpha !== 1 ? ", " + alpha : "") + ")";\r
+}\r
+\r
+function keyword(rgb) {\r
+ return reverseNames[rgb.slice(0, 3)];\r
+}\r
+\r
+// helpers\r
+function scale(num, min, max) {\r
+ return Math.min(Math.max(min, num), max);\r
+}\r
+\r
+function hexDouble(num) {\r
+ var str = num.toString(16).toUpperCase();\r
+ return (str.length < 2) ? "0" + str : str;\r
+}\r
+\r
+\r
+//create a list of reverse color names\r
+var reverseNames = {};\r
+for (var name in colorNames) {\r
+ reverseNames[colorNames[name]] = name;\r
+}\r
+\r
+},{"6":6}],3:[function(require,module,exports){\r
+/* MIT license */\r
+var convert = require(5);\r
+var string = require(2);\r
+\r
+var Color = function (obj) {\r
+ if (obj instanceof Color) {\r
+ return obj;\r
+ }\r
+ if (!(this instanceof Color)) {\r
+ return new Color(obj);\r
+ }\r
+\r
+ this.values = {\r
+ rgb: [0, 0, 0],\r
+ hsl: [0, 0, 0],\r
+ hsv: [0, 0, 0],\r
+ hwb: [0, 0, 0],\r
+ cmyk: [0, 0, 0, 0],\r
+ alpha: 1\r
+ };\r
+\r
+ // parse Color() argument\r
+ var vals;\r
+ if (typeof obj === 'string') {\r
+ vals = string.getRgba(obj);\r
+ if (vals) {\r
+ this.setValues('rgb', vals);\r
+ } else if (vals = string.getHsla(obj)) {\r
+ this.setValues('hsl', vals);\r
+ } else if (vals = string.getHwb(obj)) {\r
+ this.setValues('hwb', vals);\r
+ } else {\r
+ throw new Error('Unable to parse color from string "' + obj + '"');\r
+ }\r
+ } else if (typeof obj === 'object') {\r
+ vals = obj;\r
+ if (vals.r !== undefined || vals.red !== undefined) {\r
+ this.setValues('rgb', vals);\r
+ } else if (vals.l !== undefined || vals.lightness !== undefined) {\r
+ this.setValues('hsl', vals);\r
+ } else if (vals.v !== undefined || vals.value !== undefined) {\r
+ this.setValues('hsv', vals);\r
+ } else if (vals.w !== undefined || vals.whiteness !== undefined) {\r
+ this.setValues('hwb', vals);\r
+ } else if (vals.c !== undefined || vals.cyan !== undefined) {\r
+ this.setValues('cmyk', vals);\r
+ } else {\r
+ throw new Error('Unable to parse color from object ' + JSON.stringify(obj));\r
+ }\r
+ }\r
+};\r
+\r
+Color.prototype = {\r
+ rgb: function () {\r
+ return this.setSpace('rgb', arguments);\r
+ },\r
+ hsl: function () {\r
+ return this.setSpace('hsl', arguments);\r
+ },\r
+ hsv: function () {\r
+ return this.setSpace('hsv', arguments);\r
+ },\r
+ hwb: function () {\r
+ return this.setSpace('hwb', arguments);\r
+ },\r
+ cmyk: function () {\r
+ return this.setSpace('cmyk', arguments);\r
+ },\r
+\r
+ rgbArray: function () {\r
+ return this.values.rgb;\r
+ },\r
+ hslArray: function () {\r
+ return this.values.hsl;\r
+ },\r
+ hsvArray: function () {\r
+ return this.values.hsv;\r
+ },\r
+ hwbArray: function () {\r
+ var values = this.values;\r
+ if (values.alpha !== 1) {\r
+ return values.hwb.concat([values.alpha]);\r
+ }\r
+ return values.hwb;\r
+ },\r
+ cmykArray: function () {\r
+ return this.values.cmyk;\r
+ },\r
+ rgbaArray: function () {\r
+ var values = this.values;\r
+ return values.rgb.concat([values.alpha]);\r
+ },\r
+ hslaArray: function () {\r
+ var values = this.values;\r
+ return values.hsl.concat([values.alpha]);\r
+ },\r
+ alpha: function (val) {\r
+ if (val === undefined) {\r
+ return this.values.alpha;\r
+ }\r
+ this.setValues('alpha', val);\r
+ return this;\r
+ },\r
+\r
+ red: function (val) {\r
+ return this.setChannel('rgb', 0, val);\r
+ },\r
+ green: function (val) {\r
+ return this.setChannel('rgb', 1, val);\r
+ },\r
+ blue: function (val) {\r
+ return this.setChannel('rgb', 2, val);\r
+ },\r
+ hue: function (val) {\r
+ if (val) {\r
+ val %= 360;\r
+ val = val < 0 ? 360 + val : val;\r
+ }\r
+ return this.setChannel('hsl', 0, val);\r
+ },\r
+ saturation: function (val) {\r
+ return this.setChannel('hsl', 1, val);\r
+ },\r
+ lightness: function (val) {\r
+ return this.setChannel('hsl', 2, val);\r
+ },\r
+ saturationv: function (val) {\r
+ return this.setChannel('hsv', 1, val);\r
+ },\r
+ whiteness: function (val) {\r
+ return this.setChannel('hwb', 1, val);\r
+ },\r
+ blackness: function (val) {\r
+ return this.setChannel('hwb', 2, val);\r
+ },\r
+ value: function (val) {\r
+ return this.setChannel('hsv', 2, val);\r
+ },\r
+ cyan: function (val) {\r
+ return this.setChannel('cmyk', 0, val);\r
+ },\r
+ magenta: function (val) {\r
+ return this.setChannel('cmyk', 1, val);\r
+ },\r
+ yellow: function (val) {\r
+ return this.setChannel('cmyk', 2, val);\r
+ },\r
+ black: function (val) {\r
+ return this.setChannel('cmyk', 3, val);\r
+ },\r
+\r
+ hexString: function () {\r
+ return string.hexString(this.values.rgb);\r
+ },\r
+ rgbString: function () {\r
+ return string.rgbString(this.values.rgb, this.values.alpha);\r
+ },\r
+ rgbaString: function () {\r
+ return string.rgbaString(this.values.rgb, this.values.alpha);\r
+ },\r
+ percentString: function () {\r
+ return string.percentString(this.values.rgb, this.values.alpha);\r
+ },\r
+ hslString: function () {\r
+ return string.hslString(this.values.hsl, this.values.alpha);\r
+ },\r
+ hslaString: function () {\r
+ return string.hslaString(this.values.hsl, this.values.alpha);\r
+ },\r
+ hwbString: function () {\r
+ return string.hwbString(this.values.hwb, this.values.alpha);\r
+ },\r
+ keyword: function () {\r
+ return string.keyword(this.values.rgb, this.values.alpha);\r
+ },\r
+\r
+ rgbNumber: function () {\r
+ var rgb = this.values.rgb;\r
+ return (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];\r
+ },\r
+\r
+ luminosity: function () {\r
+ // http://www.w3.org/TR/WCAG20/#relativeluminancedef\r
+ var rgb = this.values.rgb;\r
+ var lum = [];\r
+ for (var i = 0; i < rgb.length; i++) {\r
+ var chan = rgb[i] / 255;\r
+ lum[i] = (chan <= 0.03928) ? chan / 12.92 : Math.pow(((chan + 0.055) / 1.055), 2.4);\r
+ }\r
+ return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];\r
+ },\r
+\r
+ contrast: function (color2) {\r
+ // http://www.w3.org/TR/WCAG20/#contrast-ratiodef\r
+ var lum1 = this.luminosity();\r
+ var lum2 = color2.luminosity();\r
+ if (lum1 > lum2) {\r
+ return (lum1 + 0.05) / (lum2 + 0.05);\r
+ }\r
+ return (lum2 + 0.05) / (lum1 + 0.05);\r
+ },\r
+\r
+ level: function (color2) {\r
+ var contrastRatio = this.contrast(color2);\r
+ if (contrastRatio >= 7.1) {\r
+ return 'AAA';\r
+ }\r
+\r
+ return (contrastRatio >= 4.5) ? 'AA' : '';\r
+ },\r
+\r
+ dark: function () {\r
+ // YIQ equation from http://24ways.org/2010/calculating-color-contrast\r
+ var rgb = this.values.rgb;\r
+ var yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;\r
+ return yiq < 128;\r
+ },\r
+\r
+ light: function () {\r
+ return !this.dark();\r
+ },\r
+\r
+ negate: function () {\r
+ var rgb = [];\r
+ for (var i = 0; i < 3; i++) {\r
+ rgb[i] = 255 - this.values.rgb[i];\r
+ }\r
+ this.setValues('rgb', rgb);\r
+ return this;\r
+ },\r
+\r
+ lighten: function (ratio) {\r
+ var hsl = this.values.hsl;\r
+ hsl[2] += hsl[2] * ratio;\r
+ this.setValues('hsl', hsl);\r
+ return this;\r
+ },\r
+\r
+ darken: function (ratio) {\r
+ var hsl = this.values.hsl;\r
+ hsl[2] -= hsl[2] * ratio;\r
+ this.setValues('hsl', hsl);\r
+ return this;\r
+ },\r
+\r
+ saturate: function (ratio) {\r
+ var hsl = this.values.hsl;\r
+ hsl[1] += hsl[1] * ratio;\r
+ this.setValues('hsl', hsl);\r
+ return this;\r
+ },\r
+\r
+ desaturate: function (ratio) {\r
+ var hsl = this.values.hsl;\r
+ hsl[1] -= hsl[1] * ratio;\r
+ this.setValues('hsl', hsl);\r
+ return this;\r
+ },\r
+\r
+ whiten: function (ratio) {\r
+ var hwb = this.values.hwb;\r
+ hwb[1] += hwb[1] * ratio;\r
+ this.setValues('hwb', hwb);\r
+ return this;\r
+ },\r
+\r
+ blacken: function (ratio) {\r
+ var hwb = this.values.hwb;\r
+ hwb[2] += hwb[2] * ratio;\r
+ this.setValues('hwb', hwb);\r
+ return this;\r
+ },\r
+\r
+ greyscale: function () {\r
+ var rgb = this.values.rgb;\r
+ // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale\r
+ var val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11;\r
+ this.setValues('rgb', [val, val, val]);\r
+ return this;\r
+ },\r
+\r
+ clearer: function (ratio) {\r
+ var alpha = this.values.alpha;\r
+ this.setValues('alpha', alpha - (alpha * ratio));\r
+ return this;\r
+ },\r
+\r
+ opaquer: function (ratio) {\r
+ var alpha = this.values.alpha;\r
+ this.setValues('alpha', alpha + (alpha * ratio));\r
+ return this;\r
+ },\r
+\r
+ rotate: function (degrees) {\r
+ var hsl = this.values.hsl;\r
+ var hue = (hsl[0] + degrees) % 360;\r
+ hsl[0] = hue < 0 ? 360 + hue : hue;\r
+ this.setValues('hsl', hsl);\r
+ return this;\r
+ },\r
+\r
+ /**\r
+ * Ported from sass implementation in C\r
+ * https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209\r
+ */\r
+ mix: function (mixinColor, weight) {\r
+ var color1 = this;\r
+ var color2 = mixinColor;\r
+ var p = weight === undefined ? 0.5 : weight;\r
+\r
+ var w = 2 * p - 1;\r
+ var a = color1.alpha() - color2.alpha();\r
+\r
+ var w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;\r
+ var w2 = 1 - w1;\r
+\r
+ return this\r
+ .rgb(\r
+ w1 * color1.red() + w2 * color2.red(),\r
+ w1 * color1.green() + w2 * color2.green(),\r
+ w1 * color1.blue() + w2 * color2.blue()\r
+ )\r
+ .alpha(color1.alpha() * p + color2.alpha() * (1 - p));\r
+ },\r
+\r
+ toJSON: function () {\r
+ return this.rgb();\r
+ },\r
+\r
+ clone: function () {\r
+ // NOTE(SB): using node-clone creates a dependency to Buffer when using browserify,\r
+ // making the final build way to big to embed in Chart.js. So let's do it manually,\r
+ // assuming that values to clone are 1 dimension arrays containing only numbers,\r
+ // except 'alpha' which is a number.\r
+ var result = new Color();\r
+ var source = this.values;\r
+ var target = result.values;\r
+ var value, type;\r
+\r
+ for (var prop in source) {\r
+ if (source.hasOwnProperty(prop)) {\r
+ value = source[prop];\r
+ type = ({}).toString.call(value);\r
+ if (type === '[object Array]') {\r
+ target[prop] = value.slice(0);\r
+ } else if (type === '[object Number]') {\r
+ target[prop] = value;\r
+ } else {\r
+ console.error('unexpected color value:', value);\r
+ }\r
+ }\r
+ }\r
+\r
+ return result;\r
+ }\r
+};\r
+\r
+Color.prototype.spaces = {\r
+ rgb: ['red', 'green', 'blue'],\r
+ hsl: ['hue', 'saturation', 'lightness'],\r
+ hsv: ['hue', 'saturation', 'value'],\r
+ hwb: ['hue', 'whiteness', 'blackness'],\r
+ cmyk: ['cyan', 'magenta', 'yellow', 'black']\r
+};\r
+\r
+Color.prototype.maxes = {\r
+ rgb: [255, 255, 255],\r
+ hsl: [360, 100, 100],\r
+ hsv: [360, 100, 100],\r
+ hwb: [360, 100, 100],\r
+ cmyk: [100, 100, 100, 100]\r
+};\r
+\r
+Color.prototype.getValues = function (space) {\r
+ var values = this.values;\r
+ var vals = {};\r
+\r
+ for (var i = 0; i < space.length; i++) {\r
+ vals[space.charAt(i)] = values[space][i];\r
+ }\r
+\r
+ if (values.alpha !== 1) {\r
+ vals.a = values.alpha;\r
+ }\r
+\r
+ // {r: 255, g: 255, b: 255, a: 0.4}\r
+ return vals;\r
+};\r
+\r
+Color.prototype.setValues = function (space, vals) {\r
+ var values = this.values;\r
+ var spaces = this.spaces;\r
+ var maxes = this.maxes;\r
+ var alpha = 1;\r
+ var i;\r
+\r
+ if (space === 'alpha') {\r
+ alpha = vals;\r
+ } else if (vals.length) {\r
+ // [10, 10, 10]\r
+ values[space] = vals.slice(0, space.length);\r
+ alpha = vals[space.length];\r
+ } else if (vals[space.charAt(0)] !== undefined) {\r
+ // {r: 10, g: 10, b: 10}\r
+ for (i = 0; i < space.length; i++) {\r
+ values[space][i] = vals[space.charAt(i)];\r
+ }\r
+\r
+ alpha = vals.a;\r
+ } else if (vals[spaces[space][0]] !== undefined) {\r
+ // {red: 10, green: 10, blue: 10}\r
+ var chans = spaces[space];\r
+\r
+ for (i = 0; i < space.length; i++) {\r
+ values[space][i] = vals[chans[i]];\r
+ }\r
+\r
+ alpha = vals.alpha;\r
+ }\r
+\r
+ values.alpha = Math.max(0, Math.min(1, (alpha === undefined ? values.alpha : alpha)));\r
+\r
+ if (space === 'alpha') {\r
+ return false;\r
+ }\r
+\r
+ var capped;\r
+\r
+ // cap values of the space prior converting all values\r
+ for (i = 0; i < space.length; i++) {\r
+ capped = Math.max(0, Math.min(maxes[space][i], values[space][i]));\r
+ values[space][i] = Math.round(capped);\r
+ }\r
+\r
+ // convert to all the other color spaces\r
+ for (var sname in spaces) {\r
+ if (sname !== space) {\r
+ values[sname] = convert[space][sname](values[space]);\r
+ }\r
+ }\r
+\r
+ return true;\r
+};\r
+\r
+Color.prototype.setSpace = function (space, args) {\r
+ var vals = args[0];\r
+\r
+ if (vals === undefined) {\r
+ // color.rgb()\r
+ return this.getValues(space);\r
+ }\r
+\r
+ // color.rgb(10, 10, 10)\r
+ if (typeof vals === 'number') {\r
+ vals = Array.prototype.slice.call(args);\r
+ }\r
+\r
+ this.setValues(space, vals);\r
+ return this;\r
+};\r
+\r
+Color.prototype.setChannel = function (space, index, val) {\r
+ var svalues = this.values[space];\r
+ if (val === undefined) {\r
+ // color.red()\r
+ return svalues[index];\r
+ } else if (val === svalues[index]) {\r
+ // color.red(color.red())\r
+ return this;\r
+ }\r
+\r
+ // color.red(100)\r
+ svalues[index] = val;\r
+ this.setValues(space, svalues);\r
+\r
+ return this;\r
+};\r
+\r
+if (typeof window !== 'undefined') {\r
+ window.Color = Color;\r
+}\r
+\r
+module.exports = Color;\r
+\r
+},{"2":2,"5":5}],4:[function(require,module,exports){\r
+/* MIT license */\r
+\r
+module.exports = {\r
+ rgb2hsl: rgb2hsl,\r
+ rgb2hsv: rgb2hsv,\r
+ rgb2hwb: rgb2hwb,\r
+ rgb2cmyk: rgb2cmyk,\r
+ rgb2keyword: rgb2keyword,\r
+ rgb2xyz: rgb2xyz,\r
+ rgb2lab: rgb2lab,\r
+ rgb2lch: rgb2lch,\r
+\r
+ hsl2rgb: hsl2rgb,\r
+ hsl2hsv: hsl2hsv,\r
+ hsl2hwb: hsl2hwb,\r
+ hsl2cmyk: hsl2cmyk,\r
+ hsl2keyword: hsl2keyword,\r
+\r
+ hsv2rgb: hsv2rgb,\r
+ hsv2hsl: hsv2hsl,\r
+ hsv2hwb: hsv2hwb,\r
+ hsv2cmyk: hsv2cmyk,\r
+ hsv2keyword: hsv2keyword,\r
+\r
+ hwb2rgb: hwb2rgb,\r
+ hwb2hsl: hwb2hsl,\r
+ hwb2hsv: hwb2hsv,\r
+ hwb2cmyk: hwb2cmyk,\r
+ hwb2keyword: hwb2keyword,\r
+\r
+ cmyk2rgb: cmyk2rgb,\r
+ cmyk2hsl: cmyk2hsl,\r
+ cmyk2hsv: cmyk2hsv,\r
+ cmyk2hwb: cmyk2hwb,\r
+ cmyk2keyword: cmyk2keyword,\r
+\r
+ keyword2rgb: keyword2rgb,\r
+ keyword2hsl: keyword2hsl,\r
+ keyword2hsv: keyword2hsv,\r
+ keyword2hwb: keyword2hwb,\r
+ keyword2cmyk: keyword2cmyk,\r
+ keyword2lab: keyword2lab,\r
+ keyword2xyz: keyword2xyz,\r
+\r
+ xyz2rgb: xyz2rgb,\r
+ xyz2lab: xyz2lab,\r
+ xyz2lch: xyz2lch,\r
+\r
+ lab2xyz: lab2xyz,\r
+ lab2rgb: lab2rgb,\r
+ lab2lch: lab2lch,\r
+\r
+ lch2lab: lch2lab,\r
+ lch2xyz: lch2xyz,\r
+ lch2rgb: lch2rgb\r
+}\r
+\r
+\r
+function rgb2hsl(rgb) {\r
+ var r = rgb[0]/255,\r
+ g = rgb[1]/255,\r
+ b = rgb[2]/255,\r
+ min = Math.min(r, g, b),\r
+ max = Math.max(r, g, b),\r
+ delta = max - min,\r
+ h, s, l;\r
+\r
+ if (max == min)\r
+ h = 0;\r
+ else if (r == max)\r
+ h = (g - b) / delta;\r
+ else if (g == max)\r
+ h = 2 + (b - r) / delta;\r
+ else if (b == max)\r
+ h = 4 + (r - g)/ delta;\r
+\r
+ h = Math.min(h * 60, 360);\r
+\r
+ if (h < 0)\r
+ h += 360;\r
+\r
+ l = (min + max) / 2;\r
+\r
+ if (max == min)\r
+ s = 0;\r
+ else if (l <= 0.5)\r
+ s = delta / (max + min);\r
+ else\r
+ s = delta / (2 - max - min);\r
+\r
+ return [h, s * 100, l * 100];\r
+}\r
+\r
+function rgb2hsv(rgb) {\r
+ var r = rgb[0],\r
+ g = rgb[1],\r
+ b = rgb[2],\r
+ min = Math.min(r, g, b),\r
+ max = Math.max(r, g, b),\r
+ delta = max - min,\r
+ h, s, v;\r
+\r
+ if (max == 0)\r
+ s = 0;\r
+ else\r
+ s = (delta/max * 1000)/10;\r
+\r
+ if (max == min)\r
+ h = 0;\r
+ else if (r == max)\r
+ h = (g - b) / delta;\r
+ else if (g == max)\r
+ h = 2 + (b - r) / delta;\r
+ else if (b == max)\r
+ h = 4 + (r - g) / delta;\r
+\r
+ h = Math.min(h * 60, 360);\r
+\r
+ if (h < 0)\r
+ h += 360;\r
+\r
+ v = ((max / 255) * 1000) / 10;\r
+\r
+ return [h, s, v];\r
+}\r
+\r
+function rgb2hwb(rgb) {\r
+ var r = rgb[0],\r
+ g = rgb[1],\r
+ b = rgb[2],\r
+ h = rgb2hsl(rgb)[0],\r
+ w = 1/255 * Math.min(r, Math.min(g, b)),\r
+ b = 1 - 1/255 * Math.max(r, Math.max(g, b));\r
+\r
+ return [h, w * 100, b * 100];\r
+}\r
+\r
+function rgb2cmyk(rgb) {\r
+ var r = rgb[0] / 255,\r
+ g = rgb[1] / 255,\r
+ b = rgb[2] / 255,\r
+ c, m, y, k;\r
+\r
+ k = Math.min(1 - r, 1 - g, 1 - b);\r
+ c = (1 - r - k) / (1 - k) || 0;\r
+ m = (1 - g - k) / (1 - k) || 0;\r
+ y = (1 - b - k) / (1 - k) || 0;\r
+ return [c * 100, m * 100, y * 100, k * 100];\r
+}\r
+\r
+function rgb2keyword(rgb) {\r
+ return reverseKeywords[JSON.stringify(rgb)];\r
+}\r
+\r
+function rgb2xyz(rgb) {\r
+ var r = rgb[0] / 255,\r
+ g = rgb[1] / 255,\r
+ b = rgb[2] / 255;\r
+\r
+ // assume sRGB\r
+ r = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92);\r
+ g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92);\r
+ b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92);\r
+\r
+ var x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805);\r
+ var y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722);\r
+ var z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505);\r
+\r
+ return [x * 100, y *100, z * 100];\r
+}\r
+\r
+function rgb2lab(rgb) {\r
+ var xyz = rgb2xyz(rgb),\r
+ x = xyz[0],\r
+ y = xyz[1],\r
+ z = xyz[2],\r
+ l, a, b;\r
+\r
+ x /= 95.047;\r
+ y /= 100;\r
+ z /= 108.883;\r
+\r
+ x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116);\r
+ y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116);\r
+ z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116);\r
+\r
+ l = (116 * y) - 16;\r
+ a = 500 * (x - y);\r
+ b = 200 * (y - z);\r
+\r
+ return [l, a, b];\r
+}\r
+\r
+function rgb2lch(args) {\r
+ return lab2lch(rgb2lab(args));\r
+}\r
+\r
+function hsl2rgb(hsl) {\r
+ var h = hsl[0] / 360,\r
+ s = hsl[1] / 100,\r
+ l = hsl[2] / 100,\r
+ t1, t2, t3, rgb, val;\r
+\r
+ if (s == 0) {\r
+ val = l * 255;\r
+ return [val, val, val];\r
+ }\r
+\r
+ if (l < 0.5)\r
+ t2 = l * (1 + s);\r
+ else\r
+ t2 = l + s - l * s;\r
+ t1 = 2 * l - t2;\r
+\r
+ rgb = [0, 0, 0];\r
+ for (var i = 0; i < 3; i++) {\r
+ t3 = h + 1 / 3 * - (i - 1);\r
+ t3 < 0 && t3++;\r
+ t3 > 1 && t3--;\r
+\r
+ if (6 * t3 < 1)\r
+ val = t1 + (t2 - t1) * 6 * t3;\r
+ else if (2 * t3 < 1)\r
+ val = t2;\r
+ else if (3 * t3 < 2)\r
+ val = t1 + (t2 - t1) * (2 / 3 - t3) * 6;\r
+ else\r
+ val = t1;\r
+\r
+ rgb[i] = val * 255;\r
+ }\r
+\r
+ return rgb;\r
+}\r
+\r
+function hsl2hsv(hsl) {\r
+ var h = hsl[0],\r
+ s = hsl[1] / 100,\r
+ l = hsl[2] / 100,\r
+ sv, v;\r
+\r
+ if(l === 0) {\r
+ // no need to do calc on black\r
+ // also avoids divide by 0 error\r
+ return [0, 0, 0];\r
+ }\r
+\r
+ l *= 2;\r
+ s *= (l <= 1) ? l : 2 - l;\r
+ v = (l + s) / 2;\r
+ sv = (2 * s) / (l + s);\r
+ return [h, sv * 100, v * 100];\r
+}\r
+\r
+function hsl2hwb(args) {\r
+ return rgb2hwb(hsl2rgb(args));\r
+}\r
+\r
+function hsl2cmyk(args) {\r
+ return rgb2cmyk(hsl2rgb(args));\r
+}\r
+\r
+function hsl2keyword(args) {\r
+ return rgb2keyword(hsl2rgb(args));\r
+}\r
+\r
+\r
+function hsv2rgb(hsv) {\r
+ var h = hsv[0] / 60,\r
+ s = hsv[1] / 100,\r
+ v = hsv[2] / 100,\r
+ hi = Math.floor(h) % 6;\r
+\r
+ var f = h - Math.floor(h),\r
+ p = 255 * v * (1 - s),\r
+ q = 255 * v * (1 - (s * f)),\r
+ t = 255 * v * (1 - (s * (1 - f))),\r
+ v = 255 * v;\r
+\r
+ switch(hi) {\r
+ case 0:\r
+ return [v, t, p];\r
+ case 1:\r
+ return [q, v, p];\r
+ case 2:\r
+ return [p, v, t];\r
+ case 3:\r
+ return [p, q, v];\r
+ case 4:\r
+ return [t, p, v];\r
+ case 5:\r
+ return [v, p, q];\r
+ }\r
+}\r
+\r
+function hsv2hsl(hsv) {\r
+ var h = hsv[0],\r
+ s = hsv[1] / 100,\r
+ v = hsv[2] / 100,\r
+ sl, l;\r
+\r
+ l = (2 - s) * v;\r
+ sl = s * v;\r
+ sl /= (l <= 1) ? l : 2 - l;\r
+ sl = sl || 0;\r
+ l /= 2;\r
+ return [h, sl * 100, l * 100];\r
+}\r
+\r
+function hsv2hwb(args) {\r
+ return rgb2hwb(hsv2rgb(args))\r
+}\r
+\r
+function hsv2cmyk(args) {\r
+ return rgb2cmyk(hsv2rgb(args));\r
+}\r
+\r
+function hsv2keyword(args) {\r
+ return rgb2keyword(hsv2rgb(args));\r
+}\r
+\r
+// http://dev.w3.org/csswg/css-color/#hwb-to-rgb\r
+function hwb2rgb(hwb) {\r
+ var h = hwb[0] / 360,\r
+ wh = hwb[1] / 100,\r
+ bl = hwb[2] / 100,\r
+ ratio = wh + bl,\r
+ i, v, f, n;\r
+\r
+ // wh + bl cant be > 1\r
+ if (ratio > 1) {\r
+ wh /= ratio;\r
+ bl /= ratio;\r
+ }\r
+\r
+ i = Math.floor(6 * h);\r
+ v = 1 - bl;\r
+ f = 6 * h - i;\r
+ if ((i & 0x01) != 0) {\r
+ f = 1 - f;\r
+ }\r
+ n = wh + f * (v - wh); // linear interpolation\r
+\r
+ switch (i) {\r
+ default:\r
+ case 6:\r
+ case 0: r = v; g = n; b = wh; break;\r
+ case 1: r = n; g = v; b = wh; break;\r
+ case 2: r = wh; g = v; b = n; break;\r
+ case 3: r = wh; g = n; b = v; break;\r
+ case 4: r = n; g = wh; b = v; break;\r
+ case 5: r = v; g = wh; b = n; break;\r
+ }\r
+\r
+ return [r * 255, g * 255, b * 255];\r
+}\r
+\r
+function hwb2hsl(args) {\r
+ return rgb2hsl(hwb2rgb(args));\r
+}\r
+\r
+function hwb2hsv(args) {\r
+ return rgb2hsv(hwb2rgb(args));\r
+}\r
+\r
+function hwb2cmyk(args) {\r
+ return rgb2cmyk(hwb2rgb(args));\r
+}\r
+\r
+function hwb2keyword(args) {\r
+ return rgb2keyword(hwb2rgb(args));\r
+}\r
+\r
+function cmyk2rgb(cmyk) {\r
+ var c = cmyk[0] / 100,\r
+ m = cmyk[1] / 100,\r
+ y = cmyk[2] / 100,\r
+ k = cmyk[3] / 100,\r
+ r, g, b;\r
+\r
+ r = 1 - Math.min(1, c * (1 - k) + k);\r
+ g = 1 - Math.min(1, m * (1 - k) + k);\r
+ b = 1 - Math.min(1, y * (1 - k) + k);\r
+ return [r * 255, g * 255, b * 255];\r
+}\r
+\r
+function cmyk2hsl(args) {\r
+ return rgb2hsl(cmyk2rgb(args));\r
+}\r
+\r
+function cmyk2hsv(args) {\r
+ return rgb2hsv(cmyk2rgb(args));\r
+}\r
+\r
+function cmyk2hwb(args) {\r
+ return rgb2hwb(cmyk2rgb(args));\r
+}\r
+\r
+function cmyk2keyword(args) {\r
+ return rgb2keyword(cmyk2rgb(args));\r
+}\r
+\r
+\r
+function xyz2rgb(xyz) {\r
+ var x = xyz[0] / 100,\r
+ y = xyz[1] / 100,\r
+ z = xyz[2] / 100,\r
+ r, g, b;\r
+\r
+ r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986);\r
+ g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415);\r
+ b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570);\r
+\r
+ // assume sRGB\r
+ r = r > 0.0031308 ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055)\r
+ : r = (r * 12.92);\r
+\r
+ g = g > 0.0031308 ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055)\r
+ : g = (g * 12.92);\r
+\r
+ b = b > 0.0031308 ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055)\r
+ : b = (b * 12.92);\r
+\r
+ r = Math.min(Math.max(0, r), 1);\r
+ g = Math.min(Math.max(0, g), 1);\r
+ b = Math.min(Math.max(0, b), 1);\r
+\r
+ return [r * 255, g * 255, b * 255];\r
+}\r
+\r
+function xyz2lab(xyz) {\r
+ var x = xyz[0],\r
+ y = xyz[1],\r
+ z = xyz[2],\r
+ l, a, b;\r
+\r
+ x /= 95.047;\r
+ y /= 100;\r
+ z /= 108.883;\r
+\r
+ x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116);\r
+ y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116);\r
+ z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116);\r
+\r
+ l = (116 * y) - 16;\r
+ a = 500 * (x - y);\r
+ b = 200 * (y - z);\r
+\r
+ return [l, a, b];\r
+}\r
+\r
+function xyz2lch(args) {\r
+ return lab2lch(xyz2lab(args));\r
+}\r
+\r
+function lab2xyz(lab) {\r
+ var l = lab[0],\r
+ a = lab[1],\r
+ b = lab[2],\r
+ x, y, z, y2;\r
+\r
+ if (l <= 8) {\r
+ y = (l * 100) / 903.3;\r
+ y2 = (7.787 * (y / 100)) + (16 / 116);\r
+ } else {\r
+ y = 100 * Math.pow((l + 16) / 116, 3);\r
+ y2 = Math.pow(y / 100, 1/3);\r
+ }\r
+\r
+ x = x / 95.047 <= 0.008856 ? x = (95.047 * ((a / 500) + y2 - (16 / 116))) / 7.787 : 95.047 * Math.pow((a / 500) + y2, 3);\r
+\r
+ z = z / 108.883 <= 0.008859 ? z = (108.883 * (y2 - (b / 200) - (16 / 116))) / 7.787 : 108.883 * Math.pow(y2 - (b / 200), 3);\r
+\r
+ return [x, y, z];\r
+}\r
+\r
+function lab2lch(lab) {\r
+ var l = lab[0],\r
+ a = lab[1],\r
+ b = lab[2],\r
+ hr, h, c;\r
+\r
+ hr = Math.atan2(b, a);\r
+ h = hr * 360 / 2 / Math.PI;\r
+ if (h < 0) {\r
+ h += 360;\r
+ }\r
+ c = Math.sqrt(a * a + b * b);\r
+ return [l, c, h];\r
+}\r
+\r
+function lab2rgb(args) {\r
+ return xyz2rgb(lab2xyz(args));\r
+}\r
+\r
+function lch2lab(lch) {\r
+ var l = lch[0],\r
+ c = lch[1],\r
+ h = lch[2],\r
+ a, b, hr;\r
+\r
+ hr = h / 360 * 2 * Math.PI;\r
+ a = c * Math.cos(hr);\r
+ b = c * Math.sin(hr);\r
+ return [l, a, b];\r
+}\r
+\r
+function lch2xyz(args) {\r
+ return lab2xyz(lch2lab(args));\r
+}\r
+\r
+function lch2rgb(args) {\r
+ return lab2rgb(lch2lab(args));\r
+}\r
+\r
+function keyword2rgb(keyword) {\r
+ return cssKeywords[keyword];\r
+}\r
+\r
+function keyword2hsl(args) {\r
+ return rgb2hsl(keyword2rgb(args));\r
+}\r
+\r
+function keyword2hsv(args) {\r
+ return rgb2hsv(keyword2rgb(args));\r
+}\r
+\r
+function keyword2hwb(args) {\r
+ return rgb2hwb(keyword2rgb(args));\r
+}\r
+\r
+function keyword2cmyk(args) {\r
+ return rgb2cmyk(keyword2rgb(args));\r
+}\r
+\r
+function keyword2lab(args) {\r
+ return rgb2lab(keyword2rgb(args));\r
+}\r
+\r
+function keyword2xyz(args) {\r
+ return rgb2xyz(keyword2rgb(args));\r
+}\r
+\r
+var cssKeywords = {\r
+ aliceblue: [240,248,255],\r
+ antiquewhite: [250,235,215],\r
+ aqua: [0,255,255],\r
+ aquamarine: [127,255,212],\r
+ azure: [240,255,255],\r
+ beige: [245,245,220],\r
+ bisque: [255,228,196],\r
+ black: [0,0,0],\r
+ blanchedalmond: [255,235,205],\r
+ blue: [0,0,255],\r
+ blueviolet: [138,43,226],\r
+ brown: [165,42,42],\r
+ burlywood: [222,184,135],\r
+ cadetblue: [95,158,160],\r
+ chartreuse: [127,255,0],\r
+ chocolate: [210,105,30],\r
+ coral: [255,127,80],\r
+ cornflowerblue: [100,149,237],\r
+ cornsilk: [255,248,220],\r
+ crimson: [220,20,60],\r
+ cyan: [0,255,255],\r
+ darkblue: [0,0,139],\r
+ darkcyan: [0,139,139],\r
+ darkgoldenrod: [184,134,11],\r
+ darkgray: [169,169,169],\r
+ darkgreen: [0,100,0],\r
+ darkgrey: [169,169,169],\r
+ darkkhaki: [189,183,107],\r
+ darkmagenta: [139,0,139],\r
+ darkolivegreen: [85,107,47],\r
+ darkorange: [255,140,0],\r
+ darkorchid: [153,50,204],\r
+ darkred: [139,0,0],\r
+ darksalmon: [233,150,122],\r
+ darkseagreen: [143,188,143],\r
+ darkslateblue: [72,61,139],\r
+ darkslategray: [47,79,79],\r
+ darkslategrey: [47,79,79],\r
+ darkturquoise: [0,206,209],\r
+ darkviolet: [148,0,211],\r
+ deeppink: [255,20,147],\r
+ deepskyblue: [0,191,255],\r
+ dimgray: [105,105,105],\r
+ dimgrey: [105,105,105],\r
+ dodgerblue: [30,144,255],\r
+ firebrick: [178,34,34],\r
+ floralwhite: [255,250,240],\r
+ forestgreen: [34,139,34],\r
+ fuchsia: [255,0,255],\r
+ gainsboro: [220,220,220],\r
+ ghostwhite: [248,248,255],\r
+ gold: [255,215,0],\r
+ goldenrod: [218,165,32],\r
+ gray: [128,128,128],\r
+ green: [0,128,0],\r
+ greenyellow: [173,255,47],\r
+ grey: [128,128,128],\r
+ honeydew: [240,255,240],\r
+ hotpink: [255,105,180],\r
+ indianred: [205,92,92],\r
+ indigo: [75,0,130],\r
+ ivory: [255,255,240],\r
+ khaki: [240,230,140],\r
+ lavender: [230,230,250],\r
+ lavenderblush: [255,240,245],\r
+ lawngreen: [124,252,0],\r
+ lemonchiffon: [255,250,205],\r
+ lightblue: [173,216,230],\r
+ lightcoral: [240,128,128],\r
+ lightcyan: [224,255,255],\r
+ lightgoldenrodyellow: [250,250,210],\r
+ lightgray: [211,211,211],\r
+ lightgreen: [144,238,144],\r
+ lightgrey: [211,211,211],\r
+ lightpink: [255,182,193],\r
+ lightsalmon: [255,160,122],\r
+ lightseagreen: [32,178,170],\r
+ lightskyblue: [135,206,250],\r
+ lightslategray: [119,136,153],\r
+ lightslategrey: [119,136,153],\r
+ lightsteelblue: [176,196,222],\r
+ lightyellow: [255,255,224],\r
+ lime: [0,255,0],\r
+ limegreen: [50,205,50],\r
+ linen: [250,240,230],\r
+ magenta: [255,0,255],\r
+ maroon: [128,0,0],\r
+ mediumaquamarine: [102,205,170],\r
+ mediumblue: [0,0,205],\r
+ mediumorchid: [186,85,211],\r
+ mediumpurple: [147,112,219],\r
+ mediumseagreen: [60,179,113],\r
+ mediumslateblue: [123,104,238],\r
+ mediumspringgreen: [0,250,154],\r
+ mediumturquoise: [72,209,204],\r
+ mediumvioletred: [199,21,133],\r
+ midnightblue: [25,25,112],\r
+ mintcream: [245,255,250],\r
+ mistyrose: [255,228,225],\r
+ moccasin: [255,228,181],\r
+ navajowhite: [255,222,173],\r
+ navy: [0,0,128],\r
+ oldlace: [253,245,230],\r
+ olive: [128,128,0],\r
+ olivedrab: [107,142,35],\r
+ orange: [255,165,0],\r
+ orangered: [255,69,0],\r
+ orchid: [218,112,214],\r
+ palegoldenrod: [238,232,170],\r
+ palegreen: [152,251,152],\r
+ paleturquoise: [175,238,238],\r
+ palevioletred: [219,112,147],\r
+ papayawhip: [255,239,213],\r
+ peachpuff: [255,218,185],\r
+ peru: [205,133,63],\r
+ pink: [255,192,203],\r
+ plum: [221,160,221],\r
+ powderblue: [176,224,230],\r
+ purple: [128,0,128],\r
+ rebeccapurple: [102, 51, 153],\r
+ red: [255,0,0],\r
+ rosybrown: [188,143,143],\r
+ royalblue: [65,105,225],\r
+ saddlebrown: [139,69,19],\r
+ salmon: [250,128,114],\r
+ sandybrown: [244,164,96],\r
+ seagreen: [46,139,87],\r
+ seashell: [255,245,238],\r
+ sienna: [160,82,45],\r
+ silver: [192,192,192],\r
+ skyblue: [135,206,235],\r
+ slateblue: [106,90,205],\r
+ slategray: [112,128,144],\r
+ slategrey: [112,128,144],\r
+ snow: [255,250,250],\r
+ springgreen: [0,255,127],\r
+ steelblue: [70,130,180],\r
+ tan: [210,180,140],\r
+ teal: [0,128,128],\r
+ thistle: [216,191,216],\r
+ tomato: [255,99,71],\r
+ turquoise: [64,224,208],\r
+ violet: [238,130,238],\r
+ wheat: [245,222,179],\r
+ white: [255,255,255],\r
+ whitesmoke: [245,245,245],\r
+ yellow: [255,255,0],\r
+ yellowgreen: [154,205,50]\r
+};\r
+\r
+var reverseKeywords = {};\r
+for (var key in cssKeywords) {\r
+ reverseKeywords[JSON.stringify(cssKeywords[key])] = key;\r
+}\r
+\r
+},{}],5:[function(require,module,exports){\r
+var conversions = require(4);\r
+\r
+var convert = function() {\r
+ return new Converter();\r
+}\r
+\r
+for (var func in conversions) {\r
+ // export Raw versions\r
+ convert[func + "Raw"] = (function(func) {\r
+ // accept array or plain args\r
+ return function(arg) {\r
+ if (typeof arg == "number")\r
+ arg = Array.prototype.slice.call(arguments);\r
+ return conversions[func](arg);\r
+ }\r
+ })(func);\r
+\r
+ var pair = /(\w+)2(\w+)/.exec(func),\r
+ from = pair[1],\r
+ to = pair[2];\r
+\r
+ // export rgb2hsl and ["rgb"]["hsl"]\r
+ convert[from] = convert[from] || {};\r
+\r
+ convert[from][to] = convert[func] = (function(func) { \r
+ return function(arg) {\r
+ if (typeof arg == "number")\r
+ arg = Array.prototype.slice.call(arguments);\r
+ \r
+ var val = conversions[func](arg);\r
+ if (typeof val == "string" || val === undefined)\r
+ return val; // keyword\r
+\r
+ for (var i = 0; i < val.length; i++)\r
+ val[i] = Math.round(val[i]);\r
+ return val;\r
+ }\r
+ })(func);\r
+}\r
+\r
+\r
+/* Converter does lazy conversion and caching */\r
+var Converter = function() {\r
+ this.convs = {};\r
+};\r
+\r
+/* Either get the values for a space or\r
+ set the values for a space, depending on args */\r
+Converter.prototype.routeSpace = function(space, args) {\r
+ var values = args[0];\r
+ if (values === undefined) {\r
+ // color.rgb()\r
+ return this.getValues(space);\r
+ }\r
+ // color.rgb(10, 10, 10)\r
+ if (typeof values == "number") {\r
+ values = Array.prototype.slice.call(args); \r
+ }\r
+\r
+ return this.setValues(space, values);\r
+};\r
+ \r
+/* Set the values for a space, invalidating cache */\r
+Converter.prototype.setValues = function(space, values) {\r
+ this.space = space;\r
+ this.convs = {};\r
+ this.convs[space] = values;\r
+ return this;\r
+};\r
+\r
+/* Get the values for a space. If there's already\r
+ a conversion for the space, fetch it, otherwise\r
+ compute it */\r
+Converter.prototype.getValues = function(space) {\r
+ var vals = this.convs[space];\r
+ if (!vals) {\r
+ var fspace = this.space,\r
+ from = this.convs[fspace];\r
+ vals = convert[fspace][space](from);\r
+\r
+ this.convs[space] = vals;\r
+ }\r
+ return vals;\r
+};\r
+\r
+["rgb", "hsl", "hsv", "cmyk", "keyword"].forEach(function(space) {\r
+ Converter.prototype[space] = function(vals) {\r
+ return this.routeSpace(space, arguments);\r
+ }\r
+});\r
+\r
+module.exports = convert;\r
+},{"4":4}],6:[function(require,module,exports){\r
+module.exports = {\r
+ "aliceblue": [240, 248, 255],\r
+ "antiquewhite": [250, 235, 215],\r
+ "aqua": [0, 255, 255],\r
+ "aquamarine": [127, 255, 212],\r
+ "azure": [240, 255, 255],\r
+ "beige": [245, 245, 220],\r
+ "bisque": [255, 228, 196],\r
+ "black": [0, 0, 0],\r
+ "blanchedalmond": [255, 235, 205],\r
+ "blue": [0, 0, 255],\r
+ "blueviolet": [138, 43, 226],\r
+ "brown": [165, 42, 42],\r
+ "burlywood": [222, 184, 135],\r
+ "cadetblue": [95, 158, 160],\r
+ "chartreuse": [127, 255, 0],\r
+ "chocolate": [210, 105, 30],\r
+ "coral": [255, 127, 80],\r
+ "cornflowerblue": [100, 149, 237],\r
+ "cornsilk": [255, 248, 220],\r
+ "crimson": [220, 20, 60],\r
+ "cyan": [0, 255, 255],\r
+ "darkblue": [0, 0, 139],\r
+ "darkcyan": [0, 139, 139],\r
+ "darkgoldenrod": [184, 134, 11],\r
+ "darkgray": [169, 169, 169],\r
+ "darkgreen": [0, 100, 0],\r
+ "darkgrey": [169, 169, 169],\r
+ "darkkhaki": [189, 183, 107],\r
+ "darkmagenta": [139, 0, 139],\r
+ "darkolivegreen": [85, 107, 47],\r
+ "darkorange": [255, 140, 0],\r
+ "darkorchid": [153, 50, 204],\r
+ "darkred": [139, 0, 0],\r
+ "darksalmon": [233, 150, 122],\r
+ "darkseagreen": [143, 188, 143],\r
+ "darkslateblue": [72, 61, 139],\r
+ "darkslategray": [47, 79, 79],\r
+ "darkslategrey": [47, 79, 79],\r
+ "darkturquoise": [0, 206, 209],\r
+ "darkviolet": [148, 0, 211],\r
+ "deeppink": [255, 20, 147],\r
+ "deepskyblue": [0, 191, 255],\r
+ "dimgray": [105, 105, 105],\r
+ "dimgrey": [105, 105, 105],\r
+ "dodgerblue": [30, 144, 255],\r
+ "firebrick": [178, 34, 34],\r
+ "floralwhite": [255, 250, 240],\r
+ "forestgreen": [34, 139, 34],\r
+ "fuchsia": [255, 0, 255],\r
+ "gainsboro": [220, 220, 220],\r
+ "ghostwhite": [248, 248, 255],\r
+ "gold": [255, 215, 0],\r
+ "goldenrod": [218, 165, 32],\r
+ "gray": [128, 128, 128],\r
+ "green": [0, 128, 0],\r
+ "greenyellow": [173, 255, 47],\r
+ "grey": [128, 128, 128],\r
+ "honeydew": [240, 255, 240],\r
+ "hotpink": [255, 105, 180],\r
+ "indianred": [205, 92, 92],\r
+ "indigo": [75, 0, 130],\r
+ "ivory": [255, 255, 240],\r
+ "khaki": [240, 230, 140],\r
+ "lavender": [230, 230, 250],\r
+ "lavenderblush": [255, 240, 245],\r
+ "lawngreen": [124, 252, 0],\r
+ "lemonchiffon": [255, 250, 205],\r
+ "lightblue": [173, 216, 230],\r
+ "lightcoral": [240, 128, 128],\r
+ "lightcyan": [224, 255, 255],\r
+ "lightgoldenrodyellow": [250, 250, 210],\r
+ "lightgray": [211, 211, 211],\r
+ "lightgreen": [144, 238, 144],\r
+ "lightgrey": [211, 211, 211],\r
+ "lightpink": [255, 182, 193],\r
+ "lightsalmon": [255, 160, 122],\r
+ "lightseagreen": [32, 178, 170],\r
+ "lightskyblue": [135, 206, 250],\r
+ "lightslategray": [119, 136, 153],\r
+ "lightslategrey": [119, 136, 153],\r
+ "lightsteelblue": [176, 196, 222],\r
+ "lightyellow": [255, 255, 224],\r
+ "lime": [0, 255, 0],\r
+ "limegreen": [50, 205, 50],\r
+ "linen": [250, 240, 230],\r
+ "magenta": [255, 0, 255],\r
+ "maroon": [128, 0, 0],\r
+ "mediumaquamarine": [102, 205, 170],\r
+ "mediumblue": [0, 0, 205],\r
+ "mediumorchid": [186, 85, 211],\r
+ "mediumpurple": [147, 112, 219],\r
+ "mediumseagreen": [60, 179, 113],\r
+ "mediumslateblue": [123, 104, 238],\r
+ "mediumspringgreen": [0, 250, 154],\r
+ "mediumturquoise": [72, 209, 204],\r
+ "mediumvioletred": [199, 21, 133],\r
+ "midnightblue": [25, 25, 112],\r
+ "mintcream": [245, 255, 250],\r
+ "mistyrose": [255, 228, 225],\r
+ "moccasin": [255, 228, 181],\r
+ "navajowhite": [255, 222, 173],\r
+ "navy": [0, 0, 128],\r
+ "oldlace": [253, 245, 230],\r
+ "olive": [128, 128, 0],\r
+ "olivedrab": [107, 142, 35],\r
+ "orange": [255, 165, 0],\r
+ "orangered": [255, 69, 0],\r
+ "orchid": [218, 112, 214],\r
+ "palegoldenrod": [238, 232, 170],\r
+ "palegreen": [152, 251, 152],\r
+ "paleturquoise": [175, 238, 238],\r
+ "palevioletred": [219, 112, 147],\r
+ "papayawhip": [255, 239, 213],\r
+ "peachpuff": [255, 218, 185],\r
+ "peru": [205, 133, 63],\r
+ "pink": [255, 192, 203],\r
+ "plum": [221, 160, 221],\r
+ "powderblue": [176, 224, 230],\r
+ "purple": [128, 0, 128],\r
+ "rebeccapurple": [102, 51, 153],\r
+ "red": [255, 0, 0],\r
+ "rosybrown": [188, 143, 143],\r
+ "royalblue": [65, 105, 225],\r
+ "saddlebrown": [139, 69, 19],\r
+ "salmon": [250, 128, 114],\r
+ "sandybrown": [244, 164, 96],\r
+ "seagreen": [46, 139, 87],\r
+ "seashell": [255, 245, 238],\r
+ "sienna": [160, 82, 45],\r
+ "silver": [192, 192, 192],\r
+ "skyblue": [135, 206, 235],\r
+ "slateblue": [106, 90, 205],\r
+ "slategray": [112, 128, 144],\r
+ "slategrey": [112, 128, 144],\r
+ "snow": [255, 250, 250],\r
+ "springgreen": [0, 255, 127],\r
+ "steelblue": [70, 130, 180],\r
+ "tan": [210, 180, 140],\r
+ "teal": [0, 128, 128],\r
+ "thistle": [216, 191, 216],\r
+ "tomato": [255, 99, 71],\r
+ "turquoise": [64, 224, 208],\r
+ "violet": [238, 130, 238],\r
+ "wheat": [245, 222, 179],\r
+ "white": [255, 255, 255],\r
+ "whitesmoke": [245, 245, 245],\r
+ "yellow": [255, 255, 0],\r
+ "yellowgreen": [154, 205, 50]\r
+};\r
+},{}],7:[function(require,module,exports){\r
+/**\r
+ * @namespace Chart\r
+ */\r
+var Chart = require(26)();\r
+\r
+require(25)(Chart);\r
+require(24)(Chart);\r
+require(21)(Chart);\r
+require(22)(Chart);\r
+require(23)(Chart);\r
+require(27)(Chart);\r
+require(31)(Chart);\r
+require(29)(Chart);\r
+require(30)(Chart);\r
+require(32)(Chart);\r
+require(28)(Chart);\r
+require(33)(Chart);\r
+\r
+require(34)(Chart);\r
+require(35)(Chart);\r
+require(36)(Chart);\r
+require(37)(Chart);\r
+\r
+require(40)(Chart);\r
+require(38)(Chart);\r
+require(39)(Chart);\r
+require(41)(Chart);\r
+require(42)(Chart);\r
+require(43)(Chart);\r
+\r
+// Controllers must be loaded after elements\r
+// See Chart.core.datasetController.dataElementType\r
+require(15)(Chart);\r
+require(16)(Chart);\r
+require(17)(Chart);\r
+require(18)(Chart);\r
+require(19)(Chart);\r
+require(20)(Chart);\r
+\r
+require(8)(Chart);\r
+require(9)(Chart);\r
+require(10)(Chart);\r
+require(11)(Chart);\r
+require(12)(Chart);\r
+require(13)(Chart);\r
+require(14)(Chart);\r
+\r
+window.Chart = module.exports = Chart;\r
+\r
+},{"10":10,"11":11,"12":12,"13":13,"14":14,"15":15,"16":16,"17":17,"18":18,"19":19,"20":20,"21":21,"22":22,"23":23,"24":24,"25":25,"26":26,"27":27,"28":28,"29":29,"30":30,"31":31,"32":32,"33":33,"34":34,"35":35,"36":36,"37":37,"38":38,"39":39,"40":40,"41":41,"42":42,"43":43,"8":8,"9":9}],8:[function(require,module,exports){\r
+"use strict";\r
+\r
+module.exports = function(Chart) {\r
+\r
+ Chart.Bar = function(context, config) {\r
+ config.type = 'bar';\r
+\r
+ return new Chart(context, config);\r
+ };\r
+\r
+};\r
+},{}],9:[function(require,module,exports){\r
+"use strict";\r
+\r
+module.exports = function(Chart) {\r
+\r
+ Chart.Bubble = function(context, config) {\r
+ config.type = 'bubble';\r
+ return new Chart(context, config);\r
+ };\r
+\r
+};\r
+},{}],10:[function(require,module,exports){\r
+"use strict";\r
+\r
+module.exports = function(Chart) {\r
+\r
+ Chart.Doughnut = function(context, config) {\r
+ config.type = 'doughnut';\r
+\r
+ return new Chart(context, config);\r
+ };\r
+\r
+};\r
+},{}],11:[function(require,module,exports){\r
+"use strict";\r
+\r
+module.exports = function(Chart) {\r
+\r
+ Chart.Line = function(context, config) {\r
+ config.type = 'line';\r
+\r
+ return new Chart(context, config);\r
+ };\r
+\r
+};\r
+},{}],12:[function(require,module,exports){\r
+"use strict";\r
+\r
+module.exports = function(Chart) {\r
+\r
+ Chart.PolarArea = function(context, config) {\r
+ config.type = 'polarArea';\r
+\r
+ return new Chart(context, config);\r
+ };\r
+\r
+};\r
+},{}],13:[function(require,module,exports){\r
+"use strict";\r
+\r
+module.exports = function(Chart) {\r
+ \r
+ Chart.Radar = function(context, config) {\r
+ config.options = Chart.helpers.configMerge({ aspectRatio: 1 }, config.options);\r
+ config.type = 'radar';\r
+\r
+ return new Chart(context, config);\r
+ };\r
+\r
+};\r
+\r
+},{}],14:[function(require,module,exports){\r
+"use strict";\r
+\r
+module.exports = function(Chart) {\r
+\r
+ var defaultConfig = {\r
+ hover: {\r
+ mode: 'single'\r
+ },\r
+\r
+ scales: {\r
+ xAxes: [{\r
+ type: "linear", // scatter should not use a category axis\r
+ position: "bottom",\r
+ id: "x-axis-1" // need an ID so datasets can reference the scale\r
+ }],\r
+ yAxes: [{\r
+ type: "linear",\r
+ position: "left",\r
+ id: "y-axis-1"\r
+ }]\r
+ },\r
+\r
+ tooltips: {\r
+ callbacks: {\r
+ title: function(tooltipItems, data) {\r
+ // Title doesn't make sense for scatter since we format the data as a point\r
+ return '';\r
+ },\r
+ label: function(tooltipItem, data) {\r
+ return '(' + tooltipItem.xLabel + ', ' + tooltipItem.yLabel + ')';\r
+ }\r
+ }\r
+ }\r
+ };\r
+\r
+ // Register the default config for this type\r
+ Chart.defaults.scatter = defaultConfig;\r
+\r
+ // Scatter charts use line controllers\r
+ Chart.controllers.scatter = Chart.controllers.line;\r
+\r
+ Chart.Scatter = function(context, config) {\r
+ config.type = 'scatter';\r
+ return new Chart(context, config);\r
+ };\r
+\r
+};\r
+},{}],15:[function(require,module,exports){\r
+"use strict";\r
+\r
+module.exports = function(Chart) {\r
+\r
+ var helpers = Chart.helpers;\r
+\r
+ Chart.defaults.bar = {\r
+ hover: {\r
+ mode: "label"\r
+ },\r
+\r
+ scales: {\r
+ xAxes: [{\r
+ type: "category",\r
+\r
+ // Specific to Bar Controller\r
+ categoryPercentage: 0.8,\r
+ barPercentage: 0.9,\r
+\r
+ // grid line settings\r
+ gridLines: {\r
+ offsetGridLines: true\r
+ }\r
+ }],\r
+ yAxes: [{\r
+ type: "linear"\r
+ }]\r
+ }\r
+ };\r
+\r
+ Chart.controllers.bar = Chart.DatasetController.extend({\r
+\r
+ dataElementType: Chart.elements.Rectangle,\r
+\r
+ initialize: function(chart, datasetIndex) {\r
+ Chart.DatasetController.prototype.initialize.call(this, chart, datasetIndex);\r
+\r
+ // Use this to indicate that this is a bar dataset.\r
+ this.getMeta().bar = true;\r
+ },\r
+\r
+ // Get the number of datasets that display bars. We use this to correctly calculate the bar width\r
+ getBarCount: function getBarCount() {\r
+ var me = this;\r
+ var barCount = 0;\r
+ helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) {\r
+ var meta = me.chart.getDatasetMeta(datasetIndex);\r
+ if (meta.bar && me.chart.isDatasetVisible(datasetIndex)) {\r
+ ++barCount;\r
+ }\r
+ }, me);\r
+ return barCount;\r
+ },\r
+\r
+ update: function update(reset) {\r
+ var me = this;\r
+ helpers.each(me.getMeta().data, function(rectangle, index) {\r
+ me.updateElement(rectangle, index, reset);\r
+ }, me);\r
+ },\r
+\r
+ updateElement: function updateElement(rectangle, index, reset) {\r
+ var me = this;\r
+ var meta = me.getMeta();\r
+ var xScale = me.getScaleForId(meta.xAxisID);\r
+ var yScale = me.getScaleForId(meta.yAxisID);\r
+ var scaleBase = yScale.getBasePixel();\r
+ var rectangleElementOptions = me.chart.options.elements.rectangle;\r
+ var custom = rectangle.custom || {};\r
+ var dataset = me.getDataset();\r
+\r
+ helpers.extend(rectangle, {\r
+ // Utility\r
+ _xScale: xScale,\r
+ _yScale: yScale,\r
+ _datasetIndex: me.index,\r
+ _index: index,\r
+\r
+ // Desired view properties\r
+ _model: {\r
+ x: me.calculateBarX(index, me.index),\r
+ y: reset ? scaleBase : me.calculateBarY(index, me.index),\r
+\r
+ // Tooltip\r
+ label: me.chart.data.labels[index],\r
+ datasetLabel: dataset.label,\r
+\r
+ // Appearance\r
+ base: reset ? scaleBase : me.calculateBarBase(me.index, index),\r
+ width: me.calculateBarWidth(index),\r
+ backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor),\r
+ borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped,\r
+ borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor),\r
+ borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth)\r
+ }\r
+ });\r
+ rectangle.pivot();\r
+ },\r
+\r
+ calculateBarBase: function(datasetIndex, index) {\r
+ var me = this;\r
+ var meta = me.getMeta();\r
+ var yScale = me.getScaleForId(meta.yAxisID);\r
+ var base = 0;\r
+\r
+ if (yScale.options.stacked) {\r
+ var chart = me.chart;\r
+ var datasets = chart.data.datasets;\r
+ var value = datasets[datasetIndex].data[index];\r
+\r
+ if (value < 0) {\r
+ for (var i = 0; i < datasetIndex; i++) {\r
+ var negDS = datasets[i];\r
+ var negDSMeta = chart.getDatasetMeta(i);\r
+ if (negDSMeta.bar && negDSMeta.yAxisID === yScale.id && chart.isDatasetVisible(i)) {\r
+ base += negDS.data[index] < 0 ? negDS.data[index] : 0;\r
+ }\r
+ }\r
+ } else {\r
+ for (var j = 0; j < datasetIndex; j++) {\r
+ var posDS = datasets[j];\r
+ var posDSMeta = chart.getDatasetMeta(j);\r
+ if (posDSMeta.bar && posDSMeta.yAxisID === yScale.id && chart.isDatasetVisible(j)) {\r
+ base += posDS.data[index] > 0 ? posDS.data[index] : 0;\r
+ }\r
+ }\r
+ }\r
+\r
+ return yScale.getPixelForValue(base);\r
+ }\r
+\r
+ return yScale.getBasePixel();\r
+ },\r
+\r
+ getRuler: function(index) {\r
+ var me = this;\r
+ var meta = me.getMeta();\r
+ var xScale = me.getScaleForId(meta.xAxisID);\r
+ var datasetCount = me.getBarCount();\r
+\r
+ var tickWidth;\r
+\r
+ if (xScale.options.type === 'category') {\r
+ tickWidth = xScale.getPixelForTick(index + 1) - xScale.getPixelForTick(index);\r
+ } else {\r
+ // Average width\r
+ tickWidth = xScale.width / xScale.ticks.length;\r
+ }\r
+ var categoryWidth = tickWidth * xScale.options.categoryPercentage;\r
+ var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2;\r
+ var fullBarWidth = categoryWidth / datasetCount;\r
+\r
+ if (xScale.ticks.length !== me.chart.data.labels.length) {\r
+ var perc = xScale.ticks.length / me.chart.data.labels.length;\r
+ fullBarWidth = fullBarWidth * perc;\r
+ }\r
+\r
+ var barWidth = fullBarWidth * xScale.options.barPercentage;\r
+ var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage);\r
+\r
+ return {\r
+ datasetCount: datasetCount,\r
+ tickWidth: tickWidth,\r
+ categoryWidth: categoryWidth,\r
+ categorySpacing: categorySpacing,\r
+ fullBarWidth: fullBarWidth,\r
+ barWidth: barWidth,\r
+ barSpacing: barSpacing\r
+ };\r
+ },\r
+\r
+ calculateBarWidth: function(index) {\r
+ var xScale = this.getScaleForId(this.getMeta().xAxisID);\r
+ var ruler = this.getRuler(index);\r
+ return xScale.options.stacked ? ruler.categoryWidth : ruler.barWidth;\r
+ },\r
+\r
+ // Get bar index from the given dataset index accounting for the fact that not all bars are visible\r
+ getBarIndex: function(datasetIndex) {\r
+ var barIndex = 0;\r
+ var meta, j;\r
+\r
+ for (j = 0; j < datasetIndex; ++j) {\r
+ meta = this.chart.getDatasetMeta(j);\r
+ if (meta.bar && this.chart.isDatasetVisible(j)) {\r
+ ++barIndex;\r
+ }\r
+ }\r
+\r
+ return barIndex;\r
+ },\r
+\r
+ calculateBarX: function(index, datasetIndex) {\r
+ var me = this;\r
+ var meta = me.getMeta();\r
+ var xScale = me.getScaleForId(meta.xAxisID);\r
+ var barIndex = me.getBarIndex(datasetIndex);\r
+\r
+ var ruler = me.getRuler(index);\r
+ var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo);\r
+ leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0;\r
+\r
+ if (xScale.options.stacked) {\r
+ return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing;\r
+ }\r
+\r
+ return leftTick +\r
+ (ruler.barWidth / 2) +\r
+ ruler.categorySpacing +\r
+ (ruler.barWidth * barIndex) +\r
+ (ruler.barSpacing / 2) +\r
+ (ruler.barSpacing * barIndex);\r
+ },\r
+\r
+ calculateBarY: function(index, datasetIndex) {\r
+ var me = this;\r
+ var meta = me.getMeta();\r
+ var yScale = me.getScaleForId(meta.yAxisID);\r
+ var value = me.getDataset().data[index];\r
+\r
+ if (yScale.options.stacked) {\r
+\r
+ var sumPos = 0,\r
+ sumNeg = 0;\r
+\r
+ for (var i = 0; i < datasetIndex; i++) {\r
+ var ds = me.chart.data.datasets[i];\r
+ var dsMeta = me.chart.getDatasetMeta(i);\r
+ if (dsMeta.bar && dsMeta.yAxisID === yScale.id && me.chart.isDatasetVisible(i)) {\r
+ if (ds.data[index] < 0) {\r
+ sumNeg += ds.data[index] || 0;\r
+ } else {\r
+ sumPos += ds.data[index] || 0;\r
+ }\r
+ }\r
+ }\r
+\r
+ if (value < 0) {\r
+ return yScale.getPixelForValue(sumNeg + value);\r
+ } else {\r
+ return yScale.getPixelForValue(sumPos + value);\r
+ }\r
+ }\r
+\r
+ return yScale.getPixelForValue(value);\r
+ },\r
+\r
+ draw: function(ease) {\r
+ var me = this;\r
+ var easingDecimal = ease || 1;\r
+ helpers.each(me.getMeta().data, function(rectangle, index) {\r
+ var d = me.getDataset().data[index];\r
+ if (d !== null && d !== undefined && !isNaN(d)) {\r
+ rectangle.transition(easingDecimal).draw();\r
+ }\r
+ }, me);\r
+ },\r
+\r
+ setHoverStyle: function(rectangle) {\r
+ var dataset = this.chart.data.datasets[rectangle._datasetIndex];\r
+ var index = rectangle._index;\r
+\r
+ var custom = rectangle.custom || {};\r
+ var model = rectangle._model;\r
+ model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor));\r
+ model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.getHoverColor(model.borderColor));\r
+ model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, model.borderWidth);\r
+ },\r
+\r
+ removeHoverStyle: function(rectangle) {\r
+ var dataset = this.chart.data.datasets[rectangle._datasetIndex];\r
+ var index = rectangle._index;\r
+ var custom = rectangle.custom || {};\r
+ var model = rectangle._model;\r
+ var rectangleElementOptions = this.chart.options.elements.rectangle;\r
+\r
+ model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor);\r
+ model.borderColor = custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor);\r
+ model.borderWidth = custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth);\r
+ }\r
+\r
+ });\r
+\r
+\r
+ // including horizontalBar in the bar file, instead of a file of its own\r
+ // it extends bar (like pie extends doughnut)\r
+ Chart.defaults.horizontalBar = {\r
+ hover: {\r
+ mode: "label"\r
+ },\r
+\r
+ scales: {\r
+ xAxes: [{\r
+ type: "linear",\r
+ position: "bottom"\r
+ }],\r
+ yAxes: [{\r
+ position: "left",\r
+ type: "category",\r
+\r
+ // Specific to Horizontal Bar Controller\r
+ categoryPercentage: 0.8,\r
+ barPercentage: 0.9,\r
+\r
+ // grid line settings\r
+ gridLines: {\r
+ offsetGridLines: true\r
+ }\r
+ }]\r
+ },\r
+ elements: {\r
+ rectangle: {\r
+ borderSkipped: 'left'\r
+ }\r
+ },\r
+ tooltips: {\r
+ callbacks: {\r
+ title: function(tooltipItems, data) {\r
+ // Pick first xLabel for now\r
+ var title = '';\r
+\r
+ if (tooltipItems.length > 0) {\r
+ if (tooltipItems[0].yLabel) {\r
+ title = tooltipItems[0].yLabel;\r
+ } else if (data.labels.length > 0 && tooltipItems[0].index < data.labels.length) {\r
+ title = data.labels[tooltipItems[0].index];\r
+ }\r
+ }\r
+\r
+ return title;\r
+ },\r
+ label: function(tooltipItem, data) {\r
+ var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || '';\r
+ return datasetLabel + ': ' + tooltipItem.xLabel;\r
+ }\r
+ }\r
+ }\r
+ };\r
+\r
+ Chart.controllers.horizontalBar = Chart.controllers.bar.extend({\r
+ updateElement: function updateElement(rectangle, index, reset, numBars) {\r
+ var me = this;\r
+ var meta = me.getMeta();\r
+ var xScale = me.getScaleForId(meta.xAxisID);\r
+ var yScale = me.getScaleForId(meta.yAxisID);\r
+ var scaleBase = xScale.getBasePixel();\r
+ var custom = rectangle.custom || {};\r
+ var dataset = me.getDataset();\r
+ var rectangleElementOptions = me.chart.options.elements.rectangle;\r
+\r
+ helpers.extend(rectangle, {\r
+ // Utility\r
+ _xScale: xScale,\r
+ _yScale: yScale,\r
+ _datasetIndex: me.index,\r
+ _index: index,\r
+\r
+ // Desired view properties\r
+ _model: {\r
+ x: reset ? scaleBase : me.calculateBarX(index, me.index),\r
+ y: me.calculateBarY(index, me.index),\r
+\r
+ // Tooltip\r
+ label: me.chart.data.labels[index],\r
+ datasetLabel: dataset.label,\r
+\r
+ // Appearance\r
+ base: reset ? scaleBase : me.calculateBarBase(me.index, index),\r
+ height: me.calculateBarHeight(index),\r
+ backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor),\r
+ borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped,\r
+ borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor),\r
+ borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth)\r
+ },\r
+\r
+ draw: function () {\r
+ var ctx = this._chart.ctx;\r
+ var vm = this._view;\r
+\r
+ var halfHeight = vm.height / 2,\r
+ topY = vm.y - halfHeight,\r
+ bottomY = vm.y + halfHeight,\r
+ right = vm.base - (vm.base - vm.x),\r
+ halfStroke = vm.borderWidth / 2;\r
+\r
+ // Canvas doesn't allow us to stroke inside the width so we can\r
+ // adjust the sizes to fit if we're setting a stroke on the line\r
+ if (vm.borderWidth) {\r
+ topY += halfStroke;\r
+ bottomY -= halfStroke;\r
+ right += halfStroke;\r
+ }\r
+\r
+ ctx.beginPath();\r
+\r
+ ctx.fillStyle = vm.backgroundColor;\r
+ ctx.strokeStyle = vm.borderColor;\r
+ ctx.lineWidth = vm.borderWidth;\r
+\r
+ // Corner points, from bottom-left to bottom-right clockwise\r
+ // | 1 2 |\r
+ // | 0 3 |\r
+ var corners = [\r
+ [vm.base, bottomY],\r
+ [vm.base, topY],\r
+ [right, topY],\r
+ [right, bottomY]\r
+ ];\r
+\r
+ // Find first (starting) corner with fallback to 'bottom'\r
+ var borders = ['bottom', 'left', 'top', 'right'];\r
+ var startCorner = borders.indexOf(vm.borderSkipped, 0);\r
+ if (startCorner === -1)\r
+ startCorner = 0;\r
+\r
+ function cornerAt(index) {\r
+ return corners[(startCorner + index) % 4];\r
+ }\r
+\r
+ // Draw rectangle from 'startCorner'\r
+ ctx.moveTo.apply(ctx, cornerAt(0));\r
+ for (var i = 1; i < 4; i++)\r
+ ctx.lineTo.apply(ctx, cornerAt(i));\r
+\r
+ ctx.fill();\r
+ if (vm.borderWidth) {\r
+ ctx.stroke();\r
+ }\r
+ },\r
+\r
+ inRange: function (mouseX, mouseY) {\r
+ var vm = this._view;\r
+ var inRange = false;\r
+\r
+ if (vm) {\r
+ if (vm.x < vm.base) {\r
+ inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.x && mouseX <= vm.base);\r
+ } else {\r
+ inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.base && mouseX <= vm.x);\r
+ }\r
+ }\r
+\r
+ return inRange;\r
+ }\r
+ });\r
+\r
+ rectangle.pivot();\r
+ },\r
+\r
+ calculateBarBase: function (datasetIndex, index) {\r
+ var me = this;\r
+ var meta = me.getMeta();\r
+ var xScale = me.getScaleForId(meta.xAxisID);\r
+ var base = 0;\r
+\r
+ if (xScale.options.stacked) {\r
+\r
+ var value = me.chart.data.datasets[datasetIndex].data[index];\r
+\r
+ if (value < 0) {\r
+ for (var i = 0; i < datasetIndex; i++) {\r
+ var negDS = me.chart.data.datasets[i];\r
+ var negDSMeta = me.chart.getDatasetMeta(i);\r
+ if (negDSMeta.bar && negDSMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i)) {\r
+ base += negDS.data[index] < 0 ? negDS.data[index] : 0;\r
+ }\r
+ }\r
+ } else {\r
+ for (var j = 0; j < datasetIndex; j++) {\r
+ var posDS = me.chart.data.datasets[j];\r
+ var posDSMeta = me.chart.getDatasetMeta(j);\r
+ if (posDSMeta.bar && posDSMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(j)) {\r
+ base += posDS.data[index] > 0 ? posDS.data[index] : 0;\r
+ }\r
+ }\r
+ }\r
+\r
+ return xScale.getPixelForValue(base);\r
+ }\r
+\r
+ return xScale.getBasePixel();\r
+ },\r
+\r
+ getRuler: function (index) {\r
+ var me = this;\r
+ var meta = me.getMeta();\r
+ var yScale = me.getScaleForId(meta.yAxisID);\r
+ var datasetCount = me.getBarCount();\r
+\r
+ var tickHeight;\r
+ if (yScale.options.type === 'category') {\r
+ tickHeight = yScale.getPixelForTick(index + 1) - yScale.getPixelForTick(index);\r
+ } else {\r
+ // Average width\r
+ tickHeight = yScale.width / yScale.ticks.length;\r
+ }\r
+ var categoryHeight = tickHeight * yScale.options.categoryPercentage;\r
+ var categorySpacing = (tickHeight - (tickHeight * yScale.options.categoryPercentage)) / 2;\r
+ var fullBarHeight = categoryHeight / datasetCount;\r
+\r
+ if (yScale.ticks.length !== me.chart.data.labels.length) {\r
+ var perc = yScale.ticks.length / me.chart.data.labels.length;\r
+ fullBarHeight = fullBarHeight * perc;\r
+ }\r
+\r
+ var barHeight = fullBarHeight * yScale.options.barPercentage;\r
+ var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage);\r
+\r
+ return {\r
+ datasetCount: datasetCount,\r
+ tickHeight: tickHeight,\r
+ categoryHeight: categoryHeight,\r
+ categorySpacing: categorySpacing,\r
+ fullBarHeight: fullBarHeight,\r
+ barHeight: barHeight,\r
+ barSpacing: barSpacing,\r
+ };\r
+ },\r
+\r
+ calculateBarHeight: function (index) {\r
+ var me = this;\r
+ var yScale = me.getScaleForId(me.getMeta().yAxisID);\r
+ var ruler = me.getRuler(index);\r
+ return yScale.options.stacked ? ruler.categoryHeight : ruler.barHeight;\r
+ },\r
+\r
+ calculateBarX: function (index, datasetIndex) {\r
+ var me = this;\r
+ var meta = me.getMeta();\r
+ var xScale = me.getScaleForId(meta.xAxisID);\r
+ var value = me.getDataset().data[index];\r
+\r
+ if (xScale.options.stacked) {\r
+\r
+ var sumPos = 0,\r
+ sumNeg = 0;\r
+\r
+ for (var i = 0; i < datasetIndex; i++) {\r
+ var ds = me.chart.data.datasets[i];\r
+ var dsMeta = me.chart.getDatasetMeta(i);\r
+ if (dsMeta.bar && dsMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i)) {\r
+ if (ds.data[index] < 0) {\r
+ &nb