Merge branch 'MDL-52597-master' of git://github.com/andrewnicols/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 29 Dec 2015 02:01:07 +0000 (10:01 +0800)
committerDavid Monllao <davidm@moodle.com>
Tue, 29 Dec 2015 02:01:07 +0000 (10:01 +0800)
91 files changed:
.travis.yml
backup/util/helper/restore_logs_processor.class.php
backup/util/plan/backup_structure_step.class.php
backup/util/plan/restore_structure_step.class.php
blog/edit.php
blog/external_blogs.php
blog/index.php
blog/lib.php
blog/locallib.php
blog/renderer.php
blog/rsslib.php
blog/tests/bloglib_test.php
completion/tests/behat/teacher_manual_completion.feature
enrol/database/lib.php
enrol/guest/classes/external.php [new file with mode: 0644]
enrol/guest/db/services.php [new file with mode: 0644]
enrol/guest/lib.php
enrol/guest/tests/external_test.php [new file with mode: 0644]
enrol/guest/version.php
enrol/self/edit_form.php
enrol/self/lib.php
filter/tex/latex.php
grade/report/singleview/index.php
install/lang/he/install.php
install/lang/pt_br/moodle.php
lib/behat/classes/behat_selectors.php
lib/db/services.php
lib/dml/moodle_database.php
lib/dml/tests/dml_test.php
lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-debug.js
lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-min.js
lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button.js
lib/editor/atto/plugins/rtl/yui/src/button/js/button.js
lib/moodlelib.php
lib/outputcomponents.php
lib/phpunit/classes/util.php
lib/phpunit/tests/util_test.php [new file with mode: 0644]
lib/setuplib.php
lib/testing/classes/util.php
lib/testing/generator/data_generator.php
lib/testing/tests/generator_test.php
lib/tests/behat/largeforms.feature [new file with mode: 0644]
lib/tests/fixtures/max_input_vars.php [new file with mode: 0644]
message/lib.php
message/output/airnotifier/classes/manager.php
message/tests/behat/message_participants.feature
message/upgrade.txt
mod/data/field/date/field.class.php
mod/forum/search.php
mod/lesson/tests/behat/completion_condition_time_spent.feature
mod/lesson/tests/behat/date_availability.feature
mod/lesson/tests/behat/lesson_student_resume.feature
mod/quiz/backup/moodle2/restore_quiz_activity_task.class.php
mod/quiz/locallib.php
mod/quiz/renderer.php
mod/scorm/classes/external.php
mod/scorm/datamodels/scorm_12.php
mod/scorm/datamodels/scorm_13.php
mod/scorm/locallib.php
mod/scorm/module.js
mod/scorm/report/interactions/classes/report.php
mod/scorm/report/interactions/lang/en/scormreport_interactions.php
mod/scorm/report/interactions/responsessettings_form.php
mod/scorm/report/interactions/version.php
mod/scorm/tests/externallib_test.php
mod/scorm/tests/packages/complexscorm.zip [new file with mode: 0644]
mod/scorm/version.php
mod/wiki/classes/external.php
mod/wiki/db/services.php
mod/wiki/lib.php
mod/wiki/prettyview.php
mod/wiki/tests/externallib_test.php
mod/wiki/tests/lib_test.php [new file with mode: 0644]
mod/wiki/version.php
mod/wiki/view.php
npm-shrinkwrap.json [new file with mode: 0644]
package.json
question/engine/renderer.php
question/tests/behat/behat_question.php
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd-debug.js
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd-min.js
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd.js
question/type/ddimageortext/yui/src/ddimageortext/js/ddimageortext.js
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-debug.js
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-min.js
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd.js
question/type/ddwtos/yui/src/ddwtos/js/ddwtos.js
question/type/multianswer/edit_multianswer_form.php
user/message.html
user/messageselect.php
version.php

index 08d8338..afae285 100644 (file)
@@ -58,6 +58,9 @@ cache:
       - $HOME/.composer/cache
 
 install:
+    # Disable xdebug. We aren't generating code coverage, and it has a huge impact upon test performance.
+    - rm /home/travis/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini
+
     # Set the encrypted GITHUB_TOKEN if it's available to raise the API limit.
     - if [ -n "$GITHUB_APITOKEN" ]; then composer config github-oauth.github.com $GITHUB_APITOKEN; fi
 
index 585d9b4..bc2c2b6 100644 (file)
@@ -89,11 +89,12 @@ class restore_logs_processor {
             }
             // Arrived here log is empty, no rule was able to perform the conversion, log the problem
             if (empty($newlog)) {
-                self::$task->log('Log module-action "' . $keyname . '" process problem. Not restored', backup::LOG_DEBUG);
+                self::$task->log('Log module-action "' . $keyname . '" process problem. Not restored. ' .
+                    json_encode($log), backup::LOG_DEBUG);
             }
 
         } else { // Action not found log the problem
-            self::$task->log('Log module-action "' . $keyname . '" unknown. Not restored', backup::LOG_DEBUG);
+            self::$task->log('Log module-action "' . $keyname . '" unknown. Not restored. '.json_encode($log), backup::LOG_DEBUG);
             $newlog = false;
 
         }
index 5d8cb55..e18ada4 100644 (file)
@@ -183,6 +183,10 @@ abstract class backup_structure_step extends backup_step {
      * @return void
      */
     protected function add_subplugin_structure($subplugintype, $element, $multiple, $plugintype = null, $pluginname = null) {
+        global $CFG;
+        // This global declaration is required, because where we do require_once($backupfile);
+        // That file may in turn try to do require_once($CFG->dirroot ...).
+        // That worked in the past, we should keep it working.
 
         // Verify if this is a BC call for an activity backup. See NOTE above for this special case.
         if ($plugintype === null and $pluginname === null) {
index de7b002..b59ac82 100644 (file)
@@ -313,6 +313,10 @@ abstract class restore_structure_step extends restore_step {
      * @return void
      */
     protected function add_subplugin_structure($subplugintype, $element, $plugintype = null, $pluginname = null) {
+        global $CFG;
+        // This global declaration is required, because where we do require_once($backupfile);
+        // That file may in turn try to do require_once($CFG->dirroot ...).
+        // That worked in the past, we should keep it working.
 
         // Verify if this is a BC call for an activity restore. See NOTE above for this special case.
         if ($plugintype === null and $pluginname === null) {
index 14d4c87..634c511 100644 (file)
@@ -132,8 +132,12 @@ if ($action === 'delete') {
             redirect($returnurl);
         }
     } else if (blog_user_can_edit_entry($entry)) {
-        $optionsyes = array('entryid'=>$id, 'action'=>'delete', 'confirm'=>1, 'sesskey'=>sesskey(), 'courseid'=>$courseid);
-        $optionsno = array('userid'=>$entry->userid, 'courseid'=>$courseid);
+        $optionsyes = array('entryid' => $id,
+                            'action' => 'delete',
+                            'confirm' => 1,
+                            'sesskey' => sesskey(),
+                            'courseid' => $courseid);
+        $optionsno = array('userid' => $entry->userid, 'courseid' => $courseid);
         $PAGE->set_title("$SITE->shortname: $strblogs");
         $PAGE->set_heading($SITE->fullname);
         echo $OUTPUT->header();
@@ -182,9 +186,9 @@ if (!empty($entry->id)) {
 }
 
 require_once('edit_form.php');
-$summaryoptions = array('maxfiles'=> 99, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>true, 'context'=>$sitecontext,
-    'subdirs'=>file_area_contains_subdirs($sitecontext, 'blog', 'post', $entry->id));
-$attachmentoptions = array('subdirs'=>false, 'maxfiles'=> 99, 'maxbytes'=>$CFG->maxbytes);
+$summaryoptions = array('maxfiles' => 99, 'maxbytes' => $CFG->maxbytes, 'trusttext' => true, 'context' => $sitecontext,
+    'subdirs' => file_area_contains_subdirs($sitecontext, 'blog', 'post', $entry->id));
+$attachmentoptions = array('subdirs' => false, 'maxfiles' => 99, 'maxbytes' => $CFG->maxbytes);
 
 $blogeditform = new blog_edit_form(null, compact('entry',
                                                  'summaryoptions',
index 4b0d408..6595848 100644 (file)
@@ -94,7 +94,7 @@ if (!empty($blogs)) {
         $editurl = new moodle_url('/blog/external_blog_edit.php', array('id' => $blog->id));
         $editicon = $OUTPUT->action_icon($editurl, new pix_icon('t/edit', get_string('editexternalblog', 'blog')));
 
-        $deletelink = new moodle_url('/blog/external_blogs.php', array('delete' => $blog->id, 'sesskey'=>sesskey()));
+        $deletelink = new moodle_url('/blog/external_blogs.php', array('delete' => $blog->id, 'sesskey' => sesskey()));
         $deleteicon = $OUTPUT->action_icon($deletelink, new pix_icon('t/delete', get_string('deleteexternalblog', 'blog')));
 
         $table->data[] = new html_table_row(array($blog->name,
index 56ab7eb..bc3dd1e 100644 (file)
@@ -40,13 +40,13 @@ $search   = optional_param('search', null, PARAM_RAW);
 
 comment::init();
 
-$url_params = compact('id', 'start', 'tag', 'userid', 'tagid', 'modid', 'entryid', 'groupid', 'courseid', 'search');
-foreach ($url_params as $var => $val) {
+$urlparams = compact('id', 'start', 'tag', 'userid', 'tagid', 'modid', 'entryid', 'groupid', 'courseid', 'search');
+foreach ($urlparams as $var => $val) {
     if (empty($val)) {
-        unset($url_params[$var]);
+        unset($urlparams[$var]);
     }
 }
-$PAGE->set_url('/blog/index.php', $url_params);
+$PAGE->set_url('/blog/index.php', $urlparams);
 
 // Correct tagid if a text tag is provided as a param.
 if (!empty($tag)) {
@@ -121,7 +121,7 @@ if (!empty($groupid) && empty($courseid)) {
 
 if (!$userid && has_capability('moodle/blog:view', $sitecontext) && $CFG->bloglevel > BLOG_USER_LEVEL) {
     if ($entryid) {
-        if (!$entryobject = $DB->get_record('post', array('id'=>$entryid))) {
+        if (!$entryobject = $DB->get_record('post', array('id' => $entryid))) {
             print_error('nosuchentry', 'blog');
         }
         $userid = $entryobject->userid;
@@ -148,12 +148,12 @@ if ((empty($courseid) ? true : $courseid == SITEID) && empty($userid)) {
         print_error('cannotviewsiteblog', 'blog');
     }
 
-    $COURSE = $DB->get_record('course', array('format'=>'site'));
+    $COURSE = $DB->get_record('course', array('format' => 'site'));
     $courseid = $COURSE->id;
 }
 
 if (!empty($courseid)) {
-    if (!$course = $DB->get_record('course', array('id'=>$courseid))) {
+    if (!$course = $DB->get_record('course', array('id' => $courseid))) {
         print_error('invalidcourseid');
     }
 
@@ -176,7 +176,7 @@ if (!empty($groupid)) {
         print_error(get_string('invalidgroupid', 'blog'));
     }
 
-    if (!$course = $DB->get_record('course', array('id'=>$group->courseid))) {
+    if (!$course = $DB->get_record('course', array('id' => $group->courseid))) {
         print_error('invalidcourseid');
     }
 
@@ -200,7 +200,7 @@ if (!empty($userid)) {
         print_error('blogdisable', 'blog');
     }
 
-    if (!$user = $DB->get_record('user', array('id'=>$userid))) {
+    if (!$user = $DB->get_record('user', array('id' => $userid))) {
         print_error('invaliduserid');
     }
 
index f99ed31..00a1ffa 100644 (file)
@@ -332,9 +332,9 @@ function blog_get_all_options(moodle_page $page, stdClass $userid = null) {
             $userid = $page->context->instanceid;
         }
         // Check the userid var.
-        if (!is_null($userid) && $userid!==$USER->id) {
+        if (!is_null($userid) && $userid !== $USER->id) {
             // Load the user from the userid... it MUST EXIST throw a wobbly if it doesn't!
-            $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
+            $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
         } else {
             $user = null;
         }
@@ -350,7 +350,7 @@ function blog_get_all_options(moodle_page $page, stdClass $userid = null) {
         // Get the options for the user.
         if ($user !== null and !isguestuser($user)) {
             // Load for the requested user.
-            $options[CONTEXT_USER+1] = blog_get_options_for_user($user);
+            $options[CONTEXT_USER + 1] = blog_get_options_for_user($user);
         }
         // Load for the current user.
         if (isloggedin() and !isguestuser()) {
@@ -413,7 +413,7 @@ function blog_get_options_for_user(stdClass $user=null) {
         // Not the current user, but we can view and its blogs are enabled for SITE or GLOBAL.
         $options['userentries'] = array(
             'string' => get_string('viewuserentries', 'blog', fullname($user)),
-            'link' => new moodle_url('/blog/index.php', array('userid'=>$user->id))
+            'link' => new moodle_url('/blog/index.php', array('userid' => $user->id))
         );
     } else {
         // It's the current user.
@@ -421,14 +421,14 @@ function blog_get_options_for_user(stdClass $user=null) {
             // We can view our own blogs .... BIG surprise.
             $options['view'] = array(
                 'string' => get_string('blogentries', 'blog'),
-                'link' => new moodle_url('/blog/index.php', array('userid'=>$USER->id))
+                'link' => new moodle_url('/blog/index.php', array('userid' => $USER->id))
             );
         }
         if (has_capability('moodle/blog:create', $sitecontext)) {
             // We can add to our own blog.
             $options['add'] = array(
                 'string' => get_string('addnewentry', 'blog'),
-                'link' => new moodle_url('/blog/edit.php', array('action'=>'add'))
+                'link' => new moodle_url('/blog/edit.php', array('action' => 'add'))
             );
         }
     }
@@ -560,13 +560,13 @@ function blog_get_options_for_module($module, $user=null) {
             $a->type = $modulename;
             $options['moduleview'] = array(
                 'string' => get_string('viewallmodentries', 'blog', $a),
-                'link' => new moodle_url('/blog/index.php', array('modid'=>$module->id))
+                'link' => new moodle_url('/blog/index.php', array('modid' => $module->id))
             );
         }
         // View MY entries about this module.
         $options['moduleviewmine'] = array(
             'string' => get_string('viewmyentriesaboutmodule', 'blog', $modulename),
-            'link' => new moodle_url('/blog/index.php', array('modid'=>$module->id, 'userid'=>$USER->id))
+            'link' => new moodle_url('/blog/index.php', array('modid' => $module->id, 'userid' => $USER->id))
         );
         if (!empty($user) && ($CFG->bloglevel >= BLOG_SITE_LEVEL)) {
             // View the given users entries about this module.
@@ -575,7 +575,7 @@ function blog_get_options_for_module($module, $user=null) {
             $a->user = fullname($user);
             $options['moduleviewuser'] = array(
                 'string' => get_string('blogentriesbyuseraboutmodule', 'blog', $a),
-                'link' => new moodle_url('/blog/index.php', array('modid'=>$module->id, 'userid'=>$user->id))
+                'link' => new moodle_url('/blog/index.php', array('modid' => $module->id, 'userid' => $user->id))
             );
         }
     }
@@ -584,7 +584,7 @@ function blog_get_options_for_module($module, $user=null) {
         // The user can blog about this module.
         $options['moduleadd'] = array(
             'string' => get_string('blogaboutthismodule', 'blog', $modulename),
-            'link' => new moodle_url('/blog/edit.php', array('action'=>'add', 'modid'=>$module->id))
+            'link' => new moodle_url('/blog/edit.php', array('action' => 'add', 'modid' => $module->id))
         );
     }
     // Cache the options.
@@ -673,7 +673,7 @@ function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=nu
 
         $headers['filters']['module'] = $modid;
         // A groupid param may conflict with this coursemod's courseid. Ignore groupid in that case.
-        $courseid = $DB->get_field('course_modules', 'course', array('id'=>$modid));
+        $courseid = $DB->get_field('course_modules', 'course', array('id' => $modid));
         $course = $DB->get_record('course', array('id' => $courseid));
         $cm = $DB->get_record('course_modules', array('id' => $modid));
         $cm->modname = $DB->get_field('modules', 'name', array('id' => $cm->module));
@@ -861,7 +861,7 @@ function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=nu
         if (!empty($tagid)) {
             $headers['filters']['tag'] = $tagid;
             $blogurl->param('tagid', $tagid);
-            $tagrec = $DB->get_record('tag', array('id'=>$tagid));
+            $tagrec = $DB->get_record('tag', array('id' => $tagid));
             $PAGE->navbar->add($tagrec->name, $blogurl);
         } else if (!empty($tag)) {
             if ($tagrec = $DB->get_record('tag', array('name' => $tag))) {
@@ -921,7 +921,7 @@ function blog_get_associated_count($courseid, $cmid=null) {
  * @package  core_blog
  * @category comment
  *
- * @param stdClass $comment_param {
+ * @param stdClass $commentparam {
  *              context  => context the context object
  *              courseid => int course id
  *              cm       => stdClass course module object
@@ -930,11 +930,11 @@ function blog_get_associated_count($courseid, $cmid=null) {
  * }
  * @return array
  */
-function blog_comment_permissions($comment_param) {
+function blog_comment_permissions($commentparam) {
     global $DB;
 
     // If blog is public and current user is guest, then don't let him post comments.
-    $blogentry = $DB->get_record('post', array('id' => $comment_param->itemid), 'publishstate', MUST_EXIST);
+    $blogentry = $DB->get_record('post', array('id' => $commentparam->itemid), 'publishstate', MUST_EXIST);
 
     if ($blogentry->publishstate != 'public') {
         if (!isloggedin() || isguestuser()) {
@@ -959,7 +959,7 @@ function blog_comment_permissions($comment_param) {
  * }
  * @return boolean
  */
-function blog_comment_validate($comment_param) {
+function blog_comment_validate($commentparam) {
     global $CFG, $DB, $USER;
 
     // Check if blogs are enabled user can comment.
@@ -968,22 +968,22 @@ function blog_comment_validate($comment_param) {
     }
 
     // Validate comment area.
-    if ($comment_param->commentarea != 'format_blog') {
+    if ($commentparam->commentarea != 'format_blog') {
         throw new comment_exception('invalidcommentarea');
     }
 
-    $blogentry = $DB->get_record('post', array('id' => $comment_param->itemid), '*', MUST_EXIST);
+    $blogentry = $DB->get_record('post', array('id' => $commentparam->itemid), '*', MUST_EXIST);
 
     // Validation for comment deletion.
-    if (!empty($comment_param->commentid)) {
-        if ($record = $DB->get_record('comments', array('id'=>$comment_param->commentid))) {
+    if (!empty($commentparam->commentid)) {
+        if ($record = $DB->get_record('comments', array('id' => $commentparam->commentid))) {
             if ($record->commentarea != 'format_blog') {
                 throw new comment_exception('invalidcommentarea');
             }
-            if ($record->contextid != $comment_param->context->id) {
+            if ($record->contextid != $commentparam->context->id) {
                 throw new comment_exception('invalidcontext');
             }
-            if ($record->itemid != $comment_param->itemid) {
+            if ($record->itemid != $commentparam->itemid) {
                 throw new comment_exception('invalidcommentitemid');
             }
         } else {
@@ -1005,10 +1005,10 @@ function blog_comment_validate($comment_param) {
  */
 function blog_page_type_list($pagetype, $parentcontext, $currentcontext) {
     return array(
-        '*'=>get_string('page-x', 'pagetype'),
-        'blog-*'=>get_string('page-blog-x', 'blog'),
-        'blog-index'=>get_string('page-blog-index', 'blog'),
-        'blog-edit'=>get_string('page-blog-edit', 'blog')
+        '*' => get_string('page-x', 'pagetype'),
+        'blog-*' => get_string('page-blog-x', 'blog'),
+        'blog-index' => get_string('page-blog-index', 'blog'),
+        'blog-edit' => get_string('page-blog-edit', 'blog')
     );
 }
 
index 114e38e..7968483 100644 (file)
@@ -112,7 +112,7 @@ class blog_entry implements renderable {
 
         $this->renderable = new StdClass();
 
-        $this->renderable->user = $DB->get_record('user', array('id'=>$this->userid));
+        $this->renderable->user = $DB->get_record('user', array('id' => $this->userid));
 
         // Entry comments.
         if (!empty($CFG->usecomments) and $CFG->blogusecomments) {
@@ -163,7 +163,7 @@ class blog_entry implements renderable {
                     $associations[$key]->contextlevel = $context->contextlevel;
 
                     // Course associations.
-                    if ($context->contextlevel ==  CONTEXT_COURSE) {
+                    if ($context->contextlevel == CONTEXT_COURSE) {
                         // TODO: performance!!!!
                         $instancename = $DB->get_field('course', 'shortname', array('id' => $context->instanceid));
 
@@ -174,7 +174,7 @@ class blog_entry implements renderable {
                     }
 
                     // Mod associations.
-                    if ($context->contextlevel ==  CONTEXT_MODULE) {
+                    if ($context->contextlevel == CONTEXT_MODULE) {
 
                         // Getting the activity type and the activity instance id.
                         $sql = 'SELECT cm.instance, m.name FROM {course_modules} cm
@@ -653,7 +653,7 @@ class blog_listing {
                 $assocexists = $DB->record_exists('blog_association', array());
 
                 // Begin permission sql clause.
-                $permissionsql =  '(p.userid = ? ';
+                $permissionsql = '(p.userid = ? ';
                 $params[] = $userid;
 
                 if ($CFG->bloglevel >= BLOG_SITE_LEVEL) { // Add permission to view site-level entries.
index 832d4b1..1045a89 100644 (file)
@@ -141,10 +141,10 @@ class core_blog_renderer extends plugin_renderer_base {
             if ($officialtags) {
                 $o .= get_string('tags', 'tag') .': '. $this->output->container($officialtags, 'officialblogtags');
                 if ($defaulttags) {
-                    $o .=  ', ';
+                    $o .= ', ';
                 }
             }
-            $o .=  $defaulttags;
+            $o .= $defaulttags;
             $o .= $this->output->container_end();
         }
 
@@ -155,7 +155,7 @@ class core_blog_renderer extends plugin_renderer_base {
             $assocstr = '';
             $coursesarray = array();
             foreach ($entry->renderable->blogassociations as $assocrec) {
-                if ($assocrec->contextlevel ==  CONTEXT_COURSE) {
+                if ($assocrec->contextlevel == CONTEXT_COURSE) {
                     $coursesarray[] = $this->output->action_icon($assocrec->url, $assocrec->icon, null, array(), true);
                 }
             }
@@ -166,7 +166,7 @@ class core_blog_renderer extends plugin_renderer_base {
             // Now show mod association.
             $modulesarray = array();
             foreach ($entry->renderable->blogassociations as $assocrec) {
-                if ($assocrec->contextlevel ==  CONTEXT_MODULE) {
+                if ($assocrec->contextlevel == CONTEXT_MODULE) {
                     $str = get_string('associated', 'blog', $assocrec->type) . ': ';
                     $str .= $this->output->action_icon($assocrec->url, $assocrec->icon, null, array(), true);
                     $modulesarray[] = $str;
index 3f26eb0..cad38d8 100644 (file)
@@ -35,7 +35,7 @@ require_once($CFG->dirroot .'/blog/lib.php');
  * @param int    $tagid        The id of the row in the tag table that identifies the RSS Feed
  * @return string
  */
-function blog_rss_get_url($contextid, $userid, $filtertype, $filterselect=0, $tagid=0) {
+function blog_rss_get_url($contextid, $userid, $filtertype, $filterselect = 0, $tagid = 0) {
     $componentname = 'blog';
 
     $additionalargs = null;
@@ -70,7 +70,7 @@ function blog_rss_get_url($contextid, $userid, $filtertype, $filterselect=0, $ta
  * @param int         $tagid        The id of the row in the tag table that identifies the RSS Feed
  * @param string      $tooltiptext  The tooltip to be displayed with the link
  */
-function blog_rss_print_link($context, $filtertype, $filterselect=0, $tagid=0, $tooltiptext='') {
+function blog_rss_print_link($context, $filtertype, $filterselect = 0, $tagid = 0, $tooltiptext = '') {
     global $CFG, $USER, $OUTPUT;
 
     if (!isloggedin()) {
@@ -93,7 +93,7 @@ function blog_rss_print_link($context, $filtertype, $filterselect=0, $tagid=0, $
  * @param int         $filterselect The id of the item defined by $filtertype
  * @param int         $tagid        The id of the row in the tag table that identifies the RSS Feed
  */
-function blog_rss_add_http_header($context, $title, $filtertype, $filterselect=0, $tagid=0) {
+function blog_rss_add_http_header($context, $title, $filtertype, $filterselect = 0, $tagid = 0) {
     global $PAGE, $USER, $CFG;
 
     if (!isloggedin()) {
@@ -168,7 +168,7 @@ function blog_rss_get_feed($context, $args) {
     $type  = clean_param($args[3], PARAM_ALPHA);
     $id = clean_param($args[4], PARAM_INT);  // Could be groupid / courseid  / userid  depending on $type.
 
-    $tagid=0;
+    $tagid = 0;
     if ($args[5] != 'rss.xml') {
         $tagid = clean_param($args[5], PARAM_INT);
     } else {
@@ -234,10 +234,10 @@ function blog_rss_get_feed($context, $args) {
 
     switch ($type) {
         case 'user':
-            $info = fullname($DB->get_record('user', array('id'=>$id), 'firstname,lastname'));
+            $info = fullname($DB->get_record('user', array('id' => $id), 'firstname,lastname'));
             break;
         case 'course':
-            $info = $DB->get_field('course', 'fullname', array('id'=>$id));
+            $info = $DB->get_field('course', 'fullname', array('id' => $id));
             $info = format_string($info, true, array('context' => context_course::instance($id)));
             break;
         case 'site':
@@ -245,7 +245,7 @@ function blog_rss_get_feed($context, $args) {
             break;
         case 'group':
             $group = groups_get_group($id);
-            $info = $group->name; // TODO: $DB->get_field('groups', 'name', array('id'=>$id)).
+            $info = $group->name; // TODO: $DB->get_field('groups', 'name', array('id' => $id)).
             break;
         default:
             $info = '';
@@ -253,7 +253,7 @@ function blog_rss_get_feed($context, $args) {
     }
 
     if ($tagid) {
-        $info .= ': '.$DB->get_field('tags', 'text', array('id'=>$tagid));
+        $info .= ': '.$DB->get_field('tags', 'text', array('id' => $tagid));
     }
 
     $header = rss_standard_header(get_string($type.'blog', 'blog', $info),
@@ -279,7 +279,7 @@ function blog_rss_get_feed($context, $args) {
  * @param int    $tagid The id of the row in the tag table that identifies the RSS Feed
  * @return string
  */
-function blog_rss_file_name($type, $id, $tagid=0) {
+function blog_rss_file_name($type, $id, $tagid = 0) {
     global $CFG;
 
     if ($tagid) {
@@ -298,7 +298,7 @@ function blog_rss_file_name($type, $id, $tagid=0) {
  * @param string $contents The contents of the RSS Feed file
  * @return bool whether the save was successful or not
  */
-function blog_rss_save_file($type, $id, $tagid=0, $contents='') {
+function blog_rss_save_file($type, $id, $tagid = 0, $contents = '') {
     global $CFG;
 
     $status = true;
index a0589c2..73c81f2 100644 (file)
@@ -46,9 +46,9 @@ class core_bloglib_testcase extends advanced_testcase {
         $this->resetAfterTest();
 
         // Create default course.
-        $course = $this->getDataGenerator()->create_course(array('category'=>1, 'shortname'=>'ANON'));
+        $course = $this->getDataGenerator()->create_course(array('category' => 1, 'shortname' => 'ANON'));
         $this->assertNotEmpty($course);
-        $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
+        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
         $this->assertNotEmpty($page);
 
         // Create default group.
@@ -58,7 +58,11 @@ class core_bloglib_testcase extends advanced_testcase {
         $group->id = $DB->insert_record('groups', $group);
 
         // Create default user.
-        $user = $this->getDataGenerator()->create_user(array('username'=>'testuser', 'firstname'=>'Jimmy', 'lastname'=>'Kinnon'));
+        $user = $this->getDataGenerator()->create_user(array(
+                'username' => 'testuser',
+                'firstname' => 'Jimmy',
+                'lastname' => 'Kinnon'
+        ));
 
         // Create default tag.
         $tag = new stdClass();
@@ -91,35 +95,35 @@ class core_bloglib_testcase extends advanced_testcase {
         // Try all the filters at once: Only the entry filter is active.
         $filters = array('site' => $SITE->id, 'course' => $this->courseid, 'module' => $this->cmid,
             'group' => $this->groupid, 'user' => $this->userid, 'tag' => $this->tagid, 'entry' => $this->postid);
-        $blog_listing = new blog_listing($filters);
-        $this->assertFalse(array_key_exists('site', $blog_listing->filters));
-        $this->assertFalse(array_key_exists('course', $blog_listing->filters));
-        $this->assertFalse(array_key_exists('module', $blog_listing->filters));
-        $this->assertFalse(array_key_exists('group', $blog_listing->filters));
-        $this->assertFalse(array_key_exists('user', $blog_listing->filters));
-        $this->assertFalse(array_key_exists('tag', $blog_listing->filters));
-        $this->assertTrue(array_key_exists('entry', $blog_listing->filters));
+        $bloglisting = new blog_listing($filters);
+        $this->assertFalse(array_key_exists('site', $bloglisting->filters));
+        $this->assertFalse(array_key_exists('course', $bloglisting->filters));
+        $this->assertFalse(array_key_exists('module', $bloglisting->filters));
+        $this->assertFalse(array_key_exists('group', $bloglisting->filters));
+        $this->assertFalse(array_key_exists('user', $bloglisting->filters));
+        $this->assertFalse(array_key_exists('tag', $bloglisting->filters));
+        $this->assertTrue(array_key_exists('entry', $bloglisting->filters));
 
         // Again, but without the entry filter: This time, the tag, user and module filters are active.
         $filters = array('site' => $SITE->id, 'course' => $this->courseid, 'module' => $this->cmid,
             'group' => $this->groupid, 'user' => $this->userid, 'tag' => $this->postid);
-        $blog_listing = new blog_listing($filters);
-        $this->assertFalse(array_key_exists('site', $blog_listing->filters));
-        $this->assertFalse(array_key_exists('course', $blog_listing->filters));
-        $this->assertFalse(array_key_exists('group', $blog_listing->filters));
-        $this->assertTrue(array_key_exists('module', $blog_listing->filters));
-        $this->assertTrue(array_key_exists('user', $blog_listing->filters));
-        $this->assertTrue(array_key_exists('tag', $blog_listing->filters));
+        $bloglisting = new blog_listing($filters);
+        $this->assertFalse(array_key_exists('site', $bloglisting->filters));
+        $this->assertFalse(array_key_exists('course', $bloglisting->filters));
+        $this->assertFalse(array_key_exists('group', $bloglisting->filters));
+        $this->assertTrue(array_key_exists('module', $bloglisting->filters));
+        $this->assertTrue(array_key_exists('user', $bloglisting->filters));
+        $this->assertTrue(array_key_exists('tag', $bloglisting->filters));
 
         // We should get the same result by removing the 3 inactive filters: site, course and group.
         $filters = array('module' => $this->cmid, 'user' => $this->userid, 'tag' => $this->tagid);
-        $blog_listing = new blog_listing($filters);
-        $this->assertFalse(array_key_exists('site', $blog_listing->filters));
-        $this->assertFalse(array_key_exists('course', $blog_listing->filters));
-        $this->assertFalse(array_key_exists('group', $blog_listing->filters));
-        $this->assertTrue(array_key_exists('module', $blog_listing->filters));
-        $this->assertTrue(array_key_exists('user', $blog_listing->filters));
-        $this->assertTrue(array_key_exists('tag', $blog_listing->filters));
+        $bloglisting = new blog_listing($filters);
+        $this->assertFalse(array_key_exists('site', $bloglisting->filters));
+        $this->assertFalse(array_key_exists('course', $bloglisting->filters));
+        $this->assertFalse(array_key_exists('group', $bloglisting->filters));
+        $this->assertTrue(array_key_exists('module', $bloglisting->filters));
+        $this->assertTrue(array_key_exists('user', $bloglisting->filters));
+        $this->assertTrue(array_key_exists('tag', $bloglisting->filters));
 
     }
 
index c1ce408..0a73901 100644 (file)
@@ -37,12 +37,9 @@ Feature: Allow teachers to manually mark users as complete when configured
     And I follow "View course report"
     And I should see "Student First"
     And I follow "Click to mark user complete"
-    # Running cron just after clicking sometimes fail, so navigate back
-    # and ensure the student completion is updated before running cron.
-    And I am on site homepage
-    And I follow "Completion course"
-    And I follow "View course report"
-    And "//img[contains(@alt, 'Completed')]" "xpath_element" should exist in the "student1" "table_row"
+    # Running cron just after clicking sometimes fail, as record
+    # should be created before the cron runs.
+    And I wait "1" seconds
     And I trigger cron
     And I am on site homepage
     And I log out
index c22c65e..d25e14a 100644 (file)
@@ -264,9 +264,9 @@ class enrol_database_plugin extends enrol_plugin {
                   FROM {enrol} e
                   JOIN {course} c ON c.id = e.courseid
                   JOIN {role_assignments} ra ON ra.itemid = e.id
-             LEFT JOIN {user_enrolments} ue ON ue.enrolid = e.id
+             LEFT JOIN {user_enrolments} ue ON ue.enrolid = e.id AND ue.userid = ra.userid
                  WHERE ra.userid = :userid AND e.enrol = 'database'";
-        $rs = $DB->get_recordset_sql($sql, array('userid'=>$user->id));
+        $rs = $DB->get_recordset_sql($sql, array('userid' => $user->id));
         foreach ($rs as $instance) {
             if (!$instance->cvisible and $ignorehidden) {
                 continue;
diff --git a/enrol/guest/classes/external.php b/enrol/guest/classes/external.php
new file mode 100644 (file)
index 0000000..a441ef8
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Guest enrolment method external API
+ *
+ * @package    enrol_guest
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.1
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . '/externallib.php');
+require_once($CFG->libdir . '/enrollib.php');
+
+/**
+ * Guest enrolment method external API
+ *
+ * @package    enrol_guest
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.1
+ */
+class enrol_guest_external extends external_api {
+
+    /**
+     * Returns description of get_instance_info() parameters.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.1
+     */
+    public static function get_instance_info_parameters() {
+        return new external_function_parameters(
+                array('instanceid' => new external_value(PARAM_INT, 'Instance id of guest enrolment plugin.'))
+            );
+    }
+
+    /**
+     * Return guest enrolment instance information.
+     *
+     * @param int $instanceid instance id of guest enrolment plugin.
+     * @return array warnings and instance information.
+     * @since Moodle 3.1
+     */
+    public static function get_instance_info($instanceid) {
+        global $DB;
+
+        $params = self::validate_parameters(self::get_instance_info_parameters(), array('instanceid' => $instanceid));
+        $warnings = array();
+
+        // Retrieve guest enrolment plugin.
+        $enrolplugin = enrol_get_plugin('guest');
+        if (empty($enrolplugin)) {
+            throw new moodle_exception('invaliddata', 'error');
+        }
+
+        require_login(null, false, null, false, true);
+        $enrolinstance = $DB->get_record('enrol', array('id' => $params['instanceid']), '*', MUST_EXIST);
+
+        $course = $DB->get_record('course', array('id' => $enrolinstance->courseid), '*', MUST_EXIST);
+        $context = context_course::instance($course->id);
+        if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $context)) {
+            throw new moodle_exception('coursehidden');
+        }
+
+        $instanceinfo = $enrolplugin->get_enrol_info($enrolinstance);
+        // Specific instance information.
+        $instanceinfo->passwordrequired = $instanceinfo->requiredparam->passwordrequired;
+
+        unset($instanceinfo->requiredparam);
+
+        $result = array();
+        $result['instanceinfo'] = $instanceinfo;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of get_instance_info() result value.
+     *
+     * @return external_description
+     * @since Moodle 3.1
+     */
+    public static function get_instance_info_returns() {
+        return new external_single_structure(
+            array(
+                'instanceinfo' => new external_single_structure(
+                    array(
+                        'id' => new external_value(PARAM_INT, 'Id of course enrolment instance'),
+                        'courseid' => new external_value(PARAM_INT, 'Id of course'),
+                        'type' => new external_value(PARAM_PLUGIN, 'Type of enrolment plugin'),
+                        'name' => new external_value(PARAM_RAW, 'Name of enrolment plugin'),
+                        'status' => new external_value(PARAM_BOOL, 'Is the enrolment enabled?'),
+                        'passwordrequired' => new external_value(PARAM_BOOL, 'Is a password required?'),
+                    )
+                ),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+}
diff --git a/enrol/guest/db/services.php b/enrol/guest/db/services.php
new file mode 100644 (file)
index 0000000..9127483
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Guest enrolment external functions and service definitions.
+ *
+ * @package    enrol_guest
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.1
+ */
+
+$functions = array(
+
+    'enrol_guest_get_instance_info' => array(
+        'classname'   => 'enrol_guest_external',
+        'methodname'  => 'get_instance_info',
+        'description' => 'Return guest enrolment instance information.',
+        'type'        => 'read'
+    ),
+);
index 7c0bfd2..51e2edb 100644 (file)
@@ -391,4 +391,32 @@ class enrol_guest_plugin extends enrol_plugin {
         $fields['status']          = $this->get_config('status');
         return $fields;
     }
+
+    /**
+     * Return information for enrolment instance containing list of parameters required
+     * for enrolment, name of enrolment plugin etc.
+     *
+     * @param stdClass $instance enrolment instance
+     * @return stdClass instance info.
+     * @since Moodle 3.1
+     */
+    public function get_enrol_info(stdClass $instance) {
+
+        $instanceinfo = new stdClass();
+        $instanceinfo->id = $instance->id;
+        $instanceinfo->courseid = $instance->courseid;
+        $instanceinfo->type = $this->get_name();
+        $instanceinfo->name = $this->get_instance_name($instance);
+        $instanceinfo->status = $instance->status == ENROL_INSTANCE_ENABLED;
+
+        // Specifics enrolment method parameters.
+        $instanceinfo->requiredparam = new stdClass();
+        $instanceinfo->requiredparam->passwordrequired = !empty($instance->password);
+
+        // If the plugin is enabled, return the URL for obtaining more information.
+        if ($instanceinfo->status) {
+            $instanceinfo->wsfunction = 'enrol_guest_get_instance_info';
+        }
+        return $instanceinfo;
+    }
 }
diff --git a/enrol/guest/tests/external_test.php b/enrol/guest/tests/external_test.php
new file mode 100644 (file)
index 0000000..91193af
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Self enrol external PHPunit tests
+ *
+ * @package   enrol_guest
+ * @copyright 2015 Juan Leyva <juan@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     Moodle 3.1
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+/**
+ * Guest enrolment external functions tests
+ *
+ * @package    enrol_guest
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.1
+ */
+class enrol_guest_external_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test get_instance_info
+     */
+    public function test_get_instance_info() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Check if guest enrolment plugin is enabled.
+        $guestplugin = enrol_get_plugin('guest');
+        $this->assertNotEmpty($guestplugin);
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+        $coursedata = new stdClass();
+        $coursedata->visible = 0;
+        $course = self::getDataGenerator()->create_course($coursedata);
+
+        $student = self::getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
+
+        // Add enrolment methods for course.
+        $instance = $guestplugin->add_instance($course, array('status' => ENROL_INSTANCE_ENABLED,
+                                                                'name' => 'Test instance',
+                                                                'customint6' => 1,
+                                                                'roleid' => $studentrole->id));
+
+        $this->setAdminUser();
+        $result = enrol_guest_external::get_instance_info($instance);
+        $result = external_api::clean_returnvalue(enrol_guest_external::get_instance_info_returns(), $result);
+
+        $this->assertEquals($instance, $result['instanceinfo']['id']);
+        $this->assertEquals($course->id, $result['instanceinfo']['courseid']);
+        $this->assertEquals('guest', $result['instanceinfo']['type']);
+        $this->assertEquals('Test instance', $result['instanceinfo']['name']);
+        $this->assertTrue($result['instanceinfo']['status']);
+        $this->assertFalse($result['instanceinfo']['passwordrequired']);
+
+        $DB->set_field('enrol', 'status', ENROL_INSTANCE_DISABLED, array('id' => $instance));
+
+        $result = enrol_guest_external::get_instance_info($instance);
+        $result = external_api::clean_returnvalue(enrol_guest_external::get_instance_info_returns(), $result);
+        $this->assertEquals($instance, $result['instanceinfo']['id']);
+        $this->assertEquals($course->id, $result['instanceinfo']['courseid']);
+        $this->assertEquals('guest', $result['instanceinfo']['type']);
+        $this->assertEquals('Test instance', $result['instanceinfo']['name']);
+        $this->assertFalse($result['instanceinfo']['status']);
+        $this->assertFalse($result['instanceinfo']['passwordrequired']);
+
+        $DB->set_field('enrol', 'status', ENROL_INSTANCE_ENABLED, array('id' => $instance));
+
+        // Try to retrieve information using a normal user for a hidden course.
+        $user = self::getDataGenerator()->create_user();
+        $this->setUser($user);
+        try {
+            enrol_guest_external::get_instance_info($instance);
+        } catch (moodle_exception $e) {
+            $this->assertEquals('coursehidden', $e->errorcode);
+        }
+
+        // Student user.
+        $DB->set_field('course', 'visible', 1, array('id' => $course->id));
+        $this->setUser($student);
+        $result = enrol_guest_external::get_instance_info($instance);
+        $result = external_api::clean_returnvalue(enrol_guest_external::get_instance_info_returns(), $result);
+
+        $this->assertEquals($instance, $result['instanceinfo']['id']);
+        $this->assertEquals($course->id, $result['instanceinfo']['courseid']);
+        $this->assertEquals('guest', $result['instanceinfo']['type']);
+        $this->assertEquals('Test instance', $result['instanceinfo']['name']);
+        $this->assertTrue($result['instanceinfo']['status']);
+        $this->assertFalse($result['instanceinfo']['passwordrequired']);
+    }
+}
index d916ed6..5076e7b 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111600;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015111601;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;        // Requires this Moodle version
 $plugin->component = 'enrol_guest';     // Full name of the plugin (used for diagnostics)
index b035bd3..af10050 100644 (file)
@@ -163,15 +163,17 @@ class enrol_self_edit_form extends moodleform {
         $checkpassword = false;
 
         if ($instance->id) {
-            if ($data['status'] == ENROL_INSTANCE_ENABLED) {
-                if ($instance->password !== $data['password']) {
-                    $checkpassword = true;
-                }
+            // Check the password if we are enabling the plugin again.
+            if (($instance->status == ENROL_INSTANCE_DISABLED) && ($data['status'] == ENROL_INSTANCE_ENABLED)) {
+                $checkpassword = true;
             }
-        } else {
-            if ($data['status'] == ENROL_INSTANCE_ENABLED) {
+
+            // Check the password if the instance is enabled and the password has changed.
+            if (($data['status'] == ENROL_INSTANCE_ENABLED) && ($instance->password !== $data['password'])) {
                 $checkpassword = true;
             }
+        } else {
+            $checkpassword = true;
         }
 
         if ($checkpassword) {
@@ -179,8 +181,8 @@ class enrol_self_edit_form extends moodleform {
             $policy  = $plugin->get_config('usepasswordpolicy');
             if ($require and trim($data['password']) === '') {
                 $errors['password'] = get_string('required');
-            } else if ($policy) {
-                $errmsg = '';//prevent eclipse warning
+            } else if (!empty($data['password']) && $policy) {
+                $errmsg = '';
                 if (!check_password_policy($data['password'], $errmsg)) {
                     $errors['password'] = $errmsg;
                 }
index 7ad82ef..fef3f6f 100644 (file)
@@ -685,6 +685,27 @@ class enrol_self_plugin extends enrol_plugin {
      */
     public function can_hide_show_instance($instance) {
         $context = context_course::instance($instance->courseid);
-        return has_capability('enrol/self:config', $context);
+
+        if (!has_capability('enrol/self:config', $context)) {
+            return false;
+        }
+
+        // If the instance is currently disabled, before it can be enabled,
+        // we must check whether the password meets the password policies.
+        if ($instance->status == ENROL_INSTANCE_DISABLED) {
+            if ($this->get_config('requirepassword')) {
+                if (empty($instance->password)) {
+                    return false;
+                }
+            }
+            // Only check the password if it is set.
+            if (!empty($instance->password) && $this->get_config('usepasswordpolicy')) {
+                if (!check_password_policy($instance->password, $errmsg)) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
     }
 }
index 7eb522e..e09f7e9 100644 (file)
 
             // run dvips (.dvi to .ps)
             $pathdvips = escapeshellarg(trim(get_config('filter_tex', 'pathdvips'), " '\""));
-            $command = "$pathdvips -E $dvi -o $ps";
+            $command = "$pathdvips -q -E $dvi -o $ps";
             if ($this->execute($command, $log )) {
                 return false;
             }
index 47b4578..be9461b 100644 (file)
@@ -35,11 +35,16 @@ $userid   = optional_param('userid', null, PARAM_INT);
 
 $defaulttype = $userid ? 'user' : 'select';
 
-$itemid   = optional_param('itemid', $userid, PARAM_INT);
+$itemid = optional_param('itemid', null, PARAM_INT);
 $itemtype = optional_param('item', $defaulttype, PARAM_TEXT);
 $page = optional_param('page', 0, PARAM_INT);
 $perpage = optional_param('perpage', 100, PARAM_INT);
 
+if (empty($itemid)) {
+    $itemid = $userid;
+    $itemtype = $defaulttype;
+}
+
 $courseparams = array('id' => $courseid);
 $PAGE->set_url(new moodle_url('/grade/report/singleview/index.php', $courseparams));
 $PAGE->set_pagelayout('incourse');
index 52508ae..6b87350 100644 (file)
@@ -32,8 +32,8 @@ defined('MOODLE_INTERNAL') || die();
 
 $string['admindirname'] = 'ספריית מנהל המערכת';
 $string['availablelangs'] = 'חבילות שפה זמינות';
-$string['chooselanguagehead'] = '×\91×\97ר שפה';
-$string['chooselanguagesub'] = '×\90× ×\90 ×\91×\97ר שפה עבור ההתקנה בלבד. תוכל לבחור בשפה שונה לאתר ולמשתמש באחד מהמסכים הבאים.';
+$string['chooselanguagehead'] = '×\91×\97×\99רת שפה';
+$string['chooselanguagesub'] = '×\99ש ×\9c×\91×\97×\95ר שפה עבור ההתקנה בלבד. תוכל לבחור בשפה שונה לאתר ולמשתמש באחד מהמסכים הבאים.';
 $string['clialreadyconfigured'] = 'קובץ config.php כבר קיים, אנא השתמש ב
 admin/cli/install_database.php
 אם ברצונך להתקין את אתר זה.';
@@ -42,7 +42,7 @@ $string['clialreadyinstalled'] = 'קובץ ה-config.php קיים כבר, אנא
 $string['cliinstallheader'] = 'תוכנית התקנת Moodle {$a} בשורת הפקודה';
 $string['databasehost'] = 'מסד הנתונים המארח (host)';
 $string['databasename'] = 'שם מסד הנתונים';
-$string['databasetypehead'] = '×\91×\97ר התקן מסד הנתונים';
+$string['databasetypehead'] = '×\91×\97×\99רת התקן מסד הנתונים';
 $string['dataroot'] = 'ספריית הנתונים';
 $string['datarootpermission'] = 'הרשאות תיקיות bתונים (data)';
 $string['dbprefix'] = 'Tables prefix';
index fcffdff..e8ec5d9 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'Idioma';
+$string['moodlelogo'] = 'logotipo Moodle';
 $string['next'] = 'Próximo';
 $string['previous'] = 'Anterior';
 $string['reload'] = 'Recarregar';
index 4b85386..76921aa 100644 (file)
@@ -118,7 +118,7 @@ XPATH
 XPATH
         , 'question' => <<<XPATH
 .//div[contains(concat(' ', normalize-space(@class), ' '), ' que ')]
-    [contains(div[@class='content']/div[@class='formulation'], %locator%)]
+    [contains(div[@class='content']/div[contains(concat(' ', normalize-space(@class), ' '), ' formulation ')], %locator%)]
 XPATH
         , 'region' => <<<XPATH
 .//*[self::div | self::section | self::aside | self::header | self::footer][./@id = %locator%]
index 54902d9..21af3df 100644 (file)
@@ -1168,11 +1168,13 @@ $services = array(
             'core_enrol_get_users_courses',
             'core_enrol_get_enrolled_users',
             'core_enrol_get_course_enrolment_methods',
+            'enrol_guest_get_instance_info',
             'enrol_self_enrol_user',
             'enrol_self_get_instance_info',
             'core_user_get_users_by_id',
             'core_webservice_get_site_info',
             'core_notes_create_notes',
+            'core_notes_delete_notes',
             'core_user_get_course_user_profiles',
             'core_message_send_instant_messages',
             'mod_assign_get_grades',
@@ -1272,6 +1274,8 @@ $services = array(
             'mod_imscp_get_imscps_by_courses',
             'mod_glossary_get_glossaries_by_courses',
             'mod_wiki_get_wikis_by_courses',
+            'mod_wiki_view_wiki',
+            'mod_wiki_view_page',
             ),
         'enabled' => 0,
         'restrictedusers' => 0,
index 4540c7e..89792c6 100644 (file)
@@ -404,6 +404,14 @@ abstract class moodle_database {
             case SQL_QUERY_UPDATE:
             case SQL_QUERY_STRUCTURE:
                 $this->writes++;
+            default:
+                if ((PHPUNIT_TEST) || (defined('BEHAT_TEST') && BEHAT_TEST) ||
+                    defined('BEHAT_SITE_RUNNING')) {
+
+                    // Set list of tables that are updated.
+                    require_once(__DIR__.'/../testing/classes/util.php');
+                    testing_util::set_table_modified_by_sql($sql);
+                }
         }
 
         $this->print_debug($sql, $params);
@@ -577,6 +585,11 @@ abstract class moodle_database {
     protected function where_clause($table, array $conditions=null) {
         // We accept nulls in conditions
         $conditions = is_null($conditions) ? array() : $conditions;
+
+        if (empty($conditions)) {
+            return array('', array());
+        }
+
         // Some checks performed under debugging only
         if (debugging()) {
             $columns = $this->get_columns($table);
@@ -600,9 +613,6 @@ abstract class moodle_database {
         }
 
         $allowed_types = $this->allowed_param_types();
-        if (empty($conditions)) {
-            return array('', array());
-        }
         $where = array();
         $params = array();
 
index 8b16565..7e9dc55 100644 (file)
@@ -1432,9 +1432,9 @@ class core_dml_testcase extends database_driver_testcase {
                 $this->assertSame('ddltablenotexist', $e->errorcode);
             }
         }
-        // And without params.
+
         try {
-            $records = $DB->get_records('xxxx', array());
+            $records = $DB->get_records('xxxx', array('id' => '1'));
             $this->fail('An Exception is missing, expected due to query against non-existing table');
         } catch (moodle_exception $e) {
             $this->assertInstanceOf('dml_exception', $e);
@@ -5347,7 +5347,7 @@ class core_dml_testcase extends database_driver_testcase {
 
         // The get_records() method generates 2 queries the first time is called
         // as it is fetching the table structure.
-        $whatever = $DB->get_records($tablename);
+        $whatever = $DB->get_records($tablename, array('id' => '1'));
         $this->assertEquals($initreads + 3, $DB->perf_get_reads());
         $this->assertEquals($initwrites, $DB->perf_get_writes());
 
index 21c5cda..10f02f5 100644 (file)
Binary files a/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-debug.js and b/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-debug.js differ
index 21c5088..687aea1 100644 (file)
Binary files a/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-min.js and b/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-min.js differ
index 21c5cda..10f02f5 100644 (file)
Binary files a/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button.js and b/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button.js differ
index 88bf7c0..724c21d 100644 (file)
@@ -41,7 +41,8 @@ Y.namespace('M.atto_rtl').Button = Y.Base.create('button', Y.M.editor_atto.Edito
             title: direction,
             buttonName: direction,
             callback: this._toggleRTL,
-            callbackArgs: direction
+            callbackArgs: direction,
+            tags: '[dir=ltr]'
         });
 
         direction = 'rtl';
@@ -50,7 +51,8 @@ Y.namespace('M.atto_rtl').Button = Y.Base.create('button', Y.M.editor_atto.Edito
             title: direction,
             buttonName: direction,
             callback: this._toggleRTL,
-            callbackArgs: direction
+            callbackArgs: direction,
+            tags: '[dir=rtl]'
         });
     },
 
index 55c8b69..e9598eb 100644 (file)
@@ -2026,7 +2026,7 @@ function get_user_preferences($name = null, $default = null, $user = null) {
 // FUNCTIONS FOR HANDLING TIME.
 
 /**
- * Given date parts in user time produce a GMT timestamp.
+ * Given Gregorian date parts in user time produce a GMT timestamp.
  *
  * @package core
  * @category time
@@ -2208,7 +2208,7 @@ function date_format_string($date, $format, $tz = 99) {
 
 /**
  * Given a $time timestamp in GMT (seconds since epoch),
- * returns an array that represents the date in user time
+ * returns an array that represents the Gregorian date in user time
  *
  * @package core
  * @category time
index 7e85830..b44b279 100644 (file)
@@ -1386,28 +1386,23 @@ class html_writer {
         if (!$currenttime) {
             $currenttime = time();
         }
-        $currentdate = usergetdate($currenttime);
+        $calendartype = \core_calendar\type_factory::get_calendar_instance();
+        $currentdate = $calendartype->timestamp_to_date_array($currenttime);
         $userdatetype = $type;
         $timeunits = array();
 
         switch ($type) {
             case 'years':
-                for ($i=1970; $i<=2020; $i++) {
-                    $timeunits[$i] = $i;
-                }
+                $timeunits = $calendartype->get_years();
                 $userdatetype = 'year';
                 break;
             case 'months':
-                for ($i=1; $i<=12; $i++) {
-                    $timeunits[$i] = userdate(gmmktime(12,0,0,$i,15,2000), "%B");
-                }
+                $timeunits = $calendartype->get_months();
                 $userdatetype = 'month';
                 $currentdate['month'] = (int)$currentdate['mon'];
                 break;
             case 'days':
-                for ($i=1; $i<=31; $i++) {
-                    $timeunits[$i] = $i;
-                }
+                $timeunits = $calendartype->get_days();
                 $userdatetype = 'mday';
                 break;
             case 'hours':
index 2321e4a..0da8931 100644 (file)
@@ -207,7 +207,7 @@ class phpunit_util extends testing_util {
         reset_text_filters_cache(true);
         events_get_handlers('reset');
         core_text::reset_caches();
-        get_message_processors(false, true);
+        get_message_processors(false, true, true);
         filter_manager::reset_caches();
         core_filetypes::reset_caches();
 
diff --git a/lib/phpunit/tests/util_test.php b/lib/phpunit/tests/util_test.php
new file mode 100644 (file)
index 0000000..e59d95b
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * PHPUnit Util tests
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2015 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test util extra features.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2015 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_phpunit_util_testcase extends advanced_testcase {
+    /**
+     * @dataProvider set_table_modified_by_sql_provider
+     */
+    public function test_set_table_modified_by_sql($sql, $expectations) {
+        phpunit_util::reset_updated_table_list();
+        phpunit_util::set_table_modified_by_sql($sql);
+        foreach ($expectations as $table => $present) {
+            $this->assertEquals($present, !empty(phpunit_util::$tableupdated[$table]));
+        }
+    }
+
+    public function set_table_modified_by_sql_provider() {
+        global $DB;
+        $prefix = $DB->get_prefix();
+
+        return array(
+            'Basic update' => array(
+                'sql'           => "UPDATE {$prefix}user SET username = username || '_test'",
+                'expectations'  => array(
+                    'user'      => true,
+                    'course'    => false,
+                ),
+            ),
+            'Basic update with a fieldname sharing the same prefix' => array(
+                'sql'           => "UPDATE {$prefix}user SET {$prefix}username = username || '_test'",
+                'expectations'  => array(
+                    'user'      => true,
+                    'course'    => false,
+                ),
+            ),
+            'Basic update with a table which contains the prefix' => array(
+                'sql'           => "UPDATE {$prefix}user{$prefix} SET username = username || '_test'",
+                'expectations'  => array(
+                    "user{$prefix}" => true,
+                    'course'        => false,
+                ),
+            ),
+            'Update table with a numeric name' => array(
+                'sql'           => "UPDATE {$prefix}example42 SET username = username || '_test'",
+                'expectations'  => array(
+                    'example42' => true,
+                    'user'      => false,
+                    'course'    => false,
+                ),
+            ),
+            'Drop basic table' => array(
+                'sql'           => "DROP TABLE {$prefix}user",
+                'expectations'  => array(
+                    'user'      => true,
+                    'course'    => false,
+                ),
+            ),
+            'Drop table with a numeric name' => array(
+                'sql'           => "DROP TABLE {$prefix}example42",
+                'expectations'  => array(
+                    'example42' => true,
+                    'user'      => false,
+                    'course'    => false,
+                ),
+            ),
+            'Insert in table' => array(
+                'sql'           => "INSERT INTO {$prefix}user (username,password) VALUES ('moodle', 'test')",
+                'expectations'  => array(
+                    'user'      => true,
+                    'course'    => false,
+                ),
+            ),
+        );
+    }
+}
index 155704c..d20e639 100644 (file)
@@ -1091,7 +1091,10 @@ function workaround_max_input_vars() {
         return;
     }
 
-    if (count($_POST, COUNT_RECURSIVE) < $max) {
+    // Worst case is advanced checkboxes which use up to two max_input_vars
+    // slots for each entry in $_POST, because of sending two fields with the
+    // same name. So count everything twice just in case.
+    if (count($_POST, COUNT_RECURSIVE) * 2 < $max) {
         return;
     }
 
@@ -1107,6 +1110,15 @@ function workaround_max_input_vars() {
     $fun = create_function('$p', 'return implode("'.$delim.'", $p);');
     $chunks = array_map($fun, array_chunk(explode($delim, $str), $max));
 
+    // Clear everything from existing $_POST array, otherwise it might be included
+    // twice (this affects array params primarily).
+    foreach ($_POST as $key => $value) {
+        unset($_POST[$key]);
+        // Also clear from request array - but only the things that are in $_POST,
+        // that way it will leave the things from a get request if any.
+        unset($_REQUEST[$key]);
+    }
+
     foreach ($chunks as $chunk) {
         $values = array();
         parse_str($chunk, $values);
index 4fdd3c7..1017b1b 100644 (file)
@@ -59,6 +59,16 @@ abstract class testing_util {
      */
     protected static $tablestructure = null;
 
+    /**
+     * @var array keep list of sequenceid used in a table.
+     */
+    private static $tablesequences = array();
+
+    /**
+     * @var array list of updated tables.
+     */
+    public static $tableupdated = array();
+
     /**
      * @var array original structure of all database tables
      */
@@ -78,6 +88,7 @@ abstract class testing_util {
      * @var int next sequence value for a single test cycle.
      */
     protected static $sequencenextstartingid = null;
+
     /**
      * Return the name of the JSON file containing the init filenames.
      *
@@ -263,17 +274,15 @@ abstract class testing_util {
      * @return array  $table=>$records
      */
     protected static function get_tabledata() {
-        global $CFG;
-
-        $framework = self::get_framework();
+        if (!isset(self::$tabledata)) {
+            $framework = self::get_framework();
 
-        $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
-        if (!file_exists($datafile)) {
-            // Not initialised yet.
-            return array();
-        }
+            $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
+            if (!file_exists($datafile)) {
+                // Not initialised yet.
+                return array();
+            }
 
-        if (!isset(self::$tabledata)) {
             $data = file_get_contents($datafile);
             self::$tabledata = unserialize($data);
         }
@@ -291,17 +300,15 @@ abstract class testing_util {
      * @return array $table=>$records
      */
     public static function get_tablestructure() {
-        global $CFG;
-
-        $framework = self::get_framework();
+        if (!isset(self::$tablestructure)) {
+            $framework = self::get_framework();
 
-        $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
-        if (!file_exists($structurefile)) {
-            // Not initialised yet.
-            return array();
-        }
+            $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
+            if (!file_exists($structurefile)) {
+                // Not initialised yet.
+                return array();
+            }
 
-        if (!isset(self::$tablestructure)) {
             $data = file_get_contents($structurefile);
             self::$tablestructure = unserialize($data);
         }
@@ -361,11 +368,10 @@ abstract class testing_util {
                     // incorrect table match caused by _
                     continue;
                 }
-                if (!is_null($info->auto_increment)) {
+
+                if (!is_null($info->auto_increment) && $info->rows == 0 && ($info->auto_increment == 1)) {
                     $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
-                    if ($info->auto_increment == 1) {
-                        $empties[$table] = $table;
-                    }
+                    $empties[$table] = $table;
                 }
             }
             $rs->close();
@@ -418,9 +424,14 @@ abstract class testing_util {
      *
      * @static
      * @param array $records The records to use to determine the starting value for the table.
+     * @param string $table table name.
      * @return int The value the sequence should be set to.
      */
-    private static function get_next_sequence_starting_value($records) {
+    private static function get_next_sequence_starting_value($records, $table) {
+        if (isset(self::$tablesequences[$table])) {
+            return self::$tablesequences[$table];
+        }
+
         $id = self::$sequencenextstartingid;
 
         // If there are records, calculate the minimum id we can use.
@@ -431,6 +442,9 @@ abstract class testing_util {
         }
 
         self::$sequencenextstartingid = $id + 1000;
+
+        self::$tablesequences[$table] = $id;
+
         return $id;
     }
 
@@ -453,6 +467,8 @@ abstract class testing_util {
             return;
         }
 
+        $updatedtables = self::$tableupdated;
+
         // If all starting Id's are the same, it's difficult to detect coding and testing
         // errors that use the incorrect id in tests.  The classic case is cmid vs instance id.
         // To reduce the chance of the coding error, we start sequences at different values where possible.
@@ -469,8 +485,12 @@ abstract class testing_util {
             $queries = array();
             $prefix = $DB->get_prefix();
             foreach ($data as $table => $records) {
+                // If table is not modified then no need to do anything.
+                if (!isset($updatedtables[$table])) {
+                    continue;
+                }
                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
-                    $nextid = self::get_next_sequence_starting_value($records);
+                    $nextid = self::get_next_sequence_starting_value($records, $table);
                     $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
                 }
             }
@@ -479,6 +499,7 @@ abstract class testing_util {
             }
 
         } else if ($dbfamily === 'mysql') {
+            $queries = array();
             $sequences = array();
             $prefix = $DB->get_prefix();
             $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
@@ -496,19 +517,25 @@ abstract class testing_util {
             $rs->close();
             $prefix = $DB->get_prefix();
             foreach ($data as $table => $records) {
+                // If table is not modified then no need to do anything.
+                if (!isset($updatedtables[$table])) {
+                    continue;
+                }
                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
                     if (isset($sequences[$table])) {
-                        $nextid = self::get_next_sequence_starting_value($records);
+                        $nextid = self::get_next_sequence_starting_value($records, $table);
                         if ($sequences[$table] != $nextid) {
-                            $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid");
+                            $queries[] = "ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid";
                         }
-
                     } else {
                         // some problem exists, fallback to standard code
                         $DB->get_manager()->reset_sequence($table);
                     }
                 }
             }
+            if ($queries) {
+                $DB->change_database_structure(implode(';', $queries));
+            }
 
         } else if ($dbfamily === 'oracle') {
             $sequences = self::get_sequencenames();
@@ -526,6 +553,10 @@ abstract class testing_util {
             $rs->close();
 
             foreach ($data as $table => $records) {
+                // If table is not modified then no need to do anything.
+                if (!isset($updatedtables[$table])) {
+                    continue;
+                }
                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
                     $lastrecord = end($records);
                     if ($lastrecord) {
@@ -549,11 +580,12 @@ abstract class testing_util {
         } else {
             // note: does mssql support any kind of faster reset?
             // This also implies mssql will not use unique sequence values.
-            if (is_null($empties)) {
+            if (is_null($empties) and (empty($updatedtables))) {
                 $empties = self::guess_unmodified_empty_tables();
             }
             foreach ($data as $table => $records) {
-                if (isset($empties[$table])) {
+                // If table is not modified then no need to do anything.
+                if (isset($empties[$table]) or (!isset($updatedtables[$table]))) {
                     continue;
                 }
                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
@@ -586,7 +618,26 @@ abstract class testing_util {
             return false;
         }
 
-        $empties = self::guess_unmodified_empty_tables();
+        $empties = array();
+        // Use local copy of self::$tableupdated, as list gets updated in for loop.
+        $updatedtables = self::$tableupdated;
+
+        // If empty tablesequences list then it's the very first run.
+        if (empty(self::$tablesequences) && (($DB->get_dbfamily() != 'mysql') && ($DB->get_dbfamily() != 'postgres'))) {
+            // Only Mysql and Postgres support random sequence, so don't guess, just reset everything on very first run.
+            $empties = self::guess_unmodified_empty_tables();
+        }
+
+        // Check if any table has been modified by behat selenium process.
+        if (defined('BEHAT_SITE_RUNNING')) {
+            // Crazy way to reset :(.
+            $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
+            if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
+                self::$tableupdated = array_merge(self::$tableupdated, $tablesupdated);
+                unlink($tablesupdatedfile);
+            }
+            $updatedtables = self::$tableupdated;
+        }
 
         $borkedmysql = false;
         if ($DB->get_dbfamily() === 'mysql') {
@@ -627,6 +678,12 @@ abstract class testing_util {
         }
 
         foreach ($data as $table => $records) {
+            // If table is not modified then no need to do anything.
+            // $updatedtables tables is set after the first run, so check before checking for specific table update.
+            if (!empty($updatedtables) && !isset($updatedtables[$table])) {
+                continue;
+            }
+
             if ($borkedmysql) {
                 if (empty($records) and isset($empties[$table])) {
                     continue;
@@ -699,6 +756,8 @@ abstract class testing_util {
             }
         }
 
+        self::reset_updated_table_list();
+
         return true;
     }
 
@@ -833,6 +892,50 @@ abstract class testing_util {
         return $hash;
     }
 
+    /**
+     * Set state of modified tables.
+     *
+     * @param string $sql sql which is updating the table.
+     */
+    public static function set_table_modified_by_sql($sql) {
+        global $DB;
+
+        $prefix = $DB->get_prefix();
+
+        preg_match('/( ' . $prefix . '\w*)(.*)/', $sql, $matches);
+        // Ignore random sql for testing like "XXUPDATE SET XSSD".
+        if (!empty($matches[1])) {
+            $table = trim($matches[1]);
+            $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table);
+            self::$tableupdated[$table] = true;
+
+            if (defined('BEHAT_SITE_RUNNING')) {
+                $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
+                if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
+                    $tablesupdated[$table] = true;
+                } else {
+                    $tablesupdated[$table] = true;
+                }
+                @file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT));
+            }
+        }
+    }
+
+    /**
+     * Reset updated table list. This should be done after every reset.
+     */
+    public static function reset_updated_table_list() {
+        self::$tableupdated = array();
+    }
+
+    /**
+     * Returns the path to the file which holds list of tables updated in scenario.
+     * @return string
+     */
+    protected final static function get_tables_updated_by_scenario_list_path() {
+        return self::get_dataroot() . '/tablesupdatedbyscenario.txt';
+    }
+
     /**
      * Drop the whole test database
      * @static
index 7af3e2e..7dfe2a4 100644 (file)
@@ -895,7 +895,7 @@ EOD;
      *
      * @param int $userid
      * @param int $courseid
-     * @param int $roleid optional role id, use only with manual plugin
+     * @param int|string $roleidorshortname optional role id or role shortname, use only with manual plugin
      * @param string $enrol name of enrol plugin,
      *     there must be exactly one instance in course,
      *     it must support enrol_user() method.
@@ -904,9 +904,17 @@ EOD;
      * @param int $status (optional) default to ENROL_USER_ACTIVE for new enrolments
      * @return bool success
      */
-    public function enrol_user($userid, $courseid, $roleid = null, $enrol = 'manual', $timestart = 0, $timeend = 0, $status = null) {
+    public function enrol_user($userid, $courseid, $roleidorshortname = null, $enrol = 'manual',
+            $timestart = 0, $timeend = 0, $status = null) {
         global $DB;
 
+        // If role is specified by shortname, convert it into an id.
+        if (!is_numeric($roleidorshortname) && is_string($roleidorshortname)) {
+            $roleid = $DB->get_field('role', 'id', array('shortname' => $roleidorshortname), MUST_EXIST);
+        } else {
+            $roleid = $roleidorshortname;
+        }
+
         if (!$plugin = enrol_get_plugin($enrol)) {
             return false;
         }
index e90ab38..32cb7f3 100644 (file)
@@ -371,6 +371,7 @@ class core_test_generator_testcase extends advanced_testcase {
         $user1 = $this->getDataGenerator()->create_user();
         $user2 = $this->getDataGenerator()->create_user();
         $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
 
         $this->assertEquals(3, $DB->count_records('enrol', array('enrol'=>'self')));
         $instance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'self'), '*', MUST_EXIST);
@@ -402,6 +403,13 @@ class core_test_generator_testcase extends advanced_testcase {
         $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance2->id, 'userid'=>$user1->id)));
         $this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context2->id, 'userid'=>$user1->id, 'roleid'=>$teacherrole->id)));
 
+        $result = $this->getDataGenerator()->enrol_user($user4->id, $course2->id, 'teacher', 'manual');
+        $this->assertTrue($result);
+        $this->assertTrue($DB->record_exists('user_enrolments',
+                array('enrolid' => $maninstance2->id, 'userid' => $user4->id)));
+        $this->assertTrue($DB->record_exists('role_assignments',
+                array('contextid' => $context2->id, 'userid' => $user4->id, 'roleid' => $teacherrole->id)));
+
         $result = $this->getDataGenerator()->enrol_user($user1->id, $course3->id, 0, 'manual');
         $this->assertTrue($result);
         $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance3->id, 'userid'=>$user1->id)));
diff --git a/lib/tests/behat/largeforms.feature b/lib/tests/behat/largeforms.feature
new file mode 100644 (file)
index 0000000..8fdb790
--- /dev/null
@@ -0,0 +1,108 @@
+@core
+Feature: Forms with a large number of fields
+  In order to use certain forms on large Moodle installations
+  As an admin
+  I need forms to work with more fields than the PHP max_input_vars setting
+
+  Background:
+    # Get to the fixture page.
+    Given the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1        | topics |
+    And the following "activities" exist:
+      | activity   | name | intro                                                                   | course | idnumber |
+      | label      | L1   | <a href="../lib/tests/fixtures/max_input_vars.php">FixtureLink</a> | C1     | label1   |
+    When I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I follow "FixtureLink"
+
+  # Note: These tests do not actually use JavaScript but they don't work with
+  # the headless 'browser'.
+  @javascript
+  Scenario: Small form with checkboxes (not using workaround)
+    When I follow "Advanced checkboxes / Small"
+    And I press "Submit here!"
+    Then I should see "_qf__core_max_input_vars_form=1"
+    And I should see "mform_isexpanded_id_general=1"
+    And I should see "arraytest=[13,42]"
+    And I should see "array2test=[13,42]"
+    And I should see "submitbutton=Submit here!"
+    And I should see "Bulk checkbox success: true"
+
+  @javascript
+  Scenario: Medium length form with checkboxes (needs workaround)
+    When I follow "Advanced checkboxes / Below limit"
+    And I press "Submit here!"
+    Then I should see "_qf__core_max_input_vars_form=1"
+    And I should see "mform_isexpanded_id_general=1"
+    And I should see "arraytest=[13,42]"
+    And I should see "array2test=[13,42]"
+    And I should see "submitbutton=Submit here!"
+    And I should see "Bulk checkbox success: true"
+
+  @javascript
+  Scenario: Exact PHP limit length form with checkboxes (uses workaround but doesn't need it)
+    When I follow "Advanced checkboxes / Exact PHP limit"
+    And I press "Submit here!"
+    Then I should see "_qf__core_max_input_vars_form=1"
+    And I should see "mform_isexpanded_id_general=1"
+    And I should see "arraytest=[13,42]"
+    And I should see "array2test=[13,42]"
+    And I should see "submitbutton=Submit here!"
+    And I should see "Bulk checkbox success: true"
+
+  @javascript
+  Scenario: Longer than the limit with checkboxes (needs workaround)
+    When I follow "Advanced checkboxes / Above limit"
+    And I press "Submit here!"
+    Then I should see "_qf__core_max_input_vars_form=1"
+    And I should see "mform_isexpanded_id_general=1"
+    And I should see "arraytest=[13,42]"
+    And I should see "array2test=[13,42]"
+    And I should see "submitbutton=Submit here!"
+    And I should see "Bulk checkbox success: true"
+
+  @javascript
+  Scenario: Small form with array fields (not using workaround)
+    When I follow "Select options / Small"
+    And I press "Submit here!"
+    Then I should see "_qf__core_max_input_vars_form=1"
+    And I should see "mform_isexpanded_id_general=1"
+    And I should see "arraytest=[13,42]"
+    And I should see "array2test=[13,42]"
+    And I should see "submitbutton=Submit here!"
+    And I should see "Bulk array success: true"
+
+  @javascript
+  Scenario: Below limit form with array fields (uses workaround but doesn't need it)
+    When I follow "Select options / Below limit"
+    And I press "Submit here!"
+    Then I should see "_qf__core_max_input_vars_form=1"
+    And I should see "mform_isexpanded_id_general=1"
+    And I should see "arraytest=[13,42]"
+    And I should see "array2test=[13,42]"
+    And I should see "submitbutton=Submit here!"
+    And I should see "Bulk array success: true"
+
+  @javascript
+  Scenario: Exact PHP limit length form with array fields (uses workaround but doesn't need it)
+    When I follow "Select options / Exact PHP limit"
+    And I press "Submit here!"
+    Then I should see "_qf__core_max_input_vars_form=1"
+    And I should see "mform_isexpanded_id_general=1"
+    And I should see "arraytest=[13,42]"
+    And I should see "array2test=[13,42]"
+    And I should see "submitbutton=Submit here!"
+    And I should see "Bulk array success: true"
+
+  @javascript
+  Scenario: Longer than the limit with array fields (needs workaround)
+    When I follow "Select options / Above limit"
+    And I press "Submit here!"
+    Then I should see "_qf__core_max_input_vars_form=1"
+    And I should see "mform_isexpanded_id_general=1"
+    And I should see "arraytest=[13,42]"
+    And I should see "array2test=[13,42]"
+    And I should see "submitbutton=Submit here!"
+    And I should see "Bulk array success: true"
diff --git a/lib/tests/fixtures/max_input_vars.php b/lib/tests/fixtures/max_input_vars.php
new file mode 100644 (file)
index 0000000..36d97a2
--- /dev/null
@@ -0,0 +1,231 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Fixture for Behat test of the max_input_vars handling for large forms.
+ *
+ * @package core
+ * @copyright 2015 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/formslib.php');
+
+// Behat test fixture only.
+defined('BEHAT_SITE_RUNNING') || die('Only available on Behat test server');
+
+/**
+ * Form for testing max_input_vars.
+ *
+ * @copyright 2015 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_max_input_vars_form extends moodleform {
+    /**
+     * Form definition.
+     */
+    public function definition() {
+        global $CFG, $PAGE;
+
+        $mform =& $this->_form;
+
+        $mform->addElement('header', 'general', '');
+        $mform->addElement('hidden', 'type', $this->_customdata['type']);
+        $mform->setType('type', PARAM_ALPHA);
+
+        // This is similar to how the selects are created for the role tables,
+        // without using a Moodle form element.
+        $select = html_writer::select(array(13 => 'ArrayOpt13', 42 => 'ArrayOpt4', 666 => 'ArrayOpt666'),
+                'arraytest[]', array(13, 42), false, array('multiple' => 'multiple', 'size' => 10));
+        $mform->addElement('static', 'arraybit', $select);
+
+        switch ($this->_customdata['control']) {
+            case 'c' :
+                // Create a whole stack of checkboxes.
+                for ($i = 0; $i < $this->_customdata['fieldcount']; $i++) {
+                    $mform->addElement('advcheckbox', 'test_c' . $i, 'Checkbox ' . $i);
+                }
+                break;
+
+            case 'a' :
+                // Create a very large array input type field.
+                $options = array();
+                $values = array();
+                for ($i = 0; $i < $this->_customdata['fieldcount']; $i++) {
+                    $options[$i] = 'BigArray ' . $i;
+                    if ($i !== 3) {
+                        $values[] = $i;
+                    }
+                }
+                $select = html_writer::select($options,
+                        'test_a[]', $values, false, array('multiple' => 'multiple', 'size' => 50));
+                $mform->addElement('static', 'bigarraybit', $select);
+                break;
+        }
+
+        // For the sake of it, let's have a second array.
+        $select = html_writer::select(array(13 => 'Array2Opt13', 42 => 'Array2Opt4', 666 => 'Array2Opt666'),
+                'array2test[]', array(13, 42), false, array('multiple' => 'multiple', 'size' => 10));
+        $mform->addElement('static', 'array2bit', $select);
+
+        $mform->addElement('submit', 'submitbutton', 'Submit here!');
+    }
+}
+
+require_login();
+
+$context = context_system::instance();
+
+$type = optional_param('type', '', PARAM_ALPHA);
+
+// Set up the page details.
+$PAGE->set_url(new moodle_url('/lib/tests/fixtures/max_input_vars.php'));
+$PAGE->set_context($context);
+
+if ($type) {
+    // Make it work regardless of max_input_vars setting on server, within reason.
+    if ($type[1] === 's') {
+        // Small enough to definitely fit in the area.
+        $fieldcount = 10;
+    } else if ($type[1] === 'm') {
+        // Just under the limit (will go over for advancedcheckbox).
+        $fieldcount = (int)ini_get('max_input_vars') - 100;
+    } else if ($type[1] === 'e') {
+        // Exactly on the PHP limit, taking into account extra form fields
+        // and the double fields for checkboxes.
+        if ($type[0] === 'c') {
+            $fieldcount = (int)ini_get('max_input_vars') / 2 - 2;
+        } else {
+            $fieldcount = (int)ini_get('max_input_vars') - 11;
+        }
+    } else if ($type[1] === 'l') {
+        // Just over the limit.
+        $fieldcount = (int)ini_get('max_input_vars') + 100;
+    }
+
+    $mform = new core_max_input_vars_form('max_input_vars.php',
+            array('type' => $type, 'fieldcount' => $fieldcount, 'control' => $type[0]));
+    if ($type[0] === 'c') {
+        $data = array();
+        for ($i = 0; $i < $fieldcount; $i++) {
+            if ($i === 3) {
+                // Everything is set except number 3.
+                continue;
+            }
+            $data['test_c' . $i] = 1;
+        }
+        $mform->set_data($data);
+    }
+}
+
+echo $OUTPUT->header();
+
+if ($type && ($result = $mform->get_data())) {
+    $testc = array();
+    $testa = array();
+    foreach ($_POST as $key => $value) {
+        $matches = array();
+        // Handle the 'bulk' ones separately so we can show success/fail rather
+        // than outputting a thousand items; also makes it possible to Behat-test
+        // without depending on specific value of max_input_vars.
+        if (preg_match('~^test_c([0-9]+)$~', $key, $matches)) {
+            $testc[(int)$matches[1]] = $value;
+        } else if ($key === 'test_a') {
+            $testa = $value;
+        } else {
+            // Other fields are output straight off.
+            if (is_array($value)) {
+                echo html_writer::div(s($key) . '=[' . s(implode(',', $value)) . ']');
+            } else {
+                echo html_writer::div(s($key) . '=' . s($value));
+            }
+        }
+    }
+
+    // Confirm that the bulk results are correct.
+    switch ($type[0]) {
+        case 'c' :
+            $success = true;
+            for ($i = 0; $i < $fieldcount; $i++) {
+                if (!array_key_exists($i, $testc)) {
+                    $success = false;
+                    break;
+                }
+                if ($testc[$i] != ($i == 3 ? 0 : 1)) {
+                    $success = false;
+                    break;
+                }
+            }
+            if (array_key_exists($fieldcount, $testc)) {
+                $success = false;
+            }
+            // Check using Moodle form and _param functions too.
+            $key = 'test_c' . ($fieldcount - 1);
+            if (empty($result->{$key})) {
+                $success = false;
+            }
+            if (optional_param($key, 0, PARAM_INT) !== 1) {
+                $success = false;
+            }
+            echo html_writer::div('Bulk checkbox success: ' . ($success ? 'true' : 'false'));
+            break;
+
+        case 'a' :
+            $success = true;
+            for ($i = 0; $i < $fieldcount; $i++) {
+                if ($i === 3) {
+                    if (in_array($i, $testa)) {
+                        $success = false;
+                        break;
+                    }
+                } else {
+                    if (!in_array($i, $testa)) {
+                        $success = false;
+                        break;
+                    }
+                }
+            }
+            if (in_array($fieldcount, $testa)) {
+                $success = false;
+            }
+            // Check using Moodle _param function. The form does not include these
+            // fields so it won't be in the form result.
+            $array = optional_param_array('test_a', array(), PARAM_INT);
+            if ($array != $testa) {
+                $success = false;
+            }
+            echo html_writer::div('Bulk array success: ' . ($success ? 'true' : 'false'));
+            break;
+    }
+
+} else if ($type) {
+    $mform->display();
+}
+
+// Show links to each available type of test.
+echo html_writer::start_tag('ul');
+foreach (array('c' => 'Advanced checkboxes',
+        'a' => 'Select options') as $control => $controlname) {
+    foreach (array('s' => 'Small', 'm' => 'Below limit', 'e' => 'Exact PHP limit',
+            'l' => 'Above limit') as $size => $sizename) {
+        echo html_writer::tag('li', html_writer::link('max_input_vars.php?type=' .
+                $control . $size, $controlname . ' / ' . $sizename));
+    }
+}
+echo html_writer::end_tag('ul');
+
+echo $OUTPUT->footer();
index e038f4e..52d2b77 100644 (file)
@@ -2571,14 +2571,19 @@ function message_mark_message_read($message, $timeread, $messageworkingempty=fal
  *
  * @param bool $ready only return ready-to-use processors
  * @param bool $reset Reset list of message processors (used in unit tests)
+ * @param bool $resetonly Just reset, then exit
  * @return mixed $processors array of objects containing information on message processors
  */
-function get_message_processors($ready = false, $reset = false) {
+function get_message_processors($ready = false, $reset = false, $resetonly = false) {
     global $DB, $CFG;
 
     static $processors;
     if ($reset) {
         $processors = array();
+
+        if ($resetonly) {
+            return $processors;
+        }
     }
 
     if (empty($processors)) {
index f395c28..6af514f 100644 (file)
@@ -139,6 +139,7 @@ class message_airnotifier_manager {
                 return $key['accesskey'];
             }
         }
+        debugging("Unexpected response from the Airnotifier server: $resp");
         return false;
     }
 
@@ -164,11 +165,12 @@ class message_airnotifier_manager {
         $params = array();
         $resp = $curl->post($serverurl, $params);
 
-        if ($resp = json_decode($resp, true)) {
-            if (!empty($resp['status'])) {
-                return $resp['status'] == 'ok' || $resp['status'] == 'token exists';
+        if ($token = json_decode($resp, true)) {
+            if (!empty($token['status'])) {
+                return $token['status'] == 'ok' || $token['status'] == 'token exists';
             }
         }
+        debugging("Unexpected response from the Airnotifier server: $resp");
         return false;
     }
 
index 849e57b..9a578da 100644 (file)
@@ -35,3 +35,34 @@ Feature: An user can message course participants
     And I should see "Student 1"
     And I click on "this conversation" "link" in the "//div[@class='singlemessage'][contains(., 'Teacher 1')]" "xpath_element"
     And I should see "Here it is, the message content"
+
+  Scenario: An user can message multiple course participants including him/her self
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+      | student3 | Student | 3 | student3@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Participants"
+    When I set the field with xpath "//tr[contains(normalize-space(.), 'Teacher 1')]//input[@type='checkbox']" to "1"
+    And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 1')]//input[@type='checkbox']" to "1"
+    And I set the field "With selected users..." to "Send a message"
+    And I press "OK"
+    And I set the following fields to these values:
+      | messagebody | Here it is, the message content |
+    And I press "Send message"
+    And I follow "Messages" in the user menu
+    And I select "Recent conversations" from the "Message navigation:" singleselect
+    Then I should see "Here it is, the message content"
+    And I should see "Student 1"
+    And I click on "this conversation" "link" in the "//div[@class='singlemessage'][contains(., 'Teacher 1')]" "xpath_element"
+    And I should see "Here it is, the message content"
\ No newline at end of file
index 91906a4..24604cb 100644 (file)
@@ -1,6 +1,9 @@
 This files describes API changes in /message/ messaging system,
 information provided here is intended especially for developers.
 
+=== 3.1 ===
+* get_message_processors accepts an addition parameter for testing, which will just reset processor and exit.
+
 === 2.9 ===
 * Renderer method \core_message_renderer::manage_messagingoptions now accepts a user id parameter as well.
 
index 04a6e52..600e0e9 100644 (file)
@@ -44,7 +44,18 @@ class data_field_date extends data_field_base {
             $month   = $formdata->$fieldname;
             $fieldname = 'field_' . $this->field->id . '_year';
             $year   = $formdata->$fieldname;
-            $content = make_timestamp($year, $month, $day, 12, 0, 0, 0, false);
+
+            $calendartype = \core_calendar\type_factory::get_calendar_instance();
+            $gregoriandate = $calendartype->convert_to_gregorian($year, $month, $day);
+            $content = make_timestamp(
+                $gregoriandate['year'],
+                $gregoriandate['month'],
+                $gregoriandate['day'],
+                $gregoriandate['hour'],
+                $gregoriandate['minute'],
+                0,
+                0,
+                false);
         } else if ($recordid) {
             $content = (int)$DB->get_field('data_content', 'content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid));
         } else {
@@ -89,7 +100,18 @@ class data_field_date extends data_field_base {
         $usedate = optional_param('f_'.$this->field->id.'_z', 0, PARAM_INT);
         $data = array();
         if (!empty($day) && !empty($month) && !empty($year) && $usedate == 1) {
-            $data['timestamp'] = make_timestamp($year, $month, $day, 12, 0, 0, 0, false);
+            $calendartype = \core_calendar\type_factory::get_calendar_instance();
+            $gregoriandate = $calendartype->convert_to_gregorian($year, $month, $day);
+
+            $data['timestamp'] = make_timestamp(
+                $gregoriandate['year'],
+                $gregoriandate['month'],
+                $gregoriandate['day'],
+                $gregoriandate['hour'],
+                $gregoriandate['minute'],
+                0,
+                0,
+                false);
             $data['usedate'] = 1;
             return $data;
         } else {
@@ -110,7 +132,18 @@ class data_field_date extends data_field_base {
             $content = new stdClass();
             $content->fieldid = $this->field->id;
             $content->recordid = $recordid;
-            $content->content = make_timestamp($this->year, $this->month, $this->day, 12, 0, 0, 0, false);
+
+            $calendartype = \core_calendar\type_factory::get_calendar_instance();
+            $gregoriandate = $calendartype->convert_to_gregorian($this->year, $this->month, $this->day);
+            $content->content = make_timestamp(
+                $gregoriandate['year'],
+                $gregoriandate['month'],
+                $gregoriandate['day'],
+                $gregoriandate['hour'],
+                $gregoriandate['minute'],
+                0,
+                0,
+                false);
 
             if ($oldcontent = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) {
                 $content->id = $oldcontent->id;
index 7784eb0..046ffa6 100644 (file)
@@ -46,7 +46,9 @@ $fromyear = optional_param('fromyear', 0, PARAM_INT);      // Starting date
 $fromhour = optional_param('fromhour', 0, PARAM_INT);      // Starting date
 $fromminute = optional_param('fromminute', 0, PARAM_INT);      // Starting date
 if ($timefromrestrict) {
-    $datefrom = make_timestamp($fromyear, $frommonth, $fromday, $fromhour, $fromminute);
+    $calendartype = \core_calendar\type_factory::get_calendar_instance();
+    $gregorianfrom = $calendartype->convert_to_gregorian($fromyear, $frommonth, $fromday);
+    $datefrom = make_timestamp($gregorianfrom['year'], $gregorianfrom['month'], $gregorianfrom['day'], $fromhour, $fromminute);
 } else {
     $datefrom = optional_param('datefrom', 0, PARAM_INT);      // Starting date
 }
@@ -58,7 +60,9 @@ $toyear = optional_param('toyear', 0, PARAM_INT);      // Ending date
 $tohour = optional_param('tohour', 0, PARAM_INT);      // Ending date
 $tominute = optional_param('tominute', 0, PARAM_INT);      // Ending date
 if ($timetorestrict) {
-    $dateto = make_timestamp($toyear, $tomonth, $today, $tohour, $tominute);
+    $calendartype = \core_calendar\type_factory::get_calendar_instance();
+    $gregorianto = $calendartype->convert_to_gregorian($toyear, $tomonth, $today);
+    $dateto = make_timestamp($gregorianto['year'], $gregorianto['month'], $gregorianto['day'], $tohour, $tominute);
 } else {
     $dateto = optional_param('dateto', 0, PARAM_INT);      // Ending date
 }
index 920709a..b533878 100644 (file)
@@ -4,7 +4,6 @@ Feature: Set time spent as a completion condition for a lesson
   As a teacher
   I need to set time spent to mark the lesson activity as completed
 
-  @javascript
   Scenario: Set time spent as a condition
     Given the following "users" exist:
       | username | firstname | lastname | email |
@@ -29,8 +28,8 @@ Feature: Set time spent as a completion condition for a lesson
       | Description | Test lesson description |
       | Completion tracking | Show activity as complete when conditions are met |
       | completiontimespentenabled | 1 |
-      | completiontimespent[timeunit] | 60 |
-      | completiontimespent[number] | 1 |
+      | completiontimespent[timeunit] | 1 |
+      | completiontimespent[number] | 10 |
     And I follow "Test lesson"
     And I follow "Add a content page"
     And I set the following fields to these values:
@@ -54,15 +53,17 @@ Feature: Set time spent as a completion condition for a lesson
     Then the "Test lesson" "lesson" activity with "auto" completion should be marked as not complete
     And I follow "Test lesson"
     And I press "Next page"
+    # Add 1 sec delay so lesson knows a valid attempt has been made in past.
+    And I wait "1" seconds
     And I press "Next page"
     And I should see "You completed this lesson in"
-    And I should see ", which is less than the required time of 1 min. You might need to attempt the lesson again."
+    And I should see ", which is less than the required time of 10 secs. You might need to attempt the lesson again."
     And I follow "Course 1"
     And the "Test lesson" "lesson" activity with "auto" completion should be marked as not complete
     And I follow "Course 1"
     And I follow "Test lesson"
     And I press "Next page"
-    And I wait "61" seconds
+    And I wait "11" seconds
     And I press "Next page"
     And I should not see "You might need to attempt the lesson again."
     And I follow "Course 1"
index a730490..c94eda3 100644 (file)
@@ -20,11 +20,10 @@ Feature: A teacher can set available from and deadline dates to access a lesson
     And I follow "Course 1"
     And I turn editing mode on
 
-  @javascript
   Scenario: Forbidding lesson accesses until a specified date
     Given I add a "Lesson" to section "1"
     And I expand all fieldsets
-    And I click on "id_available_enabled" "checkbox"
+    And I set the field "id_available_enabled" to "1"
     And I set the following fields to these values:
       | Name | Test lesson |
       | Description | Test lesson description |
@@ -48,11 +47,9 @@ Feature: A teacher can set available from and deadline dates to access a lesson
     Then I should see "This lesson will be open on Wednesday, 1 January 2020, 8:00"
     And I should not see "First page contents"
 
-  @javascript
   Scenario: Forbidding lesson accesses until a specified date
     Given I add a "Lesson" to section "1"
-    And I expand all fieldsets
-    And I click on "id_deadline_enabled" "checkbox"
+    And I set the field "id_deadline_enabled" to "1"
     And I set the following fields to these values:
       | Name | Test lesson |
       | Description | Test lesson description |
index 34a0cf8..946c590 100644 (file)
@@ -25,7 +25,6 @@ Feature: In a lesson activity a student should
     And I press "Save and return to course"
     And I follow "Test lesson name"
 
-  @javascript
   Scenario: resume a lesson with both content then question pages
     Given I follow "Add a content page"
     And I set the following fields to these values:
@@ -34,7 +33,7 @@ Feature: In a lesson activity a student should
       | id_answer_editor_0 | Next page |
       | id_jumpto_0 | Next page |
     And I press "Save page"
-    And I set the field "qtype" to "Question"
+    And I select "Question" from the "qtype" singleselect
     And I set the field "Select a question type" to "True/false"
     And I press "Add a question page"
     And I set the following fields to these values:
@@ -47,7 +46,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This page |
     And I press "Save page"
-    And I set the field "qtype" to "Question"
+    And I select "Question" from the "qtype" singleselect
     And I set the field "Select a question type" to "True/false"
     And I press "Add a question page"
     And I set the following fields to these values:
@@ -60,7 +59,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This page |
     And I press "Save page"
-    And I set the field "qtype" to "Add a content page"
+    And I select "Add a content page" from the "qtype" singleselect
     And I set the following fields to these values:
       | Page title | Third page name |
       | Page contents | Third page contents |
@@ -69,7 +68,7 @@ Feature: In a lesson activity a student should
       | id_answer_editor_1 | Next page |
       | id_jumpto_1 | Next page |
     And I press "Save page"
-    And I set the field "qtype" to "Add a content page"
+    And I select "Add a content page" from the "qtype" singleselect
     And I set the following fields to these values:
       | Page title | Second page name |
       | Page contents | Second page contents |
@@ -84,6 +83,8 @@ Feature: In a lesson activity a student should
     And I follow "Test lesson name"
     And I should see "First page contents"
     And I press "Next page"
+    # Add 1 sec delay so lesson knows a valid attempt has been made in past.
+    And I wait "1" seconds
     And I should see "Second page contents"
     And I press "Next page"
     And I should see "Third page contents"
@@ -92,6 +93,8 @@ Feature: In a lesson activity a student should
     And I should see "Do you want to start at the last page you saw?"
     And I follow "Yes"
     Then I should see "Third page contents"
+    # Add 1 sec delay so lesson knows differentiate 3rd and paper attempts.
+    And I wait "1" seconds
     And I press "Next page"
     And I should see "Paper is made from trees."
     And I follow "Test lesson name"
@@ -115,7 +118,6 @@ Feature: In a lesson activity a student should
     And I press "Continue"
     And I should see "Congratulations - end of lesson reached"
 
-  @javascript
   Scenario: resume a lesson with only content pages
     Given I follow "Add a content page"
     And I set the following fields to these values:
@@ -124,7 +126,7 @@ Feature: In a lesson activity a student should
       | id_answer_editor_0 | Next page |
       | id_jumpto_0 | Next page |
     And I press "Save page"
-    And I set the field "qtype" to "Add a content page"
+    And I select "Add a content page" from the "qtype" singleselect
     And I set the following fields to these values:
       | Page title | Fourth page name |
       | Page contents | Fourth page contents |
@@ -133,7 +135,7 @@ Feature: In a lesson activity a student should
       | id_answer_editor_1 | End of lesson |
       | id_jumpto_1 | End of lesson |
     And I press "Save page"
-    And I set the field "qtype" to "Add a content page"
+    And I select "Add a content page" from the "qtype" singleselect
     And I set the following fields to these values:
       | Page title | Third page name |
       | Page contents | Third page contents |
@@ -142,7 +144,7 @@ Feature: In a lesson activity a student should
       | id_answer_editor_1 | Next page |
       | id_jumpto_1 | Next page |
     And I press "Save page"
-    And I set the field "qtype" to "Add a content page"
+    And I select "Add a content page" from the "qtype" singleselect
     And I set the following fields to these values:
       | Page title | Second page name |
       | Page contents | Second page contents |
@@ -158,6 +160,8 @@ Feature: In a lesson activity a student should
     And I should see "First page contents"
     And I press "Next page"
     And I should see "Second page contents"
+    # Add 1 sec delay so lesson knows a valid attempt has been made in past.
+    And I wait "1" seconds
     And I press "Next page"
     And I should see "Third page contents"
     And I follow "Test lesson name"
@@ -167,6 +171,8 @@ Feature: In a lesson activity a student should
     And I should see "Third page contents"
     And I press "Next page"
     And I should see "Fourth page contents"
+    # Add 1 sec delay so lesson knows a valid attempt has been made in past.
+    And I wait "1" seconds
     And I press "End of lesson"
     And I log out
     And I log in as "student1"
@@ -175,7 +181,6 @@ Feature: In a lesson activity a student should
     And I should see "First page contents"
     And I log out
 
-  @javascript
   Scenario: resume a lesson with both question then content pages
     Given I follow "Add a question page"
     And I set the field "Select a question type" to "True/false"
@@ -190,7 +195,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This page |
     And I press "Save page"
-    And I set the field "qtype" to "Question"
+    And I select "Question" from the "qtype" singleselect
     And I set the field "Select a question type" to "True/false"
     And I press "Add a question page"
     And I set the following fields to these values:
@@ -203,14 +208,14 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This page |
     And I press "Save page"
-    And I set the field "qtype" to "Add a content page"
+    And I select "Add a content page" from the "qtype" singleselect
     And I set the following fields to these values:
       | Page title | Content page 2 |
       | Page contents | Second content page |
       | id_answer_editor_0 | Next page |
       | id_jumpto_0 | Next page |
     And I press "Save page"
-    And I set the field "qtype" to "Question"
+    And I select "Question" from the "qtype" singleselect
     And I set the field "Select a question type" to "True/false"
     And I press "Add a question page"
     And I set the following fields to these values:
@@ -223,7 +228,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This page |
     And I press "Save page"
-    And I set the field "qtype" to "Question"
+    And I select "Question" from the "qtype" singleselect
     And I set the field "Select a question type" to "True/false"
     And I press "Add a question page"
     And I set the following fields to these values:
@@ -236,7 +241,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This page |
     And I press "Save page"
-    And I set the field "qtype" to "Question"
+    And I select "Question" from the "qtype" singleselect
     And I set the field "Select a question type" to "True/false"
     And I press "Add a question page"
     And I set the following fields to these values:
@@ -249,7 +254,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This page |
     And I press "Save page"
-    And I set the field "qtype" to "Add a content page"
+    And I select "Add a content page" from the "qtype" singleselect
     And I set the following fields to these values:
       | Page title | Content page 1 |
       | Page contents | First content page |
@@ -275,6 +280,8 @@ Feature: In a lesson activity a student should
     And I should see "1+1=2"
     And I set the following fields to these values:
       | True | 1 |
+    # Add 1 sec delay so lesson knows a valid attempt has been made in past.
+    And I wait "1" seconds
     And I press "Submit"
     And I press "Continue"
     And I should see "2+2=4"
@@ -285,6 +292,8 @@ Feature: In a lesson activity a student should
     And I should see "2+2=4"
     And I set the following fields to these values:
       | True | 1 |
+    # Add 1 sec delay so lesson knows a valid attempt has been made in past.
+    And I wait "1" seconds
     And I press "Submit"
     And I press "Continue"
     And I should see "Second content page"
@@ -301,7 +310,6 @@ Feature: In a lesson activity a student should
     And I press "Continue"
     And I should see "Congratulations - end of lesson reached"
 
-  @javascript
   Scenario: resume a lesson with only question pages
     Given I follow "Add a question page"
     And I set the field "Select a question type" to "True/false"
@@ -316,7 +324,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This page |
     And I press "Save page"
-    And I set the field "qtype" to "Question"
+    And I select "Question" from the "qtype" singleselect
     And I set the field "Select a question type" to "True/false"
     And I press "Add a question page"
     And I set the following fields to these values:
@@ -329,7 +337,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This page |
     And I press "Save page"
-    And I set the field "qtype" to "Question"
+    And I select "Question" from the "qtype" singleselect
     And I set the field "Select a question type" to "True/false"
     And I press "Add a question page"
     And I set the following fields to these values:
@@ -342,7 +350,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This page |
     And I press "Save page"
-    And I set the field "qtype" to "Question"
+    And I select "Question" from the "qtype" singleselect
     And I set the field "Select a question type" to "True/false"
     And I press "Add a question page"
     And I set the following fields to these values:
@@ -355,7 +363,7 @@ Feature: In a lesson activity a student should
       | id_response_editor_1 | Wrong |
       | id_jumpto_1 | This page |
     And I press "Save page"
-    And I set the field "qtype" to "Question"
+    And I select "Question" from the "qtype" singleselect
     And I set the field "Select a question type" to "True/false"
     And I press "Add a question page"
     And I set the following fields to these values:
@@ -385,6 +393,8 @@ Feature: In a lesson activity a student should
     And I should see "1+1=2"
     And I set the following fields to these values:
       | True | 1 |
+    # Add 1 sec delay so lesson knows a valid attempt has been made in past.
+    And I wait "1" seconds
     And I press "Submit"
     And I press "Continue"
     And I should see "2+2=4"
@@ -395,6 +405,8 @@ Feature: In a lesson activity a student should
     And I should see "2+2=4"
     And I set the following fields to these values:
       | True | 1 |
+    # Add 1 sec delay so lesson knows a valid attempt has been made in past.
+    And I wait "1" seconds
     And I press "Submit"
     And I press "Continue"
     And I should see "Kermit is a frog"
index e9c832a..c7b99fc 100644 (file)
@@ -113,55 +113,58 @@ class restore_quiz_activity_task extends restore_activity_task {
         $rules[] = new restore_log_rule('quiz', 'addcategory',
                 'view.php?id={course_module}', '{question_category}');
         $rules[] = new restore_log_rule('quiz', 'view summary',
-                'summary.php?attempt={quiz_attempt_id}', '{quiz}');
+                'summary.php?attempt={quiz_attempt}', '{quiz}');
         $rules[] = new restore_log_rule('quiz', 'manualgrade',
-                'comment.php?attempt={quiz_attempt_id}&question={question}', '{quiz}');
+                'comment.php?attempt={quiz_attempt}&question={question}', '{quiz}');
         $rules[] = new restore_log_rule('quiz', 'manualgrading',
                 'report.php?mode=grading&q={quiz}', '{quiz}');
         // All the ones calling to review.php have two rules to handle both old and new urls
         // in any case they are always converted to new urls on restore.
         // TODO: In Moodle 2.x (x >= 5) kill the old rules.
-        // Note we are using the 'quiz_attempt_id' mapping because that is the
+        // Note we are using the 'quiz_attempt' mapping because that is the
         // one containing the quiz_attempt->ids old an new for quiz-attempt.
         $rules[] = new restore_log_rule('quiz', 'attempt',
                 'review.php?id={course_module}&attempt={quiz_attempt}', '{quiz}',
                 null, null, 'review.php?attempt={quiz_attempt}');
+        $rules[] = new restore_log_rule('quiz', 'attempt',
+                'review.php?attempt={quiz_attempt}', '{quiz}',
+                null, null, 'review.php?attempt={quiz_attempt}');
         // Old an new for quiz-submit.
         $rules[] = new restore_log_rule('quiz', 'submit',
-                'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}',
-                null, null, 'review.php?attempt={quiz_attempt_id}');
+                'review.php?id={course_module}&attempt={quiz_attempt}', '{quiz}',
+                null, null, 'review.php?attempt={quiz_attempt}');
         $rules[] = new restore_log_rule('quiz', 'submit',
-                'review.php?attempt={quiz_attempt_id}', '{quiz}');
+                'review.php?attempt={quiz_attempt}', '{quiz}');
         // Old an new for quiz-review.
         $rules[] = new restore_log_rule('quiz', 'review',
-                'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}',
-                null, null, 'review.php?attempt={quiz_attempt_id}');
+                'review.php?id={course_module}&attempt={quiz_attempt}', '{quiz}',
+                null, null, 'review.php?attempt={quiz_attempt}');
         $rules[] = new restore_log_rule('quiz', 'review',
-                'review.php?attempt={quiz_attempt_id}', '{quiz}');
+                'review.php?attempt={quiz_attempt}', '{quiz}');
         // Old an new for quiz-start attemp.
         $rules[] = new restore_log_rule('quiz', 'start attempt',
-                'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}',
-                null, null, 'review.php?attempt={quiz_attempt_id}');
+                'review.php?id={course_module}&attempt={quiz_attempt}', '{quiz}',
+                null, null, 'review.php?attempt={quiz_attempt}');
         $rules[] = new restore_log_rule('quiz', 'start attempt',
-                'review.php?attempt={quiz_attempt_id}', '{quiz}');
+                'review.php?attempt={quiz_attempt}', '{quiz}');
         // Old an new for quiz-close attemp.
         $rules[] = new restore_log_rule('quiz', 'close attempt',
-                'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}',
-                null, null, 'review.php?attempt={quiz_attempt_id}');
+                'review.php?id={course_module}&attempt={quiz_attempt}', '{quiz}',
+                null, null, 'review.php?attempt={quiz_attempt}');
         $rules[] = new restore_log_rule('quiz', 'close attempt',
-                'review.php?attempt={quiz_attempt_id}', '{quiz}');
+                'review.php?attempt={quiz_attempt}', '{quiz}');
         // Old an new for quiz-continue attempt.
         $rules[] = new restore_log_rule('quiz', 'continue attempt',
-                'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}',
-                null, null, 'review.php?attempt={quiz_attempt_id}');
+                'review.php?id={course_module}&attempt={quiz_attempt}', '{quiz}',
+                null, null, 'review.php?attempt={quiz_attempt}');
         $rules[] = new restore_log_rule('quiz', 'continue attempt',
-                'review.php?attempt={quiz_attempt_id}', '{quiz}');
+                'review.php?attempt={quiz_attempt}', '{quiz}');
         // Old an new for quiz-continue attemp.
         $rules[] = new restore_log_rule('quiz', 'continue attemp',
-                'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}',
-                null, 'continue attempt', 'review.php?attempt={quiz_attempt_id}');
+                'review.php?id={course_module}&attempt={quiz_attempt}', '{quiz}',
+                null, 'continue attempt', 'review.php?attempt={quiz_attempt}');
         $rules[] = new restore_log_rule('quiz', 'continue attemp',
-                'review.php?attempt={quiz_attempt_id}', '{quiz}',
+                'review.php?attempt={quiz_attempt}', '{quiz}',
                 null, 'continue attempt');
 
         return $rules;
index 424d689..cf54fbb 100644 (file)
@@ -36,7 +36,8 @@ require_once($CFG->dirroot . '/mod/quiz/accessmanager.php');
 require_once($CFG->dirroot . '/mod/quiz/accessmanager_form.php');
 require_once($CFG->dirroot . '/mod/quiz/renderer.php');
 require_once($CFG->dirroot . '/mod/quiz/attemptlib.php');
-require_once($CFG->libdir  . '/eventslib.php');
+require_once($CFG->libdir . '/completionlib.php');
+require_once($CFG->libdir . '/eventslib.php');
 require_once($CFG->libdir . '/filelib.php');
 require_once($CFG->libdir . '/questionlib.php');
 
@@ -1550,7 +1551,8 @@ function quiz_send_notification_messages($course, $quiz, $attempt, $context, $cm
     }
 
     // Check for notifications required.
-    $notifyfields = 'u.id, u.username, u.idnumber, u.email, u.emailstop, u.lang, u.timezone, u.mailformat, u.maildisplay, ';
+    $notifyfields = 'u.id, u.username, u.idnumber, u.email, u.emailstop, u.lang,
+            u.timezone, u.mailformat, u.maildisplay, u.auth, u.suspended, u.deleted, ';
     $notifyfields .= get_all_user_name_fields(true, 'u');
     $groups = groups_get_all_groups($course->id, $submitter->id, $cm->groupingid);
     if (is_array($groups) && count($groups) > 0) {
index b5173a2..b11ed81 100644 (file)
@@ -234,7 +234,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
 
         if ($attemptobj->get_access_manager(time())->attempt_must_be_in_popup()) {
             $this->page->requires->js_init_call('M.mod_quiz.secure_window.init_close_button',
-                    array($url), quiz_get_js_module());
+                    array($url), false, quiz_get_js_module());
             return html_writer::empty_tag('input', array('type' => 'button',
                     'value' => get_string('finishreview', 'quiz'),
                     'id' => 'secureclosebutton'));
index a67cb74..990c15f 100644 (file)
@@ -226,6 +226,21 @@ class mod_scorm_external extends external_api {
         if (!$scoes = scorm_get_scoes($scorm->id, $params['organization'])) {
             // Function scorm_get_scoes return false, not an empty array.
             $scoes = array();
+        } else {
+            $scoreturnstructure = self::get_scorm_scoes_returns();
+            foreach ($scoes as $sco) {
+                $extradata = array();
+                foreach ($sco as $element => $value) {
+                    // Check if the element is extra data (not a basic SCO element).
+                    if (!isset($scoreturnstructure->keys['scoes']->content->keys[$element])) {
+                        $extradata[] = array(
+                            'element' => $element,
+                            'value' => $value
+                        );
+                    }
+                }
+                $sco->extradata = $extradata;
+            }
         }
 
         $result = array();
@@ -257,6 +272,14 @@ class mod_scorm_external extends external_api {
                             'scormtype' => new external_value(PARAM_ALPHA, 'scorm type (asset, sco)'),
                             'title' => new external_value(PARAM_NOTAGS, 'sco title'),
                             'sortorder' => new external_value(PARAM_INT, 'sort order'),
+                            'extradata' => new external_multiple_structure(
+                                new external_single_structure(
+                                    array(
+                                        'element' => new external_value(PARAM_RAW, 'element name'),
+                                        'value' => new external_value(PARAM_RAW, 'element value')
+                                    )
+                                ), 'Additional SCO data', VALUE_OPTIONAL
+                            )
                         ), 'SCORM SCO data'
                     )
                 ),
@@ -760,16 +783,17 @@ class mod_scorm_external extends external_api {
                                                                     VALUE_OPTIONAL),
                             'lastattemptlock' => new external_value(PARAM_BOOL, 'Prevents to launch new attempts once finished',
                                                                     VALUE_OPTIONAL),
-                            'displayattemptstatus' => new external_value(PARAM_BOOL, 'Display attempts status', VALUE_OPTIONAL),
+                            'displayattemptstatus' => new external_value(PARAM_INT, 'How to display attempt status',
+                                                                            VALUE_OPTIONAL),
                             'displaycoursestructure' => new external_value(PARAM_BOOL, 'Display contents structure',
                                                                             VALUE_OPTIONAL),
                             'sha1hash' => new external_value(PARAM_NOTAGS, 'Package content or ext path hash', VALUE_OPTIONAL),
                             'md5hash' => new external_value(PARAM_NOTAGS, 'MD5 Hash of package file', VALUE_OPTIONAL),
                             'revision' => new external_value(PARAM_INT, 'Revison number', VALUE_OPTIONAL),
                             'launch' => new external_value(PARAM_INT, 'First content to launch', VALUE_OPTIONAL),
-                            'skipview' => new external_value(PARAM_BOOL, 'Skip or not content structure page', VALUE_OPTIONAL),
+                            'skipview' => new external_value(PARAM_INT, 'How to skip the content structure page', VALUE_OPTIONAL),
                             'hidebrowse' => new external_value(PARAM_BOOL, 'Disable preview mode?', VALUE_OPTIONAL),
-                            'hidetoc' => new external_value(PARAM_BOOL, 'Display or not course structure in player',
+                            'hidetoc' => new external_value(PARAM_INT, 'How to display the SCORM structure in player',
                                                             VALUE_OPTIONAL),
                             'nav' => new external_value(PARAM_INT, 'Show navigation buttons', VALUE_OPTIONAL),
                             'navpositionleft' => new external_value(PARAM_INT, 'Navigation position left', VALUE_OPTIONAL),
index a179695..dcdd884 100644 (file)
@@ -51,6 +51,7 @@ if (intval(get_config("scorm", "scorm12standard"))) {
     $cmistring4096 = $cmistring256;
 }
 
+$scorm->autocommit = ($scorm->autocommit === "1") ? true : false;
 $PAGE->requires->js_init_call('M.scorm_api.init', array($def, $cmiobj, $cmiint, $cmistring256, $cmistring4096,
                                                         scorm_debugging($scorm), $scorm->auto, $scorm->id, $CFG->wwwroot,
                                                         sesskey(), $scoid, $attempt, $mode, $id, $currentorg, $scorm->autocommit));
index f826a78..e41ca35 100644 (file)
@@ -47,6 +47,7 @@ if ($scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id), 'sort
     }
 }
 
+$scorm->autocommit = ($scorm->autocommit === "1") ? true : false;
 $PAGE->requires->js_init_call('M.scorm_api.init', array($def, $cmiobj, $cmiint, $cmicommentsuser, $cmicommentslms,
                                                         scorm_debugging($scorm), $scorm->auto, $scorm->id, $CFG->wwwroot,
                                                         sesskey(), $scoid, $attempt, $mode, $id, $currentorg, $scorm->autocommit));
index 3a6f80a..7fb480f 100644 (file)
@@ -1755,6 +1755,9 @@ function scorm_format_toc_for_treeview($user, $scorm, $scoes, $usertracks, $cmid
                 $result->toc .= html_writer::start_tag('ul');
                 $childresult = scorm_format_toc_for_treeview($user, $scorm, $sco->children, $usertracks, $cmid,
                                                                 $toclink, $currentorg, $attempt, $play, $organizationsco, true);
+
+                // Is any of the children incomplete?
+                $sco->incomplete = $childresult->incomplete;
                 $result->toc .= $childresult->toc;
                 $result->toc .= html_writer::end_tag('ul');
                 $result->toc .= html_writer::end_tag('li');
index dad0f0a..287bccb 100644 (file)
@@ -156,6 +156,9 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h
             var obj = document.createElement('iframe');
             obj.setAttribute('id', 'scorm_object');
             obj.setAttribute('type', 'text/html');
+            obj.setAttribute('allowfullscreen', 'allowfullscreen');
+            obj.setAttribute('webkitallowfullscreen', 'webkitallowfullscreen');
+            obj.setAttribute('mozallowfullscreen', 'mozallowfullscreen');
             if (!window_name && node.title != null) {
                 obj.setAttribute('src', url_prefix + node.title);
             }
index 2aee588..7af4050 100644 (file)
@@ -59,15 +59,18 @@ class report extends \mod_scorm\report {
             $includeqtext = $fromform->qtext;
             $includeresp = $fromform->resp;
             $includeright = $fromform->right;
+            $includeresult = $fromform->result;
             set_user_preference('scorm_report_pagesize', $pagesize);
             set_user_preference('scorm_report_interactions_qtext', $includeqtext);
             set_user_preference('scorm_report_interactions_resp', $includeresp);
             set_user_preference('scorm_report_interactions_right', $includeright);
+            set_user_preference('scorm_report_interactions_result', $includeresult);
         } else {
             $pagesize = get_user_preferences('scorm_report_pagesize', 0);
             $includeqtext = get_user_preferences('scorm_report_interactions_qtext', 0);
             $includeresp = get_user_preferences('scorm_report_interactions_resp', 1);
             $includeright = get_user_preferences('scorm_report_interactions_right', 0);
+            $includeresult = get_user_preferences('scorm_report_interactions_result', 0);
         }
         if ($pagesize < 1) {
             $pagesize = SCORM_REPORT_DEFAULT_PAGE_SIZE;
@@ -79,6 +82,7 @@ class report extends \mod_scorm\report {
         $displayoptions['qtext'] = $includeqtext;
         $displayoptions['resp'] = $includeresp;
         $displayoptions['right'] = $includeright;
+        $displayoptions['result'] = $includeresult;
 
         $mform->set_data($displayoptions + array('pagesize' => $pagesize));
         if ($groupmode = groups_get_activity_groupmode($cm)) {   // Groups are being used.
@@ -204,6 +208,10 @@ class report extends \mod_scorm\report {
                     $columns[] = 'right' . $id;
                     $headers[] = get_string('rightanswerx', 'scormreport_interactions', $id);
                 }
+                if ($displayoptions['result']) {
+                    $columns[] = 'result' . $id;
+                    $headers[] = get_string('resultx', 'scormreport_interactions', $id);
+                }
             }
 
             if (!$download) {
@@ -239,6 +247,9 @@ class report extends \mod_scorm\report {
                     if ($displayoptions['right']) {
                         $table->no_sorting('right'.$id);
                     }
+                    if ($displayoptions['result']) {
+                        $table->no_sorting('result'.$id);
+                    }
                 }
 
                 foreach ($scoes as $sco) {
@@ -526,6 +537,14 @@ class report extends \mod_scorm\report {
                                             $row[] = '&nbsp;';
                                         }
                                     }
+                                    if ($displayoptions['result']) {
+                                        $element = 'cmi.interactions_'.$i.'.result';
+                                        if (isset($trackdata->$element)) {
+                                            $row[] = s($trackdata->$element);
+                                        } else {
+                                            $row[] = '&nbsp;';
+                                        }
+                                    }
                                 }
                                 // End of interaction data.
                             } else {
index eb3d7ff..ed66f59 100644 (file)
@@ -29,7 +29,9 @@ $string['pluginname'] = 'Interactions report';
 $string['questionx'] = 'Question {$a}';
 $string['responsex'] = 'Response {$a}';
 $string['rightanswerx'] = 'Right answer {$a}';
+$string['resultx'] = 'Result {$a}';
 $string['summaryofquestiontext'] = 'Summary of question';
 $string['summaryofresponse'] = 'Summary of responses';
 $string['summaryofrightanswer'] = 'Summary of right answer';
+$string['summaryofresult'] = 'Summary of result';
 
index 1c145b4..0bc319b 100644 (file)
@@ -43,6 +43,7 @@ class mod_scorm_report_interactions_settings extends moodleform {
         $mform->addElement('advcheckbox', 'qtext', '', get_string('summaryofquestiontext', 'scormreport_interactions'));
         $mform->addElement('advcheckbox', 'resp', '', get_string('summaryofresponse', 'scormreport_interactions'));
         $mform->addElement('advcheckbox', 'right', '', get_string('summaryofrightanswer', 'scormreport_interactions'));
+        $mform->addElement('advcheckbox', 'result', '', get_string('summaryofresult', 'scormreport_interactions'));
 
         // -------------------------------------------------------------------------------
         $mform->addElement('header', 'preferencesuser', get_string('preferencesuser', 'scorm'));
index 945570a..f5691ca 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111600;                 // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015121600;                 // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;                 // Requires this Moodle version.
 $plugin->component = 'scormreport_interactions'; // Full name of the plugin (used for diagnostics).
index d034c0c..39dc0fd 100644 (file)
@@ -261,12 +261,27 @@ class mod_scorm_external_testcase extends externallib_advanced_testcase {
 
         $scoes = scorm_get_scoes($scorm->id);
         $sco = array_shift($scoes);
+        $sco->extradata = array();
         $this->assertEquals((array) $sco, $result['scoes'][0]);
 
         $sco = array_shift($scoes);
-        // Remove specific sco data.
+        $sco->extradata = array();
+        $sco->extradata[] = array(
+            'element' => 'isvisible',
+            'value' => $sco->isvisible
+        );
+        $sco->extradata[] = array(
+            'element' => 'parameters',
+            'value' => $sco->parameters
+        );
         unset($sco->isvisible);
         unset($sco->parameters);
+
+        // Sort the array (if we don't sort tests will fails for Postgres).
+        usort($result['scoes'][1]['extradata'], function($a, $b) {
+            return strcmp($a['element'], $b['element']);
+        });
+
         $this->assertEquals((array) $sco, $result['scoes'][1]);
 
         // Use organization.
@@ -284,6 +299,47 @@ class mod_scorm_external_testcase extends externallib_advanced_testcase {
         } catch (moodle_exception $e) {
             $this->assertEquals('invalidrecord', $e->errorcode);
         }
+
+    }
+
+    /**
+     * Test get scorm scoes (with a complex SCORM package)
+     */
+    public function test_mod_scorm_get_scorm_scoes_complex_package() {
+        global $CFG;
+
+        // As student.
+        self::setUser($this->student);
+
+        $record = new stdClass();
+        $record->course = $this->course->id;
+        $record->packagefilepath = $CFG->dirroot.'/mod/scorm/tests/packages/complexscorm.zip';
+        $scorm = self::getDataGenerator()->create_module('scorm', $record);
+
+        $result = mod_scorm_external::get_scorm_scoes($scorm->id);
+        $result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_scoes_returns(), $result);
+        $this->assertCount(9, $result['scoes']);
+        $this->assertCount(0, $result['warnings']);
+
+        $expectedscoes = array();
+        $scoreturnstructure = mod_scorm_external::get_scorm_scoes_returns();
+        $scoes = scorm_get_scoes($scorm->id);
+        foreach ($scoes as $sco) {
+            $sco->extradata = array();
+            foreach ($sco as $element => $value) {
+                // Add the extra data to the extradata array and remove the object element.
+                if (!isset($scoreturnstructure->keys['scoes']->content->keys[$element])) {
+                    $sco->extradata[] = array(
+                        'element' => $element,
+                        'value' => $value
+                    );
+                    unset($sco->{$element});
+                }
+            }
+            $expectedscoes[] = (array) $sco;
+        }
+
+        $this->assertEquals($expectedscoes, $result['scoes']);
     }
 
     /*
@@ -552,6 +608,9 @@ class mod_scorm_external_testcase extends externallib_advanced_testcase {
         $record = new stdClass();
         $record->introformat = FORMAT_HTML;
         $record->course = $course1->id;
+        $record->hidetoc = 2;
+        $record->displayattemptstatus = 2;
+        $record->skipview = 2;
         $scorm1 = self::getDataGenerator()->create_module('scorm', $record);
 
         // Second scorm.
diff --git a/mod/scorm/tests/packages/complexscorm.zip b/mod/scorm/tests/packages/complexscorm.zip
new file mode 100644 (file)
index 0000000..7c1eb9d
Binary files /dev/null and b/mod/scorm/tests/packages/complexscorm.zip differ
index 7fbaf20..22ee5e6 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111600;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2015112700;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2015111000;    // Requires this Moodle version.
 $plugin->component = 'mod_scorm';   // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 300;
index 847f788..0ce1096 100644 (file)
@@ -174,4 +174,150 @@ class mod_wiki_external extends external_api {
         );
     }
 
+    /**
+     * Describes the parameters for view_wiki.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.1
+     */
+    public static function view_wiki_parameters() {
+        return new external_function_parameters (
+            array(
+                'wikiid' => new external_value(PARAM_INT, 'Wiki instance ID.')
+            )
+        );
+    }
+
+    /**
+     * Trigger the course module viewed event and update the module completion status.
+     *
+     * @param int $wikiid The wiki instance ID.
+     * @return array of warnings and status result.
+     * @since Moodle 3.1
+     */
+    public static function view_wiki($wikiid) {
+
+        $params = self::validate_parameters(self::view_wiki_parameters(),
+                                            array(
+                                                'wikiid' => $wikiid
+                                            ));
+        $warnings = array();
+
+        // Get wiki instance.
+        if (!$wiki = wiki_get_wiki($params['wikiid'])) {
+            throw new moodle_exception('incorrectwikiid', 'wiki');
+        }
+
+        // Permission validation.
+        list($course, $cm) = get_course_and_cm_from_instance($wiki, 'wiki');
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        // Check if user can view this wiki.
+        // We don't use wiki_user_can_view because it requires to have a valid subwiki for the user.
+        if (!has_capability('mod/wiki:viewpage', $context)) {
+            throw new moodle_exception('cannotviewpage', 'wiki');
+        }
+
+        // Trigger course_module_viewed event and completion.
+        wiki_view($wiki, $course, $cm, $context);
+
+        $result = array();
+        $result['status'] = true;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Describes the view_wiki return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.1
+     */
+    public static function view_wiki_returns() {
+        return new external_single_structure(
+            array(
+                'status' => new external_value(PARAM_BOOL, 'Status: true if success.'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Describes the parameters for view_page.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.1
+     */
+    public static function view_page_parameters() {
+        return new external_function_parameters (
+            array(
+                'pageid' => new external_value(PARAM_INT, 'Wiki page ID.'),
+            )
+        );
+    }
+
+    /**
+     * Trigger the page viewed event and update the module completion status.
+     *
+     * @param int $pageid The page ID.
+     * @return array of warnings and status result.
+     * @since Moodle 3.1
+     * @throws moodle_exception if page is not valid.
+     */
+    public static function view_page($pageid) {
+
+        $params = self::validate_parameters(self::view_page_parameters(),
+                                            array(
+                                                'pageid' => $pageid
+                                            ));
+        $warnings = array();
+
+        // Get wiki page.
+        if (!$page = wiki_get_page($params['pageid'])) {
+            throw new moodle_exception('incorrectpageid', 'wiki');
+        }
+
+        // Get wiki instance.
+        if (!$wiki = wiki_get_wiki_from_pageid($params['pageid'])) {
+            throw new moodle_exception('incorrectwikiid', 'wiki');
+        }
+
+        // Permission validation.
+        list($course, $cm) = get_course_and_cm_from_instance($wiki, 'wiki');
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        // Check if user can view this wiki.
+        if (!$subwiki = wiki_get_subwiki($page->subwikiid)) {
+            throw new moodle_exception('incorrectsubwikiid', 'wiki');
+        }
+        if (!wiki_user_can_view($subwiki, $wiki)) {
+            throw new moodle_exception('cannotviewpage', 'wiki');
+        }
+
+        // Trigger page_viewed event and completion.
+        wiki_page_view($wiki, $page, $course, $cm, $context);
+
+        $result = array();
+        $result['status'] = true;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Describes the view_page return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.1
+     */
+    public static function view_page_returns() {
+        return new external_single_structure(
+            array(
+                'status' => new external_value(PARAM_BOOL, 'Status: true if success.'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
 }
index 89c5980..46cf320 100644 (file)
@@ -33,5 +33,21 @@ $functions = array(
                            'no courses are provided then all the wiki instances the user has access to will be returned.',
         'type'          => 'read',
         'capabilities'  => 'mod/wiki:viewpage'
+    ),
+
+    'mod_wiki_view_wiki' => array(
+        'classname'     => 'mod_wiki_external',
+        'methodname'    => 'view_wiki',
+        'description'   => 'Trigger the course module viewed event and update the module completion status.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/wiki:viewpage'
+    ),
+
+    'mod_wiki_view_page' => array(
+        'classname'     => 'mod_wiki_external',
+        'methodname'    => 'view_page',
+        'description'   => 'Trigger the page viewed event and update the module completion status.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/wiki:viewpage'
     )
 );
index 4e0d4a6..997475e 100644 (file)
@@ -659,3 +659,72 @@ function wiki_page_type_list($pagetype, $parentcontext, $currentcontext) {
     );
     return $module_pagetype;
 }
+
+/**
+ * Mark the activity completed (if required) and trigger the course_module_viewed event.
+ *
+ * @param  stdClass $wiki       Wiki object.
+ * @param  stdClass $course     Course object.
+ * @param  stdClass $cm         Course module object.
+ * @param  stdClass $context    Context object.
+ * @since Moodle 3.1
+ */
+function wiki_view($wiki, $course, $cm, $context) {
+    // Trigger course_module_viewed event.
+    $params = array(
+        'context' => $context,
+        'objectid' => $wiki->id
+    );
+    $event = \mod_wiki\event\course_module_viewed::create($params);
+    $event->add_record_snapshot('course_modules', $cm);
+    $event->add_record_snapshot('course', $course);
+    $event->add_record_snapshot('wiki', $wiki);
+    $event->trigger();
+
+    // Completion.
+    $completion = new completion_info($course);
+    $completion->set_module_viewed($cm);
+}
+
+/**
+ * Mark the activity completed (if required) and trigger the page_viewed event.
+ *
+ * @param  stdClass $wiki       Wiki object.
+ * @param  stdClass $page       Page object.
+ * @param  stdClass $course     Course object.
+ * @param  stdClass $cm         Course module object.
+ * @param  stdClass $context    Context object.
+ * @param  int $uid             Optional User ID.
+ * @param  array $other         Optional Other params: title, wiki ID, group ID, groupanduser, prettyview.
+ * @param  stdClass $subwiki    Optional Subwiki.
+ * @since Moodle 3.1
+ */
+function wiki_page_view($wiki, $page, $course, $cm, $context, $uid = null, $other = null, $subwiki = null) {
+
+    // Trigger course_module_viewed event.
+    $params = array(
+        'context' => $context,
+        'objectid' => $page->id
+    );
+    if ($uid != null) {
+        $params['relateduserid'] = $uid;
+    }
+    if ($other != null) {
+        $params['other'] = $other;
+    }
+
+    $event = \mod_wiki\event\page_viewed::create($params);
+
+    $event->add_record_snapshot('wiki_pages', $page);
+    $event->add_record_snapshot('course_modules', $cm);
+    $event->add_record_snapshot('course', $course);
+    $event->add_record_snapshot('wiki', $wiki);
+    if ($subwiki != null) {
+        $event->add_record_snapshot('wiki_subwikis', $subwiki);
+    }
+    $event->trigger();
+
+    // Completion.
+    $completion = new completion_info($course);
+    $completion->set_module_viewed($cm);
+}
index e496b6c..be8ecd3 100644 (file)
@@ -62,17 +62,9 @@ $wikipage = new page_wiki_prettyview($wiki, $subwiki, $cm);
 $wikipage->set_page($page);
 
 $context = context_module::instance($cm->id);
-$event = \mod_wiki\event\page_viewed::create(
-        array(
-            'context' => $context,
-            'objectid' => $pageid,
-            'other' => array('prettyview' => true)
-            )
-        );
-$event->add_record_snapshot('wiki_pages', $page);
-$event->add_record_snapshot('wiki', $wiki);
-$event->add_record_snapshot('wiki_subwikis', $subwiki);
-$event->trigger();
+
+$other = array('prettyview' => true);
+wiki_page_view($wiki, $page, $course, $cm, $context, null, $other, $subwiki);
 
 $wikipage->print_header();
 $wikipage->print_content();
index aa92959..978a841 100644 (file)
@@ -196,6 +196,123 @@ class mod_wiki_external_testcase extends externallib_advanced_testcase {
         $wikis = mod_wiki_external::get_wikis_by_courses(array($this->course->id));
         $wikis = external_api::clean_returnvalue(mod_wiki_external::get_wikis_by_courses_returns(), $wikis);
         $this->assertFalse($wikis['wikis'][0]['cancreatepages']);
+
+    }
+
+    /**
+     * Test view_wiki.
+     */
+    public function test_view_wiki() {
+
+        // Test invalid instance id.
+        try {
+            mod_wiki_external::view_wiki(0);
+            $this->fail('Exception expected due to invalid mod_wiki instance id.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('incorrectwikiid', $e->errorcode);
+        }
+
+        // Test not-enrolled user.
+        $usernotenrolled = self::getDataGenerator()->create_user();
+        $this->setUser($usernotenrolled);
+        try {
+            mod_wiki_external::view_wiki($this->wiki->id);
+            $this->fail('Exception expected due to not enrolled user.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('requireloginerror', $e->errorcode);
+        }
+
+        // Test user with full capabilities.
+        $this->setUser($this->student);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+
+        $result = mod_wiki_external::view_wiki($this->wiki->id);
+        $result = external_api::clean_returnvalue(mod_wiki_external::view_wiki_returns(), $result);
+
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = array_shift($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_wiki\event\course_module_viewed', $event);
+        $this->assertEquals($this->context, $event->get_context());
+        $moodlewiki = new \moodle_url('/mod/wiki/view.php', array('id' => $this->cm->id));
+        $this->assertEquals($moodlewiki, $event->get_url());
+        $this->assertEventContextNotUsed($event);
+        $this->assertNotEmpty($event->get_name());
+
+        // Test user with no capabilities.
+        // We need a explicit prohibit since this capability is allowed for students by default.
+        assign_capability('mod/wiki:viewpage', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
+        accesslib_clear_all_caches_for_unit_testing();
+
+        try {
+            mod_wiki_external::view_wiki($this->wiki->id);
+            $this->fail('Exception expected due to missing capability.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('cannotviewpage', $e->errorcode);
+        }
+
+    }
+
+    /**
+     * Test view_page.
+     */
+    public function test_view_page() {
+
+        // Test invalid page id.
+        try {
+            mod_wiki_external::view_page(0);
+            $this->fail('Exception expected due to invalid view_page page id.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('incorrectpageid', $e->errorcode);
+        }
+
+        // Test not-enrolled user.
+        $usernotenrolled = self::getDataGenerator()->create_user();
+        $this->setUser($usernotenrolled);
+        try {
+            mod_wiki_external::view_page($this->firstpage->id);
+            $this->fail('Exception expected due to not enrolled user.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('requireloginerror', $e->errorcode);
+        }
+
+        // Test user with full capabilities.
+        $this->setUser($this->student);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+
+        $result = mod_wiki_external::view_page($this->firstpage->id);
+        $result = external_api::clean_returnvalue(mod_wiki_external::view_page_returns(), $result);
+
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = array_shift($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_wiki\event\page_viewed', $event);
+        $this->assertEquals($this->context, $event->get_context());
+        $pageurl = new \moodle_url('/mod/wiki/view.php', array('pageid' => $this->firstpage->id));
+        $this->assertEquals($pageurl, $event->get_url());
+        $this->assertEventContextNotUsed($event);
+        $this->assertNotEmpty($event->get_name());
+
+        // Test user with no capabilities.
+        // We need a explicit prohibit since this capability is allowed for students by default.
+        assign_capability('mod/wiki:viewpage', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
+        accesslib_clear_all_caches_for_unit_testing();
+
+        try {
+            mod_wiki_external::view_page($this->firstpage->id);
+            $this->fail('Exception expected due to missing capability.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('cannotviewpage', $e->errorcode);
+        }
+
     }
 
 }
diff --git a/mod/wiki/tests/lib_test.php b/mod/wiki/tests/lib_test.php
new file mode 100644 (file)
index 0000000..941c096
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for mod_wiki lib
+ *
+ * @package    mod_wiki
+ * @category   external
+ * @copyright  2015 Dani Palou <dani@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.1
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/wiki/lib.php');
+require_once($CFG->libdir . '/completionlib.php');
+
+/**
+ * Unit tests for mod_wiki lib
+ *
+ * @package    mod_wiki
+ * @category   external
+ * @copyright  2015 Dani Palou <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.1
+ */
+class mod_wiki_lib_testcase extends advanced_testcase {
+
+    /**
+     * Test wiki_view.
+     *
+     * @return void
+     */
+    public function test_wiki_view() {
+        global $CFG;
+
+        $CFG->enablecompletion = COMPLETION_ENABLED;
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => COMPLETION_ENABLED));
+        $options = array('completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => COMPLETION_VIEW_REQUIRED);
+        $wiki = $this->getDataGenerator()->create_module('wiki', array('course' => $course->id), $options);
+        $context = context_module::instance($wiki->cmid);
+        $cm = get_coursemodule_from_instance('wiki', $wiki->id);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+
+        wiki_view($wiki, $course, $cm, $context);
+
+        $events = $sink->get_events();
+        // 2 additional events thanks to completion.
+        $this->assertCount(3, $events);
+        $event = array_shift($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_wiki\event\course_module_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $moodleurl = new \moodle_url('/mod/wiki/view.php', array('id' => $cm->id));
+        $this->assertEquals($moodleurl, $event->get_url());
+        $this->assertEventContextNotUsed($event);
+        $this->assertNotEmpty($event->get_name());
+
+        // Check completion status.
+        $completion = new completion_info($course);
+        $completiondata = $completion->get_data($cm);
+        $this->assertEquals(1, $completiondata->completionstate);
+
+    }
+
+    /**
+     * Test wiki_page_view.
+     *
+     * @return void
+     */
+    public function test_wiki_page_view() {
+        global $CFG;
+
+        $CFG->enablecompletion = COMPLETION_ENABLED;
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => COMPLETION_ENABLED));
+        $options = array('completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => COMPLETION_VIEW_REQUIRED);
+        $wiki = $this->getDataGenerator()->create_module('wiki', array('course' => $course->id), $options);
+        $context = context_module::instance($wiki->cmid);
+        $cm = get_coursemodule_from_instance('wiki', $wiki->id);
+        $firstpage = $this->getDataGenerator()->get_plugin_generator('mod_wiki')->create_first_page($wiki);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+
+        wiki_page_view($wiki, $firstpage, $course, $cm, $context);
+
+        $events = $sink->get_events();
+        // 2 additional events thanks to completion.
+        $this->assertCount(3, $events);
+        $event = array_shift($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_wiki\event\page_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $pageurl = new \moodle_url('/mod/wiki/view.php', array('pageid' => $firstpage->id));
+        $this->assertEquals($pageurl, $event->get_url());
+        $this->assertEventContextNotUsed($event);
+        $this->assertNotEmpty($event->get_name());
+
+        // Check completion status.
+        $completion = new completion_info($course);
+        $completiondata = $completion->get_data($cm);
+        $this->assertEquals(1, $completiondata->completionstate);
+
+    }
+}
index 2fa3c25..38719ee 100644 (file)
@@ -33,7 +33,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111601;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2015111602;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;    // Requires this Moodle version
 $plugin->component = 'mod_wiki';       // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index e32d9e7..a9b7319 100644 (file)
@@ -275,11 +275,6 @@ if (!wiki_user_can_view($subwiki, $wiki)) {
     print_error('cannotviewpage', 'wiki');
 }
 
-// Update 'viewed' state if required by completion system
-require_once($CFG->libdir . '/completionlib.php');
-$completion = new completion_info($course);
-$completion->set_module_viewed($cm);
-
 if (($edit != - 1) and $PAGE->user_allowed_editing()) {
     $USER->editing = $edit;
 }
@@ -290,40 +285,19 @@ $wikipage->set_gid($currentgroup);
 $wikipage->set_page($page);
 
 $context = context_module::instance($cm->id);
-if($pageid) {
-    $event = \mod_wiki\event\page_viewed::create(
-            array(
-                'context' => $context,
-                'objectid' => $pageid
-                )
-            );
-    $event->add_record_snapshot('wiki_pages', $page);
-} else if($id) {
-    $event = \mod_wiki\event\course_module_viewed::create(
-            array(
-                'context' => $context,
-                'objectid' => $wiki->id
-                )
-            );
-} else if($wid && $title) {
-    $event = \mod_wiki\event\page_viewed::create(
-            array(
-                'context' => $context,
-                'objectid' => $page->id,
-                'relateduserid' => $uid,
-                'other' => array(
-                    'title' => $title,
-                    'wid' => $wid,
-                    'group' => $gid,
-                    'groupanduser' => $groupanduser)
-                )
-            );
-    $event->add_record_snapshot('wiki_pages', $page);
+if ($pageid) {
+    wiki_page_view($wiki, $page, $course, $cm, $context, null, null, $subwiki);
+} else if ($id) {
+    wiki_view($wiki, $course, $cm, $context);
+} else if ($wid && $title) {
+    $other = array(
+        'title' => $title,
+        'wid' => $wid,
+        'group' => $gid,
+        'groupanduser' => $groupanduser
+    );
+    wiki_page_view($wiki, $page, $course, $cm, $context, $uid, $other, $subwiki);
 }
-$event->add_record_snapshot('course_modules', $cm);
-$event->add_record_snapshot('course', $course);
-$event->add_record_snapshot('wiki', $wiki);
-$event->trigger();
 
 $wikipage->print_header();
 $wikipage->print_content();
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
new file mode 100644 (file)
index 0000000..cd63c07
--- /dev/null
@@ -0,0 +1,2231 @@
+{
+  "name": "Moodle",
+  "dependencies": {
+    "grunt": {
+      "version": "0.4.5",
+      "from": "grunt@0.4.5",
+      "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz",
+      "dependencies": {
+        "async": {
+          "version": "0.1.22",
+          "from": "async@>=0.1.22 <0.2.0",
+          "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz"
+        },
+        "coffee-script": {
+          "version": "1.3.3",
+          "from": "coffee-script@>=1.3.3 <1.4.0",
+          "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz"
+        },
+        "colors": {
+          "version": "0.6.2",
+          "from": "colors@>=0.6.2 <0.7.0",
+          "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz"
+        },
+        "dateformat": {
+          "version": "1.0.2-1.2.3",
+          "from": "dateformat@1.0.2-1.2.3",
+          "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz"
+        },
+        "eventemitter2": {
+          "version": "0.4.14",
+          "from": "eventemitter2@>=0.4.13 <0.5.0",
+          "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz"
+        },
+        "findup-sync": {
+          "version": "0.1.3",
+          "from": "findup-sync@>=0.1.2 <0.2.0",
+          "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz",
+          "dependencies": {
+            "glob": {
+              "version": "3.2.11",
+              "from": "glob@>=3.2.9 <3.3.0",
+              "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
+              "dependencies": {
+                "inherits": {
+                  "version": "2.0.1",
+                  "from": "inherits@>=2.0.0 <3.0.0",
+                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                },
+                "minimatch": {
+                  "version": "0.3.0",
+                  "from": "minimatch@>=0.3.0 <0.4.0",
+                  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
+                  "dependencies": {
+                    "lru-cache": {
+                      "version": "2.7.3",
+                      "from": "lru-cache@>=2.0.0 <3.0.0",
+                      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
+                    },
+                    "sigmund": {
+                      "version": "1.0.1",
+                      "from": "sigmund@>=1.0.0 <1.1.0",
+                      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
+                    }
+                  }
+                }
+              }
+            },
+            "lodash": {
+              "version": "2.4.2",
+              "from": "lodash@>=2.4.1 <2.5.0",
+              "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
+            }
+          }
+        },
+        "glob": {
+          "version": "3.1.21",
+          "from": "glob@>=3.1.21 <3.2.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz",
+          "dependencies": {
+            "graceful-fs": {
+              "version": "1.2.3",
+              "from": "graceful-fs@>=1.2.0 <1.3.0",
+              "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz"
+            },
+            "inherits": {
+              "version": "1.0.2",
+              "from": "inherits@>=1.0.0 <2.0.0",
+              "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz"
+            }
+          }
+        },
+        "hooker": {
+          "version": "0.2.3",
+          "from": "hooker@>=0.2.3 <0.3.0",
+          "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz"
+        },
+        "iconv-lite": {
+          "version": "0.2.11",
+          "from": "iconv-lite@>=0.2.11 <0.3.0",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz"
+        },
+        "minimatch": {
+          "version": "0.2.14",
+          "from": "minimatch@>=0.2.12 <0.3.0",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
+          "dependencies": {
+            "lru-cache": {
+              "version": "2.7.3",
+              "from": "lru-cache@>=2.0.0 <3.0.0",
+              "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
+            },
+            "sigmund": {
+              "version": "1.0.1",
+              "from": "sigmund@>=1.0.0 <1.1.0",
+              "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
+            }
+          }
+        },
+        "nopt": {
+          "version": "1.0.10",
+          "from": "nopt@>=1.0.10 <1.1.0",
+          "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+          "dependencies": {
+            "abbrev": {
+              "version": "1.0.7",
+              "from": "abbrev@>=1.0.0 <2.0.0",
+              "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
+            }
+          }
+        },
+        "rimraf": {
+          "version": "2.2.8",
+          "from": "rimraf@>=2.2.8 <2.3.0",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz"
+        },
+        "lodash": {
+          "version": "0.9.2",
+          "from": "lodash@>=0.9.2 <0.10.0",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz"
+        },
+        "underscore.string": {
+          "version": "2.2.1",
+          "from": "underscore.string@>=2.2.1 <2.3.0",
+          "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz"
+        },
+        "which": {
+          "version": "1.0.9",
+          "from": "which@>=1.0.5 <1.1.0",
+          "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz"
+        },
+        "js-yaml": {
+          "version": "2.0.5",
+          "from": "js-yaml@>=2.0.5 <2.1.0",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz",
+          "dependencies": {
+            "argparse": {
+              "version": "0.1.16",
+              "from": "argparse@>=0.1.11 <0.2.0",
+              "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz",
+              "dependencies": {
+                "underscore": {
+                  "version": "1.7.0",
+                  "from": "underscore@>=1.7.0 <1.8.0",
+                  "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz"
+                },
+                "underscore.string": {
+                  "version": "2.4.0",
+                  "from": "underscore.string@>=2.4.0 <2.5.0",
+                  "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz"
+                }
+              }
+            },
+            "esprima": {
+              "version": "1.0.4",
+              "from": "esprima@>=1.0.2 <1.1.0",
+              "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz"
+            }
+          }
+        },
+        "exit": {
+          "version": "0.1.2",
+          "from": "exit@>=0.1.1 <0.2.0",
+          "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz"
+        },
+        "getobject": {
+          "version": "0.1.0",
+          "from": "getobject@>=0.1.0 <0.2.0",
+          "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz"
+        },
+        "grunt-legacy-util": {
+          "version": "0.2.0",
+          "from": "grunt-legacy-util@>=0.2.0 <0.3.0",
+          "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz"
+        },
+        "grunt-legacy-log": {
+          "version": "0.1.2",
+          "from": "grunt-legacy-log@>=0.1.0 <0.2.0",
+          "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.2.tgz",
+          "dependencies": {
+            "grunt-legacy-log-utils": {
+              "version": "0.1.1",
+              "from": "grunt-legacy-log-utils@>=0.1.1 <0.2.0",
+              "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz"
+            },
+            "lodash": {
+              "version": "2.4.2",
+              "from": "lodash@>=2.4.1 <2.5.0",
+              "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
+            },
+            "underscore.string": {
+              "version": "2.3.3",
+              "from": "underscore.string@>=2.3.3 <2.4.0",
+              "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz"
+            }
+          }
+        }
+      }
+    },
+    "grunt-contrib-jshint": {
+      "version": "0.11.3",
+      "from": "grunt-contrib-jshint@0.11.3",
+      "resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-0.11.3.tgz",
+      "dependencies": {
+        "hooker": {
+          "version": "0.2.3",
+          "from": "hooker@>=0.2.3 <0.3.0",
+          "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz"
+        },
+        "jshint": {
+          "version": "2.8.0",
+          "from": "jshint@>=2.8.0 <2.9.0",
+          "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.8.0.tgz",
+          "dependencies": {
+            "cli": {
+              "version": "0.6.6",
+              "from": "cli@>=0.6.0 <0.7.0",
+              "resolved": "https://registry.npmjs.org/cli/-/cli-0.6.6.tgz",
+              "dependencies": {
+                "glob": {
+                  "version": "3.2.11",
+                  "from": "glob@>=3.2.1 <3.3.0",
+                  "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
+                  "dependencies": {
+                    "inherits": {
+                      "version": "2.0.1",
+                      "from": "inherits@>=2.0.0 <3.0.0",
+                      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                    },
+                    "minimatch": {
+                      "version": "0.3.0",
+                      "from": "minimatch@>=0.3.0 <0.4.0",
+                      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
+                      "dependencies": {
+                        "lru-cache": {
+                          "version": "2.7.3",
+                          "from": "lru-cache@>=2.0.0 <3.0.0",
+                          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
+                        },
+                        "sigmund": {
+                          "version": "1.0.1",
+                          "from": "sigmund@>=1.0.0 <1.1.0",
+                          "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            },
+            "console-browserify": {
+              "version": "1.1.0",
+              "from": "console-browserify@>=1.1.0 <1.2.0",
+              "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+              "dependencies": {
+                "date-now": {
+                  "version": "0.1.4",
+                  "from": "date-now@>=0.1.4 <0.2.0",
+                  "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz"
+                }
+              }
+            },
+            "exit": {
+              "version": "0.1.2",
+              "from": "exit@>=0.1.0 <0.2.0",
+              "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz"
+            },
+            "htmlparser2": {
+              "version": "3.8.3",
+              "from": "htmlparser2@>=3.8.0 <3.9.0",
+              "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz",
+              "dependencies": {
+                "domhandler": {
+                  "version": "2.3.0",
+                  "from": "domhandler@>=2.3.0 <2.4.0",
+                  "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz"
+                },
+                "domutils": {
+                  "version": "1.5.1",
+                  "from": "domutils@>=1.5.0 <1.6.0",
+                  "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
+                  "dependencies": {
+                    "dom-serializer": {
+                      "version": "0.1.0",
+                      "from": "dom-serializer@>=0.0.0 <1.0.0",
+                      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+                      "dependencies": {
+                        "domelementtype": {
+                          "version": "1.1.3",
+                          "from": "domelementtype@>=1.1.1 <1.2.0",
+                          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz"
+                        },
+                        "entities": {
+                          "version": "1.1.1",
+                          "from": "entities@>=1.1.1 <1.2.0",
+                          "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz"
+                        }
+                      }
+                    }
+                  }
+                },
+                "domelementtype": {
+                  "version": "1.3.0",
+                  "from": "domelementtype@>=1.0.0 <2.0.0",
+                  "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz"
+                },
+                "readable-stream": {
+                  "version": "1.1.13",
+                  "from": "readable-stream@>=1.1.0 <1.2.0",
+                  "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.13.tgz",
+                  "dependencies": {
+                    "core-util-is": {
+                      "version": "1.0.2",
+                      "from": "core-util-is@>=1.0.0 <1.1.0",
+                      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+                    },
+                    "isarray": {
+                      "version": "0.0.1",
+                      "from": "isarray@0.0.1",
+                      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+                    },
+                    "string_decoder": {
+                      "version": "0.10.31",
+                      "from": "string_decoder@>=0.10.0 <0.11.0",
+                      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+                    },
+                    "inherits": {
+                      "version": "2.0.1",
+                      "from": "inherits@>=2.0.1 <2.1.0",
+                      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                    }
+                  }
+                },
+                "entities": {
+                  "version": "1.0.0",
+                  "from": "entities@>=1.0.0 <1.1.0",
+                  "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz"
+                }
+              }
+            },
+            "minimatch": {
+              "version": "2.0.10",
+              "from": "minimatch@>=2.0.0 <2.1.0",
+              "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz",
+              "dependencies": {
+                "brace-expansion": {
+                  "version": "1.1.2",
+                  "from": "brace-expansion@>=1.0.0 <2.0.0",
+                  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.2.tgz",
+                  "dependencies": {
+                    "balanced-match": {
+                      "version": "0.3.0",
+                      "from": "balanced-match@>=0.3.0 <0.4.0",
+                      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz"
+                    },
+                    "concat-map": {
+                      "version": "0.0.1",
+                      "from": "concat-map@0.0.1",
+                      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+                    }
+                  }
+                }
+              }
+            },
+            "shelljs": {
+              "version": "0.3.0",
+              "from": "shelljs@>=0.3.0 <0.4.0",
+              "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz"
+            },
+            "strip-json-comments": {
+              "version": "1.0.4",
+              "from": "strip-json-comments@>=1.0.0 <1.1.0",
+              "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz"
+            },
+            "lodash": {
+              "version": "3.7.0",
+              "from": "lodash@>=3.7.0 <3.8.0",
+              "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz"
+            }
+          }
+        }
+      }
+    },
+    "grunt-contrib-less": {
+      "version": "1.1.0",
+      "from": "grunt-contrib-less@1.1.0",
+      "resolved": "https://registry.npmjs.org/grunt-contrib-less/-/grunt-contrib-less-1.1.0.tgz",
+      "dependencies": {
+        "async": {
+          "version": "0.9.2",
+          "from": "async@>=0.9.0 <0.10.0",
+          "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz"
+        },
+        "chalk": {
+          "version": "1.1.1",
+          "from": "chalk@>=1.0.0 <2.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.1.tgz",
+          "dependencies": {
+            "ansi-styles": {
+              "version": "2.1.0",
+              "from": "ansi-styles@>=2.1.0 <3.0.0",
+              "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.1.0.tgz"
+            },
+            "escape-string-regexp": {
+              "version": "1.0.3",
+              "from": "escape-string-regexp@>=1.0.2 <2.0.0",
+              "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.3.tgz"
+            },
+            "has-ansi": {
+              "version": "2.0.0",
+              "from": "has-ansi@>=2.0.0 <3.0.0",
+              "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+              "dependencies": {
+                "ansi-regex": {
+                  "version": "2.0.0",
+                  "from": "ansi-regex@>=2.0.0 <3.0.0",
+                  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
+                }
+              }
+            },
+            "strip-ansi": {
+              "version": "3.0.0",
+              "from": "strip-ansi@>=3.0.0 <4.0.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.0.tgz",
+              "dependencies": {
+                "ansi-regex": {
+                  "version": "2.0.0",
+                  "from": "ansi-regex@>=2.0.0 <3.0.0",
+                  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
+                }
+              }
+            },
+            "supports-color": {
+              "version": "2.0.0",
+              "from": "supports-color@>=2.0.0 <3.0.0",
+              "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
+            }
+          }
+        },
+        "less": {
+          "version": "2.5.3",
+          "from": "less@>=2.5.0 <2.6.0",
+          "resolved": "https://registry.npmjs.org/less/-/less-2.5.3.tgz",
+          "dependencies": {
+            "errno": {
+              "version": "0.1.4",
+              "from": "errno@>=0.1.1 <0.2.0",
+              "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz",
+              "dependencies": {
+                "prr": {
+                  "version": "0.0.0",
+                  "from": "prr@>=0.0.0 <0.1.0",
+                  "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz"
+                }
+              }
+            },
+            "graceful-fs": {
+              "version": "3.0.8",
+              "from": "graceful-fs@>=3.0.5 <4.0.0",
+              "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.8.tgz"
+            },
+            "image-size": {
+              "version": "0.3.5",
+              "from": "image-size@>=0.3.5 <0.4.0",
+              "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.3.5.tgz"
+            },
+            "mime": {
+              "version": "1.3.4",
+              "from": "mime@>=1.2.11 <2.0.0",
+              "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
+            },
+            "mkdirp": {
+              "version": "0.5.1",
+              "from": "mkdirp@>=0.5.0 <0.6.0",
+              "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+              "dependencies": {
+                "minimist": {
+                  "version": "0.0.8",
+                  "from": "minimist@0.0.8",
+                  "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+                }
+              }
+            },
+            "promise": {
+              "version": "6.1.0",
+              "from": "promise@>=6.0.1 <7.0.0",
+              "resolved": "https://registry.npmjs.org/promise/-/promise-6.1.0.tgz",
+              "dependencies": {
+                "asap": {
+                  "version": "1.0.0",
+                  "from": "asap@>=1.0.0 <1.1.0",
+                  "resolved": "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz"
+                }
+              }
+            },
+            "request": {
+              "version": "2.67.0",
+              "from": "request@>=2.51.0 <3.0.0",
+              "resolved": "https://registry.npmjs.org/request/-/request-2.67.0.tgz",
+              "dependencies": {
+                "bl": {
+                  "version": "1.0.0",
+                  "from": "bl@>=1.0.0 <1.1.0",
+                  "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.0.tgz",
+                  "dependencies": {
+                    "readable-stream": {
+                      "version": "2.0.4",
+                      "from": "readable-stream@>=2.0.0 <2.1.0",
+                      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.4.tgz",
+                      "dependencies": {
+                        "core-util-is": {
+                          "version": "1.0.2",
+                          "from": "core-util-is@>=1.0.0 <1.1.0",
+                          "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+                        },
+                        "inherits": {
+                          "version": "2.0.1",
+                          "from": "inherits@>=2.0.1 <2.1.0",
+                          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                        },
+                        "isarray": {
+                          "version": "0.0.1",
+                          "from": "isarray@0.0.1",
+                          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+                        },
+                        "process-nextick-args": {
+                          "version": "1.0.6",
+                          "from": "process-nextick-args@>=1.0.0 <1.1.0",
+                          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz"
+                        },
+                        "string_decoder": {
+                          "version": "0.10.31",
+                          "from": "string_decoder@>=0.10.0 <0.11.0",
+                          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+                        },
+                        "util-deprecate": {
+                          "version": "1.0.2",
+                          "from": "util-deprecate@>=1.0.1 <1.1.0",
+                          "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
+                        }
+                      }
+                    }
+                  }
+                },
+                "caseless": {
+                  "version": "0.11.0",
+                  "from": "caseless@>=0.11.0 <0.12.0",
+                  "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz"
+                },
+                "extend": {
+                  "version": "3.0.0",
+                  "from": "extend@>=3.0.0 <3.1.0",
+                  "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz"
+                },
+                "forever-agent": {
+                  "version": "0.6.1",
+                  "from": "forever-agent@>=0.6.1 <0.7.0",
+                  "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz"
+                },
+                "form-data": {
+                  "version": "1.0.0-rc3",
+                  "from": "form-data@>=1.0.0-rc3 <1.1.0",
+                  "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc3.tgz",
+                  "dependencies": {
+                    "async": {
+                      "version": "1.5.0",
+                      "from": "async@>=1.4.0 <2.0.0",
+                      "resolved": "https://registry.npmjs.org/async/-/async-1.5.0.tgz"
+                    }
+                  }
+                },
+                "json-stringify-safe": {
+                  "version": "5.0.1",
+                  "from": "json-stringify-safe@>=5.0.1 <5.1.0",
+                  "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
+                },
+                "mime-types": {
+                  "version": "2.1.8",
+                  "from": "mime-types@>=2.1.7 <2.2.0",
+                  "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.8.tgz",
+                  "dependencies": {
+                    "mime-db": {
+                      "version": "1.20.0",
+                      "from": "mime-db@>=1.20.0 <1.21.0",
+                      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.20.0.tgz"
+                    }
+                  }
+                },
+                "node-uuid": {
+                  "version": "1.4.7",
+                  "from": "node-uuid@>=1.4.7 <1.5.0",
+                  "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz"
+                },
+                "qs": {
+                  "version": "5.2.0",
+                  "from": "qs@>=5.2.0 <5.3.0",
+                  "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz"
+                },
+                "tunnel-agent": {
+                  "version": "0.4.1",
+                  "from": "tunnel-agent@>=0.4.1 <0.5.0",
+                  "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.1.tgz"
+                },
+                "tough-cookie": {
+                  "version": "2.2.1",
+                  "from": "tough-cookie@>=2.2.0 <2.3.0",
+                  "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.1.tgz"
+                },
+                "http-signature": {
+                  "version": "1.1.0",
+                  "from": "http-signature@>=1.1.0 <1.2.0",
+                  "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.0.tgz",
+                  "dependencies": {
+                    "assert-plus": {
+                      "version": "0.1.5",
+                      "from": "assert-plus@>=0.1.5 <0.2.0",
+                      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
+                    },
+                    "jsprim": {
+                      "version": "1.2.2",
+                      "from": "jsprim@>=1.2.2 <2.0.0",
+                      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz",
+                      "dependencies": {
+                        "extsprintf": {
+                          "version": "1.0.2",
+                          "from": "extsprintf@1.0.2",
+                          "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz"
+                        },
+                        "json-schema": {
+                          "version": "0.2.2",
+                          "from": "json-schema@0.2.2",
+                          "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz"
+                        },
+                        "verror": {
+                          "version": "1.3.6",
+                          "from": "verror@1.3.6",
+                          "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz"
+                        }
+                      }
+                    },
+                    "sshpk": {
+                      "version": "1.7.1",
+                      "from": "sshpk@>=1.7.0 <2.0.0",
+                      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.7.1.tgz",
+                      "dependencies": {
+                        "asn1": {
+                          "version": "0.2.3",
+                          "from": "asn1@>=0.2.3 <0.3.0",
+                          "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz"
+                        },
+                        "assert-plus": {
+                          "version": "0.2.0",
+                          "from": "assert-plus@>=0.2.0 <0.3.0",
+                          "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz"
+                        },
+                        "dashdash": {
+                          "version": "1.10.1",
+                          "from": "dashdash@>=1.10.1 <2.0.0",
+                          "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.10.1.tgz",
+                          "dependencies": {
+                            "assert-plus": {
+                              "version": "0.1.5",
+                              "from": "assert-plus@>=0.1.0 <0.2.0",
+                              "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
+                            }
+                          }
+                        },
+                        "jsbn": {
+                          "version": "0.1.0",
+                          "from": "jsbn@>=0.1.0 <0.2.0",
+                          "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz"
+                        },
+                        "tweetnacl": {
+                          "version": "0.13.2",
+                          "from": "tweetnacl@>=0.13.0 <1.0.0",
+                          "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.2.tgz"
+                        },
+                        "jodid25519": {
+                          "version": "1.0.2",
+                          "from": "jodid25519@>=1.0.0 <2.0.0",
+                          "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz"
+                        },
+                        "ecc-jsbn": {
+                          "version": "0.1.1",
+                          "from": "ecc-jsbn@>=0.0.1 <1.0.0",
+                          "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz"
+                        }
+                      }
+                    }
+                  }
+                },
+                "oauth-sign": {
+                  "version": "0.8.0",
+                  "from": "oauth-sign@>=0.8.0 <0.9.0",
+                  "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.0.tgz"
+                },
+                "hawk": {
+                  "version": "3.1.2",
+                  "from": "hawk@>=3.1.0 <3.2.0",
+                  "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.2.tgz",
+                  "dependencies": {
+                    "hoek": {
+                      "version": "2.16.3",
+                      "from": "hoek@>=2.0.0 <3.0.0",
+                      "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz"
+                    },
+                    "boom": {
+                      "version": "2.10.1",
+                      "from": "boom@>=2.0.0 <3.0.0",
+                      "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz"
+                    },
+                    "cryptiles": {
+                      "version": "2.0.5",
+                      "from": "cryptiles@>=2.0.0 <3.0.0",
+                      "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz"
+                    },
+                    "sntp": {
+                      "version": "1.0.9",
+                      "from": "sntp@>=1.0.0 <2.0.0",
+                      "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz"
+                    }
+                  }
+                },
+                "aws-sign2": {
+                  "version": "0.6.0",
+                  "from": "aws-sign2@>=0.6.0 <0.7.0",
+                  "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz"
+                },
+                "stringstream": {
+                  "version": "0.0.5",
+                  "from": "stringstream@>=0.0.4 <0.1.0",
+                  "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz"
+                },
+                "combined-stream": {
+                  "version": "1.0.5",
+                  "from": "combined-stream@>=1.0.5 <1.1.0",
+                  "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
+                  "dependencies": {
+                    "delayed-stream": {
+                      "version": "1.0.0",
+                      "from": "delayed-stream@>=1.0.0 <1.1.0",
+                      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
+                    }
+                  }
+                },
+                "isstream": {
+                  "version": "0.1.2",
+                  "from": "isstream@>=0.1.2 <0.2.0",
+                  "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz"
+                },
+                "is-typedarray": {
+                  "version": "1.0.0",
+                  "from": "is-typedarray@>=1.0.0 <1.1.0",
+                  "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz"
+                },
+                "har-validator": {
+                  "version": "2.0.3",
+                  "from": "har-validator@>=2.0.2 <2.1.0",
+                  "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.3.tgz",
+                  "dependencies": {
+                    "commander": {
+                      "version": "2.9.0",
+                      "from": "commander@>=2.9.0 <3.0.0",
+                      "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
+                      "dependencies": {
+                        "graceful-readlink": {
+                          "version": "1.0.1",
+                          "from": "graceful-readlink@>=1.0.0",
+                          "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz"
+                        }
+                      }
+                    },
+                    "is-my-json-valid": {
+                      "version": "2.12.3",
+                      "from": "is-my-json-valid@>=2.12.3 <3.0.0",
+                      "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.12.3.tgz",
+                      "dependencies": {
+                        "generate-function": {
+                          "version": "2.0.0",
+                          "from": "generate-function@>=2.0.0 <3.0.0",
+                          "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz"
+                        },
+                        "generate-object-property": {
+                          "version": "1.2.0",
+                          "from": "generate-object-property@>=1.1.0 <2.0.0",
+                          "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
+                          "dependencies": {
+                            "is-property": {
+                              "version": "1.0.2",
+                              "from": "is-property@>=1.0.0 <2.0.0",
+                              "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz"
+                            }
+                          }
+                        },
+                        "jsonpointer": {
+                          "version": "2.0.0",
+                          "from": "jsonpointer@2.0.0",
+                          "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz"
+                        },
+                        "xtend": {
+                          "version": "4.0.1",
+                          "from": "xtend@>=4.0.0 <5.0.0",
+                          "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
+                        }
+                      }
+                    },
+                    "pinkie-promise": {
+                      "version": "2.0.0",
+                      "from": "pinkie-promise@>=2.0.0 <3.0.0",
+                      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.0.tgz",
+                      "dependencies": {
+                        "pinkie": {
+                          "version": "2.0.1",
+                          "from": "pinkie@>=2.0.0 <3.0.0",
+                          "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.1.tgz"
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            },
+            "source-map": {
+              "version": "0.4.4",
+              "from": "source-map@>=0.4.2 <0.5.0",
+              "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+              "dependencies": {
+                "amdefine": {
+                  "version": "1.0.0",
+                  "from": "amdefine@>=0.0.4",
+                  "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz"
+                }
+              }
+            }
+          }
+        },
+        "lodash": {
+          "version": "3.10.1",
+          "from": "lodash@>=3.2.0 <4.0.0",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz"
+        }
+      }
+    },
+    "grunt-contrib-uglify": {
+      "version": "0.11.0",
+      "from": "grunt-contrib-uglify@0.11.0",
+      "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-0.11.0.tgz",
+      "dependencies": {
+        "chalk": {
+          "version": "1.1.1",
+          "from": "chalk@>=1.0.0 <2.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.1.tgz",
+          "dependencies": {
+            "ansi-styles": {
+              "version": "2.1.0",
+              "from": "ansi-styles@>=2.1.0 <3.0.0",
+              "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.1.0.tgz"
+            },
+            "escape-string-regexp": {
+              "version": "1.0.3",
+              "from": "escape-string-regexp@>=1.0.2 <2.0.0",
+              "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.3.tgz"
+            },
+            "has-ansi": {
+              "version": "2.0.0",
+              "from": "has-ansi@>=2.0.0 <3.0.0",
+              "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+              "dependencies": {
+                "ansi-regex": {
+                  "version": "2.0.0",
+                  "from": "ansi-regex@>=2.0.0 <3.0.0",
+                  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
+                }
+              }
+            },
+            "strip-ansi": {
+              "version": "3.0.0",
+              "from": "strip-ansi@>=3.0.0 <4.0.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.0.tgz",
+              "dependencies": {
+                "ansi-regex": {
+                  "version": "2.0.0",
+                  "from": "ansi-regex@>=2.0.0 <3.0.0",
+                  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
+                }
+              }
+            },
+            "supports-color": {
+              "version": "2.0.0",
+              "from": "supports-color@>=2.0.0 <3.0.0",
+              "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
+            }
+          }
+        },
+        "lodash": {
+          "version": "3.10.1",
+          "from": "lodash@>=3.2.0 <4.0.0",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz"
+        },
+        "maxmin": {
+          "version": "1.1.0",
+          "from": "maxmin@>=1.0.0 <2.0.0",
+          "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-1.1.0.tgz",
+          "dependencies": {
+            "figures": {
+              "version": "1.4.0",
+              "from": "figures@>=1.0.1 <2.0.0",
+              "resolved": "https://registry.npmjs.org/figures/-/figures-1.4.0.tgz"
+            },
+            "gzip-size": {
+              "version": "1.0.0",
+              "from": "gzip-size@>=1.0.0 <2.0.0",
+              "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-1.0.0.tgz",
+              "dependencies": {
+                "concat-stream": {
+                  "version": "1.5.1",
+                  "from": "concat-stream@>=1.4.1 <2.0.0",
+                  "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.1.tgz",
+                  "dependencies": {
+                    "inherits": {
+                      "version": "2.0.1",
+                      "from": "inherits@>=2.0.1 <2.1.0",
+                      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                    },
+                    "typedarray": {
+                      "version": "0.0.6",
+                      "from": "typedarray@>=0.0.5 <0.1.0",
+                      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
+                    },
+                    "readable-stream": {
+                      "version": "2.0.4",
+                      "from": "readable-stream@>=2.0.0 <2.1.0",
+                      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.4.tgz",
+                      "dependencies": {
+                        "core-util-is": {
+                          "version": "1.0.2",
+                          "from": "core-util-is@>=1.0.0 <1.1.0",
+                          "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+                        },
+                        "isarray": {
+                          "version": "0.0.1",
+                          "from": "isarray@0.0.1",
+                          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+                        },
+                        "process-nextick-args": {
+                          "version": "1.0.6",
+                          "from": "process-nextick-args@>=1.0.0 <1.1.0",
+                          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz"
+                        },
+                        "string_decoder": {
+                          "version": "0.10.31",
+                          "from": "string_decoder@>=0.10.0 <0.11.0",
+                          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+                        },
+                        "util-deprecate": {
+                          "version": "1.0.2",
+                          "from": "util-deprecate@>=1.0.1 <1.1.0",
+                          "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
+                        }
+                      }
+                    }
+                  }
+                },
+                "browserify-zlib": {
+                  "version": "0.1.4",
+                  "from": "browserify-zlib@>=0.1.4 <0.2.0",
+                  "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz",
+                  "dependencies": {
+                    "pako": {
+                      "version": "0.2.8",
+                      "from": "pako@>=0.2.0 <0.3.0",
+                      "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.8.tgz"
+                    }
+                  }
+                }
+              }
+            },
+            "pretty-bytes": {
+              "version": "1.0.4",
+              "from": "pretty-bytes@>=1.0.0 <2.0.0",
+              "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz",
+              "dependencies": {
+                "get-stdin": {
+                  "version": "4.0.1",
+                  "from": "get-stdin@>=4.0.1 <5.0.0",
+                  "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz"
+                },
+                "meow": {
+                  "version": "3.6.0",
+                  "from": "meow@>=3.1.0 <4.0.0",
+                  "resolved": "https://registry.npmjs.org/meow/-/meow-3.6.0.tgz",
+                  "dependencies": {
+                    "camelcase-keys": {
+                      "version": "2.0.0",
+                      "from": "camelcase-keys@>=2.0.0 <3.0.0",
+                      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.0.0.tgz",
+                      "dependencies": {
+                        "camelcase": {
+                          "version": "2.0.1",
+                          "from": "camelcase@>=2.0.0 <3.0.0",
+                          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.0.1.tgz"
+                        },
+                        "map-obj": {
+                          "version": "1.0.1",
+                          "from": "map-obj@>=1.0.0 <2.0.0",
+                          "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz"
+                        }
+                      }
+                    },
+                    "loud-rejection": {
+                      "version": "1.2.0",
+                      "from": "loud-rejection@>=1.0.0 <2.0.0",
+                      "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.2.0.tgz",
+                      "dependencies": {
+                        "signal-exit": {
+                          "version": "2.1.2",
+                          "from": "signal-exit@>=2.1.2 <3.0.0",
+                          "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-2.1.2.tgz"
+                        }
+                      }
+                    },
+                    "minimist": {
+                      "version": "1.2.0",
+                      "from": "minimist@>=1.1.3 <2.0.0",
+                      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+                    },
+                    "normalize-package-data": {
+                      "version": "2.3.5",
+                      "from": "normalize-package-data@>=2.3.4 <3.0.0",
+                      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.5.tgz",
+                      "dependencies": {
+                        "hosted-git-info": {
+                          "version": "2.1.4",
+                          "from": "hosted-git-info@>=2.1.4 <3.0.0",
+                          "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.1.4.tgz"
+                        },
+                        "is-builtin-module": {
+                          "version": "1.0.0",
+                          "from": "is-builtin-module@>=1.0.0 <2.0.0",
+                          "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+                          "dependencies": {
+                            "builtin-modules": {
+                              "version": "1.1.0",
+                              "from": "builtin-modules@>=1.0.0 <2.0.0",
+                              "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.0.tgz"
+                            }
+                          }
+                        },
+                        "semver": {
+                          "version": "5.1.0",
+                          "from": "semver@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0||>=4.0.0 <5.0.0||>=5.0.0 <6.0.0",
+                          "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz"
+                        },
+                        "validate-npm-package-license": {
+                          "version": "3.0.1",
+                          "from": "validate-npm-package-license@>=3.0.1 <4.0.0",
+                          "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",
+                          "dependencies": {
+                            "spdx-correct": {
+                              "version": "1.0.2",
+                              "from": "spdx-correct@>=1.0.0 <1.1.0",
+                              "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz",
+                              "dependencies": {
+                                "spdx-license-ids": {
+                                  "version": "1.1.0",
+                                  "from": "spdx-license-ids@>=1.0.2 <2.0.0",
+                                  "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.1.0.tgz"
+                                }
+                              }
+                            },
+                            "spdx-expression-parse": {
+                              "version": "1.0.2",
+                              "from": "spdx-expression-parse@>=1.0.0 <1.1.0",
+                              "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.2.tgz",
+                              "dependencies": {
+                                "spdx-exceptions": {
+                                  "version": "1.0.4",
+                                  "from": "spdx-exceptions@>=1.0.4 <2.0.0",
+                                  "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-1.0.4.tgz"
+                                },
+                                "spdx-license-ids": {
+                                  "version": "1.1.0",
+                                  "from": "spdx-license-ids@>=1.0.2 <2.0.0",
+                                  "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.1.0.tgz"
+                                }
+                              }
+                            }
+                          }
+                        }
+                      }
+                    },
+                    "object-assign": {
+                      "version": "4.0.1",
+                      "from": "object-assign@>=4.0.1 <5.0.0",
+                      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.0.1.tgz"
+                    },
+                    "read-pkg-up": {
+                      "version": "1.0.1",
+                      "from": "read-pkg-up@>=1.0.1 <2.0.0",
+                      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+                      "dependencies": {
+                        "find-up": {
+                          "version": "1.1.0",
+                          "from": "find-up@>=1.0.0 <2.0.0",
+                          "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.0.tgz",
+                          "dependencies": {
+                            "path-exists": {
+                              "version": "2.1.0",
+                              "from": "path-exists@>=2.0.0 <3.0.0",
+                              "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz"
+                            },
+                            "pinkie-promise": {
+                              "version": "2.0.0",
+                              "from": "pinkie-promise@>=2.0.0 <3.0.0",
+                              "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.0.tgz",
+                              "dependencies": {
+                                "pinkie": {
+                                  "version": "2.0.1",
+                                  "from": "pinkie@>=2.0.0 <3.0.0",
+                                  "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.1.tgz"
+                                }
+                              }
+                            }
+                          }
+                        },
+                        "read-pkg": {
+                          "version": "1.1.0",
+                          "from": "read-pkg@>=1.0.0 <2.0.0",
+                          "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+                          "dependencies": {
+                            "load-json-file": {
+                              "version": "1.1.0",
+                              "from": "load-json-file@>=1.0.0 <2.0.0",
+                              "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+                              "dependencies": {
+                                "graceful-fs": {
+                                  "version": "4.1.2",
+                                  "from": "graceful-fs@>=4.1.2 <5.0.0",
+                                  "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.2.tgz"
+                                },
+                                "parse-json": {
+                                  "version": "2.2.0",
+                                  "from": "parse-json@>=2.2.0 <3.0.0",
+                                  "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+                                  "dependencies": {
+                                    "error-ex": {
+                                      "version": "1.3.0",
+                                      "from": "error-ex@>=1.2.0 <2.0.0",
+                                      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz",
+                                      "dependencies": {
+                                        "is-arrayish": {
+                                          "version": "0.2.1",
+                                          "from": "is-arrayish@>=0.2.1 <0.3.0",
+                                          "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"
+                                        }
+                                      }
+                                    }
+                                  }
+                                },
+                                "pify": {
+                                  "version": "2.3.0",
+                                  "from": "pify@>=2.0.0 <3.0.0",
+                                  "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"
+                                },
+                                "pinkie-promise": {
+                                  "version": "2.0.0",
+                                  "from": "pinkie-promise@>=2.0.0 <3.0.0",
+                                  "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.0.tgz",
+                                  "dependencies": {
+                                    "pinkie": {
+                                      "version": "2.0.1",
+                                      "from": "pinkie@>=2.0.0 <3.0.0",
+                                      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.1.tgz"
+                                    }
+                                  }
+                                },
+                                "strip-bom": {
+                                  "version": "2.0.0",
+                                  "from": "strip-bom@>=2.0.0 <3.0.0",
+                                  "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+                                  "dependencies": {
+                                    "is-utf8": {
+                                      "version": "0.2.0",
+                                      "from": "is-utf8@>=0.2.0 <0.3.0",
+                                      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.0.tgz"
+                                    }
+                                  }
+                                }
+                              }
+                            },
+                            "path-type": {
+                              "version": "1.1.0",
+                              "from": "path-type@>=1.0.0 <2.0.0",
+                              "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+                              "dependencies": {
+                                "graceful-fs": {
+                                  "version": "4.1.2",
+                                  "from": "graceful-fs@>=4.1.2 <5.0.0",
+                                  "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.2.tgz"
+                                },
+                                "pify": {
+                                  "version": "2.3.0",
+                                  "from": "pify@>=2.0.0 <3.0.0",
+                                  "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"
+                                },
+                                "pinkie-promise": {
+                                  "version": "2.0.0",
+                                  "from": "pinkie-promise@>=2.0.0 <3.0.0",
+                                  "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.0.tgz",
+                                  "dependencies": {
+                                    "pinkie": {
+                                      "version": "2.0.1",
+                                      "from": "pinkie@>=2.0.0 <3.0.0",
+                                      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.1.tgz"
+                                    }
+                                  }
+                                }
+                              }
+                            }
+                          }
+                        }
+                      }
+                    },
+                    "redent": {
+                      "version": "1.0.0",
+                      "from": "redent@>=1.0.0 <2.0.0",
+                      "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+                      "dependencies": {
+                        "indent-string": {
+                          "version": "2.1.0",
+                          "from": "indent-string@>=2.1.0 <3.0.0",
+                          "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+                          "dependencies": {
+                            "repeating": {
+                              "version": "2.0.0",
+                              "from": "repeating@>=2.0.0 <3.0.0",
+                              "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.0.tgz",
+                              "dependencies": {
+                                "is-finite": {
+                                  "version": "1.0.1",
+                                  "from": "is-finite@>=1.0.0 <2.0.0",
+                                  "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.1.tgz",
+                                  "dependencies": {
+                                    "number-is-nan": {
+                                      "version": "1.0.0",
+                                      "from": "number-is-nan@>=1.0.0 <2.0.0",
+                                      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz"
+                                    }
+                                  }
+                                }
+                              }
+                            }
+                          }
+                        },
+                        "strip-indent": {
+                          "version": "1.0.1",
+                          "from": "strip-indent@>=1.0.1 <2.0.0",
+                          "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz"
+                        }
+                      }
+                    },
+                    "trim-newlines": {
+                      "version": "1.0.0",
+                      "from": "trim-newlines@>=1.0.0 <2.0.0",
+                      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz"
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        "uglify-js": {
+          "version": "2.6.1",
+          "from": "uglify-js@>=2.6.0 <2.7.0",
+          "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.6.1.tgz",
+          "dependencies": {
+            "async": {
+              "version": "0.2.10",
+              "from": "async@>=0.2.6 <0.3.0",
+              "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
+            },
+            "source-map": {
+              "version": "0.5.3",
+              "from": "source-map@>=0.5.1 <0.6.0",
+              "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.3.tgz"
+            },
+            "uglify-to-browserify": {
+              "version": "1.0.2",
+              "from": "uglify-to-browserify@>=1.0.0 <1.1.0",
+              "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz"
+            },
+            "yargs": {
+              "version": "3.10.0",
+              "from": "yargs@>=3.10.0 <3.11.0",
+              "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+              "dependencies": {
+                "camelcase": {
+                  "version": "1.2.1",
+                  "from": "camelcase@>=1.0.2 <2.0.0",
+                  "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz"
+                },
+                "cliui": {
+                  "version": "2.1.0",
+                  "from": "cliui@>=2.1.0 <3.0.0",
+                  "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+                  "dependencies": {
+                    "center-align": {
+                      "version": "0.1.2",
+                      "from": "center-align@>=0.1.1 <0.2.0",
+                      "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.2.tgz",
+                      "dependencies": {
+                        "align-text": {
+                          "version": "0.1.3",
+                          "from": "align-text@>=0.1.1 <0.2.0",
+                          "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.3.tgz",
+                          "dependencies": {
+                            "kind-of": {
+                              "version": "2.0.1",
+                              "from": "kind-of@>=2.0.0 <3.0.0",
+                              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz",
+                              "dependencies": {
+                                "is-buffer": {
+                                  "version": "1.1.0",
+                                  "from": "is-buffer@>=1.0.2 <2.0.0",
+                                  "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.0.tgz"
+                                }
+                              }
+                            },
+                            "longest": {
+                              "version": "1.0.1",
+                              "from": "longest@>=1.0.1 <2.0.0",
+                              "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz"
+                            },
+                            "repeat-string": {
+                              "version": "1.5.2",
+                              "from": "repeat-string@>=1.5.2 <2.0.0",
+                              "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.5.2.tgz"
+                            }
+                          }
+                        },
+                        "lazy-cache": {
+                          "version": "0.2.4",
+                          "from": "lazy-cache@>=0.2.4 <0.3.0",
+                          "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.4.tgz"
+                        }
+                      }
+                    },
+                    "right-align": {
+                      "version": "0.1.3",
+                      "from": "right-align@>=0.1.1 <0.2.0",
+                      "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
+                      "dependencies": {
+                        "align-text": {
+                          "version": "0.1.3",
+                          "from": "align-text@>=0.1.1 <0.2.0",
+                          "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.3.tgz",
+                          "dependencies": {
+                            "kind-of": {
+                              "version": "2.0.1",
+                              "from": "kind-of@>=2.0.0 <3.0.0",
+                              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz",
+                              "dependencies": {
+                                "is-buffer": {
+                                  "version": "1.1.0",
+                                  "from": "is-buffer@>=1.0.2 <2.0.0",
+                                  "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.0.tgz"
+                                }
+                              }
+                            },
+                            "longest": {
+                              "version": "1.0.1",
+                              "from": "longest@>=1.0.1 <2.0.0",
+                              "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz"
+                            },
+                            "repeat-string": {
+                              "version": "1.5.2",
+                              "from": "repeat-string@>=1.5.2 <2.0.0",
+                              "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.5.2.tgz"
+                            }
+                          }
+                        }
+                      }
+                    },
+                    "wordwrap": {
+                      "version": "0.0.2",
+                      "from": "wordwrap@0.0.2",
+                      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz"
+                    }
+                  }
+                },
+                "decamelize": {
+                  "version": "1.1.1",
+                  "from": "decamelize@>=1.0.0 <2.0.0",
+                  "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.1.1.tgz"
+                },
+                "window-size": {
+                  "version": "0.1.0",
+                  "from": "window-size@0.1.0",
+                  "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz"
+                }
+              }
+            }
+          }
+        },
+        "uri-path": {
+          "version": "1.0.0",
+          "from": "uri-path@>=1.0.0 <2.0.0",
+          "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz"
+        }
+      }
+    },
+    "shifter": {
+      "version": "0.5.0",
+      "from": "shifter@0.5.0",
+      "resolved": "https://registry.npmjs.org/shifter/-/shifter-0.5.0.tgz",
+      "dependencies": {
+        "progress": {
+          "version": "0.1.0",
+          "from": "progress@>=0.1.0 <0.2.0",
+          "resolved": "https://registry.npmjs.org/progress/-/progress-0.1.0.tgz"
+        },
+        "ansi-color": {
+          "version": "0.2.1",
+          "from": "ansi-color@*",
+          "resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz"
+        },
+        "gear": {
+          "version": "0.8.18",
+          "from": "gear@>=0.8.0 <0.9.0",
+          "resolved": "https://registry.npmjs.org/gear/-/gear-0.8.18.tgz",
+          "dependencies": {
+            "async": {
+              "version": "0.2.10",
+              "from": "async@>=0.2.0 <0.3.0",
+              "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
+            },
+            "mkdirp": {
+              "version": "0.3.5",
+              "from": "mkdirp@>=0.3.0 <0.4.0",
+              "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
+            }
+          }
+        },
+        "gear-lib": {
+          "version": "0.8.15",
+          "from": "gear-lib@>=0.8.0 <0.9.0",
+          "resolved": "https://registry.npmjs.org/gear-lib/-/gear-lib-0.8.15.tgz",
+          "dependencies": {
+            "jslint": {
+              "version": "0.3.4",
+              "from": "jslint@>=0.3.0 <0.4.0",
+              "resolved": "https://registry.npmjs.org/jslint/-/jslint-0.3.4.tgz",
+              "dependencies": {
+                "nopt": {
+                  "version": "1.0.10",
+                  "from": "nopt@>=1.0.0 <1.1.0",
+                  "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+                  "dependencies": {
+                    "abbrev": {
+                      "version": "1.0.7",
+                      "from": "abbrev@>=1.0.0 <2.0.0",
+                      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
+                    }
+                  }
+                }
+              }
+            },
+            "jshint": {
+              "version": "2.5.11",
+              "from": "jshint@>=2.5.0 <2.6.0",
+              "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.5.11.tgz",
+              "dependencies": {
+                "cli": {
+                  "version": "0.6.6",
+                  "from": "cli@>=0.6.0 <0.7.0",
+                  "resolved": "https://registry.npmjs.org/cli/-/cli-0.6.6.tgz"
+                },
+                "console-browserify": {
+                  "version": "1.1.0",
+                  "from": "console-browserify@>=1.1.0 <1.2.0",
+                  "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+                  "dependencies": {
+                    "date-now": {
+                      "version": "0.1.4",
+                      "from": "date-now@>=0.1.4 <0.2.0",
+                      "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz"
+                    }
+                  }
+                },
+                "exit": {
+                  "version": "0.1.2",
+                  "from": "exit@>=0.1.0 <0.2.0",
+                  "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz"
+                },
+                "htmlparser2": {
+                  "version": "3.8.3",
+                  "from": "htmlparser2@>=3.8.0 <3.9.0",
+                  "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz",
+                  "dependencies": {
+                    "domhandler": {
+                      "version": "2.3.0",
+                      "from": "domhandler@>=2.3.0 <2.4.0",
+                      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz"
+                    },
+                    "domutils": {
+                      "version": "1.5.1",
+                      "from": "domutils@>=1.5.0 <1.6.0",
+                      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
+                      "dependencies": {
+                        "dom-serializer": {
+                          "version": "0.1.0",
+                          "from": "dom-serializer@>=0.0.0 <1.0.0",
+                          "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+                          "dependencies": {
+                            "domelementtype": {
+                              "version": "1.1.3",
+                              "from": "domelementtype@>=1.1.1 <1.2.0",
+                              "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz"
+                            },
+                            "entities": {
+                              "version": "1.1.1",
+                              "from": "entities@>=1.1.1 <1.2.0",
+                              "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz"
+                            }
+                          }
+                        }
+                      }
+                    },
+                    "domelementtype": {
+                      "version": "1.3.0",
+                      "from": "domelementtype@>=1.0.0 <2.0.0",
+                      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz"
+                    },
+                    "readable-stream": {
+                      "version": "1.1.13",
+                      "from": "readable-stream@>=1.1.0 <1.2.0",
+                      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.13.tgz",
+                      "dependencies": {
+                        "core-util-is": {
+                          "version": "1.0.2",
+                          "from": "core-util-is@>=1.0.0 <1.1.0",
+                          "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+                        },
+                        "isarray": {
+                          "version": "0.0.1",
+                          "from": "isarray@0.0.1",
+                          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+                        },
+                        "string_decoder": {
+                          "version": "0.10.31",
+                          "from": "string_decoder@>=0.10.0 <0.11.0",
+                          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+                        },
+                        "inherits": {
+                          "version": "2.0.1",
+                          "from": "inherits@>=2.0.1 <2.1.0",
+                          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                        }
+                      }
+                    },
+                    "entities": {
+                      "version": "1.0.0",
+                      "from": "entities@>=1.0.0 <1.1.0",
+                      "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz"
+                    }
+                  }
+                },
+                "minimatch": {
+                  "version": "1.0.0",
+                  "from": "minimatch@>=1.0.0 <1.1.0",
+                  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-1.0.0.tgz",
+                  "dependencies": {
+                    "lru-cache": {
+                      "version": "2.7.3",
+                      "from": "lru-cache@>=2.0.0 <3.0.0",
+                      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
+                    },
+                    "sigmund": {
+                      "version": "1.0.1",
+                      "from": "sigmund@>=1.0.0 <1.1.0",
+                      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
+                    }
+                  }
+                },
+                "shelljs": {
+                  "version": "0.3.0",
+                  "from": "shelljs@>=0.3.0 <0.4.0",
+                  "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz"
+                },
+                "strip-json-comments": {
+                  "version": "1.0.4",
+                  "from": "strip-json-comments@>=1.0.0 <1.1.0",
+                  "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz"
+                },
+                "underscore": {
+                  "version": "1.6.0",
+                  "from": "underscore@>=1.6.0 <1.7.0",
+                  "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz"
+                }
+              }
+            },
+            "uglify-js": {
+              "version": "1.3.5",
+              "from": "uglify-js@>=1.3.0 <1.4.0",
+              "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.3.5.tgz"
+            },
+            "csslint": {
+              "version": "0.10.0",
+              "from": "csslint@>=0.10.0 <0.11.0",
+              "resolved": "https://registry.npmjs.org/csslint/-/csslint-0.10.0.tgz",
+              "dependencies": {
+                "parserlib": {
+                  "version": "0.2.5",
+                  "from": "parserlib@>=0.2.2 <0.3.0",
+                  "resolved": "https://registry.npmjs.org/parserlib/-/parserlib-0.2.5.tgz"
+                }
+              }
+            },
+            "less": {
+              "version": "1.3.3",
+              "from": "less@>=1.3.0 <1.4.0",
+              "resolved": "https://registry.npmjs.org/less/-/less-1.3.3.tgz",
+              "dependencies": {
+                "ycssmin": {
+                  "version": "1.0.1",
+                  "from": "ycssmin@>=1.0.1",
+                  "resolved": "https://registry.npmjs.org/ycssmin/-/ycssmin-1.0.1.tgz"
+                }
+              }
+            },
+            "handlebars": {
+              "version": "2.0.0",
+              "from": "handlebars@>=2.0.0 <2.1.0",
+              "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-2.0.0.tgz",
+              "dependencies": {
+                "optimist": {
+                  "version": "0.3.7",
+