Merge branch 'MDL-50999-master' of git://github.com/andrewnicols/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 1 Sep 2015 06:35:18 +0000 (14:35 +0800)
committerDavid Monllao <davidm@moodle.com>
Tue, 1 Sep 2015 06:35:18 +0000 (14:35 +0800)
362 files changed:
admin/cli/install.php
admin/environment.xml
admin/index.php
admin/search.php
admin/settings.php
admin/settings/plugins.php
admin/tool/behat/tests/behat/edit_permissions.feature
admin/tool/behat/tests/behat/nasty_strings.feature
admin/tool/customlang/edit.php
admin/tool/customlang/renderer.php
admin/tool/health/lang/en/tool_health.php
admin/tool/langimport/lang/en/tool_langimport.php
admin/tool/langimport/tests/behat/manage_langpacks.feature
admin/tool/uploaduser/index.php
admin/tool/uploaduser/user_form.php
admin/upgradesettings.php
auth/cas/auth.php
auth/db/auth.php
availability/condition/grade/classes/condition.php
availability/tests/component_test.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_course_format_test.php
blocks/messages/block_messages.php
blocks/online_users/block_online_users.php
blocks/online_users/classes/fetcher.php [new file with mode: 0644]
blocks/online_users/tests/generator/lib.php
blocks/online_users/tests/online_users_test.php [new file with mode: 0644]
blocks/rss_client/block_rss_client.php
blocks/rss_client/db/install.xml
blocks/rss_client/db/upgrade.php [new file with mode: 0644]
blocks/rss_client/tests/cron_test.php [new file with mode: 0644]
blocks/rss_client/version.php
blocks/site_main_menu/block_site_main_menu.php
blocks/site_main_menu/tests/behat/add_url.feature [new file with mode: 0644]
blocks/tag_youtube/block_tag_youtube.php
blocks/tag_youtube/db/install.php [new file with mode: 0644]
blocks/tag_youtube/lang/en/block_tag_youtube.php
blocks/tag_youtube/settings.php [new file with mode: 0644]
blocks/tag_youtube/tests/block_tag_youtube_test.php [new file with mode: 0644]
blocks/tag_youtube/upgrade.txt [new file with mode: 0644]
blocks/tag_youtube/version.php
blocks/tags/tests/behat/tagcloud.feature
blocks/tests/behat/restrict_available_blocks.feature
blog/tests/behat/blog_visibility.feature
cache/classes/helper.php
cache/stores/file/lib.php
cache/stores/file/tests/file_test.php
cache/tests/cache_test.php
calendar/renderer.php
calendar/set.php
completion/tests/behat/behat_completion.php
composer.json
composer.lock
config-dist.php
course/format/singleactivity/lib.php
course/modlib.php
course/renderer.php
course/tests/behat/navigate_course_list.feature
course/tests/behat/restrict_available_activities.feature
enrol/locallib.php
enrol/meta/locallib.php
enrol/otherusers.php
enrol/renderer.php
grade/edit/tree/item.php
grade/edit/tree/outcomeitem.php
grade/import/csv/classes/load_data.php
grade/import/csv/tests/load_data_test.php
grade/import/lib.php
grade/lib.php
grade/report/grader/lib.php
grade/report/singleview/lib.php
grade/report/user/lib.php
grade/tests/behat/grade_aggregation.feature
grade/tests/behat/grade_aggregation_changes.feature [new file with mode: 0644]
grade/tests/behat/grade_scales.feature
grade/tests/behat/grade_single_item_scales.feature
grade/tests/behat/grade_view.feature
grade/tests/importlib_test.php [new file with mode: 0644]
group/module.js
group/overview.php
group/tests/behat/create_groups.feature
install/lang/gl/error.php
install/lang/oc_gsc/admin.php [new file with mode: 0644]
install/lang/oc_gsc/error.php [new file with mode: 0644]
install/lang/oc_gsc/install.php [new file with mode: 0644]
install/lang/oc_gsc/langconfig.php
install/lang/oc_gsc/moodle.php [new file with mode: 0644]
install/lang/pt_br/error.php
install/lang/sma/langconfig.php [moved from install/lang/sr/langconfig.php with 94% similarity]
install/lang/smj/langconfig.php [moved from install/lang/sr_cr_bo/langconfig.php with 94% similarity]
lang/en/admin.php
lang/en/auth.php
lang/en/cache.php
lang/en/deprecated.txt
lang/en/grades.php
lang/en/hub.php
lang/en/install.php
lang/en/media.php
lang/en/message.php
lang/en/notes.php
lang/en/role.php
lib/adodb/adodb.inc.php
lib/adodb/readme_moodle.txt
lib/ajax/blocks.php
lib/amd/build/mustache.min.js
lib/amd/build/templates.min.js
lib/amd/src/mustache.js
lib/amd/src/templates.js
lib/authlib.php
lib/badgeslib.php
lib/behat/behat_files.php
lib/behat/classes/behat_command.php
lib/classes/task/manager.php
lib/db/access.php
lib/db/caches.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/dml/mssql_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/editor/atto/plugins/subscript/tests/behat/subscript.feature
lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-debug.js
lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-min.js
lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button.js
lib/editor/atto/plugins/subscript/yui/src/button/js/button.js
lib/editor/atto/plugins/superscript/tests/behat/superscript.feature
lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-debug.js
lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-min.js
lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button.js
lib/editor/atto/plugins/superscript/yui/src/button/js/button.js
lib/editor/atto/plugins/table/lang/en/atto_table.php
lib/editor/atto/plugins/table/lib.php
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js
lib/editor/atto/plugins/table/yui/src/button/js/button.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js
lib/editor/atto/yui/src/editor/build.json
lib/editor/atto/yui/src/editor/js/commands.js [new file with mode: 0644]
lib/editor/atto/yui/src/editor/js/editor-plugin-buttons.js
lib/editor/atto/yui/src/editor/meta/editor.json
lib/environmentlib.php
lib/filelib.php
lib/formslib.php
lib/grade/grade_category.php
lib/grade/grade_item.php
lib/grade/tests/grade_item_test.php
lib/moodlelib.php
lib/mustache/LICENSE
lib/mustache/README.md
lib/mustache/composer.json [deleted file]
lib/mustache/readme_moodle.txt
lib/mustache/src/Mustache/Autoloader.php
lib/mustache/src/Mustache/Cache.php
lib/mustache/src/Mustache/Cache/AbstractCache.php
lib/mustache/src/Mustache/Cache/FilesystemCache.php
lib/mustache/src/Mustache/Cache/NoopCache.php
lib/mustache/src/Mustache/Compiler.php
lib/mustache/src/Mustache/Context.php
lib/mustache/src/Mustache/Engine.php
lib/mustache/src/Mustache/Exception.php
lib/mustache/src/Mustache/Exception/InvalidArgumentException.php
lib/mustache/src/Mustache/Exception/LogicException.php
lib/mustache/src/Mustache/Exception/RuntimeException.php
lib/mustache/src/Mustache/Exception/SyntaxException.php
lib/mustache/src/Mustache/Exception/UnknownFilterException.php
lib/mustache/src/Mustache/Exception/UnknownHelperException.php
lib/mustache/src/Mustache/Exception/UnknownTemplateException.php
lib/mustache/src/Mustache/HelperCollection.php
lib/mustache/src/Mustache/LambdaHelper.php
lib/mustache/src/Mustache/Loader.php
lib/mustache/src/Mustache/Loader/ArrayLoader.php
lib/mustache/src/Mustache/Loader/CascadingLoader.php
lib/mustache/src/Mustache/Loader/FilesystemLoader.php
lib/mustache/src/Mustache/Loader/InlineLoader.php
lib/mustache/src/Mustache/Loader/MutableLoader.php
lib/mustache/src/Mustache/Loader/StringLoader.php
lib/mustache/src/Mustache/Logger.php
lib/mustache/src/Mustache/Logger/AbstractLogger.php
lib/mustache/src/Mustache/Logger/StreamLogger.php
lib/mustache/src/Mustache/Parser.php
lib/mustache/src/Mustache/Template.php
lib/mustache/src/Mustache/Tokenizer.php
lib/myprofilelib.php
lib/outputlib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/phpunit/bootstraplib.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/base_testcase.php [new file with mode: 0644]
lib/phpunit/classes/basic_testcase.php
lib/phpunit/classes/constraint_object_is_equal_with_exceptions.php [new file with mode: 0644]
lib/phpunit/classes/hint_resultprinter.php
lib/phpunit/classes/util.php
lib/phpunit/lib.php
lib/phpunit/phpunit.xsd
lib/phpunit/readme.md
lib/tablelib.php
lib/testing/lib.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_navigation.php
lib/tests/behat/behat_permissions.php
lib/tests/completionlib_test.php
lib/tests/environment_test.php
lib/tests/filelib_test.php
lib/tests/scheduled_task_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
login/signup.php
login/signup_form.php
message/lib.php
message/tests/behat/display_history.feature
message/tests/behat/manage_contacts.feature
message/tests/behat/message_participants.feature
message/tests/behat/recent_conversations.feature
message/tests/behat/search_history.feature
message/tests/externallib_test.php
message/tests/messagelib_test.php
mod/assign/feedback/editpdf/classes/page_editor.php
mod/assign/lib.php
mod/book/lang/en/book.php
mod/chat/lang/en/chat.php
mod/choice/lang/en/choice.php
mod/data/field/checkbox/field.class.php
mod/data/field/date/field.class.php
mod/data/field/file/field.class.php
mod/data/field/latlong/field.class.php
mod/data/field/menu/field.class.php
mod/data/field/multimenu/field.class.php
mod/data/field/picture/field.class.php
mod/data/field/radiobutton/field.class.php
mod/data/field/textarea/field.class.php
mod/data/field/url/field.class.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/styles.css
mod/data/templates.php
mod/folder/classes/external.php [new file with mode: 0644]
mod/folder/db/services.php [new file with mode: 0644]
mod/folder/lib.php
mod/folder/tests/externallib_test.php [new file with mode: 0644]
mod/folder/tests/lib_test.php [new file with mode: 0644]
mod/folder/version.php
mod/forum/classes/post_form.php
mod/forum/discuss.php
mod/forum/externallib.php
mod/forum/lang/en/deprecated.txt [new file with mode: 0644]
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/renderer.php
mod/forum/subscribers.php
mod/forum/tests/behat/discussion_display.feature
mod/forum/tests/behat/discussion_navigation.feature
mod/forum/tests/behat/edit_post_student.feature
mod/forum/tests/behat/edit_post_teacher.feature
mod/forum/tests/behat/forum_subscriptions_availability.feature
mod/forum/tests/behat/groups_in_course_no_groups_in_forum.feature [new file with mode: 0644]
mod/forum/tests/behat/my_forum_posts.feature
mod/forum/tests/behat/no_groups_in_course.feature [new file with mode: 0644]
mod/forum/tests/behat/post_to_multiple_groups.feature
mod/forum/tests/behat/separate_group_discussions.feature
mod/forum/tests/behat/separate_group_single_group_discussions.feature
mod/forum/tests/behat/single_forum_discussion.feature
mod/forum/tests/behat/track_read_posts.feature
mod/forum/tests/behat/visible_group_discussions.feature [new file with mode: 0644]
mod/forum/tests/externallib_test.php
mod/lesson/format.php
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.php
mod/lesson/mod_form.php
mod/lesson/tests/behat/lesson_delete_answers.feature [new file with mode: 0644]
mod/lesson/tests/behat/lesson_practice.feature
mod/lti/grade.php [deleted file]
mod/lti/lib.php
mod/lti/mod_form.php
mod/lti/typessettings.php
mod/page/classes/external.php [new file with mode: 0644]
mod/page/db/services.php [new file with mode: 0644]
mod/page/lib.php
mod/page/tests/externallib_test.php [new file with mode: 0644]
mod/page/tests/lib_test.php [new file with mode: 0644]
mod/page/version.php
mod/page/view.php
mod/quiz/attemptlib.php
mod/quiz/classes/plugininfo/quizaccess.php
mod/quiz/review.php
mod/quiz/reviewquestion.php
mod/quiz/styles.css
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/version.php
mod/resource/classes/external.php [new file with mode: 0644]
mod/resource/db/services.php [new file with mode: 0644]
mod/resource/lib.php
mod/resource/tests/externallib_test.php [new file with mode: 0644]
mod/resource/tests/lib_test.php [new file with mode: 0644]
mod/resource/version.php
mod/resource/view.php
mod/scorm/lang/en/scorm.php
mod/scorm/locallib.php
mod/workshop/lib.php
my/tests/behat/restrict_available_blocks.feature
notes/delete.php
notes/externallib.php
notes/lib.php
phpunit.xml.dist
question/format/xml/format.php
question/format/xml/tests/xmlformat_test.php
question/type/questiontypebase.php
question/type/tests/questiontype_test.php
report/outline/tests/behat/user.feature
repository/filepicker.js
repository/tests/generator_test.php
repository/youtube/db/install.php
repository/youtube/lang/en/repository_youtube.php
tag/coursetagslib.php
tag/lib.php
tag/manage.php
tag/tests/events_test.php
tag/tests/taglib_test.php
theme/base/style/blocks.css
theme/base/style/filemanager.css
theme/bootstrapbase/layout/columns1.php
theme/bootstrapbase/layout/columns2.php
theme/bootstrapbase/layout/columns3.php
theme/bootstrapbase/layout/popup.php
theme/bootstrapbase/layout/secure.php
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/bootstrapoverride.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/dock.less
theme/bootstrapbase/less/moodle/message.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/renderers/core_renderer.php
theme/bootstrapbase/style/moodle.css
theme/clean/classes/core_renderer.php
theme/clean/layout/columns1.php
theme/clean/layout/columns2.php
theme/clean/layout/columns3.php
theme/clean/layout/secure.php
theme/clean/lib.php
theme/upgrade.txt
user/edit_form.php
user/editadvanced_form.php
user/lib.php
user/profile.php
user/profile/index.php
user/tests/behat/view_full_profile.feature
user/tests/userlib_test.php
user/view.php
version.php

index e43430e..131056d 100644 (file)
@@ -80,6 +80,7 @@ Options:
                       required in non-interactive mode.
 --allow-unstable      Install even if the version is not marked as stable yet,
                       required in non-interactive mode.
+--skip-database       Stop the installation before installing the database.
 -h, --help            Print out this help
 
 Example:
@@ -260,6 +261,7 @@ list($options, $unrecognized) = cli_get_params(
         'non-interactive'   => false,
         'agree-license'     => false,
         'allow-unstable'    => false,
+        'skip-database'     => false,
         'help'              => false
     ),
     array(
@@ -772,7 +774,11 @@ if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
     cli_error(get_string('pluginschecktodo', 'admin'));
 }
 
-install_cli_database($options, $interactive);
+if (!$options['skip-database']) {
+    install_cli_database($options, $interactive);
+} else {
+    echo get_string('cliskipdatabase', 'install')."\n";
+}
 
 echo get_string('cliinstallfinished', 'install')."\n";
 exit(0); // 0 means success
index e596403..32bb207 100644 (file)
       <VENDOR name="oracle" version="10.2" />
     </DATABASE>
     <PHP version="5.4.4" level="required">
+      <RESTRICT function="restrict_php_version_7" message="unsupportedphpversion7" />
     </PHP>
     <PCREUNICODE level="optional">
       <FEEDBACK>
           <ON_ERROR message="quizattemptsupgradedmessage" />
         </FEEDBACK>
       </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="slashargumentswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unsupporteddbtablerowformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
   <MOODLE version="2.8" requires="2.2">
       <VENDOR name="oracle" version="10.2" />
     </DATABASE>
     <PHP version="5.4.4" level="required">
+      <RESTRICT function="restrict_php_version_7" message="unsupportedphpversion7" />
     </PHP>
     <PCREUNICODE level="optional">
       <FEEDBACK>
           <ON_ERROR message="quizattemptsupgradedmessage" />
         </FEEDBACK>
       </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="slashargumentswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unsupporteddbtablerowformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
   <MOODLE version="2.9" requires="2.2">
       <VENDOR name="oracle" version="10.2" />
     </DATABASE>
     <PHP version="5.4.4" level="required">
+      <RESTRICT function="restrict_php_version_7" message="unsupportedphpversion7" />
     </PHP>
     <PCREUNICODE level="optional">
       <FEEDBACK>
index bad3ed9..1cea8f8 100644 (file)
@@ -513,7 +513,15 @@ if (isguestuser()) {
     redirect(get_login_url());
 }
 $context = context_system::instance();
-require_capability('moodle/site:config', $context);
+
+if (!has_capability('moodle/site:config', $context)) {
+    // Do not throw exception display an empty page with administration menu if visible for current user.
+    $PAGE->set_title($SITE->fullname);
+    $PAGE->set_heading($SITE->fullname);
+    echo $OUTPUT->header();
+    echo $OUTPUT->footer();
+    exit;
+}
 
 // check that site is properly customized
 $site = get_site();
index dc39e04..82c5969 100644 (file)
@@ -46,6 +46,8 @@ $resultshtml = admin_search_settings_html($query); // case insensitive search on
 echo '<form action="' . $PAGE->url->out(true) . '" method="post" id="adminsettings">';
 echo '<div>';
 echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
+// HACK to prevent browsers from automatically inserting the user's password into the wrong fields.
+echo prevent_form_autofill_password();
 echo '</div>';
 echo '<fieldset>';
 echo '<div class="clearer"><!-- --></div>';
index 2acbc83..e8f5782 100644 (file)
@@ -77,6 +77,8 @@ if (empty($SITE->fullname)) {
     echo html_writer::input_hidden_params($PAGE->url);
     echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
     echo '<input type="hidden" name="return" value="'.$return.'" />';
+    // HACK to prevent browsers from automatically inserting the user's password into the wrong fields.
+    echo prevent_form_autofill_password();
 
     echo $settingspage->output_html();
 
@@ -119,6 +121,8 @@ if (empty($SITE->fullname)) {
     echo html_writer::input_hidden_params($PAGE->url);
     echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
     echo '<input type="hidden" name="return" value="'.$return.'" />';
+    // HACK to prevent browsers from automatically inserting the user's password into the wrong fields.
+    echo prevent_form_autofill_password();
     echo $OUTPUT->heading($settingspage->visiblename);
 
     echo $settingspage->output_html();
index f311696..820d25b 100644 (file)
@@ -80,6 +80,9 @@ if ($hassiteconfig) {
     $temp->add(new admin_setting_heading('manageauthscommonheading', new lang_string('commonsettings', 'admin'), ''));
     $temp->add(new admin_setting_special_registerauth());
     $temp->add(new admin_setting_configcheckbox('authloginviaemail', new lang_string('authloginviaemail', 'core_auth'), new lang_string('authloginviaemail_desc', 'core_auth'), 0));
+    $temp->add(new admin_setting_configcheckbox('allowaccountssameemail',
+                    new lang_string('allowaccountssameemail', 'core_auth'),
+                    new lang_string('allowaccountssameemail_desc', 'core_auth'), 0));
     $temp->add(new admin_setting_configcheckbox('authpreventaccountcreation', new lang_string('authpreventaccountcreation', 'admin'), new lang_string('authpreventaccountcreation_help', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('loginpageautofocus', new lang_string('loginpageautofocus', 'admin'), new lang_string('loginpageautofocus_help', 'admin'), 0));
     $temp->add(new admin_setting_configselect('guestloginbutton', new lang_string('guestloginbutton', 'auth'),
index 26d4f42..29f7981 100644 (file)
@@ -15,7 +15,6 @@ Feature: Edit capabilities
       | user | course | role |
       | teacher1 | C1 | editingteacher |
 
-  @javascript
   Scenario: Default system capabilities modification
     Given I log in as "admin"
     And I set the following system permissions of "Teacher" role:
@@ -30,7 +29,6 @@ Feature: Edit capabilities
     And "moodle/grade:managesharedforms" capability has "Prevent" permission
     And "moodle/course:request" capability has "Prohibit" permission
 
-  @javascript
   Scenario: Course capabilities overrides
     Given I log in as "teacher1"
     And I follow "Course 1"
@@ -41,11 +39,11 @@ Feature: Edit capabilities
       | mod/forum:editanypost | Prevent |
       | mod/forum:addquestion | Allow |
     When I set the field "Advanced role override" to "Student (3)"
+    And I press "Go"
     Then "mod/forum:deleteanypost" capability has "Prohibit" permission
     And "mod/forum:editanypost" capability has "Prevent" permission
     And "mod/forum:addquestion" capability has "Allow" permission
 
-  @javascript
   Scenario: Module capabilities overrides
     Given I log in as "teacher1"
     And I follow "Course 1"
@@ -60,6 +58,7 @@ Feature: Edit capabilities
       | mod/forum:editanypost | Prevent |
       | mod/forum:addquestion | Allow |
     When I set the field "Advanced role override" to "Student (3)"
+    And I press "Go"
     Then "mod/forum:deleteanypost" capability has "Prohibit" permission
     And "mod/forum:editanypost" capability has "Prevent" permission
     And "mod/forum:addquestion" capability has "Allow" permission
index 41fac8d..4b4120d 100644 (file)
@@ -13,7 +13,6 @@ Feature: Transform steps arguments
     And I follow "Preferences" in the user menu
     And I follow "Edit profile"
 
-  @javascript
   Scenario: Use nasty strings on steps arguments
     When I set the field "Surname" to "$NASTYSTRING1"
     And I set the field "Description" to "$NASTYSTRING2"
@@ -24,7 +23,6 @@ Feature: Transform steps arguments
     And the field "Surname" matches value "$NASTYSTRING1"
     And the field "City/town" matches value "$NASTYSTRING3"
 
-  @javascript
   Scenario: Use nasty strings on table nodes
     When I set the following fields to these values:
       | Surname | $NASTYSTRING1 |
@@ -36,7 +34,6 @@ Feature: Transform steps arguments
     And the field "Surname" matches value "$NASTYSTRING1"
     And the field "City/town" matches value "$NASTYSTRING3"
 
-  @javascript
   Scenario: Use double quotes
     When I set the following fields to these values:
       | First name | va"lue1 |
@@ -49,7 +46,6 @@ Feature: Transform steps arguments
     And the field "Description" matches value "va\\"lue2"
     And the field "City/town" matches value "va\"lue3"
 
-  @javascript
   Scenario: Nasty strings with other contents
     When I set the field "First name" to "My Firstname $NASTYSTRING1"
     And I set the following fields to these values:
index 71c54fe..8ad4536 100644 (file)
@@ -63,7 +63,7 @@ if ($translatorsubmitted) {
     $checkin = optional_param('savecheckin', false, PARAM_RAW);
 
     if ($checkin === false) {
-        $nexturl = $PAGE->url;
+        $nexturl = new moodle_url($PAGE->url, array('p' => $currentpage));
     } else {
         $nexturl = new moodle_url('/admin/tool/customlang/index.php', array('action'=>'checkin', 'lng' => $lng, 'sesskey'=>sesskey()));
     }
index c733dda..224e76b 100644 (file)
@@ -133,6 +133,7 @@ class tool_customlang_renderer extends plugin_renderer_base {
         $output .= html_writer::start_tag('div');
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'translatorsubmitted', 'value'=>1));
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey()));
+        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'p', 'value'=>$translator->currentpage));
         $save1   = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecontinue', 'value'=>get_string('savecontinue', 'tool_customlang')));
         $save2   = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecheckin', 'value'=>get_string('savecheckin', 'tool_customlang')));
         $output .= html_writer::tag('fieldset', $save1.$save2, array('class'=>'buttonsbar'));
index 8095ab2..4120104 100644 (file)
@@ -23,7 +23,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['healthnoproblemsfound'] = 'There is no health problem found!';
+$string['healthnoproblemsfound'] = 'No health problems were found!';
 $string['healthproblemsdetected'] = 'Health problems detected!';
 $string['healthproblemsolution'] = 'Health problem solution';
 $string['healthreturntomain'] = 'Continue';
index 65a752d..a2ccf4e 100644 (file)
@@ -30,7 +30,7 @@ $string['langimportdisabled'] = 'Language import feature has been disabled. You
 $string['langpackinstalled'] = 'Language pack \'{$a}\' was successfully installed';
 $string['langpackinstalledevent'] = 'Language pack installed';
 $string['langpackremoved'] = 'Language pack \'{$a}\' was uninstalled';
-$string['langpacknotremoved'] = 'An error has occurred, language pack \'{$a}\' is not completely uninstalled, please check file permissions';
+$string['langpacknotremoved'] = 'An error has occurred; language pack \'{$a}\' is not completely uninstalled. Please check file permissions.';
 $string['langpackremovedevent'] = 'Language pack uninstalled';
 $string['langpackupdateskipped'] = 'Update of \'{$a}\' language pack skipped';
 $string['langpackuptodate'] = 'Language pack \'{$a}\' is up-to-date';
@@ -45,6 +45,6 @@ $string['pluginname'] = 'Language packs';
 $string['purgestringcaches'] = 'Purge string caches';
 $string['remotelangnotavailable'] = 'Because Moodle cannot connect to download.moodle.org, it is not possible for language packs to be installed automatically. Please download the appropriate ZIP file(s) from <a href="https://download.moodle.org/langpack/">download.moodle.org/langpack</a>, copy them to your {$a} directory and unzip them manually.';
 $string['selectlangs'] = 'Select languages to unistall!';
-$string['uninstall'] = 'Uninstall selected language packs';
+$string['uninstall'] = 'Uninstall selected language pack(s)';
 $string['uninstallconfirm'] = 'You are about to completely uninstall these language packs: <strong>{$a}</strong>. Are you sure?';
 $string['updatelangs'] = 'Update all installed language packs';
index ff21c8e..757d46e 100644 (file)
@@ -38,7 +38,7 @@ Feature: Manage language packs
     And I set the field "Available language packs" to "English - Pirate (en_ar)"
     And I press "Install selected language pack(s)"
     When I set the field "Installed language packs" to "English - Pirate (en_ar)"
-    And I press "Uninstall selected language pack"
+    And I press "Uninstall selected language pack(s)"
     And I press "Continue"
     Then I should see "Language pack 'en_ar' was uninstalled"
     And the "Installed language packs" select box should not contain "English - Pirate (en_ar)"
@@ -52,7 +52,7 @@ Feature: Manage language packs
     Given I log in as "admin"
     And I navigate to "Language packs" node in "Site administration > Language"
     When I set the field "Installed language packs" to "English (en)"
-    And I press "Uninstall selected language pack"
+    And I press "Uninstall selected language pack(s)"
     Then I should see "English language pack can not be uninstalled"
     And I navigate to "Live logs" node in "Site administration > Reports"
     And I should not see "Language pack uninstalled"
index 8d898fd..ad36657 100644 (file)
@@ -168,7 +168,7 @@ if ($formdata = $mform2->is_cancelled()) {
     $allowdeletes      = (!empty($formdata->uuallowdeletes) and $optype != UU_USER_ADDNEW and $optype != UU_USER_ADDINC);
     $allowsuspends     = (!empty($formdata->uuallowsuspends));
     $bulk              = $formdata->uubulk;
-    $noemailduplicates = $formdata->uunoemailduplicates;
+    $noemailduplicates = empty($CFG->allowaccountssameemail) ? 1 : $formdata->uunoemailduplicates;
     $standardusernames = $formdata->uustandardusernames;
     $resetpasswords    = isset($formdata->uuforcepasswordchange) ? $formdata->uuforcepasswordchange : UU_PWRESET_NONE;
 
index 2e919fd..04bb24b 100644 (file)
@@ -138,8 +138,13 @@ class admin_uploaduser_form2 extends moodleform {
         $mform->disabledIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDNEW);
         $mform->disabledIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDINC);
 
-        $mform->addElement('selectyesno', 'uunoemailduplicates', get_string('uunoemailduplicates', 'tool_uploaduser'));
-        $mform->setDefault('uunoemailduplicates', 1);
+        if (!empty($CFG->allowaccountssameemail)) {
+            $mform->addElement('selectyesno', 'uunoemailduplicates', get_string('uunoemailduplicates', 'tool_uploaduser'));
+            $mform->setDefault('uunoemailduplicates', 1);
+        } else {
+            $mform->addElement('hidden', 'uunoemailduplicates', 1);
+        }
+        $mform->setType('uunoemailduplicates', PARAM_BOOL);
 
         $mform->addElement('selectyesno', 'uustandardusernames', get_string('uustandardusernames', 'tool_uploaduser'));
         $mform->setDefault('uustandardusernames', 1);
index 38a8f06..f4aa617 100644 (file)
@@ -63,6 +63,8 @@ echo '<form action="upgradesettings.php" method="post" id="adminsettings">';
 echo '<div>';
 echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
 echo '<input type="hidden" name="return" value="'.$return.'" />';
+// HACK to prevent browsers from automatically inserting the user's password into the wrong fields.
+echo prevent_form_autofill_password();
 echo '<fieldset>';
 echo '<div class="clearer"><!-- --></div>';
 echo $newsettingshtml;
index eaf3f6c..22733b2 100644 (file)
@@ -187,7 +187,7 @@ class auth_plugin_cas extends auth_plugin_ldap {
         }
 
         // If Moodle is configured to use a proxy, phpCAS needs some curl options set.
-        if (!empty($CFG->proxyhost) && !is_proxybypass($this->config->hostname)) {
+        if (!empty($CFG->proxyhost) && !is_proxybypass(phpCAS::getServerLoginURL())) {
             phpCAS::setExtraCurlOption(CURLOPT_PROXY, $CFG->proxyhost);
             if (!empty($CFG->proxyport)) {
                 phpCAS::setExtraCurlOption(CURLOPT_PROXYPORT, $CFG->proxyport);
index 9121c6e..147ef60 100644 (file)
@@ -58,6 +58,11 @@ class auth_plugin_db extends auth_plugin_base {
     function user_login($username, $password) {
         global $CFG, $DB;
 
+        if ($this->is_configured() === false) {
+            debugging(get_string('auth_notconfigured', 'auth', $this->authtype));
+            return false;
+        }
+
         $extusername = core_text::convert($username, 'utf-8', $this->config->extencoding);
         $extpassword = core_text::convert($password, 'utf-8', $this->config->extencoding);
 
@@ -105,7 +110,7 @@ class auth_plugin_db extends auth_plugin_base {
 
             $authdb = $this->db_init();
 
-            $rs = $authdb->Execute("SELECT {$this->config->fieldpass} AS userpass
+            $rs = $authdb->Execute("SELECT {$this->config->fieldpass}
                                       FROM {$this->config->table}
                                      WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."'");
             if (!$rs) {
@@ -120,7 +125,7 @@ class auth_plugin_db extends auth_plugin_base {
             }
 
             $fields = array_change_key_case($rs->fields, CASE_LOWER);
-            $fromdb = $fields['userpass'];
+            $fromdb = $fields[strtolower($this->config->fieldpass)];
             $rs->Close();
             $authdb->Close();
 
@@ -144,8 +149,13 @@ class auth_plugin_db extends auth_plugin_base {
      * Connect to external database.
      *
      * @return ADOConnection
+     * @throws moodle_exception
      */
     function db_init() {
+        if ($this->is_configured() === false) {
+            throw new moodle_exception('auth_dbcantconnect', 'auth_db');
+        }
+
         // Connect to the external database (forcing new connection).
         $authdb = ADONewConnection($this->config->type);
         if (!empty($this->config->debugauthdb)) {
@@ -207,18 +217,21 @@ class auth_plugin_db extends auth_plugin_base {
         if ($selectfields) {
             $select = array();
             foreach ($selectfields as $localname=>$externalname) {
-                $select[] = "$externalname AS $localname";
+                $select[] = "$externalname";
             }
             $select = implode(', ', $select);
             $sql = "SELECT $select
                       FROM {$this->config->table}
                      WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."'";
+
             if ($rs = $authdb->Execute($sql)) {
                 if (!$rs->EOF) {
-                    $fields_obj = $rs->FetchObj();
-                    $fields_obj = (object)array_change_key_case((array)$fields_obj , CASE_LOWER);
-                    foreach ($selectfields as $localname=>$externalname) {
-                        $result[$localname] = core_text::convert($fields_obj->{strtolower($localname)}, $this->config->extencoding, 'utf-8');
+                    $fields = $rs->FetchRow();
+                    // Convert the associative array to an array of its values so we don't have to worry about the case of its keys.
+                    $fields = array_values($fields);
+                    foreach (array_keys($selectfields) as $index => $localname) {
+                        $value = $fields[$index];
+                        $result[$localname] = core_text::convert($value, $this->config->extencoding, 'utf-8');
                      }
                  }
                  $rs->Close();
@@ -477,15 +490,15 @@ class auth_plugin_db extends auth_plugin_base {
         $authdb = $this->db_init();
 
         // Fetch userlist.
-        $rs = $authdb->Execute("SELECT {$this->config->fielduser} AS username
+        $rs = $authdb->Execute("SELECT {$this->config->fielduser}
                                   FROM {$this->config->table} ");
 
         if (!$rs) {
             print_error('auth_dbcantconnect','auth_db');
         } else if (!$rs->EOF) {
             while ($rec = $rs->FetchRow()) {
-                $rec = (object)array_change_key_case((array)$rec , CASE_LOWER);
-                array_push($result, $rec->username);
+                $rec = array_change_key_case((array)$rec, CASE_LOWER);
+                array_push($result, $rec[strtolower($this->config->fielduser)]);
             }
         }
 
@@ -661,6 +674,18 @@ class auth_plugin_db extends auth_plugin_base {
         return ($this->config->passtype === 'internal');
     }
 
+    /**
+     * Returns false if this plugin is enabled but not configured.
+     *
+     * @return bool
+     */
+    public function is_configured() {
+        if (!empty($this->config->type)) {
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Indicates if moodle should automatically update internal user
      * records with data from external sources using the information
index 8a4bd5b..440683c 100644 (file)
@@ -228,7 +228,9 @@ class condition extends \core_availability\condition {
                         WHERE
                             gi.courseid = ?', array($userid, $courseid));
                 foreach ($rs as $record) {
-                    if (is_null($record->finalgrade)) {
+                    // This function produces division by zero error warnings when rawgrademax and rawgrademin
+                    // are equal. Below change does not affect function behavior, just avoids the warning.
+                    if (is_null($record->finalgrade) || $record->rawgrademax == $record->rawgrademin) {
                         // No grade = false.
                         $cachedgrades[$record->id] = false;
                     } else {
@@ -249,7 +251,9 @@ class condition extends \core_availability\condition {
                 // Just get current grade.
                 $record = $DB->get_record('grade_grades', array(
                     'userid' => $userid, 'itemid' => $gradeitemid));
-                if ($record && !is_null($record->finalgrade)) {
+                // This function produces division by zero error warnings when rawgrademax and rawgrademin
+                // are equal. Below change does not affect function behavior, just avoids the warning.
+                if ($record && !is_null($record->finalgrade) && $record->rawgrademax != $record->rawgrademin) {
                     $score = (($record->finalgrade - $record->rawgrademin) * 100) /
                         ($record->rawgrademax - $record->rawgrademin);
                 } else {
index 74c3a63..de05104 100644 (file)
@@ -49,7 +49,11 @@ class core_availability_component_testcase extends advanced_testcase {
         // fail, but it's obvious when running test at least.
         $pluginmanager = core_plugin_manager::instance();
         $list = $pluginmanager->get_enabled_plugins('availability');
-        $this->assertEquals(array('completion', 'date', 'grade', 'group', 'grouping', 'profile'),
-                array_keys($list));
+        $this->assertArrayHasKey('completion', $list);
+        $this->assertArrayHasKey('date', $list);
+        $this->assertArrayHasKey('grade', $list);
+        $this->assertArrayHasKey('group', $list);
+        $this->assertArrayHasKey('grouping', $list);
+        $this->assertArrayHasKey('profile', $list);
     }
 }
index 769e7ff..587015d 100644 (file)
@@ -1428,6 +1428,15 @@ class restore_process_categories_and_questions extends restore_execution_step {
  * as needed, rebuilding course cache and other friends
  */
 class restore_section_structure_step extends restore_structure_step {
+    /** @var array Cache: Array of id => course format */
+    private static $courseformats = array();
+
+    /**
+     * Resets a static cache of course formats. Required for unit testing.
+     */
+    public static function reset_caches() {
+        self::$courseformats = array();
+    }
 
     protected function define_structure() {
         global $CFG;
@@ -1600,14 +1609,13 @@ class restore_section_structure_step extends restore_structure_step {
 
     public function process_course_format_options($data) {
         global $DB;
-        static $courseformats = array();
         $courseid = $this->get_courseid();
-        if (!array_key_exists($courseid, $courseformats)) {
+        if (!array_key_exists($courseid, self::$courseformats)) {
             // It is safe to have a static cache of course formats because format can not be changed after this point.
-            $courseformats[$courseid] = $DB->get_field('course', 'format', array('id' => $courseid));
+            self::$courseformats[$courseid] = $DB->get_field('course', 'format', array('id' => $courseid));
         }
         $data = (array)$data;
-        if ($courseformats[$courseid] === $data['format']) {
+        if (self::$courseformats[$courseid] === $data['format']) {
             // Import section format options only if both courses (the one that was backed up
             // and the one we are restoring into) have same formats.
             $params = array(
index 5d2d6e0..787db96 100644 (file)
@@ -105,7 +105,7 @@ class core_backup_moodle2_course_format_testcase extends advanced_testcase {
                       'numdaystocomplete' => 2);
         $courseobject->update_section_format_options($data);
 
-        $this->backup_and_restore($course, $course);
+        $this->backup_and_restore($course, $course, backup::TARGET_EXISTING_ADDING);
 
         $sectionoptions = $courseobject->get_format_options(1);
         $this->assertArrayHasKey('numdaystocomplete', $sectionoptions);
@@ -126,37 +126,49 @@ class core_backup_moodle2_course_format_testcase extends advanced_testcase {
 
         $this->resetAfterTest(true);
         $this->setAdminUser();
-        $CFG->enableavailability = true;
-        $CFG->enablecompletion = true;
 
-        // Create a course with some availability data set.
+        // Create a source course using the test_cs2_options format.
         $generator = $this->getDataGenerator();
         $course = $generator->create_course(
             array('format' => 'test_cs2_options', 'numsections' => 3,
                 'enablecompletion' => COMPLETION_ENABLED),
             array('createsections' => true));
+
+        // Create a target course using test_cs_options format.
         $newcourse = $generator->create_course(
             array('format' => 'test_cs_options', 'numsections' => 3,
                 'enablecompletion' => COMPLETION_ENABLED),
             array('createsections' => true));
 
+        // Set section 2 to have both options, and a name.
         $courseobject = format_base::instance($course->id);
         $section = $DB->get_record('course_sections',
-            array('course' => $course->id, 'section' => 1), '*', MUST_EXIST);
-
+            array('course' => $course->id, 'section' => 2), '*', MUST_EXIST);
         $data = array('id' => $section->id,
             'numdaystocomplete' => 2,
-            'secondparameter' => 8);
+            'secondparameter' => 8
+        );
         $courseobject->update_section_format_options($data);
-        // Backup and restore it.
-        $this->backup_and_restore($course, $newcourse);
+        $DB->set_field('course_sections', 'name', 'Frogs', array('id' => $section->id));
+
+        // Backup and restore to the new course using 'add to existing' so it
+        // keeps the current (test_cs_options) format.
+        $this->backup_and_restore($course, $newcourse, backup::TARGET_EXISTING_ADDING);
 
+        // Check that the section contains the options suitable for the new
+        // format and that even the one with the same name as from the old format
+        // has NOT been set.
         $newcourseobject = format_base::instance($newcourse->id);
-        $sectionoptions = $newcourseobject->get_format_options(1);
+        $sectionoptions = $newcourseobject->get_format_options(2);
         $this->assertArrayHasKey('numdaystocomplete', $sectionoptions);
-        $this->assertArrayHasKey('secondparameter', $sectionoptions);
+        $this->assertArrayNotHasKey('secondparameter', $sectionoptions);
         $this->assertEquals(0, $sectionoptions['numdaystocomplete']);
-        $this->assertEquals(0, $sectionoptions['secondparameter']);
+
+        // However, the name should have been changed, as this does not depend
+        // on the format.
+        $modinfo = get_fast_modinfo($newcourse->id);
+        $section = $modinfo->get_section_info(2);
+        $this->assertEquals('Frogs', $section->name);
     }
 
     /**
@@ -164,9 +176,11 @@ class core_backup_moodle2_course_format_testcase extends advanced_testcase {
      *
      * @param stdClass $srccourse Course object to backup
      * @param stdClass $dstcourse Course object to restore into
+     * @param int $target Target course mode (backup::TARGET_xx)
      * @return int ID of newly restored course
      */
-    protected function backup_and_restore($srccourse, $dstcourse = null) {
+    protected function backup_and_restore($srccourse, $dstcourse = null,
+            $target = backup::TARGET_NEW_COURSE) {
         global $USER, $CFG;
 
         // Turn off file logging, otherwise it can't delete the file (Windows).
@@ -190,7 +204,7 @@ class core_backup_moodle2_course_format_testcase extends advanced_testcase {
         }
         $rc = new restore_controller($backupid, $newcourseid,
                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
-                backup::TARGET_NEW_COURSE);
+                $target);
 
         $this->assertTrue($rc->execute_precheck());
         $rc->execute_plan();
@@ -226,6 +240,12 @@ class format_test_cs_options extends format_topics {
 class format_test_cs2_options extends format_test_cs_options {
     public function section_format_options($foreditform = false) {
         return array(
+            'numdaystocomplete' => array(
+                 'type' => PARAM_INT,
+                 'label' => 'Test days',
+                 'element_type' => 'text',
+                 'default' => 0,
+             ),
             'secondparameter' => array(
                 'type' => PARAM_INT,
                 'label' => 'Test Parmater',
index c9f93b2..f0ed9b2 100644 (file)
@@ -31,7 +31,9 @@ class block_messages extends block_base {
         global $USER, $CFG, $DB, $OUTPUT;
 
         if (!$CFG->messaging) {
+            $this->content = new stdClass;
             $this->content->text = '';
+            $this->content->footer = '';
             if ($this->page->user_is_editing()) {
                 $this->content->text = get_string('disabled', 'message');
             }
index c47d460..d524a23 100644 (file)
@@ -22,6 +22,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+use block_online_users\fetcher;
+
 /**
  * This block needs to be reworked.
  * The new roles system does away with the concepts of rigid student and
@@ -56,7 +58,6 @@ class block_online_users extends block_base {
             $timetoshowusers = $CFG->block_online_users_timetosee * 60;
         }
         $now = time();
-        $timefrom = 100 * floor(($now - $timetoshowusers) / 100); // Round to nearest 100 seconds for better query cache
 
         //Calculate if we are in separate groups
         $isseparategroups = ($this->page->course->groupmode == SEPARATEGROUPS
@@ -66,82 +67,24 @@ class block_online_users extends block_base {
         //Get the user current group
         $currentgroup = $isseparategroups ? groups_get_course_group($this->page->course) : NULL;
 
-        $groupmembers = "";
-        $groupselect  = "";
-        $params = array();
-
-        //Add this to the SQL to show only group users
-        if ($currentgroup !== NULL) {
-            $groupmembers = ", {groups_members} gm";
-            $groupselect = "AND u.id = gm.userid AND gm.groupid = :currentgroup";
-            $params['currentgroup'] = $currentgroup;
-        }
+        $sitelevel = $this->page->course->id == SITEID || $this->page->context->contextlevel < CONTEXT_COURSE;
 
-        $userfields = user_picture::fields('u', array('username'));
-        $params['now'] = $now;
-        $params['timefrom'] = $timefrom;
-        if ($this->page->course->id == SITEID or $this->page->context->contextlevel < CONTEXT_COURSE) {  // Site-level
-            $sql = "SELECT $userfields, MAX(u.lastaccess) AS lastaccess
-                      FROM {user} u $groupmembers
-                     WHERE u.lastaccess > :timefrom
-                           AND u.lastaccess <= :now
-                           AND u.deleted = 0
-                           $groupselect
-                  GROUP BY $userfields
-                  ORDER BY lastaccess DESC ";
-
-           $csql = "SELECT COUNT(u.id)
-                      FROM {user} u $groupmembers
-                     WHERE u.lastaccess > :timefrom
-                           AND u.lastaccess <= :now
-                           AND u.deleted = 0
-                           $groupselect";
-
-        } else {
-            // Course level - show only enrolled users for now
-            // TODO: add a new capability for viewing of all users (guests+enrolled+viewing)
-
-            list($esqljoin, $eparams) = get_enrolled_sql($this->page->context);
-            $params = array_merge($params, $eparams);
-
-            $sql = "SELECT $userfields, MAX(ul.timeaccess) AS lastaccess
-                      FROM {user_lastaccess} ul $groupmembers, {user} u
-                      JOIN ($esqljoin) euj ON euj.id = u.id
-                     WHERE ul.timeaccess > :timefrom
-                           AND u.id = ul.userid
-                           AND ul.courseid = :courseid
-                           AND ul.timeaccess <= :now
-                           AND u.deleted = 0
-                           $groupselect
-                  GROUP BY $userfields
-                  ORDER BY lastaccess DESC";
-
-           $csql = "SELECT COUNT(u.id)
-                      FROM {user_lastaccess} ul $groupmembers, {user} u
-                      JOIN ($esqljoin) euj ON euj.id = u.id
-                     WHERE ul.timeaccess > :timefrom
-                           AND u.id = ul.userid
-                           AND ul.courseid = :courseid
-                           AND ul.timeaccess <= :now
-                           AND u.deleted = 0
-                           $groupselect";
-
-            $params['courseid'] = $this->page->course->id;
-        }
+        $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $sitelevel,
+                $this->page->context, $this->page->course->id);
 
         //Calculate minutes
         $minutes  = floor($timetoshowusers/60);
 
         // Verify if we can see the list of users, if not just print number of users
         if (!has_capability('block/online_users:viewlist', $this->page->context)) {
-            if (!$usercount = $DB->count_records_sql($csql, $params)) {
+            if (!$usercount = $onlineusers->count_users()) {
                 $usercount = get_string("none");
             }
             $this->content->text = "<div class=\"info\">".get_string("periodnminutes","block_online_users",$minutes).": $usercount</div>";
             return $this->content;
         }
-
-        if ($users = $DB->get_records_sql($sql, $params, 0, 50)) {   // We'll just take the most recent 50 maximum
+        $userlimit = 50; // We'll just take the most recent 50 maximum.
+        if ($users = $onlineusers->get_users($userlimit)) {
             foreach ($users as $user) {
                 $users[$user->id]->fullname = fullname($user);
             }
@@ -149,10 +92,10 @@ class block_online_users extends block_base {
             $users = array();
         }
 
-        if (count($users) < 50) {
+        if (count($users) < $userlimit) {
             $usercount = "";
         } else {
-            $usercount = $DB->count_records_sql($csql, $params);
+            $usercount = $onlineusers->count_users();
             $usercount = ": $usercount";
         }
 
diff --git a/blocks/online_users/classes/fetcher.php b/blocks/online_users/classes/fetcher.php
new file mode 100644 (file)
index 0000000..1c6d15a
--- /dev/null
@@ -0,0 +1,165 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File containing onlineusers class.
+ *
+ * @package    block_online_users
+ * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_online_users;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class used to list and count online users
+ *
+ * @package    block_online_users
+ * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetcher {
+
+    /** @var string The SQL query for retrieving a list of online users */
+    public $sql;
+    /** @var string The SQL query for counting the number of online users */
+    public $csql;
+    /** @var string The params for the SQL queries */
+    public $params;
+
+    /**
+     * Class constructor
+     *
+     * @param int $currentgroup The group (if any) to filter on
+     * @param int $now Time now
+     * @param int $timetoshowusers Number of seconds to show online users
+     * @param context $context Context object used to generate the sql for users enrolled in a specific course
+     * @param bool $sitelevel Whether to check online users at site level.
+     * @param int $courseid The course id to check
+     */
+    public function __construct($currentgroup, $now, $timetoshowusers, $context, $sitelevel = true, $courseid = null) {
+        $this->set_sql($currentgroup, $now, $timetoshowusers, $context, $sitelevel, $courseid);
+    }
+
+    /**
+     * Store the SQL queries & params for listing online users
+     *
+     * @param int $currentgroup The group (if any) to filter on
+     * @param int $now Time now
+     * @param int $timetoshowusers Number of seconds to show online users
+     * @param context $context Context object used to generate the sql for users enrolled in a specific course
+     * @param bool $sitelevel Whether to check online users at site level.
+     * @param int $courseid The course id to check
+     */
+    protected function set_sql($currentgroup, $now, $timetoshowusers, $context, $sitelevel, $courseid) {
+        $timefrom = 100 * floor(($now - $timetoshowusers) / 100); // Round to nearest 100 seconds for better query cache.
+
+        $groupmembers = "";
+        $groupselect  = "";
+        $groupby       = "";
+        $lastaccess    = ", lastaccess";
+        $timeaccess    = ", ul.timeaccess AS lastaccess";
+        $params = array();
+
+        $userfields = \user_picture::fields('u', array('username'));
+
+        // Add this to the SQL to show only group users.
+        if ($currentgroup !== null) {
+            $groupmembers = ", {groups_members} gm";
+            $groupselect = "AND u.id = gm.userid AND gm.groupid = :currentgroup";
+            $groupby = "GROUP BY $userfields";
+            $lastaccess = ", MAX(u.lastaccess) AS lastaccess";
+            $timeaccess = ", MAX(ul.timeaccess) AS lastaccess";
+            $params['currentgroup'] = $currentgroup;
+        }
+
+        $params['now'] = $now;
+        $params['timefrom'] = $timefrom;
+        if ($sitelevel) {
+            $sql = "SELECT $userfields $lastaccess
+                      FROM {user} u $groupmembers
+                     WHERE u.lastaccess > :timefrom
+                           AND u.lastaccess <= :now
+                           AND u.deleted = 0
+                           $groupselect $groupby
+                  ORDER BY lastaccess DESC ";
+
+            $csql = "SELECT COUNT(u.id)
+                      FROM {user} u $groupmembers
+                     WHERE u.lastaccess > :timefrom
+                           AND u.lastaccess <= :now
+                           AND u.deleted = 0
+                           $groupselect";
+
+        } else {
+            // Course level - show only enrolled users for now.
+            // TODO: add a new capability for viewing of all users (guests+enrolled+viewing).
+            list($esqljoin, $eparams) = get_enrolled_sql($context);
+            $params = array_merge($params, $eparams);
+
+            $sql = "SELECT $userfields $timeaccess
+                      FROM {user_lastaccess} ul $groupmembers, {user} u
+                      JOIN ($esqljoin) euj ON euj.id = u.id
+                     WHERE ul.timeaccess > :timefrom
+                           AND u.id = ul.userid
+                           AND ul.courseid = :courseid
+                           AND ul.timeaccess <= :now
+                           AND u.deleted = 0
+                           $groupselect $groupby
+                  ORDER BY lastaccess DESC";
+
+            $csql = "SELECT COUNT(u.id)
+                      FROM {user_lastaccess} ul $groupmembers, {user} u
+                      JOIN ($esqljoin) euj ON euj.id = u.id
+                     WHERE ul.timeaccess > :timefrom
+                           AND u.id = ul.userid
+                           AND ul.courseid = :courseid
+                           AND ul.timeaccess <= :now
+                           AND u.deleted = 0
+                           $groupselect";
+
+            $params['courseid'] = $courseid;
+        }
+        $this->sql = $sql;
+        $this->csql = $csql;
+        $this->params = $params;
+    }
+
+    /**
+     * Get a list of the most recent online users
+     *
+     * @param int $userlimit The maximum number of users that will be returned (optional, unlimited if not set)
+     * @return array
+     */
+    public function get_users($userlimit = 0) {
+        global $DB;
+        $users = $DB->get_records_sql($this->sql, $this->params, 0, $userlimit);
+        return $users;
+    }
+
+    /**
+     * Count the number of online users
+     *
+     * @return int
+     */
+    public function count_users() {
+        global $DB;
+        return $DB->count_records_sql($this->csql, $this->params);
+    }
+
+}
index dce490b..a560fd5 100644 (file)
@@ -60,4 +60,56 @@ class block_online_users_generator extends testing_block_generator {
 
         return $instance;
     }
+
+    /**
+     * Create (simulated) logged in users and add some of them to groups in a course
+     */
+    public function create_logged_in_users() {
+        global $DB;
+
+        $generator = advanced_testcase::getDataGenerator();
+        $data = array();
+
+        // Create 2 courses.
+        $course1 = $generator->create_course();
+        $data['course1'] = $course1;
+        $course2 = $generator->create_course();
+        $data['course2'] = $course2;
+
+        // Create 9 (simulated) logged in users enroled into $course1.
+        for ($i = 1; $i <= 9; $i++) {
+            $user = $generator->create_user();
+            $DB->set_field('user', 'lastaccess', time(), array('id' => $user->id));
+            $generator->enrol_user($user->id, $course1->id);
+            $DB->insert_record('user_lastaccess', array('userid' => $user->id, 'courseid' => $course1->id, 'timeaccess' => time()));
+            $data['user' . $i] = $user;
+        }
+        // Create 3 (simulated) logged in users who are not enroled into $course1.
+        for ($i = 10; $i <= 12; $i++) {
+            $user = $generator->create_user();
+            $DB->set_field('user', 'lastaccess', time(), array('id' => $user->id));
+            $data['user' . $i] = $user;
+        }
+
+        // Create 3 groups in course 1.
+        $group1 = $generator->create_group(array('courseid' => $course1->id));
+        $data['group1'] = $group1;
+        $group2 = $generator->create_group(array('courseid' => $course1->id));
+        $data['group2'] = $group2;
+        $group3 = $generator->create_group(array('courseid' => $course1->id));
+        $data['group3'] = $group3;
+
+        // Add 3 users to course group 1.
+        $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user1']->id));
+        $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user2']->id));
+        $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user3']->id));
+
+        // Add 4 users to course group 2.
+        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user3']->id));
+        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user4']->id));
+        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user5']->id));
+        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user6']->id));
+
+        return $data; // Return the user, course and group objects.
+    }
 }
diff --git a/blocks/online_users/tests/online_users_test.php b/blocks/online_users/tests/online_users_test.php
new file mode 100644 (file)
index 0000000..9ff1e3c
--- /dev/null
@@ -0,0 +1,151 @@
+<?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/>.
+
+/**
+ * Online users tests
+ *
+ * @package    block_online_users
+ * @category   test
+ * @copyright  2015 University of Nottingham <www.nottingham.ac.uk>
+ * @author     Barry Oosthuizen <barry.oosthuizen@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use block_online_users\fetcher;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Online users testcase
+ *
+ * @package    block_online_users
+ * @category   test
+ * @copyright  2015 University of Nottingham <www.nottingham.ac.uk>
+ * @author     Barry Oosthuizen <barry.oosthuizen@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_online_users_testcase extends advanced_testcase {
+
+    protected $data;
+
+    /**
+     * Tests initial setup.
+     *
+     * Prepare the site with some courses, groups, users and
+     * simulate various recent accesses.
+     */
+    protected function setUp() {
+
+        // Generate (simulated) recently logged-in users.
+        $generator = $this->getDataGenerator()->get_plugin_generator('block_online_users');
+        $this->data = $generator->create_logged_in_users();
+
+        // Confirm we have modified the site and requires reset.
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Check logged in group 1, 2 & 3 members in course 1 (should be 3, 4 and 0).
+     *
+     * @param array $data Array of user, course and group objects
+     * @param int $now Current Unix timestamp
+     * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users
+     */
+    public function test_fetcher_course1_group_members() {
+        global $CFG;
+
+        $groupid = $this->data['group1']->id;
+        $now = time();
+        $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+        $context = context_course::instance($this->data['course1']->id);
+        $courseid = $this->data['course1']->id;
+        $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals(3, $usercount, 'There was a problem counting the number of online users in group 1');
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 1');
+
+        $groupid = $this->data['group2']->id;
+        $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 2');
+        $this->assertEquals(4, $usercount, 'There was a problem counting the number of online users in group 2');
+
+        $groupid = $this->data['group3']->id;
+        $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 3');
+        $this->assertEquals(0, $usercount, 'There was a problem counting the number of online users in group 3');
+    }
+
+    /**
+     * Check logged in users in courses 1 & 2 (should be 9 and 0).
+     *
+     * @param array $data Array of user, course and group objects
+     * @param int $now Current Unix timestamp
+     * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users
+     */
+    public function test_fetcher_courses() {
+
+        global $CFG;
+
+        $currentgroup = null;
+        $now = time();
+        $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+        $context = context_course::instance($this->data['course1']->id);
+        $courseid = $this->data['course1']->id;
+        $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, false, $courseid);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in course 1');
+        $this->assertEquals(9, $usercount, 'There was a problem counting the number of online users in course 1');
+
+        $courseid = $this->data['course2']->id;
+        $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, false, $courseid);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in course 2');
+        $this->assertEquals(0, $usercount, 'There was a problem counting the number of online users in course 2');
+    }
+
+    /**
+     * Check logged in at the site level (should be 12).
+     *
+     * @param int $now Current Unix timestamp
+     * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users
+     */
+    public function test_fetcher_sitelevel() {
+        global $CFG;
+
+        $currentgroup = null;
+        $now = time();
+        $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+        $context = context_system::instance();
+        $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, true);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users at site level');
+        $this->assertEquals(12, $usercount, 'There was a problem counting the number of online users at site level');
+    }
+}
index 2838651..68a13ac 100644 (file)
@@ -23,6 +23,8 @@
  */
 
  class block_rss_client extends block_base {
+    /** The maximum time in seconds that cron will wait between attempts to retry failing RSS feeds. */
+    const CLIENT_MAX_SKIPTIME = 43200; // 60 * 60 * 12 seconds.
 
     function init() {
         $this->title = get_string('pluginname', 'block_rss_client');
     }
 
     /**
-     * cron - goes through all feeds and retrieves them with the cache
-     * duration set to 0 in order to force the retrieval of the item and
-     * refresh the cache
+     * cron - goes through all the feeds. If the feed has a skipuntil value
+     * that is less than the current time cron will attempt to retrieve it
+     * with the cache duration set to 0 in order to force the retrieval of
+     * the item and refresh the cache.
      *
-     * @return boolean true if all feeds were retrieved succesfully
+     * If a feed fails then the skipuntil time of that feed is set to be
+     * later than the next expected cron time. The amount of time will
+     * increase each time the fetch fails until the maximum is reached.
+     *
+     * If a feed that has been failing is successfully retrieved it will
+     * go back to being handled as though it had never failed.
+     *
+     * CRON should therefor process requests for permanently broken RSS
+     * feeds infrequently, and temporarily unavailable feeds will be tried
+     * less often until they become available again.
+     *
+     * @return boolean Always returns true
      */
     function cron() {
         global $CFG, $DB;
         require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
 
+        // Get the legacy cron time, strangely the cron property of block_base
+        // does not seem to get set. This means we must retrive it here.
+        $this->cron = $DB->get_field('block', 'cron', array('name' => 'rss_client'));
+
         // We are going to measure execution times
         $starttime =  microtime();
-
-        // And we have one initial $status
-        $status = true;
+        $starttimesec = time();
 
         // Fetch all site feeds.
         $rs = $DB->get_recordset('block_rss_client');
         mtrace('');
         foreach ($rs as $rec) {
             mtrace('    ' . $rec->url . ' ', '');
+
+            // Skip feed if it failed recently.
+            if ($starttimesec < $rec->skipuntil) {
+                mtrace('skipping until ' . userdate($rec->skipuntil));
+                continue;
+            }
+
             // Fetch the rss feed, using standard simplepie caching
             // so feeds will be renewed only if cache has expired
             core_php_time_limit::raise(60);
             $feed->init();
 
             if ($feed->error()) {
-                mtrace('Error: could not load/find the RSS feed');
-                $status = false;
+                // Skip this feed (for an ever-increasing time if it keeps failing).
+                $rec->skiptime = $this->calculate_skiptime($rec->skiptime);
+                $rec->skipuntil = time() + $rec->skiptime;
+                $DB->update_record('block_rss_client', $rec);
+                mtrace("Error: could not load/find the RSS feed - skipping for {$rec->skiptime} seconds.");
             } else {
                 mtrace ('ok');
+                // It worked this time, so reset the skiptime.
+                if ($rec->skiptime > 0) {
+                    $rec->skiptime = 0;
+                    $rec->skipuntil = 0;
+                    $DB->update_record('block_rss_client', $rec);
+                }
+                // Only increase the counter when a feed is sucesfully refreshed.
+                $counter ++;
             }
-            $counter ++;
         }
         $rs->close();
 
         // Show times
         mtrace($counter . ' feeds refreshed (took ' . microtime_diff($starttime, microtime()) . ' seconds)');
 
-        // And return $status
-        return $status;
+        return true;
+    }
+
+    /**
+     * Calculates a new skip time for a record based on the current skip time.
+     *
+     * @param int $currentskip The curreent skip time of a record.
+     * @return int A new skip time that should be set.
+     */
+    protected function calculate_skiptime($currentskip) {
+        // The default time to skiptime.
+        $newskiptime = $this->cron * 1.1;
+        if ($currentskip > 0) {
+            // Double the last time.
+            $newskiptime = $currentskip * 2;
+        }
+        if ($newskiptime > self::CLIENT_MAX_SKIPTIME) {
+            // Do not allow the skip time to increase indefinatly.
+            $newskiptime = self::CLIENT_MAX_SKIPTIME;
+        }
+        return $newskiptime;
     }
 }
 
index f81d650..7a7e9cb 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="blocks/rss_client/db" VERSION="20120122" COMMENT="XMLDB file for Moodle rss_client block"
+<XMLDB PATH="blocks/rss_client/db" VERSION="20150717" COMMENT="XMLDB file for Moodle rss_client block"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
 >
@@ -13,6 +13,8 @@
         <FIELD NAME="description" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="shared" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="url" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="skiptime" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="How many seconds skip this feed for (increases every time it fails, resets to 0 when it succeeds)"/>
+        <FIELD NAME="skipuntil" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Do not query this RSS feed again until this time"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id" />
diff --git a/blocks/rss_client/db/upgrade.php b/blocks/rss_client/db/upgrade.php
new file mode 100644 (file)
index 0000000..afc2b80
--- /dev/null
@@ -0,0 +1,54 @@
+<?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/>.
+
+/**
+ * Database upgrades for the RSS block.
+ *
+ * @package   block_rss_client
+ * @copyright 2014 Davo Smith
+ * @author    Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL
+ */
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Upgrade the block_rss_client database.
+ *
+ * @param int $oldversion The version number of the plugin that was installed.
+ * @return boolean
+ */
+function xmldb_block_rss_client_upgrade($oldversion) {
+    global $DB;
+    $dbman = $DB->get_manager();
+
+    if ($oldversion < 2015071700) {
+        // Support for skipping RSS feeds for a while when they fail.
+        $table = new xmldb_table('block_rss_client');
+        // How many seconds we are currently ignoring this RSS feed for (due to an error).
+        $field = new xmldb_field('skiptime', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'url');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        // When to next update this RSS feed.
+        $field = new xmldb_field('skipuntil', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'skiptime');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        upgrade_block_savepoint(true, 2015071700, 'rss_client');
+    }
+
+    return true;
+}
diff --git a/blocks/rss_client/tests/cron_test.php b/blocks/rss_client/tests/cron_test.php
new file mode 100644 (file)
index 0000000..9069c28
--- /dev/null
@@ -0,0 +1,141 @@
+<?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 tests for rss client cron.
+ *
+ * @package    block_rss_client
+ * @copyright  2015 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+require_once(dirname(dirname(__DIR__)) . '/moodleblock.class.php');
+require_once(dirname(__DIR__) . '/block_rss_client.php');
+
+/**
+ * Class for the PHPunit tests for rss client cron.
+ *
+ * @package    block_rss_client
+ * @copyright  2015 Universit of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_rss_client_cron_testcase extends advanced_testcase {
+    /**
+     * Test that when a record has a skipuntil time that is greater
+     * than the current time the attempt is skipped.
+     */
+    public function test_skip() {
+        global $DB;
+        $this->resetAfterTest();
+        // Create a RSS feed record with a skip until time set to the future.
+        $record = (object) array(
+            'userid' => 1,
+            'title' => 'Skip test feed',
+            'preferredtitle' => '',
+            'description' => 'A feed to test the skip time.',
+            'shared' => 0,
+            'url' => 'http://example.com/rss',
+            'skiptime' => 330,
+            'skipuntil' => time() + 300,
+        );
+        $DB->insert_record('block_rss_client', $record);
+
+        $block = new block_rss_client();
+        ob_start();
+        // Silence SimplePie php notices.
+        @$block->cron();
+        $cronoutput = ob_get_clean();
+        $this->assertContains('skipping until ' . userdate($record->skipuntil), $cronoutput);
+        $this->assertContains('0 feeds refreshed (took ', $cronoutput);
+    }
+
+    /**
+     * Test that when a feed has an error the skip time is increaed correctly.
+     */
+    public function test_error() {
+        global $DB;
+        $this->resetAfterTest();
+        $time = time();
+        // A record that has failed before.
+        $record = (object) array(
+            'userid' => 1,
+            'title' => 'Skip test feed',
+            'preferredtitle' => '',
+            'description' => 'A feed to test the skip time.',
+            'shared' => 0,
+            'url' => 'http://example.com/rss',
+            'skiptime' => 330,
+            'skipuntil' => $time - 300,
+        );
+        $record->id = $DB->insert_record('block_rss_client', $record);
+
+        // A record that has not failed before.
+        $record2 = (object) array(
+            'userid' => 1,
+            'title' => 'Skip test feed',
+            'preferredtitle' => '',
+            'description' => 'A feed to test the skip time.',
+            'shared' => 0,
+            'url' => 'http://example.com/rss2',
+            'skiptime' => 0,
+            'skipuntil' => 0,
+        );
+        $record2->id = $DB->insert_record('block_rss_client', $record2);
+
+        // A record that is near the maximum wait time.
+        $record3 = (object) array(
+            'userid' => 1,
+            'title' => 'Skip test feed',
+            'preferredtitle' => '',
+            'description' => 'A feed to test the skip time.',
+            'shared' => 0,
+            'url' => 'http://example.com/rss3',
+            'skiptime' => block_rss_client::CLIENT_MAX_SKIPTIME - 5,
+            'skipuntil' => $time - 1,
+        );
+        $record3->id = $DB->insert_record('block_rss_client', $record3);
+
+        // Run the cron.
+        $block = new block_rss_client();
+        ob_start();
+        // Silence SimplePie php notices.
+        @$block->cron();
+        $cronoutput = ob_get_clean();
+        $skiptime1 = $record->skiptime * 2;
+        $message1 = 'http://example.com/rss Error: could not load/find the RSS feed - skipping for ' . $skiptime1 . ' seconds.';
+        $this->assertContains($message1, $cronoutput);
+        $skiptime2 = 330; // Assumes that the cron time in the version file is 300.
+        $message2 = 'http://example.com/rss2 Error: could not load/find the RSS feed - skipping for ' . $skiptime2 . ' seconds.';
+        $this->assertContains($message2, $cronoutput);
+        $skiptime3 = block_rss_client::CLIENT_MAX_SKIPTIME;
+        $message3 = 'http://example.com/rss3 Error: could not load/find the RSS feed - skipping for ' . $skiptime3 . ' seconds.';
+        $this->assertContains($message3, $cronoutput);
+        $this->assertContains('0 feeds refreshed (took ', $cronoutput);
+
+        // Test that the records have been correctly updated.
+        $newrecord = $DB->get_record('block_rss_client', array('id' => $record->id));
+        $this->assertAttributeEquals($skiptime1, 'skiptime', $newrecord);
+        $this->assertAttributeGreaterThanOrEqual($time + $skiptime1, 'skipuntil', $newrecord);
+        $newrecord2 = $DB->get_record('block_rss_client', array('id' => $record2->id));
+        $this->assertAttributeEquals($skiptime2, 'skiptime', $newrecord2);
+        $this->assertAttributeGreaterThanOrEqual($time + $skiptime2, 'skipuntil', $newrecord2);
+        $newrecord3 = $DB->get_record('block_rss_client', array('id' => $record3->id));
+        $this->assertAttributeEquals($skiptime3, 'skiptime', $newrecord3);
+        $this->assertAttributeGreaterThanOrEqual($time + $skiptime3, 'skipuntil', $newrecord3);
+    }
+}
index d1979d3..2e2cea9 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015071700;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500;        // Requires this Moodle version
 $plugin->component = 'block_rss_client'; // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 300;               // Set min time between cron executions to 300 secs (5 mins)
index e28c75b..cd26cbc 100644 (file)
@@ -47,7 +47,7 @@ class block_site_main_menu extends block_list {
             return $this->content;
         }
 
-        $course = $this->page->course;
+        $course = get_site();
         require_once($CFG->dirroot.'/course/lib.php');
         $context = context_course::instance($course->id);
         $isediting = $this->page->user_is_editing() && has_capability('moodle/course:manageactivities', $context);
@@ -73,8 +73,9 @@ class block_site_main_menu extends block_list {
                         $attrs['title'] = $cm->modfullname;
                         $attrs['class'] = $cm->extraclasses . ' activity-action';
                         if ($cm->onclick) {
-                            $attrs['id'] = html_writer::random_id('onclick');
-                            $OUTPUT->add_action_handler(new component_action('click', $cm->onclick), $attrs['id']);
+                            // Get on-click attribute value if specified and decode the onclick - it
+                            // has already been encoded for display.
+                            $attrs['onclick'] = htmlspecialchars_decode($cm->onclick);
                         }
                         if (!$cm->visible) {
                             $attrs['class'] .= ' dimmed';
@@ -161,8 +162,9 @@ class block_site_main_menu extends block_list {
                         $attrs['title'] = $mod->modfullname;
                         $attrs['class'] = $mod->extraclasses . ' activity-action';
                         if ($mod->onclick) {
-                            $attrs['id'] = html_writer::random_id('onclick');
-                            $OUTPUT->add_action_handler(new component_action('click', $mod->onclick), $attrs['id']);
+                            // Get on-click attribute value if specified and decode the onclick - it
+                            // has already been encoded for display.
+                            $attrs['onclick'] = htmlspecialchars_decode($mod->onclick);
                         }
                         if (!$mod->visible) {
                             $attrs['class'] .= ' dimmed';
diff --git a/blocks/site_main_menu/tests/behat/add_url.feature b/blocks/site_main_menu/tests/behat/add_url.feature
new file mode 100644 (file)
index 0000000..f391d7b
--- /dev/null
@@ -0,0 +1,18 @@
+@block @block_main_menu
+Feature: Add URL to main menu block
+  In order to add helpful resources for students
+  As a admin
+  I need to add URLs to the main menu block and check it works.
+
+  @javascript
+  Scenario: Add a URL in menu block and ensure it appears
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    When I add a "URL" to section "0" and I fill the form with:
+      | Name | google |
+      | Description | gooooooooogle |
+      | External URL | http://www.google.com |
+      | id_display | In pop-up |
+    Then "google" "link" should exist in the "Main menu" "block"
+    And "Add an activity or resource" "link" should exist in the "Main menu" "block"
index a06588e..7ab13be 100644 (file)
@@ -26,14 +26,29 @@ define('DEFAULT_NUMBER_OF_VIDEOS', 5);
 
 class block_tag_youtube extends block_base {
 
+    /**
+     * @var Google_Service_Youtube
+     */
+    protected $service = null;
+
     function init() {
         $this->title = get_string('pluginname','block_tag_youtube');
+        $this->config = new stdClass();
     }
 
     function applicable_formats() {
         return array('tag' => true);
     }
 
+    /**
+     * It can be configured.
+     *
+     * @return bool
+     */
+    public function has_config() {
+        return true;
+    }
+
     function specialization() {
         $this->title = !empty($this->config->title) ? $this->config->title : get_string('pluginname', 'block_tag_youtube');
         // Convert numeric categories (old YouTube API) to
@@ -56,6 +71,14 @@ class block_tag_youtube extends block_base {
             return $this->content;
         }
 
+        $this->content = new stdClass();
+        $this->content->footer = '';
+
+        if (!$this->get_service()) {
+            $this->content->text = $this->get_error_message();
+            return $this->content;
+        }
+
         $text = '';
         if(!empty($this->config->playlist)){
             //videos from a playlist
@@ -72,31 +95,41 @@ class block_tag_youtube extends block_base {
             }
         }
 
-        $this->content = new stdClass;
         $this->content->text = $text;
-        $this->content->footer = '';
 
         return $this->content;
     }
 
     function get_videos_by_playlist(){
 
+        if (!$service = $this->get_service()) {
+            return $this->get_error_message();
+        }
+
         $numberofvideos = DEFAULT_NUMBER_OF_VIDEOS;
         if( !empty($this->config->numberofvideos)) {
             $numberofvideos = $this->config->numberofvideos;
         }
 
-        $request = 'http://gdata.youtube.com/feeds/api/playlists/' .
-                   $this->config->playlist .
-                   '?start-index=1&max-results=' .
-                   $numberofvideos .
-                   '&format=5';
+        try {
+            $response = $service->playlistItems->listPlaylistItems('id,snippet', array(
+                'playlistId' => $this->config->playlist,
+                'maxResults' => $numberofvideos
+            ));
+        } catch (Google_Service_Exception $e) {
+            debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
+            return $this->get_error_message(get_string('requesterror', 'block_tag_youtube'));
+        }
 
-        return $this->fetch_request($request);
+        return $this->render_items($response);
     }
 
     function get_videos_by_tag(){
 
+        if (!$service = $this->get_service()) {
+            return $this->get_error_message();
+        }
+
         $tagid = optional_param('id', 0, PARAM_INT);   // tag id - for backware compatibility
         $tag = optional_param('tag', '', PARAM_TAG); // tag
 
@@ -117,17 +150,26 @@ class block_tag_youtube extends block_base {
             $numberofvideos = $this->config->numberofvideos;
         }
 
-        $request = 'http://gdata.youtube.com/feeds/api/videos?vq=' .
-                   $querytag .
-                   '&start-index=1&max-results=' .
-                   $numberofvideos .
-                   '&format=5';
+        try {
+            $response = $service->search->listSearch('id,snippet', array(
+                'q' => $querytag,
+                'type' => 'video',
+                'maxResults' => $numberofvideos
+            ));
+        } catch (Google_Service_Exception $e) {
+            debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
+            return $this->get_error_message(get_string('requesterror', 'block_tag_youtube'));
+        }
 
-        return $this->fetch_request($request);
+        return $this->render_items($response);
     }
 
     function get_videos_by_tag_and_category(){
 
+        if (!$service = $this->get_service()) {
+            return $this->get_error_message();
+        }
+
         $tagid = optional_param('id', 0, PARAM_INT);   // tag id - for backware compatibility
         $tag = optional_param('tag', '', PARAM_TAG); // tag
 
@@ -148,19 +190,32 @@ class block_tag_youtube extends block_base {
             $numberofvideos = $this->config->numberofvideos;
         }
 
-        $request = 'http://gdata.youtube.com/feeds/api/videos?category=' .
-                   $this->config->category .
-                   '&vq=' .
-                   $querytag .
-                   '&start-index=1&max-results=' .
-                   $numberofvideos .
-                   '&format=5';
-
+        try {
+            $response = $service->search->listSearch('id,snippet', array(
+                'q' => $querytag,
+                'type' => 'video',
+                'maxResults' => $numberofvideos,
+                'videoCategoryId' => $this->config->category
+            ));
+        } catch (Google_Service_Exception $e) {
+            debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
+            return $this->get_error_message(get_string('requesterror', 'block_tag_youtube'));
+        }
 
-        return $this->fetch_request($request);
+        return $this->render_items($response);
     }
 
-    function fetch_request($request){
+    /**
+     * Sends a request to fetch data.
+     *
+     * @see block_tag_youtube::service
+     * @deprecated since Moodle 2.8.8, 2.9.2 and 3.0 MDL-49085 - please do not use this function any more.
+     * @param string $request
+     * @throws coding_exception
+     */
+    public function fetch_request($request) {
+        throw new coding_exception('Sorry, this function has been deprecated in Moodle 2.8.8, 2.9.2 and 3.0. Use block_tag_youtube::get_service instead.');
+
         $c = new curl(array('cache' => true, 'module_cache'=>'tag_youtube'));
         $c->setopt(array('CURLOPT_TIMEOUT' => 3, 'CURLOPT_CONNECTTIMEOUT' => 3));
 
@@ -170,32 +225,118 @@ class block_tag_youtube extends block_base {
         return $this->render_video_list($xml);
     }
 
+    /**
+     * Renders the video list.
+     *
+     * @see block_tag_youtube::render_items
+     * @deprecated since Moodle 2.8.8, 2.9.2 and 3.0 MDL-49085 - please do not use this function any more.
+     * @param SimpleXMLElement $xml
+     * @throws coding_exception
+     */
     function render_video_list(SimpleXMLElement $xml){
+        throw new coding_exception('Sorry, this function has been deprecated in Moodle 2.8.8, 2.9.2 and 3.0. Use block_tag_youtube::render_items instead.');
+    }
 
-        $text = '';
-        $text .= '<ul class="yt-video-entry unlist img-text">';
-
-        foreach($xml->entry as $entry){
-            $media = $entry->children('http://search.yahoo.com/mrss/');
-            $playerattrs = $media->group->player->attributes();
-            $url = s($playerattrs['url']);
-            $thumbattrs = $media->group->thumbnail[0]->attributes();
-            $thumbnail = s($thumbattrs['url']);
-            $title = s($media->group->title);
-            $yt = $media->children('http://gdata.youtube.com/schemas/2007');
-            $secattrs = $yt->duration->attributes();
-            $seconds = $secattrs['seconds'];
-
-            $text .= '<li>';
-            $text .= '<div class="clearfix">';
-            $text .= '<a href="'. $url . '">';
-            $text .= '<img alt="" class="youtube-thumb" src="'. $thumbnail .'" /></a>';
-            $text .= '</div><span><a href="'. $url . '">'. $title .'</a></span>';
-            $text .= '<div>';
-            $text .= format_time($seconds);
-            $text .= "</div></li>\n";
+    /**
+     * Returns an error message.
+     *
+     * Useful when the block is not properly set or something goes wrong.
+     *
+     * @param string $message The message to display.
+     * @return string HTML
+     */
+    protected function get_error_message($message = null) {
+        global $OUTPUT;
+
+        if (empty($message)) {
+            $message = get_string('apierror', 'block_tag_youtube');
+        }
+        return $OUTPUT->notification($message);
+    }
+
+    /**
+     * Gets the youtube service object.
+     *
+     * @return Google_Service_YouTube
+     */
+    protected function get_service() {
+        global $CFG;
+
+        if (!$apikey = get_config('block_tag_youtube', 'apikey')) {
+            return false;
         }
-        $text .= "</ul><div class=\"clearer\"></div>\n";
+
+        // Wrapped in an if in case we call different get_videos_* multiple times.
+        if (!isset($this->service)) {
+            require_once($CFG->libdir . '/google/lib.php');
+            $client = get_google_client();
+            $client->setDeveloperKey($apikey);
+            $client->setScopes(array(Google_Service_YouTube::YOUTUBE_READONLY));
+            $this->service = new Google_Service_YouTube($client);
+        }
+
+        return $this->service;
+    }
+
+    /**
+     * Renders the list of items.
+     *
+     * @param array $videosdata
+     * @return string HTML
+     */
+    protected function render_items($videosdata) {
+
+        if (!$videosdata || empty($videosdata->items)) {
+            if (!empty($videosdata->error)) {
+                debugging('Error fetching data from youtube: ' . $videosdata->error->message, DEBUG_DEVELOPER);
+            }
+            return '';
+        }
+
+        // If we reach that point we already know that the API key is set.
+        $service = $this->get_service();
+
+        $text = html_writer::start_tag('ul', array('class' => 'yt-video-entry unlist img-text'));
+        foreach ($videosdata->items as $video) {
+
+            // Link to the video included in the playlist if listing a playlist.
+            if (!empty($video->snippet->resourceId)) {
+                $id = $video->snippet->resourceId->videoId;
+                $playlist = '&list=' . $video->snippet->playlistId;
+            } else {
+                $id = $video->id->videoId;
+                $playlist = '';
+            }
+
+            $thumbnail = $video->snippet->getThumbnails()->getDefault();
+            $url = 'http://www.youtube.com/watch?v=' . $id . $playlist;
+
+            $videodetails = $service->videos->listVideos('id,contentDetails', array('id' => $id));
+            if ($videodetails && !empty($videodetails->items)) {
+
+                // We fetch by id so we just use the first one.
+                $details = $videodetails->items[0];
+                $start = new DateTime('@0');
+                $start->add(new DateInterval($details->contentDetails->duration));
+                $seconds = $start->format('U');
+            }
+
+            $text .= html_writer::start_tag('li');
+
+            $imgattrs = array('class' => 'youtube-thumb', 'src' => $thumbnail->url, 'alt' => $video->snippet->title);
+            $thumbhtml = html_writer::empty_tag('img', $imgattrs);
+            $link = html_writer::tag('a', $thumbhtml, array('href' => $url));
+            $text .= html_writer::tag('div', $link, array('class' => 'clearfix'));
+
+            $text .= html_writer::tag('span', html_writer::tag('a', $video->snippet->title, array('href' => $url)));
+
+            if (!empty($seconds)) {
+                $text .= html_writer::tag('div', format_time($seconds));
+            }
+            $text .= html_writer::end_tag('li');
+        }
+        $text .= html_writer::end_tag('ul');
+
         return $text;
     }
 
diff --git a/blocks/tag_youtube/db/install.php b/blocks/tag_youtube/db/install.php
new file mode 100644 (file)
index 0000000..0572762
--- /dev/null
@@ -0,0 +1,36 @@
+<?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/>.
+
+/**
+ * Tag Youtube block installation.
+ *
+ * @package    block_tag_youtube
+ * @copyright  2015 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Sets the install values for the tag_youtube entry in the block table.
+ *
+ * @return void
+ */
+function xmldb_block_tag_youtube_install() {
+    global $DB;
+
+    // Disable this block by default.
+    $DB->set_field('block', 'visible', 0, array('name' => 'tag_youtube'));
+}
+
index 2f3cec1..d56c330 100644 (file)
@@ -23,6 +23,9 @@
  */
 
 $string['anycategory'] = 'Any category';
+$string['apierror'] = 'The Youtube API key is not set. Contact your administrator.';
+$string['apikey'] = 'API key';
+$string['apikeyinfo'] = 'Get a <a href="https://developers.google.com/youtube/v3/getting-started">Google API key</a> for your Moodle site.';
 $string['autosvehicles'] = 'Autos &amp; Vehicles';
 $string['category'] = 'Category';
 $string['comedy'] = 'Comedy';
@@ -38,8 +41,9 @@ $string['newspolitics'] = 'News &amp; Politics';
 $string['numberofvideos'] = 'Number of videos';
 $string['peopleblogs'] = 'People &amp; Blogs';
 $string['petsanimals'] = 'Pets &amp; Animals';
-$string['pluginname'] = 'Youtube';
+$string['pluginname'] = 'YouTube';
+$string['requesterror'] = 'Data could not be obtained from the server. Contact your administrator if the problem persist.';
 $string['scienceandtech'] = 'Science &amp; Tech';
 $string['sports'] = 'Sports';
-$string['tag_youtube:addinstance'] = 'Add a new youtube block';
+$string['tag_youtube:addinstance'] = 'Add a new YouTube block';
 $string['travel'] = 'Travel &amp; Places';
diff --git a/blocks/tag_youtube/settings.php b/blocks/tag_youtube/settings.php
new file mode 100644 (file)
index 0000000..ad9f443
--- /dev/null
@@ -0,0 +1,30 @@
+<?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/>.
+
+/**
+ * Settings for the RSS client block.
+ *
+ * @package   block_tag_youtube
+ * @copyright 2015 David Monllao
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($ADMIN->fulltree) {
+    $settings->add(new admin_setting_configtext('block_tag_youtube/apikey', get_string('apikey', 'block_tag_youtube'),
+                       get_string('apikeyinfo', 'block_tag_youtube'), '', PARAM_RAW_TRIMMED, 40));
+}
diff --git a/blocks/tag_youtube/tests/block_tag_youtube_test.php b/blocks/tag_youtube/tests/block_tag_youtube_test.php
new file mode 100644 (file)
index 0000000..6434c44
--- /dev/null
@@ -0,0 +1,51 @@
+<?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/>.
+
+/**
+ * Block Tag Youtube tests
+ *
+ * @package    block_tag_youtube
+ * @category   test
+ * @copyright  2015 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Block Tag Youtube test class.
+ *
+ * @package   block_tag_youtube
+ * @category  test
+ * @copyright 2015 Jun Pataleta
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_block_tag_youtube_testcase extends advanced_testcase {
+
+    /**
+     * Testing the tag youtube block's initial state after a new installation.
+     *
+     * @return void
+     */
+    public function test_after_install() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Assert that tag_youtube entry exists and that its visible attribute is set to 0 (disabled).
+        $this->assertTrue($DB->record_exists('block', array('name' => 'tag_youtube', 'visible' => 0)));
+    }
+}
diff --git a/blocks/tag_youtube/upgrade.txt b/blocks/tag_youtube/upgrade.txt
new file mode 100644 (file)
index 0000000..ae3d80d
--- /dev/null
@@ -0,0 +1,8 @@
+This files describes API changes in the block tag_youtube code.
+
+=== 3.0 ===
+
+* Due to the final YouTube API v2.0 deprecation we needed to adapt the current
+  code to YouTube Data API v3. block_tag_youtube::fetch_request and
+  block_tag_youtube::render_video_list have been deprecated as they can not be
+  used any more.
index 1a5c4de..832b9f1 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015051101;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500;        // Requires this Moodle version
 $plugin->component = 'block_tag_youtube'; // Full name of the plugin (used for diagnostics)
index 0265fe9..2851ae7 100644 (file)
@@ -27,7 +27,6 @@ Feature: Block tags displaying tag cloud
     And I press "Update profile"
     And I log out
 
-  @javascript
   Scenario: Add Tags block on a front page
     When I log in as "admin"
     And I am on site homepage
@@ -41,7 +40,6 @@ Feature: Block tags displaying tag cloud
     And I click on "Dogs" "link" in the "Tags" "block"
     And I should see "Log in to the site" in the ".breadcrumb" "css_element"
 
-  @javascript
   Scenario: Add Tags block in a course
     When I log in as "teacher1"
     And I follow "Course 1"
index 4b1fd5a..363714c 100644 (file)
@@ -24,7 +24,6 @@ Feature: Allowed blocks controls
     Then I should see "Activities" in the "Activities" "block"
     And I should see "Course completion status" in the "Course completion status" "block"
 
-  @javascript
   Scenario: Blocks can not be added when the admin restricts the permissions
     Given I log in as "admin"
     And I set the following system permissions of "Teacher" role:
index bd38cad..dfb5c8f 100644 (file)
@@ -24,7 +24,6 @@ Feature: Blogs can be set to be only visible by the author.
     And I press "Save changes"
     And I log out
 
-  @javascript
   Scenario: A student can not see another student's blog entries.
     Given I log in as "testuser"
     And I follow "Course 1"
index eca785f..c25369e 100644 (file)
@@ -354,7 +354,7 @@ class cache_helper {
     protected static function ensure_ready_for_stats($store, $definition, $mode = cache_store::MODE_APPLICATION) {
         // This function is performance-sensitive, so exit as quickly as possible
         // if we do not need to do anything.
-        if (isset(self::$stats[$definition][$store])) {
+        if (isset(self::$stats[$definition]['stores'][$store])) {
             return;
         }
         if (!array_key_exists($definition, self::$stats)) {
@@ -368,7 +368,7 @@ class cache_helper {
                     )
                 )
             );
-        } else if (!array_key_exists($store, self::$stats[$definition])) {
+        } else if (!array_key_exists($store, self::$stats[$definition]['stores'])) {
             self::$stats[$definition]['stores'][$store] = array(
                 'hits' => 0,
                 'misses' => 0,
index 514a4bd..e044c91 100644 (file)
@@ -341,8 +341,8 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
             $maxtime = cache::now() - $ttl;
         }
         $readfile = false;
-        if ($this->prescan && array_key_exists($key, $this->keys)) {
-            if (!$ttl || $this->keys[$filename] >= $maxtime && file_exists($file)) {
+        if ($this->prescan && array_key_exists($filename, $this->keys)) {
+            if ((!$ttl || $this->keys[$filename] >= $maxtime) && file_exists($file)) {
                 $readfile = true;
             } else {
                 $this->delete($key);
index be321f2..887c545 100644 (file)
@@ -44,4 +44,34 @@ class cachestore_file_test extends cachestore_tests {
     protected function get_class_name() {
         return 'cachestore_file';
     }
+
+    /**
+     * Testing cachestore_file::get with prescan enabled and with
+     * deleting the cache between the prescan and the call to get.
+     *
+     * The deleting of cache simulates some other process purging
+     * the cache.
+     */
+    public function test_cache_get_with_prescan_and_purge() {
+        global $CFG;
+
+        $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'cachestore_file', 'phpunit_test');
+        $name = 'File test';
+
+        $path = make_cache_directory('cachestore_file_test');
+        $cache = new cachestore_file($name, array('path' => $path, 'prescan' => true));
+        $cache->initialise($definition);
+
+        $cache->set('testing', 'value');
+
+        $path  = make_cache_directory('cachestore_file_test');
+        $cache = new cachestore_file($name, array('path' => $path, 'prescan' => true));
+        $cache->initialise($definition);
+
+        // Let's pretend that some other process purged caches.
+        remove_dir($CFG->cachedir.'/cachestore_file_test', true);
+        make_cache_directory('cachestore_file_test');
+
+        $cache->get('testing');
+    }
 }
\ No newline at end of file
index 8300854..0d6daa7 100644 (file)
@@ -1875,4 +1875,155 @@ class core_cache_testcase extends advanced_testcase {
         $returnedinstance1->name = 'b';
         $this->assertEquals('b', $returnedinstance2->name);
     }
+
+    public function test_performance_debug() {
+        global $CFG;
+        $this->resetAfterTest(true);
+        $CFG->perfdebug = 15;
+
+        $instance = cache_config_testing::instance();
+        $applicationid = 'phpunit/applicationperf';
+        $instance->phpunit_add_definition($applicationid, array(
+            'mode' => cache_store::MODE_APPLICATION,
+            'component' => 'phpunit',
+            'area' => 'applicationperf'
+        ));
+        $sessionid = 'phpunit/sessionperf';
+        $instance->phpunit_add_definition($sessionid, array(
+            'mode' => cache_store::MODE_SESSION,
+            'component' => 'phpunit',
+            'area' => 'sessionperf'
+        ));
+        $requestid = 'phpunit/requestperf';
+        $instance->phpunit_add_definition($requestid, array(
+            'mode' => cache_store::MODE_REQUEST,
+            'component' => 'phpunit',
+            'area' => 'requestperf'
+        ));
+
+        $application = cache::make('phpunit', 'applicationperf');
+        $session = cache::make('phpunit', 'sessionperf');
+        $request = cache::make('phpunit', 'requestperf');
+
+        // Check that no stats are recorded for these definitions yet.
+        $stats = cache_helper::get_stats();
+        $this->assertArrayNotHasKey($applicationid, $stats);
+        $this->assertArrayHasKey($sessionid, $stats);       // Session cache sets a key on construct.
+        $this->assertArrayNotHasKey($requestid, $stats);
+
+        // Check that stores register misses.
+        $this->assertFalse($application->get('missMe'));
+        $this->assertFalse($application->get('missMe'));
+        $this->assertFalse($session->get('missMe'));
+        $this->assertFalse($session->get('missMe'));
+        $this->assertFalse($session->get('missMe'));
+        $this->assertFalse($request->get('missMe'));
+        $this->assertFalse($request->get('missMe'));
+        $this->assertFalse($request->get('missMe'));
+        $this->assertFalse($request->get('missMe'));
+
+        $endstats = cache_helper::get_stats();
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['misses']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['misses']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits']);
+        $this->assertEquals(1, $endstats[$sessionid]['stores']['cachestore_session']['sets']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['misses']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets']);
+
+        $startstats = cache_helper::get_stats();
+
+        // Check that stores register sets.
+        $this->assertTrue($application->set('setMe1', 1));
+        $this->assertTrue($application->set('setMe2', 2));
+        $this->assertTrue($session->set('setMe1', 1));
+        $this->assertTrue($session->set('setMe2', 2));
+        $this->assertTrue($session->set('setMe3', 3));
+        $this->assertTrue($request->set('setMe1', 1));
+        $this->assertTrue($request->set('setMe2', 2));
+        $this->assertTrue($request->set('setMe3', 3));
+        $this->assertTrue($request->set('setMe4', 4));
+
+        $endstats = cache_helper::get_stats();
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
+            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
+            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
+            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
+            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
+            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
+            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
+            $startstats[$requestid]['stores']['cachestore_static']['misses']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
+            $startstats[$requestid]['stores']['cachestore_static']['hits']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
+            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+
+        $startstats = cache_helper::get_stats();
+
+        // Check that stores register hits.
+        $this->assertEquals($application->get('setMe1'), 1);
+        $this->assertEquals($application->get('setMe2'), 2);
+        $this->assertEquals($session->get('setMe1'), 1);
+        $this->assertEquals($session->get('setMe2'), 2);
+        $this->assertEquals($session->get('setMe3'), 3);
+        $this->assertEquals($request->get('setMe1'), 1);
+        $this->assertEquals($request->get('setMe2'), 2);
+        $this->assertEquals($request->get('setMe3'), 3);
+        $this->assertEquals($request->get('setMe4'), 4);
+
+        $endstats = cache_helper::get_stats();
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
+            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
+            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
+            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
+            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
+            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
+            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
+            $startstats[$requestid]['stores']['cachestore_static']['misses']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
+            $startstats[$requestid]['stores']['cachestore_static']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
+            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+
+        $startstats = cache_helper::get_stats();
+
+        // Check that stores register through get_many.
+        $application->get_many(array('setMe1', 'setMe2'));
+        $session->get_many(array('setMe1', 'setMe2', 'setMe3'));
+        $request->get_many(array('setMe1', 'setMe2', 'setMe3', 'setMe4'));
+
+        $endstats = cache_helper::get_stats();
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
+            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
+            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
+            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
+            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
+            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
+            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
+            $startstats[$requestid]['stores']['cachestore_static']['misses']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
+            $startstats[$requestid]['stores']['cachestore_static']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
+            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+    }
 }
index 46e6c56..d11b6d9 100644 (file)
@@ -612,7 +612,7 @@ class core_calendar_renderer extends plugin_renderer_base {
 
         if (empty($subscriptions)) {
             $cell = new html_table_cell(get_string('nocalendarsubscriptions', 'calendar'));
-            $cell->colspan = 4;
+            $cell->colspan = 5;
             $table->data[] = new html_table_row(array($cell));
         }
         $strnever = new lang_string('never', 'calendar');
index 345f9e7..fab8aa5 100644 (file)
@@ -41,8 +41,6 @@
 require_once('../config.php');
 require_once($CFG->dirroot.'/calendar/lib.php');
 
-require_sesskey();
-
 $var = required_param('var', PARAM_ALPHA);
 $return = clean_param(base64_decode(required_param('return', PARAM_RAW)), PARAM_LOCALURL);
 $courseid = optional_param('id', -1, PARAM_INT);
@@ -51,6 +49,12 @@ if ($courseid != -1) {
 } else {
     $return = new moodle_url($return);
 }
+
+if (!confirm_sesskey()) {
+    // Do not call require_sesskey() since this page may be accessed without session (for example by bots).
+    redirect($return);
+}
+
 $url = new moodle_url('/calendar/set.php', array('return'=>base64_encode($return->out_as_local_url(false)), 'course' => $courseid, 'var'=>$var, 'sesskey'=>sesskey()));
 $PAGE->set_url($url);
 $PAGE->set_context(context_system::instance());
index 5125a88..6790dbf 100644 (file)
@@ -27,7 +27,8 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given,
+use Behat\Behat\Context\Step\Given,
+    Behat\Behat\Context\Step\Then,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
 
 /**
@@ -56,7 +57,7 @@ class behat_completion extends behat_base {
 
         return array(
             new Given('I go to the current course activity completion report'),
-            new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
+            new Then('"' . $this->escape($xpath) . '" "xpath_element" should exist')
         );
     }
 
@@ -75,7 +76,7 @@ class behat_completion extends behat_base {
             "/descendant::img[contains(@title, $titleliteral)]";
         return array(
             new Given('I go to the current course activity completion report'),
-            new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
+            new Then('"' . $this->escape($xpath) . '" "xpath_element" should exist')
         );
 
         return $steps;
index de96736..26e49e4 100644 (file)
@@ -1,7 +1,7 @@
 {
     "require-dev": {
-        "phpunit/phpunit": "3.7.*",
-        "phpunit/dbUnit": "1.2.*",
+        "phpunit/phpunit": "4.7.*",
+        "phpunit/dbUnit": "1.4.*",
         "moodlehq/behat-extension": "1.30.0"
     }
 }
index cec8279..719ac08 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "3ddf5ab21f539f6f64c7e80174be48bb",
+    "hash": "a85d8c9e61ccba5e235093157021f7b5",
     "packages": [],
     "packages-dev": [
         {
             ],
             "time": "2014-12-20 21:24:13"
         },
+        {
+            "name": "doctrine/instantiator",
+            "version": "1.0.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/instantiator.git",
+                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
+                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3,<8.0-DEV"
+            },
+            "require-dev": {
+                "athletic/athletic": "~0.1.8",
+                "ext-pdo": "*",
+                "ext-phar": "*",
+                "phpunit/phpunit": "~4.0",
+                "squizlabs/php_codesniffer": "~2.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marco Pivetta",
+                    "email": "ocramius@gmail.com",
+                    "homepage": "http://ocramius.github.com/"
+                }
+            ],
+            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+            "homepage": "https://github.com/doctrine/instantiator",
+            "keywords": [
+                "constructor",
+                "instantiate"
+            ],
+            "time": "2015-06-14 21:17:01"
+        },
         {
             "name": "doctrine/lexer",
             "version": "v1.0.1",
             ],
             "time": "2015-05-15 02:00:06"
         },
+        {
+            "name": "phpdocumentor/reflection-docblock",
+            "version": "2.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+                "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8",
+                "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.0"
+            },
+            "suggest": {
+                "dflydev/markdown": "~1.0",
+                "erusev/parsedown": "~1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "phpDocumentor": [
+                        "src/"
+                    ]
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "mike.vanriel@naenius.com"
+                }
+            ],
+            "time": "2015-02-03 12:10:50"
+        },
+        {
+            "name": "phpspec/prophecy",
+            "version": "v1.4.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpspec/prophecy.git",
+                "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373",
+                "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/instantiator": "^1.0.2",
+                "phpdocumentor/reflection-docblock": "~2.0",
+                "sebastian/comparator": "~1.1"
+            },
+            "require-dev": {
+                "phpspec/phpspec": "~2.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.4.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Prophecy\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Konstantin Kudryashov",
+                    "email": "ever.zet@gmail.com",
+                    "homepage": "http://everzet.com"
+                },
+                {
+                    "name": "Marcello Duarte",
+                    "email": "marcello.duarte@gmail.com"
+                }
+            ],
+            "description": "Highly opinionated mocking framework for PHP 5.3+",
+            "homepage": "https://github.com/phpspec/prophecy",
+            "keywords": [
+                "Double",
+                "Dummy",
+                "fake",
+                "mock",
+                "spy",
+                "stub"
+            ],
+            "time": "2015-04-27 22:15:08"
+        },
         {
             "name": "phpunit/dbunit",
-            "version": "1.2.3",
+            "version": "1.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/dbunit.git",
-                "reference": "8386782a2d55153e44a06eb1a9d13d6ed35d9c2d"
+                "reference": "1afe25c90834ec499f007f48dd73767fdec3bf4f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/8386782a2d55153e44a06eb1a9d13d6ed35d9c2d",
-                "reference": "8386782a2d55153e44a06eb1a9d13d6ed35d9c2d",
+                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/1afe25c90834ec499f007f48dd73767fdec3bf4f",
+                "reference": "1afe25c90834ec499f007f48dd73767fdec3bf4f",
                 "shasum": ""
             },
             "require": {
                 "ext-pdo": "*",
                 "ext-simplexml": "*",
                 "php": ">=5.3.3",
-                "phpunit/phpunit": ">=3.7.0@stable"
+                "phpunit/phpunit": "~4.0",
+                "symfony/yaml": "~2.1"
             },
             "bin": [
-                "dbunit.php"
+                "composer/bin/dbunit"
             ],
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.2.x-dev"
+                    "dev-master": "1.3.x-dev"
                 }
             },
             "autoload": {
                 "testing",
                 "xunit"
             ],
-            "time": "2013-03-01 11:50:46"
+            "time": "2015-05-21 21:11:02"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "1.2.18",
+            "version": "2.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b"
+                "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b",
-                "reference": "fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2d7c03c0e4e080901b8f33b2897b0577be18a13c",
+                "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.3",
-                "phpunit/php-file-iterator": ">=1.3.0@stable",
-                "phpunit/php-text-template": ">=1.2.0@stable",
-                "phpunit/php-token-stream": ">=1.1.3,<1.3.0"
+                "phpunit/php-file-iterator": "~1.3",
+                "phpunit/php-text-template": "~1.2",
+                "phpunit/php-token-stream": "~1.3",
+                "sebastian/environment": "^1.3.2",
+                "sebastian/version": "~1.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "3.7.*@dev"
+                "ext-xdebug": ">=2.1.4",
+                "phpunit/phpunit": "~4"
             },
             "suggest": {
                 "ext-dom": "*",
-                "ext-xdebug": ">=2.0.5"
+                "ext-xdebug": ">=2.2.1",
+                "ext-xmlwriter": "*"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.2.x-dev"
+                    "dev-master": "2.2.x-dev"
                 }
             },
             "autoload": {
                 "classmap": [
-                    "PHP/"
+                    "src/"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
-            "include-path": [
-                ""
-            ],
             "license": [
                 "BSD-3-Clause"
             ],
                 "testing",
                 "xunit"
             ],
-            "time": "2014-09-02 10:13:14"
+            "time": "2015-08-04 03:42:39"
         },
         {
             "name": "phpunit/php-file-iterator",
-            "version": "1.4.0",
+            "version": "1.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "a923bb15680d0089e2316f7a4af8f437046e96bb"
+                "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a923bb15680d0089e2316f7a4af8f437046e96bb",
-                "reference": "a923bb15680d0089e2316f7a4af8f437046e96bb",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
+                "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
                 "shasum": ""
             },
             "require": {
                 "filesystem",
                 "iterator"
             ],
-            "time": "2015-04-02 05:19:05"
+            "time": "2015-06-21 13:08:43"
         },
         {
             "name": "phpunit/php-text-template",
         },
         {
             "name": "phpunit/php-timer",
-            "version": "1.0.6",
+            "version": "1.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-timer.git",
-                "reference": "83fe1bdc5d47658b727595c14da140da92b3d66d"
+                "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/83fe1bdc5d47658b727595c14da140da92b3d66d",
-                "reference": "83fe1bdc5d47658b727595c14da140da92b3d66d",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b",
+                "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b",
                 "shasum": ""
             },
             "require": {
             "keywords": [
                 "timer"
             ],
-            "time": "2015-06-13 07:35:30"
+            "time": "2015-06-21 08:01:12"
         },
         {
             "name": "phpunit/php-token-stream",
-            "version": "1.2.2",
+            "version": "1.4.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "ad4e1e23ae01b483c16f600ff1bebec184588e32"
+                "reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/ad4e1e23ae01b483c16f600ff1bebec184588e32",
-                "reference": "ad4e1e23ae01b483c16f600ff1bebec184588e32",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/7a9b0969488c3c54fd62b4d504b3ec758fd005d9",
+                "reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9",
                 "shasum": ""
             },
             "require": {
                 "ext-tokenizer": "*",
                 "php": ">=5.3.3"
             },
+            "require-dev": {
+                "phpunit/phpunit": "~4.2"
+            },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.2-dev"
+                    "dev-master": "1.4-dev"
                 }
             },
             "autoload": {
                 "classmap": [
-                    "PHP/"
+                    "src/"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
-            "include-path": [
-                ""
-            ],
             "license": [
                 "BSD-3-Clause"
             ],
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
-                    "role": "lead"
+                    "email": "sebastian@phpunit.de"
                 }
             ],
             "description": "Wrapper around PHP's tokenizer extension.",
             "keywords": [
                 "tokenizer"
             ],
-            "time": "2014-03-03 05:10:30"
+            "time": "2015-06-19 03:43:16"
         },
         {
             "name": "phpunit/phpunit",
-            "version": "3.7.38",
+            "version": "4.7.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "38709dc22d519a3d1be46849868aa2ddf822bcf6"
+                "reference": "9b97f9d807b862c2de2a36e86690000801c85724"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/38709dc22d519a3d1be46849868aa2ddf822bcf6",
-                "reference": "38709dc22d519a3d1be46849868aa2ddf822bcf6",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9b97f9d807b862c2de2a36e86690000801c85724",
+                "reference": "9b97f9d807b862c2de2a36e86690000801c85724",
                 "shasum": ""
             },
             "require": {
-                "ext-ctype": "*",
                 "ext-dom": "*",
                 "ext-json": "*",
                 "ext-pcre": "*",
                 "ext-reflection": "*",
                 "ext-spl": "*",
                 "php": ">=5.3.3",
-                "phpunit/php-code-coverage": "~1.2",
-                "phpunit/php-file-iterator": "~1.3",
-                "phpunit/php-text-template": "~1.1",
-                "phpunit/php-timer": "~1.0",
-                "phpunit/phpunit-mock-objects": "~1.2",
-                "symfony/yaml": "~2.0"
-            },
-            "require-dev": {
-                "pear-pear.php.net/pear": "1.9.4"
+                "phpspec/prophecy": "~1.3,>=1.3.1",
+                "phpunit/php-code-coverage": "~2.1",
+                "phpunit/php-file-iterator": "~1.4",
+                "phpunit/php-text-template": "~1.2",
+                "phpunit/php-timer": ">=1.0.6",
+                "phpunit/phpunit-mock-objects": "~2.3",
+                "sebastian/comparator": "~1.1",
+                "sebastian/diff": "~1.2",
+                "sebastian/environment": "~1.2",
+                "sebastian/exporter": "~1.2",
+                "sebastian/global-state": "~1.0",
+                "sebastian/version": "~1.0",
+                "symfony/yaml": "~2.1|~3.0"
             },
             "suggest": {
                 "phpunit/php-invoker": "~1.1"
             },
             "bin": [
-                "composer/bin/phpunit"
+                "phpunit"
             ],
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.7.x-dev"
+                    "dev-master": "4.7.x-dev"
                 }
             },
             "autoload": {
                 "classmap": [
-                    "PHPUnit/"
+                    "src/"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
-            "include-path": [
-                "",
-                "../../symfony/yaml/"
-            ],
             "license": [
                 "BSD-3-Clause"
             ],
                 }
             ],
             "description": "The PHP Unit Testing framework.",
-            "homepage": "http://www.phpunit.de/",
+            "homepage": "https://phpunit.de/",
             "keywords": [
                 "phpunit",
                 "testing",
                 "xunit"
             ],
-            "time": "2014-10-17 09:04:17"
+            "time": "2015-07-13 11:28:34"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
-            "version": "1.2.3",
+            "version": "2.3.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "5794e3c5c5ba0fb037b11d8151add2a07fa82875"
+                "reference": "18dfbcb81d05e2296c0bcddd4db96cade75e6f42"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/5794e3c5c5ba0fb037b11d8151add2a07fa82875",
-                "reference": "5794e3c5c5ba0fb037b11d8151add2a07fa82875",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/18dfbcb81d05e2296c0bcddd4db96cade75e6f42",
+                "reference": "18dfbcb81d05e2296c0bcddd4db96cade75e6f42",
                 "shasum": ""
             },
             "require": {
+                "doctrine/instantiator": "~1.0,>=1.0.2",
                 "php": ">=5.3.3",
-                "phpunit/php-text-template": ">=1.1.1@stable"
+                "phpunit/php-text-template": "~1.2",
+                "sebastian/exporter": "~1.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.4"
             },
             "suggest": {
                 "ext-soap": "*"
             },
             "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.3.x-dev"
+                }
+            },
             "autoload": {
                 "classmap": [
-                    "PHPUnit/"
+                    "src/"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
-            "include-path": [
-                ""
-            ],
             "license": [
                 "BSD-3-Clause"
             ],
                 "mock",
                 "xunit"
             ],
-            "time": "2013-01-13 10:24:48"
+            "time": "2015-07-10 06:54:24"
         },
         {
             "name": "psr/log",
             ],
             "time": "2012-12-21 11:40:51"
         },
+        {
+            "name": "sebastian/comparator",
+            "version": "1.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/comparator.git",
+                "reference": "937efb279bd37a375bcadf584dec0726f84dbf22"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22",
+                "reference": "937efb279bd37a375bcadf584dec0726f84dbf22",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "sebastian/diff": "~1.2",
+                "sebastian/exporter": "~1.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.2.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "github@wallbash.com"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@2bepublished.at"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Provides the functionality to compare PHP values for equality",
+            "homepage": "http://www.github.com/sebastianbergmann/comparator",
+            "keywords": [
+                "comparator",
+                "compare",
+                "equality"
+            ],
+            "time": "2015-07-26 15:48:44"
+        },
+        {
+            "name": "sebastian/diff",
+            "version": "1.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/diff.git",
+                "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/863df9687835c62aa423a22412d26fa2ebde3fd3",
+                "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.3-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Kore Nordmann",
+                    "email": "mail@kore-nordmann.de"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Diff implementation",
+            "homepage": "http://www.github.com/sebastianbergmann/diff",
+            "keywords": [
+                "diff"
+            ],
+            "time": "2015-02-22 15:13:53"
+        },
+        {
+            "name": "sebastian/environment",
+            "version": "1.3.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/environment.git",
+                "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6324c907ce7a52478eeeaede764f48733ef5ae44",
+                "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.3.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Provides functionality to handle HHVM/PHP environments",
+            "homepage": "http://www.github.com/sebastianbergmann/environment",
+            "keywords": [
+                "Xdebug",
+                "environment",
+                "hhvm"
+            ],
+            "time": "2015-08-03 06:14:51"
+        },
+        {
+            "name": "sebastian/exporter",
+            "version": "1.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/exporter.git",
+                "reference": "7ae5513327cb536431847bcc0c10edba2701064e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e",
+                "reference": "7ae5513327cb536431847bcc0c10edba2701064e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "sebastian/recursion-context": "~1.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.2.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "github@wallbash.com"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@2bepublished.at"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "aharvey@php.net"
+                }
+            ],
+            "description": "Provides the functionality to export PHP variables for visualization",
+            "homepage": "http://www.github.com/sebastianbergmann/exporter",
+            "keywords": [
+                "export",
+                "exporter"
+            ],
+            "time": "2015-06-21 07:55:53"
+        },
+        {
+            "name": "sebastian/global-state",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/global-state.git",
+                "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c7428acdb62ece0a45e6306f1ae85e1c05b09c01",
+                "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.2"
+            },
+            "suggest": {
+                "ext-uopz": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Snapshotting of global state",
+            "homepage": "http://www.github.com/sebastianbergmann/global-state",
+            "keywords": [
+                "global state"
+            ],
+            "time": "2014-10-06 09:23:50"
+        },
+        {
+            "name": "sebastian/recursion-context",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/recursion-context.git",
+                "reference": "994d4a811bafe801fb06dccbee797863ba2792ba"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/994d4a811bafe801fb06dccbee797863ba2792ba",
+                "reference": "994d4a811bafe801fb06dccbee797863ba2792ba",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "aharvey@php.net"
+                }
+            ],
+            "description": "Provides functionality to recursively process PHP variables",
+            "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+            "time": "2015-06-21 08:04:50"
+        },
+        {
+            "name": "sebastian/version",
+            "version": "1.0.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/version.git",
+                "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
+                "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+            "homepage": "https://github.com/sebastianbergmann/version",
+            "time": "2015-06-21 13:59:46"
+        },
         {
             "name": "symfony/icu",
             "version": "v1.2.2",
         },
         {
             "name": "twig/twig",
-            "version": "v1.18.2",
+            "version": "v1.19.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twigphp/Twig.git",
-                "reference": "e8e6575abf6102af53ec283f7f14b89e304fa602"
+                "reference": "edbeaf43b0a606cdaadc32a11d2673614a377b90"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twigphp/Twig/zipball/e8e6575abf6102af53ec283f7f14b89e304fa602",
-                "reference": "e8e6575abf6102af53ec283f7f14b89e304fa602",
+                "url": "https://api.github.com/repos/twigphp/Twig/zipball/edbeaf43b0a606cdaadc32a11d2673614a377b90",
+                "reference": "edbeaf43b0a606cdaadc32a11d2673614a377b90",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.18-dev"
+                    "dev-master": "1.19-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "templating"
             ],
-            "time": "2015-06-06 23:31:24"
+            "time": "2015-07-31 13:45:26"
         }
     ],
     "aliases": [],
index 6196a57..df033d9 100644 (file)
@@ -489,8 +489,8 @@ $CFG->admin = 'admin';
 //      $CFG->supportuserid = -20;
 //
 // Moodle 2.7 introduces a locking api for critical tasks (e.g. cron).
-// The default locking system to use is DB locking for MySQL and Postgres, and File
-// locking for Oracle and SQLServer. If $CFG->preventfilelocking is set, then the default
+// The default locking system to use is DB locking for Postgres, and file locking for
+// MySQL, Oracle and SQLServer. If $CFG->preventfilelocking is set, then the default
 // will always be DB locking. It can be manually set to one of the lock
 // factory classes listed below, or one of your own custom classes implementing the
 // \core\lock\lock_factory interface.
index 4c9289b..65cd546 100644 (file)
@@ -338,16 +338,10 @@ class format_singleactivity extends format_base {
      * @return bool|null (null if the check is not possible)
      */
     public function activity_has_subtypes() {
-        global $CFG;
         if (!($modname = $this->get_activitytype())) {
             return null;
         }
-        $libfile = "$CFG->dirroot/mod/$modname/lib.php";
-        if (!file_exists($libfile)) {
-            return null;
-        }
-        include_once($libfile);
-        return function_exists($modname. '_get_types');
+        return component_callback('mod_' . $modname, 'get_types', array(), MOD_SUBTYPE_NO_CHILDREN) !== MOD_SUBTYPE_NO_CHILDREN;
     }
 
     /**
index 3d49264..89588dc 100644 (file)
@@ -232,17 +232,6 @@ function edit_module_post_actions($moduleinfo, $course) {
                 // Use updated grade_item.
                 $grade_item = $items[$itemid];
             }
-            $gradecategory = $grade_item->get_parent_category();
-            if (!empty($moduleinfo->add)) {
-                if (grade_category::aggregation_uses_aggregationcoef($gradecategory->aggregation)) {
-                    if ($gradecategory->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
-                        $grade_item->aggregationcoef = 1;
-                    } else {
-                        $grade_item->aggregationcoef = 0;
-                    }
-                    $grade_item->update();
-                }
-            }
         }
     }
 
@@ -301,17 +290,6 @@ function edit_module_post_actions($moduleinfo, $course) {
                 } else if (isset($moduleinfo->gradecat)) {
                     $outcome_item->set_parent($moduleinfo->gradecat);
                 }
-                $gradecategory = $outcome_item->get_parent_category();
-                if ($outcomeexists == false) {
-                    if (grade_category::aggregation_uses_aggregationcoef($gradecategory->aggregation)) {
-                        if ($gradecategory->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
-                            $outcome_item->aggregationcoef = 1;
-                        } else {
-                            $outcome_item->aggregationcoef = 0;
-                        }
-                        $outcome_item->update();
-                    }
-                }
             }
         }
     }
index 31d9391..ec26762 100644 (file)
@@ -1671,9 +1671,10 @@ class core_course_renderer extends plugin_renderer_base {
         $site = get_site();
         $output = '';
 
-        if (can_edit_in_category($category)) {
+        if (can_edit_in_category($coursecat->id)) {
             // Add 'Manage' button if user has permissions to edit this category.
-            $managebutton = $this->single_button(new moodle_url('/course/management.php'), get_string('managecourses'), 'get');
+            $managebutton = $this->single_button(new moodle_url('/course/management.php',
+                array('categoryid' => $coursecat->id)), get_string('managecourses'), 'get');
             $this->page->set_button($managebutton);
         }
         if (!$coursecat->id) {
index 529ae6a..cabeace 100644 (file)
@@ -17,7 +17,6 @@ Feature: Browse course list and return back from enrolment page
       | Sample course | C1        | 0        |
       | Course 1      | COURSE1   | CAT1     |
 
-  @javascript
   Scenario: A user can return to the category page from enrolment page
     When I log in as "user2"
     And I click on "Courses" "link" in the "Navigation" "block"
@@ -42,7 +41,6 @@ Feature: Browse course list and return back from enrolment page
     And I press "Continue"
     Then I should see "Edit profile" in the ".breadcrumb-nav" "css_element"
 
-  @javascript
   Scenario: User can return to the choice activity from enrolment page
     Given the following "roles" exist:
       | name                   | shortname | description      | archetype      |
index 2140e16..6fc0568 100644 (file)
@@ -29,7 +29,6 @@ Feature: Restrict activities availability
     Then I should see "Test glossary name"
     And I should see "Test chat name"
 
-  @javascript
   Scenario: Activities can not be added when the admin restricts the permissions
     Given I log in as "admin"
     And I set the following system permissions of "Teacher" role:
index 2115b15..8ea4d24 100644 (file)
@@ -237,14 +237,14 @@ class course_enrolment_manager {
             $extrafields = get_extra_user_fields($this->get_context());
             $extrafields[] = 'lastaccess';
             $ufields = user_picture::fields('u', $extrafields);
-            $sql = "SELECT DISTINCT $ufields, ul.timeaccess AS lastseen
+            $sql = "SELECT DISTINCT $ufields, COALESCE(ul.timeaccess, 0) AS lastcourseaccess
                       FROM {user} u
                       JOIN {user_enrolments} ue ON (ue.userid = u.id  AND ue.enrolid $instancessql)
                       JOIN {enrol} e ON (e.id = ue.enrolid)
                  LEFT JOIN {user_lastaccess} ul ON (ul.courseid = e.courseid AND ul.userid = u.id)
                  LEFT JOIN {groups_members} gm ON u.id = gm.userid
                      WHERE $filtersql
-                  ORDER BY u.$sort $direction";
+                  ORDER BY $sort $direction";
             $this->users[$key] = $DB->get_records_sql($sql, $params, $page*$perpage, $perpage);
         }
         return $this->users[$key];
@@ -334,20 +334,22 @@ class course_enrolment_manager {
             list($ctxcondition, $params) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'ctx');
             $params['courseid'] = $this->course->id;
             $params['cid'] = $this->course->id;
-            $sql = "SELECT ra.id as raid, ra.contextid, ra.component, ctx.contextlevel, ra.roleid, u.*, ue.lastseen
+            $extrafields = get_extra_user_fields($this->get_context());
+            $ufields = user_picture::fields('u', $extrafields);
+            $sql = "SELECT ra.id as raid, ra.contextid, ra.component, ctx.contextlevel, ra.roleid, $ufields,
+                        coalesce(u.lastaccess,0) AS lastaccess
                     FROM {role_assignments} ra
                     JOIN {user} u ON u.id = ra.userid
                     JOIN {context} ctx ON ra.contextid = ctx.id
                LEFT JOIN (
-                       SELECT ue.id, ue.userid, ul.timeaccess AS lastseen
+                       SELECT ue.id, ue.userid
                          FROM {user_enrolments} ue
-                    LEFT JOIN {enrol} e ON e.id=ue.enrolid
-                    LEFT JOIN {user_lastaccess} ul ON (ul.courseid = e.courseid AND ul.userid = ue.userid)
+                         JOIN {enrol} e ON e.id = ue.enrolid
                         WHERE e.courseid = :courseid
                        ) ue ON ue.userid=u.id
                    WHERE ctx.id $ctxcondition AND
                          ue.id IS NULL
-                ORDER BY u.$sort $direction, ctx.depth DESC";
+                ORDER BY $sort $direction, ctx.depth DESC";
             $this->otherusers[$key] = $DB->get_records_sql($sql, $params, $page*$perpage, $perpage);
         }
         return $this->otherusers[$key];
@@ -1091,7 +1093,7 @@ class course_enrolment_manager {
      * @param array $extrafields The list of fields as returned from get_extra_user_fields used to determine which
      * additional fields may be displayed
      * @param int $now The time used for lastaccess calculation
-     * @return array The fields to be displayed including userid, courseid, picture, firstname, lastseen and any
+     * @return array The fields to be displayed including userid, courseid, picture, firstname, lastcourseaccess, lastaccess and any
      * additional fields from $extrafields
      */
     private function prepare_user_for_display($user, $extrafields, $now) {
@@ -1100,7 +1102,7 @@ class course_enrolment_manager {
             'courseid'         => $this->get_course()->id,
             'picture'          => new user_picture($user),
             'firstname'        => fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())),
-            'lastseen'         => get_string('never'),
+            'lastaccess'       => get_string('never'),
             'lastcourseaccess' => get_string('never'),
         );
         foreach ($extrafields as $field) {
@@ -1108,13 +1110,13 @@ class course_enrolment_manager {
         }
 
         // Last time user has accessed the site.
-        if ($user->lastaccess) {
-            $details['lastseen'] = format_time($now - $user->lastaccess);
+        if (!empty($user->lastaccess)) {
+            $details['lastaccess'] = format_time($now - $user->lastaccess);
         }
 
         // Last time user has accessed the course.
-        if ($user->lastseen) {
-            $details['lastcourseaccess'] = format_time($now - $user->lastseen);
+        if (!empty($user->lastcourseaccess)) {
+            $details['lastcourseaccess'] = format_time($now - $user->lastcourseaccess);
         }
         return $details;
     }
index c9c8013..08c50c8 100644 (file)
@@ -80,6 +80,7 @@ class enrol_meta_handler {
      */
     protected static function sync_with_parent_course(stdClass $instance, $userid) {
         global $DB, $CFG;
+        require_once($CFG->dirroot . '/group/lib.php');
 
         $plugin = enrol_get_plugin('meta');
 
index 50c2bcb..188c9d5 100644 (file)
@@ -60,7 +60,7 @@ foreach ($extrafields as $field) {
 
 $fields = array(
     'userdetails' => $userdetails,
-    'lastseen' => get_string('lastaccess'),
+    'lastaccess' => get_string('lastaccess'),
     'role' => get_string('roles', 'role')
 );
 
@@ -68,7 +68,7 @@ $fields = array(
 if (!has_capability('moodle/course:viewhiddenuserfields', $context)) {
     $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
     if (isset($hiddenfields['lastaccess'])) {
-        unset($fields['lastseen']);
+        unset($fields['lastaccess']);
     }
 }
 
index 72b0cb2..c0c3ac8 100644 (file)
@@ -421,7 +421,7 @@ class course_enrolment_table extends html_table implements renderable {
      * @var array
      */
     protected static $sortablefields = array('firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename',
-            'alternatename', 'idnumber', 'email', 'phone1', 'phone2', 'institution', 'department' );
+            'alternatename', 'idnumber', 'email', 'phone1', 'phone2', 'institution', 'department', 'lastaccess', 'lastcourseaccess' );
 
     /**
      * Constructs the table
@@ -513,9 +513,10 @@ class course_enrolment_table extends html_table implements renderable {
                     if (!in_array($n, self::$sortablefields)) {
                         $bits[] = $l;
                     } else {
-                        $link = html_writer::link(new moodle_url($url, array(self::SORTVAR=>$n)), $fields[$name][$n]);
+                        $sorturl = new moodle_url($url, array(self::SORTVAR => $n, self::SORTDIRECTIONVAR => $this->get_field_sort_direction($n)));
+                        $link = html_writer::link($sorturl, $fields[$name][$n]);
                         if ($this->sort == $n) {
-                            $link .= html_writer::link(new moodle_url($url, array(self::SORTVAR=>$n, self::SORTDIRECTIONVAR=>$this->get_field_sort_direction($n))), $this->get_direction_icon($output, $n));
+                            $link .= $this->get_direction_icon($output, $n);
                         }
                         $bits[] = html_writer::tag('span', $link, array('class'=>'subheading_'.$n));
 
@@ -526,9 +527,10 @@ class course_enrolment_table extends html_table implements renderable {
                 if (!in_array($name, self::$sortablefields)) {
                     $newlabel = $label;
                 } else {
-                    $newlabel  = html_writer::link(new moodle_url($url, array(self::SORTVAR=>$name)), $fields[$name]);
+                    $sorturl = new moodle_url($url, array(self::SORTVAR => $name, self::SORTDIRECTIONVAR => $this->get_field_sort_direction($name)));
+                    $newlabel  = html_writer::link($sorturl, $fields[$name]);
                     if ($this->sort == $name) {
-                        $newlabel .= html_writer::link(new moodle_url($url, array(self::SORTVAR=>$name, self::SORTDIRECTIONVAR=>$this->get_field_sort_direction($name))), $this->get_direction_icon($output, $name));
+                        $newlabel .= $this->get_direction_icon($output, $name);
                     }
                 }
             }
@@ -704,13 +706,6 @@ class course_enrolment_table extends html_table implements renderable {
  */
 class course_enrolment_users_table extends course_enrolment_table {
 
-    /**
-     * An array of sortable fields
-     * @static
-     * @var array
-     */
-    protected static $sortablefields = array('firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename',
-            'alternatename', 'email', 'lastaccess');
 }
 
 /**
index acbf581..75d3901 100644 (file)
@@ -110,13 +110,19 @@ if ($mform->is_cancelled()) {
     redirect($returnurl);
 
 } else if ($data = $mform->get_data(false)) {
-    // If unset, give the aggregationcoef a default based on parent aggregation method
+
+    // This is a new item, and the category chosen is different than the default category.
+    if (empty($grade_item->id) && isset($data->parentcategory) && $parent_category->id != $data->parentcategory) {
+        $parent_category = grade_category::fetch(array('id' => $data->parentcategory));
+    }
+
+    // If unset, give the aggregation values a default based on parent aggregation method.
+    $defaults = grade_category::get_default_aggregation_coefficient_values($parent_category->aggregation);
     if (!isset($data->aggregationcoef) || $data->aggregationcoef == '') {
-        if ($parent_category->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
-            $data->aggregationcoef = 1;
-        } else {
-            $data->aggregationcoef = 0;
-        }
+        $data->aggregationcoef = $defaults['aggregationcoef'];
+    }
+    if (!isset($data->weightoverride)) {
+        $data->weightoverride = $defaults['weightoverride'];
     }
 
     if (!isset($data->gradepass) || $data->gradepass == '') {
@@ -145,6 +151,8 @@ if ($mform->is_cancelled()) {
     }
     if (isset($data->aggregationcoef2) && $parent_category->aggregation == GRADE_AGGREGATE_SUM) {
         $data->aggregationcoef2 = $data->aggregationcoef2 / 100.0;
+    } else {
+        $data->aggregationcoef2 = $defaults['aggregationcoef2'];
     }
 
     $grade_item = new grade_item(array('id'=>$id, 'courseid'=>$courseid));
@@ -162,7 +170,7 @@ if ($mform->is_cancelled()) {
 
         // set parent if needed
         if (isset($data->parentcategory)) {
-            $grade_item->set_parent($data->parentcategory, 'gradebook');
+            $grade_item->set_parent($data->parentcategory, false);
         }
 
     } else {
index 52bc618..02c76a8 100644 (file)
@@ -114,8 +114,18 @@ $mform->set_data($item);
 
 if ($data = $mform->get_data()) {
 
-    if (!isset($data->aggregationcoef)) {
-        $data->aggregationcoef = 0;
+    // This is a new item, and the category chosen is different than the default category.
+    if (empty($grade_item->id) && isset($data->parentcategory) && $parent_category->id != $data->parentcategory) {
+        $parent_category = grade_category::fetch(array('id' => $data->parentcategory));
+    }
+
+    // If unset, give the aggregation values a default based on parent aggregation method.
+    $defaults = grade_category::get_default_aggregation_coefficient_values($parent_category->aggregation);
+    if (!isset($data->aggregationcoef) || $data->aggregationcoef == '') {
+        $data->aggregationcoef = $defaults['aggregationcoef'];
+    }
+    if (!isset($data->weightoverride)) {
+        $data->weightoverride = $defaults['weightoverride'];
     }
 
     if (property_exists($data, 'calculation')) {
@@ -140,6 +150,8 @@ if ($data = $mform->get_data()) {
     }
     if (isset($data->aggregationcoef2) && $parent_category->aggregation == GRADE_AGGREGATE_SUM) {
         $data->aggregationcoef2 = $data->aggregationcoef2 / 100.0;
+    } else {
+        $data->aggregationcoef2 = $defaults['aggregationcoef2'];
     }
 
     $grade_item = new grade_item(array('id'=>$id, 'courseid'=>$courseid));
@@ -200,7 +212,7 @@ if ($data = $mform->get_data()) {
         } else {
             // set parent if needed
             if (isset($data->parentcategory)) {
-                $grade_item->set_parent($data->parentcategory, 'gradebook');
+                $grade_item->set_parent($data->parentcategory, false);
             }
         }
 
index a099f59..e9744b1 100644 (file)
@@ -577,6 +577,7 @@ class gradeimport_csv_load_data {
 
                     } else {
                         // The grade item for this is not updated.
+                        $newfeedback->importonlyfeedback = true;
                         $insertid = self::insert_grade_record($newfeedback, $this->studentid);
                         // Check to see if the insert was successful.
                         if (empty($insertid)) {
index 118906c..352365e 100644 (file)
@@ -210,6 +210,7 @@ Bobby,Bunce,,"Moodle HQ","Rock on!",student5@example.com,75.00,,75.00,{exportdat
         $testarray[$key]->feedback = $record->feedback;
         $testarray[$key]->importcode = $testobject->get_importcode();
         $testarray[$key]->importer = $USER->id;
+        $testarray[$key]->importonlyfeedback = 0;
 
         // Check that the record was inserted into the database.
         $this->assertEquals($gradeimportvalues, $testarray);
index f8d8e57..655c54b 100644 (file)
@@ -37,9 +37,10 @@ function get_new_importcode() {
  * (grade_import_value and grade_import_newitem)
  * If this function is called, we assume that all data collected
  * up to this point is fine and we can go ahead and commit
- * @param int courseid - id of the course
- * @param string importcode - import batch identifier
- * @param feedback print feedback and continue button
+ * @param int $courseid - ID of the course.
+ * @param int $importcode - Import batch identifier.
+ * @param bool $importfeedback - Whether to import feedback as well.
+ * @param bool $verbose - Print feedback and continue button.
  * @return bool success
  */
 function grade_import_commit($courseid, $importcode, $importfeedback=true, $verbose=true) {
@@ -114,6 +115,10 @@ function grade_import_commit($courseid, $importcode, $importfeedback=true, $verb
                     if (!$importfeedback) {
                         $grade->feedback = false; // ignore it
                     }
+                    if ($grade->importonlyfeedback) {
+                        // False means do not change. See grade_itme::update_final_grade().
+                        $grade->finalgrade = false;
+                    }
                     if (!$gradeitem->update_final_grade($grade->userid, $grade->finalgrade, 'import', $grade->feedback)) {
                         $errordata = new stdClass();
                         $errordata->itemname = $gradeitem->itemname;
index 84a8512..7150e30 100644 (file)
@@ -2887,6 +2887,12 @@ abstract class grade_helper {
                 continue;
             }
 
+            // Singleview doesn't doesn't accomodate for all cap combos yet, so this is hardcoded..
+            if ($plugin === 'singleview' && !has_all_capabilities(array('moodle/grade:viewall',
+                    'moodle/grade:edit'), $context)) {
+                continue;
+            }
+
             $pluginstr = get_string('pluginname', 'gradereport_'.$plugin);
             $url = new moodle_url('/grade/report/'.$plugin.'/index.php', array('id'=>$courseid));
             $gradereports[$plugin] = new grade_plugin_info($plugin, $url, $pluginstr);
index 244af2f..88742ea 100644 (file)
@@ -590,7 +590,8 @@ class grade_report_grader extends grade_report {
 
         $showuserimage = $this->get_pref('showuserimage');
         $canseeuserreport = has_capability('gradereport/'.$CFG->grade_profilereport.':view', $this->context);
-        $canseesingleview = has_capability('gradereport/singleview:view', $this->context);
+        $canseesingleview = has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall',
+            'moodle/grade:edit'), $this->context);
         $hasuserreportcell = $canseeuserreport || $canseesingleview;
 
         $strfeedback  = $this->get_lang_string("feedback");
@@ -836,7 +837,9 @@ class grade_report_grader extends grade_report {
                     }
 
                     $singleview = '';
-                    if (has_capability('gradereport/singleview:view', $this->context)) {
+                    if (has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall',
+                        'moodle/grade:edit'), $this->context)) {
+
                         $url = new moodle_url('/grade/report/singleview/index.php', array(
                             'id' => $this->course->id,
                             'item' => 'grade',
index 5ecb430..cd4fead 100644 (file)
@@ -52,7 +52,7 @@ class gradereport_singleview extends grade_report {
      * @return array List of warnings
      */
     public function process_data($data) {
-        if (has_capability('moodle/grade:manage', $this->context)) {
+        if (has_capability('moodle/grade:edit', $this->context)) {
             return $this->screen->process($data);
         }
     }
index d6928fa..0f35f82 100644 (file)
@@ -731,7 +731,12 @@ class grade_report_user extends grade_report {
                 if ($gradecat->aggregation == GRADE_AGGREGATE_SUM) {
                     // Natural aggregation/Sum of grades does not consider the mingrade, cannot traditionnally normalise it.
                     $graderange = $this->aggregationhints[$itemid]['grademax'];
-                    $gradeval = $this->aggregationhints[$itemid]['grade'] / $graderange;
+
+                    if ($graderange != 0) {
+                        $gradeval = $this->aggregationhints[$itemid]['grade'] / $graderange;
+                    } else {
+                        $gradeval = 0;
+                    }
                 } else {
                     $gradeval = grade_grade::standardise_score($this->aggregationhints[$itemid]['grade'],
                         $this->aggregationhints[$itemid]['grademin'], $this->aggregationhints[$itemid]['grademax'], 0, 1);
index e87d71b..d10d14a 100644 (file)
@@ -62,7 +62,6 @@ Feature: We can use calculated grade totals
     And I set the field "Grade display type" to "Real (percentage)"
     And I press "Save changes"
 
-  @javascript
   Scenario: Mean of grades aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Mean of grades |
@@ -81,7 +80,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "30.42 (30.42 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Weighted mean of grades aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Weighted mean of grades |
@@ -104,7 +102,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "26.94 (26.94 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Simple weighted mean of grades aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Simple weighted mean of grades |
@@ -125,7 +122,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "48.57 (48.57 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Mean of grades (with extra credits) aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Mean of grades (with extra credits) |
@@ -146,7 +142,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "47.22 (47.22 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Median of grades aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation | Median of grades |
@@ -165,7 +160,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "25.83 (25.83 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Lowest grade aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation | Lowest grade |
@@ -188,7 +182,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "0.00 (0.00 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Highest grade aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Highest grade |
@@ -209,7 +202,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "50.00 (50.00 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Mode of grades aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Mode of grades |
@@ -230,7 +222,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "50.00 (50.00 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Natural aggregation on outcome items with natural weights
     And the following config values are set as admin:
       | enableoutcomes | 1 |
@@ -292,7 +283,6 @@ Feature: We can use calculated grade totals
     And I follow "Course 1"
     And "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)"
 
-  @javascript
   Scenario: Natural aggregation on outcome items with modified weights
     And the following config values are set as admin:
       | enableoutcomes | 1 |
@@ -329,7 +319,6 @@ Feature: We can use calculated grade totals
     And I follow "Course 1"
     And "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)"
 
-  @javascript
   Scenario: Natural aggregation
     And I set the following settings for grade item "Sub category 1":
       | Aggregation          | Natural |
@@ -353,8 +342,8 @@ Feature: We can use calculated grade totals
     And I set the field "Show contribution to course total" to "Show"
     And I set the field "Show weightings" to "Show"
     And I press "Save changes"
-    And I set the field "Grade report" to "User report"
-    And I set the field "Select all or one user" to "Student 1"
+    And I select "User report" from the "Grade report" singleselect
+    And I select "Student 1" from the "Select all or one user" singleselect
     And the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Range | Contribution to course total |
       | Test assignment five | 28.57 % | 10.00 (50.00 %) | 0–20 | 1.03 % |
@@ -382,7 +371,6 @@ Feature: We can use calculated grade totals
       | Test assignment three | 30.93 %( Extra credit ) | 40.00 (26.67 %) | 0–150 | 8.25 % |
       | Test assignment four | 30.93 % | - | 0–150 | 0.00 % |
 
-  @javascript
   Scenario: Natural aggregation with drop lowest
     When I log out
     And I log in as "admin"
@@ -401,7 +389,6 @@ Feature: We can use calculated grade totals
       | Exclude empty grades | 0       |
     And I navigate to "Categories and items" node in "Grade administration > Setup"
     And I press "Add category"
-    And I click on "Show more" "link"
     And I set the following fields to these values:
       | Category name | Sub category 3 |
       | Aggregation | Natural |
@@ -465,7 +452,7 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Natural aggregation from the setup screen
-    And I set the field "Grade report" to "Categories and items"
+    And I select "Categories and items" from the "Grade report" singleselect
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Natural |
     And I set the following settings for grade item "Sub category 1":
@@ -527,7 +514,7 @@ Feature: We can use calculated grade totals
       | Aggregation          | Natural |
       | Exclude empty grades | 0       |
     And I turn editing mode off
-    And I set the field "Grade report" to "Categories and items"
+    And I select "Categories and items" from the "Grade report" singleselect
     And I set the field "Override weight of Test assignment one" to "1"
     And I set the field "Weight of Test assignment one" to "0"
     And I set the field "Override weight of Test assignment six" to "1"
@@ -542,8 +529,8 @@ Feature: We can use calculated grade totals
     And I set the field "Show weightings" to "Show"
     And I press "Save changes"
     Then I should see "75.00 (16.85 %)" in the ".course" "css_element"
-    And I set the field "Grade report" to "User report"
-    And I set the field "Select all or one user" to "Student 1"
+    And I select "User report" from the "Grade report" singleselect
+    And I select "Student 1" from the "Select all or one user" singleselect
     And the following should exist in the "user-grade" table:
       | Grade item            | Calculated weight | Grade           | Contribution to course total |
       | Test assignment five  | 57.14 %           | 10.00 (50.00 %) | 2.25 %                        |
diff --git a/grade/tests/behat/grade_aggregation_changes.feature b/grade/tests/behat/grade_aggregation_changes.feature
new file mode 100644 (file)
index 0000000..7453a73
--- /dev/null
@@ -0,0 +1,472 @@
+@core @core_grades
+Feature: Changing the aggregation of an item affects its weight and extra credit definition
+  In order to switch to another aggregation method
+  As an teacher
+  I need to be able to edit the grade category settings
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "grade categories" exist:
+      | fullname      | course | aggregation |
+      | Cat mean      | C1     | 0           |
+      | Cat median    | C1     | 2           |
+      | Cat min       | C1     | 4           |
+      | Cat max       | C1     | 6           |
+      | Cat mode      | C1     | 8           |
+      | Cat weighted  | C1     | 10          |
+      | Cat weighted2 | C1     | 10          |
+      | Cat simple    | C1     | 11          |
+      | Cat ec        | C1     | 12          |
+      | Cat natural   | C1     | 13          |
+    And the following "grade items" exist:
+      | itemname  | course | category    | aggregationcoef | aggregationcoef2 | weightoverride |
+      | Item a1   | C1     | ?           | 0               | 0                | 0              |
+      | Item a2   | C1     | ?           | 0               | 0.40             | 1              |
+      | Item a3   | C1     | ?           | 1               | 0.10             | 1              |
+      | Item a4   | C1     | ?           | 1               | 0                | 0              |
+      | Item b1   | C1     | Cat natural | 0               | 0                | 0              |
+      | Item b2   | C1     | Cat natural | 0               | 0.40             | 1              |
+      | Item b3   | C1     | Cat natural | 1               | 0.10             | 1              |
+      | Item b4   | C1     | Cat natural | 1               | 0                | 0              |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
+    And I am on site homepage
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I navigate to "Grader report" node in "Grade administration"
+    And I turn editing mode on
+    And I follow "Edit   Cat mean"
+    And I set the following fields to these values:
+      | Weight adjusted     | 1  |
+      | Weight              | 20 |
+      | Extra credit        | 0  |
+    And I press "Save changes"
+    And I follow "Edit   Cat median"
+    And I set the following fields to these values:
+      | Weight adjusted     | 1  |
+      | Weight              | 5  |
+      | Extra credit        | 0  |
+    And I press "Save changes"
+    And I follow "Edit   Cat min"
+    And I set the following fields to these values:
+      | Weight adjusted     | 0  |
+      | Weight              | 0  |
+      | Extra credit        | 1  |
+    And I press "Save changes"
+    And I follow "Edit   Item a1"
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item a2"
+    And the field "Weight adjusted" matches value "1"
+    And the field "id_aggregationcoef2" matches value "40.0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item a3"
+    And the field "Weight adjusted" matches value "1"
+    And the field "id_aggregationcoef2" matches value "10.0"
+    And the field "Extra credit" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Item a4"
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Item b1"
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item b2"
+    And the field "Weight adjusted" matches value "1"
+    And the field "id_aggregationcoef2" matches value "40.0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item b3"
+    And the field "Weight adjusted" matches value "1"
+    And the field "id_aggregationcoef2" matches value "10.0"
+    And the field "Extra credit" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Item b4"
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "1"
+    And I press "Cancel"
+
+  Scenario: Switching a category from Natural aggregation to Mean of grades and back
+    Given I follow "Edit   Course 1"
+    And I set the field "Aggregation" to "Mean of grades"
+    When I press "Save changes"
+    And I follow "Edit   Item a1"
+    Then I should not see "Weight adjusted"
+    And I should not see "Weight"
+    And I should not see "Extra credit"
+    And I press "Cancel"
+    And I follow "Edit   Item a2"
+    And I should not see "Weight adjusted"
+    And I should not see "Weight"
+    And I should not see "Extra credit"
+    And I press "Cancel"
+    And I follow "Edit   Item a3"
+    And I should not see "Weight adjusted"
+    And I should not see "Weight"
+    And I should not see "Extra credit"
+    And I press "Cancel"
+    And I follow "Edit   Item a4"
+    And I should not see "Weight adjusted"
+    And I should not see "Weight"
+    And I should not see "Extra credit"
+    And I press "Cancel"
+    And I follow "Edit   Cat mean"
+    And I expand all fieldsets
+    And I should not see "Weight adjusted"
+    And I should not see "Weight" in the "#id_headerparent" "css_element"
+    And I should not see "Extra credit"
+    And I press "Cancel"
+    And I follow "Edit   Cat median"
+    And I expand all fieldsets
+    And I should not see "Weight adjusted"
+    And I should not see "Weight" in the "#id_headerparent" "css_element"
+    And I should not see "Extra credit"
+    And I press "Cancel"
+    And I follow "Edit   Cat min"
+    And I expand all fieldsets
+    And I should not see "Weight adjusted"
+    And I should not see "Weight" in the "#id_headerparent" "css_element"
+    And I should not see "Extra credit"
+    And I press "Cancel"
+    And I follow "Edit   Cat natural"
+    And I set the field "Aggregation" to "Mean of grades"
+    And I press "Save changes"
+    And I follow "Edit   Item b1"
+    And I should not see "Weight adjusted"
+    And I should not see "Weight"
+    And I should not see "Extra credit"
+    And I press "Cancel"
+    And I follow "Edit   Item b2"
+    And I should not see "Weight adjusted"
+    And I should not see "Weight"
+    And I should not see "Extra credit"
+    And I press "Cancel"
+    And I follow "Edit   Item b3"
+    And I should not see "Weight adjusted"
+    And I should not see "Weight"
+    And I should not see "Extra credit"
+    And I press "Cancel"
+    And I follow "Edit   Item b4"
+    And I should not see "Weight adjusted"
+    And I should not see "Weight"
+    And I should not see "Extra credit"
+    And I press "Cancel"
+    # Switching back.
+    And I follow "Edit   Course 1"
+    And I set the field "Aggregation" to "Natural"
+    And I press "Save changes"
+    And I follow "Edit   Item a1"
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item a2"
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item a3"
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item a4"
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Cat mean"
+    And I expand all fieldsets
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Cat median"
+    And I expand all fieldsets
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Cat min"
+    And I expand all fieldsets
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Cat natural"
+    And I set the field "Aggregation" to "Natural"
+    And I press "Save changes"
+    And I follow "Edit   Item b1"
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit"