Merge branch 'MDL-56300_master' of git://github.com/reskit/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 17 Oct 2016 16:50:07 +0000 (18:50 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 17 Oct 2016 16:50:07 +0000 (18:50 +0200)
58 files changed:
.stylelintignore
Gruntfile.js
lib/adminlib.php
lib/classes/scss.php
lib/db/upgrade.php
lib/outputlib.php
lib/templates/skip_links.mustache
message/classes/output/messagearea/message_area.php
message/externallib.php
message/index.php
message/output/popup/tests/behat/behat_message_popup.php [new file with mode: 0644]
message/output/popup/tests/behat/message_popover_preferences.feature [new file with mode: 0644]
message/output/popup/tests/behat/message_popover_unread.feature [new file with mode: 0644]
message/output/popup/tests/behat/notification_popover_preferences.feature [new file with mode: 0644]
message/output/popup/tests/behat/notification_popover_unread.feature [new file with mode: 0644]
mod/assign/amd/build/participant_selector.min.js
mod/assign/amd/src/participant_selector.js
mod/assign/externallib.php
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/commentsearch.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/tests/externallib_test.php
mod/forum/classes/post_form.php
mod/workshop/assessment.php
mod/workshop/classes/portfolio_caller.php [new file with mode: 0644]
mod/workshop/db/access.php
mod/workshop/lang/en/workshop.php
mod/workshop/locallib.php
mod/workshop/submission.php
mod/workshop/tests/behat/behat_mod_workshop.php
mod/workshop/tests/behat/export_submission.feature [new file with mode: 0644]
mod/workshop/tests/locallib_test.php
mod/workshop/tests/portfolio_caller_test.php [new file with mode: 0644]
mod/workshop/version.php
npm-shrinkwrap.json
package.json
search/classes/manager.php
theme/boost/classes/admin_setting_scss_variables.php [deleted file]
theme/boost/config.php
theme/boost/lang/en/theme_boost.php
theme/boost/lib.php
theme/boost/scss/moodle/popover-region.scss
theme/boost/scss/moodle/user.scss
theme/boost/settings.php
theme/boost/templates/core/skip_links.mustache
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/popover_region.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/style/moodle.css
theme/clean/style/custom.css
theme/more/style/custom.css
theme/upgrade.txt
user/externallib.php
user/lib.php
user/tests/userlib_test.php
version.php

index b9e8b2c..a446cc3 100644 (file)
@@ -1,5 +1,7 @@
 # Generated by "grunt ignorefiles"
 theme/bootstrapbase/style/
+theme/clean/style/custom.css
+theme/more/style/custom.css
 node_modules/
 vendor/
 auth/cas/CAS/
index 8fc47df..5852cc4 100644 (file)
@@ -221,7 +221,12 @@ module.exports = function(grunt) {
       var eslintIgnores = ['# Generated by "grunt ignorefiles"', '*/**/yui/src/*/meta/', '*/**/build/'].concat(thirdPartyPaths);
       grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
       // Generate .stylelintignore.
-      var stylelintIgnores = ['# Generated by "grunt ignorefiles"', 'theme/bootstrapbase/style/'].concat(thirdPartyPaths);
+      var stylelintIgnores = [
+          '# Generated by "grunt ignorefiles"',
+          'theme/bootstrapbase/style/',
+          'theme/clean/style/custom.css',
+          'theme/more/style/custom.css'
+      ].concat(thirdPartyPaths);
       grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
     };
 
index d8cbf40..68e1b48 100644 (file)
@@ -9081,6 +9081,7 @@ class admin_setting_configcolourpicker extends admin_setting {
         $context = (object) [
             'id' => $this->get_id(),
             'name' => $this->get_full_name(),
+            'value' => $data,
             'icon' => $icon->export_for_template($OUTPUT),
             'haspreviewconfig' => !empty($this->previewconfig),
             'forceltr' => $this->get_force_ltr()
@@ -9699,19 +9700,13 @@ class admin_setting_searchsetupinfo extends admin_setting {
         $return = '';
         $brtag = html_writer::empty_tag('br');
 
-        // Available search areas.
         $searchareas = \core_search\manager::get_search_areas_list();
-        $anyenabled = false;
+        $anyenabled = !empty(\core_search\manager::get_search_areas_list(true));
         $anyindexed = false;
         foreach ($searchareas as $areaid => $searcharea) {
             list($componentname, $varname) = $searcharea->get_config_var_name();
-            if (!$anyenabled) {
-                $anyenabled = get_config($componentname, $varname . '_enabled');
-            }
-            if (!$anyindexed) {
-                $anyindexed = get_config($componentname, $varname . '_indexingstart');
-            }
-            if ($anyenabled && $anyindexed) {
+            if (get_config($componentname, $varname . '_indexingstart')) {
+                $anyindexed = true;
                 break;
             }
         }
index e60b8e0..5a5e2ab 100644 (file)
@@ -35,6 +35,8 @@ class core_scss extends \Leafo\ScssPhp\Compiler {
 
     /** @var string The path to the SCSS file. */
     protected $scssfile;
+    /** @var array Bits of SCSS content to prepend. */
+    protected $scssprepend = array();
     /** @var array Bits of SCSS content. */
     protected $scsscontent = array();
 
@@ -58,6 +60,16 @@ class core_scss extends \Leafo\ScssPhp\Compiler {
         $this->scsscontent[] = $scss;
     }
 
+    /**
+     * Prepend raw SCSS to what's to compile.
+     *
+     * @param string $scss SCSS code.
+     * @return void
+     */
+    public function prepend_raw_scss($scss) {
+        $this->scssprepend[] = $scss;
+    }
+
     /**
      * Set the file to compile from.
      *
@@ -78,7 +90,7 @@ class core_scss extends \Leafo\ScssPhp\Compiler {
      * @return string
      */
     public function to_css() {
-        $content = '';
+        $content = implode(';', $this->scssprepend);
         if (!empty($this->scssfile)) {
             $content .= file_get_contents($this->scssfile);
         }
index cf1a331..58f51c8 100644 (file)
@@ -2272,5 +2272,18 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2016101101.00);
     }
 
+    if ($oldversion < 2016101401.00) {
+        // Clean up repository_alfresco config unless plugin has been manually installed.
+        if (!file_exists($CFG->dirroot . '/repository/alfresco/lib.php')) {
+            // Remove capabilities.
+            capabilities_cleanup('repository_alfresco');
+            // Clean config.
+            unset_all_config_for_plugin('repository_alfresco');
+        }
+
+        // Savepoint reached.
+        upgrade_main_savepoint(true, 2016101401.00);
+    }
+
     return true;
 }
index cc580b0..726e99f 100644 (file)
@@ -444,10 +444,10 @@ class theme_config {
     public $lessvariablescallback = null;
 
     /**
-     * The name of the function to call to get extra SCSS variables.
+     * The name of the function to call to get SCSS to prepend.
      * @var string
      */
-    public $scssvariablescallback = null;
+    public $prescsscallback = null;
 
     /**
      * Sets the render method that should be used for rendering custom block regions by scripts such as my/index.php
@@ -528,7 +528,7 @@ class theme_config {
             'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow', 'darrow',
             'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations',
             'lessfile', 'extralesscallback', 'lessvariablescallback', 'blockrendermethod',
-            'scssfile', 'extrascsscallback', 'scssvariablescallback', 'csstreepostprocessor');
+            'scssfile', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor');
 
         foreach ($config as $key=>$value) {
             if (in_array($key, $configurable)) {
@@ -1190,9 +1190,9 @@ class theme_config {
 
         // Set-up the compiler.
         $compiler = new core_scss();
+        $compiler->prepend_raw_scss($this->get_pre_scss_code());
         $compiler->set_file($themescssfile);
         $compiler->append_raw_scss($this->get_extra_scss_code());
-        $compiler->add_variables($this->get_scss_variables());
 
         try {
             // Compile!
@@ -1244,64 +1244,63 @@ class theme_config {
     }
 
     /**
-     * Return extra SCSS variables to use when compiling.
+     * Return extra LESS code to add when compiling.
+     *
+     * This is intended to be used by themes to inject some LESS code
+     * before it gets compiled. If you want to inject variables you
+     * should use {@link self::get_less_variables()}.
      *
-     * @return array Where keys are the variable names, and the values are the value.
+     * @return string The LESS code to inject.
      */
-    protected function get_scss_variables() {
-        $variables = array();
+    protected function get_extra_less_code() {
+        $content = '';
 
         // Getting all the candidate functions.
         $candidates = array();
         foreach ($this->parent_configs as $parent_config) {
-            if (!isset($parent_config->scssvariablescallback)) {
+            if (!isset($parent_config->extralesscallback)) {
                 continue;
             }
-            $candidates[] = $parent_config->scssvariablescallback;
+            $candidates[] = $parent_config->extralesscallback;
         }
-        $candidates[] = $this->scssvariablescallback;
+        $candidates[] = $this->extralesscallback;
 
         // Calling the functions.
         foreach ($candidates as $function) {
             if (function_exists($function)) {
-                $vars = $function($this);
-                if (!is_array($vars)) {
-                    debugging('Callback ' . $function . ' did not return an array() as expected', DEBUG_DEVELOPER);
-                    continue;
-                }
-                $variables = array_merge($variables, $vars);
+                $content .= "\n/** Extra LESS from $function **/\n" . $function($this) . "\n";
             }
         }
 
-        return $variables;
+        return $content;
     }
 
     /**
-     * Return extra LESS code to add when compiling.
+     * Return extra SCSS code to add when compiling.
      *
-     * This is intended to be used by themes to inject some LESS code
+     * This is intended to be used by themes to inject some SCSS code
      * before it gets compiled. If you want to inject variables you
-     * should use {@link self::get_less_variables()}.
+     * should use {@link self::get_scss_variables()}.
      *
-     * @return string The LESS code to inject.
+     * @return string The SCSS code to inject.
      */
-    protected function get_extra_less_code() {
+    protected function get_extra_scss_code() {
         $content = '';
 
         // Getting all the candidate functions.
         $candidates = array();
         foreach ($this->parent_configs as $parent_config) {
-            if (!isset($parent_config->extralesscallback)) {
+            if (!isset($parent_config->extrascsscallback)) {
                 continue;
             }
-            $candidates[] = $parent_config->extralesscallback;
+            $candidates[] = $parent_config->extrascsscallback;
         }
-        $candidates[] = $this->extralesscallback;
+        $candidates[] = $this->extrascsscallback;
 
         // Calling the functions.
         foreach ($candidates as $function) {
             if (function_exists($function)) {
-                $content .= "\n/** Extra LESS from $function **/\n" . $function($this) . "\n";
+                $content .= "\n/** Extra SCSS from $function **/\n" . $function($this) . "\n";
             }
         }
 
@@ -1309,31 +1308,29 @@ class theme_config {
     }
 
     /**
-     * Return extra SCSS code to add when compiling.
+     * SCSS code to prepend when compiling.
      *
-     * This is intended to be used by themes to inject some SCSS code
-     * before it gets compiled. If you want to inject variables you
-     * should use {@link self::get_scss_variables()}.
+     * This is intended to be used by themes to inject SCSS code before it gets compiled.
      *
      * @return string The SCSS code to inject.
      */
-    protected function get_extra_scss_code() {
+    protected function get_pre_scss_code() {
         $content = '';
 
         // Getting all the candidate functions.
         $candidates = array();
         foreach ($this->parent_configs as $parent_config) {
-            if (!isset($parent_config->extrascsscallback)) {
+            if (!isset($parent_config->prescsscallback)) {
                 continue;
             }
-            $candidates[] = $parent_config->extrascsscallback;
+            $candidates[] = $parent_config->prescsscallback;
         }
-        $candidates[] = $this->extrascsscallback;
+        $candidates[] = $this->prescsscallback;
 
         // Calling the functions.
         foreach ($candidates as $function) {
             if (function_exists($function)) {
-                $content .= "\n/** Extra SCSS from $function **/\n" . $function($this) . "\n";
+                $content .= "\n/** Pre-SCSS from $function **/\n" . $function($this) . "\n";
             }
         }
 
index b0ad3e8..8a1ef0d 100644 (file)
@@ -1,6 +1,6 @@
 <div class="skiplinks">
 {{#links}}
-    <a href="{{{url}}}" class="skip">{{{text}}}</a>
+    <a href="#{{{url}}}" class="skip">{{{text}}}</a>
 {{/links}}
 </div>
 {{#js}}
index 2e64c4b..a3dd242 100644 (file)
@@ -85,10 +85,8 @@ class message_area implements templatable, renderable {
         $data->userid = $this->userid;
         $contacts = new contacts($this->otheruserid, $this->contacts);
         $data->contacts = $contacts->export_for_template($output);
-        if ($this->messages) {
-            $messages = new messages($this->userid, $this->otheruserid, $this->messages);
-            $data->messages = $messages->export_for_template($output);
-        }
+        $messages = new messages($this->userid, $this->otheruserid, $this->messages);
+        $data->messages = $messages->export_for_template($output);
         $data->isconversation = true;
         $data->requestedconversation = $this->requestedconversation;
 
index a1d2bdf..a22831c 100644 (file)
@@ -2115,7 +2115,9 @@ class core_message_external extends external_api {
         $form = new stdClass();
 
         foreach ($formvalues as $formvalue) {
-            $form->$formvalue['name'] = $formvalue['value'];
+            // Curly braces to ensure interpretation is consistent between
+            // php 5 and php 7.
+            $form->{$formvalue['name']} = $formvalue['value'];
         }
 
         $processor->process_form($form, $preferences);
index 38bc5d7..3e53d4b 100644 (file)
@@ -117,7 +117,7 @@ if (!$user2realuser) {
 
 // Mark the conversation as read.
 if (!empty($user2->id)) {
-    if ($currentuser) {
+    if ($currentuser && isset($conversations[$user2->id])) {
         // Mark the conversation we are loading as read.
         \core_message\api::mark_all_read_for_user($user1->id, $user2->id);
         // Ensure the UI knows it's read as well.
diff --git a/message/output/popup/tests/behat/behat_message_popup.php b/message/output/popup/tests/behat/behat_message_popup.php
new file mode 100644 (file)
index 0000000..58ab3a5
--- /dev/null
@@ -0,0 +1,66 @@
+<?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/>.
+
+/**
+ * Behat message popup related steps definitions.
+ *
+ * @package    message_popup
+ * @category   test
+ * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+
+/**
+ * Message popup steps definitions.
+ *
+ * @package    message_popup
+ * @category   test
+ * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_message_popup extends behat_base {
+
+    /**
+     * Open the notification popover in the nav bar.
+     *
+     * @Given /^I open the notification popover$/
+     */
+    public function i_open_the_notification_popover() {
+        $this->execute('behat_general::i_click_on',
+            array("#nav-notification-popover-container [data-region='popover-region-toggle']", 'css_element'));
+
+        $node = $this->get_selected_node('css_element',
+            '#nav-notification-popover-container [data-region="popover-region-content"]');
+        $this->ensure_node_is_visible($node);
+    }
+
+    /**
+     * Open the message popover in the nav bar.
+     *
+     * @Given /^I open the message popover$/
+     */
+    public function i_open_the_message_popover() {
+        $this->execute('behat_general::i_click_on',
+            array("#nav-message-popover-container [data-region='popover-region-toggle']", 'css_element'));
+
+        $node = $this->get_selected_node('css_element', '#nav-message-popover-container [data-region="popover-region-content"]');
+        $this->ensure_node_is_visible($node);
+    }
+}
diff --git a/message/output/popup/tests/behat/message_popover_preferences.feature b/message/output/popup/tests/behat/message_popover_preferences.feature
new file mode 100644 (file)
index 0000000..dc8d49c
--- /dev/null
@@ -0,0 +1,17 @@
+@message @message_popup
+Feature: Message popover preferences
+  In order to modify my message preferences
+  As a user
+  I can navigate to the preferences page from the popover
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email            |
+      | user1    | User      | 1        | user1@asd.com    |
+
+  @javascript
+  Scenario: User navigates to preferences page
+    Given I log in as "user1"
+    And I open the message popover
+    When I follow "Message preferences"
+    Then I should see "Message preferences"
diff --git a/message/output/popup/tests/behat/message_popover_unread.feature b/message/output/popup/tests/behat/message_popover_unread.feature
new file mode 100644 (file)
index 0000000..fdee795
--- /dev/null
@@ -0,0 +1,50 @@
+@message @message_popup @javascript
+Feature: Message popover unread messages
+  In order to be kept informed
+  As a user
+  I am notified about unread messages from other users
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+    And I log in as "student2"
+    And I send "Test message" message to "Student 1" user
+    And I log out
+
+  Scenario: Notification popover shows correct unread count
+    When I log in as "student2"
+    And I send "Test message 2" message to "Student 1" user
+    And I log out
+    And I log in as "student1"
+    # Confirm the popover is saying 1 unread conversation.
+    Then I should see "1" in the "#nav-message-popover-container [data-region='count-container']" "css_element"
+    # Open the popover.
+    And I open the message popover
+    # Confirm the conversation is visible.
+    And I should see "Test message 2" in the "#nav-message-popover-container" "css_element"
+    # Confirm the count of unread messages in the conversation is correct.
+    And I should see "2" in the "#nav-message-popover-container [data-region='unread-count']" "css_element"
+
+  Scenario: Clicking a message marks it as read
+    When I log in as "student1"
+    # Open the popover.
+    And I open the message popover
+    # Click on the conversation.
+    And I click on "[aria-label='View unread messages with Student 2']" "css_element" in the "#nav-message-popover-container" "css_element"
+    # Confirm the count element is hidden (i.e. there are no unread messages).
+    Then "[data-region='count-container']" "css_element" in the "#nav-message-popover-container" "css_element" should not be visible
+    # Confirm the message was loaded in the messaging page.
+    And I should see "Test message" in the "[data-region='message-text']" "css_element"
+
+  Scenario: Mark all notifications as read
+    When I log in as "student1"
+    # Open the popover.
+    And I open the message popover
+    # Click the mark all as read button.
+    And I click on "[data-action='mark-all-read']" "css_element" in the "#nav-message-popover-container" "css_element"
+    # Refresh the page to make sure we send a new request for the unread count.
+    And I reload the page
+    # Confirm the count element is hidden (i.e. there are no unread messages).
+    Then "[data-region='count-container']" "css_element" in the "#nav-message-popover-container" "css_element" should not be visible
diff --git a/message/output/popup/tests/behat/notification_popover_preferences.feature b/message/output/popup/tests/behat/notification_popover_preferences.feature
new file mode 100644 (file)
index 0000000..d4743da
--- /dev/null
@@ -0,0 +1,17 @@
+@message @message_popup
+Feature: Notification popover preferences
+  In order to modify my notification preferences
+  As a user
+  I can navigate to the preferences page from the popover
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email            |
+      | user1    | User      | 1        | user1@example.com    |
+
+  @javascript
+  Scenario: User navigates to preferences page
+    Given I log in as "user1"
+    And I open the notification popover
+    When I follow "Notification preferences"
+    Then I should see "Notification preferences"
diff --git a/message/output/popup/tests/behat/notification_popover_unread.feature b/message/output/popup/tests/behat/notification_popover_unread.feature
new file mode 100644 (file)
index 0000000..a3df3a1
--- /dev/null
@@ -0,0 +1,73 @@
+@message @message_popup @javascript
+Feature: Notification popover unread notifications
+  In order to be kept informed
+  As a user
+  I am notified about relevant events in Moodle
+
+  Background:
+    # This will make sure popup notifications are enabled and create
+    # two assignment notifications. One for the student submitting their
+    # assignment and another for the teacher grading it.
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    # Make sure the popup notifications are enabled for assignments.
+    And the following config values are set as admin:
+      | popup_provider_mod_assign_assign_notification_permitted | permitted | message |
+      | message_provider_mod_assign_assign_notification_loggedin | popup | message |
+      | message_provider_mod_assign_assign_notification_loggedoff | popup | message |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment name |
+      | Description | Submit your online text |
+      | assignsubmission_onlinetext_enabled | 1 |
+      | assignsubmission_file_enabled | 0 |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    And I press "Add submission"
+    # This should generate a notification.
+    And I set the following fields to these values:
+      | Online text | I'm the student first submission |
+    And I press "Save changes"
+    And I log out
+
+  Scenario: Notification popover shows correct unread count
+    When I log in as "student1"
+    # Confirm the popover is saying 1 unread notifications.
+    Then I should see "1" in the "#nav-notification-popover-container [data-region='count-container']" "css_element"
+    # Open the popover.
+    And I open the notification popover
+    # Confirm the submission notification is visible.
+    And I should see "You have submitted your assignment submission for Test assignment name" in the "#nav-notification-popover-container" "css_element"
+
+  Scenario: Clicking a notification marks it as read
+    When I log in as "student1"
+    # Open the popover.
+    And I open the notification popover
+    # Click on the submission notification.
+    And I click on "[aria-label='Unread notification: You have submitted your assignment submission for Test assignment name']" "css_element" in the "#nav-notification-popover-container" "css_element"
+    # Confirm the count element is hidden (i.e. there are no unread notifications).
+    Then "[data-region='count-container']" "css_element" in the "#nav-notification-popover-container" "css_element" should not be visible
+
+  Scenario: Mark all notifications as read
+    When I log in as "student1"
+    # Open the popover.
+    And I open the notification popover
+    # Click the mark all as read button.
+    And I click on "[data-action='mark-all-read']" "css_element" in the "#nav-notification-popover-container" "css_element"
+    # Refresh the page to make sure we send a new request for the unread count.
+    And I reload the page
+    # Confirm the count element is hidden (i.e. there are no unread notifications).
+    Then "[data-region='count-container']" "css_element" in the "#nav-notification-popover-container" "css_element" should not be visible
index f7f393b..23a7ce5 100644 (file)
Binary files a/mod/assign/amd/build/participant_selector.min.js and b/mod/assign/amd/build/participant_selector.min.js differ
index 71312a6..f92f4fb 100644 (file)
@@ -62,7 +62,8 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) {
             });
 
             var promise = ajax.call([{
-                methodname: 'mod_assign_list_participants', args: {assignid: assignmentid, groupid: 0, filter: query, limit: 30}
+                methodname: 'mod_assign_list_participants',
+                args: {assignid: assignmentid, groupid: 0, filter: query, limit: 30, includeenrolments: false}
             }]);
 
             promise[0].then(function(results) {
index f87d91d..bb95983 100644 (file)
@@ -2510,6 +2510,8 @@ class mod_assign_external extends external_api {
                 'skip' => new external_value(PARAM_INT, 'number of records to skip', VALUE_DEFAULT, 0),
                 'limit' => new external_value(PARAM_INT, 'maximum number of records to return', VALUE_DEFAULT, 0),
                 'onlyids' => new external_value(PARAM_BOOL, 'Do not return all user fields', VALUE_DEFAULT, false),
+                'includeenrolments' => new external_value(PARAM_BOOL, 'Do return courses where the user is enrolled',
+                                                          VALUE_DEFAULT, true)
             )
         );
     }
@@ -2523,11 +2525,12 @@ class mod_assign_external extends external_api {
      * @param int $skip Number of records to skip
      * @param int $limit Maximum number of records to return
      * @param bool $onlyids Only return user ids.
+     * @param bool $includeenrolments Return courses where the user is enrolled.
      * @return array of warnings and status result
      * @since Moodle 3.1
      * @throws moodle_exception
      */
-    public static function list_participants($assignid, $groupid, $filter, $skip, $limit, $onlyids) {
+    public static function list_participants($assignid, $groupid, $filter, $skip, $limit, $onlyids, $includeenrolments) {
         global $DB, $CFG;
         require_once($CFG->dirroot . "/mod/assign/locallib.php");
         require_once($CFG->dirroot . "/user/lib.php");
@@ -2539,7 +2542,8 @@ class mod_assign_external extends external_api {
                                                 'filter' => $filter,
                                                 'skip' => $skip,
                                                 'limit' => $limit,
-                                                'onlyids' => $onlyids
+                                                'onlyids' => $onlyids,
+                                                'includeenrolments' => $includeenrolments
                                             ));
         $warnings = array();
 
@@ -2551,6 +2555,17 @@ class mod_assign_external extends external_api {
 
         $participants = $assign->list_participants_with_filter_status_and_group($params['groupid']);
 
+        $userfields = user_get_default_fields();
+        if (!$params['includeenrolments']) {
+            // Remove enrolled courses from users fields to be returned.
+            $key = array_search('enrolledcourses', $userfields);
+            if ($key !== false) {
+                unset($userfields[$key]);
+            } else {
+                throw new moodle_exception('invaliduserfield', 'error', '', 'enrolledcourses');
+            }
+        }
+
         $result = array();
         $index = 0;
         foreach ($participants as $record) {
@@ -2577,7 +2592,7 @@ class mod_assign_external extends external_api {
                 }
                 // Now we do the expensive lookup of user details because we completed the filtering.
                 if (!$assign->is_blind_marking() && !$params['onlyids']) {
-                    $userdetails = user_get_user_details($record, $course);
+                    $userdetails = user_get_user_details($record, $course, $userfields);
                 } else {
                     $userdetails = array('id' => $record->id);
                 }
@@ -2620,7 +2635,6 @@ class mod_assign_external extends external_api {
         $userdesc->keys['profileimageurlsmall']->required = VALUE_OPTIONAL;
         $userdesc->keys['profileimageurl']->required = VALUE_OPTIONAL;
         $userdesc->keys['email']->desc = 'Email address';
-        $userdesc->keys['email']->desc = 'Email address';
         $userdesc->keys['idnumber']->desc = 'The idnumber of the user';
 
         // Define other keys.
index 1c32501..3392041 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index c0373e1..965e762 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index 1c32501..3392041 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index 31d3234..fc12892 100644 (file)
@@ -102,18 +102,18 @@ Y.extend(COMMENTSEARCH, M.core.dialogue, {
         e.preventDefault();
         var target = e.target.ancestor('li'),
             comment = target.getData('comment'),
-            editor = this.get('editor');
+            editor = this.get('editor'),
+            pageselect = editor.get_dialogue_element(SELECTOR.PAGESELECT);
 
         this.hide();
 
-        if (comment.pageno === editor.currentpage) {
-            comment.drawable.nodes[0].one('textarea').focus();
-        } else {
+        editor.currentpage = parseInt(pageselect.get('value'), 10);
+        if (comment.pageno !== editor.currentpage) {
             // Comment is on a different page.
             editor.currentpage = comment.pageno;
             editor.change_page();
-            comment.drawable.nodes[0].one('textarea').focus();
         }
+        comment.drawable.nodes[0].one('textarea').focus();
     },
 
     /**
index 2bdc486..032370a 100644 (file)
@@ -987,8 +987,11 @@ EDITOR.prototype = {
      */
     save_current_page: function() {
         var ajaxurl = AJAXBASE,
+            pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT),
             config;
 
+        this.currentpage = parseInt(pageselect.get('value'), 10);
+
         config = {
             method: 'post',
             context: this,
@@ -1162,7 +1165,9 @@ EDITOR.prototype = {
      */
     previous_page: function(e) {
         e.preventDefault();
-        this.currentpage--;
+        var pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT);
+
+        this.currentpage = parseInt(pageselect.get('value'), 10) - 1;
         if (this.currentpage < 0) {
             this.currentpage = 0;
         }
@@ -1176,7 +1181,9 @@ EDITOR.prototype = {
      */
     next_page: function(e) {
         e.preventDefault();
-        this.currentpage++;
+        var pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT);
+
+        this.currentpage = parseInt(pageselect.get('value'), 10) + 1;
         if (this.currentpage >= this.pages.length) {
             this.currentpage = this.pages.length - 1;
         }
index d4c17e0..b97b84b 100644 (file)
@@ -2386,7 +2386,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         $DB->update_record('user', $student);
 
         $this->setUser($teacher);
-        $participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false);
+        $participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, true);
         $this->assertCount(1, $participants);
 
         // Asser that we have a valid response data.
@@ -2401,6 +2401,12 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals($student->phone2, $participant['phone2']);
         $this->assertEquals($student->department, $participant['department']);
         $this->assertEquals($student->institution, $participant['institution']);
+        $this->assertArrayHasKey('enrolledcourses', $participant);
+
+        $participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, false);
+        // Check that the list of courses the participant is enrolled is not returned.
+        $participant = $participants[0];
+        $this->assertArrayNotHasKey('enrolledcourses', $participant);
     }
 
     /**
index 0fbe196..520a2ec 100644 (file)
@@ -147,23 +147,6 @@ class mod_forum_post_form extends moodleform {
             $mform->addElement('checkbox', 'mailnow', get_string('mailnow', 'forum'));
         }
 
-        if (!empty($CFG->forum_enabletimedposts) && !$post->parent && has_capability('mod/forum:viewhiddentimedposts', $coursecontext)) { // hack alert
-            $mform->addElement('header', 'displayperiod', get_string('displayperiod', 'forum'));
-
-            $mform->addElement('date_time_selector', 'timestart', get_string('displaystart', 'forum'), array('optional' => true));
-            $mform->addHelpButton('timestart', 'displaystart', 'forum');
-
-            $mform->addElement('date_time_selector', 'timeend', get_string('displayend', 'forum'), array('optional' => true));
-            $mform->addHelpButton('timeend', 'displayend', 'forum');
-
-        } else {
-            $mform->addElement('hidden', 'timestart');
-            $mform->setType('timestart', PARAM_INT);
-            $mform->addElement('hidden', 'timeend');
-            $mform->setType('timeend', PARAM_INT);
-            $mform->setConstants(array('timestart'=> 0, 'timeend'=>0));
-        }
-
         if ($groupmode = groups_get_activity_groupmode($cm, $course)) {
             $groupdata = groups_get_activity_allowed_groups($cm);
 
@@ -237,6 +220,24 @@ class mod_forum_post_form extends moodleform {
                 $mform->addElement('static', 'groupinfo', get_string('group'), $groupname);
             }
         }
+
+        if (!empty($CFG->forum_enabletimedposts) && !$post->parent && has_capability('mod/forum:viewhiddentimedposts', $coursecontext)) {
+            $mform->addElement('header', 'displayperiod', get_string('displayperiod', 'forum'));
+
+            $mform->addElement('date_time_selector', 'timestart', get_string('displaystart', 'forum'), array('optional' => true));
+            $mform->addHelpButton('timestart', 'displaystart', 'forum');
+
+            $mform->addElement('date_time_selector', 'timeend', get_string('displayend', 'forum'), array('optional' => true));
+            $mform->addHelpButton('timeend', 'displayend', 'forum');
+
+        } else {
+            $mform->addElement('hidden', 'timestart');
+            $mform->setType('timestart', PARAM_INT);
+            $mform->addElement('hidden', 'timeend');
+            $mform->setType('timeend', PARAM_INT);
+            $mform->setConstants(array('timestart' => 0, 'timeend' => 0));
+        }
+
         //-------------------------------------------------------------------------------
         // buttons
         if (isset($post->edit)) { // hack alert
index e4af07a..36dbcb9 100644 (file)
@@ -62,20 +62,7 @@ $canoverridegrades      = has_capability('mod/workshop:overridegrades', $worksho
 $isreviewer             = ($USER->id == $assessment->reviewerid);
 $isauthor               = ($USER->id == $submission->authorid);
 
-if ($canviewallsubmissions) {
-    // check this flag against the group membership yet
-    if (groups_get_activity_groupmode($workshop->cm) == SEPARATEGROUPS) {
-        // user must have accessallgroups or share at least one group with the submission author
-        if (!has_capability('moodle/site:accessallgroups', $workshop->context)) {
-            $usersgroups = groups_get_activity_allowed_groups($workshop->cm);
-            $authorsgroups = groups_get_all_groups($workshop->course->id, $submission->authorid, $workshop->cm->groupingid, 'g.id');
-            $sharedgroups = array_intersect_key($usersgroups, $authorsgroups);
-            if (empty($sharedgroups)) {
-                $canviewallsubmissions = false;
-            }
-        }
-    }
-}
+$canviewallsubmissions = $canviewallsubmissions && $workshop->check_group_membership($submission->authorid);
 
 if ($isreviewer or $isauthor or ($canviewallassessments and $canviewallsubmissions)) {
     // such a user can continue
diff --git a/mod/workshop/classes/portfolio_caller.php b/mod/workshop/classes/portfolio_caller.php
new file mode 100644 (file)
index 0000000..a361b24
--- /dev/null
@@ -0,0 +1,533 @@
+<?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/>.
+
+/**
+ * Provides the {@link mod_workshop_portfolio_caller} class.
+ *
+ * @package   mod_workshop
+ * @category  portfolio
+ * @copyright Loc Nguyen <ndloc1905@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/portfolio/caller.php');
+
+/**
+ * Workshop portfolio caller class to integrate with portfolio API.
+ *
+ * @package   mod_workshop
+ * @copyright Loc Nguyen <ndloc1905@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_workshop_portfolio_caller extends portfolio_module_caller_base {
+
+    /** @var workshop The workshop instance where the export is happening. */
+    protected $workshop;
+
+    /** @var int ID if the exported submission, set via the constructor. */
+    protected $submissionid;
+
+    /** @var object The submission being exported. */
+    protected $submission;
+
+    /** @var array of objects List of assessments of the exported submission. */
+    protected $assessments = [];
+
+    /**
+     * Explicit constructor to set the properties declared by the parent class.
+     *
+     * Firstly we call the parent's constructor to set the $this->id property
+     * from the passed argument. Then we populate the $this->cm so that the
+     * default parent class methods work well.
+     *
+     * @param array $callbackargs
+     */
+    public function __construct($callbackargs) {
+
+        // Let the parent class set the $this->id property.
+        parent::__construct($callbackargs);
+        // Populate the $this->cm property.
+        $this->cm = get_coursemodule_from_id('workshop', $this->id, 0, false, MUST_EXIST);
+    }
+
+    /**
+     * Return array of expected callback arguments and whether they are required or not.
+     *
+     * The 'id' argument is supposed to be our course module id (cmid) - see
+     * the parent class' properties.
+     *
+     * @return array of (string)callbackname => (bool)required
+     */
+    public static function expected_callbackargs() {
+        return [
+            'id' => true,
+            'submissionid' => true,
+        ];
+    }
+
+    /**
+     * Load data required for the export.
+     */
+    public function load_data() {
+        global $DB, $USER;
+
+        // Note that require_login() is normally called later as a part of
+        // portfolio_export_pagesetup() in the portfolio/add.php file. But we
+        // load various data depending of capabilities so it makes sense to
+        // call it explicitly here, too.
+        require_login($this->get('course'), false, $this->cm, false, true);
+
+        if (isguestuser()) {
+            throw new portfolio_caller_exception('guestsarenotallowed', 'core_error');
+        }
+
+        $workshoprecord = $DB->get_record('workshop', ['id' => $this->cm->instance], '*', MUST_EXIST);
+        $this->workshop = new workshop($workshoprecord, $this->cm, $this->get('course'));
+
+        $this->submission = $this->workshop->get_submission_by_id($this->submissionid);
+
+        // Is the user exporting her/his own submission?
+        $ownsubmission = $this->submission->authorid == $USER->id;
+
+        // Does the user have permission to see all submissions (aka is it a teacher)?
+        $canviewallsubmissions = has_capability('mod/workshop:viewallsubmissions', $this->workshop->context);
+        $canviewallsubmissions = $canviewallsubmissions && $this->workshop->check_group_membership($this->submission->authorid);
+
+        // Is the user exporting a submission that she/he has peer-assessed?
+        $userassessment = $this->workshop->get_assessment_of_submission_by_user($this->submission->id, $USER->id);
+        if ($userassessment) {
+            $this->assessments[$userassessment->id] = $userassessment;
+            $isreviewer = true;
+        }
+
+        if (!$ownsubmission and !$canviewallsubmissions and !$isreviewer) {
+            throw new portfolio_caller_exception('nopermissions', 'core_error');
+        }
+
+        // Does the user have permission to see all assessments (aka is it a teacher)?
+        $canviewallassessments = has_capability('mod/workshop:viewallassessments', $this->workshop->context);
+
+        // Load other assessments eventually if the user can see them.
+        if ($canviewallassessments or ($ownsubmission and $this->workshop->assessments_available())) {
+            foreach ($this->workshop->get_assessments_of_submission($this->submission->id) as $assessment) {
+                if ($assessment->reviewerid == $USER->id) {
+                    // User's own assessment is already loaded.
+                    continue;
+                }
+                if (is_null($assessment->grade) and !$canviewallassessments) {
+                    // Students do not see peer-assessment that are not graded.
+                    continue;
+                }
+                $this->assessments[$assessment->id] = $assessment;
+            }
+        }
+
+        // Prepare embedded and attached files for the export.
+        $this->multifiles = [];
+
+        $this->add_area_files('submission_content', $this->submission->id);
+        $this->add_area_files('submission_attachment', $this->submission->id);
+
+        foreach ($this->assessments as $assessment) {
+            $this->add_area_files('overallfeedback_content', $assessment->id);
+            $this->add_area_files('overallfeedback_attachment', $assessment->id);
+        }
+
+        $this->add_area_files('instructauthors', 0);
+
+        // If there are no files to be exported, we can offer plain HTML file export.
+        if (empty($this->multifiles)) {
+            $this->add_format(PORTFOLIO_FORMAT_PLAINHTML);
+        }
+    }
+
+    /**
+     * Prepare the package ready to be passed off to the portfolio plugin.
+     */
+    public function prepare_package() {
+
+        $canviewauthornames = has_capability('mod/workshop:viewauthornames', $this->workshop->context, $this->get('user'));
+
+        // Prepare the submission record for rendering.
+        $workshopsubmission = $this->workshop->prepare_submission($this->submission, $canviewauthornames);
+
+        // Set up the LEAP2A writer if we need it.
+        $writingleap = false;
+
+        if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
+            $leapwriter = $this->exporter->get('format')->leap2a_writer();
+            $writingleap = true;
+        }
+
+        // If writing to HTML file, accumulate the exported hypertext here.
+        $html = '';
+
+        // If writing LEAP2A, keep track of all entry ids so we can add a selection element.
+        $leapids = [];
+
+        $html .= $this->export_header($workshopsubmission);
+        $content = $this->export_content($workshopsubmission);
+        // Get rid of the JS relics left by moodleforms.
+        $content = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $content);
+        $html .= $content;
+
+        if ($writingleap) {
+            $leapids[] = $this->export_content_leap2a($leapwriter, $workshopsubmission, $content);
+        }
+
+        // Export the files.
+        foreach ($this->multifiles as $file) {
+            $this->exporter->copy_existing_file($file);
+        }
+
+        if ($writingleap) {
+            // Add an extra LEAP2A selection entry. In Mahara, this maps to a journal.
+            $selection = new portfolio_format_leap2a_entry('workshop'.$this->workshop->id,
+                get_string('pluginname', 'mod_workshop').': '.s($this->workshop->name), 'selection');
+            $leapwriter->add_entry($selection);
+            $leapwriter->make_selection($selection, $leapids, 'Grouping');
+            $leapxml = $leapwriter->to_xml();
+            $name = $this->exporter->get('format')->manifest_name();
+            $this->exporter->write_new_file($leapxml, $name, true);
+
+        } else {
+            $this->exporter->write_new_file($html, 'submission.html', true);
+        }
+    }
+
+    /**
+     * Helper method to add all files from the given location to $this->multifiles
+     *
+     * @param string $filearea
+     * @param int $itemid
+     */
+    protected function add_area_files($filearea, $itemid) {
+
+        $fs = get_file_storage();
+        $areafiles = $fs->get_area_files($this->workshop->context->id, 'mod_workshop', $filearea, $itemid, null, false);
+        if ($areafiles) {
+            $this->multifiles = array_merge($this->multifiles, array_values($areafiles));
+        }
+    }
+
+    /**
+     * Render the header of the exported content.
+     *
+     * This is mainly used for the HTML output format. In case of LEAP2A
+     * export, this is not used as the information is stored in metadata and
+     * displayed as a part of the journal and entry title in Mahara.
+     *
+     * @param workshop_submission $workshopsubmission
+     * @return string HTML
+     */
+    protected function export_header(workshop_submission $workshopsubmission) {
+
+        $output = '';
+        $output .= html_writer::tag('h2', get_string('pluginname', 'mod_workshop').': '.s($this->workshop->name));
+        $output .= html_writer::tag('h3', s($workshopsubmission->title));
+
+        $created = get_string('userdatecreated', 'workshop', userdate($workshopsubmission->timecreated));
+        $created = html_writer::tag('span', $created);
+
+        if ($workshopsubmission->timemodified > $workshopsubmission->timecreated) {
+            $modified = get_string('userdatemodified', 'workshop', userdate($workshopsubmission->timemodified));
+            $modified = ' | ' . html_writer::tag('span', $modified);
+        } else {
+            $modified = '';
+        }
+
+        $output .= html_writer::div($created.$modified);
+        $output .= html_writer::empty_tag('br');
+
+        return $output;
+    }
+
+    /**
+     * Render the content of the submission.
+     *
+     * @param workshop_submission $workshopsubmission
+     * @return string
+     */
+    protected function export_content(workshop_submission $workshopsubmission) {
+
+        $output = '';
+
+        if (!$workshopsubmission->is_anonymous()) {
+            $author = username_load_fields_from_object((object)[], $workshopsubmission, 'author');
+            $output .= html_writer::div(get_string('byfullnamewithoutlink', 'mod_workshop', fullname($author)));
+        }
+
+        $content = $this->format_exported_text($workshopsubmission->content, $workshopsubmission->contentformat);
+        $content = portfolio_rewrite_pluginfile_urls($content, $this->workshop->context->id, 'mod_workshop',
+            'submission_content', $workshopsubmission->id, $this->exporter->get('format'));
+        $output .= html_writer::div($content);
+
+        $output .= $this->export_files_list('submission_attachment');
+
+        $strategy = $this->workshop->grading_strategy_instance();
+
+        $canviewauthornames = has_capability('mod/workshop:viewauthornames', $this->workshop->context, $this->get('user'));
+        $canviewreviewernames = has_capability('mod/workshop:viewreviewernames', $this->workshop->context, $this->get('user'));
+
+        foreach ($this->assessments as $assessment) {
+            $mform = $strategy->get_assessment_form(null, 'assessment', $assessment, false);
+            $options = [
+                'showreviewer' => $canviewreviewernames,
+                'showauthor' => $canviewauthornames,
+                'showform' => true,
+                'showweight' => true,
+            ];
+            if ($assessment->reviewerid == $this->get('user')->id) {
+                $options['showreviewer'] = true;
+            }
+
+            $workshopassessment = $this->workshop->prepare_assessment($assessment, $mform, $options);
+
+            if ($assessment->reviewerid == $this->get('user')->id) {
+                $workshopassessment->title = get_string('assessmentbyyourself', 'mod_workshop');
+            } else {
+                $workshopassessment->title = get_string('assessment', 'mod_workshop');
+            }
+
+            $output .= html_writer::empty_tag('hr');
+            $output .= $this->export_assessment($workshopassessment);
+        }
+
+        if (trim($this->workshop->instructauthors)) {
+            $output .= html_writer::tag('h3', get_string('instructauthors', 'mod_workshop'));
+            $content = $this->format_exported_text($this->workshop->instructauthors, $this->workshop->instructauthorsformat);
+            $content = portfolio_rewrite_pluginfile_urls($content, $this->workshop->context->id, 'mod_workshop',
+                'instructauthors', 0, $this->exporter->get('format'));
+            $output .= $content;
+        }
+
+        return $output;
+    }
+
+    /**
+     * Render the content of an assessment.
+     *
+     * @param workshop_assessment $assessment
+     * @return string HTML
+     */
+    protected function export_assessment(workshop_assessment $assessment) {
+
+        $output = '';
+
+        if (empty($assessment->title)) {
+            $title = get_string('assessment', 'workshop');
+        } else {
+            $title = s($assessment->title);
+        }
+
+        $output .= html_writer::tag('h3', $title);
+
+        if ($assessment->reviewer) {
+            $output .= html_writer::div(get_string('byfullnamewithoutlink', 'mod_workshop', fullname($assessment->reviewer)));
+            $output .= html_writer::empty_tag('br');
+        }
+
+        if ($this->workshop->overallfeedbackmode) {
+            if ($assessment->feedbackauthorattachment or trim($assessment->feedbackauthor) !== '') {
+                $output .= html_writer::tag('h3', get_string('overallfeedback', 'mod_workshop'));
+                $content = $this->format_exported_text($assessment->feedbackauthor, $assessment->feedbackauthorformat);
+                $content = portfolio_rewrite_pluginfile_urls($content, $this->workshop->context->id, 'mod_workshop',
+                    'overallfeedback_content', $assessment->id , $this->exporter->get('format'));
+                $output .= $content;
+
+                $output .= $this->export_files_list('overallfeedback_attachment');
+            }
+        }
+
+        if ($assessment->form) {
+            $output .= $assessment->form->render();
+        }
+
+        return $output;
+    }
+
+    /**
+     * Export the files in the given file area in a list.
+     *
+     * @param string $filearea
+     * @return string HTML
+     */
+    protected function export_files_list($filearea) {
+
+        $output = '';
+        $files = [];
+
+        foreach ($this->multifiles as $file) {
+            if ($file->is_directory()) {
+                continue;
+            }
+            if ($file->get_filearea() !== $filearea) {
+                continue;
+            }
+            if ($file->is_valid_image()) {
+                // Not optimal but looks better than original images.
+                $files[] = html_writer::tag('li', $this->exporter->get('format')->file_output($file,
+                    ['attributes' => ['style' => 'max-height:24px; max-width:24px']]).' '.s($file->get_filename()));
+            } else {
+                $files[] = html_writer::tag('li', $this->exporter->get('format')->file_output($file));
+            }
+        }
+
+        if ($files) {
+            $output .= html_writer::tag('ul', implode('', $files));
+        }
+
+        return $output;
+    }
+
+    /**
+     * Helper function to call {@link format_text()} on exported text.
+     *
+     * We need to call {@link format_text()} to convert the text into HTML, but
+     * we have to keep the original @@PLUGINFILE@@ placeholder there without a
+     * warning so that {@link portfolio_rewrite_pluginfile_urls()} can do its work.
+     *
+     * @param string $text
+     * @param int $format
+     * @return string HTML
+     */
+    protected function format_exported_text($text, $format) {
+
+        $text = str_replace('@@PLUGINFILE@@', '@@ORIGINALPLUGINFILE@@', $text);
+        $html = format_text($text, $format, portfolio_format_text_options());
+        $html = str_replace('@@ORIGINALPLUGINFILE@@', '@@PLUGINFILE@@', $html);
+
+        return $html;
+    }
+
+    /**
+     * Add a LEAP2A entry element that corresponds to a submission including attachments.
+     *
+     * @param portfolio_format_leap2a_writer $leapwriter Writer object to add entries to.
+     * @param workshop_submission $workshopsubmission
+     * @param string $html The exported HTML content of the submission
+     * @return int id of new entry
+     */
+    protected function export_content_leap2a(portfolio_format_leap2a_writer $leapwriter,
+            workshop_submission $workshopsubmission, $html) {
+
+        $entry = new portfolio_format_leap2a_entry('workshopsubmission'.$workshopsubmission->id,  s($workshopsubmission->title),
+            'resource', $html);
+        $entry->published = $workshopsubmission->timecreated;
+        $entry->updated = $workshopsubmission->timemodified;
+        $entry->author = (object)[
+            'id' => $workshopsubmission->authorid,
+            'email' => $workshopsubmission->authoremail
+        ];
+        username_load_fields_from_object($entry->author, $workshopsubmission);
+
+        $leapwriter->link_files($entry, $this->multifiles);
+        $entry->add_category('web', 'resource_type');
+        $leapwriter->add_entry($entry);
+
+        return $entry->id;
+    }
+
+    /**
+     * Return URL for redirecting the user back to where the export started.
+     *
+     * @return string
+     */
+    public function get_return_url() {
+
+        $returnurl = new moodle_url('/mod/workshop/submission.php', ['cmid' => $this->cm->id, 'id' => $this->submissionid]);
+        return $returnurl->out();
+    }
+
+    /**
+     * Get navigation that logically follows from the place the user was before.
+     *
+     * @return array
+     */
+    public function get_navigation() {
+
+        $navlinks = [
+            ['name' => s($this->submission->title)],
+        ];
+
+        return [$navlinks, $this->cm];
+    }
+
+    /**
+     * How long might we expect this export to take.
+     *
+     * @return string such as PORTFOLIO_TIME_LOW
+     */
+    public function expected_time() {
+        return $this->expected_time_file();
+    }
+
+    /**
+     * Make sure that the current user is allowed to do the export.
+     *
+     * @return boolean
+     */
+    public function check_permissions() {
+        return has_capability('mod/workshop:exportsubmissions', context_module::instance($this->cm->id));
+    }
+
+    /**
+     * Return the SHA1 hash of the exported content.
+     *
+     * @return string
+     */
+    public function get_sha1() {
+
+        $identifier = 'submission:'.$this->submission->id.'@'.$this->submission->timemodified;
+
+        if ($this->assessments) {
+            $ids = array_keys($this->assessments);
+            sort($ids);
+            $identifier .= '/assessments:'.implode(',', $ids);
+        }
+
+        if ($this->multifiles) {
+            $identifier .= '/files:'.$this->get_sha1_file();
+        }
+
+        return sha1($identifier);
+    }
+
+    /**
+     * Return a nice name to be displayed about this export location.
+     *
+     * @return string
+     */
+    public static function display_name() {
+        return get_string('pluginname', 'mod_workshop');
+    }
+
+    /**
+     * What export formats the workshop generally supports.
+     *
+     * If there are no files embedded/attached, the plain HTML format is added
+     * in {@link self::load_data()}.
+     *
+     * @return array
+     */
+    public static function base_supported_formats() {
+        return [
+            PORTFOLIO_FORMAT_RICHHTML,
+            PORTFOLIO_FORMAT_LEAP2A,
+        ];
+    }
+}
index dc7cec4..9e51ff8 100644 (file)
@@ -244,4 +244,17 @@ $capabilities = array(
             'manager' => CAP_ALLOW
         )
     ),
+
+    // Ability to export submissions to a portfolio. Applies to all submissions
+    // the user has access to.
+    'mod/workshop:exportsubmissions' => array(
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'teacher' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+            'student' => CAP_ALLOW,
+        )
+    ),
 );
index 996a83e..17b4dee 100644 (file)
@@ -73,6 +73,7 @@ $string['assignedassessments'] = 'Assigned submissions to assess';
 $string['assignedassessmentsnone'] = 'You have no assigned submission to assess';
 $string['backtoeditform'] = 'Back to editing form';
 $string['byfullname'] = 'by <a href="{$a->url}">{$a->name}</a>';
+$string['byfullnamewithoutlink'] = 'by {$a}';
 $string['calculategradinggrades'] = 'Calculate assessment grades';
 $string['calculategradinggradesdetails'] = 'expected: {$a->expected}<br />calculated: {$a->calculated}';
 $string['calculatesubmissiongrades'] = 'Calculate submission grades';
@@ -143,6 +144,7 @@ $string['examplesbeforesubmission'] = 'Examples must be assessed before own subm
 $string['examplesmode'] = 'Mode of examples assessment';
 $string['examplesubmissions'] = 'Example submissions';
 $string['examplesvoluntary'] = 'Assessment of example submission is voluntary';
+$string['exportsubmission'] = 'Export this page';
 $string['feedbackauthor'] = 'Feedback for the author';
 $string['feedbackauthorattachment'] = 'Attachment';
 $string['feedbackby'] = 'Feedback by {$a}';
@@ -331,6 +333,7 @@ $string['withoutsubmission'] = 'Reviewer without own submission';
 $string['workshop:addinstance'] = 'Add a new workshop';
 $string['workshop:allocate'] = 'Allocate submissions for review';
 $string['workshop:editdimensions'] = 'Edit assessment forms';
+$string['workshop:exportsubmissions'] = 'Export submissions';
 $string['workshop:deletesubmissions'] = 'Delete submissions';
 $string['workshop:ignoredeadlines'] = 'Ignore time restrictions';
 $string['workshop:manageexamples'] = 'Manage example submissions';
index d074add..509d397 100644 (file)
@@ -2626,6 +2626,50 @@ class workshop {
         return $status;
     }
 
+    /**
+     * Check if the current user can access the other user's group.
+     *
+     * This is typically used for teacher roles that have permissions like
+     * 'view all submissions'. Even with such a permission granted, we have to
+     * check the workshop activity group mode.
+     *
+     * If the workshop is not in a group mode, or if it is in the visible group
+     * mode, this method returns true. This is consistent with how the
+     * {@link groups_get_activity_allowed_groups()} behaves.
+     *
+     * If the workshop is in a separate group mode, the current user has to
+     * have the 'access all groups' permission, or share at least one
+     * accessible group with the other user.
+     *
+     * @param int $otheruserid The ID of the other user, e.g. the author of a submission.
+     * @return bool False if the current user cannot access the other user's group.
+     */
+    public function check_group_membership($otheruserid) {
+        global $USER;
+
+        if (groups_get_activity_groupmode($this->cm) != SEPARATEGROUPS) {
+            // The workshop is not in a group mode, or it is in a visible group mode.
+            return true;
+
+        } else if (has_capability('moodle/site:accessallgroups', $this->context)) {
+            // The current user can access all groups.
+            return true;
+
+        } else {
+            $thisusersgroups = groups_get_all_groups($this->course->id, $USER->id, $this->cm->groupingid, 'g.id');
+            $otherusersgroups = groups_get_all_groups($this->course->id, $otheruserid, $this->cm->groupingid, 'g.id');
+            $commongroups = array_intersect_key($thisusersgroups, $otherusersgroups);
+
+            if (empty($commongroups)) {
+                // The current user has no group common with the other user.
+                return false;
+
+            } else {
+                // The current user has a group common with the other user.
+                return true;
+            }
+        }
+    }
 
     ////////////////////////////////////////////////////////////////////////////////
     // Internal methods (implementation details)                                  //
index 5b90986..6e9860b 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -102,20 +101,7 @@ if ($submission->id and !$workshop->modifying_submission_allowed($USER->id)) {
     $editable = false;
 }
 
-if ($canviewall) {
-    // check this flag against the group membership yet
-    if (groups_get_activity_groupmode($workshop->cm) == SEPARATEGROUPS) {
-        // user must have accessallgroups or share at least one group with the submission author
-        if (!has_capability('moodle/site:accessallgroups', $workshop->context)) {
-            $usersgroups = groups_get_activity_allowed_groups($workshop->cm);
-            $authorsgroups = groups_get_all_groups($workshop->course->id, $submission->authorid, $workshop->cm->groupingid, 'g.id');
-            $sharedgroups = array_intersect_key($usersgroups, $authorsgroups);
-            if (empty($sharedgroups)) {
-                $canviewall = false;
-            }
-        }
-    }
-}
+$canviewall = $canviewall && $workshop->check_group_membership($submission->authorid);
 
 if ($editable and $workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_SUBMISSION
         and !has_capability('mod/workshop:manageexamples', $workshop->context)) {
@@ -366,6 +352,7 @@ if ($submission->id) {
 
 // If not at removal confirmation screen, some action buttons can be displayed.
 if (!$delete) {
+    // Display create/edit button.
     if ($editable) {
         if ($submission->id) {
             $btnurl = new moodle_url($PAGE->url, array('edit' => 'on', 'id' => $submission->id));
@@ -377,11 +364,13 @@ if (!$delete) {
         echo $output->single_button($btnurl, $btntxt, 'get');
     }
 
+    // Display delete button.
     if ($submission->id and $deletable) {
         $url = new moodle_url($PAGE->url, array('delete' => 1));
         echo $output->single_button($url, get_string('deletesubmission', 'workshop'), 'get');
     }
 
+    // Display assess button.
     if ($submission->id and !$edit and !$isreviewer and $canallocate and $workshop->assessing_allowed($USER->id)) {
         $url = new moodle_url($PAGE->url, array('assess' => 1));
         echo $output->single_button($url, get_string('assess', 'workshop'), 'post');
@@ -469,4 +458,24 @@ if (!$edit and $canoverride) {
     $feedbackform->display();
 }
 
+// If portfolios are enabled and we are not on the edit/removal confirmation screen, display a button to export this page.
+// The export is not offered if the submission is seen as a published one (it has no relation to the current user.
+if (!empty($CFG->enableportfolios)) {
+    if (!$delete and !$edit and !$seenaspublished and $submission->id and ($ownsubmission or $canviewall or $isreviewer)) {
+        if (has_capability('mod/workshop:exportsubmissions', $workshop->context)) {
+            require_once($CFG->libdir.'/portfoliolib.php');
+
+            $button = new portfolio_add_button();
+            $button->set_callback_options('mod_workshop_portfolio_caller', array(
+                'id' => $workshop->cm->id,
+                'submissionid' => $submission->id,
+            ), 'mod_workshop');
+            $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
+            echo html_writer::start_tag('div', array('class' => 'singlebutton'));
+            echo $button->to_html(PORTFOLIO_ADD_FULL_FORM, get_string('exportsubmission', 'workshop'));
+            echo html_writer::end_tag('div');
+        }
+    }
+}
+
 echo $output->footer();
index ec539b2..1c9224a 100644 (file)
@@ -160,4 +160,27 @@ class behat_mod_workshop extends behat_base {
         }
         $this->find('xpath', $xpath);
     }
+
+    /**
+     * Configure portfolio plugin, set value for portfolio instance
+     *
+     * @When /^I set portfolio instance "(?P<portfolioinstance_string>(?:[^"]|\\")*)" to "(?P<value_string>(?:[^"]|\\")*)"$/
+     * @param string $portfolioinstance
+     * @param string $value
+     */
+    public function i_set_portfolio_instance_to($portfolioinstance, $value) {
+
+        $rowxpath = "//table[contains(@class, 'generaltable')]//tr//td[contains(text(), '"
+            . $portfolioinstance . "')]/following-sibling::td";
+
+        $selectxpath = $rowxpath.'//select';
+        $select = $this->find('xpath', $selectxpath);
+        $select->selectOption($value);
+
+        if (!$this->running_javascript()) {
+            $this->execute('behat_general::i_click_on_in_the',
+                array(get_string('go'), "button", $rowxpath, "xpath_element")
+            );
+        }
+    }
 }
diff --git a/mod/workshop/tests/behat/export_submission.feature b/mod/workshop/tests/behat/export_submission.feature
new file mode 100644 (file)
index 0000000..88a3037
--- /dev/null
@@ -0,0 +1,114 @@
+@mod @mod_workshop
+Feature: Exporting workshop submissions and assessments to a portfolio
+  In order to archive my workshop contribution in a personal storage
+  As a student or as a teacher
+  I need to be able to export a workshop submission and its associated assessments
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                 |
+      | student1 | Sam1      | Student1 | student1@example.com  |
+      | student2 | Sam2      | Student2 | student2@example.com  |
+      | teacher1 | Terry1    | Teacher1 | teacher1@example.com  |
+    And the following "courses" exist:
+      | fullname  | shortname |
+      | Course1   | c1        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | student1 | c1     | student        |
+      | student2 | c1     | student        |
+      | teacher1 | c1     | editingteacher |
+    And the following "activities" exist:
+      | activity | name         | intro                     | course | idnumber  |
+      | workshop | TestWorkshop | Test workshop description | c1     | workshop1 |
+    # Admin needs to enable portfolio API and set a portfolio instance first.
+    And I log in as "admin"
+    And the following config values are set as admin:
+      | enableportfolios | 1 |
+    And I expand "Site administration" node
+    And I expand "Plugins" node
+    And I expand "Portfolios" node
+    And I follow "Manage portfolios"
+    And I set portfolio instance "File download" to "Enabled and visible"
+    And I click on "Save" "button"
+    And I log out
+    # Teacher sets up assessment form and changes the phase to submission.
+    And I log in as "teacher1"
+    And I follow "Course1"
+    And I edit assessment form in workshop "TestWorkshop" as:"
+      | id_description__idx_0_editor | Aspect1 |
+      | id_description__idx_1_editor | Aspect2 |
+    And I change phase in workshop "TestWorkshop" to "Submission phase"
+    And I log out
+    # Student1 submits.
+    And I log in as "student1"
+    And I follow "Course1"
+    And I follow "TestWorkshop"
+    And I add a submission in workshop "TestWorkshop" as:"
+      | Title              | Submission1  |
+      | Submission content | Some content |
+    And I log out
+    # Student2 submits.
+    And I log in as "student2"
+    And I follow "Course1"
+    And I add a submission in workshop "TestWorkshop" as:"
+      | Title              | Submission2  |
+      | Submission content | Some content |
+    And I log out
+     # Teacher allocates reviewers and changes the phase to assessment.
+    And I log in as "teacher1"
+    And I follow "Course1"
+    And I follow "TestWorkshop"
+    And I should see "to allocate: 2"
+    And I should see "Workshop submissions report"
+    And I should see "Submitted (2) / not submitted (0)"
+    And I should see "Submission1" in the "Sam1 Student1" "table_row"
+    And I should see "Submission2" in the "Sam2 Student2" "table_row"
+    And I allocate submissions in workshop "TestWorkshop" as:"
+      | Participant   | Reviewer      |
+      | Sam1 Student1 | Sam2 Student2 |
+      | Sam2 Student2 | Sam1 Student1 |
+    And I follow "TestWorkshop"
+    And I should see "to allocate: 0"
+    And I change phase in workshop "TestWorkshop" to "Assessment phase"
+    And I log out
+
+  Scenario: Students can export their own submission to a portfolio.
+    Given I log in as "student1"
+    And I follow "Course1"
+    And I follow "TestWorkshop"
+    When I follow "My submission"
+    Then I should see "Submission1"
+    And "Export this page" "button" should exist
+    And I click on "Export this page" "button"
+    And I should see "Available export formats"
+    And I click on "Next" "button"
+    And I should see "Summary of your export"
+    And I click on "Continue" "button"
+    And I should see "Return to where you were"
+    And I log out
+
+  Scenario: Students can export submission they have peer-assessed.
+    Given I log in as "student1"
+    And I follow "Course1"
+    And I follow "TestWorkshop"
+    And I should see "Submission2"
+    And I follow "Submission2"
+    And "Export this page" "button" should exist
+    When I click on "Export this page" "button"
+    Then I should see "Available export formats"
+    And I click on "Next" "button"
+    And I should see "Summary of your export"
+    And I click on "Continue" "button"
+    And I should see "Return to where you were"
+    And I log out
+
+  Scenario: If the portfolio API is disabled, the portfolio export button is not displayed.
+    Given the following config values are set as admin:
+      | enableportfolios | 0 |
+    When I log in as "student1"
+    And I follow "Course1"
+    And I follow "TestWorkshop"
+    And I follow "My submission"
+    Then I should see "Submission1"
+    And "Export this page" "button" should not exist
index 2c43f63..2133e0a 100644 (file)
@@ -35,17 +35,20 @@ require_once(__DIR__ . '/fixtures/testable.php');
  */
 class mod_workshop_internal_api_testcase extends advanced_testcase {
 
-    /** workshop instance emulation */
+    /** @var object */
+    protected $course;
+
+    /** @var workshop */
     protected $workshop;
 
     /** setup testing environment */
     protected function setUp() {
         parent::setUp();
         $this->setAdminUser();
-        $course = $this->getDataGenerator()->create_course();
-        $workshop = $this->getDataGenerator()->create_module('workshop', array('course' => $course));
-        $cm = get_coursemodule_from_instance('workshop', $workshop->id, $course->id, false, MUST_EXIST);
-        $this->workshop = new testable_workshop($workshop, $cm, $course);
+        $this->course = $this->getDataGenerator()->create_course();
+        $workshop = $this->getDataGenerator()->create_module('workshop', array('course' => $this->course));
+        $cm = get_coursemodule_from_instance('workshop', $workshop->id, $this->course->id, false, MUST_EXIST);
+        $this->workshop = new testable_workshop($workshop, $cm, $this->course);
     }
 
     protected function tearDown() {
@@ -722,4 +725,111 @@ class mod_workshop_internal_api_testcase extends advanced_testcase {
         $this->assertFalse(workshop::is_allowed_file_type('solution.odt~', 'odt, xls'));
         $this->assertTrue(workshop::is_allowed_file_type('solution.odt~', 'odt, odt~'));
     }
+
+    /**
+     * Test workshop::check_group_membership() functionality.
+     */
+    public function test_check_group_membership() {
+        global $DB, $CFG;
+
+        $this->resetAfterTest();
+
+        $courseid = $this->course->id;
+        $generator = $this->getDataGenerator();
+
+        // Make test groups.
+        $group1 = $generator->create_group(array('courseid' => $courseid));
+        $group2 = $generator->create_group(array('courseid' => $courseid));
+        $group3 = $generator->create_group(array('courseid' => $courseid));
+
+        // Revoke the accessallgroups from non-editing teachers (tutors).
+        $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
+        unassign_capability('moodle/site:accessallgroups', $roleids['teacher']);
+
+        // Create test use accounts.
+        $teacher1 = $generator->create_user();
+        $tutor1 = $generator->create_user();
+        $tutor2 = $generator->create_user();
+        $student1 = $generator->create_user();
+        $student2 = $generator->create_user();
+        $student3 = $generator->create_user();
+
+        // Enrol the teacher (has the access all groups permission).
+        $generator->enrol_user($teacher1->id, $courseid, $roleids['editingteacher']);
+
+        // Enrol tutors (can not access all groups).
+        $generator->enrol_user($tutor1->id, $courseid, $roleids['teacher']);
+        $generator->enrol_user($tutor2->id, $courseid, $roleids['teacher']);
+
+        // Enrol students.
+        $generator->enrol_user($student1->id, $courseid, $roleids['student']);
+        $generator->enrol_user($student2->id, $courseid, $roleids['student']);
+        $generator->enrol_user($student3->id, $courseid, $roleids['student']);
+
+        // Add users in groups.
+        groups_add_member($group1, $tutor1);
+        groups_add_member($group2, $tutor2);
+        groups_add_member($group1, $student1);
+        groups_add_member($group2, $student2);
+        groups_add_member($group3, $student3);
+
+        // Workshop with no groups.
+        $workshopitem1 = $this->getDataGenerator()->create_module('workshop', [
+            'course' => $courseid,
+            'groupmode' => NOGROUPS,
+        ]);
+        $cm = get_coursemodule_from_instance('workshop', $workshopitem1->id, $courseid, false, MUST_EXIST);
+        $workshop1 = new testable_workshop($workshopitem1, $cm, $this->course);
+
+        $this->setUser($teacher1);
+        $this->assertTrue($workshop1->check_group_membership($student1->id));
+        $this->assertTrue($workshop1->check_group_membership($student2->id));
+        $this->assertTrue($workshop1->check_group_membership($student3->id));
+
+        $this->setUser($tutor1);
+        $this->assertTrue($workshop1->check_group_membership($student1->id));
+        $this->assertTrue($workshop1->check_group_membership($student2->id));
+        $this->assertTrue($workshop1->check_group_membership($student3->id));
+
+        // Workshop in visible groups mode.
+        $workshopitem2 = $this->getDataGenerator()->create_module('workshop', [
+            'course' => $courseid,
+            'groupmode' => VISIBLEGROUPS,
+        ]);
+        $cm = get_coursemodule_from_instance('workshop', $workshopitem2->id, $courseid, false, MUST_EXIST);
+        $workshop2 = new testable_workshop($workshopitem2, $cm, $this->course);
+
+        $this->setUser($teacher1);
+        $this->assertTrue($workshop2->check_group_membership($student1->id));
+        $this->assertTrue($workshop2->check_group_membership($student2->id));
+        $this->assertTrue($workshop2->check_group_membership($student3->id));
+
+        $this->setUser($tutor1);
+        $this->assertTrue($workshop2->check_group_membership($student1->id));
+        $this->assertTrue($workshop2->check_group_membership($student2->id));
+        $this->assertTrue($workshop2->check_group_membership($student3->id));
+
+        // Workshop in separate groups mode.
+        $workshopitem3 = $this->getDataGenerator()->create_module('workshop', [
+            'course' => $courseid,
+            'groupmode' => SEPARATEGROUPS,
+        ]);
+        $cm = get_coursemodule_from_instance('workshop', $workshopitem3->id, $courseid, false, MUST_EXIST);
+        $workshop3 = new testable_workshop($workshopitem3, $cm, $this->course);
+
+        $this->setUser($teacher1);
+        $this->assertTrue($workshop3->check_group_membership($student1->id));
+        $this->assertTrue($workshop3->check_group_membership($student2->id));
+        $this->assertTrue($workshop3->check_group_membership($student3->id));
+
+        $this->setUser($tutor1);
+        $this->assertTrue($workshop3->check_group_membership($student1->id));
+        $this->assertFalse($workshop3->check_group_membership($student2->id));
+        $this->assertFalse($workshop3->check_group_membership($student3->id));
+
+        $this->setUser($tutor2);
+        $this->assertFalse($workshop3->check_group_membership($student1->id));
+        $this->assertTrue($workshop3->check_group_membership($student2->id));
+        $this->assertFalse($workshop3->check_group_membership($student3->id));
+    }
 }
diff --git a/mod/workshop/tests/portfolio_caller_test.php b/mod/workshop/tests/portfolio_caller_test.php
new file mode 100644 (file)
index 0000000..f93805e
--- /dev/null
@@ -0,0 +1,190 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for mod_workshop_portfolio_caller class defined in mod/workshop/classes/portfolio_caller.php
+ *
+ * @package    mod_workshop
+ * @copyright  2016 An Pham Van <an.phamvan@harveynash.vn>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/mod/workshop/locallib.php');
+require_once(__DIR__ . '/fixtures/testable.php');
+require_once($CFG->dirroot . '/mod/workshop/classes/portfolio_caller.php');
+
+/**
+ * Unit tests for mod_workshop_portfolio_caller class
+ *
+ * @package    mod_workshop
+ * @copyright  2016 An Pham Van <an.phamvan@harveynash.vn>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_workshop_porfolio_caller_testcase extends advanced_testcase {
+
+    /** @var stdClass $workshop Basic workshop data stored in an object. */
+    protected $workshop;
+    /** @var stdClass mod info */
+    protected $cm;
+
+    /**
+     * Setup testing environment.
+     */
+    protected function setUp() {
+        parent::setUp();
+        $this->setAdminUser();
+        $course = $this->getDataGenerator()->create_course();
+        $workshop = $this->getDataGenerator()->create_module('workshop', ['course' => $course]);
+        $this->cm = get_coursemodule_from_instance('workshop', $workshop->id, $course->id, false, MUST_EXIST);
+        $this->workshop = new testable_workshop($workshop, $this->cm, $course);
+    }
+
+    /**
+     * Tear down.
+     */
+    protected function tearDown() {
+        $this->workshop = null;
+        $this->cm = null;
+        parent::tearDown();
+    }
+
+    /**
+     * Test the method mod_workshop_portfolio_caller::load_data()
+     */
+    public function test_load_data() {
+        $this->resetAfterTest(true);
+
+        $student1 = $this->getDataGenerator()->create_user();
+        $student2 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($student1->id, $this->workshop->course->id);
+        $this->getDataGenerator()->enrol_user($student2->id, $this->workshop->course->id);
+        $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
+        $subid1 = $workshopgenerator->create_submission($this->workshop->id, $student1->id);
+        $asid1 = $workshopgenerator->create_assessment($subid1, $student2->id);
+
+        $portfoliocaller = new mod_workshop_portfolio_caller(['id' => $this->workshop->cm->id, 'submissionid' => $subid1]);
+        $portfoliocaller->set_formats_from_button([]);
+        $portfoliocaller->load_data();
+
+        $reflector = new ReflectionObject($portfoliocaller);
+        $propertysubmission = $reflector->getProperty('submission');
+        $propertysubmission->setAccessible(true);
+        $submission = $propertysubmission->getValue($portfoliocaller);
+
+        $this->assertEquals($subid1, $submission->id);
+    }
+
+    /**
+     * Test the method mod_workshop_portfolio_caller::get_return_url()
+     */
+    public function test_get_return_url() {
+        $this->resetAfterTest(true);
+
+        $student1 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($student1->id, $this->workshop->course->id);
+        $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
+        $subid1 = $workshopgenerator->create_submission($this->workshop->id, $student1->id);
+
+        $portfoliocaller = new mod_workshop_portfolio_caller(['id' => $this->workshop->cm->id, 'submissionid' => $subid1]);
+        $portfoliocaller->set_formats_from_button([]);
+        $portfoliocaller->load_data();
+
+        $expected = new moodle_url('/mod/workshop/submission.php', ['cmid' => $this->workshop->cm->id, 'id' => $subid1]);
+        $actual = new moodle_url($portfoliocaller->get_return_url());
+        $this->assertTrue($expected->compare($actual));
+    }
+
+    /**
+     * Test the method mod_workshop_portfolio_caller::get_navigation()
+     */
+    public function test_get_navigation() {
+        $this->resetAfterTest(true);
+
+        $student1 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($student1->id, $this->workshop->course->id);
+        $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
+        $subid1 = $workshopgenerator->create_submission($this->workshop->id, $student1->id);
+
+        $portfoliocaller = new mod_workshop_portfolio_caller(['id' => $this->workshop->cm->id, 'submissionid' => $subid1]);
+        $portfoliocaller->set_formats_from_button([]);
+        $portfoliocaller->load_data();
+
+        $this->assertTrue(is_array($portfoliocaller->get_navigation()));
+    }
+
+    /**
+     * Test the method mod_workshop_portfolio_caller::check_permissions()
+     */
+    public function test_check_permissions_exportownsubmissionassessment() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $context = context_module::instance($this->cm->id);
+        $student1 = $this->getDataGenerator()->create_user();
+        $student2 = $this->getDataGenerator()->create_user();
+        $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
+        $this->getDataGenerator()->enrol_user($student1->id, $this->workshop->course->id, $roleids['student']);
+        $this->getDataGenerator()->enrol_user($student2->id, $this->workshop->course->id, $roleids['student']);
+        $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
+        $subid1 = $workshopgenerator->create_submission($this->workshop->id, $student1->id);
+        $asid1 = $workshopgenerator->create_assessment($subid1, $student2->id);
+        $this->setUser($student1);
+
+        $portfoliocaller = new mod_workshop_portfolio_caller(['id' => $this->workshop->cm->id, 'submissionid' => $subid1]);
+
+        role_change_permission($roleids['student'], $context, 'mod/workshop:exportsubmissions', CAP_PREVENT);
+        $this->assertFalse($portfoliocaller->check_permissions());
+
+        role_change_permission($roleids['student'], $context, 'mod/workshop:exportsubmissions', CAP_ALLOW);
+        $this->assertTrue($portfoliocaller->check_permissions());
+    }
+
+    /**
+     * Test the method mod_workshop_portfolio_caller::get_sha1()
+     */
+    public function test_get_sha1() {
+        $this->resetAfterTest(true);
+
+        $student1 = $this->getDataGenerator()->create_user();
+        $student2 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($student1->id, $this->workshop->course->id);
+        $this->getDataGenerator()->enrol_user($student2->id, $this->workshop->course->id);
+        $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
+        $subid1 = $workshopgenerator->create_submission($this->workshop->id, $student1->id);
+        $asid1 = $workshopgenerator->create_assessment($subid1, $student2->id);
+
+        $portfoliocaller = new mod_workshop_portfolio_caller(['id' => $this->workshop->cm->id, 'submissionid' => $subid1]);
+        $portfoliocaller->set_formats_from_button([]);
+        $portfoliocaller->load_data();
+
+        $this->assertTrue(is_string($portfoliocaller->get_sha1()));
+    }
+
+    /**
+     * Test function display_name()
+     * Assert that this function can return the name of the module ('Workshop').
+     */
+    public function test_display_name() {
+        $this->resetAfterTest(true);
+
+        $name = mod_workshop_portfolio_caller::display_name();
+        $this->assertEquals(get_string('pluginname', 'mod_workshop'), $name);
+    }
+}
index 63fbfc0..5d03388 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016052300;        // The current module version (YYYYMMDDXX)
+$plugin->version   = 2016100600;        // The current module version (YYYYMMDDXX)
 $plugin->requires  = 2016051900;        // Requires this Moodle version.
 $plugin->component = 'mod_workshop';
 $plugin->cron      = 60;                // Give as a chance every minute.
index caee97a..affc9f5 100644 (file)
       "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
     },
     "autoprefixer": {
-      "version": "6.3.7",
-      "from": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.3.7.tgz",
-      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.3.7.tgz"
+      "version": "6.5.1",
+      "from": "autoprefixer@>=6.0.0 <7.0.0",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.5.1.tgz"
     },
     "aws-sign2": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz"
     },
     "browserslist": {
-      "version": "1.3.5",
-      "from": "https://registry.npmjs.org/browserslist/-/browserslist-1.3.5.tgz",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.3.5.tgz"
+      "version": "1.4.0",
+      "from": "browserslist@>=1.4.0 <1.5.0",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.4.0.tgz"
     },
     "builtin-modules": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz"
     },
     "caniuse-db": {
-      "version": "1.0.30000512",
-      "from": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000512.tgz",
-      "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000512.tgz"
+      "version": "1.0.30000555",
+      "from": "caniuse-db@>=1.0.30000554 <2.0.0",
+      "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000555.tgz"
     },
     "caseless": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
     },
     "cosmiconfig": {
-      "version": "1.1.0",
-      "from": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-1.1.0.tgz",
-      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-1.1.0.tgz",
+      "version": "2.0.2",
+      "from": "cosmiconfig@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.0.2.tgz",
       "dependencies": {
         "minimist": {
           "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.4.0.tgz"
     },
     "doiuse": {
-      "version": "2.4.1",
-      "from": "https://registry.npmjs.org/doiuse/-/doiuse-2.4.1.tgz",
-      "resolved": "https://registry.npmjs.org/doiuse/-/doiuse-2.4.1.tgz",
+      "version": "2.5.0",
+      "from": "doiuse@>=2.4.1 <3.0.0",
+      "resolved": "https://registry.npmjs.org/doiuse/-/doiuse-2.5.0.tgz",
       "dependencies": {
         "source-map": {
           "version": "0.4.4",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
     },
     "ignore": {
-      "version": "3.1.3",
-      "from": "https://registry.npmjs.org/ignore/-/ignore-3.1.3.tgz",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.1.3.tgz"
+      "version": "3.2.0",
+      "from": "ignore@>=3.1.3 <4.0.0",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.2.0.tgz"
     },
     "image-size": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.0.3.tgz"
     },
     "known-css-properties": {
-      "version": "0.0.3",
-      "from": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.0.3.tgz",
-      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.0.3.tgz"
+      "version": "0.0.5",
+      "from": "known-css-properties@>=0.0.5 <0.0.6",
+      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.0.5.tgz"
     },
     "knox": {
       "version": "0.8.10",
       "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz"
     },
     "postcss": {
-      "version": "5.1.1",
-      "from": "https://registry.npmjs.org/postcss/-/postcss-5.1.1.tgz",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.1.1.tgz",
+      "version": "5.2.4",
+      "from": "postcss@>=5.0.20 <6.0.0",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.4.tgz",
       "dependencies": {
         "supports-color": {
           "version": "3.1.2",
       "from": "https://registry.npmjs.org/postcss-less/-/postcss-less-0.14.0.tgz",
       "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-0.14.0.tgz"
     },
+    "postcss-media-query-parser": {
+      "version": "0.2.1",
+      "from": "postcss-media-query-parser@>=0.2.0 <0.3.0",
+      "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.1.tgz"
+    },
     "postcss-reporter": {
       "version": "1.4.1",
       "from": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-1.4.1.tgz",
       "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz"
     },
     "postcss-scss": {
-      "version": "0.1.9",
-      "from": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-0.1.9.tgz",
-      "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-0.1.9.tgz"
+      "version": "0.3.1",
+      "from": "postcss-scss@>=0.3.0 <0.4.0",
+      "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-0.3.1.tgz"
     },
     "postcss-selector-parser": {
-      "version": "2.1.1",
-      "from": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.1.1.tgz",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.1.1.tgz"
+      "version": "2.2.1",
+      "from": "postcss-selector-parser@>=2.1.1 <3.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.1.tgz"
     },
     "postcss-value-parser": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/request/-/request-2.73.0.tgz"
     },
     "require-from-string": {
-      "version": "1.2.0",
-      "from": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.0.tgz",
-      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.0.tgz"
+      "version": "1.2.1",
+      "from": "require-from-string@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz"
     },
     "require-uncached": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.1.tgz"
     },
     "specificity": {
-      "version": "0.2.1",
-      "from": "https://registry.npmjs.org/specificity/-/specificity-0.2.1.tgz",
-      "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.2.1.tgz"
+      "version": "0.3.0",
+      "from": "specificity@>=0.3.0 <0.4.0",
+      "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.3.0.tgz"
     },
     "split2": {
       "version": "0.2.1",
       }
     },
     "stylelint": {
-      "version": "7.0.3",
-      "from": "https://registry.npmjs.org/stylelint/-/stylelint-7.0.3.tgz",
-      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-7.0.3.tgz",
+      "version": "7.4.1",
+      "from": "stylelint@7.4.1",
+      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-7.4.1.tgz",
       "dependencies": {
         "get-stdin": {
           "version": "5.0.1",
           "from": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz",
           "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz"
         },
+        "globby": {
+          "version": "6.0.0",
+          "from": "globby@>=6.0.0 <7.0.0",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-6.0.0.tgz"
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "from": "is-fullwidth-code-point@>=2.0.0 <3.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz"
+        },
         "resolve-from": {
           "version": "2.0.0",
           "from": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
           "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz"
+        },
+        "string-width": {
+          "version": "2.0.0",
+          "from": "string-width@>=2.0.0 <3.0.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.0.0.tgz"
         }
       }
     },
       }
     },
     "sugarss": {
-      "version": "0.1.5",
-      "from": "https://registry.npmjs.org/sugarss/-/sugarss-0.1.5.tgz",
-      "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-0.1.5.tgz"
+      "version": "0.1.6",
+      "from": "sugarss@>=0.1.2 <0.2.0",
+      "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-0.1.6.tgz"
     },
     "supports-color": {
       "version": "2.0.0",
index 0553787..01dc1dc 100644 (file)
@@ -13,6 +13,7 @@
     "grunt-stylelint": "0.6.0",
     "semver": "5.3.0",
     "shifter": "0.5.0",
+    "stylelint": "7.4.1",
     "stylelint-checkstyle-formatter": "0.1.0",
     "xmldom": "0.1.22",
     "xpath": "0.0.23"
index cfbab0b..f6f7599 100644 (file)
@@ -198,13 +198,10 @@ class manager {
      */
     public static function get_search_area($areaid) {
 
-        // Try both caches, it does not matter where it comes from.
+        // We have them all here.
         if (!empty(static::$allsearchareas[$areaid])) {
             return static::$allsearchareas[$areaid];
         }
-        if (!empty(static::$enabledsearchareas[$areaid])) {
-            return static::$enabledsearchareas[$areaid];
-        }
 
         $classname = static::get_area_classname($areaid);
 
@@ -224,13 +221,16 @@ class manager {
     public static function get_search_areas_list($enabled = false) {
 
         // Two different arrays, we don't expect these arrays to be big.
-        if (!$enabled && static::$allsearchareas !== null) {
-            return static::$allsearchareas;
-        } else if ($enabled && static::$enabledsearchareas !== null) {
-            return static::$enabledsearchareas;
+        if (static::$allsearchareas !== null) {
+            if (!$enabled) {
+                return static::$allsearchareas;
+            } else {
+                return static::$enabledsearchareas;
+            }
         }
 
-        $searchareas = array();
+        static::$allsearchareas = array();
+        static::$enabledsearchareas = array();
 
         $plugintypes = \core_component::get_plugin_types();
         foreach ($plugintypes as $plugintype => $unused) {
@@ -248,8 +248,10 @@ class manager {
 
                     $areaid = static::generate_areaid($componentname, $areaname);
                     $searchclass = new $classname();
-                    if (!$enabled || ($enabled && $searchclass->is_enabled())) {
-                        $searchareas[$areaid] = $searchclass;
+
+                    static::$allsearchareas[$areaid] = $searchclass;
+                    if ($searchclass->is_enabled()) {
+                        static::$enabledsearchareas[$areaid] = $searchclass;
                     }
                 }
             }
@@ -269,20 +271,17 @@ class manager {
 
                 $areaid = static::generate_areaid($componentname, $areaname);
                 $searchclass = new $classname();
-                if (!$enabled || ($enabled && $searchclass->is_enabled())) {
-                    $searchareas[$areaid] = $searchclass;
+                static::$allsearchareas[$areaid] = $searchclass;
+                if ($searchclass->is_enabled()) {
+                    static::$enabledsearchareas[$areaid] = $searchclass;
                 }
             }
         }
 
-        // Cache results.
         if ($enabled) {
-            static::$enabledsearchareas = $searchareas;
-        } else {
-            static::$allsearchareas = $searchareas;
+            return static::$enabledsearchareas;
         }
-
-        return $searchareas;
+        return static::$allsearchareas;
     }
 
     /**
diff --git a/theme/boost/classes/admin_setting_scss_variables.php b/theme/boost/classes/admin_setting_scss_variables.php
deleted file mode 100644 (file)
index 6e043fa..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-<?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/>.
-
-/**
- * Admin setting for SCSS variables.
- *
- * @package   theme_boost
- * @copyright 2016 Frédéric Massart - FMCorz.net
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Admin setting for SCSS variables class.
- *
- * @package   theme_boost
- * @copyright 2016 Frédéric Massart - FMCorz.net
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class theme_boost_admin_setting_scss_variables extends admin_setting_configtextarea {
-
-    /**
-     * Validate data before storage.
-     *
-     * @param string $data The data.
-     * @return mixed True if validated, else an error string.
-     */
-    public function validate($data) {
-        if (empty($data)) {
-            return true;
-        }
-
-        try {
-            theme_boost_parse_scss_variables($data, false);
-        } catch (moodle_exception $e) {
-            return $e->getMessage();
-        }
-
-        return true;
-    }
-
-}
-
index f60f1f4..f52949d 100644 (file)
@@ -146,6 +146,6 @@ $THEME->parents = [];
 $THEME->enable_dock = false;
 $THEME->csstreepostprocessor = 'theme_boost_css_tree_post_processor';
 $THEME->extrascsscallback = 'theme_boost_get_extra_scss';
-$THEME->scssvariablescallback = 'theme_boost_get_scss_variables';
+$THEME->prescsscallback = 'theme_boost_get_pre_scss';
 $THEME->yuicssmodules = array();
 $THEME->rendererfactory = 'theme_overridden_renderer_factory';
index 7fa2332..0750bfe 100644 (file)
@@ -30,7 +30,6 @@ $string['brandcolor_desc'] = 'The accent colour.';
 $string['choosereadme'] = 'Boost is a modern highly customizable theme. This theme is intended to be used directly, or used as a parent theme when creating new themes utilising Boostrap 4.';
 $string['currentinparentheses'] = '(current)';
 $string['configtitle'] = 'Boost';
-$string['errorparsingscssvariables'] = 'There was an error parsing the variable at line {$a}, please double check the syntax.';
 $string['generalsettings'] = 'General settings';
 $string['pluginname'] = 'Boost';
 $string['preset'] = 'Theme preset';
@@ -41,8 +40,8 @@ $string['presetpaper'] = 'Paper';
 $string['presetplain'] = 'Plain';
 $string['presetreadable'] = 'Readable';
 $string['rawscss'] = 'Raw SCSS';
-$string['rawscss_desc'] = 'Use this field to provide SCSS code which will be injected at the end of the the stylesheet.';
+$string['rawscss_desc'] = 'Use this field to provide SCSS code which will be injected at the end of the stylesheet.';
+$string['rawscsspre'] = 'Raw initial SCSS';
+$string['rawscsspre_desc'] = 'In this field you can provide initialising SCSS code, it will be injected before everything else. Most of the time you will use this setting to define variables.';
 $string['region-side-post'] = 'Right';
 $string['region-side-pre'] = 'Left';
-$string['scssvariables'] = 'SCSS variables';
-$string['scssvariables_desc'] = 'Use this field to set your own SCSS variable values. Define one variable per line. Syntax: $my-color: red;.';
index 72776d5..6f18143 100644 (file)
@@ -57,73 +57,33 @@ function theme_boost_get_extra_scss($theme) {
 }
 
 /**
- * Get additional SCSS variables.
+ * Get SCSS to prepend.
  *
  * @param theme_config $theme The theme config object.
  * @return array
  */
-function theme_boost_get_scss_variables($theme) {
-    $variables = [];
+function theme_boost_get_pre_scss($theme) {
+    $scss = '';
     $configurable = [
         // Config key => [variableName, ...].
         'brandcolor' => ['brand-primary'],
     ];
 
+    // Prepend variables first.
     foreach ($configurable as $configkey => $targets) {
         $value = $theme->settings->{$configkey};
         if (empty($value)) {
             continue;
         }
-        array_map(function($target) use (&$variables, $value) {
-            $variables[$target] = $value;
+        array_map(function($target) use (&$scss, $value) {
+            $scss .= '$' . $target . ': ' . $value . ";\n";
         }, (array) $targets);
     }
 
-    if (!empty($theme->settings->scss_variables)) {
-        $variables = array_merge($variables, theme_boost_parse_scss_variables($theme->settings->scss_variables));
+    // Prepend pre-scss.
+    if (!empty($theme->settings->scsspre)) {
+        $scss .= $theme->settings->scsspre;
     }
 
-    return $variables;
-}
-
-/**
- * Parse a string into SCSS variables.
- *
- * - One variable definition per line,
- * - The variable name is separated from the value by a colon,
- * - The dollar sign is optional,
- * - The trailing semi-colon is optional,
- * - CSS comments (starting with //) are accepted
- * - Variables names can only contain letters, numbers, hyphens and underscores.
- *
- * @param string $data The string to parse from.
- * @param bool $lenient When non lenient, an exception will be thrown when a line cannot be parsed.
- * @return array
- */
-function theme_boost_parse_scss_variables($data, $lenient = true) {
-    $variables = [];
-    $lines = explode("\n", $data);
-    $i = 0;
-
-    foreach ($lines as $line) {
-        $i++;
-        if (preg_match('@^\s*//@', $line)) {
-            continue;
-        }
-
-        $parts = explode(':', trim($line));
-        $variable = ltrim($parts[0], '$ ');
-        $value = rtrim(ltrim(isset($parts[1]) ? $parts[1] : ''), "; ");
-
-        if (empty($variable) || !preg_match('/^[a-z0-9_-]+$/i', $variable) || (empty($value) && !is_numeric($value))) {
-            if ($lenient) {
-                continue;
-            }
-            throw new moodle_exception('errorparsingscssvariables', 'theme_boost', null, $i);
-        }
-
-        $variables[$variable] = $value;
-    }
-
-    return $variables;
+    return $scss;
 }
index efcfbf3..ac73f41 100644 (file)
@@ -229,6 +229,7 @@ $content-item-unread-colour: #f4f4f4;
     }
 
     &.unread {
+        margin: 0;
         background-color: $content-item-unread-colour;
 
         &:hover {
index 378985e..4275eb9 100644 (file)
     }
 }
 
+.ajax-contact-button {
+    box-sizing: border-box;
+    position: relative;
+
+    &.loading {
+        .loading-icon {
+            display: block;
+        }
+    }
+
+    .loading-icon {
+        display: none;
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        background-color: rgba(255, 255, 255, 0.7);
+
+        .icon {
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%, -50%);
+        }
+    }
+}
+
 @media (max-width: 480px) {
     .userprofile .profile_tree {
         /** Display the profile on one column on phones@mixin  */
index e4b5eb3..505db9b 100644 (file)
@@ -58,13 +58,13 @@ if ($ADMIN->fulltree) {
     // Advanced settings.
     $page = new admin_settingpage('theme_boost_advanced', get_string('advancedsettings', 'theme_boost'));
 
-    // Raw SCSS for before the content.
-    $setting = new theme_boost_admin_setting_scss_variables('theme_boost/scss_variables',
-        get_string('scssvariables', 'theme_boost'), get_string('scssvariables_desc', 'theme_boost'), '', PARAM_RAW);
+    // Raw SCSS to include before the content.
+    $setting = new admin_setting_configtextarea('theme_boost/scsspre',
+        get_string('rawscsspre', 'theme_boost'), get_string('rawscsspre_desc', 'theme_boost'), '', PARAM_RAW);
     $setting->set_updatedcallback('theme_reset_all_caches');
     $page->add($setting);
 
-    // Raw SCSS for after the content.
+    // Raw SCSS to include after the content.
     $setting = new admin_setting_configtextarea('theme_boost/scss', get_string('rawscss', 'theme_boost'),
         get_string('rawscss_desc', 'theme_boost'), '', PARAM_RAW);
     $setting->set_updatedcallback('theme_reset_all_caches');
index 4a32d8f..2405f5f 100644 (file)
@@ -1,5 +1,5 @@
 <div>
 {{#links}}
-    <a class="sr-only sr-only-focusable" href="{{{url}}}">{{{text}}}</a>
+    <a class="sr-only sr-only-focusable" href="#{{{url}}}">{{{text}}}</a>
 {{/links}}
 </div>
\ No newline at end of file
index 08efee6..8cca61c 100644 (file)
@@ -1132,6 +1132,8 @@ tr.flagged-tag a {
             display: inline;
             float: none;
             clear: none;
+            width: auto;
+            margin: 0;
         }
         select,
         .ftext input {
index 87b966f..9b8d4e6 100644 (file)
     }
 
     &.unread {
+        margin: 0;
         background-color: #f4f4f4;
 
         &:hover {
index 9d646f0..4c33354 100644 (file)
         text-align: right;
     }
 }
+
+/**
+ * This rule has been added to duplicate the style of icons with the
+ * .iconsmall class for consistent rendering.
+ *
+ * Loading a pix icon using the template helper returns icons with
+ * the class 'smallicon' instead of 'iconsmall'.
+ */
+#page-user-profile {
+    .ajax-contact-button {
+        img {
+            &.smallicon {
+                margin: 0;
+                padding: 0.3em;
+                height: 12px;
+                width: 12px;
+                vertical-align: middle;
+            }
+        }
+    }
+}
+
 .ajax-contact-button {
-    height: 31px;
     box-sizing: border-box;
+    position: relative;
 
     &.loading {
-        > *:not(.loading-icon) {
-            display: none;
-        }
         .loading-icon {
             display: block;
         }
     }
+
     .loading-icon {
         display: none;
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        background-color: rgba(255, 255, 255, 0.7);
+
+        .smallicon {
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%, -50%);
+        }
     }
 }
 
index 76a4b08..68f885e 100644 (file)
@@ -1109,6 +1109,8 @@ tr.flagged-tag a {
   display: inline;
   float: none;
   clear: none;
+  width: auto;
+  margin: 0;
 }
 #page-enrol-users #filterform select,
 #page-enrol-users #filterform .ftext input {
@@ -7448,18 +7450,41 @@ body.path-question-type .mform fieldset.hidden {
 .path-user .node_category .viewmore {
   text-align: right;
 }
+/**
+ * This rule has been added to duplicate the style of icons with the
+ * .iconsmall class for consistent rendering.
+ *
+ * Loading a pix icon using the template helper returns icons with
+ * the class 'smallicon' instead of 'iconsmall'.
+ */
+#page-user-profile .ajax-contact-button img.smallicon {
+  margin: 0;
+  padding: 0.3em;
+  height: 12px;
+  width: 12px;
+  vertical-align: middle;
+}
 .ajax-contact-button {
-  height: 31px;
   box-sizing: border-box;
-}
-.ajax-contact-button.loading > *:not(.loading-icon) {
-  display: none;
+  position: relative;
 }
 .ajax-contact-button.loading .loading-icon {
   display: block;
 }
 .ajax-contact-button .loading-icon {
   display: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(255, 255, 255, 0.7);
+}
+.ajax-contact-button .loading-icon .smallicon {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
 }
 @media (max-width: 480px) {
   .userprofile .profile_tree {
@@ -7866,6 +7891,7 @@ body.path-question-type .mform fieldset.hidden {
   color: #fff;
 }
 .content-item-container.unread {
+  margin: 0;
   background-color: #f4f4f4;
 }
 .content-item-container.unread:hover {
index 79fa5dc..efb429b 100644 (file)
@@ -37,4 +37,4 @@ img.small-logo {
 
 /* Custom CSS Settings
 -------------------------*/
-[[setting:customcss]] /* stylelint-disable-line */
+[[setting:customcss]]
index 79fa5dc..efb429b 100644 (file)
@@ -37,4 +37,4 @@ img.small-logo {
 
 /* Custom CSS Settings
 -------------------------*/
-[[setting:customcss]] /* stylelint-disable-line */
+[[setting:customcss]]
index ed20d7e..e8848cb 100644 (file)
@@ -17,6 +17,8 @@ Removed themes:
 * Bootstrap 4 was added as part of a the new theme 'boost'.
 * Themes can now automatically compile SCSS on the fly. This works the same way as it
   does compiling LESS on the fly, effecitvely adding $THEME->scssfile to your config.
+* Two new callbacks allow themes to inject SCSS code before and after the content provided
+  by the SCSS file $THEME->scssfile. See $THEME->prescsscallback and $THEME->extrascsscallback.
 * Using .dir-rtl for RTL styling is deprecated and should not be used any more. From now
   the styles are automatically flipped when the language is right-to-left. However,
   as this is not always perfect, you can define exceptions. Please refer to the documentation
index 2d2ddd6..5bc3bf0 100644 (file)
@@ -452,6 +452,8 @@ class core_user_external extends external_api {
                             'auth' =>
                                 new external_value(core_user::get_property_type('auth'), 'Auth plugins include manual, ldap, imap, etc', VALUE_OPTIONAL, '',
                                     NULL_NOT_ALLOWED),
+                            'suspended' =>
+                                new external_value(core_user::get_property_type('suspended'), 'Suspend user account, either false to enable user login or true to disable it', VALUE_OPTIONAL),
                             'idnumber' =>
                                 new external_value(core_user::get_property_type('idnumber'), 'An arbitrary ID code number perhaps from the institution',
                                     VALUE_OPTIONAL),
@@ -572,6 +574,9 @@ class core_user_external extends external_api {
                     set_user_preference($preference['type'], $preference['value'], $user['id']);
                 }
             }
+            if (isset($user['suspended']) and $user['suspended']) {
+                \core\session\manager::kill_user_sessions($user['id']);
+            }
         }
 
         $transaction->allow_commit();
@@ -1016,6 +1021,7 @@ class core_user_external extends external_api {
             'firstaccess' => new external_value(core_user::get_property_type('firstaccess'), 'first access to the site (0 if never)', VALUE_OPTIONAL),
             'lastaccess'  => new external_value(core_user::get_property_type('lastaccess'), 'last access to the site (0 if never)', VALUE_OPTIONAL),
             'auth'        => new external_value(core_user::get_property_type('auth'), 'Auth plugins include manual, ldap, imap, etc', VALUE_OPTIONAL),
+            'suspended'   => new external_value(core_user::get_property_type('suspended'), 'Suspend user account, either false to enable user login or true to disable it', VALUE_OPTIONAL),
             'confirmed'   => new external_value(core_user::get_property_type('confirmed'), 'Active user: 1 if confirmed, 0 otherwise', VALUE_OPTIONAL),
             'lang'        => new external_value(core_user::get_property_type('lang'), 'Language code such as "en", must exist on server', VALUE_OPTIONAL),
             'calendartype' => new external_value(core_user::get_property_type('calendartype'), 'Calendar type such as "gregorian", must exist on server', VALUE_OPTIONAL),
index 910bf5c..c720d04 100644 (file)
@@ -231,7 +231,7 @@ function user_get_default_fields() {
         'institution', 'interests', 'firstaccess', 'lastaccess', 'auth', 'confirmed',
         'idnumber', 'lang', 'theme', 'timezone', 'mailformat', 'description', 'descriptionformat',
         'city', 'url', 'country', 'profileimageurlsmall', 'profileimageurl', 'customfields',
-        'groups', 'roles', 'preferences', 'enrolledcourses'
+        'groups', 'roles', 'preferences', 'enrolledcourses', 'suspended'
     );
 }
 
@@ -443,6 +443,9 @@ function user_get_user_details($user, $course = null, array $userfields = array(
     if (in_array('msn', $userfields) && $user->msn && (!isset($hiddenfields['msnid']) or $isadmin)) {
         $userdetails['msn'] = $user->msn;
     }
+    if (in_array('suspended', $userfields) && (!isset($hiddenfields['suspended']) or $isadmin)) {
+        $userdetails['suspended'] = (bool)$user->suspended;
+    }
 
     if (in_array('firstaccess', $userfields) && (!isset($hiddenfields['firstaccess']) or $isadmin)) {
         if ($user->firstaccess) {
index 72aebbb..a293dac 100644 (file)
@@ -575,4 +575,52 @@ class core_userliblib_testcase extends advanced_testcase {
 
         $CFG->coursecontact = null;
     }
+
+    /**
+     * Test user_get_user_details
+     */
+    public function test_user_get_user_details() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create user and modify user profile.
+        $teacher = $this->getDataGenerator()->create_user();
+        $student = $this->getDataGenerator()->create_user();
+        $studentfullname = fullname($student);
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course1->id);
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($teacher->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($student->id, $course1->id);
+        role_assign($teacherrole->id, $teacher->id, $coursecontext->id);
+        role_assign($studentrole->id, $student->id, $coursecontext->id);
+
+        accesslib_clear_all_caches_for_unit_testing();
+
+        // Get student details as a user with super system capabilities.
+        $result = user_get_user_details($student, $course1);
+        $this->assertEquals($student->id, $result['id']);
+        $this->assertEquals($studentfullname, $result['fullname']);
+        $this->assertEquals($course1->id, $result['enrolledcourses'][0]['id']);
+
+        $this->setUser($teacher);
+        // Get student details as a user who can only see this user in a course.
+        $result = user_get_user_details($student, $course1);
+        $this->assertEquals($student->id, $result['id']);
+        $this->assertEquals($studentfullname, $result['fullname']);
+        $this->assertEquals($course1->id, $result['enrolledcourses'][0]['id']);
+
+        // Get student details with required fields.
+        $result = user_get_user_details($student, $course1, array('id', 'fullname'));
+        $this->assertCount(2, $result);
+        $this->assertEquals($student->id, $result['id']);
+        $this->assertEquals($studentfullname, $result['fullname']);
+
+        // Get exception for invalid required fields.
+        $this->expectException('moodle_exception');
+        $result = user_get_user_details($student, $course1, array('wrongrequiredfield'));
+    }
 }
index ae0d05d..e250409 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016101400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016101401.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.