Merge branch 'MDL-59518-master' of git://github.com/ankitagarwal/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 1 Aug 2017 05:44:16 +0000 (13:44 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 1 Aug 2017 05:44:16 +0000 (13:44 +0800)
109 files changed:
admin/registration/index.php
admin/registration/register.php
admin/renderer.php
admin/search.php
admin/tests/behat/manage_tokens.feature [new file with mode: 0644]
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/model.php
admin/tool/behat/renderer.php
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/monitor/tests/behat/rule.feature
admin/webservice/tokens.php
analytics/classes/model.php
availability/condition/completion/classes/frontend.php
availability/condition/completion/lang/en/availability_completion.php
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js
availability/condition/completion/yui/src/form/js/form.js
availability/condition/grade/tests/behat/availability_grade.feature
availability/tests/behat/edit_availability.feature
backup/util/ui/tests/behat/restore_moodle2_courses.feature
blocks/activity_modules/tests/behat/block_activity_modules.feature
blocks/activity_results/tests/behat/addunsupportedactivity.feature
blocks/blog_recent/tests/behat/block_blog_recent_course.feature
blocks/calendar_month/tests/behat/block_calendar_month.feature
blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature
calendar/tests/behat/calendar_lookahead.feature
completion/tests/behat/bulk_edit_activity_completion.feature
completion/tests/behat/default_activity_completion.feature
course/classes/output/activity_navigation.php [new file with mode: 0644]
course/lib.php
course/modedit.php
course/renderer.php
course/templates/activity_navigation.mustache [new file with mode: 0644]
course/tests/behat/navigate_course_list.feature
course/tests/behat/paged_course_navigation.feature
enrol/meta/tests/behat/enrol_meta.feature
enrol/tests/behat/enrol_user.feature
grade/tests/behat/grade_minmax.feature
lang/en/admin.php
lang/en/deprecated.txt
lang/en/form.php
lang/en/hub.php
lang/en/moodle.php
lang/en/role.php
lang/en/webservice.php
lib/adminlib.php
lib/blocklib.php
lib/classes/oauth2/api.php
lib/db/access.php
lib/db/install.xml
lib/db/upgrade.php
lib/ddl/mysql_sql_generator.php
lib/dml/mariadb_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/form/filemanager.php
lib/form/filepicker.php
lib/oauthlib.php
lib/outputrenderers.php
media/player/youtube/classes/plugin.php
media/player/youtube/tests/player_test.php
message/output/airnotifier/requestaccesskey.php
mod/assign/submission/file/lang/en/assignsubmission_file.php
mod/assign/submission/file/lang/en/deprecated.txt [new file with mode: 0644]
mod/assign/submission/file/locallib.php
mod/assign/submission/file/settings.php
mod/assign/submission/file/tests/behat/file_type_restriction.feature
mod/assign/submission/file/tests/locallib_test.php
mod/book/tests/behat/edit_tags.feature
mod/data/classes/external.php
mod/data/classes/external/content_exporter.php
mod/data/locallib.php
mod/data/tests/behat/completion_condition_entries.feature
mod/data/tests/externallib_test.php
mod/data/upgrade.txt
mod/feedback/tests/behat/anonymous.feature
mod/feedback/tests/behat/coursemapping.feature
mod/feedback/tests/behat/multipleattempt.feature
mod/forum/tests/behat/posts_ordering_blog.feature
mod/glossary/tests/behat/edit_tags.feature
mod/lesson/tests/behat/duplicate_lesson_page.feature
mod/lesson/tests/behat/import_fillintheblank_question.feature
mod/lesson/tests/behat/import_images.feature
mod/lesson/tests/behat/questions_images.feature
mod/quiz/mod_form.php
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/resource/view.php
mod/scorm/lib.php
mod/upgrade.txt
mod/url/view.php
mod/wiki/lib.php
mod/workshop/lib.php
theme/boost/scss/moodle/search.scss
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/bootstrapbase/layout/columns1.php
theme/bootstrapbase/layout/columns3.php
theme/clean/layout/columns1.php
theme/clean/layout/columns3.php
theme/upgrade.txt
user/action_redir.php
user/index.php
user/tests/behat/bulk_editenrolment.feature [new file with mode: 0644]
user/tests/behat/course_preference.feature
user/tests/behat/set_default_homepage.feature
version.php
webservice/classes/token_table.php [new file with mode: 0644]
webservice/lib.php

index 8b33014..b9aeb18 100644 (file)
@@ -181,13 +181,8 @@ if (empty($cancel) and $unregistration and !$confirm) {
     echo $OUTPUT->header();
 
     //check if the site is registered on Moodle.org and display a message about registering on MOOCH
-    $registered = $DB->count_records('registration_hubs', array('huburl' => HUB_MOODLEORGHUBURL, 'confirmed' => 1));
-    if (empty($registered)) {
-        $warningmsg = get_string('registermoochtips', 'hub');
-        $warningmsg .= $renderer->single_button(new moodle_url('register.php', array('huburl' => HUB_MOODLEORGHUBURL
-                    , 'hubname' => 'Moodle.org')), get_string('register', 'admin'));
-        echo $renderer->box($warningmsg, 'buttons mdl-align generalbox adminwarning');
-    }
+    $adminrenderer = $PAGE->get_renderer('core', 'admin');
+    echo $adminrenderer->warn_if_not_registered();
 
     //do not check sesskey if confirm = false because this script is linked into email message
     if (!empty($errormessage)) {
index 895ae5c..43d2e42 100644 (file)
@@ -189,17 +189,19 @@ if (!empty($error)) {
 
 // Some Moodle.org registration explanation.
 if ($huburl == HUB_MOODLEORGHUBURL) {
+    $notificationtype = \core\output\notification::NOTIFY_ERROR;
     if (!empty($registeredhub->token)) {
         if ($registeredhub->timemodified == 0) {
             $registrationmessage = get_string('pleaserefreshregistrationunknown', 'admin');
         } else {
             $lastupdated = userdate($registeredhub->timemodified, get_string('strftimedate', 'langconfig'));
             $registrationmessage = get_string('pleaserefreshregistration', 'admin', $lastupdated);
+            $notificationtype = \core\output\notification::NOTIFY_INFO;
         }
     } else {
         $registrationmessage = get_string('registrationwarning', 'admin');
     }
-    echo $OUTPUT->notification($registrationmessage);
+    echo $OUTPUT->notification($registrationmessage, $notificationtype);
 
     echo $OUTPUT->heading(get_string('registerwithmoodleorg', 'admin'));
     $renderer = $PAGE->get_renderer('core', 'register');
index 2d6c362..f7d1516 100644 (file)
@@ -782,17 +782,36 @@ class core_admin_renderer extends plugin_renderer_base {
 
         if (!$registered) {
 
-            $registerbutton = $this->single_button(new moodle_url('/admin/registration/register.php',
+            if (has_capability('moodle/site:config', context_system::instance())) {
+                $registerbutton = $this->single_button(new moodle_url('/admin/registration/register.php',
                     array('huburl' =>  HUB_MOODLEORGHUBURL, 'hubname' => 'Moodle.org')),
                     get_string('register', 'admin'));
+                $str = 'registrationwarning';
+            } else {
+                $registerbutton = '';
+                $str = 'registrationwarningcontactadmin';
+            }
 
-            return $this->warning( get_string('registrationwarning', 'admin')
-                    . '&nbsp;' . $this->help_icon('registration', 'admin') . $registerbutton );
+            return $this->warning( get_string($str, 'admin')
+                    . '&nbsp;' . $this->help_icon('registration', 'admin') . $registerbutton ,
+                'error alert alert-danger');
         }
 
         return '';
     }
 
+    /**
+     * Return an admin page warning if site is not registered with moodle.org
+     *
+     * @return string
+     */
+    public function warn_if_not_registered() {
+        global $CFG;
+        require_once($CFG->dirroot . '/' . $CFG->admin . '/registration/lib.php');
+        $registrationmanager = new registration_manager();
+        return $this->registration_warning($registrationmanager->get_registeredhub(HUB_MOODLEORGHUBURL) ? true : false);
+    }
+
     /**
      * Helper method to render the information about the available Moodle update
      *
index 7dd8e2c..fb38431 100644 (file)
@@ -38,6 +38,12 @@ if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and
 // to modify them
 echo $OUTPUT->header($focus);
 
+// Display a warning if site is not registered.
+if (empty($query)) {
+    $adminrenderer = $PAGE->get_renderer('core', 'admin');
+    echo $adminrenderer->warn_if_not_registered();
+}
+
 echo $OUTPUT->heading(get_string('administrationsite'));
 
 if ($errormsg !== '') {
diff --git a/admin/tests/behat/manage_tokens.feature b/admin/tests/behat/manage_tokens.feature
new file mode 100644 (file)
index 0000000..d30e230
--- /dev/null
@@ -0,0 +1,26 @@
+@core @core_admin
+Feature: Manage tokens
+  In order to manage webservice usage
+  As an admin
+  I need to be able to create and delete tokens
+
+  Background:
+    Given the following "users" exist:
+    | username  | password  | firstname | lastname |
+    | testuser  | testuser  | Joe | Bloggs |
+    | testuser2 | testuser2 | TestFirstname | TestLastname |
+    And I log in as "admin"
+    And I am on site homepage
+
+  @javascript
+  Scenario: Add & delete a token
+    Given I navigate to "Plugins > Web services > Manage tokens" in site administration
+    And I follow "Add"
+    And I set the field "User" to "Joe Bloggs"
+    And I set the field "IP restriction" to "127.0.0.1"
+    When I press "Save changes"
+    Then I should see "Joe Bloggs"
+    And I should see "127.0.0.1"
+    And I follow "Delete"
+    And I press "Delete"
+    And I should not see "Joe Bloggs"
index 181b48d..36f1feb 100644 (file)
@@ -117,6 +117,20 @@ class models_list implements \renderable, \templatable {
                 $actionsmenu->add($icon);
             }
 
+            // Enable / disable.
+            if ($model->is_enabled()) {
+                $action = 'disable';
+                $text = get_string('disable');
+                $icontype = 't/block';
+            } else {
+                $action = 'enable';
+                $text = get_string('enable');
+                $icontype = 'i/checked';
+            }
+            $url = new \moodle_url('model.php', array('action' => $action, 'id' => $model->get_id()));
+            $icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
+            $actionsmenu->add($icon);
+
             // Evaluate machine-learning-based models.
             if ($model->get_indicators() && !$model->is_static()) {
                 $url = new \moodle_url('model.php', array('action' => 'evaluate', 'id' => $model->get_id()));
index 2c64179..9ef3c3d 100644 (file)
@@ -51,6 +51,13 @@ switch ($action) {
     case 'log':
         $title = get_string('viewlog', 'tool_analytics');
         break;
+    case 'enable':
+        $title = get_string('enable');
+        break;
+    case 'disable':
+        $title = get_string('disable');
+        break;
+
     default:
         throw new moodle_exception('errorunknownaction', 'analytics');
 }
@@ -63,6 +70,14 @@ $PAGE->set_heading($title);
 
 switch ($action) {
 
+    case 'enable':
+        $model->enable();
+        redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+
+    case 'disable':
+        $model->update(0, false, false);
+        redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+
     case 'edit':
 
         if ($model->is_static()) {
index 8667fa9..9bcda1f 100644 (file)
@@ -24,9 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-global $CFG;
-require_once($CFG->libdir . '/behat/classes/behat_selectors.php');
-
 /**
  * Renderer for behat tool web features
  *
@@ -44,6 +41,8 @@ class tool_behat_renderer extends plugin_renderer_base {
      * @return string HTML code
      */
     public function render_stepsdefinitions($stepsdefinitions, $form) {
+        global $CFG;
+        require_once($CFG->libdir . '/behat/classes/behat_selectors.php');
 
         $html = $this->generic_info();
 
index d8a325b..0e62104 100644 (file)
@@ -221,7 +221,7 @@ Feature: Set up contextual data for tests
     And I should see "Test workshop name"
     And I follow "Test assignment name"
     And I should see "Test assignment description"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I follow "Test assignment name with scale"
     And I follow "Edit settings"
     And the field "Type" matches value "Scale"
@@ -312,7 +312,6 @@ Feature: Set up contextual data for tests
       | fullname | course | gradecategory |
       | Grade sub category 2 | C1 | Grade category 1 |
     When I log in as "admin"
-    And I am on course index
     And I am on "Course 1" course homepage
     And I navigate to "View > Grader report" in the course gradebook
     Then I should see "Grade category 1"
index a38a75d..de818df 100644 (file)
@@ -17,7 +17,6 @@ Feature: tool_monitor_rule
     And I log in as "admin"
     And I navigate to "Event monitoring rules" node in "Site administration > Reports"
     And I click on "Enable" "link"
-    And I am on site homepage
     And I am on "Course 1" course homepage
     And I navigate to "Event monitoring rules" node in "Course administration > Reports"
     And I press "Add a new rule"
index 60ea271..f15f7c2 100644 (file)
@@ -101,7 +101,11 @@ switch ($action) {
         break;
 
     case 'delete':
-        $token = $webservicemanager->get_created_by_user_ws_token($USER->id, $tokenid);
+        $token = $webservicemanager->get_token_by_id_with_details($tokenid);
+
+        if ($token->creatorid != $USER->id) {
+            require_capability("moodle/webservice:managealltokens", context_system::instance());
+        }
 
         //Delete the token
         if ($confirm and confirm_sesskey()) {
index 79352cb..985d655 100644 (file)
@@ -395,20 +395,30 @@ class model {
      * Updates the model.
      *
      * @param int|bool $enabled
-     * @param \core_analytics\local\indicator\base[] $indicators
-     * @param string $timesplittingid
+     * @param \core_analytics\local\indicator\base[]|false $indicators False to respect current indicators
+     * @param string|false $timesplittingid False to respect current time splitting method
      * @return void
      */
-    public function update($enabled, $indicators, $timesplittingid = '') {
+    public function update($enabled, $indicators = false, $timesplittingid = '') {
         global $USER, $DB;
 
         \core_analytics\manager::check_can_manage_models();
 
         $now = time();
 
-        $indicatorclasses = self::indicator_classes($indicators);
+        if ($indicators !== false) {
+            $indicatorclasses = self::indicator_classes($indicators);
+            $indicatorsstr = json_encode($indicatorclasses);
+        } else {
+            // Respect current value.
+            $indicatorsstr = $this->model->indicators;
+        }
+
+        if ($timesplittingid === false) {
+            // Respect current value.
+            $timesplittingid = $this->model->timesplitting;
+        }
 
-        $indicatorsstr = json_encode($indicatorclasses);
         if ($this->model->timesplitting !== $timesplittingid ||
                 $this->model->indicators !== $indicatorsstr) {
             // We update the version of the model so different time splittings are not mixed up.
@@ -900,7 +910,7 @@ class model {
     /**
      * Enabled the model using the provided time splitting method.
      *
-     * @param string $timesplittingid
+     * @param string|false $timesplittingid False to respect the current time splitting method.
      * @return void
      */
     public function enable($timesplittingid = false) {
index 8b425d7..7427328 100644 (file)
@@ -65,8 +65,9 @@ class frontend extends \core_availability\frontend {
                 // Add each course-module if it has completion turned on and is not
                 // the one currently being edited.
                 if ($othercm->completion && (empty($cm) || $cm->id != $id) && !$othercm->deletioninprogress) {
-                    $cms[] = (object)array('id' => $id, 'name' =>
-                            format_string($othercm->name, true, array('context' => $context)));
+                    $cms[] = (object)array('id' => $id,
+                        'name' => format_string($othercm->name, true, array('context' => $context)),
+                        'completiongradeitemnumber' => $othercm->completiongradeitemnumber);
                 }
             }
 
index aa41e64..938cbab 100644 (file)
@@ -24,6 +24,7 @@
 
 $string['description'] = 'Require students to complete (or not complete) another activity.';
 $string['error_selectcmid'] = 'You must select an activity for the completion condition.';
+$string['error_selectcmidpassfail'] = 'You must select an activity with "Require grade" completion condition set.';
 $string['label_cm'] = 'Activity or resource';
 $string['label_completion'] = 'Required completion status';
 $string['missing'] = '(Missing activity)';
index 4885548..5212364 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js differ
index 36efbb5..10be26c 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js differ
index 4885548..5212364 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js differ
index e7b418e..77c9cf1 100644 (file)
@@ -76,4 +76,14 @@ M.availability_completion.form.fillErrors = function(errors, node) {
     if (cmid === 0) {
         errors.push('availability_completion:error_selectcmid');
     }
+    var e = parseInt(node.one('select[name=e]').get('value'), 10);
+    if (((e === 2) || (e === 3))) {
+        this.cms.forEach(function(cm) {
+            if (cm.id === cmid) {
+                if (cm.completiongradeitemnumber === null) {
+                    errors.push('availability_completion:error_selectcmidpassfail');
+                }
+            }
+        });
+    }
 };
index a984525..546e378 100644 (file)
@@ -96,7 +96,7 @@ Feature: availability_grade
     And I click on "Add submission" "button"
     And I set the field "Online text" to "Q"
     And I click on "Save changes" "button"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
 
     # None of the pages should appear (check assignment though).
     Then I should not see "P2" in the "region-main" "region"
index ce0392e..3c35227 100644 (file)
@@ -36,7 +36,7 @@ Feature: edit_availability
     And I add a "Page" to section "1"
     Then "Restrict access" "fieldset" should not exist
 
-    Given I follow "C1"
+    Given I am on "Course 1" course homepage
     When I edit the section "1"
     Then "Restrict access" "fieldset" should not exist
 
@@ -47,7 +47,7 @@ Feature: edit_availability
     And I add a "Page" to section "1"
     Then "Restrict access" "fieldset" should exist
 
-    Given I follow "C1"
+    Given I am on "Course 1" course homepage
     When I edit the section "1"
     Then "Restrict access" "fieldset" should exist
 
index a2886be..d34b8d5 100644 (file)
@@ -169,8 +169,7 @@ Feature: Restore Moodle 2 course backups
     When I backup "Course 1" course using this options:
       | Initial |  Include enrolled users | 0 |
       | Confirmation | Filename | test_backup.mbz |
-    And I am on site homepage
-    And I follow "Course 2"
+    And I am on "Course 2" course homepage
     And I navigate to "Restore" node in "Course administration"
     And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
       | Schema | Overwrite course configuration | Yes |
@@ -199,8 +198,7 @@ Feature: Restore Moodle 2 course backups
     When I backup "Course 1" course using this options:
       | Initial |  Include enrolled users | 0 |
       | Confirmation | Filename | test_backup.mbz |
-    And I am on site homepage
-    And I follow "Course 2"
+    And I am on "Course 2" course homepage
     And I navigate to "Restore" node in "Course administration"
     And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
       | Schema | Overwrite course configuration | No |
@@ -229,8 +227,7 @@ Feature: Restore Moodle 2 course backups
     When I backup "Course 1" course using this options:
       | Initial |  Include enrolled users | 0 |
       | Confirmation | Filename | test_backup.mbz |
-    And I am on site homepage
-    And I follow "Course 4"
+    And I am on "Course 4" course homepage
     And I navigate to "Restore" node in "Course administration"
     And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
       | Schema | Overwrite course configuration | No |
index 2f24252..63e6dba 100644 (file)
@@ -109,7 +109,6 @@ Feature: Block activity modules
       | workshop   | Test workshop name     | Test workshop description     | C1     | workshop1   |
 
     When I log in as "admin"
-    And I am on course index
     And I am on "Course 1" course homepage with editing mode on
     And I add the "Activities" block
     And I click on "Assignments" "link" in the "Activities" "block"
index 5103f23..982510d 100644 (file)
@@ -22,7 +22,7 @@ Feature: The activity results block doesn't display student scores for unsupport
       | Assignment name | Test assignment |
       | Description | Offline text |
       | assignsubmission_file_enabled | 0 |
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I add the "Activity results" block
     And I configure the "Activity results" block
     And I set the following fields to these values:
index 67ee460..8814cc4 100644 (file)
@@ -32,7 +32,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
     And I press "Save changes"
     Then I should see "S1 First Blog"
     And I should see "This is my awesome blog!"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I should see "S1 First Blog"
     And I follow "S1 First Blog"
     And I should see "This is my awesome blog!"
@@ -47,7 +47,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
       | Blog entry body | This is my awesome blog! |
     And I press "Save changes"
     And I wait "1" seconds
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I follow "Add an entry about this course"
     # Blog 2 of 5
     And I set the following fields to these values:
@@ -57,7 +57,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
     And I wait "1" seconds
     And I should see "S1 Second Blog"
     And I should see "This is my awesome blog!"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I follow "Add an entry about this course"
     # Blog 3 of 5
     And I set the following fields to these values:
@@ -67,7 +67,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
     And I wait "1" seconds
     And I should see "S1 Third Blog"
     And I should see "This is my awesome blog!"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I follow "Add an entry about this course"
     # Blog 4 of 5
     And I set the following fields to these values:
@@ -77,7 +77,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
     And I wait "1" seconds
     And I should see "S1 Fourth Blog"
     And I should see "This is my awesome blog!"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I follow "Add an entry about this course"
     # Blog 5 of 5
     And I set the following fields to these values:
@@ -86,7 +86,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
     And I press "Save changes"
     And I should see "S1 Fifth Blog"
     And I should see "This is my awesome blog!"
-    When I follow "C1"
+    When I am on "Course 1" course homepage
     And I should not see "S1 First Blog"
     And I should see "S1 Second Blog"
     And I should see "S1 Third Blog"
index db6f058..15e30e1 100644 (file)
@@ -81,7 +81,6 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I create a calendar event with form data:
       | id_eventtype | User |
       | id_name | User Event |
-    When I am on homepage
     And I am on "Course 1" course homepage
     And I follow "Hide course events"
     And I hover over today in the calendar
@@ -96,8 +95,7 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I create a calendar event with form data:
       | id_eventtype | User |
       | id_name | User Event |
-    When I am on homepage
-    And I am on "Course 1" course homepage
+    When I am on "Course 1" course homepage
     And I hover over today in the calendar
     Then I should see "User Event"
 
@@ -113,8 +111,7 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I create a calendar event with form data:
       | id_eventtype | User |
       | id_name | User Event |
-    When I am on homepage
-    And I am on "Course 1" course homepage
+    When I am on "Course 1" course homepage
     And I follow "Hide user events"
     And I hover over today in the calendar
     Then I should not see "User Event"
index a92c042..d41fe98 100644 (file)
@@ -60,7 +60,7 @@ Feature: Enable Block Completion in a course using activity completion
     When I log in as "student1"
     And I am on "Course 1" course homepage
     And I follow "Test page name"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     Then I should see "Status: Pending" in the "Course completion status" "block"
     And I should see "0 of 1" in the "Activity completion" "table_row"
     And I trigger cron
index 819d03e..2f82882 100644 (file)
@@ -26,7 +26,7 @@ Feature: Limit displayed upcoming events
     And I create a calendar event:
       | Type of event     | course |
       | Event title       | Two months away event |
-    When I follow "C1"
+    When I am on "Course 1" course homepage
     Then I should not see "Two months away event"
     And I am on site homepage
     And I follow "Preferences" in the user menu
index c4d0352..4d72d35 100644 (file)
@@ -27,9 +27,7 @@ Feature: Allow teachers to bulk edit activity completion rules in a course.
       | 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 am on "Course 1" course homepage with 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 |
index 13aeb24..cc32275 100644 (file)
@@ -24,9 +24,7 @@ Feature: Allow teachers to edit the default activity completion rules in a cours
       | 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 am on "Course 1" course homepage with 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 |
diff --git a/course/classes/output/activity_navigation.php b/course/classes/output/activity_navigation.php
new file mode 100644 (file)
index 0000000..9e47b35
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File containing the class activity navigation renderable.
+ *
+ * @package    core_course
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_course\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use templatable;
+
+/**
+ * The class activity navigation renderable.
+ *
+ * @package    core_course
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class activity_navigation implements renderable, templatable {
+
+    /**
+     * @var \action_link The action link object for the prev link.
+     */
+    public $prevlink = null;
+
+    /**
+     * @var \action_link The action link object for the next link.
+     */
+    public $nextlink = null;
+
+    /**
+     * Constructor.
+     *
+     * @param \cm_info|null $prevmod The previous module to display, null if none.
+     * @param \cm_info|null $nextmod The previous module to display, null if none.
+     */
+    public function __construct($prevmod, $nextmod) {
+        global $OUTPUT;
+
+        // Check if there is a previous module to display.
+        if ($prevmod) {
+            $linkurl = new \moodle_url($prevmod->url, array('forceview' => 1));
+            $linkname = $prevmod->name;
+            if (!$prevmod->visible) {
+                $linkname .= ' ' . get_string('hiddenwithbrackets');
+            }
+
+            $attributes = [
+                'classes' => 'btn btn-link',
+                'id' => 'prev-activity-link',
+                'title' => $linkname,
+            ];
+            $this->prevlink = new \action_link($linkurl, $OUTPUT->larrow() . ' ' . $linkname, null, $attributes);
+        }
+
+        // Check if there is a next module to display.
+        if ($nextmod) {
+            $linkurl = new \moodle_url($nextmod->url, array('forceview' => 1));
+            $linkname = $nextmod->name;
+            if (!$nextmod->visible) {
+                $linkname .= ' ' . get_string('hiddenwithbrackets');
+            }
+
+            $attributes = [
+                'classes' => 'btn btn-link',
+                'id' => 'next-activity-link',
+                'title' => $linkname,
+            ];
+            $this->nextlink = new \action_link($linkurl, $linkname . ' ' . $OUTPUT->rarrow(), null, $attributes);
+        }
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param \renderer_base $output Renderer base.
+     * @return \stdClass
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = new \stdClass();
+        if ($this->prevlink) {
+            $data->prevlink = $this->prevlink->export_for_template($output);
+        }
+
+        if ($this->nextlink) {
+            $data->nextlink = $this->nextlink->export_for_template($output);
+        }
+
+        return $data;
+    }
+}
index 9a68ee3..8b22a9a 100644 (file)
@@ -3479,31 +3479,33 @@ function duplicate_module($course, $cm) {
         }
     }
 
+    $rc->destroy();
+
+    if (empty($CFG->keeptempdirectoriesonbackup)) {
+        fulldelete($backupbasepath);
+    }
+
     // If we know the cmid of the new course module, let us move it
     // right below the original one. otherwise it will stay at the
     // end of the section.
     if ($newcmid) {
-        $info = get_fast_modinfo($course);
-        $newcm = $info->get_cm($newcmid);
         $section = $DB->get_record('course_sections', array('id' => $cm->section, 'course' => $cm->course));
-        moveto_module($newcm, $section, $cm);
-        moveto_module($cm, $section, $newcm);
+        $modarray = explode(",", trim($section->sequence));
+        $cmindex = array_search($cm->id, $modarray);
+        if ($cmindex !== false && $cmindex < count($modarray) - 1) {
+            $newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course);
+            moveto_module($newcm, $section, $modarray[$cmindex + 1]);
+        }
 
         // Update calendar events with the duplicated module.
         // The following line is to be removed in MDL-58906.
         course_module_update_calendar_events($newcm->modname, null, $newcm);
 
         // Trigger course module created event. We can trigger the event only if we know the newcmid.
+        $newcm = get_fast_modinfo($cm->course)->get_cm($newcmid);
         $event = \core\event\course_module_created::create_from_cm($newcm);
         $event->trigger();
     }
-    rebuild_course_cache($cm->course);
-
-    $rc->destroy();
-
-    if (empty($CFG->keeptempdirectoriesonbackup)) {
-        fulldelete($backupbasepath);
-    }
 
     return isset($newcm) ? $newcm : null;
 }
index 1a87fb8..f048bcd 100644 (file)
@@ -157,11 +157,11 @@ if ($mform->is_cancelled()) {
     }
 
     if (isset($fromform->submitbutton)) {
+        $url = new moodle_url("/mod/$module->name/view.php", array('id' => $fromform->coursemodule, 'forceview' => 1));
         if (empty($fromform->showgradingmanagement)) {
-            redirect("$CFG->wwwroot/mod/$module->name/view.php?id=$fromform->coursemodule");
+            redirect($url);
         } else {
-            $returnurl = new moodle_url("/mod/$module->name/view.php", array('id' => $fromform->coursemodule));
-            redirect($fromform->gradingman->get_management_url($returnurl));
+            redirect($fromform->gradingman->get_management_url($url));
         }
     } else {
         redirect(course_get_url($course, $cw->section, array('sr' => $sectionreturn)));
index accae17..a2d222b 100644 (file)
@@ -2100,6 +2100,19 @@ class core_course_renderer extends plugin_renderer_base {
                 set_attributes(array('class' => 'frontpage-category-names'));
         return $this->coursecat_tree($chelper, coursecat::get(0));
     }
+
+    /**
+     * Renders the activity navigation.
+     *
+     * Defer to template.
+     *
+     * @param \core_course\output\activity_navigation $page
+     * @return string html for the page
+     */
+    public function render_activity_navigation(\core_course\output\activity_navigation $page) {
+        $data = $page->export_for_template($this->output);
+        return $this->output->render_from_template('core_course/activity_navigation', $data);
+    }
 }
 
 /**
diff --git a/course/templates/activity_navigation.mustache b/course/templates/activity_navigation.mustache
new file mode 100644 (file)
index 0000000..fb9c390
--- /dev/null
@@ -0,0 +1,69 @@
+{{!
+    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/activity_navigation
+
+    Displays the activity navigation
+
+    Context variables required for this template:
+    * prevlink Object - The action link data for the previous activity link. Corresponds with the core/action_link context.
+    * nextlink Object - The action link data for the next activity link. Corresponds with the core/action_link context.
+
+    Example context (json):
+    {
+        "prevlink": {
+            "disabled": false,
+            "url": "#",
+            "id": "test-id-1",
+            "classes": "btn btn-link",
+            "attributes": [
+                {
+                    "name": "title",
+                    "value": "Activity A"
+                }
+            ],
+            "text": "◄ Activity A"
+        },
+        "nextlink": {
+            "disabled": false,
+            "url": "#",
+            "id": "test-id-2",
+            "classes": "btn btn-link",
+            "attributes": [
+                {
+                    "name": "title",
+                    "value": "Activity C"
+                }
+            ],
+            "text": "Activity C ►"
+        }
+    }
+}}
+<div>
+{{< core/columns-1to1to1}}
+    {{$column1}}
+        <div class="pull-left">
+            {{#prevlink}}{{> core/action_link }}{{/prevlink}}
+        </div>
+    {{/column1}}
+    {{$column3}}
+        <div class="pull-right">
+            {{#nextlink}}{{> core/action_link }}{{/nextlink}}
+        </div>
+    {{/column3}}
+{{/ core/columns-1to1to1}}
+</div>
index acd8219..cce5ffa 100644 (file)
@@ -26,7 +26,7 @@ Feature: Browse course list and return back from enrolment page
     Then I should see "Courses" in the ".breadcrumb-nav" "css_element"
     And I click on "Courses" "link" in the ".breadcrumb-nav" "css_element"
     And I follow "Sample category"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I press "Continue"
     And I should see "Sample category" in the ".breadcrumb-nav" "css_element"
 
@@ -45,7 +45,7 @@ Feature: Browse course list and return back from enrolment page
     And I open my profile in edit mode
     And I expand "Courses" node
     And I expand "Sample category" node
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I press "Continue"
     Then I should see "Edit profile" in the ".breadcrumb-nav" "css_element"
 
index 0f929bd..3e88929 100644 (file)
@@ -12,9 +12,9 @@ Feature: Course paged mode
     And I log in as "admin"
     And I am on "Course 1" course homepage
     Then I click on <section2> "link" in the <section2> "section"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I click on <section3> "link" in the <section3> "section"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I click on <section1> "link" in the <section1> "section"
     And I should see <section1> in the "div.single-section" "css_element"
     And I should see <section2> in the ".single-section span.mdl-right" "css_element"
@@ -45,9 +45,9 @@ Feature: Course paged mode
     And I log in as "admin"
     And I am on "Course 1" course homepage
     Then I click on <section2> "link" in the <section2> "section"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I click on <section3> "link" in the <section3> "section"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I click on <section1> "link" in the <section1> "section"
     And I should see <section1> in the "div.single-section" "css_element"
     And I should see <section2> in the ".single-section span.mdl-right" "css_element"
index 4478229..bc0f917 100644 (file)
@@ -96,8 +96,7 @@ Feature: Enrolments are synchronised with meta courses
     And I press "Next"
     And I press "Perform restore"
     And I trigger cron
-    And I am on course index
-    And I follow "Course 4"
+    And I am on "Course 4" course homepage
     And I navigate to "Enrolment methods" node in "Course administration > Users"
     Then I should see "Course meta link (Course 1)"
     And I should see "Course meta link (Course 2)"
index aa88075..294ee58 100644 (file)
@@ -12,7 +12,6 @@ Feature: User can be enrolled into a course
       | fullname   | shortname |
       | Course 001 | C001      |
     And I log in as "admin"
-    And I am on course index
     And I am on "Course 001" course homepage
 
   Scenario: User can be enrolled without javascript
index 13f1b75..b1535e0 100644 (file)
@@ -24,8 +24,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
     And I log in as "admin"
     And I set the following administration settings values:
       | grade_minmaxtouse | Min and max grades as specified in grade item settings |
-    And I am on site homepage
-    And I follow "C1"
+    And I am on "C1" course homepage
     And I navigate to "Setup > Gradebook setup" in the course gradebook
     And I press "Add grade item"
     And I set the following fields to these values:
@@ -66,7 +65,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
       | Aggregation          | Natural |
     And I log out
     And I log in as "teacher1"
-    And I follow "C1"
+    And I am on "C1" course homepage
     And I navigate to "View > Grader report" in the course gradebook
     And I turn editing mode on
     And I give the grade "75.00" to the user "Student 1" for the grade item "MI 1"
index 84e3c02..13267b8 100644 (file)
@@ -944,6 +944,7 @@ $string['registerwithmoodleorg'] = 'Register your site';
 $string['registration'] = 'Registration';
 $string['registration_help'] = 'It is recommended that you register your site in order to receive security alerts and access to Moodle.net, our course sharing platform.';
 $string['registrationwarning'] = 'Your site is not yet registered.';
+$string['registrationwarningcontactadmin'] = 'Your site is not yet registered. Please notify your administrator.';
 $string['releasenoteslink'] = 'For information about this version of Moodle, please see the online <a target="_blank" href="{$a}">Release Notes</a>';
 $string['rememberusername'] = 'Remember username';
 $string['rememberusername_desc'] = 'Enable if you want to store permanent cookies with usernames during user login. Permanent cookies may be considered a privacy issue if used without consent.';
index bbdb91a..55a0e70 100644 (file)
@@ -60,3 +60,4 @@ error:badjson,core_badges
 error:backpackloginfailed,core_badges
 signinwithyouremail,core_badges
 sectionusedefaultname,core
+registermoochtips,core_hub
index 2b9b10a..b899919 100644 (file)
@@ -42,6 +42,7 @@ $string['err_numeric'] = 'You must enter a number here.';
 $string['err_rangelength'] = 'You must enter between {$a->format[0]} and {$a->format[1]} characters here.';
 $string['err_required'] = 'You must supply a value here.';
 $string['err_wrongfileextension'] = 'Some files ({$a->wrongfiles}) cannot be uploaded. Only file types {$a->whitelist} are allowed.';
+$string['filesofthesetypes'] = 'Accepted files types:';
 $string['filetypesany'] = 'All file types';
 $string['filetypesnotall'] = 'It is not allowed to select \'All file types\' here';
 $string['filetypesnotwhitelisted'] = 'These file types are not allowed here: {$a}';
index 3bf13d5..0d168b8 100644 (file)
@@ -164,7 +164,6 @@ $string['registeredmoodleorg'] = 'Moodle ({$a})';
 $string['registeredon'] = 'Where your site is registered';
 $string['registereduserdevices'] = 'Number of users with registered mobile devices ({$a})';
 $string['registeredactiveuserdevices'] = 'Number of active users with registered mobile devices which are receiving notifications ({$a})';
-$string['registermoochtips'] = 'Register your site with Moodle to get security alerts and access to Moodle.net, our course sharing platform.';
 $string['registersite'] = 'Register with {$a}';
 $string['registerwith'] = 'Register with a hub';
 $string['registrationconfirmed'] = 'Site registration confirmed';
@@ -268,3 +267,6 @@ $string['wrongurlformat'] = 'Bad URL format';
 $string['xmlrpcdisabledcommunity'] = 'The XML-RPC extension is not enabled on the server. You can not search and download courses.';
 $string['xmlrpcdisabledpublish'] = 'The XML-RPC extension is not enabled on the server. You can not publish courses or manage published courses.';
 $string['xmlrpcdisabledregistration'] = 'The XML-RPC extension is not enabled on the server. You will not be able to unregister or update your registration until you enable it.';
+
+// Deprecated since Moodle 3.4.
+$string['registermoochtips'] = 'Register your site with Moodle to get security alerts and access to Moodle.net, our course sharing platform.';
index 6f54a70..64d5b19 100644 (file)
@@ -931,6 +931,7 @@ $string['hiddensections'] = 'Hidden sections';
 $string['hiddensections_help'] = 'This setting determines whether hidden sections are displayed to students in collapsed form (perhaps for a course in weekly format to indicate holidays) or are completely hidden.';
 $string['hiddensectionscollapsed'] = 'Hidden sections are shown in collapsed form';
 $string['hiddensectionsinvisible'] = 'Hidden sections are completely invisible';
+$string['hiddenwithbrackets'] = '(hidden)';
 $string['hide'] = 'Hide';
 $string['hideadvancedsettings'] = 'Hide advanced settings';
 $string['hidechartdata'] = 'Hide chart data';
index 45870a2..2bfc5df 100644 (file)
@@ -455,9 +455,9 @@ $string['useshowadvancedtochange'] = 'Use \'Show advanced\' to change';
 $string['viewingdefinitionofrolex'] = 'Viewing the definition of role \'{$a}\'';
 $string['viewrole'] = 'View role details';
 $string['webservice:createtoken'] = 'Create a web service token';
+$string['webservice:managealltokens'] = 'Manage all users\' web services';
 $string['webservice:createmobiletoken'] = 'Create a web service token for mobile access';
 $string['whydoesuserhavecap'] = 'Why does {$a->fullname} have capability {$a->capability} in context {$a->context}?';
 $string['whydoesusernothavecap'] = 'Why does {$a->fullname} not have capability {$a->capability} in context {$a->context}?';
 $string['xroleassignments'] = '{$a}\'s role assignments';
 $string['xuserswiththerole'] = 'Users with the role "{$a->role}"';
-
index ebfe938..057f9f6 100644 (file)
@@ -131,6 +131,7 @@ $string['norequiredcapability'] = 'No required capability';
 $string['notoken'] = 'The token list is empty.';
 $string['onesystemcontrolling'] = 'Allow an external system to control Moodle';
 $string['onesystemcontrollingdescription'] = 'The following steps help you to set up the Moodle web services to allow an external system to interact with Moodle. This includes setting up a token (security key) authentication method.';
+$string['onlyseecreatedtokens'] = 'Only tokens you own or created can be seen. You can still delete other tokens.';
 $string['operation'] = 'Operation';
 $string['optional'] = 'Optional';
 $string['passwordisexpired'] = 'Password is expired.';
index 5386669..410130e 100644 (file)
@@ -9524,84 +9524,29 @@ class admin_setting_managewebservicetokens extends admin_setting {
      * @return string
      */
     public function output_html($data, $query='') {
-        global $CFG, $OUTPUT, $DB, $USER;
+        global $CFG, $OUTPUT;
 
-        // display strings
-        $stroperation = get_string('operation', 'webservice');
-        $strtoken = get_string('token', 'webservice');
-        $strservice = get_string('service', 'webservice');
-        $struser = get_string('user');
-        $strcontext = get_string('context', 'webservice');
-        $strvaliduntil = get_string('validuntil', 'webservice');
-        $striprestriction = get_string('iprestriction', 'webservice');
+        require_once($CFG->dirroot . '/webservice/classes/token_table.php');
+        $baseurl = new moodle_url('/' . $CFG->admin . '/settings.php?section=webservicetokens');
 
         $return = $OUTPUT->box_start('generalbox webservicestokenui');
 
-        $table = new html_table();
-        $table->head  = array($strtoken, $struser, $strservice, $striprestriction, $strvaliduntil, $stroperation);
-        $table->colclasses = array('leftalign', 'leftalign', 'leftalign', 'centeralign', 'centeralign', 'centeralign');
-        $table->id = 'webservicetokens';
-        $table->attributes['class'] = 'admintable generaltable';
+        if (has_capability('moodle/webservice:managealltokens', context_system::instance())) {
+            $return .= \html_writer::div(get_string('onlyseecreatedtokens', 'webservice'));
+        }
+
+        $table = new \webservice\token_table('webservicetokens');
+        $table->define_baseurl($baseurl);
+        $table->attributes['class'] = 'admintable generaltable'; // Any need changing?
         $table->data  = array();
+        ob_start();
+        $table->out(10, false);
+        $tablehtml = ob_get_contents();
+        ob_end_clean();
+        $return .= $tablehtml;
 
         $tokenpageurl = "$CFG->wwwroot/$CFG->admin/webservice/tokens.php?sesskey=" . sesskey();
 
-        //TODO: in order to let the administrator delete obsolete token, split this request in multiple request or use LEFT JOIN
-
-        //here retrieve token list (including linked users firstname/lastname and linked services name)
-        $sql = "SELECT t.id, t.token, u.id AS userid, u.firstname, u.lastname, s.name, t.iprestriction, t.validuntil, s.id AS serviceid
-                  FROM {external_tokens} t, {user} u, {external_services} s
-                 WHERE t.creatorid=? AND t.tokentype = ? AND s.id = t.externalserviceid AND t.userid = u.id";
-        $tokens = $DB->get_records_sql($sql, array($USER->id, EXTERNAL_TOKEN_PERMANENT));
-        if (!empty($tokens)) {
-            foreach ($tokens as $token) {
-                //TODO: retrieve context
-
-                $delete = "<a href=\"".$tokenpageurl."&amp;action=delete&amp;tokenid=".$token->id."\">";
-                $delete .= get_string('delete')."</a>";
-
-                $validuntil = '';
-                if (!empty($token->validuntil)) {
-                    $validuntil = userdate($token->validuntil, get_string('strftimedatetime', 'langconfig'));
-                }
-
-                $iprestriction = '';
-                if (!empty($token->iprestriction)) {
-                    $iprestriction = $token->iprestriction;
-                }
-
-                $userprofilurl = new moodle_url('/user/profile.php?id='.$token->userid);
-                $useratag = html_writer::start_tag('a', array('href' => $userprofilurl));
-                $useratag .= $token->firstname." ".$token->lastname;
-                $useratag .= html_writer::end_tag('a');
-
-                //check user missing capabilities
-                require_once($CFG->dirroot . '/webservice/lib.php');
-                $webservicemanager = new webservice();
-                $usermissingcaps = $webservicemanager->get_missing_capabilities_by_users(
-                        array(array('id' => $token->userid)), $token->serviceid);
-
-                if (!is_siteadmin($token->userid) and
-                        array_key_exists($token->userid, $usermissingcaps)) {
-                    $missingcapabilities = implode(', ',
-                            $usermissingcaps[$token->userid]);
-                    if (!empty($missingcapabilities)) {
-                        $useratag .= html_writer::tag('div',
-                                        get_string('usermissingcaps', 'webservice',
-                                                $missingcapabilities)
-                                        . '&nbsp;' . $OUTPUT->help_icon('missingcaps', 'webservice'),
-                                        array('class' => 'missingcaps'));
-                    }
-                }
-
-                $table->data[] = array($token->token, $useratag, $token->name, $iprestriction, $validuntil, $delete);
-            }
-
-            $return .= html_writer::table($table);
-        } else {
-            $return .= get_string('notoken', 'webservice');
-        }
-
         $return .= $OUTPUT->box_end();
         // add a token to the table
         $return .= "<a href=\"".$tokenpageurl."&amp;action=create\">";
index acdaab3..62b8bf5 100644 (file)
@@ -728,6 +728,8 @@ class block_manager {
         $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = bi.id AND ctx.contextlevel = :contextlevel)";
 
         $systemcontext = context_system::instance();
+        list($bpcontext, $bpcontextidparams) = $DB->get_in_or_equal(array($context->id, $systemcontext->id),
+                SQL_PARAMS_NAMED, 'bpcontextid');
         $params = array(
             'contextlevel' => CONTEXT_BLOCK,
             'subpage1' => $this->page->subpage,
@@ -761,7 +763,7 @@ class block_manager {
                 FROM {block_instances} bi
                 JOIN {block} b ON bi.blockname = b.name
                 LEFT JOIN {block_positions} bp ON bp.blockinstanceid = bi.id
-                                                  AND bp.contextid = :contextid1
+                                                  AND bp.contextid $bpcontext
                                                   AND bp.pagetype = :pagetype
                                                   AND bp.subpage = :subpage1
                 $ccjoin
@@ -779,7 +781,8 @@ class block_manager {
                     COALESCE(bp.weight, bi.defaultweight),
                     bi.id";
 
-        $allparams = $params + $parentcontextparams + $pagetypepatternparams + $requiredbythemeparams + $requiredbythemenotparams;
+        $allparams = $params + $parentcontextparams + $pagetypepatternparams + $requiredbythemeparams;
+        $allparams = $allparams + $requiredbythemenotparams + $bpcontextidparams;
         $blockinstances = $DB->get_recordset_sql($sql, $allparams);
 
         $this->birecordsbyregion = $this->prepare_per_region_arrays();
index 966ea3f..a27e605 100644 (file)
@@ -752,7 +752,7 @@ class api {
         $record->issuerid = $issuer->get('id');
         $record->refreshtoken = $refreshtoken;
         $record->grantedscopes = $scopes;
-        $record->email = $userinfo['email'];
+        $record->email = isset($userinfo['email']) ? $userinfo['email'] : '';
         $record->username = $userinfo['username'];
 
         $systemaccount = new system_account(0, $record);
index 808d0b9..86c450c 100644 (file)
@@ -1845,6 +1845,13 @@ $capabilities = array(
             'manager' => CAP_ALLOW
         )
     ),
+    'moodle/webservice:managealltokens' => array(
+
+        'riskbitmask' => RISK_CONFIG | RISK_DATALOSS | RISK_PERSONAL,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array()
+    ),
     'moodle/webservice:createmobiletoken' => array(
 
         'riskbitmask' => RISK_SPAM | RISK_PERSONAL,
index e947ca7..b83f318 100644 (file)
         <FIELD NAME="issuerid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the oauth 2 identity issuer"/>
         <FIELD NAME="refreshtoken" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The refresh token used to request access tokens."/>
         <FIELD NAME="grantedscopes" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The scopes that this system account has been granted access to."/>
-        <FIELD NAME="email" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The email that was connected to this issuer."/>
+        <FIELD NAME="email" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The email that was connected to this issuer."/>
         <FIELD NAME="username" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The username that was connected as a system account to this issue."/>
       </FIELDS>
       <KEYS>
index ca24cd9..3120399 100644 (file)
@@ -2214,5 +2214,17 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017072000.02);
     }
 
+    if ($oldversion < 2017072700.01) {
+        // Changing nullability of field email on table oauth2_system_account to null.
+        $table = new xmldb_table('oauth2_system_account');
+        $field = new xmldb_field('email', XMLDB_TYPE_TEXT, null, null, null, null, null, 'grantedscopes');
+
+        // Launch change of nullability for field email.
+        $dbman->change_field_notnull($table, $field);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017072700.01);
+    }
+
     return true;
 }
index b3ecee8..efc610c 100644 (file)
@@ -329,13 +329,10 @@ class mysql_sql_generator extends sql_generator {
      * @return array of sql statements
      */
     public function getCreateTempTableSQL($xmldb_table) {
-        $engine = $this->mdb->get_dbengine();
         // Do we know collation?
         $collation = $this->mdb->get_dbcollation();
         $this->temptables->add_temptable($xmldb_table->getName());
 
-        $rowformat = $this->mdb->get_row_format_sql($engine, $collation);
-
         $sqlarr = parent::getCreateTableSQL($xmldb_table);
 
         // Let's inject the extra MySQL tweaks.
@@ -347,7 +344,7 @@ class mysql_sql_generator extends sql_generator {
                     if (strpos($collation, 'utf8_') === 0) {
                         $sqlarr[$i] .= " DEFAULT CHARACTER SET utf8";
                     }
-                    $sqlarr[$i] .= " DEFAULT COLLATE $collation $rowformat";
+                    $sqlarr[$i] .= " DEFAULT COLLATE $collation ROW_FORMAT=DYNAMIC";
                 }
             }
         }
index ffc7796..48debee 100644 (file)
@@ -88,6 +88,12 @@ class mariadb_native_moodle_database extends mysqli_native_moodle_database {
         return array('description'=>$this->mysqli->server_info, 'version'=>$version);
     }
 
+    protected function has_breaking_change_quoted_defaults() {
+        $version = $this->get_server_info()['version'];
+        // Breaking change since 10.2.7: MDEV-13132.
+        return version_compare($version, '10.2.7', '>=');
+    }
+
     /**
      * It is time to require transactions everywhere.
      *
index 15926ee..fee8b73 100644 (file)
@@ -715,7 +715,7 @@ class mysqli_native_moodle_database extends moodle_database {
                 $rawcolumn->numeric_scale            = null;
                 $rawcolumn->is_nullable              = $rawcolumn->null; unset($rawcolumn->null);
                 $rawcolumn->column_default           = $rawcolumn->default; unset($rawcolumn->default);
-                $rawcolumn->column_key               = $rawcolumn->key; unset($rawcolumn->default);
+                $rawcolumn->column_key               = $rawcolumn->key; unset($rawcolumn->key);
 
                 if (preg_match('/(enum|varchar)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
                     $rawcolumn->data_type = $matches[1];
@@ -783,6 +783,14 @@ class mysqli_native_moodle_database extends moodle_database {
         return $structure;
     }
 
+    /**
+     * Indicates whether column information retrieved from `information_schema.columns` has default values quoted or not.
+     * @return boolean True when default values are quoted (breaking change); otherwise, false.
+     */
+    protected function has_breaking_change_quoted_defaults() {
+        return false;
+    }
+
     /**
      * Returns moodle column info for raw column from information schema.
      * @param stdClass $rawcolumn
@@ -794,7 +802,11 @@ class mysqli_native_moodle_database extends moodle_database {
         $info->name           = $rawcolumn->column_name;
         $info->type           = $rawcolumn->data_type;
         $info->meta_type      = $this->mysqltype2moodletype($rawcolumn->data_type);
-        $info->default_value  = $rawcolumn->column_default;
+        if ($this->has_breaking_change_quoted_defaults()) {
+            $info->default_value = trim($rawcolumn->column_default, "'");
+        } else {
+            $info->default_value = $rawcolumn->column_default;
+        }
         $info->has_default    = !is_null($rawcolumn->column_default);
         $info->not_null       = ($rawcolumn->is_nullable === 'NO');
         $info->primary_key    = ($rawcolumn->column_key === 'PRI');
index 93ff4cf..71686d0 100644 (file)
@@ -300,6 +300,14 @@ class MoodleQuickForm_filemanager extends HTML_QuickForm_element implements temp
         // label element needs 'for' attribute work
         $html .= html_writer::empty_tag('input', array('value' => '', 'id' => 'id_'.$elname, 'type' => 'hidden'));
 
+        if (!empty($options->accepted_types) && $options->accepted_types != '*') {
+            $html .= html_writer::tag('p', get_string('filesofthesetypes', 'form'));
+            $util = new \core_form\filetypes_util();
+            $filetypes = $options->accepted_types;
+            $filetypedescriptions = $util->describe_file_types($filetypes);
+            $html .= $OUTPUT->render_from_template('core_form/filetypes-descriptions', $filetypedescriptions);
+        }
+
         return $html;
     }
 
index c1485b2..a376459 100644 (file)
@@ -184,6 +184,14 @@ class MoodleQuickForm_filepicker extends HTML_QuickForm_input implements templat
         $html .= "<div><object type='text/html' data='$nonjsfilepicker' height='160' width='600' style='border:1px solid #000'></object></div>";
         $html .= '</noscript>';
 
+        if (!empty($options->accepted_types) && $options->accepted_types != '*') {
+            $html .= html_writer::tag('p', get_string('filesofthesetypes', 'form'));
+            $util = new \core_form\filetypes_util();
+            $filetypes = $options->accepted_types;
+            $filetypedescriptions = $util->describe_file_types($filetypes);
+            $html .= $OUTPUT->render_from_template('core_form/filetypes-descriptions', $filetypedescriptions);
+        }
+
         return $html;
     }
 
index 8f3e795..f918112 100644 (file)
@@ -557,6 +557,10 @@ abstract class oauth2_client extends curl {
 
         $r = json_decode($response);
 
+        if (is_null($r)) {
+            throw new moodle_exception("Could not decode JSON token response");
+        }
+
         if (!empty($r->error)) {
             throw new moodle_exception($r->error . ' ' . $r->error_description);
         }
@@ -610,6 +614,9 @@ abstract class oauth2_client extends curl {
             }
         }
 
+        // Force JSON format content in response.
+        $this->setHeader('Accept: application/json');
+
         $response = parent::request($murl->out(false), $options);
 
         $this->resetHeader();
index 08e917b..9c9c316 100644 (file)
@@ -813,6 +813,69 @@ class core_renderer extends renderer_base {
         return '<div role="main">'.$this->unique_main_content_token.'</div>';
     }
 
+    /**
+     * Returns standard navigation between activities in a course.
+     *
+     * @return string the navigation HTML.
+     */
+    public function activity_navigation() {
+        // First we should check if we want to add navigation.
+        $context = $this->page->context;
+        if (($this->page->pagelayout !== 'incourse' && $this->page->pagelayout !== 'frametop')
+            || $context->contextlevel != CONTEXT_MODULE) {
+            return '';
+        }
+
+        // If the activity is in stealth mode, show no links.
+        if ($this->page->cm->is_stealth()) {
+            return '';
+        }
+
+        // Get a list of all the activities in the course.
+        $course = $this->page->cm->get_course();
+        $modules = get_fast_modinfo($course->id)->get_cms();
+
+        // Put the modules into an array in order by the position they are shown in the course.
+        $mods = [];
+        foreach ($modules as $module) {
+            // Only add activities the user can access, aren't in stealth mode and have a url (eg. mod_label does not).
+            if (!$module->uservisible || $module->is_stealth() || empty($module->url)) {
+                continue;
+            }
+            $mods[$module->id] = $module;
+        }
+
+        $nummods = count($mods);
+
+        // If there is only one mod then do nothing.
+        if ($nummods == 1) {
+            return '';
+        }
+
+        // Get an array of just the course module ids used to get the cmid value based on their position in the course.
+        $modids = array_keys($mods);
+
+        // Get the position in the array of the course module we are viewing.
+        $position = array_search($this->page->cm->id, $modids);
+
+        $prevmod = null;
+        $nextmod = null;
+
+        // Check if we have a previous mod to show.
+        if ($position > 0) {
+            $prevmod = $mods[$modids[$position - 1]];
+        }
+
+        // Check if we have a next mod to show.
+        if ($position < ($nummods - 1)) {
+            $nextmod = $mods[$modids[$position + 1]];
+        }
+
+        $activitynav = new \core_course\output\activity_navigation($prevmod, $nextmod);
+        $renderer = $this->page->get_renderer('core', 'course');
+        return $renderer->render($activitynav);
+    }
+
     /**
      * The standard tags (typically script tags that are not needed earlier) that
      * should be output after everything else. Designed to be called in theme layout.php files.
@@ -3176,7 +3239,7 @@ EOD;
             array('role' => 'button', 'tabindex' => 0));
         $formattrs = array('class' => 'search-input-form', 'action' => $CFG->wwwroot . '/search/index.php');
         $inputattrs = array('type' => 'text', 'name' => 'q', 'placeholder' => get_string('search', 'search'),
-            'size' => 13, 'tabindex' => -1, 'id' => 'id_q_' . $id);
+            'size' => 13, 'tabindex' => -1, 'id' => 'id_q_' . $id, 'class' => 'form-control');
 
         $contents = html_writer::tag('label', get_string('enteryoursearchquery', 'search'),
             array('for' => 'id_q_' . $id, 'class' => 'accesshide')) . html_writer::tag('input', '', $inputattrs);
index 8cf80a1..dd14789 100644 (file)
@@ -159,7 +159,7 @@ OET;
         $shortlink = '((youtu|y2u)\.be/)';
 
         // Initial part of link.
-        $start = '~^https?://(www\.)?(' . $link . '|' . $shortlink . ')';
+        $start = '~^https?://((www|m)\.)?(' . $link . '|' . $shortlink . ')';
         // Middle bit: Video key value.
         $middle = '([a-z0-9\-_]+)';
         return $start . $middle . core_media_player_external::END_LINK_REGEX_PART;
index ffaf52e..5c9d851 100644 (file)
@@ -70,6 +70,9 @@ class media_youtube_testcase extends advanced_testcase {
         $url = new moodle_url('http://www.youtube.com/v/vyrwMmsufJc');
         $t = $manager->embed_url($url);
         $this->assertContains('</iframe>', $t);
+        $url = new moodle_url('http://m.youtube.com/watch?v=vyrwMmsufJc');
+        $t = $manager->embed_url($url);
+        $this->assertContains('</iframe>', $t);
 
         // Format: youtube video within playlist.
         $url = new moodle_url('https://www.youtube.com/watch?v=dv2f_xfmbD8&index=4&list=PLxcO_MFWQBDcyn9xpbmx601YSDlDcTcr0');
index 0b5ff28..3d1991f 100644 (file)
@@ -23,7 +23,6 @@
  */
 
 require('../../../config.php');
-require_once($CFG->dirroot . '/' . $CFG->admin . '/registration/lib.php');
 
 define('AIRNOTIFIER_PUBLICURL', 'https://messages.moodle.net');
 
@@ -50,10 +49,10 @@ $msg = "";
 // If we are requesting a key to the official message system, verify first that this site is registered.
 // This check is also done in Airnotifier.
 if (strpos($CFG->airnotifierurl, AIRNOTIFIER_PUBLICURL) !== false ) {
-    $registrationmanager = new registration_manager();
-    if (!$registrationmanager->get_registeredhub(HUB_MOODLEORGHUBURL)) {
-        $msg = get_string('sitemustberegistered', 'message_airnotifier');
-        $msg .= $OUTPUT->continue_button($returl);
+    $adminrenderer = $PAGE->get_renderer('core', 'admin');
+    $msg = $adminrenderer->warn_if_not_registered();
+    if ($msg) {
+        $msg .= html_writer::div(get_string('sitemustberegistered', 'message_airnotifier'));
 
         echo $OUTPUT->header();
         echo $OUTPUT->box($msg, 'generalbox');
index c501bf8..45dd3ff 100644 (file)
@@ -24,7 +24,7 @@
 
 
 $string['acceptedfiletypes'] = 'Accepted file types';
-$string['acceptedfiletypes_help'] = 'Accepted file types can be restricted by entering a comma-separated list of mimetypes, e.g. video/mp4, audio/mp3, image/png, image/jpeg, or file extensions including a dot, e.g. .png, .jpg. If the field is left empty, then all file types are allowed.';
+$string['acceptedfiletypes_help'] = 'Accepted file types can be restricted by entering a list of file extensions. If the field is left empty, then all file types are allowed.';
 $string['configmaxbytes'] = 'Maximum file size';
 $string['countfiles'] = '{$a} files';
 $string['default'] = 'Enabled by default';
@@ -34,8 +34,6 @@ $string['enabled'] = 'File submissions';
 $string['enabled_help'] = 'If enabled, students are able to upload one or more files as their submission.';
 $string['eventassessableuploaded'] = 'A file has been uploaded.';
 $string['file'] = 'File submissions';
-$string['filesofthesetypes'] = 'Files of these types may be added to the submission:';
-$string['filetypewithexts'] = '{$a->name} &mdash; {$a->extlist}';
 $string['maxbytes'] = 'Maximum file size';
 $string['maxfiles'] = 'Maximum files per submission';
 $string['maxfiles_help'] = 'If file submissions are enabled, each assignment can be set to accept up to this number of files for their submission.';
@@ -43,8 +41,11 @@ $string['maxfilessubmission'] = 'Maximum number of uploaded files';
 $string['maxfilessubmission_help'] = 'If file submissions are enabled, each student will be able to upload up to this number of files for their submission.';
 $string['maximumsubmissionsize'] = 'Maximum submission size';
 $string['maximumsubmissionsize_help'] = 'Files uploaded by students may be up to this size.';
-$string['nonexistentfiletypes'] = 'The following file types were not recognised: {$a}';
 $string['numfilesforlog'] = 'The number of file(s) : {$a} file(s).';
 $string['pluginname'] = 'File submissions';
 $string['siteuploadlimit'] = 'Site upload limit';
 $string['submissionfilearea'] = 'Uploaded submission files';
+// Deprecated since Moodle 3.4.
+$string['filesofthesetypes'] = 'Files of these types may be added to the submission:';
+$string['filetypewithexts'] = '{$a->name} &mdash; {$a->extlist}';
+$string['nonexistentfiletypes'] = 'The following file types were not recognised: {$a}';
diff --git a/mod/assign/submission/file/lang/en/deprecated.txt b/mod/assign/submission/file/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..c3ca9ee
--- /dev/null
@@ -0,0 +1,3 @@
+filesofthesetypes,assignsubmission_file
+filetypewithexts,assignsubmission_file
+nonexistentfiletypes,assignsubmission_file
index d96b293..8a82199 100644 (file)
@@ -113,23 +113,10 @@ class assign_submission_file extends assign_submission_plugin {
                            'notchecked');
 
         $name = get_string('acceptedfiletypes', 'assignsubmission_file');
-        $mform->addElement('text', 'assignsubmission_file_filetypes', $name, array('size' => '60'));
+        $mform->addElement('filetypes', 'assignsubmission_file_filetypes', $name);
         $mform->addHelpButton('assignsubmission_file_filetypes', 'acceptedfiletypes', 'assignsubmission_file');
-        $mform->setType('assignsubmission_file_filetypes', PARAM_RAW);
         $mform->setDefault('assignsubmission_file_filetypes', $defaultfiletypes);
         $mform->disabledIf('assignsubmission_file_filetypes', 'assignsubmission_file_enabled', 'notchecked');
-        $mform->addFormRule(function ($values, $files) {
-            if (empty($values['assignsubmission_file_filetypes'])) {
-                return true;
-            }
-            $nonexistent = $this->get_nonexistent_file_types($values['assignsubmission_file_filetypes']);
-            if (empty($nonexistent)) {
-                return true;
-            } else {
-                $a = join(' ', $nonexistent);
-                return ["assignsubmission_file_filetypes" => get_string('nonexistentfiletypes', 'assignsubmission_file', $a)];
-            }
-        });
     }
 
     /**
@@ -160,7 +147,7 @@ class assign_submission_file extends assign_submission_plugin {
         $fileoptions = array('subdirs' => 1,
                                 'maxbytes' => $this->get_config('maxsubmissionsizebytes'),
                                 'maxfiles' => $this->get_config('maxfilesubmissions'),
-                                'accepted_types' => $this->get_accepted_types(),
+                                'accepted_types' => $this->get_configured_typesets(),
                                 'return_types' => (FILE_INTERNAL | FILE_CONTROLLED_LINK));
         if ($fileoptions['maxbytes'] == 0) {
             // Use module default.
@@ -178,6 +165,7 @@ class assign_submission_file extends assign_submission_plugin {
      * @return bool
      */
     public function get_form_elements($submission, MoodleQuickForm $mform, stdClass $data) {
+        global $OUTPUT;
 
         if ($this->get_config('maxfilesubmissions') <= 0) {
             return false;
@@ -195,34 +183,6 @@ class assign_submission_file extends assign_submission_plugin {
                                                   $submissionid);
         $mform->addElement('filemanager', 'files_filemanager', $this->get_name(), null, $fileoptions);
 
-        if (!empty($this->get_config('filetypeslist'))) {
-            $text = html_writer::tag('p', get_string('filesofthesetypes', 'assignsubmission_file'));
-            $text .= html_writer::start_tag('ul');
-
-            $typesets = $this->get_configured_typesets();
-            foreach ($typesets as $type) {
-                $a = new stdClass();
-                $extensions = file_get_typegroup('extension', $type);
-                $typetext = html_writer::tag('li', $type);
-                // Only bother checking if it's a mimetype or group if it has extensions in the group.
-                if (!empty($extensions)) {
-                    if (strpos($type, '/') !== false) {
-                        $a->name = get_mimetype_description($type);
-                        $a->extlist = implode(' ', $extensions);
-                        $typetext = html_writer::tag('li', get_string('filetypewithexts', 'assignsubmission_file', $a));
-                    } else if (get_string_manager()->string_exists("group:$type", 'mimetypes')) {
-                        $a->name = get_string("group:$type", 'mimetypes');
-                        $a->extlist = implode(' ', $extensions);
-                        $typetext = html_writer::tag('li', get_string('filetypewithexts', 'assignsubmission_file', $a));
-                    }
-                }
-                $text .= $typetext;
-            }
-
-            $text .= html_writer::end_tag('ul');
-            $mform->addElement('static', '', '', $text);
-        }
-
         return true;
     }
 
@@ -639,56 +599,9 @@ class assign_submission_file extends assign_submission_plugin {
     private function get_configured_typesets() {
         $typeslist = (string)$this->get_config('filetypeslist');
 
-        $sets = $this->get_typesets($typeslist);
-
-        return $sets;
-    }
+        $util = new \core_form\filetypes_util();
+        $sets = $util->normalize_file_types($typeslist);
 
-    /**
-     * Get the type sets passed.
-     *
-     * @param string $types The space , ; separated list of types
-     * @return array('groupname', 'mime/type', ...)
-     */
-    private function get_typesets($types) {
-        $sets = array();
-        if (!empty($types)) {
-            $sets = preg_split('/[\s,;:"\']+/', $types, null, PREG_SPLIT_NO_EMPTY);
-        }
         return $sets;
     }
-
-
-    /**
-     * Return the accepted types list for the file manager component.
-     *
-     * @return array|string
-     */
-    private function get_accepted_types() {
-        $acceptedtypes = $this->get_configured_typesets();
-
-        if (!empty($acceptedtypes)) {
-            return $acceptedtypes;
-        }
-
-        return '*';
-    }
-
-    /**
-     * List the nonexistent file types that need to be removed.
-     *
-     * @param string $types space , or ; separated types
-     * @return array A list of the nonexistent file types.
-     */
-    private function get_nonexistent_file_types($types) {
-        $nonexistent = [];
-        foreach ($this->get_typesets($types) as $type) {
-            // If there's no extensions under that group, it doesn't exist.
-            $extensions = file_get_typegroup('extension', [$type]);
-            if (empty($extensions)) {
-                $nonexistent[$type] = true;
-            }
-        }
-        return array_keys($nonexistent);
-    }
 }
index a895e62..9ed9b5e 100644 (file)
@@ -32,9 +32,9 @@ $settings->add(new admin_setting_configtext('assignsubmission_file/maxfiles',
                    new lang_string('maxfiles', 'assignsubmission_file'),
                    new lang_string('maxfiles_help', 'assignsubmission_file'), 20, PARAM_INT));
 
-$settings->add(new admin_setting_configtext('assignsubmission_file/filetypes',
+$settings->add(new admin_setting_filetypes('assignsubmission_file/filetypes',
                    new lang_string('defaultacceptedfiletypes', 'assignsubmission_file'),
-                   new lang_string('acceptedfiletypes_help', 'assignsubmission_file'), '', PARAM_RAW, 60));
+                   new lang_string('acceptedfiletypes_help', 'assignsubmission_file'), ''));
 
 if (isset($CFG->maxbytes)) {
 
index 34f1c07..9fd2ad4 100644 (file)
@@ -30,25 +30,32 @@ Feature: In an assignment, limit submittable file types
     And I navigate to "Edit settings" in current page administration
     When I set the field "Accepted file types" to "image/png;doesntexist;.anything;unreal/mimetype;nodot"
     And I press "Save and display"
-    And I should see "The following file types were not recognised: doesntexist .anything unreal/mimetype nodot"
+    And I should see "Unknown file types: .doesntexist, .anything, unreal/mimetype, .nodot"
     And I set the field "Accepted file types" to "image/png;spreadsheet"
     And I press "Save and display"
     And I navigate to "Edit settings" in current page administration
-    Then the field "Accepted file types" matches value "image/png;spreadsheet"
+    And the field "Accepted file types" matches value "image/png,spreadsheet"
+    And I set the field "Accepted file types" to ""
+    And I press "Choose"
+    And I set the field "Image files" to "1"
+    And I press "Save changes"
+    And I press "Save and display"
+    And I navigate to "Edit settings" in current page administration
+    Then the field "Accepted file types" matches value "image"
 
   @javascript @_file_upload
   Scenario: Uploading permitted file types for an assignment
     Given the following "activities" exist:
       | activity | course | idnumber | name                 | intro                       | duedate    | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | assignsubmission_file_maxfiles | assignsubmission_file_maxsizebytes | assignsubmission_file_filetypes |
-      | assign   | C1     | assign1  | Test assignment name | Test assignment description | 1388534400 | 0                                   | 1                             | 3                              | 0                                  | image/png;spreadsheet;.xml;.txt  |
+      | assign   | C1     | assign1  | Test assignment name | Test assignment description | 1388534400 | 0                                   | 1                             | 3                              | 0                                  | image/png,spreadsheet,.xml,.txt  |
     And I log in as "student1"
     And I am on "Course 1" course homepage
     And I follow "Test assignment name"
     When I press "Add submission"
     And I should see "Files of these types may be added to the submission"
-    And I should see "Image (PNG) — .png"
-    And I should see "Spreadsheet files — .csv .gsheet .ods .ots .xls .xlsx .xlsm"
-    And I should see ".txt"
+    And I should see "Image (PNG)"
+    And I should see "Spreadsheet files"
+    And I should see "Text file"
     And I upload "lib/tests/fixtures/gd-logo.png" file to "File submissions" filemanager
     And I upload "lib/tests/fixtures/tabfile.csv" file to "File submissions" filemanager
     And I upload "lib/tests/fixtures/empty.txt" file to "File submissions" filemanager
index 618a787..2291d62 100644 (file)
@@ -136,70 +136,4 @@ class assignsubmission_file_locallib_testcase extends advanced_testcase {
             'Without file' => [null, true]
         ];
     }
-
-    /**
-     * Data provider for testing test_get_nonexistent_file_types.
-     *
-     * @return array
-     */
-    public function get_nonexistent_file_types_provider() {
-        return [
-            'Nonexistent extensions are not allowed' => [
-                'filetypes' => '.rat',
-                'expected' => ['.rat']
-            ],
-            'Multiple nonexistent extensions are not allowed' => [
-                'filetypes' => '.ricefield .rat',
-                'expected' => ['.ricefield', '.rat']
-            ],
-            'Existent extension is allowed' => [
-                'filetypes' => '.xml',
-                'expected' => []
-            ],
-            'Existent group is allowed' => [
-                'filetypes' => 'web_file',
-                'expected' => []
-            ],
-            'Nonexistent group is not allowed' => [
-                'filetypes' => '©ç√√ß∂å√©åß©√',
-                'expected' => ['©ç√√ß∂å√©åß©√']
-            ],
-            'Existent mimetype is allowed' => [
-                'filetypes' => 'application/xml',
-                'expected' => []
-            ],
-            'Nonexistent mimetype is not allowed' => [
-                'filetypes' => 'ricefield/rat',
-                'expected' => ['ricefield/rat']
-            ],
-            'Multiple nonexistent mimetypes are not allowed' => [
-                'filetypes' => 'ricefield/rat cam/ball',
-                'expected' => ['ricefield/rat', 'cam/ball']
-            ],
-            'Missing dot in extension is not allowed' => [
-                'filetypes' => 'png',
-                'expected' => ['png']
-            ],
-            'Some existent some not' => [
-                'filetypes' => '.txt application/xml web_file ©ç√√ß∂å√©åß©√ .png ricefield/rat document png',
-                'expected' => ['©ç√√ß∂å√©åß©√', 'ricefield/rat', 'png']
-            ]
-        ];
-    }
-
-    /**
-     * Test get_nonexistent_file_types().
-     * @dataProvider get_nonexistent_file_types_provider
-     * @param string $filetypes The filetypes to check
-     * @param array $expected The expected result. The list of non existent file types.
-     */
-    public function test_get_nonexistent_file_types($filetypes, $expected) {
-        $this->resetAfterTest();
-        $method = new ReflectionMethod(assign_submission_file::class, 'get_nonexistent_file_types');
-        $method->setAccessible(true);
-        $plugin = $this->assign->get_submission_plugin_by_type('file');
-        $nonexistentfiletypes = $method->invokeArgs($plugin, [$filetypes]);
-        $this->assertSame($expected, $nonexistentfiletypes);
-    }
-
 }
index 8a23482..938b07f 100644 (file)
@@ -17,8 +17,7 @@ Feature: Edited book chapters handle tags correctly
       | teacher1 | C1 | editingteacher |
       | student1 | C1 | student |
     And I log in as "teacher1"
-    And I am on "Course 1" course homepage
-    And I turn editing mode on
+    And I am on "Course 1" course homepage with editing mode on
     And I add a "Book" to section "1" and I fill the form with:
       | Name | Test book |
       | Description | A book about dreams! |
index e71c98c..0cd9457 100644 (file)
@@ -596,7 +596,7 @@ class mod_data_external extends external_api {
 
         $params = array('databaseid' => $databaseid);
         $params = self::validate_parameters(self::get_fields_parameters(), $params);
-        $warnings = array();
+        $fields = $warnings = array();
 
         list($database, $course, $cm, $context) = self::validate_database($params['databaseid']);
 
@@ -807,7 +807,9 @@ class mod_data_external extends external_api {
                 'entries' => new external_multiple_structure(
                     record_exporter::get_read_structure()
                 ),
-                'totalcount' => new external_value(PARAM_INT, 'Total count of records.'),
+                'totalcount' => new external_value(PARAM_INT, 'Total count of records returned by the search.'),
+                'maxcount' => new external_value(PARAM_INT, 'Total count of records that the user could see in the database
+                    (if all the search criterias were removed).', VALUE_OPTIONAL),
                 'listviewcontents' => new external_value(PARAM_RAW, 'The list view contents as is rendered in the site.',
                                                             VALUE_OPTIONAL),
                 'warnings' => new external_warnings()
index f7806eb..3e41e9e 100644 (file)
@@ -70,12 +70,12 @@ class content_exporter extends exporter {
                 'null' => NULL_ALLOWED,
             ),
             'content3' => array(
-                'type' => PARAM_BOOL,
+                'type' => PARAM_RAW,
                 'description' => 'Contents.',
                 'null' => NULL_ALLOWED,
             ),
             'content4' => array(
-                'type' => PARAM_BOOL,
+                'type' => PARAM_RAW,
                 'description' => 'Contents.',
                 'null' => NULL_ALLOWED,
             ),
index 89624ef..93be6ac 100644 (file)
@@ -1006,15 +1006,17 @@ function data_search_entries($data, $cm, $context, $mode, $currentgroup, $search
 
     $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams);
     $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data->id);
-    $totalcount = count($newrecordids);
     $selectdata = $where . $groupselect . $approveselect;
 
     if (!empty($advanced)) {
         $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
         $sqlselect = $advancedsearchsql['sql'];
         $allparams = array_merge($allparams, $advancedsearchsql['params']);
+        $totalcount = count($newrecordids);
     } else {
         $sqlselect  = "SELECT $what $fromsql $sortorder";
+        $sqlcountselect  = "SELECT $count $fromsql";
+        $totalcount = $DB->count_records_sql($sqlcountselect, $allparams);
     }
 
     // Work out the paging numbers and counts.
index d9da1db..4f74d2c 100644 (file)
@@ -17,8 +17,7 @@ Feature: Set entries required as a completion condition for a data item
       | teacher1 | C1 | editingteacher |
       | student1 | C1 | student |
     And I log in as "teacher1"
-    And I follow "C1"
-    And I turn editing mode on
+    And I am on "Course 1" course homepage with editing mode on
     And I add a "Database" to section "1" and I fill the form with:
       | Name | Test database name |
       | Description | Test database description |
@@ -26,38 +25,38 @@ Feature: Set entries required as a completion condition for a data item
       | completionview | 0 |
       | completionentriesenabled | checked |
       | completionentries        | 2 |
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I add a "Text input" field to "Test database name" database and I fill the form with:
       | Field name | Test field name |
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I log out
     When I log in as "student1"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I add an entry to "Test database name" database with:
       | Test field name | Student original entry |
     And I press "Save and view"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I log out
     And I log in as "teacher1"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     #One entry is not enough to mark as complete
     And "Student 1" user has not completed "Test database name" activity
     And I log out
     When I log in as "student1"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I add an entry to "Test database name" database with:
       | Test field name | Student second entry |
     And I press "Save and view"
     And I log out
     And I log in as "teacher1"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     Then "Student 1" user has completed "Test database name" activity
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Test database name"
     And I navigate to "Edit settings" in current page administration
     And I press "Unlock completion"
     And I set the field "completionentries" to "1"
     And I press "Save and display"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     Then "Student 1" user has completed "Test database name" activity
     And I log out
index 1b61d09..8220817 100644 (file)
@@ -659,6 +659,18 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         }
     }
 
+    /**
+     * Test get_fields_database_without_fields.
+     */
+    public function test_get_fields_database_without_fields() {
+
+        $this->setUser($this->student1);
+        $result = mod_data_external::get_fields($this->database->id);
+        $result = external_api::clean_returnvalue(mod_data_external::get_fields_returns(), $result);
+
+        $this->assertEmpty($result['fields']);
+    }
+
     /**
      * Test search_entries.
      */
@@ -666,12 +678,25 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         global $DB;
         list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
 
-        // First do a normal text search as student 1. I should see my two group entries.
         $this->setUser($this->student1);
+        // Empty search, it should return all the visible entries.
+        $result = mod_data_external::search_entries($this->database->id, 0, false);
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(2, $result['entries']);
+        $this->assertEquals(2, $result['totalcount']);
+
+        // Search for something that does not exists.
+        $result = mod_data_external::search_entries($this->database->id, 0, false, 'abc');
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+        $this->assertEquals(0, $result['totalcount']);
+
+        // Search by text matching all the entries.
         $result = mod_data_external::search_entries($this->database->id, 0, false, 'text');
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(2, $result['entries']);
         $this->assertEquals(2, $result['totalcount']);
+        $this->assertEquals(2, $result['maxcount']);
 
         // Now as the other student I should receive my not approved entry. Apply ordering here.
         $this->setUser($this->student2);
@@ -679,6 +704,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(3, $result['entries']);
         $this->assertEquals(3, $result['totalcount']);
+        $this->assertEquals(3, $result['maxcount']);
         // The not approved one should be the first.
         $this->assertEquals($entry13, $result['entries'][0]['id']);
 
@@ -688,6 +714,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(1, $result['entries']);
         $this->assertEquals(1, $result['totalcount']);
+        $this->assertEquals(1, $result['maxcount']);
         $this->assertEquals($this->student3->id, $result['entries'][0]['userid']);
 
         // Same normal text search as teacher.
@@ -696,6 +723,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(4, $result['entries']);  // I can see all groups and non approved.
         $this->assertEquals(4, $result['totalcount']);
+        $this->assertEquals(4, $result['maxcount']);
 
         // Pagination.
         $this->setUser($this->teacher);
@@ -703,6 +731,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(2, $result['entries']);  // Only 2 per page.
         $this->assertEquals(4, $result['totalcount']);
+        $this->assertEquals(4, $result['maxcount']);
 
         // Now advanced search or not dinamic fields (user firstname for example).
         $this->setUser($this->student1);
@@ -713,6 +742,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(1, $result['entries']);
         $this->assertEquals(1, $result['totalcount']);
+        $this->assertEquals(2, $result['maxcount']);
         $this->assertEquals($this->student2->id, $result['entries'][0]['userid']);  // I only found mine!
 
         // Advanced search for fields.
@@ -724,6 +754,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(2, $result['entries']);  // Found two entries matching this.
         $this->assertEquals(2, $result['totalcount']);
+        $this->assertEquals(2, $result['maxcount']);
 
         // Combined search.
         $field2 = $DB->get_record('data_fields', array('type' => 'number'));
@@ -736,6 +767,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(1, $result['entries']);  // Only one matching everything.
         $this->assertEquals(1, $result['totalcount']);
+        $this->assertEquals(2, $result['maxcount']);
 
         // Combined search (no results).
         $field2 = $DB->get_record('data_fields', array('type' => 'number'));
@@ -747,6 +779,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(0, $result['entries']);  // Only one matching everything.
         $this->assertEquals(0, $result['totalcount']);
+        $this->assertEquals(2, $result['maxcount']);
     }
 
     /**
index 7ee17a8..92ede9d 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /mod/data - plugins,
 information provided here is intended especially for developers.
 
+=== 3.4 ===
+* External function mod_data_external::search_entries() now returns the maxcount field: Total count of records that the user could
+    see in the database (if all the search criterias were removed).
+
 === 3.3.2 ===
 * data_refresh_events() Now takes two additional parameters to refine the update to a specific instance. This function
   now optionally takes the module instance object or ID, and the course module object or ID. Please try to send the full
index 849b945..cfe497c 100644 (file)
@@ -247,7 +247,7 @@ Feature: Anonymous feedback
 
   Scenario: Collecting new non-anonymous feedback from a previously anonymous feedback activity
     When I log in as "teacher"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
@@ -260,7 +260,7 @@ Feature: Anonymous feedback
       | Maximum characters accepted | 200                    |
     And I log out
     When I log in as "user1"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I follow "Answer the questions..."
     And I set the following fields to these values:
@@ -269,7 +269,7 @@ Feature: Anonymous feedback
     And I log out
     # Switch to non-anon responses.
     And I log in as "teacher"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
@@ -278,7 +278,7 @@ Feature: Anonymous feedback
     And I log out
     # Now leave a non-anon feedback as user1
     When I log in as "user1"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I follow "Answer the questions..."
     And I set the following fields to these values:
@@ -287,7 +287,7 @@ Feature: Anonymous feedback
     And I log out
     # Now check the responses are correct.
     When I log in as "teacher"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I follow "Show responses"
     And I should see "Anonymous entries (1)"
index 12fba55..d4a0315 100644 (file)
@@ -81,7 +81,7 @@ Feature: Mapping courses in a feedback
       | this is a simple multiple choice | option d |
     And I press "Submit your answers"
     And I press "Continue"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I click on "Course feedback" "link" in the "Feedback" "block"
     And I follow "Answer the questions..."
     And I should not see "Acceptance test site" in the ".feedback_form" "css_element"
index aa226bf..4087dd6 100644 (file)
@@ -25,7 +25,7 @@ Feature: Non anonymous feedback with multiple submissions
 
   Scenario: Completing a feedback second time
     When I log in as "teacher"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I click on "Edit questions" "link" in the "[role=main]" "css_element"
     And I add a "Short text answer" question to the feedback with:
@@ -39,7 +39,7 @@ Feature: Non anonymous feedback with multiple submissions
       | Maximum characters accepted | 200        |
     And I log out
     And I log in as "user1"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I follow "Answer the questions..."
     And I set the following fields to these values:
@@ -50,7 +50,7 @@ Feature: Non anonymous feedback with multiple submissions
     And I press "Submit your answers"
     And I log out
     And I log in as "user1"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I follow "Answer the questions..."
     Then the field "first" matches value "111"
index 91c5105..95a227b 100644 (file)
@@ -67,7 +67,6 @@ Feature: Blog posts are always displayed in reverse chronological order
       | Message | Reply to the first post |
     And I press "Post to forum"
     And I wait to be redirected
-    And I am on site homepage
     And I am on "Course 1" course homepage
     And I follow "Course blog forum"
     #
index 61d7c65..3812ace 100644 (file)
@@ -17,8 +17,7 @@ Feature: Edited glossary entries handle tags correctly
       | teacher1 | C1 | editingteacher |
       | student1 | C1 | student |
     And I log in as "teacher1"
-    And I am on "Course 1" course homepage
-    And I turn editing mode on
+    And I am on "Course 1" course homepage with editing mode on
     And I add a "Glossary" to section "1" and I fill the form with:
       | Name | Test glossary |
       | Description | A glossary about dreams! |
index 53ff09f..3cbc37b 100644 (file)
@@ -20,8 +20,7 @@ Feature: In a lesson activity, a teacher can duplicate a lesson page
     And I follow "Manage private files"
     And I upload "mod/lesson/tests/fixtures/moodle_logo.jpg" file to "Files" filemanager
     And I click on "Save changes" "button"
-    When I am on homepage
-    And I am on "Course 1" course homepage with editing mode on
+    When I am on "Course 1" course homepage with editing mode on
     And I add a "Lesson" to section "1" and I fill the form with:
       | Name | Test lesson name |
       | Description | Test lesson description |
index 9fa478c..80d26e1 100644 (file)
@@ -17,8 +17,7 @@ Feature: In a lesson activity, teacher can import blackboard fill in the blank q
       | teacher1 | C1 | editingteacher |
       | student1 | C1 | student |
     And I log in as "teacher1"
-    When I am on homepage
-    And I am on "Course 1" course homepage with editing mode on
+    When I am on "Course 1" course homepage with editing mode on
     And I add a "Lesson" to section "1" and I fill the form with:
       | Name | Test lesson name |
       | Description | Test lesson description |
index c9f996c..88adddc 100644 (file)
@@ -17,8 +17,7 @@ Feature: In a lesson activity, teacher can import embedded images in questions a
       | teacher1 | C1 | editingteacher |
       | student1 | C1 | student |
     And I log in as "teacher1"
-    When I am on homepage
-    And I am on "Course 1" course homepage with editing mode on
+    When I am on "Course 1" course homepage with editing mode on
     And I add a "Lesson" to section "1" and I fill the form with:
       | Name | Test lesson name |
       | Description | Test lesson description |
index ce3af1a..0dc725c 100644 (file)
@@ -20,8 +20,7 @@ Feature: In a lesson activity, teacher can add embedded images in questions answ
     And I follow "Manage private files"
     And I upload "mod/lesson/tests/fixtures/moodle_logo.jpg" file to "Files" filemanager
     And I click on "Save changes" "button"
-    When I am on homepage
-    And I am on "Course 1" course homepage with editing mode on
+    When I am on "Course 1" course homepage with editing mode on
     And I add a "Lesson" to section "1" and I fill the form with:
       | Name | Test lesson name |
       | Description | Test lesson description |
index 615c66f..6a97bd1 100644 (file)
@@ -514,6 +514,11 @@ class mod_quiz_mod_form extends moodleform_mod {
                 $toform[$name] = $value;
             }
         }
+
+        // Completion settings check.
+        if (empty($toform['completionusegrade'])) {
+            $toform['completionpass'] = 0; // Forced unchecked.
+        }
     }
 
     public function validation($data, $files) {
@@ -618,7 +623,7 @@ class mod_quiz_mod_form extends moodleform_mod {
         $group = array();
         $group[] = $mform->createElement('advcheckbox', 'completionpass', null, get_string('completionpass', 'quiz'),
                 array('group' => 'cpass'));
-
+        $mform->disabledIf('completionpass', 'completionusegrade', 'notchecked');
         $group[] = $mform->createElement('advcheckbox', 'completionattemptsexhausted', null,
                 get_string('completionattemptsexhausted', 'quiz'),
                 array('group' => 'cattempts'));
index 735b7ed..58e1399 100644 (file)
@@ -40,14 +40,14 @@ Feature: Set a quiz to be marked complete when the student uses all attempts all
     And I set the field "False" to "1"
     And I press "Finish attempt ..."
     And I press "Submit all and finish"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And the "Test quiz name" "quiz" activity with "auto" completion should be marked as not complete
     And I follow "Test quiz name"
     And I press "Re-attempt quiz"
     And I set the field "False" to "1"
     And I press "Finish attempt ..."
     And I press "Submit all and finish"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     Then "Completed: Test quiz name" "icon" should exist in the "li.modtype_quiz" "css_element"
     And I log out
     And I log in as "teacher1"
index f8e783c..7afbec5 100644 (file)
@@ -40,7 +40,7 @@ Feature: Set a quiz to be marked complete when the student passes
     And I set the field "True" to "1"
     And I press "Finish attempt ..."
     And I press "Submit all and finish"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     Then "Completed: Test quiz name" "icon" should exist in the "li.modtype_quiz" "css_element"
     And I log out
     And I log in as "teacher1"
index 74bf44d..f6362c2 100644 (file)
@@ -31,6 +31,7 @@ require_once($CFG->libdir.'/completionlib.php');
 $id       = optional_param('id', 0, PARAM_INT); // Course Module ID
 $r        = optional_param('r', 0, PARAM_INT);  // Resource instance ID
 $redirect = optional_param('redirect', 0, PARAM_BOOL);
+$forceview = optional_param('forceview', 0, PARAM_BOOL);
 
 if ($r) {
     if (!$resource = $DB->get_record('resource', array('id'=>$r))) {
@@ -76,12 +77,7 @@ if (count($files) < 1) {
 $resource->mainfile = $file->get_filename();
 $displaytype = resource_get_final_display_type($resource);
 if ($displaytype == RESOURCELIB_DISPLAY_OPEN || $displaytype == RESOURCELIB_DISPLAY_DOWNLOAD) {
-    // For 'open' and 'download' links, we always redirect to the content - except
-    // if the user just chose 'save and display' from the form then that would be
-    // confusing
-    if (strpos(get_local_referer(false), 'modedit.php') === false) {
-        $redirect = true;
-    }
+    $redirect = true;
 }
 
 // Don't redirect teachers, otherwise they can not access course or module settings.
@@ -91,7 +87,7 @@ if ($redirect && !course_get_format($course)->has_view_page() &&
     $redirect = false;
 }
 
-if ($redirect) {
+if ($redirect && !$forceview) {
     // coming from course page or url index page
     // this redirect trick solves caching problems when tracking views ;-)
     $path = '/'.$context->id.'/mod_resource/content/'.$resource->revision.$file->get_filepath().$file->get_filename();
index 7b8b91a..dc20f0b 100644 (file)
@@ -831,7 +831,9 @@ function scorm_reset_userdata($data) {
         $status[] = array('component' => $componentstr, 'item' => get_string('deleteallattempts', 'scorm'), 'error' => false);
     }
 
-    // No dates to shift here.
+    // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+    // See MDL-9367.
+    shift_course_mod_dates('scorm', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
 
     return $status;
 }
index d2a8dab..1edbcad 100644 (file)
@@ -1,6 +1,18 @@
 This files describes API changes in /mod/* - activity modules,
 information provided here is intended especially for developers.
 
+=== 3.4 ===
+
+* Navigation between activities via a previous and next link was added to Boost, Clean and Bootstrapbase. This
+  was made possible by a new function core_renderer->activity_navigation(). However, there was an issue when linking
+  to the mod_resource and mod_url view.php pages where it would automatically download the file, or redirect to
+  the URL. It was noticed that this was not the case when editing the module and clicking 'Save and display' which would
+  take you to the pages without downloading the file or redirecting to a link. The reason this worked was because of the
+  hard-coded check 'if (strpos(get_local_referer(false), 'modedit.php') === false) {' in the view.php files. This check
+  has been removed in favour of an optional_param('forceview'). If you are using the above hard-coded check in your
+  plugin it is recommended to remove it and use the optional param as it will prevent the navigation from working as
+  expected.
+
 === 3.3 ===
 
 * External functions that were returning file information now return the following additional file fields:
index 4ca3131..cc5c12f 100644 (file)
@@ -31,6 +31,7 @@ require_once($CFG->libdir . '/completionlib.php');
 $id       = optional_param('id', 0, PARAM_INT);        // Course module ID
 $u        = optional_param('u', 0, PARAM_INT);         // URL instance id
 $redirect = optional_param('redirect', 0, PARAM_BOOL);
+$forceview = optional_param('forceview', 0, PARAM_BOOL);
 
 if ($u) {  // Two ways to specify the module
     $url = $DB->get_record('url', array('id'=>$u), '*', MUST_EXIST);
@@ -66,14 +67,10 @@ unset($exturl);
 
 $displaytype = url_get_final_display_type($url);
 if ($displaytype == RESOURCELIB_DISPLAY_OPEN) {
-    // For 'open' links, we always redirect to the content - except if the user
-    // just chose 'save and display' from the form then that would be confusing
-    if (strpos(get_local_referer(false), 'modedit.php') === false) {
-        $redirect = true;
-    }
+    $redirect = true;
 }
 
-if ($redirect) {
+if ($redirect && !$forceview) {
     // coming from course page or url index page,
     // the redirection is needed for completion tracking and logging
     $fullurl = str_replace('&amp;', '&', url_get_full_url($url, $cm, $course));
index 728af4c..0a187a6 100644 (file)
@@ -230,6 +230,11 @@ function wiki_reset_userdata($data) {
             }
         }
     }
+
+    // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+    // See MDL-9367.
+    shift_course_mod_dates('wiki', array('editbegin', 'editend'), $data->timeshift, $data->courseid);
+
     return $status;
 }
 
index 67197cc..63cd369 100644 (file)
@@ -1890,6 +1890,11 @@ function workshop_reset_course_form_defaults(stdClass $course) {
 function workshop_reset_userdata(stdClass $data) {
     global $CFG, $DB;
 
+    // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+    // See MDL-9367.
+    shift_course_mod_dates('workshop', array('submissionstart', 'submissionend', 'assessmentstart', 'assessmentend'),
+        $data->timeshift, $data->courseid);
+
     if (empty($data->reset_workshop_submissions)
             and empty($data->reset_workshop_assessments)
             and empty($data->reset_workshop_phase) ) {
index a125f5d..615d18d 100644 (file)
@@ -36,6 +36,7 @@
 
 .search-input-wrapper > form > input {
     margin: 0;
+    height: 23px;
 }
 
 .search-input-wrapper form.expanded {
index 162abae..a25cdca 100644 (file)
@@ -58,6 +58,7 @@
                     <div class="card card-block">
                     {{{ output.course_content_header }}}
                     {{{ output.main_content }}}
+                    {{{ output.activity_navigation }}}
                     {{{ output.course_content_footer }}}
                     </div>
                 </section>
index 2a7758c..1c2c735 100644 (file)
@@ -82,6 +82,7 @@
                     {{/hasregionmainsettingsmenu}}
                     {{{ output.course_content_header }}}
                     {{{ output.main_content }}}
+                    {{{ output.activity_navigation }}}
                     {{{ output.course_content_footer }}}
                     </div>
                 </section>
index 97b9672..59afcc6 100644 (file)
@@ -63,6 +63,7 @@ echo $OUTPUT->doctype() ?>
             <?php
             echo $OUTPUT->course_content_header();
             echo $OUTPUT->main_content();
+            echo $OUTPUT->activity_navigation();
             echo $OUTPUT->course_content_footer();
             ?>
         </section>
index 91cb6da..f9635da 100644 (file)
@@ -69,6 +69,7 @@ echo $OUTPUT->doctype() ?>
                     <?php
                     echo $OUTPUT->course_content_header();
                     echo $OUTPUT->main_content();
+                    echo $OUTPUT->activity_navigation();
                     echo $OUTPUT->course_content_footer();
                     ?>
                 </section>
index 3070704..53042bc 100644 (file)
@@ -65,6 +65,7 @@ echo $OUTPUT->doctype() ?>
             <?php
             echo $OUTPUT->course_content_header();
             echo $OUTPUT->main_content();
+            echo $OUTPUT->activity_navigation();
             echo $OUTPUT->course_content_footer();
             ?>
         </section>
index a9c4fa0..44bc41f 100644 (file)
@@ -76,6 +76,7 @@ echo $OUTPUT->doctype() ?>
                     <?php
                     echo $OUTPUT->course_content_header();
                     echo $OUTPUT->main_content();
+                    echo $OUTPUT->activity_navigation();
                     echo $OUTPUT->course_content_footer();
                     ?>
                 </section>
index ecf77f0..b080c25 100644 (file)
@@ -9,6 +9,8 @@ information provided here is intended especially for theme designer.
   the first one was renamed to loginform.mustache - see MDL-58970.
 * The Boost flat navigation nodes now have several data-attributes which let plugin developers
   access properties from the underlying navigation nodes in the browser - see MDL-59425.
+* Navigation between activities via a previous and next link was added to Boost, Clean and Bootstrapbase. This
+  is made possible by a new function core_renderer->activity_navigation().
 
 === 3.3 ===
 
index 0e9ae10..d5eed25 100644 (file)
 
 require_once("../config.php");
 
-$formaction = required_param('formaction', PARAM_FILE);
+$formaction = required_param('formaction', PARAM_LOCALURL);
 $id = required_param('id', PARAM_INT);
 
 $PAGE->set_url('/user/action_redir.php', array('formaction' => $formaction, 'id' => $id));
+list($formaction) = explode('?', $formaction, 2);
 
 // Add every page will be redirected by this script.
 $actions = array(
         'messageselect.php',
         'addnote.php',
         'groupaddnote.php',
+        'bulkchange.php'
         );
 
 if (array_search($formaction, $actions) === false) {
@@ -44,4 +46,120 @@ if (!confirm_sesskey()) {
     print_error('confirmsesskeybad');
 }
 
-require_once($formaction);
+if ($formaction == 'bulkchange.php') {
+    // Backwards compatibility for enrolment plugins bulk change functionality.
+    // This awful code is adapting from the participant page with it's param names and values
+    // to the values expected by the bulk enrolment changes forms.
+    $formaction = required_param('formaction', PARAM_URL);
+    require_once($CFG->dirroot . '/enrol/locallib.php');
+
+    $url = new moodle_url($formaction);
+    // Get the enrolment plugin type and bulk action from the url.
+    $plugin = $url->param('plugin');
+    $operationname = $url->param('operation');
+
+    $course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST);
+    $context = context_course::instance($id);
+    $PAGE->set_context($context);
+
+    $instances = enrol_get_instances($course->id, false);
+    $instance = false;
+    foreach ($instances as $oneinstance) {
+        if ($oneinstance->enrol == $plugin) {
+            $instance = $oneinstance;
+            break;
+        }
+    }
+    if (!$instance) {
+        print_error('errorwithbulkoperation', 'enrol');
+    }
+
+    $manager = new course_enrolment_manager($PAGE, $course, $instance->id);
+    $plugins = $manager->get_enrolment_plugins();
+
+    if (!isset($plugins[$plugin])) {
+        print_error('errorwithbulkoperation', 'enrol');
+    }
+
+    $plugin = $plugins[$plugin];
+
+    $operations = $plugin->get_bulk_operations($manager);
+
+    if (!isset($operations[$operationname])) {
+        print_error('errorwithbulkoperation', 'enrol');
+    }
+    $operation = $operations[$operationname];
+
+    $userids = optional_param_array('userid', array(), PARAM_INT);
+    $default = new moodle_url('/user/index.php', ['id' => $course->id]);
+    $returnurl = new moodle_url(optional_param('returnto', $default, PARAM_URL));
+
+    if (empty($userids)) {
+        $userids = optional_param_array('bulkuser', array(), PARAM_INT);
+    }
+    if (empty($userids)) {
+        // The first time list hack.
+        if (empty($userids) and $post = data_submitted()) {
+            foreach ($post as $k => $v) {
+                if (preg_match('/^user(\d+)$/', $k, $m)) {
+                    $userids[] = $m[1];
+                }
+            }
+        }
+    }
+
+    if (empty($userids)) {
+        redirect($returnurl, get_string('noselectedusers', 'bulkusers'));
+    }
+
+    $users = $manager->get_users_enrolments($userids);
+
+    // We may have users from any kind of enrolment, we need to filter for the enrolment plugin matching the bulk action.
+    $matchesplugin = function($user) use ($plugin) {
+        foreach ($user->enrolments as $enrolment) {
+            if ($enrolment->enrolmentplugin->get_name() == $plugin->get_name()) {
+                return true;
+            }
+        }
+        return false;
+    };
+    $users = array_filter($users, $matchesplugin);
+
+    if (empty($users)) {
+        redirect($returnurl, get_string('noselectedusers', 'bulkusers'));
+    }
+
+    // Get the form for the bulk operation.
+    $mform = $operation->get_form($PAGE->url, array('users' => $users));
+    // If the mform is false then attempt an immediate process. This may be an immediate action that
+    // doesn't require user input OR confirmation.... who know what but maybe one day.
+    if ($mform === false) {
+        if ($operation->process($manager, $users, new stdClass)) {
+            redirect($returnurl);
+        } else {
+            print_error('errorwithbulkoperation', 'enrol');
+        }
+    }
+    // Check if the bulk operation has been cancelled.
+    if ($mform->is_cancelled()) {
+        redirect($returnurl);
+    }
+    if ($mform->is_submitted() && $mform->is_validated() && confirm_sesskey()) {
+        if ($operation->process($manager, $users, $mform->get_data())) {
+            redirect($returnurl);
+        }
+    }
+
+    $pagetitle = get_string('bulkuseroperation', 'enrol');
+
+    $PAGE->set_title($pagetitle);
+    $PAGE->set_heading($pagetitle);
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading($operation->get_title());
+    $mform->display();
+    echo $OUTPUT->footer();
+    exit();
+
+} else {
+    require_once($formaction);
+}
index f5edc82..b83eb53 100644 (file)
@@ -363,6 +363,22 @@ if ($bulkoperations) {
         $displaylist['groupaddnote.php'] = get_string('groupaddnewnote', 'notes');
     }
 
+    $plugins = $manager->get_enrolment_plugins();
+    foreach ($plugins as $plugin) {
+        $bulkoperations = $plugin->get_bulk_operations($manager);
+
+        $pluginoptions = [];
+        foreach ($bulkoperations as $key => $bulkoperation) {
+            $params = ['plugin' => $plugin->get_name(), 'operation' => $key];
+            $url = new moodle_url('bulkchange.php', $params);
+            $pluginoptions[$url->out(false)] = $bulkoperation->get_title();
+        }
+        if (!empty($pluginoptions)) {
+            $name = get_string('pluginname', 'enrol_' . $plugin->get_name());
+            $displaylist[] = [$name => $pluginoptions];
+        }
+    }
+
     echo $OUTPUT->help_icon('withselectedusers');
     echo html_writer::tag('label', get_string("withselectedusers"), array('for' => 'formactionid'));
     echo html_writer::select($displaylist, 'formaction', '', array('' => 'choosedots'), array('id' => 'formactionid'));
diff --git a/user/tests/behat/bulk_editenrolment.feature b/user/tests/behat/bulk_editenrolment.feature
new file mode 100644 (file)
index 0000000..7a85f72
--- /dev/null
@@ -0,0 +1,45 @@
+@core @core_user
+Feature: Bulk enrolments
+  In order to manage a course site
+  As a teacher
+  I need to be able to bulk edit enrolments
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+      | teacher1 | C1 | editingteacher |
+
+  @javascript
+  Scenario: Bulk edit enrolments
+    When I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I follow "Participants"
+    And I press "Select all"
+    And I set the field "With selected users..." to "Edit selected user enrolments"
+    And I set the field "Alter status" to "Suspended"
+    And I press "Save changes"
+    Then I should see "Suspended" in the "Teacher 1" "table_row"
+    Then I should see "Suspended" in the "Student 1" "table_row"
+    And I should see "Suspended" in the "Student 2" "table_row"
+
+  @javascript
+  Scenario: Bulk delete enrolments
+    When I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I follow "Participants"
+    And I press "Select all"
+    And I set the field "With selected users..." to "Delete selected user enrolments"
+    And I press "Unenrol users"
+    Then I should not see "Student 1"
+    And I should not see "Student 2"
+    And I should not see "Teacher 1"
index c14ca65..6d0ed76 100644 (file)
@@ -17,8 +17,7 @@ Feature: As a user, "Course preferences" allows me to set my course preference(s
     # See that the "activity chooser" is enabled by default.
     Given the field "enableactivitychooser" matches value "1"
     # See that the "activity chooser" is actually shown by default in course page.
-    When I am on homepage
-    And I am on "Course 1" course homepage
+    When I am on "Course 1" course homepage
     And I should not see "Add an activity or resource" in the "Topic 1" "section"
     And I turn editing mode on
     Then I should see "Add an activity or resource" in the "Topic 1" "section"
@@ -28,8 +27,7 @@ Feature: As a user, "Course preferences" allows me to set my course preference(s
   Scenario: As a user, "activity chooser" should be disabled when I uncheck it in "Course preferences"
     Given I set the field "enableactivitychooser" to "0"
     And I press "Save changes"
-    When I am on homepage
-    And I am on "Course 1" course homepage
+    When I am on "Course 1" course homepage
     And I should not see "Add a resource..." in the "Topic 1" "section"
     And I turn editing mode on
     Then I should see "Add a resource..." in the "Topic 1" "section"
index 58d99cc..8da6610 100644 (file)
@@ -33,6 +33,5 @@ Feature: Set the site home page and dashboard as the default home page
     And I follow "Dashboard"
     And I follow "Make this my default home page"
     And I should not see "Make this my default home page"
-    And I am on site homepage
-    When I am on "Course 1" course homepage
+    And I am on "Course 1" course homepage
     Then "Dashboard" "text" should exist in the ".breadcrumb-nav" "css_element"
index 7ae262a..cf1bb36 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017072700.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017072700.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
diff --git a/webservice/classes/token_table.php b/webservice/classes/token_table.php
new file mode 100644 (file)
index 0000000..059fc04
--- /dev/null
@@ -0,0 +1,260 @@
+<?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 the class used for the displaying the tokens table.
+ *
+ * @package    core_webservice
+ * @copyright  2017 John Okely <john@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace webservice;
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . '/tablelib.php');
+require_once($CFG->dirroot . '/webservice/lib.php');
+require_once($CFG->dirroot . '/user/lib.php');
+
+/**
+ * Class for the displaying the participants table.
+ *
+ * @package    core_webservice
+ * @copyright  2017 John Okely <john@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class token_table extends \table_sql {
+
+    /**
+     * @var bool $showalltokens Whether or not the user is able to see all tokens.
+     */
+    protected $showalltokens;
+
+    /**
+     * Sets up the table.
+     * @param int $id The id of the table
+     */
+    public function __construct($id) {
+        parent::__construct($id);
+
+        // Get the context.
+        $context = \context_system::instance();
+
+        // Can we see tokens created by all users?
+        $this->showalltokens = has_capability('moodle/webservice:managealltokens', $context);
+
+        // Define the headers and columns.
+        $headers = [];
+        $columns = [];
+
+        $headers[] = get_string('token', 'webservice');
+        $columns[] = 'token';
+        $headers[] = get_string('user');
+        $columns[] = 'fullname';
+        $headers[] = get_string('service', 'webservice');
+        $columns[] = 'name';
+        $headers[] = get_string('iprestriction', 'webservice');
+        $columns[] = 'iprestriction';
+        $headers[] = get_string('validuntil', 'webservice');
+        $columns[] = 'validuntil';
+        if ($this->showalltokens) {
+            // Only need to show creator if you can see tokens created by other people.
+            $headers[] = get_string('tokencreator', 'webservice');
+            $columns[] = 'creatorlastname'; // So we can have semi-useful sorting. Table SQL doesn't two fullname collumns.
+        }
+        $headers[] = get_string('operation', 'webservice');
+        $columns[] = 'operation';
+
+        $this->define_columns($columns);
+        $this->define_headers($headers);
+
+        $this->no_sorting('operation');
+        $this->no_sorting('token');
+        $this->no_sorting('iprestriction');
+
+        $this->set_attribute('id', $id);
+    }
+
+    /**
+     * Generate the operation column.
+     *
+     * @param \stdClass $data Data for the current row
+     * @return string Content for the column
+     */
+    public function col_operation($data) {
+        $tokenpageurl = new \moodle_url(
+            "/admin/webservice/tokens.php",
+            [
+                "sesskey" => sesskey(),
+                "action" => "delete",
+                "tokenid" => $data->id
+            ]
+        );
+        return \html_writer::link($tokenpageurl, get_string("delete"));
+    }
+
+    /**
+     * Generate the validuntil column.
+     *
+     * @param \stdClass $data Data for the current row
+     * @return string Content for the column
+     */
+    public function col_validuntil($data) {
+        if (empty($data->validuntil)) {
+            return '';
+        } else {
+            return userdate($data->validuntil, get_string('strftimedatetime', 'langconfig'));
+        }
+    }
+
+    /**
+     * Generate the fullname column. Also includes capabilities the user is missing for the webservice (if any)
+     *
+     * @param \stdClass $data Data for the current row
+     * @return string Content for the column
+     */
+    public function col_fullname($data) {
+        global $OUTPUT;
+
+        $userprofilurl = new \moodle_url('/user/profile.php', ['id' => $data->userid]);
+        $content = \html_writer::link($userprofilurl, fullname($data));
+
+        // Make up list of capabilities that the user is missing for the given webservice.
+        $webservicemanager = new \webservice();
+        $usermissingcaps = $webservicemanager->get_missing_capabilities_by_users([['id' => $data->userid]], $data->serviceid);
+
+        if (!is_siteadmin($data->userid) && array_key_exists($data->userid, $usermissingcaps)) {
+            $missingcapabilities = implode(', ', $usermissingcaps[$data->userid]);
+            if (!empty($missingcapabilities)) {
+                $capabilitiesstring = get_string('usermissingcaps', 'webservice', $missingcapabilities) . '&nbsp;' .
+                        $OUTPUT->help_icon('missingcaps', 'webservice');
+                $content .= \html_writer::div($capabilitiesstring, 'missingcaps');
+            }
+        }
+
+        return $content;
+    }
+
+    /**
+     * Generate the token column.
+     *
+     * @param \stdClass $data Data for the current row
+     * @return string Content for the column
+     */
+    public function col_token($data) {
+        global $USER;
+        // Hide the token if it wasn't created by the current user.
+        if ($data->creatorid != $USER->id) {
+            return '-';
+        }
+
+        return $data->token;
+    }
+
+    /**
+     * Generate the creator column.
+     *
+     * @param \stdClass $data
+     * @return string
+     */
+    public function col_creatorlastname($data) {
+        // We have loaded all the name fields for the creator, with the 'creator' prefix.
+        // So just remove the prefix and make up a user object.
+        $user = [];
+        foreach ($data as $key => $value) {
+            if (strpos($key, 'creator') !== false) {
+                $newkey = str_replace('creator', '', $key);
+                $user[$newkey] = $value;
+            }
+        }
+
+        $creatorprofileurl = new \moodle_url('/user/profile.php', ['id' => $data->creatorid]);
+        return \html_writer::link($creatorprofileurl, fullname((object)$user));
+    }
+
+    /**
+     * This function is used for the extra user fields.
+     *
+     * These are being dynamically added to the table so there are no functions 'col_<userfieldname>' as
+     * the list has the potential to increase in the future and we don't want to have to remember to add
+     * a new method to this class. We also don't want to pollute this class with unnecessary methods.
+     *
+     * @param string $colname The column name
+     * @param \stdClass $data
+     * @return string
+     */
+    public function other_cols($colname, $data) {
+        return s($data->{$colname});
+    }
+
+    /**
+     * Query the database for results to display in the table.
+     *
+     * Note: Initial bars are not implemented for this table because it includes user details twice and the initial bars do not work
+     * when the user table is included more than once.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar Not implemented. Please pass false.
+     */
+    public function query_db($pagesize, $useinitialsbar = false) {
+        global $DB, $USER;
+
+        if ($useinitialsbar) {
+            debugging('Initial bar not implemented yet. Call out($pagesize, false)');
+        }
+
+        $usernamefields = get_all_user_name_fields(true, 'u');
+        $creatorfields = get_all_user_name_fields(true, 'c', null, 'creator');
+
+        $params = ["tokenmode" => EXTERNAL_TOKEN_PERMANENT];
+
+        // TODO: in order to let the administrator delete obsolete token, split the request in multiple request or use LEFT JOIN.
+
+        if ($this->showalltokens) {
+            // Show all tokens.
+            $sql = "SELECT t.id, t.token, u.id AS userid, $usernamefields, s.name, t.iprestriction, t.validuntil, s.id AS serviceid,
+                           t.creatorid, $creatorfields
+                      FROM {external_tokens} t, {user} u, {external_services} s, {user} c
+                     WHERE t.tokentype = :tokenmode AND s.id = t.externalserviceid AND t.userid = u.id AND c.id = t.creatorid";
+            $countsql = "SELECT COUNT(t.id)
+                           FROM {external_tokens} t, {user} u, {external_services} s, {user} c
+                          WHERE t.tokentype = :tokenmode AND s.id = t.externalserviceid AND t.userid = u.id AND c.id = t.creatorid";
+        } else {
+            // Only show tokens created by the current user.
+            $sql = "SELECT t.id, t.token, u.id AS userid, $usernamefields, s.name, t.iprestriction, t.validuntil, s.id AS serviceid,
+                           t.creatorid, $creatorfields
+                      FROM {external_tokens} t, {user} u, {external_services} s, {user} c
+                     WHERE t.creatorid=:userid AND t.tokentype = :tokenmode AND s.id = t.externalserviceid AND t.userid = u.id AND
+                           c.id = t.creatorid";
+            $countsql = "SELECT COUNT(t.id)
+                           FROM {external_tokens} t, {user} u, {external_services} s, {user} c
+                          WHERE t.creatorid=:userid AND t.tokentype = :tokenmode AND s.id = t.externalserviceid AND
+                                t.userid = u.id AND c.id = t.creatorid";
+            $params["userid"] = $USER->id;
+        }
+
+        $sort = $this->get_sql_sort();
+        if ($sort) {
+            $sql = $sql . ' ORDER BY ' . $sort;
+        }
+
+        $total = $DB->count_records_sql($countsql, $params);
+        $this->pagesize($pagesize, $total);
+
+        $this->rawdata = $DB->get_recordset_sql($sql, $params, $this->get_page_start(), $this->get_page_size());
+    }
+}
index 0101cca..2117e8b 100644 (file)
@@ -402,6 +402,29 @@ class webservice {
         return $token;
     }
 
+    /**
+     * Return a token of an arbitrary user by tokenid, including details of the associated user and the service name.
+     * If no tokens exist an exception is thrown
+     *
+     * The returned value is a stdClass:
+     * ->id token id
+     * ->token
+     * ->firstname user firstname
+     * ->lastname
+     * ->name service name
+     *
+     * @param int $tokenid token id
+     * @return stdClass
+     */
+    public function get_token_by_id_with_details($tokenid) {
+        global $DB;
+        $sql = "SELECT t.id, t.token, u.id AS userid, u.firstname, u.lastname, s.name, t.creatorid
+                FROM {external_tokens} t, {user} u, {external_services} s
+                WHERE t.id=? AND t.tokentype = ? AND s.id = t.externalserviceid AND t.userid = u.id";
+        $token = $DB->get_record_sql($sql, array($tokenid, EXTERNAL_TOKEN_PERMANENT), MUST_EXIST);
+        return $token;
+    }
+
     /**
      * Return a database token record for a token id
      *