Merge branch 'MDL-58490-master' of git://github.com/lameze/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Fri, 21 Apr 2017 00:52:15 +0000 (08:52 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Fri, 21 Apr 2017 00:52:15 +0000 (08:52 +0800)
219 files changed:
.travis.yml
admin/environment.xml
admin/tool/lp/tests/behat/behat_tool_lp_data_generators.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/usertours/tests/behat/create_tour.feature
auth/mnet/auth.php
auth/oauth2/classes/auth.php
auth/upgrade.txt
backup/moodle2/backup_course_task.class.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_course_task.class.php
backup/moodle2/restore_stepslib.php
blocks/myoverview/amd/build/event_list.min.js
blocks/myoverview/amd/build/event_list_by_course.min.js
blocks/myoverview/amd/src/event_list.js
blocks/myoverview/amd/src/event_list_by_course.js
blocks/myoverview/classes/output/main.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/course-event-list.mustache
blocks/myoverview/templates/course-item.mustache
blocks/myoverview/templates/course-paging-content-item.mustache
blocks/myoverview/templates/course-paging-content.mustache
blocks/myoverview/templates/course-summary.mustache
blocks/myoverview/templates/courses-view-course-item.mustache
blocks/myoverview/templates/event-list.mustache
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/myoverview/tests/behat/block_myoverview_progress.feature
blocks/tests/behat/configure_block_throughout_site.feature
blocks/upgrade.txt
calendar/classes/local/api.php
calendar/export_execute.php
calendar/externallib.php
calendar/lib.php
calendar/renderer.php
calendar/tests/lib_test.php
calendar/tests/local_api_test.php
competency/classes/persistent.php
completion/classes/bulkedit_form.php [new file with mode: 0644]
completion/classes/defaultedit_form.php [new file with mode: 0644]
completion/classes/edit_base_form.php [new file with mode: 0644]
completion/classes/manager.php [new file with mode: 0644]
completion/tests/behat/bulk_edit_activity_completion.feature [new file with mode: 0644]
completion/tests/behat/default_activity_completion.feature [new file with mode: 0644]
completion/tests/bulk_update_test.php [new file with mode: 0644]
course/bulkcompletion.php [new file with mode: 0644]
course/classes/external/course_summary_exporter.php
course/classes/output/bulk_activity_completion_renderer.php [new file with mode: 0644]
course/completion.php
course/defaultcompletion.php [new file with mode: 0644]
course/dnduploadlib.php
course/editbulkcompletion.php [new file with mode: 0644]
course/editdefaultcompletion.php [new file with mode: 0644]
course/format/topics/format.js
course/format/weeks/format.js
course/lib.php
course/modedit.php
course/modlib.php
course/moodleform_mod.php
course/templates/activityinstance.mustache [new file with mode: 0644]
course/templates/bulkactivitycompletion.mustache [new file with mode: 0644]
course/templates/defaultactivitycompletion.mustache [new file with mode: 0644]
course/templates/editbulkactivitycompletion.mustache [new file with mode: 0644]
course/templates/editdefaultcompletion.mustache [new file with mode: 0644]
course/tests/externallib_test.php
course/tests/modlib_test.php
enrol/lti/tests/helper_test.php
enrol/tests/enrollib_test.php
files/converter/googledrive/classes/converter.php [new file with mode: 0644]
files/converter/googledrive/classes/rest.php [new file with mode: 0644]
files/converter/googledrive/lang/en/fileconverter_googledrive.php [new file with mode: 0644]
files/converter/googledrive/lib.php [new file with mode: 0644]
files/converter/googledrive/settings.php [new file with mode: 0644]
files/converter/googledrive/test.php [new file with mode: 0644]
files/converter/googledrive/tests/fixtures/source.docx [new file with mode: 0644]
files/converter/googledrive/version.php [new file with mode: 0644]
files/tests/externallib_test.php
filter/emoticon/tests/filter_test.php
lang/en/admin.php
lang/en/auth.php
lang/en/completion.php
lib/authlib.php
lib/classes/event/completion_defaults_updated.php [new file with mode: 0644]
lib/classes/oauth2/issuer.php
lib/classes/oauth2/rest.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/plugin_manager.php
lib/db/install.xml
lib/db/upgrade.php
lib/enrollib.php
lib/filelib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputlib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/pagelib.php
lib/phpunit/bootstrap.php
lib/tests/configonlylib_test.php
lib/tests/filter_manager_test.php
lib/tests/moodlelib_test.php
lib/tests/outputcomponents_test.php
lib/tests/setuplib_test.php
lib/tests/weblib_format_text_test.php
lib/upgrade.txt
lib/upgradelib.php
media/player/videojs/amd/build/Youtube-lazy.min.js
media/player/videojs/amd/build/video-lazy.min.js
media/player/videojs/amd/src/Youtube-lazy.js
media/player/videojs/amd/src/video-lazy.js
media/player/videojs/fonts/VideoJS.svg
media/player/videojs/readme_moodle.txt
media/player/videojs/styles.css
media/player/videojs/thirdpartylibs.xml
media/player/videojs/videojs/lang/ru.js
media/player/videojs/videojs/lang/uk.js
media/player/videojs/videojs/lang/zh-TW.js
media/player/videojs/videojs/video-js.swf
message/amd/build/message_repository.min.js
message/amd/src/message_repository.js
message/output/popup/amd/build/notification_repository.min.js
message/output/popup/amd/src/notification_repository.js
mod/assign/feedback/editpdf/styles.css
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js
mod/assign/feedback/editpdf/yui/src/editor/js/commentsearch.js
mod/assign/gradeform.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/choice/lib.php
mod/choice/mod_form.php
mod/choice/tests/lib_test.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/mod_form.php
mod/data/tests/lib_test.php
mod/feedback/classes/completion.php
mod/feedback/classes/external.php
mod/feedback/classes/structure.php
mod/feedback/complete.php
mod/feedback/lib.php
mod/feedback/mod_form.php
mod/feedback/tests/external_test.php
mod/feedback/tests/lib_test.php
mod/feedback/view.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/mod_form.php
mod/forum/tests/lib_test.php
mod/forum/tests/mail_test.php
mod/glossary/backup/moodle2/backup_glossary_stepslib.php
mod/glossary/backup/moodle2/restore_glossary_stepslib.php
mod/glossary/db/tag.php [new file with mode: 0644]
mod/glossary/deleteentry.php
mod/glossary/edit.php
mod/glossary/edit_form.php
mod/glossary/formats/continuous/continuous_format.php
mod/glossary/formats/dictionary/dictionary_format.php
mod/glossary/formats/encyclopedia/encyclopedia_format.php
mod/glossary/formats/faq/faq_format.php
mod/glossary/formats/fullwithauthor/fullwithauthor_format.php
mod/glossary/formats/fullwithoutauthor/fullwithoutauthor_format.php
mod/glossary/lang/en/glossary.php
mod/glossary/lib.php
mod/glossary/locallib.php
mod/glossary/mod_form.php
mod/glossary/showentry_ajax.php
mod/glossary/tests/behat/edit_tags.feature [new file with mode: 0644]
mod/glossary/tests/generator/lib.php
mod/glossary/tests/generator_test.php
mod/glossary/tests/lib_test.php
mod/glossary/version.php
mod/lesson/classes/external.php
mod/lesson/classes/external/lesson_summary_exporter.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/mod_form.php
mod/lesson/pagetypes/cluster.php
mod/lesson/pagetypes/endofbranch.php
mod/lesson/pagetypes/endofcluster.php
mod/lesson/tests/external_test.php
mod/lesson/tests/lib_test.php
mod/lesson/upgrade.txt
mod/lesson/view.php
mod/lti/servicelib.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/tests/lib_test.php
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/scorm/mod_form.php
mod/scorm/tests/lib_test.php
mod/survey/lib.php
mod/survey/mod_form.php
mod/survey/tests/lib_test.php
mod/upgrade.txt
mod/wiki/mod_form.php
pix/i/dashboard.png [new file with mode: 0644]
pix/i/dashboard.svg [new file with mode: 0644]
report/stats/locallib.php
repository/onedrive/lib.php
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/icons.scss
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-item.mustache
theme/bootstrapbase/templates/block_myoverview/course-paging-content-item.mustache
theme/bootstrapbase/templates/block_myoverview/course-summary.mustache
theme/bootstrapbase/templates/block_myoverview/courses-view-course-item.mustache
user/lib.php
version.php

index 799bd5d..ff2b674 100644 (file)
@@ -16,6 +16,9 @@ php:
     - 7.1
     - 5.6
 
+addons:
+  postgresql: "9.3"
+
 services:
     - redis-server
 
@@ -114,7 +117,7 @@ before_script:
 
         # The wwwroot and dataroot.
         sed -i \
-          -e "s%http://example.com/moodle%http://localhost%" \
+          -e "s%http://example.com/moodle%https://localhost%" \
           -e "s%/home/example/moodledata%/home/travis/roots/base%" \
           config.php ;
 
index 38207ed..f875211 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="3.3" requires="2.7">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="5.5.31" />
+      <VENDOR name="mysql" version="5.5.31" />
+      <VENDOR name="postgres" version="9.3" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="10.2" />
+    </DATABASE>
+    <PHP version="5.6.5" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="opensslrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlreader" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="intlrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbstorageengine" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <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_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unoconvwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfileformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfilepertable" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddblargeprefix" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="ishttpswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="incompleteunicodesupport" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index 8eea3dc..166b4c5 100644 (file)
@@ -175,11 +175,11 @@ class behat_tool_lp_data_generators extends behat_base {
         if (isset($data['framework'])) {
             $framework = competency_framework::get_record(array('idnumber' => $data['framework']));
             if ($framework) {
-                $data['competencyframeworkid'] = $framework->get_id();
+                $data['competencyframeworkid'] = $framework->get('id');
             } else {
                 $framework = competency_framework::get_record(array('id' => $data['framework']));
                 if ($framework) {
-                    $data['competencyframeworkid'] = $framework->get_id();
+                    $data['competencyframeworkid'] = $framework->get('id');
                 } else {
                     throw new Exception('Could not resolve framework with idnumber or id : "' . $data['category'] . '"');
                 }
index be10099..04c0e24 100644 (file)
@@ -183,9 +183,6 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $this->assertTrue(isset($token->privatetoken));
 
         // Enable requeriments.
-        $CFG->httpswwwroot = str_replace('http:', 'https:', $CFG->httpswwwroot);    // Mock https.
-        $CFG->enablewebservices = 1;
-        $CFG->enablemobilewebservice = 1;
         $_GET['wstoken'] = $token->token;   // Mock parameters.
 
         // Even if we force the password change for the current user we should be able to retrieve the key.
@@ -212,8 +209,13 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
      * Test get_autologin_key missing ws.
      */
     public function test_get_autologin_key_missing_ws() {
+        global $CFG;
         $this->resetAfterTest(true);
 
+        // Need to disable webservices to verify that's checked.
+        $CFG->enablewebservices = 0;
+        $CFG->enablemobilewebservice = 0;
+
         $this->setAdminUser();
         $this->expectException('moodle_exception');
         $this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
@@ -226,10 +228,12 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
     public function test_get_autologin_key_missing_https() {
         global $CFG;
 
+        // Need to simulate a non HTTPS site here.
+        $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot);
+        $CFG->httpswwwroot = str_replace('https:', 'http:', $CFG->wwwroot);
+
         $this->resetAfterTest(true);
         $this->setAdminUser();
-        $CFG->enablewebservices = 1;
-        $CFG->enablemobilewebservice = 1;
 
         $this->expectException('moodle_exception');
         $this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
@@ -244,9 +248,6 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
 
         $this->resetAfterTest(true);
         $this->setAdminUser();
-        $CFG->enablewebservices = 1;
-        $CFG->enablemobilewebservice = 1;
-        $CFG->httpswwwroot = str_replace('http:', 'https:', $CFG->httpswwwroot);
 
         $this->expectException('moodle_exception');
         $this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
@@ -262,9 +263,6 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $this->resetAfterTest(true);
         $user = $this->getDataGenerator()->create_user();
         $this->setUser($user);
-        $CFG->enablewebservices = 1;
-        $CFG->enablemobilewebservice = 1;
-        $CFG->httpswwwroot = str_replace('http:', 'https:', $CFG->httpswwwroot);
 
         $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
 
index 6a5f152..33c7db7 100644 (file)
@@ -20,7 +20,7 @@ Feature: Add a new user tour
       | Display in middle of page   | Welcome           | Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful |
     And I add steps to the "First tour" tour:
       | targettype                  | targetvalue_block | Title             | Content |
-      | Block                       | My overview       | My overview   | This area shows you what's happening in some of your courses  |
+      | Block                       | Course overview   | Course overview   | This area shows you what's happening in some of your courses  |
       | Block                       | Calendar          | Calendar          | This is the Calendar. All of your assignments and due dates can be found here |
     And I add steps to the "First tour" tour:
       | targettype                  | targetvalue_selector | Title             | Content |
index b318772..d4293f4 100644 (file)
@@ -1064,19 +1064,10 @@ class auth_plugin_mnet extends auth_plugin_base {
     }
 
     /**
-     * Returns a list of potential IdPs that this authentication plugin supports.
-     * This is used to provide links on the login page.
+     * Returns a list of MNet IdPs that the user can roam from.
      *
-     * @param string $wantsurl the relative url fragment the user wants to get to.  You can use this to compose a returnurl, for example
-     *
-     * @return array like:
-     *              array(
-     *                  array(
-     *                      'url' => 'http://someurl',
-     *                      'icon' => new pix_icon(...),
-     *                      'name' => get_string('somename', 'auth_yourplugin'),
-     *                 ),
-     *             )
+     * @param string $wantsurl The relative url fragment the user wants to get to.
+     * @return array List of arrays with keys url, icon and name.
      */
     function loginpage_idp_list($wantsurl) {
         global $DB, $CFG;
index 0106627..fc230aa 100644 (file)
@@ -179,7 +179,7 @@ class auth extends \auth_plugin_base {
      * Return a list of identity providers to display on the login page.
      *
      * @param string|moodle_url $wantsurl The requested URL.
-     * @return array (containing url, iconurl and name).
+     * @return array List of arrays with keys url, iconurl and name.
      */
     public function loginpage_idp_list($wantsurl) {
         $providers = \core\oauth2\api::get_all_issuers();
index 8ee79dc..def7fa2 100644 (file)
@@ -7,6 +7,8 @@ information provided here is intended especially for developers.
   Plugins should use a settings.php file to manage configurations rather than using the config.html files.
 * The function 'print_auth_lock_options' has been replaced by 'display_auth_lock_options' which uses the admin settings API.
   See auth_manual as an exmple of how it can be used.  More information can be found in MDL-12689.
+* The list of supported identity providers (SSO IdP) returned by the 'loginpage_idp_list' method (used to render the
+  login page and login block links) now supports a new key 'iconurl' which should be used instead of the legacy 'icon'.
 
 === 3.2 ===
 
index 7cb258b..f9ae5dc 100644 (file)
@@ -130,6 +130,9 @@ class backup_course_task extends backup_task {
         // Generate the course competencies.
         $this->add_step(new backup_course_competencies_structure_step('course_competencies', 'competencies.xml'));
 
+        // Annotate activity completion defaults.
+        $this->add_step(new backup_completion_defaults_structure_step('course_completion_defaults', 'completiondefaults.xml'));
+
         // Generate the inforef file (must be after ALL steps gathering annotations of ANY type)
         $this->add_step(new backup_inforef_structure_step('course', 'inforef.xml'));
 
index 83300f1..8855d58 100644 (file)
@@ -2612,3 +2612,49 @@ class backup_course_completion_structure_step extends backup_structure_step {
 
     }
 }
+
+/**
+ * Backup completion defaults for each module type.
+ *
+ * @package     core_backup
+ * @copyright   2017 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backup_completion_defaults_structure_step extends backup_structure_step {
+
+    /**
+     * To conditionally decide if one step will be executed or no
+     */
+    protected function execute_condition() {
+        // No completion on front page.
+        if ($this->get_courseid() == SITEID) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * The structure of the course completion backup
+     *
+     * @return backup_nested_element
+     */
+    protected function define_structure() {
+
+        $cc = new backup_nested_element('course_completion_defaults');
+
+        $defaults = new backup_nested_element('course_completion_default', array('id'), array(
+            'modulename', 'completion', 'completionview', 'completionusegrade', 'completionexpected', 'customrules'
+        ));
+
+        // Use module name instead of module id so we can insert into another site later.
+        $sourcesql = "SELECT d.id, m.name as modulename, d.completion, d.completionview, d.completionusegrade,
+                  d.completionexpected, d.customrules
+                FROM {course_completion_defaults} d join {modules} m on d.module = m.id
+                WHERE d.course = ?";
+        $defaults->set_source_sql($sourcesql, array(backup::VAR_COURSEID));
+
+        $cc->add_child($defaults);
+        return $cc;
+
+    }
+}
index 1c07e9c..74bfa6c 100644 (file)
@@ -118,6 +118,9 @@ class restore_course_task extends restore_task {
         // Course competencies.
         $this->add_step(new restore_course_competencies_structure_step('course_competencies', 'competencies.xml'));
 
+        // Activity completion defaults.
+        $this->add_step(new restore_completion_defaults_structure_step('course_completion_defaults', 'completiondefaults.xml'));
+
         // At the end, mark it as built
         $this->built = true;
     }
index 5f4cf05..a0fc059 100644 (file)
@@ -3272,14 +3272,14 @@ class restore_course_competencies_structure_step extends restore_structure_step
             return;
         }
         $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber,
-            'competencyframeworkid' => $framework->get_id()));
+            'competencyframeworkid' => $framework->get('id')));
         if (!$competency) {
             return;
         }
-        $this->set_mapping(\core_competency\competency::TABLE, $data->id, $competency->get_id());
+        $this->set_mapping(\core_competency\competency::TABLE, $data->id, $competency->get('id'));
 
         $params = array(
-            'competencyid' => $competency->get_id(),
+            'competencyid' => $competency->get('id'),
             'courseid' => $this->task->get_courseid()
         );
         $query = 'competencyid = :competencyid AND courseid = :courseid';
@@ -3383,13 +3383,13 @@ class restore_activity_competencies_structure_step extends restore_structure_ste
             return;
         }
         $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber,
-            'competencyframeworkid' => $framework->get_id()));
+            'competencyframeworkid' => $framework->get('id')));
         if (!$competency) {
             return;
         }
 
         $params = array(
-            'competencyid' => $competency->get_id(),
+            'competencyid' => $competency->get('id'),
             'cmid' => $this->task->get_moduleid()
         );
         $query = 'competencyid = :competencyid AND cmid = :cmid';
@@ -5428,3 +5428,73 @@ abstract class restore_questions_activity_structure_step extends restore_activit
         }
     }
 }
+
+/**
+ * Restore completion defaults for each module type
+ *
+ * @package     core_backup
+ * @copyright   2017 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_completion_defaults_structure_step extends restore_structure_step {
+    /**
+     * To conditionally decide if this step must be executed.
+     */
+    protected function execute_condition() {
+        // No completion on the front page.
+        if ($this->get_courseid() == SITEID) {
+            return false;
+        }
+
+        // No default completion info found, don't execute.
+        $fullpath = $this->task->get_taskbasepath();
+        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
+        if (!file_exists($fullpath)) {
+            return false;
+        }
+
+        // Arrived here, execute the step.
+        return true;
+    }
+
+    /**
+     * Function that will return the structure to be processed by this restore_step.
+     *
+     * @return restore_path_element[]
+     */
+    protected function define_structure() {
+        return [new restore_path_element('completion_defaults', '/course_completion_defaults/course_completion_default')];
+    }
+
+    /**
+     * Processor for path element 'completion_defaults'
+     *
+     * @param stdClass|array $data
+     */
+    protected function process_completion_defaults($data) {
+        global $DB;
+
+        $data = (array)$data;
+        $oldid = $data['id'];
+        unset($data['id']);
+
+        // Find the module by name since id may be different in another site.
+        if (!$mod = $DB->get_record('modules', ['name' => $data['modulename']])) {
+            return;
+        }
+        unset($data['modulename']);
+
+        // Find the existing record.
+        $newid = $DB->get_field('course_completion_defaults', 'id',
+            ['course' => $this->task->get_courseid(), 'module' => $mod->id]);
+        if (!$newid) {
+            $newid = $DB->insert_record('course_completion_defaults',
+                ['course' => $this->task->get_courseid(), 'module' => $mod->id] + $data);
+        } else {
+            $DB->update_record('course_completion_defaults', ['id' => $newid] + $data);
+        }
+
+        // Save id mapping for restoring associated events.
+        $this->set_mapping('course_completion_defaults', $oldid, $newid);
+    }
+}
\ No newline at end of file
index 7ebbaf3..cdc315e 100644 (file)
Binary files a/blocks/myoverview/amd/build/event_list.min.js and b/blocks/myoverview/amd/build/event_list.min.js differ
index 589626c..4abb4fb 100644 (file)
Binary files a/blocks/myoverview/amd/build/event_list_by_course.min.js and b/blocks/myoverview/amd/build/event_list_by_course.min.js differ
index ee66549..17e888f 100644 (file)
@@ -222,12 +222,13 @@ define(['jquery', 'core/notification', 'core/templates',
      *
      * @method eventBelongsInContainer
      * @private
+     * @param {object} root         The root element
      * @param {object} event        The calendar event
      * @param {object} container    The group event list container
      * @return {bool}
      */
-    var eventBelongsInContainer = function(event, container) {
-        var todayTime = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000),
+    var eventBelongsInContainer = function(root, event, container) {
+        var todayTime = root.attr('data-midnight'),
             timeUntilContainerStart = +container.attr('data-start-day') * SECONDS_IN_DAY,
             timeUntilContainerEnd = +container.attr('data-end-day') * SECONDS_IN_DAY,
             timeUntilEventNeedsAction = timeUntilEvent(todayTime, event);
@@ -246,12 +247,13 @@ define(['jquery', 'core/notification', 'core/templates',
      *
      * @method getFilterCallbackForContainer
      * @private
+     * @param {object} root      The root element
      * @param {object} container Event list group container
      * @return {function}
      */
-    var getFilterCallbackForContainer = function(container) {
+    var getFilterCallbackForContainer = function(root, container) {
         return function(event) {
-            return eventBelongsInContainer(event, $(container));
+            return eventBelongsInContainer(root, event, $(container));
         };
     };
 
@@ -278,7 +280,7 @@ define(['jquery', 'core/notification', 'core/templates',
         // that belong to that group (as defined by the group's day range). The matching
         // list of calendar events are rendered and added to the DOM within that group.
         return $.when.apply($, $.map(root.find(SELECTORS.EVENT_LIST_GROUP_CONTAINER), function(container) {
-            var events = calendarEvents.filter(getFilterCallbackForContainer(container));
+            var events = calendarEvents.filter(getFilterCallbackForContainer(root, container));
 
             if (events.length) {
                 renderCount += events.length;
@@ -314,12 +316,8 @@ define(['jquery', 'core/notification', 'core/templates',
         var limit = +root.attr('data-limit'),
             courseId = +root.attr('data-course-id'),
             lastId = root.attr('data-last-id'),
-            date = new Date(),
-            startTime;
-
-        date.setDate(date.getDate() - 14);
-        date.setHours(0, 0, 0, 0);
-        startTime = Math.floor(date / 1000);
+            midnight = root.attr('data-midnight'),
+            startTime = midnight - (14 * SECONDS_IN_DAY);
 
         // Don't load twice.
         if (isLoading(root)) {
index db182ee..69fae95 100644 (file)
@@ -29,6 +29,8 @@ define(
 ],
 function($, EventList, EventsRepository) {
 
+    var SECONDS_IN_DAY = 60 * 60 * 24;
+
     var SELECTORS = {
         EVENTS_BY_COURSE_CONTAINER: '[data-region="course-events-container"]',
     };
@@ -46,10 +48,8 @@ function($, EventList, EventsRepository) {
             return;
         }
 
-        var date = new Date();
-        date.setDate(date.getDate() - 14);
-        date.setHours(0, 0, 0, 0);
-        var startTime = Math.floor(date / 1000);
+        var midnight = root.attr('data-midnight');
+        var startTime = midnight - (14 * SECONDS_IN_DAY);
         var limit = courseBlocks.attr('data-limit');
         var courseIds = courseBlocks.map(function() {
             return $(this).attr('data-course-id');
index ee34bd3..e7896e0 100644 (file)
@@ -72,6 +72,7 @@ class main implements renderable, templatable {
         $noeventsurl = $output->image_url('activities', 'block_myoverview')->out();
 
         return [
+            'midnight' => usergetmidnight(time()),
             'coursesview' => $coursesview->export_for_template($output),
             'urls' => [
                 'nocourses' => $nocoursesurl,
index 87ce648..a67caec 100644 (file)
@@ -25,8 +25,8 @@
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
 $string['morecourses'] = 'More courses';
-$string['myoverview:addinstance'] = 'Add a new my overview block';
-$string['myoverview:myaddinstance'] = 'Add a new my overview block to Dashboard';
+$string['myoverview:addinstance'] = 'Add a new course overview block';
+$string['myoverview:myaddinstance'] = 'Add a new course overview block to Dashboard';
 $string['nocourses'] = 'No courses';
 $string['nocoursesinprogress'] = 'No in progress courses';
 $string['nocoursesfuture'] = 'No future courses';
@@ -35,7 +35,7 @@ $string['noevents'] = 'No upcoming activities due';
 $string['next30days'] = 'Next 30 days';
 $string['next7days'] = 'Next 7 days';
 $string['past'] = 'Past';
-$string['pluginname'] = 'My overview';
+$string['pluginname'] = 'Course overview';
 $string['recentlyoverdue'] = 'Recently overdue';
 $string['sortbycourses'] = 'Sort by courses';
 $string['sortbydates'] = 'Sort by dates';
index 77cf57c..ac16e44 100644 (file)
@@ -28,6 +28,7 @@
      data-limit="{{$limit}}20{{/limit}}"
      data-course-id="{{$courseid}}{{/courseid}}"
      data-last-id="{{$lastid}}{{/lastid}}"
+     data-midnight="{{midnight}}"
      id="event-list-container-{{$courseid}}{{/courseid}}">
 
     <div data-region="event-list-content">
index 7d2250b..c7ce9d8 100644 (file)
@@ -23,8 +23,6 @@
     {
         "shortname": "course 3",
         "viewurl": "https://www.google.com",
-        "startdate": 1490320388,
-        "enddate": 1490320388,
         "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
     }
 }}
index c20a052..e9c99f7 100644 (file)
             {
                 "fullnamedisplay": "course 1",
                 "viewurl": "https://www.google.com",
-                "startdate": 1490252232,
-                "enddate": 1490252232,
                 "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
             },
             {
                 "fullnamedisplay": "course 2",
                 "viewurl": "https://www.google.com",
-                "startdate": 1490252232,
-                "enddate": 1490252232,
                 "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
             }
         ]
index 21a5561..85ee437 100644 (file)
                     {
                         "fullnamedisplay": "course 1",
                         "viewurl": "https://www.google.com",
-                        "startdate": 1490252232,
-                        "enddate": 1490252232,
                         "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
                     },
                     {
                         "fullnamedisplay": "course 2",
                         "viewurl": "https://www.google.com",
-                        "startdate": 1490252232,
-                        "enddate": 1490252232,
                         "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
                     }
                 ]
index dcde9a3..f9fcf9f 100644 (file)
@@ -23,8 +23,6 @@
     {
         "fullnamedisplay": "course 3",
         "viewurl": "https://www.google.com",
-        "startdate": 1490320388,
-        "enddate": 1490320388,
         "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
     }
 }}
             </div>
         </div>
     </div>
-    <p class="text-muted">
-        {{#userdate}}{{startdate}}, {{#str}}strftimedate, langconfig{{/str}}{{/userdate}}
-        {{#enddate}}
-        - {{#userdate}}{{enddate}}, {{#str}}strftimedate, langconfig{{/str}}{{/userdate}}
-        {{/enddate}}
-    </p>
     <p class="text-muted">
         {{#shortentext}} 140, {{summary}}{{/shortentext}}
     </p>
index 529e33b..0d95121 100644 (file)
@@ -23,8 +23,6 @@
     {
         "fullnamedisplay": "course 3",
         "viewurl": "https://www.google.com",
-        "startdate": 1490252232,
-        "enddate": 1490252232,
         "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
     }
 }}
                     </div>
                 </div>
             </div>
-            <p class="text-muted">
-                {{#userdate}}{{startdate}}, {{#str}}strftimedate, langconfig{{/str}}{{/userdate}}
-                {{#enddate}}
-                - {{#userdate}}{{enddate}}, {{#str}}strftimedate, langconfig{{/str}}{{/userdate}}
-                {{/enddate}}
-            </p>
             <p class="text-muted">
                 {{#shortentext}} 140, {{summary}}{{/shortentext}}
             </p>
index 83f6234..ae5553b 100644 (file)
@@ -27,6 +27,7 @@
      data-limit="{{$limit}}20{{/limit}}"
      data-course-id="{{$courseid}}{{/courseid}}"
      data-last-id="{{$lastid}}{{/lastid}}"
+     data-midnight="{{midnight}}"
      id="event-list-container-{{$courseid}}{{/courseid}}">
 
     <div data-region="event-list-content">
index 7aec53c..42bb41c 100644 (file)
@@ -29,53 +29,53 @@ Feature: The my overview block allows users to easily access their courses and s
 
   Scenario: View courses and upcoming activities on timeline view
     Given I log in as "student1"
-    And I click on "Timeline" "link" in the "My overview" "block"
-    When I click on "Sort by dates" "link" in the "My overview" "block"
-    Then I should see "Next 7 days" in the "My overview" "block"
-    And I should see "Choice Test choice 1 closes" in the "My overview" "block"
-    And I should see "View choices" in the "My overview" "block"
-    And I should see "Feedback Test feedback 1 closes" in the "My overview" "block"
-    And I should see "Answer the questions" in the "My overview" "block"
-    And I should see "##tomorrow##j M, H:i##" in the "My overview" "block"
-    And I should see "Future" in the "My overview" "block"
-    And I should see "Choice Test choice 3 closes" in the "My overview" "block"
-    And I should see "Feedback Test feedback 3 closes" in the "My overview" "block"
-    And I should see "##last day of next month##j M, H:i##" in the "My overview" "block"
+    And I click on "Timeline" "link" in the "Course overview" "block"
+    When I click on "Sort by dates" "link" in the "Course overview" "block"
+    Then I should see "Next 7 days" in the "Course overview" "block"
+    And I should see "Choice Test choice 1 closes" in the "Course overview" "block"
+    And I should see "View choices" in the "Course overview" "block"
+    And I should see "Feedback Test feedback 1 closes" in the "Course overview" "block"
+    And I should see "Answer the questions" in the "Course overview" "block"
+    And I should see "##tomorrow##j M, H:i##" in the "Course overview" "block"
+    And I should see "Future" in the "Course overview" "block"
+    And I should see "Choice Test choice 3 closes" in the "Course overview" "block"
+    And I should see "Feedback Test feedback 3 closes" in the "Course overview" "block"
+    And I should see "##last day of next month##j M, H:i##" in the "Course overview" "block"
     And I log out
 
   Scenario: Past activities should not be displayed on the timeline view
     Given I log in as "student1"
-    And I click on "Timeline" "link" in the "My overview" "block"
-    When I click on "Sort by dates" "link" in the "My overview" "block"
-    And I should not see "Choice Test choice 2 closes" in the "My overview" "block"
-    And I should not see "##1 month ago##j M, H:i##" in the "My overview" "block"
+    And I click on "Timeline" "link" in the "Course overview" "block"
+    When I click on "Sort by dates" "link" in the "Course overview" "block"
+    And I should not see "Choice Test choice 2 closes" in the "Course overview" "block"
+    And I should not see "##1 month ago##j M, H:i##" in the "Course overview" "block"
     And I log out
 
   Scenario: See the courses I am enrolled by their status on courses view
     Given I log in as "student1"
-    And I click on "Courses" "link" in the "My overview" "block"
-    And I click on "In progress" "link" in the "My overview" "block"
-    And I should see "Course 2" in the "My overview" "block"
-    And I should see "##yesterday##j F Y##" in the "My overview" "block"
-    And I should see "##tomorrow##j F Y##" in the "My overview" "block"
-    And I should not see "Course 1" in the "My overview" "block"
-    And I click on "Future" "link" in the "My overview" "block"
-    And I should see "Course 3" in the "My overview" "block"
-    And I should see "##first day of next month##j F Y##" in the "My overview" "block"
-    And I should see "##last day of next month##j F Y##" in the "My overview" "block"
-    And I should not see "Course 1" in the "My overview" "block"
-    When I click on "Past" "link" in the "My overview" "block"
-    Then I should see "Course 1" in the "My overview" "block"
-    And I should not see "Course 2" in the "My overview" "block"
-    And I should not see "Course 3" in the "My overview" "block"
-    And I should see "##1 month ago##j F Y##" in the "My overview" "block"
-    And I should see "##15 days ago##j F Y##" in the "My overview" "block"
+    And I click on "Courses" "link" in the "Course overview" "block"
+    And I click on "In progress" "link" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should see "##yesterday##j F Y##" in the "Course overview" "block"
+    And I should see "##tomorrow##j F Y##" in the "Course overview" "block"
+    And I should not see "Course 1" in the "Course overview" "block"
+    And I click on "Future" "link" in the "Course overview" "block"
+    And I should see "Course 3" in the "Course overview" "block"
+    And I should see "##first day of next month##j F Y##" in the "Course overview" "block"
+    And I should see "##last day of next month##j F Y##" in the "Course overview" "block"
+    And I should not see "Course 1" in the "Course overview" "block"
+    When I click on "Past" "link" in the "Course overview" "block"
+    Then I should see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should see "##1 month ago##j F Y##" in the "Course overview" "block"
+    And I should see "##15 days ago##j F Y##" in the "Course overview" "block"
     And I log out
 
   Scenario: No activities should be displayed if the user is not enrolled
     Given I log in as "student2"
-    And I click on "Timeline" "link" in the "My overview" "block"
-    And I should see "No upcoming activities" in the "My overview" "block"
-    When I click on "Courses" "link" in the "My overview" "block"
-    Then I should see "No courses" in the "My overview" "block"
+    And I click on "Timeline" "link" in the "Course overview" "block"
+    And I should see "No upcoming activities" in the "Course overview" "block"
+    When I click on "Courses" "link" in the "Course overview" "block"
+    Then I should see "No courses" in the "Course overview" "block"
     And I log out
index 90508bb..d361f87 100644 (file)
@@ -1,5 +1,5 @@
 @block @block_myoverview @javascript
-Feature: My overview block show users their progress on courses
+Feature: Course overview block show users their progress on courses
   In order to enable the my overview block in a course
   As a student
   I can see the progress percentage of the courses I am enrolled in
@@ -22,14 +22,14 @@ Feature: My overview block show users their progress on courses
 
   Scenario: Course progress percentage should not be displayed if completion is not enabled
     Given I log in as "student1"
-    And I click on "Timeline" "link" in the "My overview" "block"
-    When I click on "Sort by courses" "link" in the "My overview" "block"
-    Then I should see "Choice Test choice 1 closes" in the "My overview" "block"
-    And I should not see "0%" in the "My overview" "block"
-    And I click on "Courses" "link" in the "My overview" "block"
-    And I click on "In progress" "link" in the "My overview" "block"
-    And I should see "Course 1" in the "My overview" "block"
-    And I should not see "0%" in the "My overview" "block"
+    And I click on "Timeline" "link" in the "Course overview" "block"
+    When I click on "Sort by courses" "link" in the "Course overview" "block"
+    Then I should see "Choice Test choice 1 closes" in the "Course overview" "block"
+    And I should not see "0%" in the "Course overview" "block"
+    And I click on "Courses" "link" in the "Course overview" "block"
+    And I click on "In progress" "link" in the "Course overview" "block"
+    And I should see "Course 1" in the "Course overview" "block"
+    And I should not see "0%" in the "Course overview" "block"
     And I log out
 
   Scenario: User complete activity and verify his progress
@@ -43,20 +43,20 @@ Feature: My overview block show users their progress on courses
     And I press "Save and return to course"
     And I log out
     And I log in as "student1"
-    And I click on "Sort by courses" "link" in the "My overview" "block"
-    And I should see "Choice Test choice 1 closes" in the "My overview" "block"
-    And I should see "0%" in the "My overview" "block"
-    And I click on "Courses" "link" in the "My overview" "block"
-    When I click on "In progress" "link" in the "My overview" "block"
-    Then I should see "Course 1" in the "My overview" "block"
-    And I should see "0%" in the "My overview" "block"
+    And I click on "Sort by courses" "link" in the "Course overview" "block"
+    And I should see "Choice Test choice 1 closes" in the "Course overview" "block"
+    And I should see "0%" in the "Course overview" "block"
+    And I click on "Courses" "link" in the "Course overview" "block"
+    When I click on "In progress" "link" in the "Course overview" "block"
+    Then I should see "Course 1" in the "Course overview" "block"
+    And I should see "0%" in the "Course overview" "block"
     And I am on "Course 1" course homepage
     And I follow "Test choice 1"
     And I follow "Dashboard" in the user menu
-    And I click on "Sort by courses" "link" in the "My overview" "block"
-    And I should see "100%" in the "My overview" "block"
-    And I click on "Courses" "link" in the "My overview" "block"
-    And I click on "In progress" "link" in the "My overview" "block"
-    And I should see "Course 1" in the "My overview" "block"
-    And I should see "100%" in the "My overview" "block"
+    And I click on "Sort by courses" "link" in the "Course overview" "block"
+    And I should see "100%" in the "Course overview" "block"
+    And I click on "Courses" "link" in the "Course overview" "block"
+    And I click on "In progress" "link" in the "Course overview" "block"
+    And I should see "Course 1" in the "Course overview" "block"
+    And I should see "100%" in the "Course overview" "block"
     And I log out
index 40ae8db..d548f60 100644 (file)
@@ -70,4 +70,4 @@ Feature: Add and configure blocks throughout the site
       | Block title | Foo " onload="document.getElementsByTagName('body')[0].remove()" alt=" |
       | Content     | Example |
     When I press "Save changes"
-    Then I should see "My overview"
+    Then I should see "Course overview"
index 6add035..0fa83c7 100644 (file)
@@ -4,9 +4,9 @@ information provided here is intended especially for developers.
 === 3.3 ===
 
 * block_manager::get_required_by_theme_block_types() is no longer static.
-* The 'Course overview' block has been removed from core as it is being replaced by the 'My overview' block.
-  During the upgrade process the 'Course overview' block will be uninstalled and all its settings will be deleted.
-  If you wish to keep the 'Course overview' block and its settings, download it from moodle.org and put it back in
+* The plugin block_course_overview has been removed from core and is being replaced by block_myoverview.
+  During the upgrade process the block_course_overview block will be uninstalled and all its settings will be deleted.
+  If you wish to keep the block_course_overview block and its settings, download it from moodle.org and put it back in
   the blocks/ directory BEFORE UPGRADING.
 
 === 3.1 ===
index 18682ff..4eb25fd 100644 (file)
@@ -104,62 +104,6 @@ class api {
         );
     }
 
-    /**
-     * Get legacy calendar events
-     *
-     * @param int $tstart Start time of time range for events
-     * @param int $tend End time of time range for events
-     * @param array|int|boolean $users array of users, user id or boolean for all/no user events
-     * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events
-     * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events
-     * @param boolean $withduration whether only events starting within time range selected
-     *                              or events in progress/already started selected as well
-     * @param boolean $ignorehidden whether to select only visible events or all events
-     * @return array $events of selected events or an empty array if there aren't any (or there was an error)
-     */
-    public static function get_legacy_events(
-        $tstart,
-        $tend,
-        $users,
-        $groups,
-        $courses,
-        $withduration = true,
-        $ignorehidden = true
-    ) {
-        $fixedparams = array_map(function($param) {
-            if ($param === true) {
-                return null;
-            }
-
-            if (!is_array($param)) {
-                return [$param];
-            }
-
-            return $param;
-        }, [$users, $groups, $courses]);
-
-        $mapper = \core_calendar\local\event\container::get_event_mapper();
-        $events = self::get_events(
-            $tstart,
-            $tend,
-            null,
-            null,
-            null,
-            null,
-            40,
-            null,
-            $fixedparams[0],
-            $fixedparams[1],
-            $fixedparams[2],
-            $withduration,
-            $ignorehidden
-        );
-
-        return array_reduce($events, function($carry, $event) use ($mapper) {
-            return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)];
-        }, []);
-    }
-
     /**
      * Get a list of action events for the logged in user by the given
      * timesort values.
index 0d0d75a..41e611d 100644 (file)
@@ -180,7 +180,7 @@ if(!empty($what) && !empty($time)) {
         die();
     }
 }
-$events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, $users, $groups, array_keys($paramcourses), false);
+$events = calendar_get_legacy_events($timestart, $timeend, $users, $groups, array_keys($paramcourses), false);
 
 $ical = new iCalendar;
 $ical->add_property('method', 'PUBLISH');
index 418731e..074f0e0 100644 (file)
@@ -233,7 +233,7 @@ class core_calendar_external extends external_api {
         }
 
         // Event list does not check visibility and permissions, we'll check that later.
-        $eventlist = \core_calendar\local\api::get_legacy_events($params['options']['timestart'], $params['options']['timeend'],
+        $eventlist = calendar_get_legacy_events($params['options']['timestart'], $params['options']['timeend'],
             $funcparam['users'], $funcparam['groups'], $funcparam['courses'], true, $params['options']['ignorehidden']);
 
         // WS expects arrays.
index 01abe8d..94233f1 100644 (file)
@@ -1398,7 +1398,7 @@ function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyea
     }
 
     // Get the events matching our criteria. Don't forget to offset the timestamps for the user's TZ.
-    $events = \core_calendar\local\api::get_legacy_events($display->tstart, $display->tend, $users, $groups, $courses);
+    $events = calendar_get_legacy_events($display->tstart, $display->tend, $users, $groups, $courses);
 
     // Set event course class for course events.
     if (!empty($events)) {
@@ -1715,7 +1715,7 @@ function calendar_get_upcoming($courses, $groups, $users, $daysinfuture, $maxeve
     $display->tend = usergetmidnight($display->tstart + DAYSECS * $display->range + 3 * HOURSECS) - 1;
 
     // Get the events matching our criteria.
-    $events = \core_calendar\local\api::get_legacy_events($display->tstart, $display->tend, $users, $groups, $courses);
+    $events = calendar_get_legacy_events($display->tstart, $display->tend, $users, $groups, $courses);
 
     // This is either a genius idea or an idiot idea: in order to not complicate things, we use this rule: if, after
     // possibly removing SITEID from $courses, there is only one course left, then clicking on a day in the month
@@ -3374,3 +3374,62 @@ function core_calendar_user_preferences() {
         'choices' => array(0, 1));
     return $preferences;
 }
+
+/**
+ * Get legacy calendar events
+ *
+ * @param int $tstart Start time of time range for events
+ * @param int $tend End time of time range for events
+ * @param array|int|boolean $users array of users, user id or boolean for all/no user events
+ * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events
+ * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events
+ * @param boolean $withduration whether only events starting within time range selected
+ *                              or events in progress/already started selected as well
+ * @param boolean $ignorehidden whether to select only visible events or all events
+ * @return array $events of selected events or an empty array if there aren't any (or there was an error)
+ */
+function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $withduration = true, $ignorehidden = true) {
+    // Normalise the users, groups and courses parameters so that they are compliant with \core_calendar\local\api::get_events().
+    // Existing functions that were using the old calendar_get_events() were passing a mixture of array, int, boolean for these
+    // parameters, but with the new API method, only null and arrays are accepted.
+    list($userparam, $groupparam, $courseparam) = array_map(function($param) {
+        // If parameter is true, return null.
+        if ($param === true) {
+            return null;
+        }
+
+        // If parameter is false, return an empty array.
+        if ($param === false) {
+            return [];
+        }
+
+        // If the parameter is a scalar value, enclose it in an array.
+        if (!is_array($param)) {
+            return [$param];
+        }
+
+        // No normalisation required.
+        return $param;
+    }, [$users, $groups, $courses]);
+
+    $mapper = \core_calendar\local\event\container::get_event_mapper();
+    $events = \core_calendar\local\api::get_events(
+        $tstart,
+        $tend,
+        null,
+        null,
+        null,
+        null,
+        40,
+        null,
+        $userparam,
+        $groupparam,
+        $courseparam,
+        $withduration,
+        $ignorehidden
+    );
+
+    return array_reduce($events, function($carry, $event) use ($mapper) {
+        return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)];
+    }, []);
+}
index 378dc98..ed4bcef 100644 (file)
@@ -375,7 +375,7 @@ class core_calendar_renderer extends plugin_renderer_base {
         }
 
         // Get events from database
-        $events = \core_calendar\local\api::get_legacy_events($display->tstart, $display->tend, $calendar->users, $calendar->groups,
+        $events = calendar_get_legacy_events($display->tstart, $display->tend, $calendar->users, $calendar->groups,
             $calendar->courses);
         if (!empty($events)) {
             foreach($events as $eventid => $event) {
index 797dc89..750bb90 100644 (file)
@@ -392,4 +392,187 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $count = $DB->count_records('event', array('subscriptionid' => $sub->id));
         $this->assertEquals($count, 1);
     }
+
+    /**
+     * Test for calendar_get_legacy_events() when there are user and group overrides.
+     */
+    public function test_get_legacy_events_with_overrides() {
+        $generator = $this->getDataGenerator();
+
+        $course = $generator->create_course();
+
+        $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        if (!isset($params['course'])) {
+            $params['course'] = $course->id;
+        }
+
+        $instance = $plugingenerator->create_instance($params);
+
+        // Create users.
+        $useroverridestudent = $generator->create_user();
+        $group1student = $generator->create_user();
+        $group2student = $generator->create_user();
+        $group12student = $generator->create_user();
+        $nogroupstudent = $generator->create_user();
+
+        // Enrol users.
+        $generator->enrol_user($useroverridestudent->id, $course->id, 'student');
+        $generator->enrol_user($group1student->id, $course->id, 'student');
+        $generator->enrol_user($group2student->id, $course->id, 'student');
+        $generator->enrol_user($group12student->id, $course->id, 'student');
+        $generator->enrol_user($nogroupstudent->id, $course->id, 'student');
+
+        // Create groups.
+        $group1 = $generator->create_group(['courseid' => $course->id]);
+        $group2 = $generator->create_group(['courseid' => $course->id]);
+
+        // Add members to groups.
+        $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group1student->id]);
+        $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group2student->id]);
+        $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group12student->id]);
+        $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group12student->id]);
+        $now = time();
+
+        // Events with the same module name, instance and event type.
+        $events = [
+            [
+                'name' => 'Assignment 1 due date',
+                'description' => '',
+                'format' => 0,
+                'courseid' => $course->id,
+                'groupid' => 0,
+                'userid' => 2,
+                'modulename' => 'assign',
+                'instance' => $instance->id,
+                'eventtype' => 'due',
+                'timestart' => $now,
+                'timeduration' => 0,
+                'visible' => 1
+            ], [
+                'name' => 'Assignment 1 due date - User override',
+                'description' => '',
+                'format' => 1,
+                'courseid' => 0,
+                'groupid' => 0,
+                'userid' => $useroverridestudent->id,
+                'modulename' => 'assign',
+                'instance' => $instance->id,
+                'eventtype' => 'due',
+                'timestart' => $now + 86400,
+                'timeduration' => 0,
+                'visible' => 1,
+                'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY
+            ], [
+                'name' => 'Assignment 1 due date - Group A override',
+                'description' => '',
+                'format' => 1,
+                'courseid' => $course->id,
+                'groupid' => $group1->id,
+                'userid' => 2,
+                'modulename' => 'assign',
+                'instance' => $instance->id,
+                'eventtype' => 'due',
+                'timestart' => $now + (2 * 86400),
+                'timeduration' => 0,
+                'visible' => 1,
+                'priority' => 1,
+            ], [
+                'name' => 'Assignment 1 due date - Group B override',
+                'description' => '',
+                'format' => 1,
+                'courseid' => $course->id,
+                'groupid' => $group2->id,
+                'userid' => 2,
+                'modulename' => 'assign',
+                'instance' => $instance->id,
+                'eventtype' => 'due',
+                'timestart' => $now + (3 * 86400),
+                'timeduration' => 0,
+                'visible' => 1,
+                'priority' => 2,
+            ],
+        ];
+
+        foreach ($events as $event) {
+            calendar_event::create($event, false);
+        }
+
+        $timestart = $now - 100;
+        $timeend = $now + (3 * 86400);
+        $groups = [$group1->id, $group2->id];
+
+        // Get user override events.
+        $this->setUser($useroverridestudent);
+        $events = calendar_get_legacy_events($timestart, $timeend, $useroverridestudent->id, $groups, $course->id);
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertEquals('Assignment 1 due date - User override', $event->name);
+
+        // Get event for user with override but with the timestart and timeend parameters only covering the original event.
+        $events = calendar_get_legacy_events($timestart, $now, $useroverridestudent->id, $groups, $course->id);
+        $this->assertCount(0, $events);
+
+        // Get events for user that does not belong to any group and has no user override events.
+        $this->setUser($nogroupstudent);
+        $events = calendar_get_legacy_events($timestart, $timeend, $nogroupstudent->id, $groups, $course->id);
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertEquals('Assignment 1 due date', $event->name);
+
+        // Get events for user that belongs to groups A and B and has no user override events.
+        $this->setUser($group12student);
+        $events = calendar_get_legacy_events($timestart, $timeend, $group12student->id, $groups, $course->id);
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertEquals('Assignment 1 due date - Group B override', $event->name);
+
+        // Get events for user that belongs to group A and has no user override events.
+        $this->setUser($group1student);
+        $events = calendar_get_legacy_events($timestart, $timeend, $group1student->id, $groups, $course->id);
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertEquals('Assignment 1 due date - Group A override', $event->name);
+
+        // Add repeating events.
+        $repeatingevents = [
+            [
+                'name' => 'Repeating site event',
+                'description' => '',
+                'format' => 1,
+                'courseid' => SITEID,
+                'groupid' => 0,
+                'userid' => 2,
+                'repeatid' => $event->id,
+                'modulename' => '0',
+                'instance' => 0,
+                'eventtype' => 'site',
+                'timestart' => $now + 86400,
+                'timeduration' => 0,
+                'visible' => 1,
+            ],
+            [
+                'name' => 'Repeating site event',
+                'description' => '',
+                'format' => 1,
+                'courseid' => SITEID,
+                'groupid' => 0,
+                'userid' => 2,
+                'repeatid' => $event->id,
+                'modulename' => '0',
+                'instance' => 0,
+                'eventtype' => 'site',
+                'timestart' => $now + (2 * 86400),
+                'timeduration' => 0,
+                'visible' => 1,
+            ],
+        ];
+
+        foreach ($repeatingevents as $event) {
+            calendar_event::create($event, false);
+        }
+
+        // Make sure repeating events are not filtered out.
+        $events = calendar_get_legacy_events($timestart, $timeend, true, true, true);
+        $this->assertCount(3, $events);
+    }
 }
\ No newline at end of file
index 599a605..b206454 100644 (file)
@@ -661,7 +661,7 @@ class core_calendar_local_api_testcase extends advanced_testcase {
         $timeend = time() + 60;
 
         // Get all events.
-        $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, true, 0, true);
+        $events = calendar_get_legacy_events($timestart, $timeend, true, 0, true);
         $this->assertCount(2, $events);
 
         // Disable the lesson module.
@@ -670,192 +670,9 @@ class core_calendar_local_api_testcase extends advanced_testcase {
         $DB->update_record('modules', $modulerecord);
 
         // Check that we only return the assign event.
-        $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, true, 0, true);
+        $events = calendar_get_legacy_events($timestart, $timeend, true, 0, true);
         $this->assertCount(1, $events);
         $event = reset($events);
         $this->assertEquals('assign', $event->modulename);
     }
-
-    /**
-     * Test for \core_calendar\local\api::get_legacy_events() when there are user and group overrides.
-     */
-    public function test_get_legacy_events_with_overrides() {
-        $generator = $this->getDataGenerator();
-
-        $course = $generator->create_course();
-
-        $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
-        if (!isset($params['course'])) {
-            $params['course'] = $course->id;
-        }
-
-        $instance = $plugingenerator->create_instance($params);
-
-        // Create users.
-        $useroverridestudent = $generator->create_user();
-        $group1student = $generator->create_user();
-        $group2student = $generator->create_user();
-        $group12student = $generator->create_user();
-        $nogroupstudent = $generator->create_user();
-
-        // Enrol users.
-        $generator->enrol_user($useroverridestudent->id, $course->id, 'student');
-        $generator->enrol_user($group1student->id, $course->id, 'student');
-        $generator->enrol_user($group2student->id, $course->id, 'student');
-        $generator->enrol_user($group12student->id, $course->id, 'student');
-        $generator->enrol_user($nogroupstudent->id, $course->id, 'student');
-
-        // Create groups.
-        $group1 = $generator->create_group(['courseid' => $course->id]);
-        $group2 = $generator->create_group(['courseid' => $course->id]);
-
-        // Add members to groups.
-        $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group1student->id]);
-        $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group2student->id]);
-        $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group12student->id]);
-        $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group12student->id]);
-        $now = time();
-
-        // Events with the same module name, instance and event type.
-        $events = [
-            [
-                'name' => 'Assignment 1 due date',
-                'description' => '',
-                'format' => 0,
-                'courseid' => $course->id,
-                'groupid' => 0,
-                'userid' => 2,
-                'modulename' => 'assign',
-                'instance' => $instance->id,
-                'eventtype' => 'due',
-                'timestart' => $now,
-                'timeduration' => 0,
-                'visible' => 1
-            ], [
-                'name' => 'Assignment 1 due date - User override',
-                'description' => '',
-                'format' => 1,
-                'courseid' => 0,
-                'groupid' => 0,
-                'userid' => $useroverridestudent->id,
-                'modulename' => 'assign',
-                'instance' => $instance->id,
-                'eventtype' => 'due',
-                'timestart' => $now + 86400,
-                'timeduration' => 0,
-                'visible' => 1,
-                'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY
-            ], [
-                'name' => 'Assignment 1 due date - Group A override',
-                'description' => '',
-                'format' => 1,
-                'courseid' => $course->id,
-                'groupid' => $group1->id,
-                'userid' => 2,
-                'modulename' => 'assign',
-                'instance' => $instance->id,
-                'eventtype' => 'due',
-                'timestart' => $now + (2 * 86400),
-                'timeduration' => 0,
-                'visible' => 1,
-                'priority' => 1,
-            ], [
-                'name' => 'Assignment 1 due date - Group B override',
-                'description' => '',
-                'format' => 1,
-                'courseid' => $course->id,
-                'groupid' => $group2->id,
-                'userid' => 2,
-                'modulename' => 'assign',
-                'instance' => $instance->id,
-                'eventtype' => 'due',
-                'timestart' => $now + (3 * 86400),
-                'timeduration' => 0,
-                'visible' => 1,
-                'priority' => 2,
-            ],
-        ];
-
-        foreach ($events as $event) {
-            calendar_event::create($event, false);
-        }
-
-        $timestart = $now - 100;
-        $timeend = $now + (3 * 86400);
-        $groups = [$group1->id, $group2->id];
-
-        // Get user override events.
-        $this->setUser($useroverridestudent);
-        $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, $useroverridestudent->id, $groups, $course->id);
-        $this->assertCount(1, $events);
-        $event = reset($events);
-        $this->assertEquals('Assignment 1 due date - User override', $event->name);
-
-        // Get event for user with override but with the timestart and timeend parameters only covering the original event.
-        $events = \core_calendar\local\api::get_legacy_events($timestart, $now, $useroverridestudent->id, $groups, $course->id);
-        $this->assertCount(0, $events);
-
-        // Get events for user that does not belong to any group and has no user override events.
-        $this->setUser($nogroupstudent);
-        $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, $nogroupstudent->id, $groups, $course->id);
-        $this->assertCount(1, $events);
-        $event = reset($events);
-        $this->assertEquals('Assignment 1 due date', $event->name);
-
-        // Get events for user that belongs to groups A and B and has no user override events.
-        $this->setUser($group12student);
-        $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, $group12student->id, $groups, $course->id);
-        $this->assertCount(1, $events);
-        $event = reset($events);
-        $this->assertEquals('Assignment 1 due date - Group B override', $event->name);
-
-        // Get events for user that belongs to group A and has no user override events.
-        $this->setUser($group1student);
-        $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, $group1student->id, $groups, $course->id);
-        $this->assertCount(1, $events);
-        $event = reset($events);
-        $this->assertEquals('Assignment 1 due date - Group A override', $event->name);
-
-        // Add repeating events.
-        $repeatingevents = [
-            [
-                'name' => 'Repeating site event',
-                'description' => '',
-                'format' => 1,
-                'courseid' => SITEID,
-                'groupid' => 0,
-                'userid' => 2,
-                'repeatid' => $event->id,
-                'modulename' => '0',
-                'instance' => 0,
-                'eventtype' => 'site',
-                'timestart' => $now + 86400,
-                'timeduration' => 0,
-                'visible' => 1,
-            ],
-            [
-                'name' => 'Repeating site event',
-                'description' => '',
-                'format' => 1,
-                'courseid' => SITEID,
-                'groupid' => 0,
-                'userid' => 2,
-                'repeatid' => $event->id,
-                'modulename' => '0',
-                'instance' => 0,
-                'eventtype' => 'site',
-                'timestart' => $now + (2 * 86400),
-                'timeduration' => 0,
-                'visible' => 1,
-            ],
-        ];
-
-        foreach ($repeatingevents as $event) {
-            calendar_event::create($event, false);
-        }
-
-        // Make sure repeating events are not filtered out.
-        $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, true, true, true);
-        $this->assertCount(3, $events);
-    }
 }
index 61e6159..6e1223d 100644 (file)
@@ -60,7 +60,7 @@ abstract class persistent extends \core\persistent {
         } else if (strpos($method, 'set_') === 0) {
             return $this->set(substr($method, 4), $arguments[0]);
         }
-        throw new coding_exception('Unexpected method call: ' . $method);
+        throw new \coding_exception('Unexpected method call: ' . $method);
     }
 
 }
diff --git a/completion/classes/bulkedit_form.php b/completion/classes/bulkedit_form.php
new file mode 100644 (file)
index 0000000..a67cce7
--- /dev/null
@@ -0,0 +1,158 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Bulk edit activity completion form
+ *
+ * @package     core_completion
+ * @copyright   2017 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Bulk edit activity completion form
+ *
+ * @package     core_completion
+ * @copyright   2017 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_completion_bulkedit_form extends core_completion_edit_base_form {
+    /** @var cm_info[] list of selected course modules */
+    protected $cms = [];
+    /** @var array Do not use directly, call $this->get_module_names() */
+    protected $_modnames = null;
+
+    /**
+     * Returns list of types of selected modules
+     *
+     * @return array modname=>modfullname
+     */
+    protected function get_module_names() {
+        if ($this->_modnames !== null) {
+            return $this->_modnames;
+        }
+        $this->_modnames = [];
+        foreach ($this->cms as $cm) {
+            $this->_modnames[$cm->modname] = $cm->modfullname;
+        }
+        return $this->_modnames;
+    }
+
+    /**
+     * Returns an instance of component-specific module form for the first selected module
+     *
+     * @return moodleform_mod|null
+     */
+    protected function get_module_form() {
+        global $CFG, $PAGE;
+
+        if ($this->_moduleform) {
+            return $this->_moduleform;
+        }
+
+        $cm = reset($this->cms);
+        $course = $this->course;
+        $modname = $cm->modname;
+
+        $modmoodleform = "$CFG->dirroot/mod/$modname/mod_form.php";
+        if (file_exists($modmoodleform)) {
+            require_once($modmoodleform);
+        } else {
+            print_error('noformdesc');
+        }
+
+        list($cmrec, $context, $module, $data, $cw) = get_moduleinfo_data($cm, $course);
+        $data->return = 0;
+        $data->sr = 0;
+        $data->update = $modname;
+
+        // Initialise the form but discard all JS requirements it adds, our form has already added them.
+        $mformclassname = 'mod_'.$modname.'_mod_form';
+        $PAGE->start_collecting_javascript_requirements();
+        $this->_moduleform = new $mformclassname($data, 0, $cmrec, $course);
+        $PAGE->end_collecting_javascript_requirements();
+
+        return $this->_moduleform;
+    }
+
+    /**
+     * Form definition
+     */
+    public function definition() {
+        $this->cms = $this->_customdata['cms'];
+        $cm = reset($this->cms); // First selected course module.
+        $this->course = $cm->get_course();
+
+        $mform = $this->_form;
+
+        $idx = 0;
+        foreach ($this->cms as $cm) {
+            $mform->addElement('hidden', 'cmid['.$idx.']', $cm->id);
+            $mform->setType('cmid['.$idx.']', PARAM_INT);
+            $idx++;
+        }
+
+        parent::definition();
+
+        $modform = $this->get_module_form();
+        if ($modform) {
+            // Pre-fill the form with the current completion rules of the first selected module.
+            list($cmrec, $context, $module, $data, $cw) = get_moduleinfo_data($cm->get_course_module_record(), $this->course);
+            $data = (array)$data;
+            $modform->data_preprocessing($data);
+            // Unset fields that will conflict with this form and set data to this form.
+            unset($data['cmid']);
+            unset($data['id']);
+            $this->set_data($data);
+        }
+    }
+
+    /**
+     * Form validation
+     *
+     * @param array $data array of ("fieldname"=>value) of submitted data
+     * @param array $files array of uploaded files "element_name"=>tmp_file_path
+     * @return array of "element_name"=>"error_description" if there are errors,
+     *         or an empty array if everything is OK (true allowed for backwards compatibility too).
+     */
+    public function validation($data, $files) {
+        global $CFG;
+        $errors = parent::validation($data, $files);
+
+        // Completion: Don't let them choose automatic completion without turning
+        // on some conditions.
+        if (array_key_exists('completion', $data) &&
+                $data['completion'] == COMPLETION_TRACKING_AUTOMATIC && !empty($data['completionusegrade'])) {
+            require_once($CFG->libdir.'/gradelib.php');
+            $moduleswithoutgradeitem = [];
+            foreach ($this->cms as $cm) {
+                $item = grade_item::fetch(array('courseid' => $cm->course, 'itemtype' => 'mod',
+                    'itemmodule' => $cm->modname, 'iteminstance' => $cm->instance,
+                    'itemnumber' => 0));
+                if (!$item) {
+                    $moduleswithoutgradeitem[] = $cm->get_formatted_name();
+                }
+            }
+            if ($moduleswithoutgradeitem) {
+                $errors['completionusegrade'] = get_string('nogradeitem', 'completion', join(', ', $moduleswithoutgradeitem));
+            }
+        }
+
+        return $errors;
+    }
+}
\ No newline at end of file
diff --git a/completion/classes/defaultedit_form.php b/completion/classes/defaultedit_form.php
new file mode 100644 (file)
index 0000000..71ce1e1
--- /dev/null
@@ -0,0 +1,124 @@
+<?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/>.
+
+/**
+ * Default activity completion form
+ *
+ * @package     core_completion
+ * @copyright   2017 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Default activity completion form
+ *
+ * @package     core_completion
+ * @copyright   2017 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_completion_defaultedit_form extends core_completion_edit_base_form {
+    /** @var array */
+    protected $modules;
+    /** @var array */
+    protected $_modnames;
+
+    /**
+     * Returns list of types of selected modules
+     *
+     * @return array modname=>modfullname
+     */
+    protected function get_module_names() {
+        if ($this->_modnames !== null) {
+            return $this->_modnames;
+        }
+        $this->_modnames = [];
+        foreach ($this->modules as $module) {
+            $this->_modnames[$module->name] = $module->formattedname;
+        }
+        return $this->_modnames;
+    }
+
+    /**
+     * Returns an instance of component-specific module form for the first selected module
+     *
+     * @return moodleform_mod|null
+     */
+    protected function get_module_form() {
+        global $CFG, $PAGE;
+
+        if ($this->_moduleform) {
+            return $this->_moduleform;
+        }
+
+        $modnames = array_keys($this->get_module_names());
+        $modname = $modnames[0];
+        $course = $this->course;
+
+        $modmoodleform = "$CFG->dirroot/mod/$modname/mod_form.php";
+        if (file_exists($modmoodleform)) {
+            require_once($modmoodleform);
+        } else {
+            print_error('noformdesc');
+        }
+
+        list($module, $context, $cw, $cmrec, $data) = prepare_new_moduleinfo_data($course, $modname, 0);
+        $data->return = 0;
+        $data->sr = 0;
+        $data->add = $modname;
+
+        // Initialise the form but discard all JS requirements it adds, our form has already added them.
+        $mformclassname = 'mod_'.$modname.'_mod_form';
+        $PAGE->start_collecting_javascript_requirements();
+        $this->_moduleform = new $mformclassname($data, 0, $cmrec, $course);
+        $PAGE->end_collecting_javascript_requirements();
+
+        return $this->_moduleform;
+    }
+
+    /**
+     * Form definition,
+     */
+    public function definition() {
+        $this->course = $this->_customdata['course'];
+        $this->modules = $this->_customdata['modules'];
+
+        $mform = $this->_form;
+
+        foreach ($this->modules as $modid => $module) {
+            $mform->addElement('hidden', 'modids['.$modid.']', $modid);
+            $mform->setType('modids['.$modid.']', PARAM_INT);
+        }
+
+        parent::definition();
+
+        $modform = $this->get_module_form();
+        if ($modform) {
+            $modnames = array_keys($this->get_module_names());
+            $modname = $modnames[0];
+            // Pre-fill the form with the current completion rules of the first selected module type.
+            list($module, $context, $cw, $cmrec, $data) = prepare_new_moduleinfo_data($this->course, $modname, 0);
+            $data = (array)$data;
+            $modform->data_preprocessing($data);
+            // Unset fields that will conflict with this form and set data to this form.
+            unset($data['cmid']);
+            unset($data['modids']);
+            unset($data['id']);
+            $this->set_data($data);
+        }
+    }
+}
\ No newline at end of file
diff --git a/completion/classes/edit_base_form.php b/completion/classes/edit_base_form.php
new file mode 100644 (file)
index 0000000..676d712
--- /dev/null
@@ -0,0 +1,282 @@
+<?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/>.
+
+/**
+ * Base form for changing completion rules
+ *
+ * @package     core_completion
+ * @copyright   2017 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->libdir.'/completionlib.php');
+require_once($CFG->dirroot.'/course/modlib.php');
+
+/**
+ * Base form for changing completion rules. Used in bulk editing activity completion and editing default activity completion
+ *
+ * @package     core_completion
+ * @copyright   2017 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class core_completion_edit_base_form extends moodleform {
+    /** @var moodleform_mod Do not use directly, call $this->get_module_form() */
+    protected $_moduleform = null;
+    /** @var bool */
+    protected $hascustomrules = false;
+    /** @var stdClass */
+    protected $course;
+
+    /**
+     * Returns list of types of selected module types
+     *
+     * @return array modname=>modfullname
+     */
+    abstract protected function get_module_names();
+
+    /**
+     * Returns true if all selected modules support tracking view.
+     *
+     * @return bool
+     */
+    protected function support_views() {
+        foreach ($this->get_module_names() as $modname => $modfullname) {
+            if (!plugin_supports('mod', $modname, FEATURE_COMPLETION_TRACKS_VIEWS, false)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns true if all selected modules support grading.
+     *
+     * @return bool
+     */
+    protected function support_grades() {
+        foreach ($this->get_module_names() as $modname => $modfullname) {
+            if (!plugin_supports('mod', $modname, FEATURE_GRADE_HAS_GRADE, false)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns an instance of component-specific module form for the first selected module
+     *
+     * @return moodleform_mod|null
+     */
+    abstract protected function get_module_form();
+
+    /**
+     * If all selected modules are of the same module type, adds custom completion rules from this module type
+     *
+     * @return array
+     */
+    protected function add_custom_completion_rules() {
+        $modnames = array_keys($this->get_module_names());
+        if (count($modnames) != 1 || !plugin_supports('mod', $modnames[0], FEATURE_COMPLETION_HAS_RULES, false)) {
+            return [];
+        }
+
+        try {
+            // Add completion rules from the module form to this form.
+            $moduleform = $this->get_module_form();
+            $moduleform->_form = $this->_form;
+            if ($customcompletionelements = $moduleform->add_completion_rules()) {
+                $this->hascustomrules = true;
+            }
+            return $customcompletionelements;
+        } catch (Exception $e) {
+            debugging('Could not add custom completion rule of module ' . $modnames[0] .
+                ' to this form, this has to be fixed by the developer', DEBUG_DEVELOPER);
+            return [];
+        }
+    }
+
+    /**
+     * Checks if at least one of the custom completion rules is enabled
+     *
+     * @param array $data Input data (not yet validated)
+     * @return bool True if one or more rules is enabled, false if none are;
+     *   default returns false
+     */
+    protected function completion_rule_enabled($data) {
+        if ($this->hascustomrules) {
+            return $this->get_module_form()->completion_rule_enabled($data);
+        }
+        return false;
+    }
+
+    /**
+     * Returns list of modules that have automatic completion rules that are not shown on this form
+     * (because they are not present in at least one other selected module).
+     *
+     * @return array
+     */
+    protected function get_modules_with_hidden_rules() {
+        $modnames = $this->get_module_names();
+        if (count($modnames) <= 1) {
+            // No rules definitions conflicts if there is only one module type.
+            return [];
+        }
+
+        $conflicts = [];
+
+        if (!$this->support_views()) {
+            // If we don't display views rule but at least one module supports it - we have conflicts.
+            foreach ($modnames as $modname => $modfullname) {
+                if (empty($conflicts[$modname]) && plugin_supports('mod', $modname, FEATURE_COMPLETION_TRACKS_VIEWS, false)) {
+                    $conflicts[$modname] = $modfullname;
+                }
+            }
+        }
+
+        if (!$this->support_grades()) {
+            // If we don't display grade rule but at least one module supports it - we have conflicts.
+            foreach ($modnames as $modname => $modfullname) {
+                if (empty($conflicts[$modname]) && plugin_supports('mod', $modname, FEATURE_GRADE_HAS_GRADE, false)) {
+                    $conflicts[$modname] = $modfullname;
+                }
+            }
+        }
+
+        foreach ($modnames as $modname => $modfullname) {
+            // We do not display any custom completion rules, find modules that define them and add to conflicts list.
+            if (empty($conflicts[$modname]) && plugin_supports('mod', $modname, FEATURE_COMPLETION_HAS_RULES, false)) {
+                $conflicts[$modname] = $modfullname;
+            }
+        }
+
+        return $conflicts;
+    }
+
+    /**
+     * Form definition
+     */
+    public function definition() {
+        $mform = $this->_form;
+
+        // Course id.
+        $mform->addElement('hidden', 'id', $this->course->id);
+        $mform->setType('id', PARAM_INT);
+
+        // Unlock completion automatically (this element can be used in validation).
+        $mform->addElement('hidden', 'completionunlocked', 1);
+        $mform->setType('completionunlocked', PARAM_INT);
+
+        $mform->addElement('select', 'completion', get_string('completion', 'completion'),
+            array(COMPLETION_TRACKING_NONE => get_string('completion_none', 'completion'),
+                COMPLETION_TRACKING_MANUAL => get_string('completion_manual', 'completion')));
+        $mform->addHelpButton('completion', 'completion', 'completion');
+        $mform->setDefault('completion', COMPLETION_TRACKING_NONE);
+
+        // Automatic completion once you view it.
+        $autocompletionpossible = false;
+        if ($this->support_views()) {
+            $mform->addElement('advcheckbox', 'completionview', get_string('completionview', 'completion'),
+                get_string('completionview_desc', 'completion'));
+            $mform->disabledIf('completionview', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC);
+            $autocompletionpossible = true;
+        }
+
+        // Automatic completion once it's graded.
+        if ($this->support_grades()) {
+            $mform->addElement('advcheckbox', 'completionusegrade', get_string('completionusegrade', 'completion'),
+                get_string('completionusegrade_desc', 'completion'));
+            $mform->disabledIf('completionusegrade', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC);
+            $mform->addHelpButton('completionusegrade', 'completionusegrade', 'completion');
+            $autocompletionpossible = true;
+        }
+
+        // Automatic completion according to module-specific rules.
+        foreach ($this->add_custom_completion_rules() as $element) {
+            $mform->disabledIf($element, 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC);
+            $autocompletionpossible = true;
+        }
+
+        // Automatic option only appears if possible.
+        if ($autocompletionpossible) {
+            $mform->getElement('completion')->addOption(
+                get_string('completion_automatic', 'completion'),
+                COMPLETION_TRACKING_AUTOMATIC);
+        }
+
+        // Completion expected at particular date? (For progress tracking).
+        $mform->addElement('date_selector', 'completionexpected',
+            get_string('completionexpected', 'completion'), ['optional' => true]);
+        $mform->addHelpButton('completionexpected', 'completionexpected', 'completion');
+        $mform->disabledIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE);
+
+        if ($conflicts = $this->get_modules_with_hidden_rules()) {
+            $mform->addElement('static', 'qwerty', '', get_string('hiddenrules', 'completion', join(', ', $conflicts)));
+        }
+
+        $this->add_action_buttons();
+    }
+
+    /**
+     * Form validation
+     *
+     * @param array $data array of ("fieldname"=>value) of submitted data
+     * @param array $files array of uploaded files "element_name"=>tmp_file_path
+     * @return array of "element_name"=>"error_description" if there are errors,
+     *         or an empty array if everything is OK (true allowed for backwards compatibility too).
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        // Completion: Don't let them choose automatic completion without turning
+        // on some conditions.
+        if (array_key_exists('completion', $data) &&
+            $data['completion'] == COMPLETION_TRACKING_AUTOMATIC) {
+            if (empty($data['completionview']) && empty($data['completionusegrade']) &&
+                !$this->completion_rule_enabled($data)) {
+                $errors['completion'] = get_string('badautocompletion', 'completion');
+            }
+        }
+
+        return $errors;
+    }
+
+    /**
+     * Returns if this form has custom completion rules. This is only possible if all selected modules have the same
+     * module type and this module type supports custom completion rules
+     *
+     * @return bool
+     */
+    public function has_custom_completion_rules() {
+        return $this->hascustomrules;
+    }
+
+    /**
+     * Return submitted data if properly submitted or returns NULL if validation fails or
+     * if there is no submitted data.
+     *
+     * @return object submitted data; NULL if not valid or not submitted or cancelled
+     */
+    public function get_data() {
+        $data = parent::get_data();
+        if ($data && $this->hascustomrules) {
+            $this->get_module_form()->data_postprocessing($data);
+        }
+        return $data;
+    }
+}
\ No newline at end of file
diff --git a/completion/classes/manager.php b/completion/classes/manager.php
new file mode 100644 (file)
index 0000000..543978d
--- /dev/null
@@ -0,0 +1,479 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Bulk activity completion manager class
+ *
+ * @package     core_completion
+ * @category    completion
+ * @copyright   2017 Adrian Greeve
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_completion;
+
+use stdClass;
+use context_course;
+use cm_info;
+use tabobject;
+use lang_string;
+use moodle_url;
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Bulk activity completion manager class
+ *
+ * @package     core_completion
+ * @category    completion
+ * @copyright   2017 Adrian Greeve
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager {
+
+    /**
+     * @var int $courseid the course id.
+     */
+    protected $courseid;
+
+    /**
+     * manager constructor.
+     * @param int $courseid the course id.
+     */
+    public function __construct($courseid) {
+        $this->courseid = $courseid;
+    }
+
+    /**
+     * Gets the data (context) to be used with the bulkactivitycompletion template.
+     *
+     * @return stdClass data for use with the bulkactivitycompletion template.
+     */
+    public function get_activities_and_headings() {
+        global $OUTPUT;
+        $moduleinfo = get_fast_modinfo($this->courseid);
+        $sections = $moduleinfo->get_sections();
+        $data = new stdClass;
+        $data->courseid = $this->courseid;
+        $data->sesskey = sesskey();
+        $data->helpicon = $OUTPUT->help_icon('bulkcompletiontracking', 'core_completion');
+        $data->sections = [];
+        foreach ($sections as $sectionnumber => $section) {
+            $sectioninfo = $moduleinfo->get_section_info($sectionnumber);
+
+            $sectionobject = new stdClass();
+            $sectionobject->sectionnumber = $sectionnumber;
+            $sectionobject->name = get_section_name($this->courseid, $sectioninfo);
+            $sectionobject->activities = $this->get_activities($section, true);
+            $data->sections[] = $sectionobject;
+        }
+        return $data;
+    }
+
+    /**
+     * Gets the data (context) to be used with the activityinstance template
+     *
+     * @param array $cmids list of course module ids
+     * @param bool $withcompletiondetails include completion details
+     * @return array
+     */
+    public function get_activities($cmids, $withcompletiondetails = false) {
+        $moduleinfo = get_fast_modinfo($this->courseid);
+        $activities = [];
+        foreach ($cmids as $cmid) {
+            $mod = $moduleinfo->get_cm($cmid);
+            if (!$mod->uservisible) {
+                continue;
+            }
+            $moduleobject = new stdClass();
+            $moduleobject->cmid = $cmid;
+            $moduleobject->modname = $mod->get_formatted_name();
+            $moduleobject->icon = $mod->get_icon_url()->out();
+            $moduleobject->url = $mod->url;
+            $moduleobject->canmanage = $withcompletiondetails && self::can_edit_bulk_completion($this->courseid, $mod);
+
+            // Get activity completion information.
+            if ($moduleobject->canmanage) {
+                $moduleobject->completionstatus = $this->get_completion_detail($mod);
+            } else {
+                $moduleobject->completionstatus = ['icon' => null, 'string' => null];
+            }
+            if (self::can_edit_bulk_completion($this->courseid, $mod)) {
+                $activities[] = $moduleobject;
+            }
+        }
+        return $activities;
+    }
+
+
+    /**
+     * Get completion information on the selected module or module type
+     *
+     * @param cm_info|stdClass $mod either instance of cm_info (with 'customcompletionrules' in customdata) or
+     *      object with fields ->completion, ->completionview, ->completionexpected, ->completionusegrade
+     *      and ->customdata['customcompletionrules']
+     * @return array
+     */
+    private function get_completion_detail($mod) {
+        global $OUTPUT;
+        $strings = [];
+        switch ($mod->completion) {
+            case COMPLETION_TRACKING_NONE:
+                $strings['string'] = get_string('none');
+                break;
+
+            case COMPLETION_TRACKING_MANUAL:
+                $strings['string'] = get_string('manual', 'completion');
+                $strings['icon'] = $OUTPUT->pix_icon('i/completion-manual-y', get_string('completion_manual', 'completion'));
+                break;
+
+            case COMPLETION_TRACKING_AUTOMATIC:
+                $strings['string'] = get_string('withconditions', 'completion');
+
+                // Get the descriptions for all the active completion rules for the module.
+                if ($ruledescriptions = $this->get_completion_active_rule_descriptions($mod)) {
+                    foreach ($ruledescriptions as $ruledescription) {
+                        $strings['string'] .= \html_writer::empty_tag('br') . $ruledescription;
+                    }
+                }
+
+                $strings['icon'] = $OUTPUT->pix_icon('i/completion-auto-y', get_string('completion_automatic', 'completion'));
+                break;
+
+            default:
+                $strings['string'] = get_string('none');
+                break;
+        }
+        return $strings;
+    }
+
+    /**
+     * Get the descriptions for all active conditional completion rules for the current module.
+     *
+     * @param cm_info|stdClass $moduledata either instance of cm_info (with 'customcompletionrules' in customdata) or
+     *      object with fields ->completion, ->completionview, ->completionexpected, ->completionusegrade
+     *      and ->customdata['customcompletionrules']
+     * @return array $activeruledescriptions an array of strings describing the active completion rules.
+     */
+    protected function get_completion_active_rule_descriptions($moduledata) {
+        $activeruledescriptions = [];
+
+        // Generate the description strings for the core conditional completion rules (if set).
+        if (!empty($moduledata->completionview)) {
+            $activeruledescriptions[] = get_string('completionview_desc', 'completion');
+        }
+        if ($moduledata instanceof cm_info && !is_null($moduledata->completiongradeitemnumber) ||
+            ($moduledata instanceof stdClass && !empty($moduledata->completionusegrade))) {
+            $activeruledescriptions[] = get_string('completionusegrade_desc', 'completion');
+        }
+
+        // Now, ask the module to provide descriptions for its custom conditional completion rules.
+        if ($customruledescriptions = component_callback($moduledata->modname,
+                'get_completion_active_rule_descriptions', [$moduledata])) {
+            $activeruledescriptions = array_merge($activeruledescriptions, $customruledescriptions);
+        }
+
+        if (!empty($moduledata->completionexpected)) {
+            $activeruledescriptions[] = get_string('completionexpecteddesc', 'completion',
+                userdate($moduledata->completionexpected));
+        }
+
+        return $activeruledescriptions;
+    }
+
+    /**
+     * Gets the course modules for the current course.
+     *
+     * @return stdClass $data containing the modules
+     */
+    public function get_activities_and_resources() {
+        global $DB, $OUTPUT, $CFG;
+        require_once($CFG->dirroot.'/course/lib.php');
+
+        // Get enabled activities and resources.
+        $modules = $DB->get_records('modules', ['visible' => 1], 'name ASC');
+        $data = new stdClass();
+        $data->courseid = $this->courseid;
+        $data->sesskey = sesskey();
+        $data->helpicon = $OUTPUT->help_icon('bulkcompletiontracking', 'core_completion');
+        // Add icon information.
+        $data->modules = array_values($modules);
+        $coursecontext = context_course::instance($this->courseid);
+        $canmanage = has_capability('moodle/course:manageactivities', $coursecontext);
+        $course = get_course($this->courseid);
+        foreach ($data->modules as $module) {
+            $module->icon = $OUTPUT->image_url('icon', $module->name)->out();
+            $module->formattedname = format_string(get_string('modulenameplural', 'mod_' . $module->name),
+                true, ['context' => $coursecontext]);
+            $module->canmanage = $canmanage && course_allowed_module($course, $module->name);
+            $defaults = self::get_default_completion($course, $module, false);
+            $defaults->modname = $module->name;
+            $module->completionstatus = $this->get_completion_detail($defaults);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Checks if current user can edit activity completion
+     *
+     * @param int|stdClass $courseorid
+     * @param \cm_info|null $cm if specified capability for a given coursemodule will be check,
+     *     if not specified capability to edit at least one activity is checked.
+     */
+    public static function can_edit_bulk_completion($courseorid, $cm = null) {
+        if ($cm) {
+            return $cm->uservisible && has_capability('moodle/course:manageactivities', $cm->context);
+        }
+        $coursecontext = context_course::instance(is_object($courseorid) ? $courseorid->id : $courseorid);
+        if (has_capability('moodle/course:manageactivities', $coursecontext)) {
+            return true;
+        }
+        $modinfo = get_fast_modinfo($courseorid);
+        foreach ($modinfo->cms as $mod) {
+            if ($mod->uservisible && has_capability('moodle/course:manageactivities', $mod->context)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Gets the available completion tabs for the current course and user.
+     *
+     * @param stdClass|int $courseorid the course object or id.
+     * @return tabobject[]
+     */
+    public static function get_available_completion_tabs($courseorid) {
+        $tabs = [];
+
+        $courseid = is_object($courseorid) ? $courseorid->id : $courseorid;
+        $coursecontext = context_course::instance($courseid);
+
+        if (has_capability('moodle/course:update', $coursecontext)) {
+            $tabs[] = new tabobject(
+                'completion',
+                new moodle_url('/course/completion.php', ['id' => $courseid]),
+                new lang_string('coursecompletion', 'completion')
+            );
+        }
+
+        if (has_capability('moodle/course:manageactivities', $coursecontext)) {
+            $tabs[] = new tabobject(
+                'defaultcompletion',
+                new moodle_url('/course/defaultcompletion.php', ['id' => $courseid]),
+                new lang_string('defaultcompletion', 'completion')
+            );
+        }
+
+        if (self::can_edit_bulk_completion($courseorid)) {
+            $tabs[] = new tabobject(
+                'bulkcompletion',
+                new moodle_url('/course/bulkcompletion.php', ['id' => $courseid]),
+                new lang_string('bulkactivitycompletion', 'completion')
+            );
+        }
+
+        return $tabs;
+    }
+
+    /**
+     * Applies completion from the bulk edit form to all selected modules
+     *
+     * @param stdClass $data data received from the core_completion_bulkedit_form
+     * @param bool $updateinstances if we need to update the instance tables of the module (i.e. 'assign', 'forum', etc.) -
+     *      if no module-specific completion rules were added to the form, update of the module table is not needed.
+     */
+    public function apply_completion($data, $updateinstances) {
+        $updated = [];
+        $modinfo = get_fast_modinfo($this->courseid);
+
+        $cmids = $data->cmid;
+
+        $data = (array)$data;
+        unset($data['id']); // This is a course id, we don't want to confuse it with cmid or instance id.
+        unset($data['cmid']);
+        unset($data['submitbutton']);
+
+        foreach ($cmids as $cmid) {
+            $cm = $modinfo->get_cm($cmid);
+            if (self::can_edit_bulk_completion($this->courseid, $cm) && $this->apply_completion_cm($cm, $data, $updateinstances)) {
+                $updated[] = $cm->id;
+            }
+        }
+        if ($updated) {
+            // Now that modules are fully updated, also update completion data if required.
+            // This will wipe all user completion data and recalculate it.
+            rebuild_course_cache($this->courseid, true);
+            $modinfo = get_fast_modinfo($this->courseid);
+            $completion = new \completion_info($modinfo->get_course());
+            foreach ($updated as $cmid) {
+                $completion->reset_all_state($modinfo->get_cm($cmid));
+            }
+
+            // And notify the user of the result.
+            \core\notification::add(get_string('activitycompletionupdated', 'core_completion'), \core\notification::SUCCESS);
+        }
+    }
+
+    /**
+     * Applies new completion rules to one course module
+     *
+     * @param \cm_info $cm
+     * @param array $data
+     * @param bool $updateinstance if we need to update the instance table of the module (i.e. 'assign', 'forum', etc.) -
+     *      if no module-specific completion rules were added to the form, update of the module table is not needed.
+     * @return bool if module was updated
+     */
+    protected function apply_completion_cm(\cm_info $cm, $data, $updateinstance) {
+        global $DB;
+
+        $defaults = ['completion' => COMPLETION_DISABLED, 'completionview' => COMPLETION_VIEW_NOT_REQUIRED,
+            'completionexpected' => 0, 'completiongradeitemnumber' => null];
+
+        if ($cm->completion == $data['completion'] && $cm->completion != COMPLETION_TRACKING_AUTOMATIC) {
+            // If old and new completion are either both "manual" or both "none" - no changes are needed.
+            return false;
+        }
+
+        $data += ['completion' => $cm->completion,
+            'completionexpected' => $cm->completionexpected,
+            'completionview' => $cm->completionview];
+
+        if (array_key_exists('completionusegrade', $data)) {
+            // Convert the 'use grade' checkbox into a grade-item number: 0 if checked, null if not.
+            $data['completiongradeitemnumber'] = !empty($data['completionusegrade']) ? 0 : null;
+            unset($data['completionusegrade']);
+        } else {
+            $data['completiongradeitemnumber'] = $cm->completiongradeitemnumber;
+        }
+
+        // Update module instance table.
+        if ($updateinstance) {
+            $moddata = ['id' => $cm->instance, 'timemodified' => time()] + array_diff_key($data, $defaults);
+            $DB->update_record($cm->modname, $moddata);
+        }
+
+        // Update course modules table.
+        $cmdata = ['id' => $cm->id, 'timemodified' => time()] + array_intersect_key($data, $defaults);
+        $DB->update_record('course_modules', $cmdata);
+
+        \core\event\course_module_updated::create_from_cm($cm, $cm->context)->trigger();
+
+        return true;
+    }
+
+
+    /**
+     * Saves default completion from edit form to all selected module types
+     *
+     * @param stdClass $data data received from the core_completion_bulkedit_form
+     * @param bool $updatecustomrules if we need to update the custom rules of the module -
+     *      if no module-specific completion rules were added to the form, update of the module table is not needed.
+     */
+    public function apply_default_completion($data, $updatecustomrules) {
+        global $DB;
+
+        $courseid = $data->id;
+        $coursecontext = context_course::instance($courseid);
+        if (!$modids = $data->modids) {
+            return;
+        }
+        $defaults = [
+            'completion' => COMPLETION_DISABLED,
+            'completionview' => COMPLETION_VIEW_NOT_REQUIRED,
+            'completionexpected' => 0,
+            'completionusegrade' => 0
+        ];
+
+        $data = (array)$data;
+
+        if ($updatecustomrules) {
+            $customdata = array_diff_key($data, $defaults);
+            $data['customrules'] = $customdata ? json_encode($customdata) : null;
+            $defaults['customrules'] = null;
+        }
+        $data = array_intersect_key($data, $defaults);
+
+        // Get names of the affected modules.
+        list($modidssql, $params) = $DB->get_in_or_equal($modids);
+        $params[] = 1;
+        $modules = $DB->get_records_select_menu('modules', 'id ' . $modidssql . ' and visible = ?', $params, '', 'id, name');
+
+        // Get an associative array of [module_id => course_completion_defaults_id].
+        list($in, $params) = $DB->get_in_or_equal($modids);
+        $params[] = $courseid;
+        $defaultsids = $DB->get_records_select_menu('course_completion_defaults', 'module ' . $in . ' and course = ?', $params, '',
+                                                      'module, id');
+
+        foreach ($modids as $modid) {
+            if (!array_key_exists($modid, $modules)) {
+                continue;
+            }
+            if (isset($defaultsids[$modid])) {
+                $DB->update_record('course_completion_defaults', $data + ['id' => $defaultsids[$modid]]);
+            } else {
+                $defaultsids[$modid] = $DB->insert_record('course_completion_defaults', $data + ['course' => $courseid,
+                                                                                                 'module' => $modid]);
+            }
+            // Trigger event.
+            \core\event\completion_defaults_updated::create([
+                'objectid' => $defaultsids[$modid],
+                'context' => $coursecontext,
+                'other' => ['modulename' => $modules[$modid]],
+            ])->trigger();
+        }
+
+        // Add notification.
+        \core\notification::add(get_string('defaultcompletionupdated', 'completion'), \core\notification::SUCCESS);
+    }
+
+    /**
+     * Returns default completion rules for given module type in the given course
+     *
+     * @param stdClass $course
+     * @param stdClass $module
+     * @param bool $flatten if true all module custom completion rules become properties of the same object,
+     *   otherwise they can be found as array in ->customdata['customcompletionrules']
+     * @return stdClass
+     */
+    public static function get_default_completion($course, $module, $flatten = true) {
+        global $DB, $CFG;
+        if ($data = $DB->get_record('course_completion_defaults', ['course' => $course->id, 'module' => $module->id],
+            'completion, completionview, completionexpected, completionusegrade, customrules')) {
+            if ($data->customrules && ($customrules = @json_decode($data->customrules, true))) {
+                if ($flatten) {
+                    foreach ($customrules as $key => $value) {
+                        $data->$key = $value;
+                    }
+                } else {
+                    $data->customdata['customcompletionrules'] = $customrules;
+                }
+            }
+            unset($data->customrules);
+        } else {
+            $data = new stdClass();
+            $data->completion = COMPLETION_TRACKING_NONE;
+            if ($CFG->completiondefault) {
+                $completion = new \completion_info(get_fast_modinfo($course->id)->get_course());
+                if ($completion->is_enabled() && plugin_supports('mod', $module->name, FEATURE_MODEDIT_DEFAULT_COMPLETION, true)) {
+                    $data->completion = COMPLETION_TRACKING_MANUAL;
+                    $data->completionview = 1;
+                }
+            }
+        }
+        return $data;
+    }
+}
\ No newline at end of file
diff --git a/completion/tests/behat/bulk_edit_activity_completion.feature b/completion/tests/behat/bulk_edit_activity_completion.feature
new file mode 100644 (file)
index 0000000..2ef1ea2
--- /dev/null
@@ -0,0 +1,60 @@
+@core @core_completion
+Feature: Allow teachers to bulk edit activity completion rules in a course.
+  In order to avoid editing single activities
+  As a teacher
+  I need to be able to edit the completion rules for a group of activities.
+
+  # Given I am a teacher in a course with completion tracking enabled and activities present.
+  # When I bulk edit activity completion rules for activities of the same kind.
+  # Then the completion rules should be updated for all selected activities.
+  @javascript
+  Scenario: Bulk edit activity completion rules
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | Frist | teacher1@example.com |
+      | student1 | Student | First | student1@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+    And the following "activities" exist:
+      | activity | course | idnumber | name | intro | grade |
+      | assign | C1 | a1 | Test assignment one | Submit something! | 300 |
+      | assign | C1 | a2 | Test assignment two | Submit something! | 100 |
+      | assign | C1 | a3 | Test assignment three | Submit something! | 150 |
+      | assign | C1 | a4 | Test assignment four | Submit nothing! | 150 |
+    And I log in as "teacher1"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | Enable completion tracking | Yes |
+    And I press "Save and display"
+    When I navigate to "Course completion" in current page administration
+    And I follow "Bulk edit activity completion"
+    And I click on "Test assignment one" "checkbox"
+    And I click on "Test assignment two" "checkbox"
+    And I click on "Edit" "button"
+    And I should see "Completion tracking"
+    And I should see "The changes will affect the following 2 Activities/Resources"
+    And I should see "Student must submit to this activity to complete it"
+    And I select "Show activity as complete when conditions are met" from the "completion" singleselect
+    And I click on "completionview" "checkbox"
+    And I click on "completionusegrade" "checkbox"
+    And I click on "completionsubmit" "checkbox"
+    And I click on "Save changes" "button"
+    Then I should see "Changes saved"
+    And I should see "With conditions" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element"
+    And I should see "Student must view this activity to complete it" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element"
+    And I should see "Student must receive a grade to complete this activity" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element"
+    And I should see "Student must submit to this activity to complete it" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element"
+    And I should not see "Completion expected on" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element"
+    And I should see "With conditions" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element"
+    And I should see "Student must view this activity to complete it" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element"
+    And I should see "Student must receive a grade to complete this activity" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element"
+    And I should see "Student must submit to this activity to complete it" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element"
+    And I should not see "Completion expected on" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element"
diff --git a/completion/tests/behat/default_activity_completion.feature b/completion/tests/behat/default_activity_completion.feature
new file mode 100644 (file)
index 0000000..d414560
--- /dev/null
@@ -0,0 +1,52 @@
+@core @core_completion
+Feature: Allow teachers to edit the default activity completion rules in a course.
+  In order to set the activity completion defaults for new activities
+  As a teacher
+  I need to be able to edit the completion rules for a group of activities.
+
+  # Given I am a teacher in a course with completion tracking enabled and activities present.
+  # When I edit activity completion defaults for activity types.
+  # Then the completion rule defaults should apply only to activities created from that point onwards.
+  @javascript
+  Scenario: Bulk edit activity completion default rules
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | Frist | teacher1@example.com |
+      | student1 | Student | First | student1@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+    And the following "activities" exist:
+      | activity | course | idnumber | name | intro | grade |
+      | assign | C1 | a1 | Test assignment one | Submit something! | 300 |
+    And I log in as "teacher1"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | Enable completion tracking | Yes |
+    And I press "Save and display"
+    When I navigate to "Course completion" in current page administration
+    And I follow "Default activity completion"
+    And I click on "Assignments" "checkbox"
+    And I click on "Edit" "button"
+    And I should see "Completion tracking"
+    And I should see "The changes will affect the following 1 Activities/Resources"
+    And I should see "Student must submit to this activity to complete it"
+    And I set the following fields to these values:
+      | completion | Show activity as complete when conditions are met|
+      | completionview | 1 |
+      | completionusegrade | 1 |
+      | completionsubmit | 1 |
+    And I click on "Save changes" "button"
+    Then I should see "Changes saved"
+    And I should see "With conditions" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element"
+    And I should see "Student must view this activity to complete it" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element"
+    And I should see "Student must receive a grade to complete this activity" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element"
+    And I should see "Student must submit to this activity to complete it" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element"
+    And I should not see "Completion expected on" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element"
diff --git a/completion/tests/bulk_update_test.php b/completion/tests/bulk_update_test.php
new file mode 100644 (file)
index 0000000..69dcd09
--- /dev/null
@@ -0,0 +1,269 @@
+<?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/>.
+
+/**
+ * External completion functions unit tests
+ *
+ * @package    core_completion
+ * @copyright  2017 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/completionlib.php');
+
+/**
+ * External completion functions unit tests
+ *
+ * @package    core_completion
+ * @copyright  2017 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_completion_bulk_update_testcase extends advanced_testcase {
+
+    /**
+     * Provider for test_bulk_form_submit_single
+     * @return array
+     */
+    public function bulk_form_submit_single_provider() {
+        return [
+            'assign-1' => ['assign', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionsubmit' => 1]],
+            'assign-2' => ['assign', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'book-1' => ['book', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]],
+            'book-2' => ['book', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'chat-1' => ['chat', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]],
+            'chat-2' => ['chat', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'choice-1' => ['choice', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionsubmit' => 1]],
+            'choice-2' => ['choice', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'data-1' => ['data', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]],
+            'data-2' => ['data', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'data-3' => ['data',
+                ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1, 'completionentries' => 3,
+                    'completionentriesenabled' => 1],
+                ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1, 'completionentries' => 3]],
+            'feedback-1' => ['feedback', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 0,
+                'completionsubmit' => 1]],
+            'feedback-2' => ['feedback', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'folder-1' => ['folder', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]],
+            'folder-2' => ['folder', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'forum-1' => ['forum',
+                ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completiondiscussions' => 1,
+                    'completiondiscussionsenabled' => 1],
+                ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completiondiscussions' => 1]],
+            'forum-2' => ['forum', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'glossary-1' => ['glossary',
+                ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1, 'completionentries' => 3,
+                    'completionentriesenabled' => 1],
+                ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1, 'completionentries' => 3]],
+            'glossary-2' => ['glossary', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'imscp-1' => ['imscp', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]],
+            'imscp-2' => ['imscp', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'label-1' => ['label', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'lesson-1' => ['lesson', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionendreached' => 1]],
+            'lesson-2' => ['lesson', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'lti-1' => ['lti', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]],
+            'lti-2' => ['lti', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'page-1' => ['page', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]],
+            'page-2' => ['page', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'quiz-1' => ['quiz', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionpass' => 1]],
+            'quiz-2' => ['quiz', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'resource-1' => ['resource', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]],
+            'resource-2' => ['resource', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'scorm-1' => ['scorm',
+                ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionscorerequired' => 1,
+                    'completionstatusrequired' => [2 => 'passed']],
+                ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionscorerequired' => 1,
+                    'completionstatusrequired' => 2]],
+            'scorm-2' => ['scorm', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'survey-1' => ['survey', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionsubmit' => 1]],
+            'survey-2' => ['survey', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'url-1' => ['url', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]],
+            'url-2' => ['url', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'wiki-1' => ['wiki', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]],
+            'wiki-2' => ['wiki', ['completion' => COMPLETION_TRACKING_MANUAL]],
+            'workshop-1' => ['workshop', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]],
+            'workshop-2' => ['workshop', ['completion' => COMPLETION_TRACKING_MANUAL]],
+        ];
+    }
+
+    /**
+     * Creates an instance of bulk edit completion form for one activity, validates and saves it
+     *
+     * @dataProvider bulk_form_submit_single_provider
+     * @param string $modname
+     * @param array $submitdata data to use in mock form submit
+     * @param array|null $validatedata data to validate the
+     */
+    public function test_bulk_form_submit_single($modname, $submitdata, $validatedata = null) {
+        global $DB;
+
+        if ($validatedata === null) {
+            $validatedata = $submitdata;
+        }
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        list($course, $cms) = $this->create_course_and_modules([$modname]);
+
+        // Submit the bulk completion form with the provided data and make sure it returns the same data.
+        core_completion_bulkedit_form::mock_submit(['id' => $course->id, 'cmid' => array_keys($cms)] + $submitdata, []);
+        $form = new core_completion_bulkedit_form(null, ['cms' => $cms]);
+        $this->assertTrue($form->is_validated());
+        $data = $form->get_data();
+        foreach ($validatedata as $key => $value) {
+            $this->assertEquals($value, $data->$key);
+        }
+
+        // Apply completion rules to the modules.
+        $manager = new core_completion\manager($course->id);
+        $manager->apply_completion($data, $form->has_custom_completion_rules());
+
+        // Make sure either course_modules or instance table was respectfully updated.
+        $cm = reset($cms);
+        $cmrec = $DB->get_record('course_modules', ['id' => $cm->id]);
+        $instancerec = $DB->get_record($modname, ['id' => $cm->instance]);
+        foreach ($validatedata as $key => $value) {
+            if (property_exists($cmrec, $key)) {
+                $this->assertEquals($value, $cmrec->$key);
+            } else {
+                $this->assertEquals($value, $instancerec->$key);
+            }
+        }
+    }
+
+    /**
+     * Creates a course and the number of modules
+     * @param array $modulenames
+     * @return array array of two elements - course and list of cm_info objects
+     */
+    protected function create_course_and_modules($modulenames) {
+        global $CFG, $PAGE;
+
+        $CFG->enablecompletion = true;
+        $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1], ['createsections' => true]);
+        $PAGE->set_course($course);
+
+        $cmids = [];
+        foreach ($modulenames as $modname) {
+            $module = $this->getDataGenerator()->create_module($modname, ['course' => $course->id]);
+            $cmids[] = $module->cmid;
+        }
+        $modinfo = get_fast_modinfo($course);
+        $cms = [];
+        foreach ($cmids as $cmid) {
+            $cms[$cmid] = $modinfo->get_cm($cmid);
+        }
+        return [$course, $cms];
+    }
+
+    /**
+     * Provider for test_bulk_form_submit_multiple
+     * @return array
+     */
+    public function bulk_form_submit_multiple_provider() {
+        return [
+            'Several modules with the same module type (choice)' => [
+                [
+                    'modulenames' => ['choice', 'choice', 'choice'],
+                    'submitdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionsubmit' => 1],
+                    'validatedata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionsubmit' => 1],
+                    'cmdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC],
+                    'instancedata' => [['completionsubmit' => 1], ['completionsubmit' => 1], ['completionsubmit' => 1]]
+                ]
+            ],
+            'Several modules with different module type' => [
+                [
+                    'modulenames' => ['choice', 'forum'],
+                    'submitdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1],
+                    'validatedata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1],
+                    'cmdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC],
+                    'instancedata' => null
+                ]
+            ],
+            'Setting manual completion (completionview shoud be ignored)' => [
+                [
+                    'modulenames' => ['scorm', 'forum', 'label', 'assign'],
+                    'submitdata' => ['completion' => COMPLETION_TRACKING_MANUAL, 'completionview' => 1],
+                    'validatedata' => [],
+                    'cmdata' => ['completion' => COMPLETION_TRACKING_MANUAL, 'completionview' => 0],
+                    'instancedata' => null
+                ]
+            ],
+            'If at least one module does not support completionsubmit it can\'t be set' => [
+                [
+                    'modulenames' => ['survey', 'wiki'],
+                    'submitdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1, 'completionsubmit' => 1],
+                    'validatedata' => [],
+                    'cmdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1],
+                    'instancedata' => [['completionsubmit' => 0], []]
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * Use bulk completion edit for updating multiple modules
+     *
+     * @dataProvider bulk_form_submit_multiple_provider
+     * @param array $providerdata
+     */
+    public function test_bulk_form_submit_multiple($providerdata) {
+        global $DB;
+
+        $modulenames = $providerdata['modulenames'];
+        $submitdata = $providerdata['submitdata'];
+        $validatedata = $providerdata['validatedata'];
+        $cmdata = $providerdata['cmdata'];
+        $instancedata = $providerdata['instancedata'];
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        list($course, $cms) = $this->create_course_and_modules($modulenames);
+
+        // Submit the bulk completion form with the provided data and make sure it returns the same data.
+        core_completion_bulkedit_form::mock_submit(['id' => $course->id, 'cmid' => array_keys($cms)] + $submitdata, []);
+        $form = new core_completion_bulkedit_form(null, ['cms' => $cms]);
+        $this->assertTrue($form->is_validated());
+        $data = $form->get_data();
+        foreach ($validatedata as $key => $value) {
+            $this->assertEquals($value, $data->$key);
+        }
+
+        // Apply completion rules to the modules.
+        $manager = new core_completion\manager($course->id);
+        $manager->apply_completion($data, $form->has_custom_completion_rules());
+
+        // Make sure either course_modules or instance table was respectfully updated.
+        $cnt = 0;
+        foreach ($cms as $cm) {
+            $cmrec = $DB->get_record('course_modules', ['id' => $cm->id]);
+            $instancerec = $DB->get_record($cm->modname, ['id' => $cm->instance]);
+            foreach ($cmdata as $key => $value) {
+                $this->assertEquals($value, $cmrec->$key, 'Error asserting that value for the field ' . $key.' ' .
+                    $cmrec->$key . ' matches expected value ' . $value);
+            }
+            if ($instancedata) {
+                foreach ($instancedata[$cnt] as $key => $value) {
+                    $this->assertEquals($value, $instancerec->$key, 'Error asserting that value for the field ' . $key . ' '.
+                        $instancerec->$key . ' matches expected value ' . $value);
+                }
+            }
+            $cnt++;
+        }
+    }
+}
diff --git a/course/bulkcompletion.php b/course/bulkcompletion.php
new file mode 100644 (file)
index 0000000..1343983
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Bulk activity completion selection
+ *
+ * @package     core_completion
+ * @category    completion
+ * @copyright   2017 Adrian Greeve
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__.'/../config.php');
+require_once($CFG->dirroot.'/course/lib.php');
+require_once($CFG->libdir.'/completionlib.php');
+
+$id = required_param('id', PARAM_INT); // Course id.
+$cmids = optional_param_array('cmid', [], PARAM_INT);
+
+// Perform some basic access control checks.
+if ($id) {
+
+    if ($id == SITEID) {
+        // Don't allow editing of 'site course' using this form.
+        print_error('cannoteditsiteform');
+    }
+
+    if (!$course = $DB->get_record('course', array('id' => $id))) {
+        print_error('invalidcourseid');
+    }
+    require_login($course);
+
+} else {
+    require_login();
+    print_error('needcourseid');
+}
+
+// Set up the page.
+navigation_node::override_active_url(new moodle_url('/course/completion.php', array('id' => $course->id)));
+$PAGE->set_course($course);
+$PAGE->set_url('/course/bulkcompletion.php', array('id' => $course->id));
+$PAGE->set_title($course->shortname);
+$PAGE->set_heading($course->fullname);
+$PAGE->set_pagelayout('admin');
+
+// Check access.
+if (!core_completion\manager::can_edit_bulk_completion($id)) {
+    require_capability('moodle/course:manageactivities', context_course::instance($course->id));
+}
+
+// Get all that stuff I need for the renderer.
+$manager = new \core_completion\manager($id);
+$bulkcompletiondata = $manager->get_activities_and_headings();
+
+$renderer = $PAGE->get_renderer('core_course', 'bulk_activity_completion');
+
+// Print the form.
+echo $OUTPUT->header();
+echo $OUTPUT->heading(get_string('bulkactivitycompletion', 'completion'));
+
+echo $renderer->navigation($course, 'bulkcompletion');
+
+$PAGE->requires->yui_module('moodle-core-formchangechecker',
+        'M.core_formchangechecker.init',
+        array(array(
+            'formid' => 'theform'
+        ))
+);
+$PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
+
+
+echo $renderer->bulkcompletion($bulkcompletiondata);
+
+echo $OUTPUT->footer();
index 531ed8c..113a3c7 100644 (file)
@@ -63,6 +63,7 @@ class course_summary_exporter extends \core\external\exporter {
             ),
             'summary' => array(
                 'type' => PARAM_RAW,
+                'null' => NULL_ALLOWED
             ),
             'startdate' => array(
                 'type' => PARAM_INT,
diff --git a/course/classes/output/bulk_activity_completion_renderer.php b/course/classes/output/bulk_activity_completion_renderer.php
new file mode 100644 (file)
index 0000000..9d4b752
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * Contains renderers for the bulk activity completion stuff.
+ *
+ * @package core_course
+ * @copyright 2017 Adrian Greeve
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->dirroot.'/course/renderer.php');
+
+/**
+ * Main renderer for the bulk activity completion stuff.
+ *
+ * @package core_course
+ * @copyright 2017 Adrian Greeve
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_course_bulk_activity_completion_renderer extends plugin_renderer_base {
+
+    /**
+     * Render the navigation tabs for the completion page.
+     *
+     * @param int|stdClass $courseorid the course object or id.
+     * @param String $page the tab to focus.
+     * @return string html
+     */
+    public function navigation($courseorid, $page) {
+        $tabs = core_completion\manager::get_available_completion_tabs($courseorid);
+        if (count($tabs) > 1) {
+            return $this->tabtree($tabs, $page);
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * Render the bulk completion tab.
+     *
+     * @param Array|stdClass $data the context data to pass to the template.
+     * @return bool|string
+     */
+    public function bulkcompletion($data) {
+        return parent::render_from_template('core_course/bulkactivitycompletion', $data);
+    }
+
+    /**
+     * Render the default completion tab.
+     *
+     * @param Array|stdClass $data the context data to pass to the template.
+     * @return bool|string
+     */
+    public function defaultcompletion($data) {
+        return parent::render_from_template('core_course/defaultactivitycompletion', $data);
+    }
+
+    /**
+     * Renders the form for bulk editing activities completion
+     *
+     * @param moodleform $form
+     * @param array $activities
+     * @return string
+     */
+    public function edit_bulk_completion($form, $activities) {
+        ob_start();
+        $form->display();
+        $formhtml = ob_get_contents();
+        ob_end_clean();
+
+        $data = (object)[
+            'form' => $formhtml,
+            'activities' => array_values($activities),
+            'activitiescount' => count($activities),
+        ];
+        return parent::render_from_template('core_course/editbulkactivitycompletion', $data);
+    }
+
+    /**
+     * Renders the form for editing default completion
+     *
+     * @param moodleform $form
+     * @param array $modules
+     * @return string
+     */
+    public function edit_default_completion($form, $modules) {
+        ob_start();
+        $form->display();
+        $formhtml = ob_get_contents();
+        ob_end_clean();
+
+        $data = (object)[
+            'form' => $formhtml,
+            'modules' => array_values($modules),
+            'modulescount' => count($modules),
+        ];
+        return parent::render_from_template('core_course/editdefaultcompletion', $data);
+    }
+}
index e38bace..26f03cf 100644 (file)
@@ -53,7 +53,16 @@ if ($id) {
         print_error('invalidcourseid');
     }
     require_login($course);
-    require_capability('moodle/course:update', context_course::instance($course->id));
+    $context = context_course::instance($course->id);
+    if (!has_capability('moodle/course:update', $context)) {
+        // User is not allowed to modify course completion.
+        // Check if they can see default completion or edit bulk completion and redirect there.
+        if ($tabs = core_completion\manager::get_available_completion_tabs($course)) {
+            redirect($tabs[0]->link);
+        } else {
+            require_capability('moodle/course:update', $context);
+        }
+    }
 
 } else {
     require_login();
@@ -148,10 +157,14 @@ if ($form->is_cancelled()){
     redirect($url);
 }
 
+$renderer = $PAGE->get_renderer('core_course', 'bulk_activity_completion');
+
 // Print the form.
 echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('editcoursecompletionsettings', 'core_completion'));
 
+echo $renderer->navigation($course, 'completion');
+
 $form->display();
 
 echo $OUTPUT->footer();
diff --git a/course/defaultcompletion.php b/course/defaultcompletion.php
new file mode 100644 (file)
index 0000000..4b5f98f
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Bulk activity completion selection
+ *
+ * @package     core_completion
+ * @category    completion
+ * @copyright   2017 Adrian Greeve
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__.'/../config.php');
+require_once($CFG->dirroot.'/course/lib.php');
+require_once($CFG->libdir.'/completionlib.php');
+
+$id = required_param('id', PARAM_INT);       // Course id.
+
+// Perform some basic access control checks.
+if ($id) {
+
+    if ($id == SITEID) {
+        // Don't allow editing of 'site course' using this form.
+        print_error('cannoteditsiteform');
+    }
+
+    if (!$course = $DB->get_record('course', array('id' => $id))) {
+        print_error('invalidcourseid');
+    }
+    require_login($course);
+    require_capability('moodle/course:manageactivities', context_course::instance($course->id));
+
+} else {
+    require_login();
+    print_error('needcourseid');
+}
+
+// Set up the page.
+navigation_node::override_active_url(new moodle_url('/course/completion.php', array('id' => $course->id)));
+$PAGE->set_course($course);
+$PAGE->set_url('/course/bulkcompletion.php', array('id' => $course->id));
+$PAGE->set_title($course->shortname);
+$PAGE->set_heading($course->fullname);
+$PAGE->set_pagelayout('admin');
+
+// Get all that stuff I need for the renderer.
+$manager = new \core_completion\manager($id);
+$activityresourcedata = $manager->get_activities_and_resources();
+
+$renderer = $PAGE->get_renderer('core_course', 'bulk_activity_completion');
+
+// Print the form.
+echo $OUTPUT->header();
+echo $OUTPUT->heading(get_string('defaultcompletion', 'completion'));
+
+echo $renderer->navigation($course, 'defaultcompletion');
+
+$PAGE->requires->yui_module('moodle-core-formchangechecker',
+        'M.core_formchangechecker.init',
+        array(array(
+            'formid' => 'theform'
+        ))
+);
+$PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
+
+echo $renderer->defaultcompletion($activityresourcedata);
+
+echo $OUTPUT->footer();
index 843ea9a..73ecfc8 100644 (file)
@@ -559,34 +559,11 @@ class dndupload_ajax_processor {
      */
     protected function create_course_module() {
         global $CFG;
+        require_once($CFG->dirroot.'/course/modlib.php');
+        list($module, $context, $cw, $cm, $data) = prepare_new_moduleinfo_data($this->course, $this->module->name, $this->section);
 
-        if (!course_allowed_module($this->course, $this->module->name)) {
-            throw new coding_exception("The module {$this->module->name} is not allowed to be added to this course");
-        }
-
-        $this->cm = new stdClass();
-        $this->cm->course = $this->course->id;
-        $this->cm->section = $this->section;
-        $this->cm->module = $this->module->id;
-        $this->cm->modulename = $this->module->name;
-        $this->cm->instance = 0; // This will be filled in after we create the instance.
-        $this->cm->visible = 1;
-        $this->cm->groupmode = $this->course->groupmode;
-        $this->cm->groupingid = $this->course->defaultgroupingid;
-
-        // Set the correct default for completion tracking.
-        $this->cm->completion = COMPLETION_TRACKING_NONE;
-        $completion = new completion_info($this->course);
-        if ($completion->is_enabled() && $CFG->completiondefault) {
-            if (plugin_supports('mod', $this->cm->modulename, FEATURE_MODEDIT_DEFAULT_COMPLETION, true)) {
-                $this->cm->completion = COMPLETION_TRACKING_MANUAL;
-            }
-        }
-
-        if (!$this->cm->id = add_course_module($this->cm)) {
-            throw new coding_exception("Unable to create the course module");
-        }
-        $this->cm->coursemodule = $this->cm->id;
+        $data->coursemodule = $data->id = add_course_module($data);
+        $this->cm = $data;
     }
 
     /**
diff --git a/course/editbulkcompletion.php b/course/editbulkcompletion.php
new file mode 100644 (file)
index 0000000..65d60ac
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Bulk activity completion selection
+ *
+ * @package     core_completion
+ * @copyright   2017 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . "/../config.php");
+require_once($CFG->libdir . '/completionlib.php');
+
+$courseid = required_param('id', PARAM_INT);
+$cmids = optional_param_array('cmid', [], PARAM_INT);
+$course = get_course($courseid);
+require_login($course);
+
+navigation_node::override_active_url(new moodle_url('/course/completion.php', array('id' => $course->id)));
+$PAGE->set_url(new moodle_url('/course/editbulkcompletion.php', ['id' => $courseid]));
+$PAGE->set_title($course->shortname);
+$PAGE->set_heading($course->fullname);
+$PAGE->set_pagelayout('admin');
+
+if (!core_completion\manager::can_edit_bulk_completion($course)) {
+    require_capability('moodle/course:manageactivities', context_course::instance($course->id));
+}
+
+// Prepare list of modules to be updated.
+$modinfo = get_fast_modinfo($courseid);
+$cms = [];
+foreach ($cmids as $cmid) {
+    $cm = $modinfo->get_cm($cmid);
+    if (core_completion\manager::can_edit_bulk_completion($course, $cm)) {
+        $cms[$cm->id] = $cm;
+    }
+}
+
+$returnurl = new moodle_url('/course/bulkcompletion.php', ['id' => $course->id]);
+$manager = new \core_completion\manager($course->id);
+if (empty($cms)) {
+    redirect($returnurl);
+}
+$form = new core_completion_bulkedit_form(null, ['cms' => $cms]);
+
+if ($form->is_cancelled()) {
+    redirect($returnurl);
+} else if ($data = $form->get_data()) {
+    $manager->apply_completion($data, $form->has_custom_completion_rules());
+    redirect($returnurl);
+}
+
+$renderer = $PAGE->get_renderer('core_course', 'bulk_activity_completion');
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading(get_string('bulkactivitycompletion', 'completion'));
+
+echo $renderer->navigation($course, 'bulkcompletion');
+
+echo $renderer->edit_bulk_completion($form, $manager->get_activities(array_keys($cms)));
+
+echo $OUTPUT->footer();
+
diff --git a/course/editdefaultcompletion.php b/course/editdefaultcompletion.php
new file mode 100644 (file)
index 0000000..17897e7
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Bulk activity completion selection
+ *
+ * @package     core_completion
+ * @copyright   2017 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . "/../config.php");
+require_once($CFG->libdir . '/completionlib.php');
+
+$courseid = required_param('id', PARAM_INT);
+$modids = optional_param_array('modids', [], PARAM_INT);
+$course = get_course($courseid);
+require_login($course);
+
+navigation_node::override_active_url(new moodle_url('/course/completion.php', array('id' => $course->id)));
+$PAGE->set_url(new moodle_url('/course/editdefaultcompletion.php', ['id' => $courseid]));
+$PAGE->set_title($course->shortname);
+$PAGE->set_heading($course->fullname);
+$PAGE->set_pagelayout('admin');
+
+require_capability('moodle/course:update', context_course::instance($course->id));
+
+// Prepare list of selected modules.
+$manager = new \core_completion\manager($course->id);
+$allmodules = $manager->get_activities_and_resources();
+$modules = [];
+foreach ($allmodules->modules as $module) {
+    if ($module->canmanage && in_array($module->id, $modids)) {
+        $modules[$module->id] = $module;
+    }
+}
+
+$returnurl = new moodle_url('/course/defaultcompletion.php', ['id' => $course->id]);
+if (empty($modules)) {
+    redirect($returnurl);
+}
+
+$form = new core_completion_defaultedit_form(null, ['course' => $course, 'modules' => $modules]);
+
+if ($form->is_cancelled()) {
+    redirect($returnurl);
+} else if ($data = $form->get_data()) {
+    $manager->apply_default_completion($data, $form->has_custom_completion_rules());
+    redirect($returnurl);
+}
+
+$renderer = $PAGE->get_renderer('core_course', 'bulk_activity_completion');
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading(get_string('defaultcompletion', 'completion'));
+
+echo $renderer->navigation($course, 'defaultcompletion');
+
+echo $renderer->edit_default_completion($form, $modules);
+
+echo $OUTPUT->footer();
+
index 7b41c39..c7e6941 100644 (file)
@@ -58,7 +58,7 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
         SECTIONNAME : 'sectionname'
     },
     SELECTORS = {
-        SECTIONLEFTSIDE : '.left .section-handle img'
+        SECTIONLEFTSIDE : '.left .section-handle .icon'
     };
 
     if (response.action == 'move') {
index c9d072f..1fb0c8e 100644 (file)
@@ -58,7 +58,7 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
         SECTIONNAME : 'sectionname'
     },
     SELECTORS = {
-        SECTIONLEFTSIDE : '.left .section-handle img'
+        SECTIONLEFTSIDE : '.left .section-handle .icon'
     };
 
     if (response.action == 'move') {
index 3e99f0b..e60bd2c 100644 (file)
@@ -3867,9 +3867,14 @@ function course_get_user_navigation_options($context, $course = null) {
 function course_get_user_administration_options($course, $context) {
     global $CFG;
     $isfrontpage = $course->id == SITEID;
+    $completionenabled = $CFG->enablecompletion && $course->enablecompletion;
+    $hascompletiontabs = count(core_completion\manager::get_available_completion_tabs($course, $context)) > 0;
 
     $options = new stdClass;
     $options->update = has_capability('moodle/course:update', $context);
+    $options->editcompletion = $CFG->enablecompletion &&
+                               $course->enablecompletion &&
+                               ($options->update || $hascompletiontabs);
     $options->filters = has_capability('moodle/filter:manage', $context) &&
                         count(filter_get_available_in_context($context)) > 0;
     $options->reports = has_capability('moodle/site:viewreports', $context);
index c386e59..1a87fb8 100644 (file)
@@ -148,13 +148,6 @@ if ($mform->is_cancelled()) {
         redirect(course_get_url($course, $cw->section, array('sr' => $sectionreturn)));
     }
 } else if ($fromform = $mform->get_data()) {
-    // Convert the grade pass value - we may be using a language which uses commas,
-    // rather than decimal points, in numbers. These need to be converted so that
-    // they can be added to the DB.
-    if (isset($fromform->gradepass)) {
-        $fromform->gradepass = unformat_float($fromform->gradepass);
-    }
-
     if (!empty($fromform->update)) {
         list($cm, $fromform) = update_moduleinfo($cm, $fromform, $course, $mform);
     } else if (!empty($fromform->add)) {
index 68ed661..d4cc82b 100644 (file)
@@ -760,6 +760,12 @@ function prepare_new_moduleinfo_data($course, $modulename, $section) {
     $data->instance         = '';
     $data->coursemodule     = '';
 
+    // Apply completion defaults.
+    $defaults = \core_completion\manager::get_default_completion($course, $module);
+    foreach ($defaults as $key => $value) {
+        $data->$key = $value;
+    }
+
     if (plugin_supports('mod', $data->modulename, FEATURE_MOD_INTRO, true)) {
         $draftid_editor = file_get_submitted_draft_itemid('introeditor');
         file_prepare_draft_area($draftid_editor, null, null, null, null, array('subdirs'=>true));
index 46ed50a..06cc2b1 100644 (file)
@@ -1,6 +1,8 @@
 <?php
 require_once ($CFG->libdir.'/formslib.php');
 require_once($CFG->libdir.'/completionlib.php');
+require_once($CFG->libdir.'/gradelib.php');
+require_once($CFG->libdir.'/plagiarismlib.php');
 
 /**
  * This class adds extra methods to form wrapper specific to be used for module
@@ -181,6 +183,9 @@ abstract class moodleform_mod extends moodleform {
     }
 
     /**
+     * Allows module to modify data returned by get_moduleinfo_data() or prepare_new_moduleinfo_data() before calling set_data()
+     * This method is also called in the bulk activity completion form.
+     *
      * Only available on moodleform_mod.
      *
      * @param array $default_values passed by reference
@@ -926,7 +931,7 @@ abstract class moodleform_mod extends moodleform {
         // If the 'show description' feature is enabled, this checkbox appears below the intro.
         // We want to hide that when using the singleactivity course format because it is confusing.
         if ($this->_features->showdescription  && $this->courseformat->has_view_page()) {
-            $mform->addElement('checkbox', 'showdescription', get_string('showdescription'));
+            $mform->addElement('advcheckbox', 'showdescription', get_string('showdescription'));
             $mform->addHelpButton('showdescription', 'showdescription');
         }
     }
@@ -1064,6 +1069,40 @@ abstract class moodleform_mod extends moodleform {
             }
         }
     }
+
+    /**
+     * Allows modules to modify the data returned by form get_data().
+     * This method is also called in the bulk activity completion form.
+     *
+     * Only available on moodleform_mod.
+     *
+     * @param stdClass $data passed by reference
+     */
+    public function data_postprocessing($data) {
+    }
+
+    /**
+     * Return submitted data if properly submitted or returns NULL if validation fails or
+     * if there is no submitted data.
+     *
+     * Do not override this method, override data_postprocessing() instead.
+     *
+     * @return object submitted data; NULL if not valid or not submitted or cancelled
+     */
+    public function get_data() {
+        $data = parent::get_data();
+        if ($data) {
+            // Convert the grade pass value - we may be using a language which uses commas,
+            // rather than decimal points, in numbers. These need to be converted so that
+            // they can be added to the DB.
+            if (isset($data->gradepass)) {
+                $data->gradepass = unformat_float($data->gradepass);
+            }
+
+            $this->data_postprocessing($data);
+        }
+        return $data;
+    }
 }
 
 
diff --git a/course/templates/activityinstance.mustache b/course/templates/activityinstance.mustache
new file mode 100644 (file)
index 0000000..8b1fd6c
--- /dev/null
@@ -0,0 +1,64 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core_course/activityinstance
+
+    Activity completion selector.
+
+    Example context (json):
+    {
+        "activities": [{
+            "cmid": "4",
+            "modname": "Test activity",
+            "icon": "https://raw.githubusercontent.com/moodle/moodle/master/mod/feedback/pix/icon.png",
+            "completionstatus": {
+                "string": "Manual",
+                "icon": "https://raw.githubusercontent.com/moodle/moodle/master/pix/i/completion-manual-enabled.png"
+            }
+        }]
+    }
+}}
+{{#activities}}
+<div class="row m-b-1 row-fluid">
+    <div class="activityinstance col-xs-6 span6">
+        <div class="mod-indent-outer"></div>
+        <div>
+            {{#canmanage}}
+            <label class="accesshide" for="selectactivity_{{cmid}}">{{#str}}select, completion{{/str}} {{modname}}</label>
+            <input type="checkbox" id="selectactivity_{{cmid}}" class="m-r-1" name="cmid[]" data-section="{{sectionnumber}}" value="{{cmid}}" aria-label="{{#str}}checkactivity, completion, {{modname}}{{/str}}">
+            {{/canmanage}}
+            <a href="{{url}}">
+            <img src="{{icon}}" class="iconlarge activityicon" alt=" " role="presentation" />
+            <span class="instancename">{{modname}}</span>
+            </a>
+        </div>
+    </div>
+    <div class="activity-completionstatus col-xs-6 span6" id="completionstatus_{{cmid}}">
+        <div class="col-sm-1 span1  p-l-0">
+            {{#completionstatus.icon}}
+                {{{completionstatus.icon}}}
+            {{/completionstatus.icon}}
+            {{^completionstatus.icon}}
+                <span class="m-r-3"></span>
+            {{/completionstatus.icon}}
+        </div>
+        <div class="col-sm-11 span11  p-l-0">
+            <span class="text-muted muted">{{{completionstatus.string}}}</span>
+        </div>
+    </div>
+</div>
+{{/activities}}
diff --git a/course/templates/bulkactivitycompletion.mustache b/course/templates/bulkactivitycompletion.mustache
new file mode 100644 (file)
index 0000000..57fafd9
--- /dev/null
@@ -0,0 +1,126 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core_course/bulkactivitycompletion
+
+    Activity completion selector.
+
+    Example context (json):
+    {
+        "courseid": "2",
+        "sesskey": "AAAAAA",
+        "sections": [{
+            "sectionnumber": "0",
+            "name": "General",
+            "activities": [{
+                "cmid": "4",
+                "modname": "Test activity",
+                "icon": "https://raw.githubusercontent.com/moodle/moodle/master/mod/feedback/pix/icon.png",
+                "completionstatus": {
+                    "string": "Manual",
+                    "icon": "https://raw.githubusercontent.com/moodle/moodle/master/pix/i/completion-manual-enabled.png"
+                }
+            }]
+        }]
+    }
+}}
+<div class="container-fluid">
+    <div class="row m-b-2">
+        <div class="col">{{#str}}bulkactivitydetail, core_completion{{/str}}</div>
+    </div>
+<form method="post" action="editbulkcompletion.php" class="mform" id="theform">
+    <div class="row m-b-2">
+        <div class="col">
+            <input type="submit" value="{{#str}}edit{{/str}}" class="btn btn-primary" name="submitbutton" aria-label="{{#str}}updateactivities, completion{{/str}}" disabled/>
+        </div>
+    </div>
+    <div class="row m-b-1">
+        <div class="col-xs-6 span6">
+            <input type="checkbox" class="mastercheck m-r-1" aria-label="{{#str}}checkall, completion{{/str}}">
+            <label class="font-weight-bold">{{#str}}activitieslabel, core_completion{{/str}}</label>
+        </div>
+        <div class="col-xs-6 span6">
+            <label class="font-weight-bold">{{#str}}completion, core_completion{{/str}}</label>
+            <span>{{{helpicon}}}</span>
+        </div>
+    </div>
+    <hr class="row">
+    <div class="topics">
+        {{#sections}}
+                <div class="m-b-1">
+                    <div class="row m-b-1 row-fluid">
+                        <div class="col-sm-12">
+                            <input type="checkbox" data-section-master="{{sectionnumber}}" class="m-r-1" aria-label="{{#str}}checkallsection, completion, {{name}}{{/str}}">
+                            <h3 class="d-inline-block">{{name}}</h3>
+                        </div>
+                    </div>
+                    {{> core_course/activityinstance}}
+                </div>
+                <hr class="row">
+        {{/sections}}
+    </div>
+    <input type="hidden" name="id" value="{{courseid}}" />
+    <input type="hidden" name="sesskey" value="{{sesskey}}" />
+    <div class="row">
+        <div class="col">
+            <input type="submit" value="{{#str}}edit{{/str}}" class="btn btn-primary" name="submitbutton" disabled/>
+        </div>
+    </div>
+</form>
+</div>
+
+{{#js}}
+require([
+    'jquery',
+], function($) {
+    $('.mastercheck').click(function() {
+        var checked = $('.mastercheck').is(':checked');
+        $('input[type=checkbox]').each(function() {
+            $(this).prop('checked', checked);
+            $(this).trigger('change'); // Hmmm. Could be smarter about this and only trigger once for the first checkbox.
+        });
+    });
+    var mastersection = $('input[data-section-master]');
+    mastersection.click(function() {
+        var checked = $(this).is(':checked');
+        var dataid = $(this).attr('data-section-master');
+        $('input[type=checkbox][data-section=\'' + dataid + '\']').each(function() {
+            $(this).prop('checked', checked);
+            $(this).trigger('change'); // Hmmm. Could be smarter about this and only trigger once for the first checkbox.
+        });
+    });
+
+    $('input[type=checkbox][id^=selectactivity_]').change(function() {
+        if ($(this).is(':checked')) {
+            $('[name=submitbutton]').removeAttr('disabled');
+        } else {
+            // Is this the last activity checkbox to be un-checked? If so, disable the edit button.
+            var somechecked = false;
+            $('input[type=checkbox][id^=selectactivity_]').each(function() {
+                if ($(this).is(':checked')) {
+                    somechecked = true;
+                    return false;
+                }
+                return true;
+            });
+            if (!somechecked) {
+                $('[name=submitbutton]').attr('disabled', 'disabled');
+            }
+        }
+    });
+});
+{{/js}}
\ No newline at end of file
diff --git a/course/templates/defaultactivitycompletion.mustache b/course/templates/defaultactivitycompletion.mustache
new file mode 100644 (file)
index 0000000..cfd0676
--- /dev/null
@@ -0,0 +1,130 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core_course/defaultactivitycompletion
+
+    Activity completion selector.
+
+    Example context (json):
+    {
+        "courseid": "2",
+        "sesskey": "AAAAAA",
+        "modules": [{
+            "id": "10",
+            "formattedname": "Assignment",
+            "canmanage": true,
+            "icon": "https://raw.githubusercontent.com/moodle/moodle/master/mod/assign/pix/icon.png",
+            "completionstatus": {
+                "string": "Manual",
+                "icon": "https://raw.githubusercontent.com/moodle/moodle/master/pix/i/completion-manual-enabled.png"
+            }
+        }]
+    }
+}}
+<div class="container-fluid">
+    <div class="row m-b-2">
+        <div class="col">{{#str}}bulkactivitydetail, core_completion{{/str}}</div>
+    </div>
+<form method="post" action="editdefaultcompletion.php" class="mform" id="theform">
+    <div class="row m-b-2">
+        <div class="col">
+            <input type="submit" value="{{#str}}edit{{/str}}" class="btn btn-primary" name="submitbutton" aria-label="{{#str}}updateactivities, completion{{/str}}" disabled/>
+        </div>
+    </div>
+    <div class="row m-b-1">
+        <div class="col-xs-6 span6">
+            <input type="checkbox" class="mastercheck m-r-1" aria-label="{{#str}}checkall, completion{{/str}}">
+            <label class="font-weight-bold">{{#str}}activitieslabel, core_completion{{/str}}</label>
+        </div>
+        <div class="col-xs-6 span6">
+            <label class="font-weight-bold">{{#str}}completion, core_completion{{/str}}</label>
+            <span>{{{helpicon}}}</span>
+        </div>
+    </div>
+    <hr class="row">
+    <div class="modules">
+        {{#modules}}
+            {{#canmanage}}
+             <div class="m-b-1">
+                <div class="row m-b-1 row-fluid">
+                    <div class="col-xs-6 span6">
+                        <label class="accesshide" for="modtype_{{id}}">{{#str}}select, core_completion{{/str}} {{formattedname}}</label>
+                        <input id="modtype_{{id}}" type="checkbox" class="m-r-1" name="modids[]" value="{{id}}" aria-label="{{#str}}checkactivity, completion, {{formattedname}}{{/str}}">
+                        <img src="{{icon}}" alt=" " role="presentation" />
+                        <span>{{formattedname}}</span>
+                    </div>
+                    <div class="activity-completionstatus col-xs-6 span6">
+                        <div class="col-sm-1 span1 p-l-0">
+                            {{#completionstatus.icon}}
+                                {{{completionstatus.icon}}}
+                            {{/completionstatus.icon}}
+                            {{^completionstatus.icon}}
+                                <span class="m-r-3"></span>
+                            {{/completionstatus.icon}}
+                        </div>
+                        <div class="col-sm-11 span11 p-l-0">
+                            <span class="text-muted muted">{{{completionstatus.string}}}</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <hr class="row">
+            {{/canmanage}}
+        {{/modules}}
+    </div>
+    <input type="hidden" name="id" value="{{courseid}}" />
+    <input type="hidden" name="sesskey" value="{{sesskey}}" />
+    <div class="row">
+        <div class="col">
+            <input type="submit" value="{{#str}}edit{{/str}}" class="btn btn-primary" name="submitbutton" disabled/>
+        </div>
+    </div>
+</form>
+</div>
+
+{{#js}}
+require([
+    'jquery',
+], function($) {
+    $('.mastercheck').click(function() {
+        var checked = $('.mastercheck').is(':checked');
+        $('input[type=checkbox]').each(function() {
+            $(this).prop('checked', checked);
+            $(this).trigger('change');
+        });
+    });
+
+    $('input[type=checkbox][id^=modtype_]').change(function() {
+        if ($(this).is(':checked')) {
+            $('[name=submitbutton]').removeAttr('disabled');
+        } else {
+            // Is this the last activity checkbox to be un-checked? If so, disable the edit button.
+            var somechecked = false;
+            $('input[type=checkbox][id^=modtype_]').each(function() {
+                if ($(this).is(':checked')) {
+                    somechecked = true;
+                    return false;
+                }
+                return true;
+            });
+            if (!somechecked) {
+                $('[name=submitbutton]').attr('disabled', 'disabled');
+            }
+        }
+    });
+});
+{{/js}}
\ No newline at end of file
diff --git a/course/templates/editbulkactivitycompletion.mustache b/course/templates/editbulkactivitycompletion.mustache
new file mode 100644 (file)
index 0000000..01498c4
--- /dev/null
@@ -0,0 +1,40 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core_course/editbulkactivitycompletion
+
+    Edit default module completion screen
+
+    Example context (json):
+    {
+        "form": "<form><p>Placeholder for the form</p></form>",
+        "activitiescount": 1,
+        "activities": [{
+            "cmid": "4",
+            "modname": "Test activity",
+            "icon": "https://raw.githubusercontent.com/moodle/moodle/master/mod/feedback/pix/icon.png"
+        }]
+    }
+}}
+<p>{{#str}}modifybulkactions, completion{{/str}}</p>
+{{{form}}}
+<div class="container-fluid">
+    <p>{{#str}}affectedactivities, completion, {{activitiescount}}{{/str}}</p>
+    <div class="activities">
+        {{> core_course/activityinstance}}
+    </div>
+</div>
diff --git a/course/templates/editdefaultcompletion.mustache b/course/templates/editdefaultcompletion.mustache
new file mode 100644 (file)
index 0000000..706761b
--- /dev/null
@@ -0,0 +1,47 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core_course/editdefaultcompletion
+
+    Edit default module completion screen
+
+    Example context (json):
+    {
+        "form": "<form><p>Placeholder for the form</p></form>",
+        "modulescount": 1,
+        "modules": [{
+            "id": "10",
+            "formattedname": "Assignment",
+            "icon": "https://raw.githubusercontent.com/moodle/moodle/master/mod/assign/pix/icon.png"
+        }]
+    }
+}}
+<p>{{#str}}modifybulkactions, completion{{/str}}</p>
+{{{form}}}
+<p>{{#str}}affectedactivities, completion, {{modulescount}}{{/str}}</p>
+<div class="container-fluid">
+    <div class="modules m-b-1">
+        <div class="row m-b-1">
+            {{#modules}}
+                <div class="col-sm-2">
+                    <img src="{{icon}}" class="m-r-1 m-b-1" alt=" " role="presentation" />
+                    <span>{{{formattedname}}}</span>
+                </div>
+            {{/modules}}
+        </div>
+    </div>
+</div>
index 9d0cb22..054f2ce 100644 (file)
@@ -1942,7 +1942,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                 $adminoptions->{$option['name']} = $option['available'];
             }
             if ($course['id'] == SITEID) {
-                $this->assertCount(15, $course['options']);
+                $this->assertCount(16, $course['options']);
                 $this->assertFalse($adminoptions->update);
                 $this->assertFalse($adminoptions->filters);
                 $this->assertFalse($adminoptions->reports);
@@ -1957,8 +1957,9 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                 $this->assertFalse($adminoptions->publish);
                 $this->assertFalse($adminoptions->reset);
                 $this->assertFalse($adminoptions->roles);
+                $this->assertFalse($adminoptions->editcompletion);
             } else {
-                $this->assertCount(14, $course['options']);
+                $this->assertCount(15, $course['options']);
                 $this->assertFalse($adminoptions->update);
                 $this->assertFalse($adminoptions->filters);
                 $this->assertFalse($adminoptions->reports);
@@ -1973,6 +1974,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                 $this->assertFalse($adminoptions->publish);
                 $this->assertFalse($adminoptions->reset);
                 $this->assertFalse($adminoptions->roles);
+                $this->assertFalse($adminoptions->editcompletion);
             }
         }
     }
index f4b607c..2196333 100644 (file)
@@ -62,6 +62,7 @@ class core_course_modlib_testcase extends advanced_testcase {
         $expecteddata->instance         = '';
         $expecteddata->coursemodule     = '';
         $expecteddata->advancedgradingmethod_submissions = ''; // Not grading methods enabled by default.
+        $expecteddata->completion       = 0;
         // Unset untestable.
         unset($data->introeditor);
         unset($data->_advancedgradingdata);
index 09c5804..a601cf7 100644 (file)
@@ -260,7 +260,7 @@ class enrol_lti_helper_testcase extends advanced_testcase {
 
         $id = $tool1->id;
         $launchurl = \enrol_lti\helper::get_launch_url($id);
-        $this->assertEquals('http://www.example.com/moodle/enrol/lti/tool.php?id=' . $id, $launchurl->out());
+        $this->assertEquals('https://www.example.com/moodle/enrol/lti/tool.php?id=' . $id, $launchurl->out());
     }
 
     /**
@@ -281,13 +281,13 @@ class enrol_lti_helper_testcase extends advanced_testcase {
         $id = $tool1->id;
         $token = \enrol_lti\helper::generate_cartridge_token($id);
         $launchurl = \enrol_lti\helper::get_cartridge_url($tool1);
-        $this->assertEquals('http://www.example.com/moodle/enrol/lti/cartridge.php?id=' . $id . '&amp;token=' . $token,
+        $this->assertEquals('https://www.example.com/moodle/enrol/lti/cartridge.php?id=' . $id . '&amp;token=' . $token,
                             $launchurl->out());
 
         $CFG->slasharguments = true;
 
         $launchurl = \enrol_lti\helper::get_cartridge_url($tool1);
-        $this->assertEquals('http://www.example.com/moodle/enrol/lti/cartridge.php/' . $id . '/' . $token . '/cartridge.xml',
+        $this->assertEquals('https://www.example.com/moodle/enrol/lti/cartridge.php/' . $id . '/' . $token . '/cartridge.xml',
                             $launchurl->out());
 
         $CFG->slasharguments = $slasharguments;
@@ -311,13 +311,13 @@ class enrol_lti_helper_testcase extends advanced_testcase {
         $id = $tool1->id;
         $token = \enrol_lti\helper::generate_proxy_token($id);
         $launchurl = \enrol_lti\helper::get_proxy_url($tool1);
-        $this->assertEquals('http://www.example.com/moodle/enrol/lti/proxy.php?id=' . $id . '&amp;token=' . $token,
+        $this->assertEquals('https://www.example.com/moodle/enrol/lti/proxy.php?id=' . $id . '&amp;token=' . $token,
                             $launchurl->out());
 
         $CFG->slasharguments = true;
 
         $launchurl = \enrol_lti\helper::get_proxy_url($tool1);
-        $this->assertEquals('http://www.example.com/moodle/enrol/lti/proxy.php/' . $id . '/' . $token . '/',
+        $this->assertEquals('https://www.example.com/moodle/enrol/lti/proxy.php/' . $id . '/' . $token . '/',
                             $launchurl->out());
 
         $CFG->slasharguments = $slasharguments;
index b9c5d7f..d7a3ea7 100644 (file)
@@ -280,6 +280,62 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertEquals($sharedcourse->id, $course1->id);
     }
 
+    public function test_enrol_get_shared_courses_different_methods() {
+        global $DB, $CFG;
+
+        require_once($CFG->dirroot . '/enrol/self/externallib.php');
+
+        $this->resetAfterTest();
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+
+        $course1 = $this->getDataGenerator()->create_course();
+
+        // Enrol user1 and user2 in course1 with a different enrolment methode.
+        // Add self enrolment method for course1.
+        $selfplugin = enrol_get_plugin('self');
+        $this->assertNotEmpty($selfplugin);
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->assertNotEmpty($studentrole);
+
+        $instance1id = $selfplugin->add_instance($course1, array('status' => ENROL_INSTANCE_ENABLED,
+                                                                 'name' => 'Test instance 1',
+                                                                 'customint6' => 1,
+                                                                 'roleid' => $studentrole->id));
+
+        $instance1 = $DB->get_record('enrol', array('id' => $instance1id), '*', MUST_EXIST);
+
+        self::setUser($user2);
+        // Self enrol me (user2).
+        $result = enrol_self_external::enrol_user($course1->id);
+
+        // Enrol user1 manually.
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, null, 'manual');
+
+        $course2 = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course2->id);
+
+        $course3 = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user2->id, $course3->id);
+
+        // Test that user1 and user2 have courses in common.
+        $this->assertTrue(enrol_get_shared_courses($user1, $user2, false, true));
+        // Test that user1 and user3 have no courses in common.
+        $this->assertFalse(enrol_get_shared_courses($user1, $user3, false, true));
+
+        // Test retrieving the courses in common.
+        $sharedcourses = enrol_get_shared_courses($user1, $user2, true);
+
+        // Only should be one shared course.
+        $this->assertCount(1, $sharedcourses);
+        $sharedcourse = array_shift($sharedcourses);
+        // It should be course 1.
+        $this->assertEquals($sharedcourse->id, $course1->id);
+    }
+
     /**
      * Test user enrolment created event.
      */
diff --git a/files/converter/googledrive/classes/converter.php b/files/converter/googledrive/classes/converter.php
new file mode 100644 (file)
index 0000000..db189e6
--- /dev/null
@@ -0,0 +1,270 @@
+<?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/>.
+
+/**
+ * Class for converting files between different file formats using google drive.
+ *
+ * @package    fileconverter_googledrive
+ * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace fileconverter_googledrive;
+
+defined('MOODLE_INTERNAL') || die();
+
+use stored_file;
+use moodle_exception;
+use moodle_url;
+use \core_files\conversion;
+
+/**
+ * Class for converting files between different formats using unoconv.
+ *
+ * @package    fileconverter_googledrive
+ * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class converter implements \core_files\converter_interface {
+
+    /** @var array $imports List of supported import file formats */
+    private static $imports = [
+        'doc' => 'application/vnd.google-apps.document',
+        'docx' => 'application/vnd.google-apps.document',
+        'rtf' => 'application/vnd.google-apps.document',
+        'xls' => 'application/vnd.google-apps.spreadsheet',
+        'xlsx' => 'application/vnd.google-apps.spreadsheet',
+        'ppt' => 'application/vnd.google-apps.presentation',
+        'pptx' => 'application/vnd.google-apps.presentation'
+    ];
+
+    /** @var array $export List of supported export file formats */
+    private static $exports = [
+        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+        'rtf' => 'application/rtf',
+        'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+        'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+        'pdf' => 'application/pdf',
+        'txt' => 'text/plain'
+    ];
+
+    /**
+     * Convert a document to a new format and return a conversion object relating to the conversion in progress.
+     *
+     * @param   \core_files\conversion $conversion The file to be converted
+     * @return  this
+     */
+    public function start_document_conversion(\core_files\conversion $conversion) {
+        global $CFG;
+
+        $file = $conversion->get_sourcefile();
+        $format = $conversion->get('targetformat');
+
+        $issuerid = get_config('fileconverter_googledrive', 'issuerid');
+        if (empty($issuerid)) {
+            $conversion->set('status', conversion::STATUS_FAILED);
+            return $this;
+        }
+
+        $issuer = \core\oauth2\api::get_issuer($issuerid);
+        if (empty($issuer)) {
+            $conversion->set('status', conversion::STATUS_FAILED);
+            return $this;
+        }
+        $client = \core\oauth2\api::get_system_oauth_client($issuer);
+
+        $service = new \fileconverter_googledrive\rest($client);
+
+        $contenthash = $file->get_contenthash();
+
+        $originalname = $file->get_filename();
+        if (strpos($originalname, '.') === false) {
+            $conversion->set('status', conversion::STATUS_FAILED);
+            return $this;
+        }
+        $importextension = substr($originalname, strrpos($originalname, '.') + 1);
+
+        $importformat = self::$imports[$importextension];
+        $exportformat = self::$exports[$format];
+
+        $metadata = [
+            'name' => $contenthash,
+            'mimeType' => $importformat
+        ];
+
+        $filecontent = $file->get_content();
+        $filesize = $file->get_filesize();
+        $filemimetype = $file->get_mimetype();
+
+        // Start resumable upload.
+        // First create empty file.
+        $params = [
+            'uploadType' => 'resumable',
+            'fields' => 'id,name'
+        ];
+
+        $client->setHeader('X-Upload-Content-Type: ' . $filemimetype);
+        $client->setHeader('X-Upload-Content-Length: ' . $filesize);
+
+        $headers = $service->call('upload', $params, json_encode($metadata));
+
+        $uploadurl;
+        // Google returns a location header with the location for the upload.
+        foreach ($headers as $header) {
+            if (strpos($header, 'Location:') === 0) {
+                $uploadurl = trim(substr($header, strpos($header, ':') + 1));
+            }
+        }
+
+        if (empty($uploadurl)) {
+            $conversion->set('status', conversion::STATUS_FAILED);
+            return $this;
+        }
+
+        $params = [
+            'uploadurl' => $uploadurl
+        ];
+        $result = $service->call('upload_content', $params, $filecontent, $filemimetype);
+
+        $fileid = $result->id;
+        // Now export it again.
+        $params = ['mimeType' => $exportformat];
+        $sourceurl = new moodle_url('https://www.googleapis.com/drive/v3/files/' . $fileid . '/export', $params);
+        $source = $sourceurl->out(false);
+
+        $tmp = make_request_directory();
+        $downloadto = $tmp . '/' . $fileid . '.' . $format;
+
+        $options = ['filepath' => $downloadto, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5];
+        $success = $client->download_one($source, null, $options);
+
+        if ($success) {
+            $conversion->store_destfile_from_path($downloadto);
+            $conversion->set('status', conversion::STATUS_COMPLETE);
+            $conversion->update();
+        } else {
+            $conversion->set('status', conversion::STATUS_FAILED);
+        }
+        // Cleanup.
+        $params = [
+            'fileid' => $fileid
+        ];
+        $service->call('delete', $params);
+
+        return $this;
+    }
+
+    /**
+     * Generate and serve the test document.
+     *
+     * @return  stored_file
+     */
+    public function serve_test_document() {
+        global $CFG;
+        require_once($CFG->libdir . '/filelib.php');
+
+        $filerecord = [
+            'contextid' => \context_system::instance()->id,
+            'component' => 'test',
+            'filearea' => 'fileconverter_googledrive',
+            'itemid' => 0,
+            'filepath' => '/',
+            'filename' => 'conversion_test.docx'
+        ];
+
+        // Get the fixture doc file content and generate and stored_file object.
+        $fs = get_file_storage();
+        $testdocx = $fs->get_file($filerecord['contextid'], $filerecord['component'], $filerecord['filearea'],
+                $filerecord['itemid'], $filerecord['filepath'], $filerecord['filename']);
+
+        if (!$testdocx) {
+            $fixturefile = dirname(__DIR__) . '/tests/fixtures/source.docx';
+            $testdocx = $fs->create_file_from_pathname($filerecord, $fixturefile);
+        }
+
+        $conversion = new \core_files\conversion(0, (object) [
+            'targetformat' => 'pdf',
+        ]);
+
+        $conversion->set_sourcefile($testdocx);
+        $conversion->create();
+
+        // Convert the doc file to pdf and send it direct to the browser.
+        $this->start_document_conversion($conversion);
+
+        $testfile = $conversion->get_destfile();
+        readfile_accel($testfile, 'application/pdf', true);
+    }
+
+    /**
+     * Poll an existing conversion for status update.
+     *
+     * @param   conversion $conversion The file to be converted
+     * @return  $this;
+     */
+    public function poll_conversion_status(conversion $conversion) {
+        return $this;
+    }
+
+    /**
+     * Whether the plugin is configured and requirements are met.
+     *
+     * @return  bool
+     */
+    public static function are_requirements_met() {
+        $issuerid = get_config('fileconverter_googledrive', 'issuerid');
+        if (empty($issuerid)) {
+            return false;
+        }
+
+        $issuer = \core\oauth2\api::get_issuer($issuerid);
+        if (empty($issuer)) {
+            return false;
+        }
+
+        if (!$issuer->get('enabled')) {
+            return false;
+        }
+
+        if (!$issuer->is_system_account_connected()) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Whether a file conversion can be completed using this converter.
+     *
+     * @param   string $from The source type
+     * @param   string $to The destination type
+     * @return  bool
+     */
+    public static function supports($from, $to) {
+        // This is not a one-liner because of php 5.6.
+        $imports = self::$imports;
+        $exports = self::$exports;
+        return isset($imports[$from]) && isset($exports[$to]);
+    }
+
+    /**
+     * A list of the supported conversions.
+     *
+     * @return  string
+     */
+    public function get_supported_conversions() {
+        return implode(', ', ['rtf', 'doc', 'xls', 'docx', 'xlsx', 'ppt', 'pptx', 'pdf']);
+    }
+}
diff --git a/files/converter/googledrive/classes/rest.php b/files/converter/googledrive/classes/rest.php
new file mode 100644 (file)
index 0000000..2be6934
--- /dev/null
@@ -0,0 +1,79 @@
+<?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/>.
+
+/**
+ * Google Drive Rest API.
+ *
+ * @package    fileconverter_googledrive
+ * @copyright  2017 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace fileconverter_googledrive;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Google Drive Rest API.
+ *
+ * @copyright  2017 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class rest extends \core\oauth2\rest {
+
+    /**
+     * Define the functions of the rest API.
+     *
+     * @return array Example:
+     *  [ 'listFiles' => [ 'method' => 'get', 'endpoint' => 'http://...', 'args' => [ 'folder' => PARAM_STRING ] ] ]
+     */
+    public function get_api_functions() {
+        return [
+            'upload' => [
+                'endpoint' => 'https://www.googleapis.com/upload/drive/v3/files',
+                'method' => 'post',
+                'args' => [
+                    'uploadType' => PARAM_RAW,
+                    'fields' => PARAM_RAW
+                ],
+                'response' => 'headers'
+            ],
+            'upload_content' => [
+                'endpoint' => '{uploadurl}',
+                'method' => 'put',
+                'args' => [
+                    'uploadurl' => PARAM_URL
+                ],
+                'response' => 'json'
+            ],
+            'create' => [
+                'endpoint' => 'https://www.googleapis.com/drive/v3/files',
+                'method' => 'post',
+                'args' => [
+                    'fields' => PARAM_RAW
+                ],
+                'response' => 'json'
+            ],
+            'delete' => [
+                'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}',
+                'method' => 'delete',
+                'args' => [
+                    'fileid' => PARAM_RAW
+                ],
+                'response' => 'json'
+            ],
+        ];
+    }
+}
diff --git a/files/converter/googledrive/lang/en/fileconverter_googledrive.php b/files/converter/googledrive/lang/en/fileconverter_googledrive.php
new file mode 100644 (file)
index 0000000..b99ed87
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for plugin 'fileconverter_googledrive'
+ *
+ * @package   fileconverter_googledrive
+ * @copyright 2017 Damyon Wiese
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['pluginname'] = 'Google Drive';
+$string['disabled'] = 'Disabled';
+$string['issuer'] = 'OAuth 2 Service';
+$string['issuer_help'] = 'The OAuth 2 service used to access google drive.';
+$string['test_converter'] = 'Test this converter is working properly.';
+$string['test_conversion'] = 'Test document conversion';
+$string['test_conversionready'] = 'This document converter is configured properly.';
+$string['test_conversionnotready'] = 'This document converter is not configured properly.';
+$string['test_issuerinvalid'] = 'The OAuth service in the document converter settings is set to an invalid value.';
+$string['test_issuernotenabled'] = 'The OAuth service set in the document converter settings is not enabled.';
+$string['test_issuernotconnected'] = 'The OAuth service set in the document converter settings does not have a system account connected.';
+$string['test_issuernotset'] = 'The OAuth service needs to be set in the document converter settings.';
diff --git a/files/converter/googledrive/lib.php b/files/converter/googledrive/lib.php
new file mode 100644 (file)
index 0000000..72b3a74
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This plugin is used to convert documents with google drive.
+ *
+ * @package    fileconverter_googledrive
+ * @copyright  2017 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Callback to get the required scopes for system account.
+ *
+ * @param \core\oauth2\issuer $issuer
+ * @return string
+ */
+function fileconverter_googledrive_oauth2_system_scopes(\core\oauth2\issuer $issuer) {
+    if ($issuer->get('id') == get_config('fileconverter_googledrive', 'issuerid')) {
+        return 'https://www.googleapis.com/auth/drive';
+    }
+    return '';
+}
diff --git a/files/converter/googledrive/settings.php b/files/converter/googledrive/settings.php
new file mode 100644 (file)
index 0000000..bcedc58
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Link to the OAuth 2 service we will use.
+ *
+ * @package   fileconverter_googledrive
+ * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($hassiteconfig) {
+    $options = [];
+    $issuers = \core\oauth2\api::get_all_issuers();
+
+    $options[''] = get_string('disabled', 'fileconverter_googledrive');
+    foreach ($issuers as $issuer) {
+        $options[$issuer->get('id')] = s($issuer->get('name'));
+    }
+
+    $settings->add(new admin_setting_configselect('fileconverter_googledrive/issuerid',
+                                                  get_string('issuer', 'fileconverter_googledrive'),
+                                                  get_string('issuer_help', 'fileconverter_googledrive'),
+                                                  '',
+                                                  $options));
+
+    $url = new moodle_url('/files/converter/googledrive/test.php');
+    $link = html_writer::link($url, get_string('test_converter', 'fileconverter_googledrive'));
+    $settings->add(new admin_setting_heading('test_converter', '', $link));
+}
diff --git a/files/converter/googledrive/test.php b/files/converter/googledrive/test.php
new file mode 100644 (file)
index 0000000..d090db1
--- /dev/null
@@ -0,0 +1,96 @@
+<?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/>.
+
+/**
+ * Test that googledrive is configured correctly
+ *
+ * @package   fileconverter_googledrive
+ * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+require(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/filelib.php');
+
+$sendpdf = optional_param('sendpdf', 0, PARAM_BOOL);
+
+$PAGE->set_url(new moodle_url('/files/converter/googledrive/test.php'));
+$PAGE->set_context(context_system::instance());
+
+require_login();
+require_capability('moodle/site:config', context_system::instance());
+
+$strheading = get_string('test_conversion', 'fileconverter_googledrive');
+$PAGE->navbar->add(get_string('administrationsite'));
+$PAGE->navbar->add(get_string('plugins', 'admin'));
+$PAGE->navbar->add(get_string('pluginname', 'fileconverter_googledrive'),
+        new moodle_url('/admin/settings.php', array('section' => 'fileconvertergoogledrive')));
+$PAGE->navbar->add($strheading);
+$PAGE->set_heading($strheading);
+$PAGE->set_title($strheading);
+
+$converter = new \fileconverter_googledrive\converter();
+
+if ($sendpdf) {
+    require_sesskey();
+
+    $converter->serve_test_document();
+    die();
+}
+
+$result = $converter->are_requirements_met();
+if ($result) {
+    $msg = $OUTPUT->notification(get_string('test_conversionready', 'fileconverter_googledrive'), 'success');
+    $pdflink = new moodle_url($PAGE->url, array('sendpdf' => 1, 'sesskey' => sesskey()));
+    $msg .= html_writer::link($pdflink, get_string('test_conversion', 'fileconverter_googledrive'));
+    $msg .= html_writer::empty_tag('br');
+} else {
+
+    // Diagnostics time.
+    $issuerid = get_config('fileconverter_googledrive', 'issuerid');
+    $msg = '';
+    if (empty($issuerid)) {
+        $msg = $OUTPUT->notification(get_string('test_issuernotset', 'fileconverter_googledrive'), 'warning');
+    }
+
+    if (empty($msg)) {
+        $issuer = \core\oauth2\api::get_issuer($issuerid);
+        if (empty($issuer)) {
+            $msg = $OUTPUT->notification(get_string('test_issuerinvalid', 'fileconverter_googledrive'), 'warning');
+        }
+    }
+
+    if (empty($msg)) {
+        if (!$issuer->get('enabled')) {
+            $msg = $OUTPUT->notification(get_string('test_issuernotenabled', 'fileconverter_googledrive'), 'warning');
+        }
+    }
+
+    if (empty($msg)) {
+        if (!$issuer->is_system_account_connected()) {
+            $msg = $OUTPUT->notification(get_string('test_issuernotconnected', 'fileconverter_googledrive'), 'warning');
+        }
+    }
+
+    if (empty($msg)) {
+        $msg = $OUTPUT->notification(get_string('test_conversionnotready', 'fileconverter_googledrive'), 'warning');
+    }
+}
+$returl = new moodle_url('/admin/settings.php', array('section' => 'fileconvertergoogledrive'));
+$msg .= $OUTPUT->continue_button($returl);
+
+echo $OUTPUT->header();
+echo $OUTPUT->box($msg, 'generalbox');
+echo $OUTPUT->footer();
diff --git a/files/converter/googledrive/tests/fixtures/source.docx b/files/converter/googledrive/tests/fixtures/source.docx
new file mode 100644 (file)
index 0000000..286c58a
Binary files /dev/null and b/files/converter/googledrive/tests/fixtures/source.docx differ
diff --git a/files/converter/googledrive/version.php b/files/converter/googledrive/version.php
new file mode 100644 (file)
index 0000000..7838390
--- /dev/null
@@ -0,0 +1,29 @@
+<?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/>.
+
+/**
+ * Version details
+ *
+ * @package   fileconverter_googledrive
+ * @copyright 2017 Damyon Wiese
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2017020600;         // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2016112900;         // Requires this Moodle version
+$plugin->component = 'fileconverter_googledrive'; // Full name of the plugin (used for diagnostics).
index 76cf28b..b652e6d 100644 (file)
@@ -271,7 +271,7 @@ class core_files_externallib_testcase extends advanced_testcase {
                                         'itemid' => $itemid,
                                         'filepath' => '/',
                                         'filename' => 'Simple4.txt',
-                                        'url' => 'http://www.example.com/moodle/pluginfile.php/'.$context->id.'/mod_data/content/'.$itemid.'/Simple4.txt',
+                                        'url' => 'https://www.example.com/moodle/pluginfile.php/'.$context->id.'/mod_data/content/'.$itemid.'/Simple4.txt',
                                         'isdir' => false,
                                         'timemodified' => $timemodified,
                                         'timecreated' => $timecreated,
index 8cb2ee3..da3576a 100644 (file)
@@ -54,7 +54,7 @@ class filter_emoticon_testcase extends advanced_testcase {
         $this->assertEquals($expected, $filter->filter('(grr)', $options));
 
         // And texts matching target formats are filtered.
-        $expected = '<img class="icon emoticon" alt="angry" title="angry" src="http://www.example.com/moodle/theme/image.php/_s/boost/core/1/s/angry" />';
+        $expected = '<img class="icon emoticon" alt="angry" title="angry" src="https://www.example.com/moodle/theme/image.php/_s/boost/core/1/s/angry" />';
         $options = array('originalformat' => FORMAT_HTML); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
         $this->assertEquals($expected, $filter->filter('(grr)', $options));
     }
index 25b1373..2df58eb 100644 (file)
@@ -622,6 +622,7 @@ $string['iplookupinfo'] = 'By default Moodle uses the free online NetGeo (The In
 It is recommended to install local copy of free GeoLite2 City database from MaxMind.<br />
 IP address location is displayed on simple map or using Google Maps. Please note that you need to have a Google account and apply for free Google Maps API key to enable interactive maps.';
 $string['iplookupmaxmindnote'] = 'This product includes GeoLite2 data created by MaxMind, available from <a href="http://www.maxmind.com">http://www.maxmind.com</a>.';
+$string['ishttpswarning'] = 'It has been detected that your site is not secured using HTTPS. For increased security and improved integrations with other systems is highly recommended to migrate your site to HTTPS.';
 $string['keeptagnamecase'] = 'Keep tag name casing';
 $string['lang'] = 'Default language';
 $string['langcache'] = 'Cache language menu';
@@ -800,6 +801,7 @@ $string['oauthrefreshtokenexpiredshort'] = 'OAuth refresh token expired for {$a-
 $string['onlynoreply'] = 'Only when from a no-reply address';
 $string['opcacherecommended'] = 'PHP opcode caching improves performance and lowers memory requirements, OPcache extension is recommended and fully supported.';
 $string['opensslrecommended'] = 'Installing the optional OpenSSL library is highly recommended -- it enables Moodle Networking functionality.';
+$string['opensslrequired'] = 'The OpenSSL PHP extension is now required by Moodle to provide stronger cryptographic services.';
 $string['opentogoogle'] = 'Open to Google';
 $string['optionalmaintenancemessage'] = 'Optional maintenance message';
 $string['order1'] = 'First';
index 50d9592..c289b3b 100644 (file)
@@ -29,9 +29,11 @@ $string['auth_common_settings'] = 'Common settings';
 $string['auth_data_mapping'] = 'Data mapping';
 $string['authenticationoptions'] = 'Authentication options';
 $string['auth_fieldlock'] = 'Lock value';
+$string['auth_fieldlockfield'] = 'Lock value ({$a})';
 $string['auth_fieldlock_expl'] = '<p><b>Lock value:</b> If enabled, will prevent Moodle users and admins from editing the field directly. Use this option if you are maintaining this data in the external auth system. </p>';
 $string['auth_fieldlocks'] = 'Lock user fields';
 $string['auth_fieldlocks_help'] = '<p>You can lock user data fields. This is useful for sites where the user data is maintained by the administrators manually by editing user records or uploading using the \'Upload users\' facility. If you are locking fields that are required by Moodle, make sure that you provide that data when creating user accounts or the accounts will be unusable.</p><p>Consider setting the lock mode to \'Unlocked if empty\' to avoid this problem.</p>';
+$string['auth_fieldmapping'] = 'Data mapping ({$a})';
 $string['auth_changepasswordhelp'] = 'Change password help';
 $string['auth_changepasswordhelp_expl'] = 'Display lost password help to users who have lost their {$a} password. This will be displayed either as well as or instead of the <strong>Change Password URL</strong> or Internal Moodle password change.';
 $string['auth_changepasswordurl'] = 'Change password URL';
@@ -53,8 +55,10 @@ $string['auth_sync_suspended']  = 'When enabled, the suspended attribute will be
 $string['auth_sync_suspended_key'] = 'Synchronise local user suspension status';
 $string['auth_sync_script'] = 'User account synchronisation';
 $string['auth_updatelocal'] = 'Update local';
+$string['auth_updatelocalfield'] = 'Update local ({$a})';
 $string['auth_updatelocal_expl'] = '<p><b>Update local:</b> If enabled, the field will be updated (from external auth) every time the user logs in or there is a user synchronisation. Fields set to update locally should be locked.</p>';
 $string['auth_updateremote'] = 'Update external';
+$string['auth_updateremotefield'] = 'Update external ({$a})';
 $string['auth_updateremote_expl'] = '<p><b>Update external:</b> If enabled, the external auth will be updated when the user record is updated. Fields should be unlocked to allow edits.</p>';
 $string['auth_updateremote_ldap'] = '<p><b>Note:</b> Updating external LDAP data requires that you set binddn and bindpw to a bind-user with editing privileges to all the user records. It currently does not preserve multi-valued attributes, and will remove extra values on update. </p>';
 $string['auth_user_create'] = 'Enable user creation';
index 41bc6c2..46d7092 100644 (file)
 
 $string['achievinggrade'] = 'Achieving grade';
 $string['activities'] = 'Activities';
+$string['activitieslabel'] = 'Activities / Resources';
 $string['activityaggregation'] = 'Condition requires';
 $string['activityaggregation_all'] = 'ALL selected activities to be completed';
 $string['activityaggregation_any'] = 'ANY selected activities to be completed';
 $string['activitiescompleted'] = 'Activity completion';
 $string['activitiescompletednote'] = 'Note: Activity completion must be set for an activity to appear in the above list.';
 $string['activitycompletion'] = 'Activity completion';
+$string['activitycompletionupdated'] = 'Changes saved';
+$string['affectedactivities'] = 'The changes will affect the following <b>{$a}</b> Activities/Resources';
 $string['aggregationmethod'] = 'Aggregation method';
 $string['all'] = 'All';
 $string['any'] = 'Any';
 $string['approval'] = 'Approval';
 $string['badautocompletion'] = 'When you select automatic completion, you must also enable at least one requirement (below).';
+$string['bulkactivitycompletion'] = 'Bulk edit activity completion';
+$string['bulkactivitydetail'] = 'Select the activities you wish to bulk edit.';
+$string['bulkcompletiontracking'] = 'Completion tracking';
+$string['bulkcompletiontracking_help'] = '<strong>None:</strong> Do not indicate activity completion
+
+<strong>Manual:</strong> Students can manually mark the activity as completed
+
+<strong>With condition(s):</strong> Show activity as complete when conditions are met';
+$string['checkall'] = 'Check or uncheck all activities and resources';
+$string['checkallsection'] = 'Check or uncheck all activities and resources in the following section: {$a}';
+$string['checkactivity'] = 'Checkbox for activity / resource: {$a}';
 $string['completed'] = 'Completed';
 $string['completedunlocked'] = 'Completion options unlocked';
 $string['completedunlockedtext'] = 'When you save changes, completion state for all students will be erased. If you change your mind about this, do not save the form.';
@@ -68,6 +82,7 @@ $string['completiondisabled'] = 'Disabled, not shown in activity settings';
 $string['completionenabled'] = 'Enabled, control via completion and activity settings';
 $string['completionexpected'] = 'Expect completed on';
 $string['completionexpected_help'] = 'This setting specifies the date when the activity is expected to be completed.';
+$string['completionexpecteddesc'] = 'Completion expected on {$a}';
 $string['completionexpectedfor'] = 'Expected completion for \'{$a->modulename}\' activity \'{$a->instancename}\'';
 $string['completionicons'] = 'Completion tick boxes';
 $string['completionicons_help'] = 'A tick next to an activity name may be used to indicate when the activity is complete.
@@ -86,6 +101,7 @@ $string['completionsettingslocked'] = 'Completion settings locked';
 $string['completionusegrade'] = 'Require grade';
 $string['completionusegrade_desc'] = 'Student must receive a grade to complete this activity';
 $string['completionusegrade_help'] = 'If enabled, the activity is considered complete when a student receives a grade. Pass and fail icons may be displayed if a pass grade for the activity has been set.';
+$string['completionupdated'] = 'Updated completion for activity <b>{$a}</b>';
 $string['completionview'] = 'Require view';
 $string['completionview_desc'] = 'Student must view this activity to complete it';
 $string['configcompletiondefault'] = 'The default setting for completion tracking when creating new activities.';
@@ -110,9 +126,12 @@ $string['csvdownload'] = 'Download in spreadsheet format (UTF-8 .csv)';
 $string['datepassed'] = 'Date passed';
 $string['days'] = 'Days';
 $string['daysoftotal'] = '{$a->days} of {$a->total}';
+$string['defaultcompletion'] = 'Default activity completion';
+$string['defaultcompletionupdated'] = 'Changes saved';
 $string['deletecompletiondata'] = 'Delete completion data';
 $string['dependencies'] = 'Dependencies';
 $string['dependenciescompleted'] = 'Completion of other courses';
+$string['hiddenrules'] = 'Some settings specific to <b>{$a}</b> have been hidden. To view unselect other activities';
 $string['editcoursecompletionsettings'] = 'Edit course completion settings';
 $string['enablecompletion'] = 'Enable completion tracking';
 $string['enablecompletion_help'] = 'If enabled, activity completion conditions may be set in the activity settings and/or course completion conditions may be set. It is recommended to have this enabled in order for the course progress dashboard to display meaningful data.';
@@ -128,11 +147,13 @@ $string['err_system'] = 'An internal error occurred in the completion system. (S
 $string['eventcoursecompleted'] = 'Course completed';
 $string['eventcoursecompletionupdated'] = 'Course completion updated';
 $string['eventcoursemodulecompletionupdated'] = 'Course module completion updated';
+$string['eventdefaultcompletionupdated'] = 'Default for course module completion updated';
 $string['excelcsvdownload'] = 'Download in Excel-compatible format (.csv)';
 $string['fraction'] = 'Fraction';
 $string['graderequired'] = 'Required course grade';
 $string['gradexrequired'] = '{$a} required';
 $string['inprogress'] = 'In progress';
+$string['manual'] = 'Manual';
 $string['manualcompletionby'] = 'Manual completion by others';
 $string['manualcompletionbynote'] = 'Note: The capability moodle/course:markcomplete must be allowed for a role to appear in the list.';
 $string['manualselfcompletion'] = 'Manual self completion';
@@ -140,8 +161,10 @@ $string['manualselfcompletionnote'] = 'Note: The self completion block should be
 $string['markcomplete'] = 'Mark complete';
 $string['markedcompleteby'] = 'Marked complete by {$a}';
 $string['markingyourselfcomplete'] = 'Marking yourself complete';
+$string['modifybulkactions'] = 'Modify the actions you wish to bulk edit';
 $string['moredetails'] = 'More details';
 $string['nocriteriaset'] = 'No completion criteria set for this course';
+$string['nogradeitem'] = 'Require grade can not be enabled for <b>{$a}</b> because grades are not available there';
 $string['notcompleted'] = 'Not completed';
 $string['notenroled'] = 'You are not enrolled in this course';
 $string['nottracked'] = 'You are currently not being tracked by completion in this course';
@@ -159,6 +182,7 @@ $string['remainingenroledfortime'] = 'Remaining enrolled for a specified period
 $string['remainingenroleduntildate'] = 'Remaining enrolled until a specified date';
 $string['reportpage'] = 'Showing users {$a->from} to {$a->to} of {$a->total}.';
 $string['requiredcriteria'] = 'Required criteria';
+$string['resetactivities'] = 'Clear all checked activities and resources';
 $string['restoringcompletiondata'] = 'Writing completion data';
 $string['roleaggregation'] = 'Condition requires';
 $string['roleaggregation_all'] = 'ALL selected roles to mark when the condition is met';
@@ -166,6 +190,7 @@ $string['roleaggregation_any'] = 'ANY selected roles to mark when the condition
 $string['roleidnotfound'] = 'Role ID {$a} not found';
 $string['saved'] = 'Saved';
 $string['seedetails'] = 'See details';
+$string['select'] = 'Select';
 $string['self'] = 'Self';
 $string['selfcompletion'] = 'Self completion';
 $string['showinguser'] = 'Showing user';
@@ -174,10 +199,12 @@ $string['unenrolment'] = 'Unenrolment';
 $string['unit'] = 'Unit';
 $string['unlockcompletion'] = 'Unlock completion options';
 $string['unlockcompletiondelete'] = 'Unlock completion options and delete user completion data';
+$string['updateactivities'] = 'Update completion status of checked activities';
 $string['usealternateselector'] = 'Use the alternate course selector';
 $string['usernotenroled'] = 'User is not enrolled in this course';
 $string['viewcoursereport'] = 'View course report';
 $string['viewingactivity'] = 'Viewing the {$a}';
+$string['withconditions'] = 'With conditions';
 $string['writingcompletiondata'] = 'Writing completion data';
 $string['xdays'] = '{$a} days';
 $string['yourprogress'] = 'Your progress';
index ced11af..657f887 100644 (file)
@@ -561,18 +561,25 @@ class auth_plugin_base {
 
     /**
      * Returns a list of potential IdPs that this authentication plugin supports.
-     * This is used to provide links on the login page.
      *
-     * @param string $wantsurl the relative url fragment the user wants to get to.  You can use this to compose a returnurl, for example
+     * This is used to provide links on the login page and the login block.
      *
-     * @return array like:
-     *              array(
-     *                  array(
-     *                      'url' => 'http://someurl',
-     *                      'icon' => new pix_icon(...),
-     *                      'name' => get_string('somename', 'auth_yourplugin'),
-     *                 ),
-     *             )
+     * The parameter $wantsurl is typically used by the plugin to implement a
+     * return-url feature.
+     *
+     * The returned value is expected to be a list of associative arrays with
+     * string keys:
+     *
+     * - url => (moodle_url|string) URL of the page to send the user to for authentication
+     * - name => (string) Human readable name of the IdP
+     * - iconurl => (moodle_url|string) URL of the icon representing the IdP (since Moodle 3.3)
+     *
+     * For legacy reasons, pre-3.3 plugins can provide the icon via the key:
+     *
+     * - icon => (pix_icon) Icon representing the IdP
+     *
+     * @param string $wantsurl The relative url fragment the user wants to get to.
+     * @return array List of associative arrays with keys url, name, iconurl|icon
      */
     function loginpage_idp_list($wantsurl) {
         return array();
@@ -614,10 +621,12 @@ class auth_plugin_base {
     /**
      * Return the list of enabled identity providers.
      *
-     * Each identity provider data contains the keys 'url' (string), 'name' (string) and 'icon' (pix_icon).
+     * Each identity provider data contains the keys url, name and iconurl (or
+     * icon). See the documentation of {@link auth_plugin_base::loginpage_idp_list()}
+     * for detailed description of the returned structure.
      *
      * @param array $authsequence site's auth sequence (list of auth plugins ordered)
-     * @return array an array of enabled identity providers
+     * @return array List of arrays describing the identity providers
      */
     public static function get_identity_providers($authsequence) {
         global $SESSION;
@@ -634,7 +643,7 @@ class auth_plugin_base {
     /**
      * Prepare a list of identity providers for output.
      *
-     * @param array $identityproviders
+     * @param array $identityproviders as returned by {@link self::get_identity_providers()}
      * @param renderer_base $output
      * @return array the identity providers ready for output
      */
@@ -642,8 +651,10 @@ class auth_plugin_base {
         $data = [];
         foreach ($identityproviders as $idp) {
             if (!empty($idp['icon'])) {
-                $idp['iconurl'] = $output->image_url($idp['icon']->key, $idp['icon']->component);
+                // Pre-3.3 auth plugins provide icon as a pix_icon instance.
+                $idp['iconurl'] = $output->image_url($idp['icon']->pix, $idp['icon']->component);
             } else if ($idp['iconurl'] instanceof moodle_url) {
+                // New auth plugins (since 3.3) provide iconurl.
                 $idp['iconurl'] = $idp['iconurl']->out(false);
             }
             unset($idp['icon']);
@@ -1024,26 +1035,26 @@ function display_auth_lock_options($settings, $auth, $userfields, $helptext, $ma
             // We are mapping to a remote field here.
             // Mapping.
             $settings->add(new admin_setting_configtext("auth_{$auth}/field_map_{$field}",
-                    $fieldname, '', '', PARAM_ALPHANUMEXT, 30));
+                    get_string('auth_fieldmapping', 'auth', $fieldname), '', '', PARAM_ALPHANUMEXT, 30));
 
             // Update local.
             $settings->add(new admin_setting_configselect("auth_{$auth}/field_updatelocal_{$field}",
-                    get_string('auth_updatelocal', 'auth'), '', 'oncreate', $updatelocaloptions));
+                    get_string('auth_updatelocalfield', 'auth', $fieldname), '', 'oncreate', $updatelocaloptions));
 
             // Update remote.
             if ($updateremotefields) {
                     $settings->add(new admin_setting_configselect("auth_{$auth}/field_updateremote_{$field}",
-                        get_string('auth_updateremote', 'auth'), '', 0, $updateextoptions));
+                        get_string('auth_updateremotefield', 'auth', $fieldname), '', 0, $updateextoptions));
             }
 
             // Lock fields.
             $settings->add(new admin_setting_configselect("auth_{$auth}/field_lock_{$field}",
-                    get_string('auth_fieldlock', 'auth'), '', 'unlocked', $lockoptions));
+                    get_string('auth_fieldlockfield', 'auth', $fieldname), '', 'unlocked', $lockoptions));
 
         } else {
             // Lock fields Only.
             $settings->add(new admin_setting_configselect("auth_{$auth}/field_lock_{$field}",
-                    get_string('auth_fieldlock', 'auth'), '', 'unlocked', $lockoptions));
+                    get_string('auth_fieldlockfield', 'auth', $fieldname), '', 'unlocked', $lockoptions));
         }
     }
 }
diff --git a/lib/classes/event/completion_defaults_updated.php b/lib/classes/event/completion_defaults_updated.php
new file mode 100644 (file)
index 0000000..daa0bd3
--- /dev/null
@@ -0,0 +1,101 @@
+<?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/>.
+
+/**
+ * Default completion for activity in a course updated event
+ *
+ * @package    core
+ * @copyright  2017 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Default completion for activity in a course updated event
+ *
+ * @package    core
+ * @since      Moodle 3.3
+ * @copyright  2017 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class completion_defaults_updated extends base {
+
+    /**
+     * Initialise the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'course_completion_defaults';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventdefaultcompletionupdated', 'completion');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/course/defaultcompletion.php', array('id' => $this->courseid));
+    }
+
+    /**
+     * Returns non-localised description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' updated the default completion for module " .
+            "'{$this->other['modulename']}' in course with id '$this->courseid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if ($this->contextlevel != CONTEXT_COURSE) {
+            throw new \coding_exception('Context passed must be course context.');
+        }
+        if (!isset($this->other['modulename'])) {
+            throw new \coding_exception('The \'modulename\' value must be set in other.');
+        }
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        parent::get_objectid_mapping();
+        return array('db' => 'course_completion_defaults', 'restore' => 'course_completion_defaults');
+    }
+}
index e58b07c..7960ea6 100644 (file)
@@ -180,9 +180,25 @@ class issuer extends persistent {
             return false;
         }
         $sys = system_account::get_record(['issuerid' => $this->get('id')]);
-        if (!empty($sys) and !empty($sys->get('refreshtoken'))) {
-            return true;
+        if (empty($sys) || empty($sys->get('refreshtoken'))) {
+            return false;
         }
-        return false;
+
+        $scopes = api::get_system_scopes_for_issuer($this);
+
+        $grantedscopes = $sys->get('grantedscopes');
+
+        $scopes = explode(' ', $scopes);
+
+        foreach ($scopes as $scope) {
+            if (!empty($scope)) {
+                if (strpos(' ' . $grantedscopes . ' ', ' ' . $scope . ' ') === false) {
+                    // We have not been granted all the scopes that are required.
+                    return false;
+                }
+            }
+        }
+
+        return true;
     }
 }
index b4a7873..ca48a35 100644 (file)
@@ -39,7 +39,7 @@ require_once($CFG->libdir . '/filelib.php');
 abstract class rest {
 
     /** @var curl $curl */
-    private $curl;
+    protected $curl;
 
     /**
      * Constructor.
@@ -66,7 +66,7 @@ abstract class rest {
      * @param string $rawpost Optional param to include in the body of a post.
      * @return string|object
      */
-    public function call($functionname, $functionargs, $rawpost = false) {
+    public function call($functionname, $functionargs, $rawpost = false, $contenttype = false) {
         $functions = $this->get_api_functions();
         $supportedmethods = [ 'get', 'put', 'post', 'patch', 'head', 'delete' ];
         if (empty($functions[$functionname])) {
@@ -106,7 +106,11 @@ abstract class rest {
             $callargs = $rawpost;
         }
 
-        $this->curl->setHeader('Content-type: application/json');
+        if (empty($contenttype)) {
+            $this->curl->setHeader('Content-type: application/json');
+        } else {
+            $this->curl->setHeader('Content-type: ' . $contenttype);
+        }
         $response = $this->curl->$method($endpoint, $callargs);
 
         if ($this->curl->errno == 0) {
@@ -117,6 +121,8 @@ abstract class rest {
                     throw new rest_exception($json->error->code . ': ' . $json->error->message);
                 }
                 return $json;
+            } else if ($responsetype == 'headers') {
+                $response = $this->curl->get_raw_response();
             }
             return $response;
         } else {
index 15ea1d2..ef217a6 100644 (file)
@@ -199,6 +199,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:i/cohort' => 'fa-users',
             'core:i/competencies' => 'fa-check-square-o',
             'core:i/completion_self' => 'fa-user-o',
+            'core:i/dashboard' => 'fa-tachometer',
             'core:i/lock' => 'fa-lock',
             'core:i/courseevent' => 'fa-calendar',
             'core:i/db' => 'fa-database',
index 04a0105..22da707 100644 (file)
@@ -1757,7 +1757,7 @@ class core_plugin_manager {
             ),
 
             'fileconverter' => array(
-                'unoconv'
+                'unoconv', 'googledrive'
             ),
 
             'editor' => array(
index 3b9525d..39ab0c6 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20170310" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20170316" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="uniqinternal" TYPE="unique" FIELDS="issuerid, internalfield"/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="course_completion_defaults" COMMENT="Default settings for activities completion">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="module" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="completion" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="completionview" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="completionusegrade" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="completionexpected" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="customrules" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="module" TYPE="foreign" FIELDS="module" REFTABLE="modules" REFFIELDS="id"/>
+        <KEY NAME="course" TYPE="foreign" FIELDS="course" REFTABLE="course" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="coursemodule" UNIQUE="true" FIELDS="course, module"/>
+      </INDEXES>
+    </TABLE>
   </TABLES>
 </XMLDB>
index dd84448..007594f 100644 (file)
@@ -2613,7 +2613,7 @@ function xmldb_main_upgrade($oldversion) {
 
     if ($oldversion < 2017040400.00) {
 
-        // If the 'Course overview' block is no longer present, replace with the 'My overview' block.
+        // If block_course_overview is no longer present, replace with block_myoverview.
         if (!file_exists($CFG->dirroot . '/blocks/course_overview/block_course_overview.php')) {
             $DB->set_field('block_instances', 'blockname', 'myoverview', array('blockname' => 'course_overview'));
         }
@@ -2623,10 +2623,10 @@ function xmldb_main_upgrade($oldversion) {
 
     if ($oldversion < 2017040401.00) {
 
-        // If the 'Course overview' block is no longer present, remove it.
+        // If block_course_overview is no longer present, remove it.
         // Note - we do not need to completely remove the block context etc because we
-        // have replaced all occurrences of the 'Course overview' block with the 'My overview'
-        // block in the upgrade step above.
+        // have replaced all occurrences of block_course_overview with block_myoverview
+        // in the upgrade step above.
         if (!file_exists($CFG->dirroot . '/blocks/course_overview/block_course_overview.php')) {
             // Delete the block from the block table.
             $DB->delete_records('block', array('name' => 'course_overview'));
@@ -2803,5 +2803,36 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017040700.04);
     }
 
+    if ($oldversion < 2017041801.00) {
+
+        // Define table course_completion_defaults to be created.
+        $table = new xmldb_table('course_completion_defaults');
+
+        // Adding fields to table course_completion_defaults.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('module', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('completion', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('completionview', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('completionusegrade', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('completionexpected', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('customrules', XMLDB_TYPE_TEXT, null, null, null, null, null);
+
+        // Adding keys to table course_completion_defaults.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('module', XMLDB_KEY_FOREIGN, array('module'), 'modules', array('id'));
+        $table->add_key('course', XMLDB_KEY_FOREIGN, array('course'), 'course', array('id'));
+
+        // Adding indexes to table course_completion_defaults.
+        $table->add_index('coursemodule', XMLDB_INDEX_UNIQUE, array('course', 'module'));
+
+        // Conditionally launch create table for course_completion_defaults.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        upgrade_main_savepoint(true, 2017041801.00);
+    }
+
     return true;
 }
index dfa2dca..25f7c3f 100644 (file)
@@ -290,8 +290,11 @@ function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $che
         return false;
     }
 
-    list($plugins, $params) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee');
-    $params['enabled'] = ENROL_INSTANCE_ENABLED;
+    list($plugins1, $params1) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee1');
+    list($plugins2, $params2) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee2');
+    $params = array_merge($params1, $params2);
+    $params['enabled1'] = ENROL_INSTANCE_ENABLED;
+    $params['enabled2'] = ENROL_INSTANCE_ENABLED;
     $params['active1'] = ENROL_USER_ACTIVE;
     $params['active2'] = ENROL_USER_ACTIVE;
     $params['user1']   = $user1;
@@ -309,11 +312,12 @@ function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $che
               FROM {course} c
               JOIN (
                 SELECT DISTINCT c.id
-                  FROM {enrol} e
-                  JOIN {user_enrolments} ue1 ON (ue1.enrolid = e.id AND ue1.status = :active1 AND ue1.userid = :user1)
-                  JOIN {user_enrolments} ue2 ON (ue2.enrolid = e.id AND ue2.status = :active2 AND ue2.userid = :user2)
-                  JOIN {course} c ON (c.id = e.courseid AND c.visible = 1)
-                 WHERE e.status = :enabled AND e.enrol $plugins
+                  FROM {course} c
+                  JOIN {enrol} e1 ON (c.id = e1.courseid AND e1.status = :enabled1 AND e1.enrol $plugins1)
+                  JOIN {user_enrolments} ue1 ON (ue1.enrolid = e1.id AND ue1.status = :active1 AND ue1.userid = :user1)
+                  JOIN {enrol} e2 ON (c.id = e2.courseid AND e2.status = :enabled2 AND e2.enrol $plugins2)
+                  JOIN {user_enrolments} ue2 ON (ue2.enrolid = e2.id AND ue2.status = :active2 AND ue2.userid = :user2)
+                 WHERE c.visible = 1
               ) ec ON ec.id = c.id
               $ctxjoin";
 
index 0c314f5..b1dafb9 100644 (file)
@@ -3603,20 +3603,31 @@ class curl {
      * @return bool
      */
     public function put($url, $params = array(), $options = array()) {
-        $file = $params['file'];
-        if (!is_file($file)) {
-            return null;
-        }
-        $fp   = fopen($file, 'r');
-        $size = filesize($file);
-        $options['CURLOPT_PUT']        = 1;
-        $options['CURLOPT_INFILESIZE'] = $size;
-        $options['CURLOPT_INFILE']     = $fp;
-        if (!isset($this->options['CURLOPT_USERPWD'])) {
-            $this->setopt(array('CURLOPT_USERPWD'=>'anonymous: noreply@moodle.org'));
+        $file = '';
+        $fp = false;
+        if (isset($params['file'])) {
+            $file = $params['file'];
+            if (is_file($file)) {
+                $fp   = fopen($file, 'r');
+                $size = filesize($file);
+                $options['CURLOPT_PUT']        = 1;
+                $options['CURLOPT_INFILESIZE'] = $size;
+                $options['CURLOPT_INFILE']     = $fp;
+            } else {
+                return null;
+            }
+            if (!isset($this->options['CURLOPT_USERPWD'])) {
+                $this->setopt(array('CURLOPT_USERPWD' => 'anonymous: noreply@moodle.org'));
+            }
+        } else {
+            $options['CURLOPT_CUSTOMREQUEST'] = 'PUT';
+            $options['CURLOPT_POSTFIELDS'] = $params;
         }
+
         $ret = $this->request($url, $options);
-        fclose($fp);
+        if ($fp !== false) {
+            fclose($fp);
+        }
         return $ret;
     }
 
index 2331474..aefd190 100644 (file)
@@ -4986,6 +4986,9 @@ function remove_course_contents($courseid, $showfeedback = true, array $options
 
     // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
 
+    // Delete completion defaults.
+    $DB->delete_records("course_completion_defaults", array("course" => $courseid));
+
     // Remove all data from availability and completion tables that is associated
     // with course-modules belonging to this course. Note this is done even if the
     // features are not enabled now, in case they were enabled previously.
index 3467ecc..13c5ae7 100644 (file)
@@ -4207,14 +4207,14 @@ class settings_navigation extends navigation_node {
             $coursenode->add($editstring, $editurl, self::TYPE_SETTING, null, 'turneditingonoff', new pix_icon('i/edit', ''));
         }
 
-        if ($adminoptions->update) {
-
+        if ($adminoptions->editcompletion) {
             // Add the course completion settings link
-            if ($CFG->enablecompletion && $course->enablecompletion) {
-                $url = new moodle_url('/course/completion.php', array('id'=>$course->id));
-                $coursenode->add(get_string('coursecompletion', 'completion'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/settings', ''));
-            }
-        } else if ($adminoptions->tags) {
+            $url = new moodle_url('/course/completion.php', array('id' => $course->id));
+            $coursenode->add(get_string('coursecompletion', 'completion'), $url, self::TYPE_SETTING, null, null,
+                             new pix_icon('i/settings', ''));
+        }
+
+        if (!$adminoptions->update && $adminoptions->tags) {
             $url = new moodle_url('/course/tags.php', array('id' => $course->id));
             $coursenode->add(get_string('coursetags', 'tag'), $url, self::TYPE_SETTING, null, 'coursetags', new pix_icon('i/settings', ''));
         }