Merge branch 'mdl-50957' of git://github.com/cdsmith-umn/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 25 Aug 2015 02:06:49 +0000 (10:06 +0800)
committerDavid Monllao <davidm@moodle.com>
Tue, 25 Aug 2015 02:06:49 +0000 (10:06 +0800)
318 files changed:
admin/cli/install.php
admin/environment.xml
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/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
course/format/singleactivity/lib.php
course/modlib.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/report/singleview/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/install.php
lang/en/notes.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_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/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/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/classes/plugininfo/quizaccess.php
mod/quiz/styles.css
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/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
tag/lib.php
tag/manage.php
tag/tests/taglib_test.php
theme/base/style/blocks.css
theme/base/style/filemanager.css
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/course.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/upgrade.txt
user/edit_form.php
user/editadvanced_form.php
user/lib.php
user/profile/index.php
user/tests/behat/view_full_profile.feature
user/tests/userlib_test.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 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 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..4cdcdf7 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';
@@ -39,6 +42,7 @@ $string['numberofvideos'] = 'Number of videos';
 $string['peopleblogs'] = 'People &amp; Blogs';
 $string['petsanimals'] = 'Pets &amp; Animals';
 $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';
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 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 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 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 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" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item b2"
+    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 b3"
+    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 b4"
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+
+  Scenario: Switching a category from Natural aggregation to Weighted mean of grades and back
+    Given I follow "Edit   Course 1"
+    And I set the field "Aggregation" to "Weighted 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 "Extra credit"
+    And the field "Item weight" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Item a2"
+    And I should not see "Weight adjusted"
+    And I should not see "Extra credit"
+    And the field "Item weight" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Item a3"
+    And I should not see "Weight adjusted"
+    And I should not see "Extra credit"
+    And the field "Item weight" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Item a4"
+    And I should not see "Weight adjusted"
+    And I should not see "Extra credit"
+    And the field "Item weight" matches value "1"
+    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 "Extra credit"
+    And the field "Item weight" matches value "1"
+    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 "Extra credit"
+    And the field "Item weight" matches value "1"
+    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 "Extra credit"
+    And the field "Item weight" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Cat natural"
+    And I set the field "Aggregation" to "Weighted 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 "Extra credit"
+    And the field "Item weight" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Item b2"
+    And I should not see "Weight adjusted"
+    And I should not see "Extra credit"
+    And the field "Item weight" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Item b3"
+    And I should not see "Weight adjusted"
+    And I should not see "Extra credit"
+    And the field "Item weight" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Item b4"
+    And I should not see "Weight adjusted"
+    And I should not see "Extra credit"
+    And the field "Item weight" matches value "1"
+    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" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item b2"
+    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 b3"
+    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 b4"
+    And the field "Weight adjusted" matches value "0"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+
+  @javascript
+  Scenario: Switching grade items between categories
+    # Move to same aggregation (Natural).
+    Given I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the field "Select Item a1" to "1"
+    And I set the field "Select Item a2" to "1"
+    And I set the field "Select Item a3" to "1"
+    And I set the field "Select Item a4" to "1"
+    When I select "Cat natural" from the "Move selected items to" singleselect
+    And I navigate to "Grader report" node in "Grade administration"
+    And I follow "Edit   Item a1"
+    Then 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"
+    # Move to Mean of grades (with extra credit).
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the field "Select Item a1" to "1"
+    And I set the field "Select Item a2" to "1"
+    And I set the field "Select Item a3" to "1"
+    And I set the field "Select Item a4" to "1"
+    And I select "Cat ec" from the "Move selected items to" singleselect
+    And I navigate to "Grader report" node in "Grade administration"
+    And I follow "Edit   Item a1"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item a2"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item a3"
+    And the field "Extra credit" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Item a4"
+    And the field "Extra credit" matches value "1"
+    And I press "Cancel"
+    # Move to Simple weight mean of grades.
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the field "Select Item a1" to "1"
+    And I set the field "Select Item a2" to "1"
+    And I set the field "Select Item a3" to "1"
+    And I set the field "Select Item a4" to "1"
+    And I select "Cat simple" from the "Move selected items to" singleselect
+    And I navigate to "Grader report" node in "Grade administration"
+    And I follow "Edit   Item a1"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item a2"
+    And the field "Extra credit" matches value "0"
+    And I press "Cancel"
+    And I follow "Edit   Item a3"
+    And the field "Extra credit" matches value "1"
+    And I press "Cancel"
+    And I follow "Edit   Item a4"
+    And the field "Extra credit" matches value "1"
+    And I press "Cancel"
+    # Move to Weighted mean of grades.
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the field "Select Item a1" to "1"
+    And I set the field "Select Item a2" to "1"
+    And I set the field "Select Item a3" to "1"
+    And I set the field "Select Item a4" to "1"
+    And I select "Cat weighted" from the "Move selected items to" singleselect
+    And I navigate to "Grader report" node in "Grade administration"
+    And I follow "Edit   Item a1"
+    And the field "Item weight" matches value "1"
+    And I set the field "Item weight" to "2"
+    And I press "Save changes"
+    And I follow "Edit   Item a2"
+    And the field "Item weight" matches value "1"
+    And I set the field "Item weight" to "5"
+    And I press "Save changes"
+    And I follow "Edit   Item a3"
+    And the field "Item weight" matches value "1"
+    And I set the field "Item weight" to "8"
+    And I press "Save changes"
+    And I follow "Edit   Item a4"
+    And the field "Item weight" matches value "1"
+    And I set the field "Item weight" to "11"
+    And I press "Save changes"
+    # Move to same (Weighted mean of grades).
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the field "Select Item a1" to "1"
+    And I set the field "Select Item a2" to "1"
+    And I set the field "Select Item a3" to "1"
+    And I set the field "Select Item a4" to "1"
+    And I select "Cat weighted2" from the "Move selected items to" singleselect
+    And I wait "2" seconds
+    And I navigate to "Grader report" node in "Grade administration"
+    And I follow "Edit   Item a1"
+    And the field "Item weight" matches value "2"
+    And I press "Save changes"
+    And I follow "Edit   Item a2"
+    And the field "Item weight" matches value "5"
+    And I press "Save changes"
+    And I follow "Edit   Item a3"
+    And the field "Item weight" matches value "8"
+    And I press "Save changes"
+    And I follow "Edit   Item a4"
+    And the field "Item weight" matches value "11"
+    And I press "Save changes"
+    # Move back to Natural.
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the field "Select Item a1" to "1"
+    And I set the field "Select Item a2" to "1"
+    And I set the field "Select Item a3" to "1"
+    And I set the field "Select Item a4" to "1"
+    And I select "Course 1" from the "Move selected items to" singleselect
+    And I navigate to "Grader report" node in "Grade administration"
+    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"
index d45e489..5cd48b0 100644 (file)
@@ -70,7 +70,6 @@ Feature: View gradebook when scales are used
     And I follow "Grader report"
     And I turn editing mode on
 
-  @javascript
   Scenario: Test displaying scales in gradebook in aggregation method Natural
     When I turn editing mode off
     Then the following should exist in the "user-grades" table:
@@ -85,14 +84,13 @@ Feature: View gradebook when scales are used
       | Range              | F–A      | 0.00–5.00      | 0.00–5.00    |
       | Overall average    | C        | 3.00           | 3.00         |
     And I follow "User report"
-    And I set the field "Select all or one user" to "Student 3"
-    And I click on "Select all or one user" "select"
+    And I select "Student 3" from the "Select all or one user" singleselect
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range | Percentage | Contribution to course total |
       | Test assignment one | C     | F–A   | 50.00 %    | 60.00 %                      |
       | Sub category 1 total      | 3.00  | 0–5   | 60.00 %    | -                            |
       | Course total        | 3.00  | 0–5   | 60.00 %    | -                            |
-    And I set the field "jump" to "Categories and items"
+    And I select "Categories and items" from the "Grade report" singleselect
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | 5.00      |
@@ -108,7 +106,6 @@ Feature: View gradebook when scales are used
       | Sub category 1 total      | 4.00  | 0–5   | 80.00 %    | -                            |
       | Course total        | 4.00  | 0–5   | 80.00 %    | -                            |
 
-  @javascript
   Scenario Outline: Test displaying scales in gradebook in all other aggregation methods
     When I follow "Edit   Course 1"
     And I set the field "Aggregation" to "<aggregation>"
@@ -133,14 +130,13 @@ Feature: View gradebook when scales are used
       | Range              | F–A      | 1.00–5.00      | 0.00–100.00    |
       | Overall average    | C        | 3.00           | <overallavg>   |
     And I follow "User report"
-    And I set the field "Select all or one user" to "Student 3"
-    And I click on "Select all or one user" "select"
+    And I select "Student 3" from the "Select all or one user" singleselect
     And the following should exist in the "user-grade" table:
       | Grade item                   | Grade          | Range | Percentage    | Contribution to course total |
       | Test assignment one          | C              | F–A   | 50.00 %       | <contrib3>                   |
       | Sub category (<aggregation>) total<aggregation>. | 3.00           | 1–5   | 50.00 %       | -                            |
       | Course total<aggregation>.   | <coursetotal3> | 0–100 | <courseperc3> | -                            |
-    And I set the field "jump" to "Categories and items"
+    And I select "Categories and items" from the "Grade report" singleselect
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | A (5)     |
@@ -159,7 +155,7 @@ Feature: View gradebook when scales are used
     Examples:
       | aggregation                         | coursetotal1 | coursetotal2 | coursetotal3 | coursetotal4 | coursetotal5 |overallavg | courseperc2 | courseperc3 | contrib2 | contrib3 |
       | Mean of grades                      | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
-      | Weighted mean of grades             | -            | -            | -            | -            | -            | -         | -           | -           | 0.00 %   | 0.00 %   |
+      | Weighted mean of grades             | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
       | Simple weighted mean of grades      | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
       | Mean of grades (with extra credits) | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
       | Median of grades                    | 100.00       | 75.00        | 50.00        | 25.00        | 0.00         | 50.00     | 75.00 %     | 50.00 %     | 75.00 %  | 50.00 %  |
index e902e63..867c70c 100644 (file)
@@ -125,7 +125,7 @@ Feature: View gradebook when single item scales are used
     Examples:
       | aggregation                         | contrib1 | cattotal1 | coursetotal1 | catavg | overallavg |
       | Mean of grades                      | 100.00 % | 100.00    | 100.00       | 100.00 | 100.00     |
-      | Weighted mean of grades             | 0.00 %   | 100.00    | -            | 100.00 | -          |
+      | Weighted mean of grades             | 0.00 %   | 100.00    | 100.00       | 100.00 | 100.00     |
       | Simple weighted mean of grades      | 0.00 %   | -         | -            | -      | -          |
       | Mean of grades (with extra credits) | 100.00 % | 100.00    | 100.00       | 100.00 | 100.00     |
       | Median of grades                    | 100.00 % | 100.00    | 100.00       | 100.00 | 100.00     |
index 5662e39..9aa8684 100644 (file)
@@ -57,9 +57,8 @@ Feature: We can enter in grades and view reports from the gradebook
     And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment name 2"
     And I press "Save changes"
 
-  @javascript
   Scenario: Grade a grade item and ensure the results display correctly in the gradebook
-    When I set the field "Grade report" to "User report"
+    When I select "User report" from the "Grade report" singleselect
     And the "Grade report" select box should contain "Grader report"
     And the "Grade report" select box should contain "Outcomes report"
     And the "Grade report" select box should contain "User report"
@@ -80,14 +79,13 @@ Feature: We can enter in grades and view reports from the gradebook
     And "Course 1" row "Grade" column of "overview-grade" table should contain "170.00"
     And "Course 1" row "Grade" column of "overview-grade" table should not contain "90.00"
 
-  @javascript
   Scenario: We can add a weighting to a grade item and it is displayed properly in the user report
-    When I set the field "Grade report" to "Categories and items"
+    When I select "Categories and items" from the "Grade report" singleselect
     And I set the following settings for grade item "Course 1":
       | Aggregation | Weighted mean of grades |
     And I set the field "Extra credit value for Test assignment name" to "0.72"
     And I press "Save changes"
-    And I set the field "Grade report" to "User report"
+    And I select "User report" from the "Grade report" singleselect
     And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the following fields to these values:
       | Show weightings | Show |
diff --git a/grade/tests/importlib_test.php b/grade/tests/importlib_test.php
new file mode 100644 (file)
index 0000000..0acea9e
--- /dev/null
@@ -0,0 +1,208 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for grade/import/lib.php.
+ *
+ * @package   core_grade
+ * @category  phpunit
+ * @copyright 2015 Adrian Greeve <adrian@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/grade/import/lib.php');
+
+/**
+ * Tests grade_import_lib functions.
+ */
+class core_grade_import_lib_test extends advanced_testcase {
+
+    /**
+     * Import grades into 'grade_import_values' table. This is done differently in the various import plugins,
+     * so there is no direct API to call.
+     *
+     * @param array $data Information to be inserted into the table.
+     * @return int The insert ID of the sql statement.
+     */
+    private function import_grades($data) {
+        global $DB, $USER;
+        $graderecord = new stdClass();
+        $graderecord->importcode = $data['importcode'];
+        if (isset($data['itemid'])) {
+            $graderecord->itemid = $data['itemid'];
+        }
+        $graderecord->userid = $data['userid'];
+        if (isset($data['importer'])) {
+            $graderecord->importer = $data['importer'];
+        } else {
+            $graderecord->importer = $USER->id;
+        }
+        if (isset($data['finalgrade'])) {
+            $graderecord->finalgrade = $data['finalgrade'];
+        } else {
+            $graderecord->finalgrade = rand(0, 100);
+        }
+        if (isset($data['feedback'])) {
+            $graderecord->feedback = $data['feedback'];
+        }
+        if (isset($data['importonlyfeedback'])) {
+            $graderecord->importonlyfeedback = $data['importonlyfeedback'];
+        } else {
+            $graderecord->importonlyfeedback = false;
+        }
+        if (isset($data['newgradeitem'])) {
+            $graderecord->newgradeitem = $data['newgradeitem'];
+        }
+        return $DB->insert_record('grade_import_values', $graderecord);
+    }
+
+    /**
+     * Tests for importing grades from an external source.
+     */
+    public function test_grade_import_commit() {
+        global $USER, $DB, $CFG;
+        $this->resetAfterTest();
+
+        $importcode = get_new_importcode();
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $course = $this->getDataGenerator()->create_course();
+        $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id));
+        $itemname = $assign->name;
+        $modulecontext = context_module::instance($assign->cmid);
+        // The generator returns a dummy object, lets get the real assign object.
+        $assign = new assign($modulecontext, false, false);
+        $cm = $assign->get_course_module();
+
+        // Enrol users in the course.
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+
+        // Enter a new grade into an existing grade item.
+        $gradeitem = grade_item::fetch(array('courseid' => $course->id, 'itemtype' => 'mod'));
+
+        // Keep this value around for a test further down.
+        $originalgrade = 55;
+        $this->import_grades(array(
+            'importcode' => $importcode,
+            'itemid' => $gradeitem->id,
+            'userid' => $user1->id,
+            'finalgrade' => $originalgrade
+        ));
+
+        $status = grade_import_commit($course->id, $importcode, false, false);
+        $this->assertTrue($status);
+
+        // Get imported grade_grade.
+