Merge branch 'MDL-35788-master' of git://github.com/andrewnicols/moodle
authorJun Pataleta <jun@moodle.com>
Thu, 4 Oct 2018 04:45:09 +0000 (12:45 +0800)
committerJun Pataleta <jun@moodle.com>
Thu, 4 Oct 2018 04:45:09 +0000 (12:45 +0800)
84 files changed:
admin/tool/dataprivacy/classes/metadata_registry.php
admin/tool/dataprivacy/classes/output/renderer.php
admin/tool/dataprivacy/classes/output/summary_page.php [new file with mode: 0644]
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/lib.php
admin/tool/dataprivacy/summary.php [new file with mode: 0644]
admin/tool/dataprivacy/templates/summary.mustache [new file with mode: 0644]
auth/classes/output/login.php
blocks/timeline/tests/behat/block_timeline_dates.feature
grade/edit/tree/lib.php
lang/en/deprecated.txt
lang/en/message.php
lang/en/moodle.php
lib/badgeslib.php
lib/behat/behat_base.php
lib/classes/event/message_contact_blocked.php
lib/classes/event/message_contact_unblocked.php
lib/classes/event/message_user_blocked.php [new file with mode: 0644]
lib/classes/event/message_user_unblocked.php [new file with mode: 0644]
lib/db/install.xml
lib/db/messages.php
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/outputrenderers.php
lib/upgrade.txt
message/amd/build/message_area_profile.min.js
message/amd/src/message_area_profile.js
message/classes/api.php
message/classes/output/messagearea/messages.php
message/classes/privacy/provider.php
message/externallib.php
message/lib.php
message/pendingcontactrequests.php [new file with mode: 0644]
message/tests/api_test.php
message/tests/behat/manage_contacts.feature
message/tests/events_test.php
message/tests/externallib_test.php
message/tests/messagelib_test.php
message/tests/privacy_provider_test.php
message/upgrade.txt
mod/assign/locallib.php
mod/assign/tests/locallib_test.php
mod/imscp/lib.php
mod/imscp/tests/lib_test.php
mod/label/lib.php
mod/label/tests/lib_test.php
mod/lesson/lib.php
mod/lesson/tests/lib_test.php
mod/quiz/attemptlib.php
mod/quiz/locallib.php
mod/quiz/report/grading/tests/behat/grading.feature
mod/quiz/report/overview/tests/behat/basic.feature
mod/quiz/report/reportlib.php
mod/quiz/report/responses/tests/behat/basic.feature
mod/quiz/report/statistics/lang/en/quiz_statistics.php
mod/quiz/report/statistics/statistics_table.php
mod/quiz/report/statistics/tests/behat/basic.feature [new file with mode: 0644]
mod/quiz/report/statistics/tests/statistics_table_test.php [new file with mode: 0644]
mod/quiz/tests/behat/attempt_basic.feature
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/behat/preview.feature
mod/quiz/tests/behat/quiz_reset.feature
mod/quiz/tests/generator/lib.php
mod/quiz/tests/reportlib_test.php
mod/workshop/form/numerrors/edit_form.php
mod/workshop/form/numerrors/lang/en/workshopform_numerrors.php
question/behaviour/interactive/behaviour.php
question/classes/statistics/questions/all_calculated_for_qubaid_condition.php
question/classes/statistics/questions/calculated_question_summary.php [new file with mode: 0644]
question/tests/calculated_question_summary_test.php [new file with mode: 0644]
question/tests/generator/lib.php
question/type/essay/question.php
question/type/missingtype/question.php
question/type/numerical/question.php
question/type/questionbase.php
question/type/shortanswer/question.php
question/type/truefalse/question.php
repository/dropbox/pix/icon.png
repository/dropbox/pix/icon.svg [new file with mode: 0644]
theme/boost/classes/output/core_renderer.php
version.php

index e29174a..96c9e55 100644 (file)
@@ -80,9 +80,11 @@ class metadata_registry {
                 // Check if the interface is deprecated.
                 if (!$manager->is_empty_subsystem($component)) {
                     $classname = $manager->get_provider_classname_for_component($component);
-                    $componentclass = new $classname();
-                    if ($componentclass instanceof \core_privacy\local\deprecated) {
-                        $internaldata['deprecated'] = true;
+                    if (class_exists($classname)) {
+                        $componentclass = new $classname();
+                        if ($componentclass instanceof \core_privacy\local\deprecated) {
+                            $internaldata['deprecated'] = true;
+                        }
                     }
                 }
 
index 09bcb1c..341bc8a 100644 (file)
@@ -139,4 +139,15 @@ class renderer extends plugin_renderer_base {
         $data = $page->export_for_template($this);
         return parent::render_from_template('tool_dataprivacy/data_deletion', $data);
     }
+
+    /**
+     * Render the user data retention summary page.
+     *
+     * @param  summary_page $page
+     * @return string html for the page.
+     */
+    public function render_summary_page(summary_page $page) {
+        $data = $page->export_for_template($this);
+        return parent::render_from_template('tool_dataprivacy/summary', $data);
+    }
 }
diff --git a/admin/tool/dataprivacy/classes/output/summary_page.php b/admin/tool/dataprivacy/classes/output/summary_page.php
new file mode 100644 (file)
index 0000000..4b93b82
--- /dev/null
@@ -0,0 +1,132 @@
+<?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/>.
+
+/**
+ * Summary page renderable.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Adrian Greeve
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use templatable;
+
+
+/**
+ * Class containing the summary page renderable.
+ *
+ * @copyright  2018 Adrian Greeve
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class summary_page implements renderable, templatable {
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     * @return array
+     */
+    public function export_for_template(renderer_base $output) {
+        $contextlevels = [
+            'contextlevelname10' => CONTEXT_SYSTEM,
+            'contextlevelname30' => CONTEXT_USER,
+            'contextlevelname40' => CONTEXT_COURSECAT,
+            'contextlevelname50' => CONTEXT_COURSE,
+            'contextlevelname70' => CONTEXT_MODULE,
+            'contextlevelname80' => CONTEXT_BLOCK
+        ];
+
+        $data = [];
+        $context = \context_system::instance();
+
+        foreach ($contextlevels as $levelname => $level) {
+            $classname = \context_helper::get_class_for_level($level);
+            list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
+            $purposeid = get_config('tool_dataprivacy', $purposevar);
+            $categoryid = get_config('tool_dataprivacy', $categoryvar);
+
+            $section = [];
+            $section['contextname'] = get_string($levelname, 'tool_dataprivacy');
+
+            if (empty($purposeid)) {
+                list($purposeid, $categoryid) =
+                        \tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level);
+            }
+            if ($purposeid == -1) {
+                $purposeid = 0;
+            }
+            $purpose = new \tool_dataprivacy\purpose($purposeid);
+            $export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]);
+            $purposedata = $export->export($output);
+            $section['purpose'] = $purposedata;
+
+            if (empty($categoryid)) {
+                list($purposeid, $categoryid) =
+                        \tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level);
+            }
+            if ($categoryid == -1) {
+                $categoryid = 0;
+            }
+            $category = new \tool_dataprivacy\category($categoryid);
+            $export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]);
+            $categorydata = $export->export($output);
+            $section['category'] = $categorydata;
+            $data['contexts'][] = $section;
+        }
+
+        // Get activity module plugin info.
+        $pluginmanager = \core_plugin_manager::instance();
+        $modplugins = $pluginmanager->get_enabled_plugins('mod');
+
+        foreach ($modplugins as $name) {
+            $classname = \context_helper::get_class_for_level($contextlevels['contextlevelname70']);
+            list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name);
+            $categoryid = get_config('tool_dataprivacy', $categoryvar);
+            $purposeid = get_config('tool_dataprivacy', $purposevar);
+            if ($categoryid === false && $purposeid === false) {
+                // If no purpose and category has been set for this plugin, then there's no need to show this on the list.
+                continue;
+            }
+
+            $section = [];
+            $section['contextname'] = $pluginmanager->plugin_name('mod_' . $name);
+
+            if ($purposeid == -1) {
+                $purposeid = 0;
+            }
+            $purpose = new \tool_dataprivacy\purpose($purposeid);
+            $export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]);
+            $purposedata = $export->export($output);
+            $section['purpose'] = $purposedata;
+
+            if ($categoryid == -1) {
+                $categoryid = 0;
+            }
+            $category = new \tool_dataprivacy\category($categoryid);
+            $export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]);
+            $categorydata = $export->export($output);
+            $section['category'] = $categorydata;
+
+            $data['contexts'][] = $section;
+        }
+
+        return $data;
+    }
+}
index 7b8aa51..5ab4dfd 100644 (file)
@@ -76,6 +76,8 @@ $string['dataprivacy:downloadownrequest'] = 'Download your own exported data';
 $string['dataprivacy:downloadallrequests'] = 'Download exported data for everyone';
 $string['dataregistry'] = 'Data registry';
 $string['dataregistryinfo'] = 'The data registry enables categories (types of data) and purposes (the reasons for processing data) to be set for all content on the site - from users and courses down to activities and blocks. For each purpose, a retention period may be set. When a retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
+$string['dataretentionexplanation'] = 'This summary shows the default categories and purposes for retaining user\'s information on this system. Certain areas of the system may have more specific categories and purposes than those listed here.';
+$string['dataretentionsummary'] = 'Data retention summary';
 $string['datarequestcreatedforuser'] = 'Data request created for {$a}';
 $string['datarequestemailsubject'] = 'Data request: {$a}';
 $string['datarequests'] = 'Data requests';
@@ -276,6 +278,7 @@ $string['statuspending'] = 'Pending';
 $string['statusrejected'] = 'Rejected';
 $string['subjectscope'] = 'Subject scope';
 $string['subjectscope_help'] = 'The subject scope lists the roles which may be assigned in this context.';
+$string['summary'] = 'Registry configuration summary';
 $string['user'] = 'User';
 $string['viewrequest'] = 'View the request';
 $string['visible'] = 'Expand all';
index fbeb61d..a9712d1 100644 (file)
@@ -65,6 +65,11 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
         $category->add_node($node);
     }
 
+    $summaryurl = new moodle_url('/admin/tool/dataprivacy/summary.php');
+    $summarynode = new core_user\output\myprofile\node('privacyandpolicies', 'retentionsummary',
+            get_string('dataretentionsummary', 'tool_dataprivacy'), null, $summaryurl);
+    $category->add_node($summarynode);
+
     // Add the Privacy category to the tree if it's not empty and it doesn't exist.
     $nodes = $category->nodes;
     if (!empty($nodes)) {
@@ -77,6 +82,20 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
     return false;
 }
 
+/**
+ * Callback to add footer elements.
+ *
+ * @return string HTML footer content
+ */
+function tool_dataprivacy_standard_footer_html() {
+
+    $url = new moodle_url('/admin/tool/dataprivacy/summary.php');
+    $output = html_writer::link($url, get_string('dataretentionsummary', 'tool_dataprivacy'));
+    $output = html_writer::div($output, 'summaryfooter');
+
+    return $output;
+}
+
 /**
  * Fragment to add a new purpose.
  *
diff --git a/admin/tool/dataprivacy/summary.php b/admin/tool/dataprivacy/summary.php
new file mode 100644 (file)
index 0000000..3b99ee3
--- /dev/null
@@ -0,0 +1,41 @@
+<?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/>.
+
+/**
+ * Prints the compliance data registry main page.
+ *
+ * @copyright 2018 onwards Adrian Greeve <adriangreeve.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package tool_dataprivacy
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
+
+$url = new moodle_url('/' . $CFG->admin . '/tool/dataprivacy/summary.php');
+$title = get_string('summary', 'tool_dataprivacy');
+
+$context = \context_system::instance();
+$PAGE->set_url($url);
+$PAGE->set_context($context);
+$PAGE->set_title($title);
+$PAGE->set_heading($SITE->fullname);
+
+$output = $PAGE->get_renderer('tool_dataprivacy');
+echo $output->header();
+$summarypage = new \tool_dataprivacy\output\summary_page();
+echo $output->render($summarypage);
+echo $output->footer();
diff --git a/admin/tool/dataprivacy/templates/summary.mustache b/admin/tool/dataprivacy/templates/summary.mustache
new file mode 100644 (file)
index 0000000..22c0221
--- /dev/null
@@ -0,0 +1,123 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_dataprivacy/summary
+
+    Summary
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+
+    Context variables required for this template:
+
+    Example context (json):
+    {
+
+        "contexts": [
+            {
+                "contextname": "Site",
+                "category":
+                {
+                    "name": "Test category",
+                    "description": "<p>Description for category</p>"
+                },
+                "purpose":
+                {
+                    "name": "Test purpose",
+                    "description": "<p>Description for purpose</p>",
+                    "lawfulbases": "gdpr_art_6_1_c",
+                    "sensitivedatareasons": "gdpr_art_9_2_f",
+                    "formattedlawfulbases": [
+                        {
+                            "name": "Lawful base 1(a)",
+                            "description": "We need your information"
+                        },
+                        {
+                            "name": "Lawful base 1(b)",
+                            "description": "We really do need your information"
+                        }
+                    ],
+                    "formattedsensitivedatareasons": [
+                        {
+                            "name": "Sensitive data reason number 1",
+                            "description": "Number 1"
+                        },
+                        {
+                            "name": "Sensitive data reason number 1",
+                            "description": "Number 2"
+                        }
+                    ],
+                    "formattedretentionperiod": "10 Years"
+                }
+            }
+        ]
+    }
+}}
+<h2>{{#str}}dataretentionsummary, tool_dataprivacy{{/str}}</h2>
+<p>{{#str}}dataretentionexplanation, tool_dataprivacy{{/str}}</p>
+<div>
+    {{#contexts}}
+        <div class="card mb-3">
+            <div class="card-header"><h3>{{contextname}}</h3></div>
+            <div class="card-body p-l-2 p-r-2">
+
+                {{#category.name}}
+                <h4>{{#str}}category, tool_dataprivacy{{/str}}</h4>
+                <dl>
+                    <dt>{{category.name}}</dt>
+                    <dd>{{{category.description}}}</dd>
+                </dl>
+                <hr />
+                {{/category.name}}
+                <h4>{{#str}}purpose, tool_dataprivacy{{/str}}</h4>
+                <dl>
+                    <dt>{{purpose.name}}</dt>
+                    <dd>{{{purpose.description}}}</dd>
+                    <dt>{{#str}}retentionperiod, tool_dataprivacy{{/str}}</dt>
+                    <dd>{{purpose.formattedretentionperiod}}</dd>
+                </dl>
+                {{#purpose.lawfulbases}}
+                <table class="table table-bordered">
+                    <thead><tr><th colspan="2">{{#str}}lawfulbases, tool_dataprivacy{{/str}}</th></tr></thead>
+                    <tbody>
+                    {{#purpose.formattedlawfulbases}}
+                        <tr>
+                            <td>{{name}}</td>
+                            <td>{{description}}</td>
+                        </tr>
+                    {{/purpose.formattedlawfulbases}}
+                    </tbody>
+                </table>
+                {{/purpose.lawfulbases}}
+                {{#purpose.sensitivedatareasons}}
+                <table class="table table-bordered">
+                    <thead><tr><th colspan="2">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th></tr></thead>
+                    <tbody>
+                    {{#purpose.formattedsensitivedatareasons}}
+                        <tr>
+                            <td>{{name}}</td>
+                            <td>{{description}}</td>
+                        </tr>
+                    {{/purpose.formattedsensitivedatareasons}}
+                    </tbody>
+                </table>
+                {{/purpose.sensitivedatareasons}}
+            </div>
+        </div>
+    {{/contexts}}
+</div>
\ No newline at end of file
index 9ec9c32..973c7d7 100644 (file)
@@ -85,7 +85,11 @@ class login implements renderable, templatable {
         $this->canloginasguest = $CFG->guestloginbutton and !isguestuser();
         $this->canloginbyemail = !empty($CFG->authloginviaemail);
         $this->cansignup = $CFG->registerauth == 'email' || !empty($CFG->registerauth);
-        $this->cookieshelpicon = new help_icon('cookiesenabled', 'core');
+        if ($CFG->rememberusername == 0) {
+            $this->cookieshelpicon = new help_icon('cookiesenabledonlysession', 'core');
+        } else {
+            $this->cookieshelpicon = new help_icon('cookiesenabled', 'core');
+        }
 
         $this->autofocusform = !empty($CFG->loginpageautofocus);
         $this->rememberusername = isset($CFG->rememberusername) and $CFG->rememberusername == 2;
index 6aaf269..22c04ec 100644 (file)
@@ -64,7 +64,7 @@ Feature: The timeline block allows users to see upcoming activities
     And I should see "Test feedback 3 closes" in the "Timeline" "block"
     And I should not see "Test choice 2 closes" in the "Timeline" "block"
     And I should not see "Test feedback 2 closes" in the "Timeline" "block"
-    And I click on "[data-region='paging-bar'] [data-control='next']" "css_element" in the "Timeline" "block"
+    And I click on "[data-region='paging-bar'] [data-control='next'] [data-region='page-link']" "css_element" in the "Timeline" "block"
     And I should see "Test feedback 2 closes" in the "Timeline" "block"
     And I should not see "Test assign 1 is due" in the "Timeline" "block"
     And I should not see "Test feedback 1 closes" in the "Timeline" "block"
index fe7c132..86cf33e 100644 (file)
@@ -113,10 +113,8 @@ class grade_edit_tree {
 
         $object = $element['object'];
         $eid    = $element['eid'];
-        $object->name = format_string($this->gtree->get_element_header($element, true, true, true, true, true),
-            false, array("context" => context_course::instance($COURSE->id)));
+        $object->name = $this->gtree->get_element_header($element, true, true, true, true, true);
         $object->stripped_name = $this->gtree->get_element_header($element, false, false, false);
-
         $is_category_item = false;
         if ($element['type'] == 'categoryitem' || $element['type'] == 'courseitem') {
             $is_category_item = true;
index 5a3e194..873ad79 100644 (file)
@@ -135,3 +135,5 @@ keepsearching,core
 allfieldsrequired,core
 previewhtml,core
 messagedselecteduserfailed,core
+eventmessagecontactblocked,core_message
+eventmessagecontactunblocked,core_message
index 4517a30..41fb26e 100644 (file)
@@ -28,9 +28,11 @@ $string['ago'] = '{$a} ago';
 $string['allusers'] = 'All messages from all users';
 $string['backupmessageshelp'] = 'If enabled, then instant messages will be included in SITE automated backups';
 $string['blockcontact'] = 'Block contact';
+$string['blockedusers'] = 'Blocked users';
 $string['blocknoncontacts'] = 'Prevent non-contacts from messaging me';
 $string['canceledit'] = 'Cancel editing messages';
 $string['contactblocked'] = 'Contact blocked';
+$string['contactrequests'] = 'Contact requests';
 $string['contacts'] = 'Contacts';
 $string['defaultmessageoutputs'] = 'Default message outputs';
 $string['defaults'] = 'Defaults';
@@ -48,10 +50,10 @@ $string['errortranslatingdefault'] = 'Error translating default setting provided
 $string['eventnotificationviewed'] = 'Notification viewed';
 $string['eventnotificationsent'] = 'Notification sent';
 $string['eventmessagecontactadded'] = 'Message contact added';
-$string['eventmessagecontactblocked'] = 'Message contact blocked';
 $string['eventmessagecontactremoved'] = 'Message contact removed';
-$string['eventmessagecontactunblocked'] = 'Message contact unblocked';
 $string['eventmessagedeleted'] = 'Message deleted';
+$string['eventmessageuserblocked'] = 'User blocked';
+$string['eventmessageuserunblocked'] = 'User unblocked';
 $string['eventmessageviewed'] = 'Message viewed';
 $string['eventmessagesent'] = 'Message sent';
 $string['forced'] = 'Locked';
@@ -69,6 +71,8 @@ $string['managemessageoutputs'] = 'Manage message outputs';
 $string['messageoutputs'] = 'Message outputs';
 $string['messagepreferences'] = 'Message preferences';
 $string['message'] = 'Message';
+$string['messagecontactrequestsnotification'] = '{$a} wants to be added as a contact';
+$string['messagecontactrequestsnotificationsubject'] = '{$a} wants to be added as a contact';
 $string['messagepreferences'] = 'Message preferences';
 $string['messages'] = 'Messages';
 $string['messagingdatahasnotbeenmigrated'] = 'Your messages are temporarily unavailable due to upgrades in the messaging infrastructure. Please wait for them to be migrated.';
@@ -107,9 +111,13 @@ $string['privacy:metadata:messages:smallmessage'] = 'A small version of the mess
 $string['privacy:metadata:messages:subject'] = 'The subject of the message';
 $string['privacy:metadata:messages:timecreated'] = 'The time when the message was created';
 $string['privacy:metadata:message_contacts'] = 'The list of contacts';
-$string['privacy:metadata:message_contacts:blocked'] = 'Flag whether or not the user is blocked';
 $string['privacy:metadata:message_contacts:contactid'] = 'The ID of the user who is a contact';
+$string['privacy:metadata:message_contacts:timecreated'] = 'The time when the contact was created';
 $string['privacy:metadata:message_contacts:userid'] = 'The ID of the user whose contact list we are viewing';
+$string['privacy:metadata:message_contact_requests'] = 'The list of contact requests';
+$string['privacy:metadata:message_contact_requests:requesteduserid'] = 'The ID of the user who received the contact request';
+$string['privacy:metadata:message_contact_requests:timecreated'] = 'The time when the contact request was created';
+$string['privacy:metadata:message_contact_requests:userid'] = 'The ID of the user who sent the contact request';
 $string['privacy:metadata:message_conversation_members'] = 'The list of users in a conversation';
 $string['privacy:metadata:message_conversation_members:conversationid'] = 'The ID of the conversation';
 $string['privacy:metadata:message_conversation_members:timecreated'] = 'The time when the member was created';
@@ -119,6 +127,10 @@ $string['privacy:metadata:message_user_actions:action'] = 'The action that was p
 $string['privacy:metadata:message_user_actions:messageid'] = 'The ID of the message this action belongs to';
 $string['privacy:metadata:message_user_actions:timecreated'] = 'The time when the action was created';
 $string['privacy:metadata:message_user_actions:userid'] = 'The ID of the user who performed this action';
+$string['privacy:metadata:message_users_blocked'] = 'The list of blocked users';
+$string['privacy:metadata:message_users_blocked:blockeduserid'] = 'The ID of the user who was blocked';
+$string['privacy:metadata:message_users_blocked:timecreated'] = 'The time when the blocked action was created';
+$string['privacy:metadata:message_users_blocked:userid'] = 'The ID of the user who performed the blocked action';
 $string['privacy:metadata:notifications'] = 'Notifications';
 $string['privacy:metadata:notifications:component'] = 'The component responsible for sending the notification';
 $string['privacy:metadata:notifications:contexturl'] = 'The URL related to this notification';
@@ -180,4 +192,6 @@ $string['writeamessage'] = 'Write a message...';
 $string['you'] = 'You:';
 
 // Deprecated since Moodle 3.6.
+$string['eventmessagecontactblocked'] = 'Message contact blocked';
+$string['eventmessagecontactunblocked'] = 'Message contact unblocked';
 $string['messagingdisabled'] = 'Messaging is disabled on this site, emails will be sent instead';
index eb72e18..ee02e87 100644 (file)
@@ -276,6 +276,8 @@ $string['cookiesenabled_help'] = 'Two cookies are used by this site:
 The essential one is the session cookie, usually called MoodleSession. You must allow this cookie into your browser to provide continuity and maintain your login from page to page. When you log out or close the browser this cookie is destroyed (in your browser and on the server).
 
 The other cookie is purely for convenience, usually called something like MOODLEID. It just remembers your username within the browser. This means when you return to this site the username field on the login page will be already filled out for you. It is safe to refuse this cookie - you will just have to retype your username every time you log in.';
+$string['cookiesenabledonlysession'] = 'Cookies must be enabled in your browser';
+$string['cookiesenabledonlysession_help'] = 'This site uses one session cookie, usually called MoodleSession. You must allow this cookie into your browser to provide continuity and maintain your login from page to page. When you log out or close the browser this cookie is destroyed (in your browser and on the server).';
 $string['cookiesnotenabled'] = 'Unfortunately, cookies are currently not enabled in your browser';
 $string['copy'] = 'copy';
 $string['copyasnoun'] = 'copy';
@@ -1194,6 +1196,7 @@ $string['messageprovider:courserequested'] = 'Course creation request notificati
 $string['messageprovider:courserequestrejected'] = 'Course creation request rejection notification';
 $string['messageprovider:errors'] = 'Important errors with the site';
 $string['messageprovider:errors_help'] = 'These are important errors that an administrator should know about.';
+$string['messageprovider:messagecontactrequests'] = 'Message contact requests notification';
 $string['messageprovider:notices'] = 'Notices about minor problems';
 $string['messageprovider:notices_help'] = 'These are notices that an administrator might be interested in seeing.';
 $string['messageprovider:insights'] = 'Insights generated by prediction models';
@@ -1512,6 +1515,7 @@ $string['payments'] = 'Payments';
 $string['paymentsorry'] = 'Thank you for your payment!  Unfortunately your payment has not yet been fully processed, and you are not yet registered to enter the course "{$a->fullname}".  Please try continuing to the course in a few seconds, but if you continue to have trouble then please alert the {$a->teacher} or the site administrator';
 $string['paymentthanks'] = 'Thank you for your payment!  You are now enrolled in your course:<br />"{$a}"';
 $string['pendingrequests'] = 'Pending requests';
+$string['percents'] = '{$a} %';
 $string['periodending'] = 'Period ending ({$a})';
 $string['perpage'] = 'Per page';
 $string['perpagea'] = 'Per page: {$a}';
index ddbdbfb..9c1e698 100644 (file)
@@ -1054,7 +1054,7 @@ function badges_bake($hash, $badgeid, $userid = 0, $pathhash = false) {
 
     $fs = get_file_storage();
     if (!$fs->file_exists($user_context->id, 'badges', 'userbadge', $badge->id, '/', $hash . '.png')) {
-        if ($file = $fs->get_file($badge_context->id, 'badges', 'badgeimage', $badge->id, '/', 'f1.png')) {
+        if ($file = $fs->get_file($badge_context->id, 'badges', 'badgeimage', $badge->id, '/', 'f3.png')) {
             $contents = $file->get_content();
 
             $filehandler = new PNG_MetaDataHandler($contents);
index e7c631f..f1daac0 100644 (file)
@@ -946,6 +946,34 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         return $DB->get_record('user', ['id' => $userid]);
     }
 
+    /**
+     * Set current $USER, reset access cache.
+     *
+     * In some cases, behat will execute the code as admin but in many cases we need to set an specific user as some
+     * API's might rely on the logged user to take some action.
+     *
+     * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
+     */
+    public static function set_user($user = null) {
+        global $DB;
+
+        if (is_object($user)) {
+            $user = clone($user);
+        } else if (!$user) {
+            // Assign valid data to admin user (some generator-related code needs a valid user).
+            $user = $DB->get_record('user', array('username' => 'admin'));
+        } else {
+            $user = $DB->get_record('user', array('id' => $user));
+        }
+        unset($user->description);
+        unset($user->access);
+        unset($user->preference);
+
+        // Ensure session is empty, as it may contain caches and user specific info.
+        \core\session\manager::init_empty_session();
+
+        \core\session\manager::set_user($user);
+    }
     /**
      * Trigger click on node via javascript instead of actually clicking on it via pointer.
      *
index da9fc4c..1c207a3 100644 (file)
@@ -26,6 +26,9 @@ namespace core\event;
 
 defined('MOODLE_INTERNAL') || die();
 
+debugging('core\\event\\message_contact_blocked has been deprecated. Please use
+        core\\event\\message_user_blocked instead', DEBUG_DEVELOPER);
+
 /**
  * Message contact blocked event class.
  *
@@ -99,4 +102,13 @@ class message_contact_blocked extends base {
         // Messaging contacts are not backed up, so no need to map them on restore.
         return array('db' => 'message_contacts', 'restore' => base::NOT_MAPPED);
     }
+
+    /**
+     * This event has been deprecated.
+     *
+     * @return boolean
+     */
+    public static function is_deprecated() {
+        return true;
+    }
 }
index 417a1a6..61f83e6 100644 (file)
@@ -26,6 +26,9 @@ namespace core\event;
 
 defined('MOODLE_INTERNAL') || die();
 
+debugging('core\\event\\message_contact_unblocked has been deperecated. Please use
+        core\\event\\message_user_unblocked instead', DEBUG_DEVELOPER);
+
 /**
  * Message contact unblocked event class.
  *
@@ -99,4 +102,13 @@ class message_contact_unblocked extends base {
         // Messaging contacts are not backed up, so no need to map them on restore.
         return array('db' => 'message_contacts', 'restore' => base::NOT_MAPPED);
     }
+
+    /**
+     * This event has been deprecated.
+     *
+     * @return boolean
+     */
+    public static function is_deprecated() {
+        return true;
+    }
 }
diff --git a/lib/classes/event/message_user_blocked.php b/lib/classes/event/message_user_blocked.php
new file mode 100644 (file)
index 0000000..cd6abbf
--- /dev/null
@@ -0,0 +1,83 @@
+<?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/>.
+
+/**
+ * Message user blocked event.
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Message user blocked event class.
+ *
+ * @package    core
+ * @since      Moodle 3.6
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class message_user_blocked extends base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'message_users_blocked';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventmessageusersblocked', 'message');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' blocked the user with id '$this->relateduserid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        // Blocked users are not backed up, so no need to map them on restore.
+        return array('db' => 'message_users_blocked', 'restore' => base::NOT_MAPPED);
+    }
+}
diff --git a/lib/classes/event/message_user_unblocked.php b/lib/classes/event/message_user_unblocked.php
new file mode 100644 (file)
index 0000000..2463c80
--- /dev/null
@@ -0,0 +1,83 @@
+<?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/>.
+
+/**
+ * Message users unblocked event.
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Message users unblocked event class.
+ *
+ * @package    core
+ * @since      Moodle 3.6
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class message_user_unblocked extends base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'message_users_blocked';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventmessageusersunblocked', 'message');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' unblocked the user with id '$this->relateduserid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        // Messaging contacts are not backed up, so no need to map them on restore.
+        return array('db' => 'message_users_blocked', 'restore' => base::NOT_MAPPED);
+    }
+}
index 2101c71..455b29c 100644 (file)
         <KEY NAME="useridto" TYPE="foreign" FIELDS="useridto" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
     </TABLE>
-    <TABLE NAME="message_contacts" COMMENT="Maintains lists of relationships between users">
+    <TABLE NAME="message_contacts" COMMENT="Maintains lists of contacts between users">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
-        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="contactid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="blocked" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="contactid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="contactid" TYPE="foreign" FIELDS="contactid" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
         <INDEX NAME="userid-contactid" UNIQUE="true" FIELDS="userid, contactid"/>
       </INDEXES>
     </TABLE>
+    <TABLE NAME="message_contact_requests" COMMENT="Maintains list of contact requests between users">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="requesteduserid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="requesteduserid" TYPE="foreign" FIELDS="requesteduserid" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="userid-requesteduserid" UNIQUE="true" FIELDS="userid, requesteduserid"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="message_users_blocked" COMMENT="Maintains lists of blocked users">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="blockeduserid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="blockeduserid" TYPE="foreign" FIELDS="blockeduserid" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="userid-blockeduserid" UNIQUE="true" FIELDS="userid, blockeduserid"/>
+      </INDEXES>
+    </TABLE>
     <TABLE NAME="modules" COMMENT="modules available in the site">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
index 68ac95f..7a08841 100644 (file)
@@ -105,5 +105,13 @@ $messageproviders = array (
     // User insights.
     'insights' => array (
          'capability'  => 'moodle/analytics:listinsights'
-    )
+    ),
+
+    // Message contact requests.
+    'messagecontactrequests' => [
+        'defaults' => [
+            'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
+            'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF,
+        ]
+    ],
 );
index 08195cc..2334240 100644 (file)
@@ -821,11 +821,21 @@ $functions = array(
         'description' => 'Updates existing groupings',
         'type' => 'write',
     ),
+    'core_message_block_user' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'block_user',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Blocks a user',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
     'core_message_block_contacts' => array(
         'classname' => 'core_message_external',
         'methodname' => 'block_contacts',
         'classpath' => 'message/externallib.php',
-        'description' => 'Block contacts',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Block contacts',
         'type' => 'write',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
@@ -834,11 +844,47 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'create_contacts',
         'classpath' => 'message/externallib.php',
-        'description' => 'Add contacts to the contact list',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Add contacts to the contact list',
         'type' => 'write',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+    'core_message_get_contact_requests' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'get_contact_requests',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Returns contact requests for a user',
+        'type' => 'read',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+    'core_message_create_contact_request' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'create_contact_request',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Creates a contact request',
+        'type' => 'write',
         'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+    'core_message_confirm_contact_request' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'confirm_contact_request',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Confirms a contact request',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+    'core_message_decline_contact_request' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'decline_contact_request',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Declines a contact request',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_message_delete_contacts' => array(
         'classname' => 'core_message_external',
@@ -848,7 +894,6 @@ $functions = array(
         'type' => 'write',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
-        'ajax' => true,
     ),
     'core_message_delete_conversation' => array(
         'classname' => 'core_message_external',
@@ -1043,11 +1088,21 @@ $functions = array(
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'core_message_unblock_user' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'unblock_user',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Unblocks a user',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
     'core_message_unblock_contacts' => array(
         'classname' => 'core_message_external',
         'methodname' => 'unblock_contacts',
         'classpath' => 'message/externallib.php',
-        'description' => 'Unblock contacts',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                         Unblock contacts',
         'type' => 'write',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
index 7f6a1f1..a6087d3 100644 (file)
@@ -2167,7 +2167,7 @@ function xmldb_main_upgrade($oldversion) {
         $table = new xmldb_table('message_user_actions');
 
         // Conditionally launch add index.
-        $index = new xmldb_index('userid_messageid_action', XMLDB_INDEX_UNIQUE, array('userid, messageid, action'));
+        $index = new xmldb_index('userid_messageid_action', XMLDB_INDEX_UNIQUE, array('userid', 'messageid', 'action'));
         if (!$dbman->index_exists($table, $index)) {
             $dbman->add_index($table, $index);
         }
@@ -2181,7 +2181,7 @@ function xmldb_main_upgrade($oldversion) {
         $table = new xmldb_table('messages');
 
         // Conditionally launch add index.
-        $index = new xmldb_index('conversationid_timecreated', XMLDB_INDEX_NOTUNIQUE, array('conversationidtimecreated'));
+        $index = new xmldb_index('conversationid_timecreated', XMLDB_INDEX_NOTUNIQUE, array('conversationid', 'timecreated'));
         if (!$dbman->index_exists($table, $index)) {
             $dbman->add_index($table, $index);
         }
@@ -2202,7 +2202,7 @@ function xmldb_main_upgrade($oldversion) {
 
     if ($oldversion < 2018040500.01) {
 
-        // Define field indexpriority to be added to search_index_requests. Allow null initially.
+        // Define field cohort to be added to theme. Allow null initially.
         $table = new xmldb_table('cohort');
         $field = new xmldb_field('theme', XMLDB_TYPE_CHAR, '50',
                 null, null, null, null, 'timemodified');
@@ -2324,6 +2324,7 @@ function xmldb_main_upgrade($oldversion) {
                 $DB->delete_records('blog_association', ['contextid' => $module->modcontextid]);
             }
         }
+
         // Main savepoint reached.
         upgrade_main_savepoint(true, 2018083100.01);
     }
@@ -2373,7 +2374,7 @@ function xmldb_main_upgrade($oldversion) {
 
     if ($oldversion < 2018092100.02) {
         $table = new xmldb_table('question');
-        $index = new xmldb_index('categoryidnumber', XMLDB_INDEX_UNIQUE, array('categoryidnumber'));
+        $index = new xmldb_index('categoryidnumber', XMLDB_INDEX_UNIQUE, array('category', 'idnumber'));
         if (!$dbman->index_exists($table, $index)) {
             $dbman->add_index($table, $index);
         }
@@ -2391,7 +2392,7 @@ function xmldb_main_upgrade($oldversion) {
 
     if ($oldversion < 2018092100.04) {
         $table = new xmldb_table('question_categories');
-        $index = new xmldb_index('contextididnumber', XMLDB_INDEX_UNIQUE, array('contextididnumber'));
+        $index = new xmldb_index('contextididnumber', XMLDB_INDEX_UNIQUE, array('contextid', 'idnumber'));
         if (!$dbman->index_exists($table, $index)) {
             $dbman->add_index($table, $index);
         }
@@ -2399,5 +2400,129 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2018092100.04);
     }
 
+    if ($oldversion < 2018092800.00) {
+        // Alter the table 'message_contacts'.
+        $table = new xmldb_table('message_contacts');
+
+        // Remove index so we can alter the fields.
+        $index = new xmldb_index('userid-contactid', XMLDB_INDEX_UNIQUE, ['userid', 'contactid']);
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        // Remove defaults of '0' from the 'userid' and 'contactid' fields.
+        $field = new xmldb_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'id');
+        $dbman->change_field_default($table, $field);
+
+        $field = new xmldb_field('contactid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'userid');
+        $dbman->change_field_default($table, $field);
+
+        // Add the missing FKs that will now be added to new installs.
+        $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
+        $dbman->add_key($table, $key);
+
+        $key = new xmldb_key('contactid', XMLDB_KEY_FOREIGN, ['contactid'], 'user', ['id']);
+        $dbman->add_key($table, $key);
+
+        // Re-add the index.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Add the field 'timecreated'. Allow null, since existing records won't have an accurate value we can use.
+        $field = new xmldb_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'blocked');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Create new 'message_contact_requests' table.
+        $table = new xmldb_table('message_contact_requests');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null);
+        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'id');
+        $table->add_field('requesteduserid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'userid');
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'requesteduserid');
+
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id'], null, null);
+        $table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
+        $table->add_key('requesteduserid', XMLDB_KEY_FOREIGN, ['requesteduserid'], 'user', ['id']);
+
+        $table->add_index('userid-requesteduserid', XMLDB_INDEX_UNIQUE, ['userid', 'requesteduserid']);
+
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Create new 'message_users_blocked' table.
+        $table = new xmldb_table('message_users_blocked');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null);
+        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'id');
+        $table->add_field('blockeduserid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'userid');
+        // Allow NULLs in the 'timecreated' field because we will be moving existing data here that has no timestamp.
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'blockeduserid');
+
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id'], null, null);
+        $table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
+        $table->add_key('blockeduserid', XMLDB_KEY_FOREIGN, ['blockeduserid'], 'user', ['id']);
+
+        $table->add_index('userid-blockeduserid', XMLDB_INDEX_UNIQUE, ['userid', 'blockeduserid']);
+
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        upgrade_main_savepoint(true, 2018092800.00);
+    }
+
+    if ($oldversion < 2018092800.01) {
+        // Move all the 'blocked' contacts to the new table 'message_users_blocked'.
+        $updatesql = "INSERT INTO {message_users_blocked} (userid, blockeduserid, timecreated)
+                           SELECT userid, contactid, null as timecreated
+                             FROM {message_contacts}
+                            WHERE blocked = :blocked";
+        $DB->execute($updatesql, ['blocked' => 1]);
+
+        // Removed the 'blocked' column from 'message_contacts'.
+        $table = new xmldb_table('message_contacts');
+        $field = new xmldb_field('blocked');
+        $dbman->drop_field($table, $field);
+
+        upgrade_main_savepoint(true, 2018092800.01);
+    }
+
+    if ($oldversion < 2018092800.02) {
+        // Delete any contacts that are not mutual (meaning they both haven't added each other).
+        $sql = "SELECT c1.id
+                  FROM {message_contacts} c1
+             LEFT JOIN {message_contacts} c2
+                    ON c1.userid = c2.contactid
+                   AND c1.contactid = c2.userid
+                 WHERE c2.id IS NULL";
+        if ($contacts = $DB->get_records_sql($sql)) {
+            list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contacts));
+            $DB->delete_records_select('message_contacts', "id $insql", $inparams);
+        }
+
+        upgrade_main_savepoint(true, 2018092800.02);
+    }
+
+    if ($oldversion < 2018092800.03) {
+        // Remove any duplicate rows - from now on adding contacts just requires 1 row.
+        // The person who made the contact request (userid) and the person who approved
+        // it (contactid). Upgrade the table so that the first person to add the contact
+        // was the one who made the request.
+        $sql = "SELECT c1.id
+                  FROM {message_contacts} c1
+            INNER JOIN {message_contacts} c2
+                    ON c1.userid = c2.contactid
+                   AND c1.contactid = c2.userid
+                 WHERE c1.id > c2.id";
+        if ($contacts = $DB->get_records_sql($sql)) {
+            list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contacts));
+            $DB->delete_records_select('message_contacts', "id $insql", $inparams);
+        }
+
+        upgrade_main_savepoint(true, 2018092800.03);
+    }
+
     return true;
 }
index ca248e7..44e8b8b 100644 (file)
@@ -3455,3 +3455,159 @@ function get_roles_with_assignment_on_context(context $context) {
 
     return get_roles_used_in_context($context, false);
 }
+
+/**
+ * Add the selected user as a contact for the current user
+ *
+ * @deprecated since Moodle 3.6
+ * @param int $contactid the ID of the user to add as a contact
+ * @param int $blocked 1 if you wish to block the contact
+ * @param int $userid the user ID of the user we want to add the contact for, defaults to current user if not specified.
+ * @return bool/int false if the $contactid isnt a valid user id. True if no changes made.
+ *                  Otherwise returns the result of update_record() or insert_record()
+ */
+function message_add_contact($contactid, $blocked = 0, $userid = 0) {
+    debugging('message_add_contact() is deprecated. Please use \core_message\api::create_contact_request() instead. ' .
+        'If you wish to block or unblock a user please use \core_message\api::is_blocked() and ' .
+        '\core_message\api::block_user() or \core_message\api::unblock_user() respectively.', DEBUG_DEVELOPER);
+
+    global $USER, $DB;
+
+    if (!$DB->record_exists('user', array('id' => $contactid))) {
+        return false;
+    }
+
+    if (empty($userid)) {
+        $userid = $USER->id;
+    }
+
+    // Check if a record already exists as we may be changing blocking status.
+    if (\core_message\api::is_contact($userid, $contactid)) {
+        $isblocked = \core_message\api::is_blocked($userid, $contactid);
+        // Check if blocking status has been changed.
+        if ($isblocked != $blocked) {
+            if ($blocked == 1) {
+                if (!$isblocked) {
+                    \core_message\api::block_user($userid, $contactid);
+                }
+            } else {
+                \core_message\api::unblock_user($userid, $contactid);
+            }
+
+            return true;
+        } else {
+            // No change to blocking status.
+            return true;
+        }
+    } else {
+        if ($blocked == 1) {
+            if (!\core_message\api::is_blocked($userid, $contactid)) {
+                \core_message\api::block_user($userid, $contactid);
+            }
+        } else {
+            \core_message\api::unblock_user($userid, $contactid);
+            if (!\core_message\api::does_contact_request_exist($userid, $contactid)) {
+                \core_message\api::create_contact_request($userid, $contactid);
+            }
+        }
+
+        return true;
+    }
+}
+
+/**
+ * Remove a contact.
+ *
+ * @deprecated since Moodle 3.6
+ * @param int $contactid the user ID of the contact to remove
+ * @param int $userid the user ID of the user we want to remove the contacts for, defaults to current user if not specified.
+ * @return bool returns the result of delete_records()
+ */
+function message_remove_contact($contactid, $userid = 0) {
+    debugging('message_remove_contact() is deprecated. Please use \core_message\api::remove_contact() instead.',
+        DEBUG_DEVELOPER);
+
+    global $USER;
+
+    if (empty($userid)) {
+        $userid = $USER->id;
+    }
+
+    \core_message\api::remove_contact($userid, $contactid);
+
+    return true;
+}
+
+/**
+ * Unblock a contact.
+ *
+ * @deprecated since Moodle 3.6
+ * @param int $contactid the user ID of the contact to unblock
+ * @param int $userid the user ID of the user we want to unblock the contact for, defaults to current user
+ *  if not specified.
+ * @return bool returns the result of delete_records()
+ */
+function message_unblock_contact($contactid, $userid = 0) {
+    debugging('message_unblock_contact() is deprecated. Please use \core_message\api::unblock_user() instead.',
+        DEBUG_DEVELOPER);
+
+    global $DB, $USER;
+
+    if (!$DB->record_exists('user', array('id' => $contactid))) {
+        return false;
+    }
+
+    if (empty($userid)) {
+        $userid = $USER->id;
+    }
+
+    \core_message\api::unblock_user($userid, $contactid);
+
+    return true;
+}
+
+/**
+ * Block a user.
+ *
+ * @deprecated since Moodle 3.6
+ * @param int $contactid the user ID of the user to block
+ * @param int $userid the user ID of the user we want to unblock the contact for, defaults to current user
+ *  if not specified.
+ * @return bool
+ */
+function message_block_contact($contactid, $userid = 0) {
+    debugging('message_block_contact() is deprecated. Please use \core_message\api::is_blocked() and ' .
+        '\core_message\api::block_user() instead.', DEBUG_DEVELOPER);
+
+    global $DB, $USER;
+
+    if (!$DB->record_exists('user', array('id' => $contactid))) {
+        return false;
+    }
+
+    if (empty($userid)) {
+        $userid = $USER->id;
+    }
+
+    if (!\core_message\api::is_blocked($userid, $contactid)) {
+        \core_message\api::block_user($userid, $contactid);
+    }
+
+    return true;
+}
+
+/**
+ * Load a user's contact record
+ *
+ * @deprecated since Moodle 3.6
+ * @param int $contactid the user ID of the user whose contact record you want
+ * @return array message contacts
+ */
+function message_get_contact($contactid) {
+    debugging('message_get_contact() is deprecated. Please use \core_message\api::get_contact() instead.',
+        DEBUG_DEVELOPER);
+
+    global $USER;
+
+    return \core_message\api::get_contact($USER->id, $contactid);
+}
index 454adf3..a074857 100644 (file)
@@ -4182,7 +4182,7 @@ EOD;
 
                 // Check to see if we should be displaying a message button.
                 if (!empty($CFG->messaging) && $USER->id != $user->id && has_capability('moodle/site:sendmessage', $context)) {
-                    $iscontact = !empty(message_get_contact($user->id));
+                    $iscontact = \core_message\api::is_contact($USER->id, $user->id);
                     $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts';
                     $contacturlaction = $iscontact ? 'removecontact' : 'addcontact';
                     $contactimage = $iscontact ? 'removecontact' : 'addcontact';
@@ -4388,10 +4388,16 @@ EOD;
      * @return string
      */
     public function render_login(\core_auth\output\login $form) {
+        global $CFG;
+
         $context = $form->export_for_template($this);
 
         // Override because rendering is not supported in template yet.
-        $context->cookieshelpiconformatted = $this->help_icon('cookiesenabled');
+        if ($CFG->rememberusername == 0) {
+            $context->cookieshelpiconformatted = $this->help_icon('cookiesenabledonlysession');
+        } else {
+            $context->cookieshelpiconformatted = $this->help_icon('cookiesenabled');
+        }
         $context->errorformatted = $this->error_text($context->error);
 
         return $this->render_from_template('core/loginform', $context);
index 6d6ecec..288ed76 100644 (file)
@@ -130,6 +130,11 @@ any group. Besides, groups_get_members_ids_sql, get_enrolled_sql and get_enrolle
 the groupid field.
 * Added $CFG->conversionattemptlimit setting to config.php allowing a maximum number of retries before giving up conversion
   of a given document by the assignfeedback_editpdf\task\convert_submissions task. Default value: 3.
+* The following events have been deprecated and should not be used any more:
+  - message_contact_blocked
+  - message_contact_unblocked
+  The reason for this is because you can now block/unblock users without them necessarily being a contact. These events
+  have been replaced with message_user_blocked and message_user_unblocked respectively.
 
 === 3.5 ===
 
index 1e38b6b..c99cadb 100644 (file)
Binary files a/message/amd/build/message_area_profile.min.js and b/message/amd/build/message_area_profile.min.js differ
index c4cedfb..eb73dcb 100644 (file)
@@ -153,7 +153,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
          * @private
          */
         Profile.prototype._blockContact = function() {
-            var action = this._performAction('core_message_block_contacts', 'unblockcontact', 'profile-block-contact',
+            var action = this._performAction('core_message_block_user', 'unblockcontact', 'profile-block-contact',
                 'profile-unblock-contact', '');
             return action.then(function() {
                 this.messageArea.trigger(Events.CONTACTBLOCKED, this._getUserId());
@@ -167,7 +167,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
          * @private
          */
         Profile.prototype._unblockContact = function() {
-            var action = this._performAction('core_message_unblock_contacts', 'blockcontact', 'profile-unblock-contact',
+            var action = this._performAction('core_message_unblock_user', 'blockcontact', 'profile-unblock-contact',
                 'profile-block-contact', 'danger');
             return action.then(function() {
                 this.messageArea.trigger(Events.CONTACTUNBLOCKED, this._getUserId());
@@ -181,7 +181,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
          * @private
          */
         Profile.prototype._addContact = function() {
-            var action = this._performAction('core_message_create_contacts', 'removecontact', 'profile-add-contact',
+            var action = this._performAction('core_message_create_contact_request', 'removecontact', 'profile-add-contact',
                 'profile-remove-contact', 'danger');
             return action.then(function() {
                 this.messageArea.trigger(Events.CONTACTADDED, this._getUserId());
@@ -214,14 +214,40 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
          * @private
          */
         Profile.prototype._performAction = function(service, string, oldaction, newaction, newclass) {
+            // This is a temporary hack as we are getting rid of this UI.
+            var userargs = '';
+            switch (service) {
+                case 'core_message_block_user':
+                    userargs = {
+                        userid: this.messageArea.getCurrentUserId(),
+                        blockeduserid: this._getUserId()
+                    };
+                    break;
+                case 'core_message_unblock_user':
+                    userargs = {
+                        userid: this.messageArea.getCurrentUserId(),
+                        unblockeduserid: this._getUserId()
+                    };
+                    break;
+                case 'core_message_create_contact_request':
+                    userargs = {
+                        userid: this.messageArea.getCurrentUserId(),
+                        requesteduserid: this._getUserId()
+                    };
+                    break;
+                default:
+                    userargs = {
+                        userid: this.messageArea.getCurrentUserId(),
+                        userids: [
+                            this._getUserId()
+                        ]
+                    };
+            }
+
+
             var promises = Ajax.call([{
                 methodname: service,
-                args: {
-                    userid: this.messageArea.getCurrentUserId(),
-                    userids: [
-                        this._getUserId()
-                    ]
-                }
+                args: userargs
             }]);
 
             return promises[0].then(function() {
index 51a067a..8bc93a7 100644 (file)
@@ -63,8 +63,8 @@ class api {
         $ufields2 = \user_picture::fields('u2', array('lastaccess'), 'userto_id', 'userto_');
 
         $sql = "SELECT m.id, m.useridfrom, mcm.userid as useridto, m.subject, m.fullmessage, m.fullmessagehtml, m.fullmessageformat,
-                       m.smallmessage, m.timecreated, 0 as isread, $ufields, mcont.blocked as userfrom_blocked, $ufields2,
-                       mcont2.blocked as userto_blocked
+                       m.smallmessage, m.timecreated, 0 as isread, $ufields, mub.id as userfrom_blocked, $ufields2,
+                       mub2.id as userto_blocked
                   FROM {messages} m
             INNER JOIN {user} u
                     ON u.id = m.useridfrom
@@ -74,10 +74,10 @@ class api {
                     ON mcm.conversationid = m.conversationid
             INNER JOIN {user} u2
                     ON u2.id = mcm.userid
-             LEFT JOIN {message_contacts} mcont
-                    ON (mcont.contactid = u.id AND mcont.userid = ?)
-             LEFT JOIN {message_contacts} mcont2
-                    ON (mcont2.contactid = u2.id AND mcont2.userid = ?)
+             LEFT JOIN {message_users_blocked} mub
+                    ON (mub.blockeduserid = u.id AND mub.userid = ?)
+             LEFT JOIN {message_users_blocked} mub2
+                    ON (mub2.blockeduserid = u2.id AND mub2.userid = ?)
              LEFT JOIN {message_user_actions} mua
                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
                  WHERE (m.useridfrom = ? OR mcm.userid = ?)
@@ -101,7 +101,7 @@ class api {
                     $message->isread = true;
                 }
                 $blockedcol = $prefix . 'blocked';
-                $message->blocked = $message->$blockedcol;
+                $message->blocked = $message->$blockedcol ? 1 : 0;
 
                 $message->messageid = $message->id;
                 $conversations[] = helper::create_contact($message, $prefix);
@@ -126,12 +126,12 @@ class api {
 
         // Get all the users in the course.
         list($esql, $params) = get_enrolled_sql(\context_course::instance($courseid), '', 0, true);
-        $sql = "SELECT u.*, mc.blocked
+        $sql = "SELECT u.*, mub.id as isblocked
                   FROM {user} u
                   JOIN ($esql) je
                     ON je.id = u.id
-             LEFT JOIN {message_contacts} mc
-                    ON (mc.contactid = u.id AND mc.userid = :userid)
+             LEFT JOIN {message_users_blocked} mub
+                    ON (mub.blockeduserid = u.id AND mub.userid = :userid)
                  WHERE u.deleted = 0";
         // Add more conditions.
         $fullname = $DB->sql_fullname();
@@ -144,6 +144,7 @@ class api {
         $contacts = array();
         if ($users = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) {
             foreach ($users as $user) {
+                $user->blocked = $user->isblocked ? 1 : 0;
                 $contacts[] = helper::create_contact($user);
             }
         }
@@ -172,19 +173,22 @@ class api {
 
         // Ok, let's search for contacts first.
         $contacts = array();
-        $sql = "SELECT $ufields, mc.blocked
+        $sql = "SELECT $ufields, mub.id as isuserblocked
                   FROM {user} u
                   JOIN {message_contacts} mc
                     ON u.id = mc.contactid
+             LEFT JOIN {message_users_blocked} mub
+                    ON (mub.userid = :userid2 AND mub.blockeduserid = u.id)
                  WHERE mc.userid = :userid
                    AND u.deleted = 0
                    AND u.confirmed = 1
                    AND " . $DB->sql_like($fullname, ':search', false) . "
                    AND u.id $exclude
               ORDER BY " . $DB->sql_fullname();
-        if ($users = $DB->get_records_sql($sql, array('userid' => $userid, 'search' => '%' . $search . '%') + $excludeparams,
-            0, $limitnum)) {
+        if ($users = $DB->get_records_sql($sql, array('userid' => $userid, 'userid2' => $userid,
+                'search' => '%' . $search . '%') + $excludeparams, 0, $limitnum)) {
             foreach ($users as $user) {
+                $user->blocked = $user->isuserblocked ? 1 : 0;
                 $contacts[] = helper::create_contact($user);
             }
         }
@@ -225,7 +229,7 @@ class api {
                                      WHERE userid = :userid)
               ORDER BY " . $DB->sql_fullname();
         if ($users = $DB->get_records_sql($sql,  array('userid' => $userid, 'search' => '%' . $search . '%') + $excludeparams,
-            0, $limitnum)) {
+                0, $limitnum)) {
             foreach ($users as $user) {
                 $noncontacts[] = helper::create_contact($user);
             }
@@ -324,7 +328,7 @@ class api {
             return [];
         }
 
-        $contactssql = "SELECT contactid, blocked
+        $contactssql = "SELECT contactid
                           FROM {message_contacts}
                          WHERE userid = ?
                            AND contactid $useridsql";
@@ -401,21 +405,41 @@ class api {
     public static function get_contacts($userid, $limitfrom = 0, $limitnum = 0) {
         global $DB;
 
-        $arrcontacts = array();
-        $sql = "SELECT u.*, mc.blocked
+        $contactids = [];
+        $sql = "SELECT mc.*
                   FROM {message_contacts} mc
-                  JOIN {user} u
-                    ON mc.contactid = u.id
-                 WHERE mc.userid = :userid
-                   AND u.deleted = 0
-              ORDER BY " . $DB->sql_fullname();
-        if ($contacts = $DB->get_records_sql($sql, array('userid' => $userid), $limitfrom, $limitnum)) {
+                 WHERE mc.userid = ? OR mc.contactid = ?
+              ORDER BY timecreated DESC";
+        if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
             foreach ($contacts as $contact) {
-                $arrcontacts[] = helper::create_contact($contact);
+                if ($userid == $contact->userid) {
+                    $contactids[] = $contact->contactid;
+                } else {
+                    $contactids[] = $contact->userid;
+                }
+            }
+        }
+
+        if (!empty($contactids)) {
+            list($insql, $inparams) = $DB->get_in_or_equal($contactids);
+
+            $sql = "SELECT u.*, mub.id as isblocked
+                      FROM {user} u
+                 LEFT JOIN {message_users_blocked} mub
+                        ON u.id = mub.blockeduserid
+                     WHERE u.id $insql";
+            if ($contacts = $DB->get_records_sql($sql, $inparams)) {
+                $arrcontacts = [];
+                foreach ($contacts as $contact) {
+                    $contact->blocked = $contact->isblocked ? 1 : 0;
+                    $arrcontacts[] = helper::create_contact($contact);
+                }
+
+                return $arrcontacts;
             }
         }
 
-        return $arrcontacts;
+        return [];
     }
 
     /**
@@ -434,21 +458,24 @@ class api {
         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
                               FROM {message_contacts} mc
                         INNER JOIN {user} u
-                                ON u.id = mc.contactid
+                                ON (u.id = mc.contactid OR u.id = mc.userid)
                          LEFT JOIN {messages} m
-                                ON m.useridfrom = mc.contactid
+                                ON ((m.useridfrom = mc.contactid OR m.useridfrom = mc.userid) AND m.useridfrom != ?)
                          LEFT JOIN {message_conversation_members} mcm
                                 ON mcm.conversationid = m.conversationid AND mcm.userid = ? AND mcm.userid != m.useridfrom
                          LEFT JOIN {message_user_actions} mua
                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
+                         LEFT JOIN {message_users_blocked} mub
+                                ON (mub.userid = ? AND mub.blockeduserid = u.id)
                              WHERE mua.id is NULL
-                               AND mc.userid = ?
-                               AND mc.blocked = 0
+                               AND mub.id is NULL
+                               AND (mc.userid = ? OR mc.contactid = ?)
+                               AND u.id != ?
                                AND u.deleted = 0
                           GROUP BY $userfields";
 
-        return $DB->get_records_sql($unreadcountssql, [$userid, $userid, self::MESSAGE_ACTION_READ,
-            $userid, $userid], $limitfrom, $limitnum);
+        return $DB->get_records_sql($unreadcountssql, [$userid, $userid, $userid, self::MESSAGE_ACTION_READ,
+            $userid, $userid, $userid, $userid], $limitfrom, $limitnum);
     }
 
     /**
@@ -474,14 +501,17 @@ class api {
                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
                          LEFT JOIN {message_contacts} mc
                                 ON (mc.userid = ? AND mc.contactid = u.id)
+                         LEFT JOIN {message_users_blocked} mub
+                                ON (mub.userid = ? AND mub.blockeduserid = u.id)
                              WHERE mcm.userid = ?
                                AND mcm.userid != m.useridfrom
                                AND mua.id is NULL
+                               AND mub.id is NULL
                                AND mc.id is NULL
                                AND u.deleted = 0
                           GROUP BY $userfields";
 
-        return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid],
+        return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid],
             $limitfrom, $limitnum);
     }
 
@@ -550,7 +580,7 @@ class api {
      * @return \stdClass
      */
     public static function get_profile($userid, $otheruserid) {
-        global $CFG, $DB, $PAGE;
+        global $CFG, $PAGE;
 
         require_once($CFG->dirroot . '/user/lib.php');
 
@@ -587,15 +617,8 @@ class api {
             }
         }
 
-        // Check if the contact has been blocked.
-        $contact = $DB->get_record('message_contacts', array('userid' => $userid, 'contactid' => $otheruserid));
-        if ($contact) {
-            $data->isblocked = (bool) $contact->blocked;
-            $data->iscontact = true;
-        } else {
-            $data->isblocked = false;
-            $data->iscontact = false;
-        }
+        $data->isblocked = self::is_blocked($userid, $otheruserid);
+        $data->iscontact = self::is_contact($userid, $otheruserid);
 
         return $data;
     }
@@ -854,9 +877,9 @@ class api {
             $user = $USER;
         }
 
-        $sql = "SELECT count(mc.id)
-                  FROM {message_contacts} mc
-                 WHERE mc.userid = :userid AND mc.blocked = 1";
+        $sql = "SELECT count(mub.id)
+                  FROM {message_users_blocked} mub
+                 WHERE mub.userid = :userid";
         return $DB->count_records_sql($sql, array('userid' => $user->id));
     }
 
@@ -890,8 +913,14 @@ class api {
         if ($sender !== null && isset($sender->id)) {
             $senderid = $sender->id;
         }
+
+        $systemcontext = \context_system::instance();
+        if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
+            return true;
+        }
+
         // The recipient has specifically blocked this sender.
-        if (self::is_user_blocked($recipient->id, $senderid)) {
+        if (self::is_blocked($recipient->id, $senderid)) {
             return false;
         }
 
@@ -908,7 +937,7 @@ class api {
      * @return bool true if $sender is blocked, false otherwise.
      */
     public static function is_user_non_contact_blocked($recipient, $sender = null) {
-        global $USER, $DB;
+        global $USER;
 
         if (is_null($sender)) {
             // The message is from the logged in user, unless otherwise specified.
@@ -918,8 +947,7 @@ class api {
         $blockednoncontacts = get_user_preferences('message_blocknoncontacts', '', $recipient->id);
         if (!empty($blockednoncontacts)) {
             // Confirm the sender is a contact of the recipient.
-            $exists = $DB->record_exists('message_contacts', array('userid' => $recipient->id, 'contactid' => $sender->id));
-            if ($exists) {
+            if (self::is_contact($sender->id, $recipient->id)) {
                 // All good, the recipient is a contact of the sender.
                 return false;
             } else {
@@ -937,12 +965,16 @@ class api {
      * Note: This function will always return false if the sender has the
      * readallmessages capability at the system context level.
      *
+     * @deprecated since 3.6
      * @param int $recipientid User ID of the recipient.
      * @param int $senderid User ID of the sender.
      * @return bool true if $sender is blocked, false otherwise.
      */
     public static function is_user_blocked($recipientid, $senderid = null) {
-        global $USER, $DB;
+        debugging('\core_message\api::is_user_blocked is deprecated and should not be used.',
+            DEBUG_DEVELOPER);
+
+        global $USER;
 
         if (is_null($senderid)) {
             // The message is from the logged in user, unless otherwise specified.
@@ -954,7 +986,7 @@ class api {
             return false;
         }
 
-        if ($DB->get_field('message_contacts', 'blocked', ['userid' => $recipientid, 'contactid' => $senderid])) {
+        if (self::is_blocked($recipientid, $senderid)) {
             return true;
         }
 
@@ -1079,12 +1111,11 @@ class api {
 
         $userfields = \user_picture::fields('u', array('lastaccess'));
         $blockeduserssql = "SELECT $userfields
-                              FROM {message_contacts} mc
+                              FROM {message_users_blocked} mub
                         INNER JOIN {user} u
-                                ON u.id = mc.contactid
+                                ON u.id = mub.blockeduserid
                              WHERE u.deleted = 0
-                               AND mc.userid = ?
-                               AND mc.blocked = 1
+                               AND mub.userid = ?
                           GROUP BY $userfields
                           ORDER BY u.firstname ASC";
         return $DB->get_records_sql($blockeduserssql, [$userid]);
@@ -1287,4 +1318,298 @@ class api {
 
         return $conversation->id;
     }
+
+    /**
+     * Checks if a user can create a contact request.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     * @return bool
+     */
+    public static function can_create_contact(int $userid, int $requesteduserid) : bool {
+        global $CFG;
+
+        // If we can't message at all, then we can't create a contact.
+        if (empty($CFG->messaging)) {
+            return false;
+        }
+
+        // If we can message anyone on the site then we can create a contact.
+        if ($CFG->messagingallusers) {
+            return true;
+        }
+
+        // We need to check if they are in the same course.
+        return enrol_sharing_course($userid, $requesteduserid);
+    }
+
+    /**
+     * Handles creating a contact request.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     */
+    public static function create_contact_request(int $userid, int $requesteduserid) {
+        global $DB;
+
+        $request = new \stdClass();
+        $request->userid = $userid;
+        $request->requesteduserid = $requesteduserid;
+        $request->timecreated = time();
+
+        $DB->insert_record('message_contact_requests', $request);
+
+        // Send a notification.
+        $userfrom = \core_user::get_user($userid);
+        $userfromfullname = fullname($userfrom);
+        $userto = \core_user::get_user($requesteduserid);
+        $url = new \moodle_url('/message/pendingcontactrequests.php');
+
+        $subject = get_string('messagecontactrequestsnotificationsubject', 'core_message', $userfromfullname);
+        $fullmessage = get_string('messagecontactrequestsnotification', 'core_message', $userfromfullname);
+
+        $message = new \core\message\message();
+        $message->courseid = SITEID;
+        $message->component = 'moodle';
+        $message->name = 'messagecontactrequests';
+        $message->notification = 1;
+        $message->userfrom = $userfrom;
+        $message->userto = $userto;
+        $message->subject = $subject;
+        $message->fullmessage = text_to_html($fullmessage);
+        $message->fullmessageformat = FORMAT_HTML;
+        $message->fullmessagehtml = $fullmessage;
+        $message->smallmessage = '';
+        $message->contexturl = $url->out(false);
+
+        message_send($message);
+    }
+
+
+    /**
+     * Handles confirming a contact request.
+     *
+     * @param int $userid The id of the user who created the contact request
+     * @param int $requesteduserid The id of the user confirming the request
+     */
+    public static function confirm_contact_request(int $userid, int $requesteduserid) {
+        global $DB;
+
+        if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
+                'requesteduserid' => $requesteduserid])) {
+            self::add_contact($userid, $requesteduserid);
+
+            $DB->delete_records('message_contact_requests', ['id' => $request->id]);
+        }
+    }
+
+    /**
+     * Handles declining a contact request.
+     *
+     * @param int $userid The id of the user who created the contact request
+     * @param int $requesteduserid The id of the user declining the request
+     */
+    public static function decline_contact_request(int $userid, int $requesteduserid) {
+        global $DB;
+
+        if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
+                'requesteduserid' => $requesteduserid])) {
+            $DB->delete_records('message_contact_requests', ['id' => $request->id]);
+        }
+    }
+
+    /**
+     * Handles returning the contact requests for a user.
+     *
+     * This also includes the user data necessary to display information
+     * about the user.
+     *
+     * It will not include blocked users.
+     *
+     * @param int $userid
+     * @return array The list of contact requests
+     */
+    public static function get_contact_requests(int $userid) : array {
+        global $DB;
+
+        // Used to search for contacts.
+        $ufields = \user_picture::fields('u');
+
+        $sql = "SELECT $ufields, mcr.id as contactrequestid
+                  FROM {user} u
+                  JOIN {message_contact_requests} mcr
+                    ON u.id = mcr.userid
+             LEFT JOIN {message_users_blocked} mub
+                    ON (mub.userid = ? AND mub.blockeduserid = u.id)
+                 WHERE mcr.requesteduserid = ?
+                   AND u.deleted = 0
+                   AND mub.id is NULL
+              ORDER BY mcr.timecreated DESC";
+
+        return $DB->get_records_sql($sql, [$userid, $userid]);
+    }
+
+    /**
+     * Handles adding a contact.
+     *
+     * @param int $userid The id of the user who requested to be a contact
+     * @param int $contactid The id of the contact
+     */
+    public static function add_contact(int $userid, int $contactid) {
+        global $DB;
+
+        $messagecontact = new \stdClass();
+        $messagecontact->userid = $userid;
+        $messagecontact->contactid = $contactid;
+        $messagecontact->timecreated = time();
+        $messagecontact->id = $DB->insert_record('message_contacts', $messagecontact);
+
+        $eventparams = [
+            'objectid' => $messagecontact->id,
+            'userid' => $userid,
+            'relateduserid' => $contactid,
+            'context' => \context_user::instance($userid)
+        ];
+        $event = \core\event\message_contact_added::create($eventparams);
+        $event->add_record_snapshot('message_contacts', $messagecontact);
+        $event->trigger();
+    }
+
+    /**
+     * Handles removing a contact.
+     *
+     * @param int $userid The id of the user who is removing a user as a contact
+     * @param int $contactid The id of the user to be removed as a contact
+     */
+    public static function remove_contact(int $userid, int $contactid) {
+        global $DB;
+
+        if ($contact = self::get_contact($userid, $contactid)) {
+            $DB->delete_records('message_contacts', ['id' => $contact->id]);
+
+            $event = \core\event\message_contact_removed::create(array(
+                'objectid' => $contact->id,
+                'userid' => $userid,
+                'relateduserid' => $contactid,
+                'context' => \context_user::instance($userid)
+            ));
+            $event->add_record_snapshot('message_contacts', $contact);
+            $event->trigger();
+        }
+    }
+
+    /**
+     * Handles blocking a user.
+     *
+     * @param int $userid The id of the user who is blocking
+     * @param int $usertoblockid The id of the user being blocked
+     */
+    public static function block_user(int $userid, int $usertoblockid) {
+        global $DB;
+
+        $blocked = new \stdClass();
+        $blocked->userid = $userid;
+        $blocked->blockeduserid = $usertoblockid;
+        $blocked->timecreated = time();
+        $blocked->id = $DB->insert_record('message_users_blocked', $blocked);
+
+        // Trigger event for blocking a contact.
+        $event = \core\event\message_user_blocked::create(array(
+            'objectid' => $blocked->id,
+            'userid' => $userid,
+            'relateduserid' => $usertoblockid,
+            'context' => \context_user::instance($userid)
+        ));
+        $event->add_record_snapshot('message_users_blocked', $blocked);
+        $event->trigger();
+    }
+
+    /**
+     * Handles unblocking a user.
+     *
+     * @param int $userid The id of the user who is unblocking
+     * @param int $usertounblockid The id of the user being unblocked
+     */
+    public static function unblock_user(int $userid, int $usertounblockid) {
+        global $DB;
+
+        if ($blockeduser = $DB->get_record('message_users_blocked',
+                ['userid' => $userid, 'blockeduserid' => $usertounblockid])) {
+            $DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]);
+
+            // Trigger event for unblocking a contact.
+            $event = \core\event\message_user_unblocked::create(array(
+                'objectid' => $blockeduser->id,
+                'userid' => $userid,
+                'relateduserid' => $usertounblockid,
+                'context' => \context_user::instance($userid)
+            ));
+            $event->add_record_snapshot('message_users_blocked', $blockeduser);
+            $event->trigger();
+        }
+    }
+
+    /**
+     * Checks if users are already contacts.
+     *
+     * @param int $userid The id of one of the users
+     * @param int $contactid The id of the other user
+     * @return bool Returns true if they are a contact, false otherwise
+     */
+    public static function is_contact(int $userid, int $contactid) : bool {
+        global $DB;
+
+        $sql = "SELECT id
+                  FROM {message_contacts} mc
+                 WHERE (mc.userid = ? AND mc.contactid = ?)
+                    OR (mc.userid = ? AND mc.contactid = ?)";
+        return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]);
+    }
+
+    /**
+     * Returns the row in the database table message_contacts that represents the contact between two people.
+     *
+     * @param int $userid The id of one of the users
+     * @param int $contactid The id of the other user
+     * @return mixed A fieldset object containing the record, false otherwise
+     */
+    public static function get_contact(int $userid, int $contactid) {
+        global $DB;
+
+        $sql = "SELECT mc.*
+                  FROM {message_contacts} mc
+                 WHERE (mc.userid = ? AND mc.contactid = ?)
+                    OR (mc.userid = ? AND mc.contactid = ?)";
+        return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]);
+    }
+
+    /**
+     * Checks if a user is already blocked.
+     *
+     * @param int $userid
+     * @param int $blockeduserid
+     * @return bool Returns true if they are a blocked, false otherwise
+     */
+    public static function is_blocked(int $userid, int $blockeduserid) : bool {
+        global $DB;
+
+        return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
+    }
+
+    /**
+     * Checks if a contact request already exists between users.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     * @return bool Returns true if a contact request exists, false otherwise
+     */
+    public static function does_contact_request_exist(int $userid, int $requesteduserid) : bool {
+        global $DB;
+
+        $sql = "SELECT id
+                  FROM {message_contact_requests} mcr
+                 WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
+                    OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
+        return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
+    }
 }
index a9b53d3..7d98e7b 100644 (file)
@@ -101,7 +101,8 @@ class messages implements templatable, renderable {
             $data->messages[] = $message->export_for_template($output);
         }
 
-        $data->isblocked = api::is_user_blocked($this->currentuserid, $this->otheruserid);
+        $blockeduserid = $this->otheruserid ?? $USER->id;
+        $data->isblocked = api::is_blocked($this->currentuserid, $blockeduserid);
 
         return $data;
     }
index 8551353..b9049e5 100644 (file)
@@ -90,11 +90,31 @@ class provider implements
             [
                 'userid' => 'privacy:metadata:message_contacts:userid',
                 'contactid' => 'privacy:metadata:message_contacts:contactid',
-                'blocked' => 'privacy:metadata:message_contacts:blocked',
+                'timecreated' => 'privacy:metadata:message_contacts:timecreated',
             ],
             'privacy:metadata:message_contacts'
         );
 
+        $items->add_database_table(
+            'message_contact_requests',
+            [
+                'userid' => 'privacy:metadata:message_contact_requests:userid',
+                'requesteduserid' => 'privacy:metadata:message_contact_requests:requesteduserid',
+                'timecreated' => 'privacy:metadata:message_contact_requests:timecreated',
+            ],
+            'privacy:metadata:message_contact_requests'
+        );
+
+        $items->add_database_table(
+            'message_users_blocked',
+            [
+                'userid' => 'privacy:metadata:message_users_blocked:userid',
+                'blockeduserid' => 'privacy:metadata:message_users_blocked:blockeduserid',
+                'timecreated' => 'privacy:metadata:message_users_blocked:timecreated',
+            ],
+            'privacy:metadata:message_users_blocked'
+        );
+
         $items->add_database_table(
             'notifications',
             [
@@ -186,6 +206,12 @@ class provider implements
         // Export the contacts.
         self::export_user_data_contacts($userid);
 
+        // Export the contact requests.
+        self::export_user_data_contact_requests($userid);
+
+        // Export the blocked users.
+        self::export_user_data_blocked_users($userid);
+
         // Export the notifications.
         self::export_user_data_notifications($userid);
 
@@ -209,6 +235,8 @@ class provider implements
         $DB->delete_records('message_user_actions');
         $DB->delete_records('message_conversation_members');
         $DB->delete_records('message_contacts');
+        $DB->delete_records('message_contact_requests');
+        $DB->delete_records('message_users_blocked');
         $DB->delete_records('notifications');
     }
 
@@ -239,6 +267,8 @@ class provider implements
         $DB->delete_records('message_user_actions', ['userid' => $userid]);
         $DB->delete_records('message_conversation_members', ['userid' => $userid]);
         $DB->delete_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]);
+        $DB->delete_records_select('message_contact_requests', 'userid = ? OR requesteduserid = ?', [$userid, $userid]);
+        $DB->delete_records_select('message_users_blocked', 'userid = ? OR blockeduserid = ?', [$userid, $userid]);
         $DB->delete_records_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
     }
 
@@ -253,18 +283,70 @@ class provider implements
         $context = \context_system::instance();
 
         // Get the user's contacts.
-        if ($contacts = $DB->get_records('message_contacts', ['userid' => $userid], 'id ASC')) {
+        if ($contacts = $DB->get_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid], 'id ASC')) {
             $contactdata = [];
             foreach ($contacts as $contact) {
                 $contactdata[] = (object) [
-                    'contact' => transform::user($contact->contactid),
-                    'blocked' => transform::yesno($contact->blocked)
+                    'contact' => transform::user($contact->contactid)
                 ];
             }
             writer::with_context($context)->export_data([get_string('contacts', 'core_message')], (object) $contactdata);
         }
     }
 
+    /**
+     * Export the messaging contact requests data.
+     *
+     * @param int $userid
+     */
+    protected static function export_user_data_contact_requests(int $userid) {
+        global $DB;
+
+        $context = \context_system::instance();
+
+        if ($contactrequests = $DB->get_records_select('message_contact_requests', 'userid = ? OR requesteduserid = ?',
+                [$userid, $userid], 'id ASC')) {
+            $contactrequestsdata = [];
+            foreach ($contactrequests as $contactrequest) {
+                if ($userid == $contactrequest->requesteduserid) {
+                    $maderequest = false;
+                    $contactid = $contactrequest->userid;
+                } else {
+                    $maderequest = true;
+                    $contactid = $contactrequest->requesteduserid;
+                }
+
+                $contactrequestsdata[] = (object) [
+                    'contactrequest' => transform::user($contactid),
+                    'maderequest' => transform::yesno($maderequest)
+                ];
+            }
+            writer::with_context($context)->export_data([get_string('contactrequests', 'core_message')],
+                (object) $contactrequestsdata);
+        }
+    }
+
+    /**
+     * Export the messaging blocked users data.
+     *
+     * @param int $userid
+     */
+    protected static function export_user_data_blocked_users(int $userid) {
+        global $DB;
+
+        $context = \context_system::instance();
+
+        if ($blockedusers = $DB->get_records('message_users_blocked', ['userid' => $userid], 'id ASC')) {
+            $blockedusersdata = [];
+            foreach ($blockedusers as $blockeduser) {
+                $blockedusersdata[] = (object) [
+                    'blockeduser' => transform::user($blockeduser->blockeduserid)
+                ];
+            }
+            writer::with_context($context)->export_data([get_string('blockedusers', 'core_message')], (object) $blockedusersdata);
+        }
+    }
+
     /**
      * Export the messaging data.
      *
index 5151adb..f678c2f 100644 (file)
@@ -90,23 +90,30 @@ class core_message_external extends external_api {
         foreach($params['messages'] as $message) {
             $receivers[] = $message['touserid'];
         }
-        list($sqluserids, $sqlparams) = $DB->get_in_or_equal($receivers, SQL_PARAMS_NAMED, 'userid_');
+        list($sqluserids, $sqlparams) = $DB->get_in_or_equal($receivers);
         $tousers = $DB->get_records_select("user", "id " . $sqluserids . " AND deleted = 0", $sqlparams);
         $blocklist   = array();
         $contactlist = array();
-        $sqlparams['contactid'] = $USER->id;
+        $contactsqlparams = array_merge($sqlparams, [$USER->id], [$USER->id], $sqlparams);
         $rs = $DB->get_recordset_sql("SELECT *
                                         FROM {message_contacts}
-                                       WHERE userid $sqluserids
-                                             AND contactid = :contactid", $sqlparams);
+                                       WHERE (userid $sqluserids AND contactid = ?)
+                                          OR (userid = ? AND contactid $sqluserids)", $contactsqlparams);
         foreach ($rs as $record) {
-            if ($record->blocked) {
-                // $record->userid is blocking current user
-                $blocklist[$record->userid] = true;
-            } else {
-                // $record->userid have current user as contact
-                $contactlist[$record->userid] = true;
+            $useridtouse = $record->userid;
+            if ($record->userid == $USER->id) {
+                $useridtouse = $record->contactid;
             }
+            $contactlist[$useridtouse] = true;
+        }
+        $rs->close();
+        $blocksqlparams = array_merge($sqlparams, [$USER->id]);
+        $rs = $DB->get_recordset_sql("SELECT *
+                                        FROM {message_users_blocked}
+                                       WHERE userid $sqluserids
+                                         AND blockeduserid = ?", $blocksqlparams);
+        foreach ($rs as $record) {
+            $blocklist[$record->userid] = true;
         }
         $rs->close();
 
@@ -191,6 +198,7 @@ class core_message_external extends external_api {
     /**
      * Create contacts parameters description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_function_parameters
      * @since Moodle 2.5
      */
@@ -210,6 +218,7 @@ class core_message_external extends external_api {
     /**
      * Create contacts.
      *
+     * @deprecated since Moodle 3.6
      * @param array $userids array of user IDs.
      * @param int $userid The id of the user we are creating the contacts for
      * @return external_description
@@ -256,6 +265,7 @@ class core_message_external extends external_api {
     /**
      * Create contacts return description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_description
      * @since Moodle 2.5
      */
@@ -263,6 +273,15 @@ class core_message_external extends external_api {
         return new external_warnings();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function create_contacts_is_deprecated() {
+        return true;
+    }
+
     /**
      * Delete contacts parameters description.
      *
@@ -315,7 +334,7 @@ class core_message_external extends external_api {
         $params = self::validate_parameters(self::delete_contacts_parameters(), $params);
 
         foreach ($params['userids'] as $id) {
-            message_remove_contact($id, $userid);
+            \core_message\api::remove_contact($userid, $id);
         }
 
         return null;
@@ -331,9 +350,121 @@ class core_message_external extends external_api {
         return null;
     }
 
+    /**
+     * Block user parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function block_user_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user who is blocking'),
+                'blockeduserid' => new external_value(PARAM_INT, 'The id of the user being blocked'),
+            ]
+        );
+    }
+
+    /**
+     * Blocks a user.
+     *
+     * @param int $userid The id of the user who is blocking
+     * @param int $blockeduserid The id of the user being blocked
+     * @return external_description
+     */
+    public static function block_user(int $userid, int $blockeduserid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid, 'blockeduserid' => $blockeduserid];
+        $params = self::validate_parameters(self::block_user_parameters(), $params);
+
+        if (!\core_message\api::is_blocked($params['userid'], $params['blockeduserid'])) {
+            \core_message\api::block_user($params['userid'], $params['blockeduserid']);
+        }
+
+        return [];
+    }
+
+    /**
+     * Block user return description.
+     *
+     * @return external_description
+     */
+    public static function block_user_returns() {
+        return new external_warnings();
+    }
+
+    /**
+     * Unblock user parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function unblock_user_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user who is unblocking'),
+                'unblockeduserid' => new external_value(PARAM_INT, 'The id of the user being unblocked'),
+            ]
+        );
+    }
+
+    /**
+     * Unblock user.
+     *
+     * @param int $userid The id of the user who is unblocking
+     * @param int $unblockeduserid The id of the user being unblocked
+     */
+    public static function unblock_user(int $userid, int $unblockeduserid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid, 'unblockeduserid' => $unblockeduserid];
+        $params = self::validate_parameters(self::unblock_user_parameters(), $params);
+
+        \core_message\api::unblock_user($params['userid'], $params['unblockeduserid']);
+
+        return [];
+    }
+
+    /**
+     * Unblock user return description.
+     *
+     * @return external_description
+     */
+    public static function unblock_user_returns() {
+        return new external_warnings();
+    }
+
     /**
      * Block contacts parameters description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_function_parameters
      * @since Moodle 2.5
      */
@@ -353,6 +484,7 @@ class core_message_external extends external_api {
     /**
      * Block contacts.
      *
+     * @deprecated since Moodle 3.6
      * @param array $userids array of user IDs.
      * @param int $userid The id of the user we are blocking the contacts for
      * @return external_description
@@ -399,6 +531,7 @@ class core_message_external extends external_api {
     /**
      * Block contacts return description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_description
      * @since Moodle 2.5
      */
@@ -406,9 +539,19 @@ class core_message_external extends external_api {
         return new external_warnings();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function block_contacts_is_deprecated() {
+        return true;
+    }
+
     /**
      * Unblock contacts parameters description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_function_parameters
      * @since Moodle 2.5
      */
@@ -428,6 +571,7 @@ class core_message_external extends external_api {
     /**
      * Unblock contacts.
      *
+     * @deprecated since Moodle 3.6
      * @param array $userids array of user IDs.
      * @param int $userid The id of the user we are unblocking the contacts for
      * @return null
@@ -467,6 +611,7 @@ class core_message_external extends external_api {
     /**
      * Unblock contacts return description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_description
      * @since Moodle 2.5
      */
@@ -474,6 +619,265 @@ class core_message_external extends external_api {
         return null;
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function unblock_contacts_is_deprecated() {
+        return true;
+    }
+
+    /**
+     * Returns contact requests parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function get_contact_requests_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user we want the requests for')
+            ]
+        );
+    }
+
+    /**
+     * Handles returning the contact requests for a user.
+     *
+     * This also includes the user data necessary to display information
+     * about the user.
+     *
+     * It will not include blocked users.
+     *
+     * @param int $userid The id of the user we want to get the contact requests for
+     */
+    public static function get_contact_requests(int $userid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid];
+        $params = self::validate_parameters(self::get_contact_requests_parameters(), $params);
+
+        return \core_message\api::get_contact_requests($params['userid']);
+    }
+
+    /**
+     * Returns the contact requests return description.
+     *
+     * @return external_description
+     */
+    public static function get_contact_requests_returns() {
+        return new external_multiple_structure(
+            new external_single_structure(
+                [
+                    'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'),
+                    'contactrequestid' => new external_value(PARAM_INT, 'The ID of the contact request'),
+                    'picture' => new external_value(core_user::get_property_type('picture'), 'The picture'),
+                    'firstname' => new external_value(core_user::get_property_type('firstname'),
+                        'The first name(s) of the user'),
+                    'lastname' => new external_value(core_user::get_property_type('lastname'),
+                        'The family name of the user'),
+                    'firstnamephonetic' => new external_value(core_user::get_property_type('firstnamephonetic'),
+                        'The phonetic first name of the user'),
+                    'lastnamephonetic' => new external_value(core_user::get_property_type('lastnamephonetic'),
+                        'The phonetic last name of the user'),
+                    'middlename' => new external_value(core_user::get_property_type('middlename'),
+                        'The middle name of the user'),
+                    'alternatename' => new external_value(core_user::get_property_type('alternatename'),
+                        'The alternate name of the user'),
+                    'email' => new external_value(core_user::get_property_type('email'), 'An email address')
+                ]
+            )
+        );
+    }
+
+    /**
+     * Creates a contact request parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function create_contact_request_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user making the request'),
+                'requesteduserid' => new external_value(PARAM_INT, 'The id of the user being requested')
+            ]
+        );
+    }
+
+    /**
+     * Creates a contact request.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     */
+    public static function create_contact_request(int $userid, int $requesteduserid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
+        $params = self::validate_parameters(self::create_contact_request_parameters(), $params);
+
+        if (!\core_message\api::can_create_contact($params['userid'], $params['requesteduserid'])) {
+            $warning[] = [
+                'item' => 'user',
+                'itemid' => $params['requesteduserid'],
+                'warningcode' => 'cannotcreatecontactrequest',
+                'message' => 'You are unable to create a contact request for this user'
+            ];
+            return $warning;
+        }
+
+        if (!\core_message\api::does_contact_request_exist($params['userid'], $params['requesteduserid'])) {
+            \core_message\api::create_contact_request($params['userid'], $params['requesteduserid']);
+        }
+
+        return [];
+    }
+
+    /**
+     * Creates a contact request return description.
+     *
+     * @return external_description
+     */
+    public static function create_contact_request_returns() {
+        return new external_warnings();
+    }
+
+    /**
+     * Confirm a contact request parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function confirm_contact_request_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user making the request'),
+                'requesteduserid' => new external_value(PARAM_INT, 'The id of the user being requested')
+            ]
+        );
+    }
+
+    /**
+     * Confirm a contact request.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     */
+    public static function confirm_contact_request(int $userid, int $requesteduserid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $requesteduserid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
+        $params = self::validate_parameters(self::confirm_contact_request_parameters(), $params);
+
+        \core_message\api::confirm_contact_request($params['userid'], $params['requesteduserid']);
+
+        return [];
+    }
+
+    /**
+     * Confirm a contact request return description.
+     *
+     * @return external_description
+     */
+    public static function confirm_contact_request_returns() {
+        return new external_warnings();
+    }
+
+    /**
+     * Declines a contact request parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function decline_contact_request_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user making the request'),
+                'requesteduserid' => new external_value(PARAM_INT, 'The id of the user being requested')
+            ]
+        );
+    }
+
+    /**
+     * Declines a contact request.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     */
+    public static function decline_contact_request(int $userid, int $requesteduserid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $requesteduserid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
+        $params = self::validate_parameters(self::decline_contact_request_parameters(), $params);
+
+        \core_message\api::decline_contact_request($params['userid'], $params['requesteduserid']);
+
+        return [];
+    }
+
+    /**
+     * Declines a contact request return description.
+     *
+     * @return external_description
+     */
+    public static function decline_contact_request_returns() {
+        return new external_warnings();
+    }
+
     /**
      * Return the structure of a message area contact.
      *
index a3f99fc..1fbe61c 100644 (file)
@@ -151,156 +151,6 @@ function message_format_message_text($message, $forcetexttohtml = false) {
     return format_text($messagetext, $format, $options);
 }
 
-/**
- * Add the selected user as a contact for the current user
- *
- * @param int $contactid the ID of the user to add as a contact
- * @param int $blocked 1 if you wish to block the contact
- * @param int $userid the user ID of the user we want to add the contact for, defaults to current user if not specified.
- * @return bool/int false if the $contactid isnt a valid user id. True if no changes made.
- *                  Otherwise returns the result of update_record() or insert_record()
- */
-function message_add_contact($contactid, $blocked = 0, $userid = 0) {
-    global $USER, $DB;
-
-    if (!$DB->record_exists('user', array('id' => $contactid))) { // invalid userid
-        return false;
-    }
-
-    if (empty($userid)) {
-        $userid = $USER->id;
-    }
-
-    // Check if a record already exists as we may be changing blocking status.
-    if (($contact = $DB->get_record('message_contacts', array('userid' => $userid, 'contactid' => $contactid))) !== false) {
-        // Check if blocking status has been changed.
-        if ($contact->blocked != $blocked) {
-            $contact->blocked = $blocked;
-            $DB->update_record('message_contacts', $contact);
-
-            if ($blocked == 1) {
-                // Trigger event for blocking a contact.
-                $event = \core\event\message_contact_blocked::create(array(
-                    'objectid' => $contact->id,
-                    'userid' => $contact->userid,
-                    'relateduserid' => $contact->contactid,
-                    'context'  => context_user::instance($contact->userid)
-                ));
-                $event->add_record_snapshot('message_contacts', $contact);
-                $event->trigger();
-            } else {
-                // Trigger event for unblocking a contact.
-                $event = \core\event\message_contact_unblocked::create(array(
-                    'objectid' => $contact->id,
-                    'userid' => $contact->userid,
-                    'relateduserid' => $contact->contactid,
-                    'context'  => context_user::instance($contact->userid)
-                ));
-                $event->add_record_snapshot('message_contacts', $contact);
-                $event->trigger();
-            }
-
-            return true;
-        } else {
-            // No change to blocking status.
-            return true;
-        }
-
-    } else {
-        // New contact record.
-        $contact = new stdClass();
-        $contact->userid = $userid;
-        $contact->contactid = $contactid;
-        $contact->blocked = $blocked;
-        $contact->id = $DB->insert_record('message_contacts', $contact);
-
-        $eventparams = array(
-            'objectid' => $contact->id,
-            'userid' => $contact->userid,
-            'relateduserid' => $contact->contactid,
-            'context'  => context_user::instance($contact->userid)
-        );
-
-        if ($blocked) {
-            $event = \core\event\message_contact_blocked::create($eventparams);
-        } else {
-            $event = \core\event\message_contact_added::create($eventparams);
-        }
-        // Trigger event.
-        $event->trigger();
-
-        return true;
-    }
-}
-
-/**
- * remove a contact
- *
- * @param int $contactid the user ID of the contact to remove
- * @param int $userid the user ID of the user we want to remove the contacts for, defaults to current user if not specified.
- * @return bool returns the result of delete_records()
- */
-function message_remove_contact($contactid, $userid = 0) {
-    global $USER, $DB;
-
-    if (empty($userid)) {
-        $userid = $USER->id;
-    }
-
-    if ($contact = $DB->get_record('message_contacts', array('userid' => $userid, 'contactid' => $contactid))) {
-        $DB->delete_records('message_contacts', array('id' => $contact->id));
-
-        // Trigger event for removing a contact.
-        $event = \core\event\message_contact_removed::create(array(
-            'objectid' => $contact->id,
-            'userid' => $contact->userid,
-            'relateduserid' => $contact->contactid,
-            'context'  => context_user::instance($contact->userid)
-        ));
-        $event->add_record_snapshot('message_contacts', $contact);
-        $event->trigger();
-
-        return true;
-    }
-
-    return false;
-}
-
-/**
- * Unblock a contact. Note that this reverts the previously blocked user back to a non-contact.
- *
- * @param int $contactid the user ID of the contact to unblock
- * @param int $userid the user ID of the user we want to unblock the contact for, defaults to current user
- *  if not specified.
- * @return bool returns the result of delete_records()
- */
-function message_unblock_contact($contactid, $userid = 0) {
-    return message_add_contact($contactid, 0, $userid);
-}
-
-/**
- * Block a user.
- *
- * @param int $contactid the user ID of the user to block
- * @param int $userid the user ID of the user we want to unblock the contact for, defaults to current user
- *  if not specified.
- * @return bool
- */
-function message_block_contact($contactid, $userid = 0) {
-    return message_add_contact($contactid, 1, $userid);
-}
-
-/**
- * Load a user's contact record
- *
- * @param int $contactid the user ID of the user whose contact record you want
- * @return array message contacts
- */
-function message_get_contact($contactid) {
-    global $USER, $DB;
-    return $DB->get_record('message_contacts', array('userid' => $USER->id, 'contactid' => $contactid));
-}
-
 /**
  * Search through course users.
  *
@@ -337,6 +187,7 @@ function message_search_users($courseids, $searchtext, $sort='', $exceptions='')
 
     $params = array(
         'userid' => $USER->id,
+        'userid2' => $USER->id,
         'query' => "%$searchtext%"
     );
 
@@ -357,10 +208,12 @@ function message_search_users($courseids, $searchtext, $sort='', $exceptions='')
 
     if (in_array(SITEID, $courseids)) {
         // Search on site level.
-        return $DB->get_records_sql("SELECT $ufields, mc.id as contactlistid, mc.blocked
+        return $DB->get_records_sql("SELECT $ufields, mc.id as contactlistid, mub.id as userblockedid
                                        FROM {user} u
                                        LEFT JOIN {message_contacts} mc
                                             ON mc.contactid = u.id AND mc.userid = :userid
+                                       LEFT JOIN {message_users_blocked} mub
+                                            ON mub.userid = :userid2 AND mub.blockeduserid = u.id
                                       WHERE u.deleted = '0' AND u.confirmed = '1'
                                             AND (".$DB->sql_like($fullname, ':query', false).")
                                             $except
@@ -379,11 +232,13 @@ function message_search_users($courseids, $searchtext, $sort='', $exceptions='')
 
         // Everyone who has a role assignment in this course or higher.
         // TODO: add enabled enrolment join here (skodak)
-        $users = $DB->get_records_sql("SELECT DISTINCT $ufields, mc.id as contactlistid, mc.blocked
+        $users = $DB->get_records_sql("SELECT DISTINCT $ufields, mc.id as contactlistid, mub.id as userblockedid
                                          FROM {user} u
                                          JOIN {role_assignments} ra ON ra.userid = u.id
                                          LEFT JOIN {message_contacts} mc
                                               ON mc.contactid = u.id AND mc.userid = :userid
+                                         LEFT JOIN {message_users_blocked} mub
+                                              ON mub.userid = :userid2 AND mub.blockeduserid = u.id
                                         WHERE u.deleted = '0' AND u.confirmed = '1'
                                               AND (".$DB->sql_like($fullname, ':query', false).")
                                               AND ra.contextid $contextwhere
diff --git a/message/pendingcontactrequests.php b/message/pendingcontactrequests.php
new file mode 100644 (file)
index 0000000..cab15e9
--- /dev/null
@@ -0,0 +1,93 @@
+<?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/>.
+
+/**
+ * A page displaying the user's contact requests.
+ *
+ * This is a temporary (well, should be) page until the new UI is introduced for 3.6.
+ *
+ * @package    core_message
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../config.php');
+require_once($CFG->dirroot . '/message/externallib.php');
+
+require_login(null, false);
+
+if (isguestuser()) {
+    redirect($CFG->wwwroot);
+}
+
+if (empty($CFG->messaging)) {
+    print_error('disabled', 'message');
+}
+
+$id = optional_param('id', '', PARAM_INT); // The id of the request.
+$action = optional_param('action', '', PARAM_ALPHA);
+
+// Confirm the request is able to be approved/disapproved.
+if ($id) {
+    $request = $DB->get_record('message_contact_requests', ['id' => $id, 'requesteduserid' => $USER->id], '*', MUST_EXIST);
+}
+
+// Use external functions as these are what we will be using in the new UI.
+if ($id && $action && confirm_sesskey()) {
+    if ($action == 'approve') {
+        core_message_external::confirm_contact_request($request->userid, $USER->id);
+    } else if ($action == 'decline') {
+        core_message_external::decline_contact_request($request->userid, $USER->id);
+    }
+
+    redirect(new moodle_url('/message/pendingcontactrequests.php'));
+}
+
+$table = new html_table();
+
+$headers = [];
+$headers[] = '';
+$headers[] = '';
+
+$table->head = $headers;
+
+// Use external functions as these are what we will be using in the new UI.
+if ($contactrequests = core_message_external::get_contact_requests($USER->id)) {
+    foreach ($contactrequests as $contactrequest) {
+        $approvelink = new moodle_url('/message/pendingcontactrequests.php', ['id' => $contactrequest->contactrequestid,
+            'action' => 'approve', 'sesskey' => sesskey()]);
+        $declinelink = new moodle_url('/message/pendingcontactrequests.php', ['id' => $contactrequest->contactrequestid,
+            'action' => 'decline', 'sesskey' => sesskey()]);
+
+        $cells = array();
+        $cells[] = fullname($contactrequest);
+        $cells[] = html_writer::link($approvelink, get_string('approve')) . " | " .
+            html_writer::link($declinelink, get_string('cancel'));
+        $table->data[] = new html_table_row($cells);
+    }
+}
+
+$url = new moodle_url('/message/pendingcontactrequests.php');
+$PAGE->set_url($url);
+
+$PAGE->set_context(context_user::instance($USER->id));
+$PAGE->set_pagelayout('standard');
+$PAGE->set_title('Pending contact requests');
+$PAGE->set_heading('Pending contact requests');
+
+echo $OUTPUT->header();
+echo html_writer::table($table);
+echo $OUTPUT->footer();
index 42f8192..0278d67 100644 (file)
@@ -102,20 +102,20 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
      * Test count_blocked_users.
      */
     public function test_count_blocked_users() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
-        // Create users to add to the admin's contact list.
+        // Create user to add to the admin's block list.
         $user1 = $this->getDataGenerator()->create_user();
-        $user2 = $this->getDataGenerator()->create_user();
 
         $this->assertEquals(0, \core_message\api::count_blocked_users());
 
-        // Add 1 blocked and 1 normal contact to admin's contact list.
-        message_add_contact($user1->id);
-        message_add_contact($user2->id, 1);
+        // Add 1 blocked user to admin's blocked user list.
+        \core_message\api::block_user($USER->id, $user1->id);
 
-        $this->assertEquals(0, \core_message\api::count_blocked_users($user2));
+        $this->assertEquals(0, \core_message\api::count_blocked_users($user1));
         $this->assertEquals(1, \core_message\api::count_blocked_users());
     }
 
@@ -140,7 +140,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user2 = self::getDataGenerator()->create_user($user2);
 
         // Block the second user.
-        message_block_contact($user2->id, $user1->id);
+        \core_message\api::block_user($user1->id, $user2->id);
 
         $user3 = new stdClass();
         $user3->firstname = 'User';
@@ -247,9 +247,9 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->getDataGenerator()->enrol_user($user1->id, $course5->id, $role->id);
 
         // Add some users as contacts.
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user3->id, 0, $user1->id);
-        message_add_contact($user4->id, 0, $user1->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
 
         // Remove the viewparticipants capability from one of the courses.
         $course5context = context_course::instance($course5->id);
@@ -280,25 +280,31 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         // Create some users.
         $user1 = self::getDataGenerator()->create_user();
         $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
 
         // The person doing the search.
         $this->setUser($user1);
 
         // Send some messages back and forth.
         $time = 1;
-        $this->send_fake_message($user1, $user2, 'Yo!', 0, $time);
-        $this->send_fake_message($user2, $user1, 'Sup mang?', 0, $time + 1);
-        $this->send_fake_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 2);
-        $this->send_fake_message($user2, $user1, 'Word.', 0, $time + 3);
+        $this->send_fake_message($user3, $user1, 'Don\'t block me.', 0, $time);
+        $this->send_fake_message($user1, $user2, 'Yo!', 0, $time + 1);
+        $this->send_fake_message($user2, $user1, 'Sup mang?', 0, $time + 2);
+        $this->send_fake_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 3);
+        $this->send_fake_message($user2, $user1, 'Word.', 0, $time + 4);
+
+        // Block user 3.
+        \core_message\api::block_user($user1->id, $user3->id);
 
         // Perform a search.
         $messages = \core_message\api::search_messages($user1->id, 'o');
 
         // Confirm the data is correct.
-        $this->assertEquals(2, count($messages));
+        $this->assertEquals(3, count($messages));
 
         $message1 = $messages[0];
         $message2 = $messages[1];
+        $message3 = $messages[2];
 
         $this->assertEquals($user2->id, $message1->userid);
         $this->assertEquals($user2->id, $message1->useridfrom);
@@ -321,6 +327,17 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertTrue($message2->isread);
         $this->assertFalse($message2->isblocked);
         $this->assertNull($message2->unreadcount);
+
+        $this->assertEquals($user3->id, $message3->userid);
+        $this->assertEquals($user3->id, $message3->useridfrom);
+        $this->assertEquals(fullname($user3), $message3->fullname);
+        $this->assertTrue($message3->ismessaging);
+        $this->assertEquals('Don\'t block me.', $message3->lastmessage);
+        $this->assertNotEmpty($message3->messageid);
+        $this->assertNull($message3->isonline);
+        $this->assertFalse($message3->isread);
+        $this->assertTrue($message3->isblocked);
+        $this->assertNull($message3->unreadcount);
     }
 
     /**
@@ -939,15 +956,16 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user5 = self::getDataGenerator()->create_user($user5);
 
         // Add some users as contacts.
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user3->id, 0, $user1->id);
-        message_add_contact($user4->id, 0, $user1->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
 
         // Retrieve the contacts.
         $contacts = \core_message\api::get_contacts($user1->id);
 
         // Confirm the data is correct.
         $this->assertEquals(3, count($contacts));
+        usort($contacts, ['static', 'sort_contacts']);
 
         $contact1 = $contacts[0];
         $contact2 = $contacts[1];
@@ -1315,7 +1333,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->setUser($user1);
 
         // Block the second user.
-        message_block_contact($user2->id);
+        \core_message\api::block_user($user1->id, $user2->id);
 
         // Check that the second user can no longer send the first user a message.
         $this->assertFalse(\core_message\api::can_post_message($user1, $user2));
@@ -1343,10 +1361,17 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertTrue(\core_message\api::is_user_non_contact_blocked($user2));
 
         // Add the first user as a contact for the second user.
-        message_add_contact($user1->id, 0, $user2->id);
+        \core_message\api::add_contact($user2->id, $user1->id);
 
         // Check that the return result is now false.
         $this->assertFalse(\core_message\api::is_user_non_contact_blocked($user2));
+
+        // Set the first user's preference to not receive messages from non-contacts.
+        set_user_preference('message_blocknoncontacts', 1, $user1->id);
+        $this->setUser($user2);
+        // Confirm it is still false. We want to ensure a contact request works both ways
+        // as it is now an agreement between users.
+        $this->assertFalse(\core_message\api::is_user_non_contact_blocked($user1));
     }
 
     /**
@@ -1363,16 +1388,19 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // User shouldn't be blocked.
         $this->assertFalse(\core_message\api::is_user_blocked($user1->id, $user2->id));
+        $this->assertDebuggingCalled();
 
         // Block the user.
-        message_block_contact($user2->id);
+        \core_message\api::block_user($user1->id, $user2->id);
 
         // User should be blocked.
         $this->assertTrue(\core_message\api::is_user_blocked($user1->id, $user2->id));
+        $this->assertDebuggingCalled();
 
         // Unblock the user.
-        message_unblock_contact($user2->id);
+        \core_message\api::unblock_user($user1->id, $user2->id);
         $this->assertFalse(\core_message\api::is_user_blocked($user1->id, $user2->id));
+        $this->assertDebuggingCalled();
     }
 
     /**
@@ -1386,13 +1414,14 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->setUser($user1);
 
         // Block the admin user.
-        message_block_contact(2);
+        \core_message\api::block_user($user1->id, 2);
 
         // Now change to the admin user.
         $this->setAdminUser();
 
         // As the admin you should still be able to send messages to the user.
         $this->assertFalse(\core_message\api::is_user_blocked($user1->id));
+        $this->assertDebuggingCalled();
     }
 
     /*
@@ -1623,13 +1652,12 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user2 = $this->getDataGenerator()->create_user();
 
         // Add users to the admin's contact list.
-        message_add_contact($user1->id);
-        message_add_contact($user2->id, 1);
+        \core_message\api::block_user($USER->id, $user2->id);
 
         $this->assertCount(1, \core_message\api::get_blocked_users($USER->id));
 
         // Block other user.
-        message_block_contact($user1->id);
+        \core_message\api::block_user($USER->id, $user1->id);
         $this->assertCount(2, \core_message\api::get_blocked_users($USER->id));
 
         // Test deleting users.
@@ -1649,9 +1677,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user4 = self::getDataGenerator()->create_user();
 
         // Add the users to each of their contacts.
-        message_add_contact($user1->id, 0, $user2->id);
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user3->id, 0, $user2->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user2->id, $user3->id);
 
         $this->send_fake_message($user1, $user2);
         $this->send_fake_message($user1, $user2);
@@ -1669,13 +1696,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Get the contacts and the unread message count.
         $messages = \core_message\api::get_contacts_with_unread_message_count($user2->id);
-
         // Confirm the size is correct.
         $this->assertCount(2, $messages);
         ksort($messages);
 
         $messageinfo1 = array_shift($messages);
         $messageinfo2 = array_shift($messages);
+
         $this->assertEquals($user1->id, $messageinfo1->id);
         $this->assertEquals(4, $messageinfo1->messagecount);
         $this->assertEquals($user3->id, $messageinfo2->id);
@@ -1742,7 +1769,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user2 = self::getDataGenerator()->create_user();
 
         // Add the users to each of their contacts.
-        message_add_contact($user1->id, 0, $user2->id);
+        \core_message\api::add_contact($user2->id, $user1->id);
 
         // Check we get the correct message count.
         $messages = \core_message\api::get_contacts_with_unread_message_count($user2->id);
@@ -1768,8 +1795,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user4 = self::getDataGenerator()->create_user();
 
         // Add a user to the contact list of the users we are testing this function with.
-        message_add_contact($user4->id, 0, $user1->id);
-        message_add_contact($user4->id, 0, $user2->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
+        \core_message\api::add_contact($user2->id, $user4->id);
 
         $this->send_fake_message($user1, $user2);
         $this->send_fake_message($user1, $user2);
@@ -1939,4 +1966,274 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals($conversationid,
             \core_message\api::get_conversation_between_users([$user1->id, $user2->id]));
     }
+
+    /**
+     * Test can create a contact request.
+     */
+    public function test_can_create_contact_request() {
+        global $CFG;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+        $this->assertFalse(\core_message\api::can_create_contact($user1->id, $user2->id));
+
+        // Re-enable messaging.
+        $CFG->messaging = 1;
+
+        // Allow users to message anyone site-wide.
+        $CFG->messagingallusers = 1;
+        $this->assertTrue(\core_message\api::can_create_contact($user1->id, $user2->id));
+
+        // Disallow users from messaging anyone site-wide.
+        $CFG->messagingallusers = 0;
+        $this->assertFalse(\core_message\api::can_create_contact($user1->id, $user2->id));
+
+        // Put the users in the same course so a contact request should be possible.
+        $course = self::getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->assertTrue(\core_message\api::can_create_contact($user1->id, $user2->id));
+    }
+
+    /**
+     * Test creating a contact request.
+     */
+    public function test_create_contact_request() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+
+        $request = $DB->get_records('message_contact_requests');
+
+        $this->assertCount(1, $request);
+
+        $request = reset($request);
+
+        $this->assertEquals($user1->id, $request->userid);
+        $this->assertEquals($user2->id, $request->requesteduserid);
+    }
+
+    /**
+     * Test confirming a contact request.
+     */
+    public function test_confirm_contact_request() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+
+        \core_message\api::confirm_contact_request($user1->id, $user2->id);
+
+        $this->assertEquals(0, $DB->count_records('message_contact_requests'));
+
+        $contact = $DB->get_records('message_contacts');
+
+        $this->assertCount(1, $contact);
+
+        $contact = reset($contact);
+
+        $this->assertEquals($user1->id, $contact->userid);
+        $this->assertEquals($user2->id, $contact->contactid);
+    }
+
+    /**
+     * Test declining a contact request.
+     */
+    public function test_decline_contact_request() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+
+        \core_message\api::decline_contact_request($user1->id, $user2->id);
+
+        $this->assertEquals(0, $DB->count_records('message_contact_requests'));
+        $this->assertEquals(0, $DB->count_records('message_contacts'));
+    }
+
+    /**
+     * Test retrieving contact requests.
+     */
+    public function test_get_contact_requests() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Block one user, their request should not show up.
+        \core_message\api::block_user($user1->id, $user3->id);
+
+        \core_message\api::create_contact_request($user2->id, $user1->id);
+        \core_message\api::create_contact_request($user3->id, $user1->id);
+
+        $requests = \core_message\api::get_contact_requests($user1->id);
+
+        $this->assertCount(1, $requests);
+
+        $request = reset($requests);
+
+        $this->assertEquals($user2->id, $request->id);
+        $this->assertEquals($user2->picture, $request->picture);
+        $this->assertEquals($user2->firstname, $request->firstname);
+        $this->assertEquals($user2->lastname, $request->lastname);
+        $this->assertEquals($user2->firstnamephonetic, $request->firstnamephonetic);
+        $this->assertEquals($user2->lastnamephonetic, $request->lastnamephonetic);
+        $this->assertEquals($user2->middlename, $request->middlename);
+        $this->assertEquals($user2->alternatename, $request->alternatename);
+        $this->assertEquals($user2->email, $request->email);
+    }
+
+    /**
+     * Test adding contacts.
+     */
+    public function test_add_contact() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::add_contact($user1->id, $user2->id);
+
+        $contact = $DB->get_records('message_contacts');
+
+        $this->assertCount(1, $contact);
+
+        $contact = reset($contact);
+
+        $this->assertEquals($user1->id, $contact->userid);
+        $this->assertEquals($user2->id, $contact->contactid);
+    }
+
+    /**
+     * Test removing contacts.
+     */
+    public function test_remove_contact() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::remove_contact($user1->id, $user2->id);
+
+        $this->assertEquals(0, $DB->count_records('message_contacts'));
+    }
+
+    /**
+     * Test blocking users.
+     */
+    public function test_block_user() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        $blockedusers = $DB->get_records('message_users_blocked');
+
+        $this->assertCount(1, $blockedusers);
+
+        $blockeduser = reset($blockedusers);
+
+        $this->assertEquals($user1->id, $blockeduser->userid);
+        $this->assertEquals($user2->id, $blockeduser->blockeduserid);
+    }
+
+    /**
+     * Test unblocking users.
+     */
+    public function test_unblock_user() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::block_user($user1->id, $user2->id);
+        \core_message\api::unblock_user($user1->id, $user2->id);
+
+        $this->assertEquals(0, $DB->count_records('message_users_blocked'));
+    }
+
+    /**
+     * Test is contact check.
+     */
+    public function test_is_contact() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        \core_message\api::add_contact($user1->id, $user2->id);
+
+        $this->assertTrue(\core_message\api::is_contact($user1->id, $user2->id));
+        $this->assertTrue(\core_message\api::is_contact($user2->id, $user1->id));
+        $this->assertFalse(\core_message\api::is_contact($user2->id, $user3->id));
+    }
+
+    /**
+     * Test get contact.
+     */
+    public function test_get_contact() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::add_contact($user1->id, $user2->id);
+
+        $contact = \core_message\api::get_contact($user1->id, $user2->id);
+
+        $this->assertEquals($user1->id, $contact->userid);
+        $this->assertEquals($user2->id, $contact->contactid);
+    }
+
+    /**
+     * Test is blocked checked.
+     */
+    public function test_is_blocked() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->assertFalse(\core_message\api::is_blocked($user1->id, $user2->id));
+        $this->assertFalse(\core_message\api::is_blocked($user2->id, $user1->id));
+
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        $this->assertTrue(\core_message\api::is_blocked($user1->id, $user2->id));
+        $this->assertFalse(\core_message\api::is_blocked($user2->id, $user1->id));
+    }
+
+    /**
+     * Test the contact request exist check.
+     */
+    public function test_does_contact_request_exist() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->assertFalse(\core_message\api::does_contact_request_exist($user1->id, $user2->id));
+        $this->assertFalse(\core_message\api::does_contact_request_exist($user2->id, $user1->id));
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+
+        $this->assertTrue(\core_message\api::does_contact_request_exist($user1->id, $user2->id));
+        $this->assertTrue(\core_message\api::does_contact_request_exist($user2->id, $user1->id));
+    }
+
+    /**
+     * Comparison function for sorting contacts.
+     *
+     * @param stdClass $a
+     * @param stdClass $b
+     * @return bool
+     */
+    protected static function sort_contacts($a, $b) {
+        return $a->userid > $b->userid;
+    }
 }
index 062ed01..135e154 100644 (file)
@@ -6,16 +6,34 @@ Feature: Manage contacts
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email            |
+      | username | firstname | lastname | email                |
       | user1    | User      | 1        | user1@example.com    |
       | user2    | User      | 2        | user2@example.com    |
       | user3    | User      | 3        | user3@example.com    |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | messagingallusers | 1 |
+    And I log out
     And I log in as "user1"
     And I view the "User 2" contact in the message area
     And I click on "Add contact" "link"
     And I view the "User 3" contact in the message area
     And I click on "Add contact" "link"
     And I log out
+    # Approve the contact request for user2
+    And I log in as "user2"
+    And I open the notification popover
+    And I click on "View full notification" "link" in the "#nav-notification-popover-container" "css_element"
+    And I click on "Go to" "link" in the "[data-region=footer]" "css_element"
+    And I click on "Approve" "link" in the "User 1" "table_row"
+    And I log out
+    # Approve the contact request for user3
+    And I log in as "user3"
+    And I open the notification popover
+    And I click on "View full notification" "link" in the "#nav-notification-popover-container" "css_element"
+    And I click on "Go to" "link" in the "[data-region=footer]" "css_element"
+    And I click on "Approve" "link" in the "User 1" "table_row"
+    And I log out
 
   Scenario: Add contact shows in contacts tab
     When I log in as "user1"
index 3f63ed0..25660ce 100644 (file)
@@ -52,6 +52,8 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
      * Test the message contact added event.
      */
     public function test_message_contact_added() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
@@ -60,7 +62,7 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
 
         // Trigger and capture the event when adding a contact.
         $sink = $this->redirectEvents();
-        message_add_contact($user->id);
+        \core_message\api::add_contact($USER->id, $user->id);
         $events = $sink->get_events();
         $event = reset($events);
 
@@ -78,6 +80,8 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
      * Test the message contact removed event.
      */
     public function test_message_contact_removed() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
@@ -85,11 +89,11 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
         $user = $this->getDataGenerator()->create_user();
 
         // Add the user to the admin's contact list.
-        message_add_contact($user->id);
+        \core_message\api::add_contact($USER->id, $user->id);
 
         // Trigger and capture the event when adding a contact.
         $sink = $this->redirectEvents();
-        message_remove_contact($user->id);
+        \core_message\api::remove_contact($USER->id, $user->id);
         $events = $sink->get_events();
         $event = reset($events);
 
@@ -104,61 +108,37 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
     }
 
     /**
-     * Test the message contact blocked event.
+     * Test the message user blocked event.
      */
-    public function test_message_contact_blocked() {
+    public function test_message_user_blocked() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
         // Create a user to add to the admin's contact list.
         $user = $this->getDataGenerator()->create_user();
-        $user2 = $this->getDataGenerator()->create_user();
 
         // Add the user to the admin's contact list.
-        message_add_contact($user->id);
+        \core_message\api::add_contact($USER->id, $user->id);
 
         // Trigger and capture the event when blocking a contact.
         $sink = $this->redirectEvents();
-        message_block_contact($user->id);
+        \core_message\api::block_user($USER->id, $user->id);
         $events = $sink->get_events();
         $event = reset($events);
 
         // Check that the event data is valid.
-        $this->assertInstanceOf('\core\event\message_contact_blocked', $event);
+        $this->assertInstanceOf('\core\event\message_user_blocked', $event);
         $this->assertEquals(context_user::instance(2), $event->get_context());
-        $expected = array(SITEID, 'message', 'block contact', 'index.php?user1=' . $user->id . '&amp;user2=2', $user->id);
-        $this->assertEventLegacyLogData($expected, $event);
-        $url = new moodle_url('/message/index.php', array('user1' => $event->userid, 'user2' => $event->relateduserid));
-        $this->assertEquals($url, $event->get_url());
-
-        // Make sure that the contact blocked event is not triggered again.
-        $sink->clear();
-        message_block_contact($user->id);
-        $events = $sink->get_events();
-        $event = reset($events);
-        $this->assertEmpty($event);
-        // Make sure that we still have 1 blocked user.
-        $this->assertEquals(1, \core_message\api::count_blocked_users());
-
-        // Now blocking a user that is not a contact.
-        $sink->clear();
-        message_block_contact($user2->id);
-        $events = $sink->get_events();
-        $event = reset($events);
-
-        // Check that the event data is valid.
-        $this->assertInstanceOf('\core\event\message_contact_blocked', $event);
-        $this->assertEquals(context_user::instance(2), $event->get_context());
-        $expected = array(SITEID, 'message', 'block contact', 'index.php?user1=' . $user2->id . '&amp;user2=2', $user2->id);
-        $this->assertEventLegacyLogData($expected, $event);
-        $url = new moodle_url('/message/index.php', array('user1' => $event->userid, 'user2' => $event->relateduserid));
-        $this->assertEquals($url, $event->get_url());
     }
 
     /**
-     * Test the message contact unblocked event.
+     * Test the message user unblocked event.
      */
-    public function test_message_contact_unblocked() {
+    public function test_message_user_unblocked() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
@@ -166,39 +146,25 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
         $user = $this->getDataGenerator()->create_user();
 
         // Add the user to the admin's contact list.
-        message_add_contact($user->id);
+        \core_message\api::add_contact($USER->id, $user->id);
 
         // Block the user.
-        message_block_contact($user->id);
+        \core_message\api::block_user($USER->id, $user->id);
         // Make sure that we have 1 blocked user.
         $this->assertEquals(1, \core_message\api::count_blocked_users());
 
         // Trigger and capture the event when unblocking a contact.
         $sink = $this->redirectEvents();
-        message_unblock_contact($user->id);
+        \core_message\api::unblock_user($USER->id, $user->id);
         $events = $sink->get_events();
         $event = reset($events);
 
         // Check that the event data is valid.
-        $this->assertInstanceOf('\core\event\message_contact_unblocked', $event);
+        $this->assertInstanceOf('\core\event\message_user_unblocked', $event);
         $this->assertEquals(context_user::instance(2), $event->get_context());
-        $expected = array(SITEID, 'message', 'unblock contact', 'index.php?user1=' . $user->id . '&amp;user2=2', $user->id);
-        $this->assertEventLegacyLogData($expected, $event);
-        $url = new moodle_url('/message/index.php', array('user1' => $event->userid, 'user2' => $event->relateduserid));
-        $this->assertEquals($url, $event->get_url());
 
         // Make sure that we have no blocked users.
         $this->assertEmpty(\core_message\api::count_blocked_users());
-
-        // Make sure that the contact unblocked event is not triggered again.
-        $sink->clear();
-        message_unblock_contact($user->id);
-        $events = $sink->get_events();
-        $event = reset($events);
-        $this->assertEmpty($event);
-
-        // Make sure that we still have no blocked users.
-        $this->assertEmpty(\core_message\api::count_blocked_users());
     }
 
     /**
index f78cc46..784ad14 100644 (file)
@@ -91,42 +91,154 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
     }
 
     /**
-     * Test send_instant_messages
+     * Test send_instant_messages.
      */
     public function test_send_instant_messages() {
+        global $DB, $USER;
 
-        global $DB, $USER, $CFG;
+        $this->resetAfterTest();
 
-        $this->resetAfterTest(true);
         // Transactions used in tests, tell phpunit use alternative reset method.
         $this->preventResetByRollback();
 
-        // Turn off all message processors (so nothing is really sent)
-        require_once($CFG->dirroot . '/message/lib.php');
-        $messageprocessors = get_message_processors();
-        foreach($messageprocessors as $messageprocessor) {
-            $messageprocessor->enabled = 0;
-            $DB->update_record('message_processors', $messageprocessor);
-        }
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
 
-        // Set the required capabilities by the external function
-        $contextid = context_system::instance()->id;
-        $roleid = $this->assignUserCapability('moodle/site:sendmessage', $contextid);
+        $this->setUser($user1);
+
+        // Create test message data.
+        $message1 = array();
+        $message1['touserid'] = $user2->id;
+        $message1['text'] = 'the message.';
+        $message1['clientmsgid'] = 4;
+        $messages = array($message1);
+
+        $sentmessages = core_message_external::send_instant_messages($messages);
+        $sentmessages = external_api::clean_returnvalue(core_message_external::send_instant_messages_returns(), $sentmessages);
+
+        $sentmessage = reset($sentmessages);
+
+        $sql = "SELECT m.*, mcm.userid as useridto
+                 FROM {messages} m
+           INNER JOIN {message_conversations} mc
+                   ON m.conversationid = mc.id
+           INNER JOIN {message_conversation_members} mcm
+                   ON mcm.conversationid = mc.id
+                WHERE mcm.userid != ?
+                  AND m.id = ?";
+        $themessage = $DB->get_record_sql($sql, [$USER->id, $sentmessage['msgid']]);
+
+        // Confirm that the message was inserted correctly.
+        $this->assertEquals($themessage->useridfrom, $user1->id);
+        $this->assertEquals($themessage->useridto, $message1['touserid']);
+        $this->assertEquals($themessage->smallmessage, $message1['text']);
+        $this->assertEquals($sentmessage['clientmsgid'], $message1['clientmsgid']);
+    }
+
+    /**
+     * Test send_instant_messages to a user who has blocked you.
+     */
+    public function test_send_instant_messages_blocked_user() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Transactions used in tests, tell phpunit use alternative reset method.
+        $this->preventResetByRollback();
 
         $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        \core_message\api::block_user($user2->id, $user1->id);
 
         // Create test message data.
         $message1 = array();
-        $message1['touserid'] = $user1->id;
+        $message1['touserid'] = $user2->id;
         $message1['text'] = 'the message.';
         $message1['clientmsgid'] = 4;
         $messages = array($message1);
 
         $sentmessages = core_message_external::send_instant_messages($messages);
+        $sentmessages = external_api::clean_returnvalue(core_message_external::send_instant_messages_returns(), $sentmessages);
 
-        // We need to execute the return values cleaning process to simulate the web service server.
+        $sentmessage = reset($sentmessages);
+
+        $this->assertEquals(get_string('userisblockingyou', 'message'), $sentmessage['errormessage']);
+
+        $this->assertEquals(0, $DB->count_records('messages'));
+    }
+
+    /**
+     * Test send_instant_messages when sending a message to a non-contact who has blocked non-contacts.
+     */
+    public function test_send_instant_messages_block_non_contacts() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Transactions used in tests, tell phpunit use alternative reset method.
+        $this->preventResetByRollback();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Set the user preference so user 2 does not accept messages from non-contacts.
+        set_user_preference('message_blocknoncontacts', 1, $user2);
+
+        // Create test message data.
+        $message1 = array();
+        $message1['touserid'] = $user2->id;
+        $message1['text'] = 'the message.';
+        $message1['clientmsgid'] = 4;
+        $messages = array($message1);
+
+        $sentmessages = core_message_external::send_instant_messages($messages);
+        $sentmessages = external_api::clean_returnvalue(core_message_external::send_instant_messages_returns(), $sentmessages);
+
+        $sentmessage = reset($sentmessages);
+
+        $this->assertEquals(get_string('userisblockingyounoncontact', 'message', fullname($user2)), $sentmessage['errormessage']);
+
+        $this->assertEquals(0, $DB->count_records('messages'));
+    }
+
+    /**
+     * Test send_instant_messages when sending a message to a contact who has blocked non-contacts.
+     */
+    public function test_send_instant_messages_block_non_contacts_but_am_contact() {
+        global $DB, $USER;
+
+        $this->resetAfterTest(true);
+
+        // Transactions used in tests, tell phpunit use alternative reset method.
+        $this->preventResetByRollback();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Set the user preference so user 2 does not accept messages from non-contacts.
+        set_user_preference('message_blocknoncontacts', 1, $user2);
+
+        \core_message\api::add_contact($user1->id, $user2->id);
+
+        // Create test message data.
+        $message1 = array();
+        $message1['touserid'] = $user2->id;
+        $message1['text'] = 'the message.';
+        $message1['clientmsgid'] = 4;
+        $messages = array($message1);
+
+        $sentmessages = core_message_external::send_instant_messages($messages);
         $sentmessages = external_api::clean_returnvalue(core_message_external::send_instant_messages_returns(), $sentmessages);
 
+        $sentmessage = reset($sentmessages);
+
         $sql = "SELECT m.*, mcm.userid as useridto
                  FROM {messages} m
            INNER JOIN {message_conversations} mc
@@ -135,13 +247,75 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                    ON mcm.conversationid = mc.id
                 WHERE mcm.userid != ?
                   AND m.id = ?";
-        $themessage = $DB->get_record_sql($sql, [$USER->id, $sentmessages[0]['msgid']]);
+        $themessage = $DB->get_record_sql($sql, [$USER->id, $sentmessage['msgid']]);
 
         // Confirm that the message was inserted correctly.
-        $this->assertEquals($themessage->useridfrom, $USER->id);
+        $this->assertEquals($themessage->useridfrom, $user1->id);
         $this->assertEquals($themessage->useridto, $message1['touserid']);
         $this->assertEquals($themessage->smallmessage, $message1['text']);
-        $this->assertEquals($sentmessages[0]['clientmsgid'], $message1['clientmsgid']);
+        $this->assertEquals($sentmessage['clientmsgid'], $message1['clientmsgid']);
+    }
+
+    /**
+     * Test send_instant_messages with no capabilities
+     */
+    public function test_send_instant_messages_no_capability() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Transactions used in tests, tell phpunit use alternative reset method.
+        $this->preventResetByRollback();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Unset the required capabilities by the external function.
+        $contextid = context_system::instance()->id;
+        $userrole = $DB->get_record('role', array('shortname' => 'user'));
+        $this->unassignUserCapability('moodle/site:sendmessage', $contextid, $userrole->id);
+
+        // Create test message data.
+        $message1 = array();
+        $message1['touserid'] = $user2->id;
+        $message1['text'] = 'the message.';
+        $message1['clientmsgid'] = 4;
+        $messages = array($message1);
+
+        $this->expectException('required_capability_exception');
+        core_message_external::send_instant_messages($messages);
+    }
+
+    /**
+     * Test send_instant_messages when messaging is disabled.
+     */
+    public function test_send_instant_messages_messaging_disabled() {
+        global $CFG;
+
+        $this->resetAfterTest(true);
+
+        // Transactions used in tests, tell phpunit use alternative reset method.
+        $this->preventResetByRollback();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+
+        // Create test message data.
+        $message1 = array();
+        $message1['touserid'] = $user2->id;
+        $message1['text'] = 'the message.';
+        $message1['clientmsgid'] = 4;
+        $messages = array($message1);
+
+        $this->expectException('moodle_exception');
+        core_message_external::send_instant_messages($messages);
     }
 
     /**
@@ -159,21 +333,25 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Adding a contact.
         $return = core_message_external::create_contacts(array($user2->id));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Adding a contact who is already a contact.
         $return = core_message_external::create_contacts(array($user2->id));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Adding multiple contacts.
         $return = core_message_external::create_contacts(array($user3->id, $user4->id));
+        $this->assertDebuggingCalledCount(2);
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Adding a non-existing user.
         $return = core_message_external::create_contacts(array(99999));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertCount(1, $return);
         $return = array_pop($return);
@@ -182,6 +360,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Adding contacts with valid and invalid parameters.
         $return = core_message_external::create_contacts(array($user5->id, 99999));
+        $this->assertDebuggingCalledCount(2);
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertCount(1, $return);
         $return = array_pop($return);
@@ -207,8 +386,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $user5 = self::getDataGenerator()->create_user();
         $user6 = self::getDataGenerator()->create_user();
         $this->setUser($user1);
-        $this->assertEquals(array(), core_message_external::create_contacts(
-            array($user3->id, $user4->id, $user5->id, $user6->id)));
+
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
+        \core_message\api::add_contact($user1->id, $user5->id);
+        \core_message\api::add_contact($user1->id, $user6->id);
 
         // Removing a non-contact.
         $return = core_message_external::delete_contacts(array($user2->id));
@@ -248,25 +430,32 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $user4 = self::getDataGenerator()->create_user();
         $user5 = self::getDataGenerator()->create_user();
         $this->setUser($user1);
-        $this->assertEquals(array(), core_message_external::create_contacts(array($user3->id, $user4->id, $user5->id)));
+
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
+        \core_message\api::add_contact($user1->id, $user5->id);
 
         // Blocking a contact.
         $return = core_message_external::block_contacts(array($user2->id));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Blocking a contact who is already a contact.
         $return = core_message_external::block_contacts(array($user2->id));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Blocking multiple contacts.
         $return = core_message_external::block_contacts(array($user3->id, $user4->id));
+        $this->assertDebuggingCalledCount(2);
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Blocking a non-existing user.
         $return = core_message_external::block_contacts(array(99999));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
         $this->assertCount(1, $return);
         $return = array_pop($return);
@@ -275,6 +464,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Blocking contacts with valid and invalid parameters.
         $return = core_message_external::block_contacts(array($user5->id, 99999));
+        $this->assertDebuggingCalledCount(2);
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
         $this->assertCount(1, $return);
         $return = array_pop($return);
@@ -300,33 +490,494 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $user5 = self::getDataGenerator()->create_user();
         $user6 = self::getDataGenerator()->create_user();
         $this->setUser($user1);
-        $this->assertEquals(array(), core_message_external::create_contacts(
-            array($user3->id, $user4->id, $user5->id, $user6->id)));
+
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
+        \core_message\api::add_contact($user1->id, $user5->id);
+        \core_message\api::add_contact($user1->id, $user6->id);
 
         // Removing a non-contact.
         $return = core_message_external::unblock_contacts(array($user2->id));
+        $this->assertDebuggingCalled();
         $this->assertNull($return);
 
         // Removing one contact.
         $return = core_message_external::unblock_contacts(array($user3->id));
+        $this->assertDebuggingCalled();
         $this->assertNull($return);
 
         // Removing multiple contacts.
         $return = core_message_external::unblock_contacts(array($user4->id, $user5->id));
+        $this->assertDebuggingCalledCount(2);
         $this->assertNull($return);
 
         // Removing contact from unexisting user.
         $return = core_message_external::unblock_contacts(array(99999));
+        $this->assertDebuggingCalled();
         $this->assertNull($return);
 
         // Removing mixed valid and invalid data.
         $return = core_message_external::unblock_contacts(array($user6->id, 99999));
+        $this->assertDebuggingCalledCount(2);
         $this->assertNull($return);
 
         // Try to unblock a contact of another user contact list, should throw an exception.
         // All assertions must be added before this point.
         $this->expectException('required_capability_exception');
         core_message_external::unblock_contacts(array($user2->id), $user3->id);
+        $this->assertDebuggingCalled();
+    }
+
+    /**
+     * Test getting contact requests.
+     */
+    public function test_get_contact_requests() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Block one user, their request should not show up.
+        \core_message\api::block_user($user1->id, $user3->id);
+
+        \core_message\api::create_contact_request($user2->id, $user1->id);
+        \core_message\api::create_contact_request($user3->id, $user1->id);
+
+        $requests = core_message_external::get_contact_requests($user1->id);
+        $requests = external_api::clean_returnvalue(core_message_external::get_contact_requests_returns(), $requests);
+
+        $this->assertCount(1, $requests);
+
+        $request = reset($requests);
+
+        $this->assertEquals($user2->id, $request['id']);
+        $this->assertEquals($user2->picture, $request['picture']);
+        $this->assertEquals($user2->firstname, $request['firstname']);
+        $this->assertEquals($user2->lastname, $request['lastname']);
+        $this->assertEquals($user2->firstnamephonetic, $request['firstnamephonetic']);
+        $this->assertEquals($user2->lastnamephonetic, $request['lastnamephonetic']);
+        $this->assertEquals($user2->middlename, $request['middlename']);
+        $this->assertEquals($user2->alternatename, $request['alternatename']);
+        $this->assertEquals($user2->email, $request['email']);
+    }
+
+    /**
+     * Test getting contact requests with messaging disabled.
+     */
+    public function test_get_contact_requests_messaging_disabled() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+
+        // Ensure an exception is thrown.
+        $this->expectException('moodle_exception');
+        core_message_external::get_contact_requests($user1->id);
+    }
+
+    /**
+     * Test getting contact requests with no permission.
+     */
+    public function test_get_contact_requests_no_permission() {
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user3);
+
+        // Ensure an exception is thrown.
+        $this->expectException('required_capability_exception');
+        core_message_external::create_contact_request($user1->id, $user2->id);
+    }
+
+    /**
+     * Test creating a contact request.
+     */
+    public function test_create_contact_request() {
+        global $CFG, $DB;
+
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Allow users to message anyone site-wide.
+        $CFG->messagingallusers = 1;
+
+        $return = core_message_external::create_contact_request($user1->id, $user2->id);
+        $return = external_api::clean_returnvalue(core_message_external::create_contact_request_returns(), $return);
+        $this->assertEquals(array(), $return);
+
+        $request = $DB->get_records('message_contact_requests');
+
+        $this->assertCount(1, $request);
+
+        $request = reset($request);
+
+        $this->assertEquals($user1->id, $request->userid);
+        $this->assertEquals($user2->id, $request->requesteduserid);
+    }
+
+    /**
+     * Test creating a contact request when not allowed.
+     */
+    public function test_create_contact_request_not_allowed() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        $CFG->messagingallusers = 0;
+
+        $return = core_message_external::create_contact_request($user1->id, $user2->id);
+        $return = external_api::clean_returnvalue(core_message_external::create_contact_request_returns(), $return);
+
+        $warning = reset($return);
+
+        $this->assertEquals('user', $warning['item']);
+        $this->assertEquals($user2->id, $warning['itemid']);
+        $this->assertEquals('cannotcreatecontactrequest', $warning['warningcode']);
+        $this->assertEquals('You are unable to create a contact request for this user', $warning['message']);
+    }
+
+    /**
+     * Test creating a contact request with messaging disabled.
+     */
+    public function test_create_contact_request_messaging_disabled() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+
+        // Ensure an exception is thrown.
+        $this->expectException('moodle_exception');
+        core_message_external::create_contact_request($user1->id, $user2->id);
+    }
+
+    /**
+     * Test creating a contact request with no permission.
+     */
+    public function test_create_contact_request_no_permission() {
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user3);
+
+        // Ensure an exception is thrown.
+        $this->expectException('required_capability_exception');
+        core_message_external::create_contact_request($user1->id, $user2->id);
+    }
+
+    /**
+     * Test confirming a contact request.
+     */
+    public function test_confirm_contact_request() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+
+        $this->setUser($user2);
+
+        $return = core_message_external::confirm_contact_request($user1->id, $user2->id);
+        $return = external_api::clean_returnvalue(core_message_external::confirm_contact_request_returns(), $return);
+        $this->assertEquals(array(), $return);
+
+        $this->assertEquals(0, $DB->count_records('message_contact_requests'));
+
+        $contact = $DB->get_records('message_contacts');
+
+        $this->assertCount(1, $contact);
+
+        $contact = reset($contact);
+
+        $this->assertEquals($user1->id, $contact->userid);
+        $this->assertEquals($user2->id, $contact->contactid);
+    }
+
+    /**
+     * Test confirming a contact request with messaging disabled.
+     */
+    public function test_confirm_contact_request_messaging_disabled() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+
+        // Ensure an exception is thrown.
+        $this->expectException('moodle_exception');
+        core_message_external::confirm_contact_request($user1->id, $user2->id);
+    }
+
+    /**
+     * Test confirming a contact request with no permission.
+     */
+    public function test_confirm_contact_request_no_permission() {
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user3);
+
+        // Ensure an exception is thrown.
+        $this->expectException('required_capability_exception');
+        core_message_external::confirm_contact_request($user1->id, $user2->id);
+    }
+
+    /**
+     * Test declining a contact request.
+     */
+    public function test_decline_contact_request() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+
+        $this->setUser($user2);
+
+        $return = core_message_external::decline_contact_request($user1->id, $user2->id);
+        $return = external_api::clean_returnvalue(core_message_external::decline_contact_request_returns(), $return);
+        $this->assertEquals(array(), $return);
+
+        $this->assertEquals(0, $DB->count_records('message_contact_requests'));
+        $this->assertEquals(0, $DB->count_records('message_contacts'));
+    }
+
+    /**
+     * Test declining a contact request with messaging disabled.
+     */
+    public function test_decline_contact_request_messaging_disabled() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+
+        // Ensure an exception is thrown.
+        $this->expectException('moodle_exception');
+        core_message_external::decline_contact_request($user1->id, $user2->id);
+    }
+
+    /**
+     * Test declining a contact request with no permission.
+     */
+    public function test_decline_contact_request_no_permission() {
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user3);
+
+        // Ensure an exception is thrown.
+        $this->expectException('required_capability_exception');
+        core_message_external::decline_contact_request($user1->id, $user2->id);
+    }
+
+    /**
+     * Test blocking a user.
+     */
+    public function test_block_user() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Blocking a user.
+        $return = core_message_external::block_user($user1->id, $user2->id);
+        $return = external_api::clean_returnvalue(core_message_external::block_user_returns(), $return);
+        $this->assertEquals(array(), $return);
+
+        // Get list of blocked users.
+        $record = $DB->get_record('message_users_blocked', []);
+
+        $this->assertEquals($user1->id, $record->userid);
+        $this->assertEquals($user2->id, $record->blockeduserid);
+
+        // Blocking a user who is already blocked.
+        $return = core_message_external::block_user($user1->id, $user2->id);
+        $return = external_api::clean_returnvalue(core_message_external::block_user_returns(), $return);
+        $this->assertEquals(array(), $return);
+
+        $this->assertEquals(1, $DB->count_records('message_users_blocked'));
+    }
+
+    /**
+     * Test blocking a user with messaging disabled.
+     */
+    public function test_block_user_messaging_disabled() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+
+        // Ensure an exception is thrown.
+        $this->expectException('moodle_exception');
+        core_message_external::block_user($user1->id, $user2->id);
+    }
+
+    /**
+     * Test blocking a user with no permission.
+     */
+    public function test_block_user_no_permission() {
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user3);
+
+        // Ensure an exception is thrown.
+        $this->expectException('required_capability_exception');
+        core_message_external::block_user($user1->id, $user2->id);
+    }
+
+    /**
+     * Test unblocking a user.
+     */
+    public function test_unblock_user() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Block the user.
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        // Unblocking a user.
+        $return = core_message_external::unblock_user($user1->id, $user2->id);
+        $return = external_api::clean_returnvalue(core_message_external::unblock_user_returns(), $return);
+        $this->assertEquals(array(), $return);
+
+        $this->assertEquals(0, $DB->count_records('message_users_blocked'));
+
+        // Unblocking a user who is already unblocked.
+        $return = core_message_external::unblock_user($user1->id, $user2->id);
+        $return = external_api::clean_returnvalue(core_message_external::unblock_user_returns(), $return);
+        $this->assertEquals(array(), $return);
+
+        $this->assertEquals(0, $DB->count_records('message_users_blocked'));
+    }
+
+    /**
+     * Test unblocking a user with messaging disabled.
+     */
+    public function test_unblock_user_messaging_disabled() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+
+        // Ensure an exception is thrown.
+        $this->expectException('moodle_exception');
+        core_message_external::unblock_user($user1->id, $user2->id);
+    }
+
+    /**
+     * Test unblocking a user with no permission.
+     */
+    public function test_unblock_user_no_permission() {
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user3);
+
+        // Ensure an exception is thrown.
+        $this->expectException('required_capability_exception');
+        core_message_external::unblock_user($user1->id, $user2->id);
     }
 
     /**
@@ -348,8 +999,10 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Login as user1.
         $this->setUser($user1);
-        $this->assertEquals(array(), core_message_external::create_contacts(
-            array($user_offline1->id, $user_offline2->id, $user_offline3->id, $user_online->id)));
+        \core_message\api::add_contact($user1->id, $user_offline1->id);
+        \core_message\api::add_contact($user1->id, $user_offline2->id);
+        \core_message\api::add_contact($user1->id, $user_offline3->id);
+        \core_message\api::add_contact($user1->id, $user_online->id);
 
         // User_stranger sends a couple of messages to user1.
         $this->send_message($user_stranger, $user1, 'Hello there!');
@@ -368,6 +1021,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertCount(1, $contacts['online']);
         $this->assertCount(3, $contacts['strangers']);
         core_message_external::block_contacts(array($user_blocked->id));
+        $this->assertDebuggingCalled();
         $contacts = core_message_external::get_contacts();
         $contacts = external_api::clean_returnvalue(core_message_external::get_contacts_returns(), $contacts);
         $this->assertCount(3, $contacts['offline']);
@@ -772,8 +1426,9 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Login as user1.
         $this->setUser($user1);
-        $this->assertEquals(array(), core_message_external::create_contacts(
-            array($useroffline1->id, $useroffline2->id)));
+
+        \core_message\api::add_contact($user1->id, $useroffline1->id);
+        \core_message\api::add_contact($user1->id, $useroffline2->id);
 
         // The userstranger sends a couple of messages to user1.
         $this->send_message($userstranger, $user1, 'Hello there!');
@@ -791,6 +1446,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Block the $userblocked and retrieve again the list.
         core_message_external::block_contacts(array($userblocked->id));
+        $this->assertDebuggingCalled();
         $blockedusers = core_message_external::get_blocked_users($user1->id);
         $blockedusers = external_api::clean_returnvalue(core_message_external::get_blocked_users_returns(), $blockedusers);
         $this->assertCount(1, $blockedusers['users']);
@@ -800,7 +1456,6 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $blockedusers = core_message_external::get_blocked_users($user1->id);
         $blockedusers = external_api::clean_returnvalue(core_message_external::get_blocked_users_returns(), $blockedusers);
         $this->assertCount(0, $blockedusers['users']);
-
     }
 
     /**
@@ -815,8 +1470,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Login as user1.
         $this->setUser($user1);
-        $this->assertEquals(array(), core_message_external::create_contacts(
-            array($user2->id, $user3->id)));
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
 
         // The user2 sends a couple of messages to user1.
         $this->send_message($user2, $user1, 'Hello there!');
@@ -874,8 +1529,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Login as user1.
         $this->setUser($user1);
-        $this->assertEquals(array(), core_message_external::create_contacts(
-            array($user2->id, $user3->id)));
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
 
         // The user2 sends a couple of notifications to user1.
         $this->send_message($user2, $user1, 'Hello there!', 1);
@@ -936,7 +1591,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Login as user1.
         $this->setUser($user1);
-        $this->assertEquals(array(), core_message_external::create_contacts(array($user2->id, $user3->id)));
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
 
         // User user1 does not interchange messages with user3.
         $m1to2 = message_post_message($user1, $user2, 'some random text 1', FORMAT_MOODLE);
@@ -1165,7 +1821,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $user2 = self::getDataGenerator()->create_user($user2);
 
         // Block the second user.
-        message_block_contact($user2->id, $user1->id);
+        \core_message\api::block_user($user1->id, $user2->id);
 
         $user3 = new stdClass();
         $user3->firstname = 'User';
@@ -1375,9 +2031,9 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->getDataGenerator()->enrol_user($user1->id, $course3->id, 'student');
 
         // Add some users as contacts.
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user3->id, 0, $user1->id);
-        message_add_contact($user4->id, 0, $user1->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
 
         // Perform a search.
         $result = core_message_external::data_for_messagearea_search_users($user1->id, 'search');
@@ -1463,9 +2119,9 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $course3 = $this->getDataGenerator()->create_course($course3);
 
         // Add some users as contacts.
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user3->id, 0, $user1->id);
-        message_add_contact($user4->id, 0, $user1->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
 
         // Perform a search.
         $result = core_message_external::data_for_messagearea_search_users($user1->id, 'search');
@@ -1913,9 +2569,9 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $user5 = self::getDataGenerator()->create_user($user5);
 
         // Add some users as contacts.
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user3->id, 0, $user1->id);
-        message_add_contact($user4->id, 0, $user1->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
 
         // Retrieve the contacts.
         $result = core_message_external::data_for_messagearea_contacts($user1->id);
@@ -1926,6 +2582,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Confirm the data is correct.
         $contacts = $result['contacts'];
+        usort($contacts, ['static', 'sort_contacts']);
         $this->assertCount(3, $contacts);
 
         $contact1 = $contacts[0];
@@ -1995,9 +2652,9 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $user5 = self::getDataGenerator()->create_user($user5);
 
         // Add some users as contacts.
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user3->id, 0, $user1->id);
-        message_add_contact($user4->id, 0, $user1->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
 
         // Retrieve the contacts.
         $result = core_message_external::data_for_messagearea_contacts($user1->id);
@@ -2008,6 +2665,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Confirm the data is correct.
         $contacts = $result['contacts'];
+        usort($contacts, ['static', 'sort_contacts']);
         $this->assertCount(3, $contacts);
 
         $contact1 = $contacts[0];
@@ -2894,4 +3552,15 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->expectException('moodle_exception');
         $prefs = core_message_external::get_user_message_preferences($otheruser->id);
     }
+
+    /**
+     * Comparison function for sorting contacts.
+     *
+     * @param array $a
+     * @param array $b
+     * @return bool
+     */
+    protected static function sort_contacts($a, $b) {
+        return $a['userid'] > $b['userid'];
+    }
 }
index e2c2dcd..0aaac07 100644 (file)
@@ -106,6 +106,8 @@ class core_message_messagelib_testcase extends advanced_testcase {
      * Test message_get_blocked_users.
      */
     public function test_message_get_blocked_users() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
@@ -113,15 +115,13 @@ class core_message_messagelib_testcase extends advanced_testcase {
         $user1 = $this->getDataGenerator()->create_user();
         $user2 = $this->getDataGenerator()->create_user();
 
-        // Add users to the admin's contact list.
-        message_add_contact($user1->id);
-        message_add_contact($user2->id, 1);
+        \core_message\api::block_user($USER->id, $user2->id);
 
         $this->assertCount(1, message_get_blocked_users());
         $this->assertDebuggingCalled();
 
         // Block other user.
-        message_block_contact($user1->id);
+        \core_message\api::block_user($USER->id, $user1->id);
         $this->assertCount(2, message_get_blocked_users());
         $this->assertDebuggingCalled();
 
@@ -149,8 +149,8 @@ class core_message_messagelib_testcase extends advanced_testcase {
         $user3 = $this->getDataGenerator()->create_user(); // Stranger.
 
         // Add users to the admin's contact list.
-        message_add_contact($user1->id);
-        message_add_contact($user2->id);
+        \core_message\api::add_contact($USER->id, $user1->id);
+        \core_message\api::add_contact($USER->id, $user2->id);
 
         // Send some messages.
         $this->send_fake_message($user1, $USER);
@@ -173,7 +173,7 @@ class core_message_messagelib_testcase extends advanced_testcase {
         $this->assertCount(3, $strangers);
 
         // Block 1 user.
-        message_block_contact($user2->id);
+        \core_message\api::block_user($USER->id, $user2->id);
         list($onlinecontacts, $offlinecontacts, $strangers) = message_get_contacts();
         $this->assertDebuggingCalled();
         $this->assertCount(0, $onlinecontacts);
@@ -286,29 +286,34 @@ class core_message_messagelib_testcase extends advanced_testcase {
      * Test message_add_contact.
      */
     public function test_message_add_contact() {
+        global $DB, $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
         // Create a user to add to the admin's contact list.
         $user1 = $this->getDataGenerator()->create_user();
         $user2 = $this->getDataGenerator()->create_user();
-        $user3 = $this->getDataGenerator()->create_user();
 
         message_add_contact($user1->id);
-        message_add_contact($user2->id, 0);
-        // Add duplicate contact and make sure only 1 record exists.
+        $this->assertDebuggingCalled();
+        $this->assertEquals(1, $DB->count_records('message_contact_requests'));
+
         message_add_contact($user2->id, 1);
+        $this->assertDebuggingCalled();
+        $this->assertEquals(1, $DB->count_records('message_users_blocked'));
 
-        $this->assertNotEmpty(message_get_contact($user1->id));
-        $this->assertNotEmpty(message_get_contact($user2->id));
-        $this->assertEquals(false, message_get_contact($user3->id));
-        $this->assertEquals(1, \core_message\api::count_blocked_users());
+        message_add_contact($user2->id, 0);
+        $this->assertDebuggingCalled();
+        $this->assertEquals(0, $DB->count_records('message_users_blocked'));
     }
 
     /**
      * Test message_remove_contact.
      */
     public function test_message_remove_contact() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
@@ -316,18 +321,21 @@ class core_message_messagelib_testcase extends advanced_testcase {
         $user = $this->getDataGenerator()->create_user();
 
         // Add the user to the admin's contact list.
-        message_add_contact($user->id);
-        $this->assertNotEmpty(message_get_contact($user->id));
+        \core_message\api::add_contact($USER->id, $user->id);
 
         // Remove user from admin's contact list.
         message_remove_contact($user->id);
+        $this->assertDebuggingCalled();
         $this->assertEquals(false, message_get_contact($user->id));
+        $this->assertDebuggingCalled();
     }
 
     /**
      * Test message_block_contact.
      */
     public function test_message_block_contact() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
@@ -336,13 +344,14 @@ class core_message_messagelib_testcase extends advanced_testcase {
         $user2 = $this->getDataGenerator()->create_user();
 
         // Add users to the admin's contact list.
-        message_add_contact($user1->id);
-        message_add_contact($user2->id);
+        \core_message\api::add_contact($USER->id, $user1->id);
+        \core_message\api::add_contact($USER->id, $user2->id);
 
         $this->assertEquals(0, \core_message\api::count_blocked_users());
 
         // Block 1 user.
         message_block_contact($user2->id);
+        $this->assertDebuggingCalled();
         $this->assertEquals(1, \core_message\api::count_blocked_users());
 
     }
@@ -351,21 +360,21 @@ class core_message_messagelib_testcase extends advanced_testcase {
      * Test message_unblock_contact.
      */
     public function test_message_unblock_contact() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
         // Create a user to add to the admin's contact list.
         $user1 = $this->getDataGenerator()->create_user();
-        $user2 = $this->getDataGenerator()->create_user();
-
-        // Add users to the admin's contact list.
-        message_add_contact($user1->id);
-        message_add_contact($user2->id, 1); // Add blocked contact.
 
+        // Add users to the admin's blocked list.
+        \core_message\api::block_user($USER->id, $user1->id);
         $this->assertEquals(1, \core_message\api::count_blocked_users());
 
         // Unblock user.
-        message_unblock_contact($user2->id);
+        message_unblock_contact($user1->id);
+        $this->assertDebuggingCalled();
         $this->assertEquals(0, \core_message\api::count_blocked_users());
     }
 
@@ -373,6 +382,8 @@ class core_message_messagelib_testcase extends advanced_testcase {
      * Test message_search_users.
      */
     public function test_message_search_users() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
@@ -381,8 +392,8 @@ class core_message_messagelib_testcase extends advanced_testcase {
         $user2 = $this->getDataGenerator()->create_user(array('firstname' => 'Test2', 'lastname' => 'user2'));
 
         // Add users to the admin's contact list.
-        message_add_contact($user1->id);
-        message_add_contact($user2->id); // Add blocked contact.
+        \core_message\api::add_contact($USER->id, $user1->id);
+        \core_message\api::add_contact($USER->id, $user2->id);
 
         $this->assertCount(1, message_search_users(0, 'Test1'));
         $this->assertCount(2, message_search_users(0, 'Test'));
index 976df59..3500c79 100644 (file)
@@ -45,7 +45,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $collection = new collection('core_message');
         $newcollection = provider::get_metadata($collection);
         $itemcollection = $newcollection->get_collection();
-        $this->assertCount(6, $itemcollection);
+        $this->assertCount(8, $itemcollection);
 
         $messagestable = array_shift($itemcollection);
         $this->assertEquals('messages', $messagestable->get_name());
@@ -59,6 +59,12 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $messagecontacts = array_shift($itemcollection);
         $this->assertEquals('message_contacts', $messagecontacts->get_name());
 
+        $messagecontactrequests = array_shift($itemcollection);
+        $this->assertEquals('message_contact_requests', $messagecontactrequests->get_name());
+
+        $messageusersblocked = array_shift($itemcollection);
+        $this->assertEquals('message_users_blocked', $messageusersblocked->get_name());
+
         $notificationstable = array_shift($itemcollection);
         $this->assertEquals('notifications', $notificationstable->get_name());
 
@@ -92,9 +98,21 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $privacyfields = $messagecontacts->get_privacy_fields();
         $this->assertArrayHasKey('userid', $privacyfields);
         $this->assertArrayHasKey('contactid', $privacyfields);
-        $this->assertArrayHasKey('blocked', $privacyfields);
+        $this->assertArrayHasKey('timecreated', $privacyfields);
         $this->assertEquals('privacy:metadata:message_contacts', $messagecontacts->get_summary());
 
+        $privacyfields = $messagecontactrequests->get_privacy_fields();
+        $this->assertArrayHasKey('userid', $privacyfields);
+        $this->assertArrayHasKey('requesteduserid', $privacyfields);
+        $this->assertArrayHasKey('timecreated', $privacyfields);
+        $this->assertEquals('privacy:metadata:message_contact_requests', $messagecontactrequests->get_summary());
+
+        $privacyfields = $messageusersblocked->get_privacy_fields();
+        $this->assertArrayHasKey('userid', $privacyfields);
+        $this->assertArrayHasKey('blockeduserid', $privacyfields);
+        $this->assertArrayHasKey('timecreated', $privacyfields);
+        $this->assertEquals('privacy:metadata:message_users_blocked', $messageusersblocked->get_summary());
+
         $privacyfields = $notificationstable->get_privacy_fields();
         $this->assertArrayHasKey('useridfrom', $privacyfields);
         $this->assertArrayHasKey('useridto', $privacyfields);
@@ -198,12 +216,9 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $user3 = $this->getDataGenerator()->create_user();
         $user4 = $this->getDataGenerator()->create_user();
 
-        // This user will not be added as a contact.
-        $this->getDataGenerator()->create_user();
-
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user3->id, 0, $user1->id);
-        message_add_contact($user4->id, 1, $user1->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
 
         $this->export_context_data_for_user($user1->id, \context_system::instance(), 'core_message');
 
@@ -216,15 +231,83 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
 
         $contact1 = array_shift($contacts);
         $this->assertEquals($user2->id, $contact1->contact);
-        $this->assertEquals(get_string('no'), $contact1->blocked);
 
         $contact2 = array_shift($contacts);
         $this->assertEquals($user3->id, $contact2->contact);
-        $this->assertEquals(get_string('no'), $contact2->blocked);
 
         $contact3 = array_shift($contacts);
         $this->assertEquals($user4->id, $contact3->contact);
-        $this->assertEquals(get_string('yes'), $contact3->blocked);
+    }
+
+    /**
+     * Test for provider::export_user_data().
+     */
+    public function test_export_for_context_with_contact_requests() {
+        $this->resetAfterTest();
+
+        // Create users to test with.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+        \core_message\api::create_contact_request($user3->id, $user1->id);
+        \core_message\api::create_contact_request($user1->id, $user4->id);
+
+        $this->export_context_data_for_user($user1->id, \context_system::instance(), 'core_message');
+
+        $writer = writer::with_context(\context_system::instance());
+
+        $contactrequests = (array) $writer->get_data([get_string('contactrequests', 'core_message')]);
+
+        $this->assertCount(3, $contactrequests);
+
+        $contactrequest1 = array_shift($contactrequests);
+        $this->assertEquals($user2->id, $contactrequest1->contactrequest);
+        $this->assertEquals(get_string('yes'), $contactrequest1->maderequest);
+
+        $contactrequest2 = array_shift($contactrequests);
+        $this->assertEquals($user3->id, $contactrequest2->contactrequest);
+        $this->assertEquals(get_string('no'), $contactrequest2->maderequest);
+
+        $contactrequest3 = array_shift($contactrequests);
+        $this->assertEquals($user4->id, $contactrequest3->contactrequest);
+        $this->assertEquals(get_string('yes'), $contactrequest3->maderequest);
+    }
+
+    /**
+     * Test for provider::export_user_data().
+     */
+    public function test_export_for_context_with_blocked_users() {
+        $this->resetAfterTest();
+
+        // Create users to test with.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+
+        \core_message\api::block_user($user1->id, $user2->id);
+        \core_message\api::block_user($user1->id, $user3->id);
+        \core_message\api::block_user($user1->id, $user4->id);
+
+        $this->export_context_data_for_user($user1->id, \context_system::instance(), 'core_message');
+
+        $writer = writer::with_context(\context_system::instance());
+
+        $blockedusers = (array) $writer->get_data([get_string('blockedusers', 'core_message')]);
+
+        $this->assertCount(3, $blockedusers);
+
+        $blockeduser1 = array_shift($blockedusers);
+        $this->assertEquals($user2->id, $blockeduser1->blockeduser);
+
+        $blockeduser2 = array_shift($blockedusers);
+        $this->assertEquals($user3->id, $blockeduser2->blockeduser);
+
+        $blockeduser3 = array_shift($blockedusers);
+        $this->assertEquals($user4->id, $blockeduser3->blockeduser);
     }
 
     /**
@@ -380,6 +463,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // Create users to test with.
         $user1 = $this->getDataGenerator()->create_user();
         $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
 
         $now = time();
         $timeread = $now - DAYSECS;
@@ -387,8 +472,13 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $systemcontext = \context_system::instance();
 
         // Create contacts.
-        message_add_contact($user1->id, 0, $user2->id);
-        message_add_contact($user2->id, 0, $user1->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+
+        // Create contact requests.
+        \core_message\api::create_contact_request($user1->id, $user3->id);
+
+        // Block a user.
+        \core_message\api::block_user($user1->id, $user3->id);
 
         // Create messages.
         $m1 = $this->create_message($user1->id, $user2->id, $now + (9 * DAYSECS), true);
@@ -401,8 +491,14 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // Delete one of the messages.
         \core_message\api::delete_message($user1->id, $m2);
 
-        // There should be 2 contacts.
-        $this->assertEquals(2, $DB->count_records('message_contacts'));
+        // There should be 1 contact.
+        $this->assertEquals(1, $DB->count_records('message_contacts'));
+
+        // There should be 1 contact request.
+        $this->assertEquals(1, $DB->count_records('message_contact_requests'));
+
+        // There should be 1 blocked user.
+        $this->assertEquals(1, $DB->count_records('message_users_blocked'));
 
         // There should be two messages.
         $this->assertEquals(2, $DB->count_records('messages'));
@@ -413,13 +509,15 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be two conversation members.
         $this->assertEquals(2, $DB->count_records('message_conversation_members'));
 
-        // There should be two notifications.
-        $this->assertEquals(2, $DB->count_records('notifications'));
+        // There should be two notifications + one for the contact request.
+        $this->assertEquals(3, $DB->count_records('notifications'));
 
         provider::delete_data_for_all_users_in_context($systemcontext);
 
         // Confirm all has been deleted.
         $this->assertEquals(0, $DB->count_records('message_contacts'));
+        $this->assertEquals(0, $DB->count_records('message_contact_requests'));
+        $this->assertEquals(0, $DB->count_records('message_users_blocked'));
         $this->assertEquals(0, $DB->count_records('messages'));
         $this->assertEquals(0, $DB->count_records('message_user_actions'));
         $this->assertEquals(0, $DB->count_records('message_conversation_members'));
@@ -438,14 +536,24 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $user1 = $this->getDataGenerator()->create_user();
         $user2 = $this->getDataGenerator()->create_user();
         $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+        $user5 = $this->getDataGenerator()->create_user();
+        $user6 = $this->getDataGenerator()->create_user();
 
         $now = time();
         $timeread = $now - DAYSECS;
 
         // Create contacts.
-        message_add_contact($user1->id, 0, $user2->id);
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user2->id, 0, $user3->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user2->id, $user3->id);
+
+        // Create contact requests.
+        \core_message\api::create_contact_request($user1->id, $user3->id);
+        \core_message\api::create_contact_request($user2->id, $user4->id);
+
+        // Block users.
+        \core_message\api::block_user($user1->id, $user5->id);
+        \core_message\api::block_user($user2->id, $user6->id);
 
         // Create messages.
         $m1 = $this->create_message($user1->id, $user2->id, $now + (9 * DAYSECS), $timeread);
@@ -459,8 +567,14 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // Delete one of the messages.
         \core_message\api::delete_message($user1->id, $m2);
 
-        // There should be 3 contacts.
-        $this->assertEquals(3, $DB->count_records('message_contacts'));
+        // There should be 2 contacts.
+        $this->assertEquals(2, $DB->count_records('message_contacts'));
+
+        // There should be 1 contact request.
+        $this->assertEquals(2, $DB->count_records('message_contact_requests'));
+
+        // There should be 1 blocked user.
+        $this->assertEquals(2, $DB->count_records('message_users_blocked'));
 
         // There should be two messages.
         $this->assertEquals(2, $DB->count_records('messages'));
@@ -471,8 +585,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be two conversation members.
         $this->assertEquals(2, $DB->count_records('message_conversation_members'));
 
-        // There should be three notifications.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        // There should be three notifications + two for the contact requests.
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         $systemcontext = \context_system::instance();
         $contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'core_message',
@@ -481,6 +595,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
 
         // Confirm the user 2 data still exists.
         $contacts = $DB->get_records('message_contacts');
+        $contactrequests = $DB->get_records('message_contact_requests');
+        $blockedusers = $DB->get_records('message_users_blocked');
         $messages = $DB->get_records('messages');
         $muas = $DB->get_records('message_user_actions');
         $mcms = $DB->get_records('message_conversation_members');
@@ -488,8 +604,18 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
 
         $this->assertCount(1, $contacts);
         $contact = reset($contacts);
-        $this->assertEquals($user3->id, $contact->userid);
-        $this->assertEquals($user2->id, $contact->contactid);
+        $this->assertEquals($user2->id, $contact->userid);
+        $this->assertEquals($user3->id, $contact->contactid);
+
+        $this->assertCount(1, $contactrequests);
+        $contactrequest = reset($contactrequests);
+        $this->assertEquals($user2->id, $contactrequest->userid);
+        $this->assertEquals($user4->id, $contactrequest->requesteduserid);
+
+        $this->assertCount(1, $blockedusers);
+        $blockeduser = reset($blockedusers);
+        $this->assertEquals($user2->id, $blockeduser->userid);
+        $this->assertEquals($user6->id, $blockeduser->blockeduserid);
 
         $this->assertCount(1, $messages);
         $message = reset($messages);
@@ -505,8 +631,14 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $mcm = reset($mcms);
         $this->assertEquals($user2->id, $mcm->userid);
 
-        $this->assertCount(1, $notifications);
-        $notification = reset($notifications);
+        $this->assertCount(2, $notifications);
+        ksort($notifications);
+
+        $notification = array_shift($notifications);
+        $this->assertEquals($user2->id, $notification->useridfrom);
+        $this->assertEquals($user4->id, $notification->useridto);
+
+        $notification = array_shift($notifications);
         $this->assertEquals($user2->id, $notification->useridfrom);
         $this->assertEquals($user3->id, $notification->useridto);
     }
index 4f40560..2ada652 100644 (file)
@@ -19,6 +19,19 @@ information provided here is intended especially for developers.
   * message_can_post_message()
   * message_is_user_non_contact_blocked()
   * message_is_user_blocked()
+* The following functions have been deprecated and should not be used any more:
+  - message_add_contact()
+  - message_remove_contact()
+  - message_unblock_contact()
+  - message_block_contact()
+  - message_get_contact()
+  Please see their declaration in lib/deprecatedlib.php to view their alternatives (if applicable).
+* The following methods have been deprecated and should not be used any more:
+  - \core_message\api::is_user_blocked()
+* The following web services have been deprecated. Please do not call these any more.
+  - core_message_external::block_contacts, please use core_message_external::block_user instead.
+  - core_message_external::unblock_contacts, please use core_message_external::unblock_user instead.
+  - core_message_external::create_contacts, please use core_message_external::create_contact_request instead.
 
 === 3.5 ===
 
index a2dd3e4..bf1b717 100644 (file)
@@ -2371,6 +2371,7 @@ class assign {
         // Submissions are included if all are true:
         //   - The assignment is visible in the gradebook.
         //   - No previous notification has been sent.
+        //   - The grader was a real user, not an automated process.
         //   - If marking workflow is not enabled, the grade was updated in the past 24 hours, or
         //     if marking workflow is enabled, the workflow state is at 'released'.
         $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities,
@@ -2384,7 +2385,7 @@ class assign {
             LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id
                  WHERE ((a.markingworkflow = 0 AND g.timemodified >= :yesterday AND g.timemodified <= :today) OR
                         (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
-                       uf.mailed = 0 AND gri.hidden = 0
+                       g.grader > 0 AND uf.mailed = 0 AND gri.hidden = 0
               ORDER BY a.course, cm.id";
 
         $params = array(
@@ -3714,7 +3715,8 @@ class assign {
                 $grade->timemodified = $grade->timecreated;
             }
             $grade->grade = -1;
-            $grade->grader = $USER->id;
+            // Do not set the grader id here as it would be the admin users which is incorrect.
+            $grade->grader = -1;
             if ($attemptnumber >= 0) {
                 $grade->attemptnumber = $attemptnumber;
             }
@@ -5100,8 +5102,10 @@ class assign {
                     $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
                 }
                 $gradeddate = $gradebookgrade->dategraded;
-                if (isset($grade->grader)) {
+                if (isset($grade->grader) && $grade->grader > 0) {
                     $grader = $DB->get_record('user', array('id' => $grade->grader));
+                } else if (isset($gradebookgrade->usermodified) && $gradebookgrade->usermodified > 0) {
+                    $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
                 }
             }
 
@@ -5262,10 +5266,12 @@ class assign {
             // First lookup the grader info.
             if (isset($gradercache[$grade->grader])) {
                 $grade->grader = $gradercache[$grade->grader];
-            } else {
+            } else if ($grade->grader > 0) {
                 // Not in cache - need to load the grader record.
                 $grade->grader = $DB->get_record('user', array('id'=>$grade->grader));
-                $gradercache[$grade->grader->id] = $grade->grader;
+                if ($grade->grader) {
+                    $gradercache[$grade->grader->id] = $grade->grader;
+                }
             }
 
             // Now get the gradefordisplay.
index d4b17e9..13e0bd6 100644 (file)
@@ -3887,6 +3887,7 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $assign->get_user_grade($student->id, true);
 
         // Set the grade to something errant.
+        // We don't set the grader here, so we expect it to be -1 as a result.
         $DB->set_field(
             'assign_grades',
             'grade',
@@ -3904,6 +3905,7 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         // Check that the gradebook was updated with the assign grade. So we can guarentee test results later on.
         $expectedgrade = $grade == -1 ? null : $grade; // Assign sends null to the gradebook for -1 grades.
         $gradegrade = grade_grade::fetch(array('userid' => $student->id, 'itemid' => $assign->get_grade_item()->id));
+        $this->assertEquals(-1, $gradegrade->usermodified);
         $this->assertEquals($expectedgrade, $gradegrade->rawgrade);
 
         // Call fix_null_grades().
@@ -3915,6 +3917,9 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
 
         $gradegrade = grade_grade::fetch(array('userid' => $student->id, 'itemid' => $assign->get_grade_item()->id));
 
+        $this->assertEquals(-1, $gradegrade->usermodified);
+        $this->assertEquals($gradebookvalue, $gradegrade->finalgrade);
+
         // Check that the grade was updated in the gradebook by fix_null_grades.
         $this->assertEquals($gradebookvalue, $gradegrade->finalgrade);
     }
index 1a7eea2..b637862 100644 (file)
@@ -485,15 +485,22 @@ function imscp_check_updates_since(cm_info $cm, $from, $filter = array()) {
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
+ * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
  * @return \core_calendar\local\event\entities\action_interface|null
  */
 function mod_imscp_core_calendar_provide_event_action(calendar_event $event,
-                                                      \core_calendar\action_factory $factory) {
-    $cm = get_fast_modinfo($event->courseid)->instances['imscp'][$event->instance];
+                                                      \core_calendar\action_factory $factory,
+                                                      int $userid = 0) {
+    $cm = get_fast_modinfo($event->courseid, $userid)->instances['imscp'][$event->instance];
+
+    if (!$cm->uservisible) {
+        // The module is not visible to the user for any reason.
+        return null;
+    }
 
     $completion = new \completion_info($cm->get_course());
 
-    $completiondata = $completion->get_data($cm, false);
+    $completiondata = $completion->get_data($cm, false, $userid);
 
     if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
         return null;
index cd2e13e..f54e03c 100644 (file)
@@ -133,6 +133,107 @@ class mod_imscp_lib_testcase extends advanced_testcase {
         $this->assertTrue($actionevent->is_actionable());
     }
 
+    public function test_imscp_core_calendar_provide_event_action_for_user() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create a student.
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create the activity.
+        $imscp = $this->getDataGenerator()->create_module('imscp', array('course' => $course->id));
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $imscp->id,
+            \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+        // Now log out.
+        $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+        $this->setUser();
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event for the student.
+        $actionevent = mod_imscp_core_calendar_provide_event_action($event, $factory, $student->id);
+
+        // Confirm the event was decorated.
+        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
+        $this->assertEquals(get_string('view'), $actionevent->get_name());
+        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
+        $this->assertEquals(1, $actionevent->get_item_count());
+        $this->assertTrue($actionevent->is_actionable());
+    }
+
+    public function test_imscp_core_calendar_provide_event_action_as_non_user() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create the activity.
+        $course = $this->getDataGenerator()->create_course();
+        $imscp = $this->getDataGenerator()->create_module('imscp', array('course' => $course->id));
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $imscp->id,
+                \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+        // Now log out.
+        $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+        $this->setUser();
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event.
+        $actionevent = mod_imscp_core_calendar_provide_event_action($event, $factory);
+
+        // Confirm the event is not shown at all.
+        $this->assertNull($actionevent);
+    }
+
+    public function test_imscp_core_calendar_provide_event_action_in_hidden_section() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create a student.
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create the activity.
+        $imscp = $this->getDataGenerator()->create_module('imscp', array('course' => $course->id));
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $imscp->id,
+                \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+        // Set sections 0 as hidden.
+        set_section_visible($course->id, 0, 0);
+
+        // Now log out.
+        $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+        $this->setUser();
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event for the student.
+        $actionevent = mod_imscp_core_calendar_provide_event_action($event, $factory, $student->id);
+
+        // Confirm the event is not shown at all.
+        $this->assertNull($actionevent);
+    }
+
     public function test_imscp_core_calendar_provide_event_action_already_completed() {
         global $CFG;
 
@@ -167,6 +268,45 @@ class mod_imscp_lib_testcase extends advanced_testcase {
         $this->assertNull($actionevent);
     }
 
+    public function test_imscp_core_calendar_provide_event_action_already_completed_for_user() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $CFG->enablecompletion = 1;
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
+
+        // Create a student.
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create the activity.
+        $imscp = $this->getDataGenerator()->create_module('imscp', array('course' => $course->id),
+            array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS));
+
+        // Get some additional data.
+        $cm = get_coursemodule_from_instance('imscp', $imscp->id);
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $imscp->id,
+            \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+        // Mark the activity as completed.
+        $completion = new completion_info($course);
+        $completion->set_module_viewed($cm, $student->id);
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event for the student.
+        $actionevent = mod_imscp_core_calendar_provide_event_action($event, $factory, $student->id);
+
+        // Ensure result was null.
+        $this->assertNull($actionevent);
+    }
+
     /**
      * Creates an action event.
      *
index 079dae2..22f51c1 100644 (file)
@@ -362,15 +362,22 @@ function label_check_updates_since(cm_info $cm, $from, $filter = array()) {
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
+ * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
  * @return \core_calendar\local\event\entities\action_interface|null
  */
 function mod_label_core_calendar_provide_event_action(calendar_event $event,
-                                                      \core_calendar\action_factory $factory) {
-    $cm = get_fast_modinfo($event->courseid)->instances['label'][$event->instance];
+                                                      \core_calendar\action_factory $factory,
+                                                      int $userid = 0) {
+    $cm = get_fast_modinfo($event->courseid, $userid)->instances['label'][$event->instance];
+
+    if (!$cm->uservisible) {
+        // The module is not visible to the user for any reason.
+        return null;
+    }
 
     $completion = new \completion_info($cm->get_course());
 
-    $completiondata = $completion->get_data($cm, false);
+    $completiondata = $completion->get_data($cm, false, $userid);
 
     if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
         return null;
index 38d9834..20d5816 100644 (file)
@@ -67,6 +67,88 @@ class mod_label_lib_testcase extends advanced_testcase {
         $this->assertTrue($actionevent->is_actionable());
     }
 
+    public function test_label_core_calendar_provide_event_action_as_non_user() {
+        global $CFG;
+
+        // Create the activity.
+        $course = $this->getDataGenerator()->create_course();
+        $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id));
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $label->id,
+                \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+        // Now log out.
+        $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+        $this->setUser();
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event.
+        $actionevent = mod_label_core_calendar_provide_event_action($event, $factory);
+
+        // Confirm the event is not shown at all.
+        $this->assertNull($actionevent);
+    }
+
+    public function test_label_core_calendar_provide_event_action_in_hidden_section() {
+        // Create the activity.
+        $course = $this->getDataGenerator()->create_course();
+        $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id));
+
+        // Create a student.
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $label->id,
+                \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+        // Set sections 0 as hidden.
+        set_section_visible($course->id, 0, 0);
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event for the student.
+        $actionevent = mod_label_core_calendar_provide_event_action($event, $factory, $student->id);
+
+        // Confirm the event is not shown at all.
+        $this->assertNull($actionevent);
+    }
+
+    public function test_label_core_calendar_provide_event_action_for_user() {
+        global $CFG;
+
+        // Create the activity.
+        $course = $this->getDataGenerator()->create_course();
+        $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id));
+
+        // Enrol a student in the course.
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $label->id,
+            \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+        // Now, log out.
+        $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+        $this->setUser();
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event for the student.
+        $actionevent = mod_label_core_calendar_provide_event_action($event, $factory, $student->id);
+
+        // Confirm the event was decorated.
+        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
+        $this->assertEquals(get_string('view'), $actionevent->get_name());
+        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
+        $this->assertEquals(1, $actionevent->get_item_count());
+        $this->assertTrue($actionevent->is_actionable());
+    }
+
     public function test_label_core_calendar_provide_event_action_already_completed() {
         global $CFG;
 
@@ -98,6 +180,40 @@ class mod_label_lib_testcase extends advanced_testcase {
         $this->assertNull($actionevent);
     }
 
+    public function test_label_core_calendar_provide_event_action_already_completed_for_user() {
+        global $CFG;
+
+        $CFG->enablecompletion = 1;
+
+        // Create the activity.
+        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
+        $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id),
+                array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS));
+
+        // Enrol a student in the course.
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Get some additional data.
+        $cm = get_coursemodule_from_instance('label', $label->id);
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $label->id,
+                \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
+
+        // Mark the activity as completed for the student.
+        $completion = new completion_info($course);
+        $completion->set_module_viewed($cm, $student->id);
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event for the student.
+        $actionevent = mod_label_core_calendar_provide_event_action($event, $factory, $student->id);
+
+        // Ensure result was null.
+        $this->assertNull($actionevent);
+    }
+
     /**
      * Creates an action event.
      *
index 885e8ae..2963827 100644 (file)
@@ -1653,23 +1653,35 @@ function lesson_check_updates_since(cm_info $cm, $from, $filter = array()) {
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
+ * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
  * @return \core_calendar\local\event\entities\action_interface|null
  */
 function mod_lesson_core_calendar_provide_event_action(calendar_event $event,
-                                                       \core_calendar\action_factory $factory) {
+                                                       \core_calendar\action_factory $factory,
+                                                       int $userid = 0) {
     global $DB, $CFG, $USER;
     require_once($CFG->dirroot . '/mod/lesson/locallib.php');
 
-    $cm = get_fast_modinfo($event->courseid)->instances['lesson'][$event->instance];
+    if (!$userid) {
+        $userid = $USER->id;
+    }
+
+    $cm = get_fast_modinfo($event->courseid, $userid)->instances['lesson'][$event->instance];
+
+    if (!$cm->uservisible) {
+        // The module is not visible to the user for any reason.
+        return null;
+    }
+
     $lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST));
 
-    if ($lesson->count_user_retries($USER->id)) {
+    if ($lesson->count_user_retries($userid)) {
         // If the user has attempted the lesson then there is no further action for the user.
         return null;
     }
 
     // Apply overrides.
-    $lesson->update_effective_access($USER->id);
+    $lesson->update_effective_access($userid);
 
     return $factory->create_instance(
         get_string('startlesson', 'lesson'),
index 049724f..74641d0 100644 (file)
@@ -243,6 +243,103 @@ class mod_lesson_lib_testcase extends advanced_testcase {
         $this->assertTrue($actionevent->is_actionable());
     }
 
+    public function test_lesson_core_calendar_provide_event_action_open_as_non_user() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create a lesson activity.
+        $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id,
+                'available' => time() - DAYSECS, 'deadline' => time() + DAYSECS));
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $lesson->id, LESSON_EVENT_TYPE_OPEN);
+
+        // Now, log out.
+        $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+        $this->setUser();
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event.
+        $actionevent = mod_lesson_core_calendar_provide_event_action($event, $factory);
+
+        // Confirm the event is not shown at all.
+        $this->assertNull($actionevent);
+    }
+
+    public function test_lesson_core_calendar_provide_event_action_open_for_user() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create a student.
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create a lesson activity.
+        $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id,
+                'available' => time() - DAYSECS, 'deadline' => time() + DAYSECS));
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $lesson->id, LESSON_EVENT_TYPE_OPEN);
+
+        // Now, log out.
+        $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+        $this->setUser();
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event for the student.
+        $actionevent = mod_lesson_core_calendar_provide_event_action($event, $factory, $student->id);
+
+        // Confirm the event was decorated.
+        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
+        $this->assertEquals(get_string('startlesson', 'lesson'), $actionevent->get_name());
+        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
+        $this->assertEquals(1, $actionevent->get_item_count());
+        $this->assertTrue($actionevent->is_actionable());
+    }
+
+    public function test_lesson_core_calendar_provide_event_action_open_in_hidden_section() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create a student.
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create a lesson activity.
+        $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id,
+                'available' => time() - DAYSECS, 'deadline' => time() + DAYSECS));
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $lesson->id, LESSON_EVENT_TYPE_OPEN);
+
+        // Set sections 0 as hidden.
+        set_section_visible($course->id, 0, 0);
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event for the student.
+        $actionevent = mod_lesson_core_calendar_provide_event_action($event, $factory, $student->id);
+
+        // Confirm the event is not shown at all.
+        $this->assertNull($actionevent);
+    }
+
     public function test_lesson_core_calendar_provide_event_action_closed() {
         $this->resetAfterTest();
         $this->setAdminUser();
@@ -271,6 +368,43 @@ class mod_lesson_lib_testcase extends advanced_testcase {
         $this->assertFalse($actionevent->is_actionable());
     }
 
+    public function test_lesson_core_calendar_provide_event_action_closed_for_user() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create a student.
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create a lesson activity.
+        $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id,
+                'deadline' => time() - DAYSECS));
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $lesson->id, LESSON_EVENT_TYPE_OPEN);
+
+        // Now, log out.
+        $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+        $this->setUser();
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event.
+        $actionevent = mod_lesson_core_calendar_provide_event_action($event, $factory, $student->id);
+
+        // Confirm the event was decorated.
+        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
+        $this->assertEquals(get_string('startlesson', 'lesson'), $actionevent->get_name());
+        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
+        $this->assertEquals(1, $actionevent->get_item_count());
+        $this->assertFalse($actionevent->is_actionable());
+    }
+
     public function test_lesson_core_calendar_provide_event_action_open_in_future() {
         $this->resetAfterTest();
         $this->setAdminUser();
@@ -299,6 +433,43 @@ class mod_lesson_lib_testcase extends advanced_testcase {
         $this->assertFalse($actionevent->is_actionable());
     }
 
+    public function test_lesson_core_calendar_provide_event_action_open_in_future_for_user() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create a student.
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create a lesson activity.
+        $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id,
+                'available' => time() + DAYSECS));
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $lesson->id, LESSON_EVENT_TYPE_OPEN);
+
+        // Now, log out.
+        $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+        $this->setUser();
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event.
+        $actionevent = mod_lesson_core_calendar_provide_event_action($event, $factory, $student->id);
+
+        // Confirm the event was decorated.
+        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
+        $this->assertEquals(get_string('startlesson', 'lesson'), $actionevent->get_name());
+        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
+        $this->assertEquals(1, $actionevent->get_item_count());
+        $this->assertFalse($actionevent->is_actionable());
+    }
+
     public function test_lesson_core_calendar_provide_event_action_no_time_specified() {
         $this->resetAfterTest();
         $this->setAdminUser();
@@ -326,6 +497,42 @@ class mod_lesson_lib_testcase extends advanced_testcase {
         $this->assertTrue($actionevent->is_actionable());
     }
 
+    public function test_lesson_core_calendar_provide_event_action_no_time_specified_for_user() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create a student.
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create a lesson activity.
+        $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id));
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $lesson->id, LESSON_EVENT_TYPE_OPEN);
+
+        // Now, log out.
+        $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
+        $this->setUser();
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event.
+        $actionevent = mod_lesson_core_calendar_provide_event_action($event, $factory, $student->id);
+
+        // Confirm the event was decorated.
+        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
+        $this->assertEquals(get_string('startlesson', 'lesson'), $actionevent->get_name());
+        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
+        $this->assertEquals(1, $actionevent->get_item_count());
+        $this->assertTrue($actionevent->is_actionable());
+    }
+
     public function test_lesson_core_calendar_provide_event_action_after_attempt() {
         global $DB;
 
@@ -376,6 +583,57 @@ class mod_lesson_lib_testcase extends advanced_testcase {
         $this->assertNull($action);
     }
 
+    public function test_lesson_core_calendar_provide_event_action_after_attempt_for_user() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create 2 students in the course.
+        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        // Create a lesson activity.
+        $lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $course->id));
+
+        // Create a calendar event.
+        $event = $this->create_action_event($course->id, $lesson->id, LESSON_EVENT_TYPE_OPEN);
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_lesson');
+        $tfrecord = $generator->create_question_truefalse($lesson);
+
+        // Now, do something in the lesson as student1.
+        $this->setUser($student1);
+        mod_lesson_external::launch_attempt($lesson->id);
+        $data = array(
+            array(
+                'name' => 'answerid',
+                'value' => $DB->get_field('lesson_answers', 'id', array('pageid' => $tfrecord->id, 'jumpto' => -1)),
+            ),
+            array(
+                'name' => '_qf__lesson_display_answer_form_truefalse',
+                'value' => 1,
+            )
+        );
+        mod_lesson_external::process_page($lesson->id, $tfrecord->id, $data);
+        mod_lesson_external::finish_attempt($lesson->id);
+
+        // Now, log in as the other student.
+        $this->setUser($student2);
+
+        // Create an action factory.
+        $factory = new \core_calendar\action_factory();
+
+        // Decorate action event.
+        $action = mod_lesson_core_calendar_provide_event_action($event, $factory, $student1->id);
+
+        // Confirm there was no action for the user.
+        $this->assertNull($action);
+    }
+
     /**
      * Creates an action event.
      *
index c65a659..00e1eb1 100644 (file)
@@ -1101,7 +1101,7 @@ class quiz_attempt {
      * @return question_usage_by_activity the usage.
      */
     public function get_question_usage() {
-        if (!PHPUNIT_TEST) {
+        if (!(PHPUNIT_TEST || defined('BEHAT_TEST'))) {
             throw new coding_exception('get_question_usage is only for use in unit tests. ' .
                     'For other operations, use the quiz_attempt api, or extend it properly.');
         }
@@ -1814,10 +1814,12 @@ class quiz_attempt {
      * @param int  $timestamp  the timestamp that should be stored as the modifed
      *                         time in the database for these actions. If null, will use the current time.
      * @param bool $becomingoverdue
-     * @param array|null $simulatedresponses If not null, then we are testing, and this is an array of simulated data, keys are slot
-     *                                          nos and values are arrays representing student responses which will be passed to
-     *                                          question_definition::prepare_simulated_post_data method and then have the
-     *                                          appropriate prefix added.
+     * @param array|null $simulatedresponses If not null, then we are testing, and this is an array of simulated data.
+     *      There are two formats supported here, for historical reasons. The newer approach is to pass an array created by
+     *      {@link core_question_generator::get_simulated_post_data_for_questions_in_usage()}.
+     *      the second is to pass an array slot no => contains arrays representing student
+     *      responses which will be passed to {@link question_definition::prepare_simulated_post_data()}.
+     *      This second method will probably get deprecated one day.
      */
     public function process_submitted_actions($timestamp, $becomingoverdue = false, $simulatedresponses = null) {
         global $DB;
@@ -1825,7 +1827,12 @@ class quiz_attempt {
         $transaction = $DB->start_delegated_transaction();
 
         if ($simulatedresponses !== null) {
-            $simulatedpostdata = $this->quba->prepare_simulated_post_data($simulatedresponses);
+            if (is_int(key($simulatedresponses))) {
+                // Legacy approach. Should be removed one day.
+                $simulatedpostdata = $this->quba->prepare_simulated_post_data($simulatedresponses);
+            } else {
+                $simulatedpostdata = $simulatedresponses;
+            }
         } else {
             $simulatedpostdata = null;
         }
index e27fbc8..0c46d27 100644 (file)
@@ -2373,14 +2373,19 @@ function quiz_validate_new_attempt(quiz $quizobj, quiz_access_manager $accessman
 /**
  * Prepare and start a new attempt deleting the previous preview attempts.
  *
- * @param  quiz $quizobj quiz object
- * @param  int $attemptnumber the attempt number
- * @param  object $lastattempt last attempt object
+ * @param quiz $quizobj quiz object
+ * @param int $attemptnumber the attempt number
+ * @param object $lastattempt last attempt object
  * @param bool $offlineattempt whether is an offline attempt or not
+ * @param array $forcedrandomquestions slot number => question id. Used for random questions,
+ *      to force the choice of a particular actual question. Intended for testing purposes only.
+ * @param array $forcedvariants slot number => variant. Used for questions with variants,
+ *      to force the choice of a particular variant. Intended for testing purposes only.
  * @return object the new attempt
  * @since  Moodle 3.1
  */
-function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $offlineattempt = false) {
+function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $lastattempt,
+        $offlineattempt = false, $forcedrandomquestions = [], $forcedvariants = []) {
     global $DB, $USER;
 
     // Delete any previous preview attempts belonging to this user.
@@ -2394,7 +2399,8 @@ function quiz_prepare_and_start_new_attempt(quiz $quizobj, $attemptnumber, $last
     $attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, $quizobj->is_preview_user());
 
     if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) {
-        $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow);
+        $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow,
+                $forcedrandomquestions, $forcedvariants);
     } else {
         $attempt = quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt);
     }
index 09e648b..840902b 100644 (file)
@@ -17,50 +17,35 @@ Feature: Basic use of the Manual grading report
       | user     | course | role           |
       | teacher1 | C1     | editingteacher |
       | student1 | C1     | student        |
-    And I log in as "teacher1"
-    And I am on "Course 1" course homepage with editing mode on
-    And I add a "Quiz" to section "1" and I fill the form with:
-      | Name        | Quiz 1             |
-      | Description | Quiz 1 description |
-    And I add a "Short answer" question to the "Quiz 1" quiz with:
-      | Question name    | Short answer 001                     |
-      | Question text    | Where is the capital city of France? |
-      | Answer 1         | Paris                                |
-      | Grade            | 100%                                 |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name             | questiontext                         | answer 1 | grade |
+      | Test questions   | shortanswer | Short answer 001 | Where is the capital city of France? | Paris    | 100%  |
+    And the following "activities" exist:
+      | activity   | name   | course | idnumber |
+      | quiz       | Quiz 1 | C1     | quiz1    |
+    And quiz "Quiz 1" contains the following questions:
+      | question          | page |
+      | Short answer 001  | 1    |
 
     # Check report shows nothing when there are no attempts.
+    When I log in as "teacher1"
     And I am on "Course 1" course homepage
     And I follow "Quiz 1"
-    When I navigate to "Results > Manual grading" in current page administration
+    And I navigate to "Results > Manual grading" in current page administration
     Then I should see "Manual grading"
     And I should see "Quiz 1"
     And I should see "Nothing to display"
     And I follow "Also show questions that have been graded automatically"
     And I should see "Nothing to display"
-    And I log out
-
-    # Create an attempt.
-    And I log in as "student1"
-    And I am on "Course 1" course homepage
-    And I follow "Quiz 1"
-    And I press "Attempt quiz now"
-    And I should see "Question 1"
-    And I should see "Not yet answered"
-    And I should see "Where is the capital city of France?"
-    And I set the field "Answer:" to "Paris"
-    And I press "Finish attempt ..."
-    And I should see "Answer saved"
-    And I press "Submit all and finish"
-    And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
-    And I log out
 
     # Use the manual grading report.
-    And I log in as "teacher1"
-    And I am on "Course 1" course homepage
-    And I follow "Quiz 1"
-    And I navigate to "Results > Manual grading" in current page administration
-    And I should see "Manual grading"
-    And I follow "Also show questions that have been graded automatically"
+    And user "student1" has attempted "Quiz 1" with responses:
+      | slot | response |
+      |   1  | Paris    |
+    And I reload the page
     And I should see "Short answer 001"
     And "Short answer 001" row "To grade" column of "questionstograde" table should contain "0"
     And "Short answer 001" row "Already graded" column of "questionstograde" table should contain "0"
index 8799c79..766f37a 100644 (file)
@@ -35,29 +35,14 @@ Feature: Basic use of the Grades report
       | question | page | maxmark |
       | TF1      | 1    |         |
       | TF2      | 1    | 3.0     |
-
-    # Add some attempts
-    And I log in as "student1"
-    And I am on "Course 1" course homepage
-    And I follow "Quiz 1"
-    And I press "Attempt quiz now"
-    And I click on "True" "radio" in the "First question" "question"
-    And I click on "False" "radio" in the "Second question" "question"
-    And I press "Finish attempt ..."
-    And I press "Submit all and finish"
-    And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
-    And I log out
-
-    And I log in as "student2"
-    And I am on "Course 1" course homepage
-    And I follow "Quiz 1"
-    And I press "Attempt quiz now"
-    And I click on "True" "radio" in the "First question" "question"
-    And I click on "True" "radio" in the "Second question" "question"
-    And I press "Finish attempt ..."
-    And I press "Submit all and finish"
-    And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
-    And I log out
+    And user "student1" has attempted "Quiz 1" with responses:
+      | slot | response |
+      |   1  | True     |
+      |   2  | False    |
+    And user "student2" has attempted "Quiz 1" with responses:
+      | slot | response |
+      |   1  | True     |
+      |   2  | True     |
 
     # Basic check of the Grades report
     When I log in as "teacher1"
index 7d585b8..4696715 100644 (file)
@@ -327,7 +327,8 @@ function quiz_report_scale_summarks_as_percentage($rawmark, $quiz, $round = true
     if ($round) {
         $mark = quiz_format_grade($quiz, $mark);
     }
-    return $mark . '%';
+
+    return get_string('percents', 'moodle', $mark);
 }
 
 /**
index b8344ea..dc1570d 100644 (file)
@@ -40,29 +40,21 @@ Feature: Basic use of the Responses report
     Then I should see "Attempts: 0"
     And I should see "Nothing to display"
     And I set the field "Attempts from" to "enrolled users who have not attempted the quiz"
-    And I press "Show report"
-    And "Student One" row "State" column of "responses" table should contain "-"
+    And I log out
 
   @javascript
   Scenario: Report works when there are attempts
-    # Add an attempt
-    Given I log in as "student1"
-    And I am on "Course 1" course homepage
-    And I follow "Quiz 1"
-    And I press "Attempt quiz now"
-    And I set the field "Answer" to "1.0"
-    And I press "Check"
-    And I press "Try again"
-    And I set the field "Answer" to "3.0"
-    And I press "Check"
-    And I press "Try again"
-    And I set the field "Answer" to "3.14"
-    And I press "Check"
-    And I press "Finish attempt ..."
-    And I press "Submit all and finish"
-    And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
-    And I log out
-
+    Given user "student1" has started an attempt at quiz "Quiz 1"
+    And user "student1" has checked answers in their attempt at quiz "Quiz 1":
+      | slot | response |
+      |   1  | 1.0      |
+    And user "student1" has checked answers in their attempt at quiz "Quiz 1":
+      | slot | response |
+      |   1  | 3.0      |
+    And user "student1" has checked answers in their attempt at quiz "Quiz 1":
+      | slot | response |
+      |   1  | 3.14     |
+    And user "student1" has finished an attempt at quiz "Quiz 1"
     When I log in as "teacher"
     And I am on "Course 1" course homepage
     And I follow "Quiz 1"
index 38895d0..40b5849 100644 (file)
@@ -100,6 +100,8 @@ $string['quizname'] = 'Quiz name';
 $string['quizoverallstatistics'] = 'Quiz overall statistics';
 $string['quizstructureanalysis'] = 'Quiz structure analysis';
 $string['random_guess_score'] = 'Random guess score';
+$string['rangeofvalues'] = 'Range of statistics for these questions';
+$string['rangebetween'] = '{$a->min} − {$a->max}';
 $string['recalculatenow'] = 'Recalculate now';
 $string['reportsettings'] = 'Statistics calculation settings';
 $string['response'] = 'Response';
@@ -115,4 +117,5 @@ $string['statisticsreportgraph'] = 'Statistics for question positions';
 $string['statistics:view'] = 'View statistics report';
 $string['statsfor'] = 'Quiz statistics (for {$a})';
 $string['variant'] = 'Variant';
+$string['viewanalysis'] = 'View details';
 $string['whichtries'] = 'Analyze responses for';
index 348532f..90adc89 100644 (file)
@@ -26,6 +26,8 @@ defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->libdir.'/tablelib.php');
 
+use \core_question\statistics\questions\calculated_question_summary;
+
 /**
  * This table has one row for each question in the quiz, with sub-rows when
  * random questions and variants appear.
@@ -132,12 +134,35 @@ class quiz_statistics_table extends flexible_table {
         parent::setup();
     }
 
+    /**
+     * Open a div tag to wrap statistics table.
+     */
+    public function  wrap_html_start() {
+        // Horrible Moodle 2.0 wide-content work-around.
+        if (!$this->is_downloading()) {
+            echo html_writer::start_tag('div', array('id' => 'tablecontainer',
+                    'class' => 'statistics-tablecontainer'));
+        }
+    }
+
+    /**
+     * Close a statistics table div.
+     */
+    public function wrap_html_finish() {
+        if (!$this->is_downloading()) {
+            echo html_writer::end_tag('div');
+        }
+    }
+
     /**
      * The question number.
      * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
     protected function col_number($questionstat) {
+        if ($this->is_calculated_question_summary($questionstat)) {
+            return '';
+        }
         if (!isset($questionstat->question->number)) {
             return '';
         }
@@ -160,7 +185,11 @@ class quiz_statistics_table extends flexible_table {
      * @return string contents of this table cell.
      */
     protected function col_icon($questionstat) {
-        return print_question_icon($questionstat->question, true);
+        if ($this->is_calculated_question_summary($questionstat)) {
+            return '';
+        } else {
+            return print_question_icon($questionstat->question, true);
+        }
     }
 
     /**
@@ -169,8 +198,12 @@ class quiz_statistics_table extends flexible_table {
      * @return string contents of this table cell.
      */
     protected function col_actions($questionstat) {
-        return quiz_question_action_icons($this->quiz, $this->cmid,
-                $questionstat->question, $this->baseurl, $questionstat->variant);
+        if ($this->is_calculated_question_summary($questionstat)) {
+            return '';
+        } else {
+            return quiz_question_action_icons($this->quiz, $this->cmid,
+                    $questionstat->question, $this->baseurl, $questionstat->variant);
+        }
     }
 
     /**
@@ -227,13 +260,16 @@ class quiz_statistics_table extends flexible_table {
                 // Question in a slot, we are not on a page showing structural analysis of one slot,
                 // we don't want linking on those pages.
                 $number = $questionstat->question->number;
+                $israndomquestion = $questionstat->question->qtype == 'random';
                 $url = new moodle_url($baseurl, array('slot' => $questionstat->slot));
-                if ($questionstat->get_variants() || $questionstat->get_sub_question_ids()) {
-                    // Question can be broken down into sub-questions or variants. Link will show structural analysis page.
-                    $name = html_writer::link($url,
-                                              $name,
-                                              array('title' => get_string('slotstructureanalysis', 'quiz_statistics', $number)));
-                } else {
+
+                if ($this->is_calculated_question_summary($questionstat)) {
+                    // Only make the random question summary row name link to the slot structure
+                    // analysis page with specific text to clearly indicate the link to the user.
+                    // Random and variant question rows will render the name without a link to improve clarity
+                    // in the UI.
+                    $name = html_writer::div(get_string('rangeofvalues', 'quiz_statistics'));
+                } else if (!$israndomquestion && !$questionstat->get_variants() && !$questionstat->get_sub_question_ids()) {
                     // Question cannot be broken down into sub-questions or variants. Link will show response analysis page.
                     $name = html_writer::link($url,
                                               $name,
@@ -247,7 +283,9 @@ class quiz_statistics_table extends flexible_table {
             $name = html_writer::tag('div', $name, array('class' => 'dubious'));
         }
 
-        if (!empty($questionstat->minmedianmaxnotice)) {
+        if ($this->is_calculated_question_summary($questionstat)) {
+            $name .= html_writer::link($url, get_string('viewanalysis', 'quiz_statistics'));
+        } else if (!empty($questionstat->minmedianmaxnotice)) {
             $name = get_string($questionstat->minmedianmaxnotice, 'quiz_statistics') . '<br />' . $name;
         }
 
@@ -261,11 +299,16 @@ class quiz_statistics_table extends flexible_table {
      * @return string contents of this table cell.
      */
     protected function col_s($questionstat) {
-        if (!isset($questionstat->s)) {
+        if ($this->is_calculated_question_summary($questionstat)) {
+            list($min, $max) = $questionstat->get_min_max_of('s');
+            $min = $min ?: 0;
+            $max = $max ?: 0;
+            return $this->format_range($min, $max);
+        } else if (!isset($questionstat->s)) {
             return 0;
+        } else {
+            return $questionstat->s;
         }
-
-        return $questionstat->s;
     }
 
     /**
@@ -274,11 +317,14 @@ class quiz_statistics_table extends flexible_table {
      * @return string contents of this table cell.
      */
     protected function col_facility($questionstat) {
-        if (is_null($questionstat->facility)) {
+        if ($this->is_calculated_question_summary($questionstat)) {
+            list($min, $max) = $questionstat->get_min_max_of('facility');
+            return $this->format_percentage_range($min, $max);
+        } else if (is_null($questionstat->facility)) {
             return '';
+        } else {
+            return $this->format_percentage($questionstat->facility);
         }
-
-        return number_format($questionstat->facility*100, 2) . '%';
     }
 
     /**
@@ -287,11 +333,14 @@ class quiz_statistics_table extends flexible_table {
      * @return string contents of this table cell.
      */
     protected function col_sd($questionstat) {
-        if (is_null($questionstat->sd) || $questionstat->maxmark == 0) {
+        if ($this->is_calculated_question_summary($questionstat)) {
+            list($min, $max) = $questionstat->get_min_max_of('sd');
+            return $this->format_percentage_range($min, $max);
+        } else if (is_null($questionstat->sd) || $questionstat->maxmark == 0) {
             return '';
+        } else {
+            return $this->format_percentage($questionstat->sd / $questionstat->maxmark);
         }
-
-        return number_format($questionstat->sd*100 / $questionstat->maxmark, 2) . '%';
     }
 
     /**
@@ -300,11 +349,14 @@ class quiz_statistics_table extends flexible_table {
      * @return string contents of this table cell.
      */
     protected function col_random_guess_score($questionstat) {
-        if (is_null($questionstat->randomguessscore)) {
+        if ($this->is_calculated_question_summary($questionstat)) {
+            list($min, $max) = $questionstat->get_min_max_of('randomguessscore');
+            return $this->format_percentage_range($min, $max);
+        } else if (is_null($questionstat->randomguessscore)) {
             return '';
+        } else {
+            return $this->format_percentage($questionstat->randomguessscore);
         }
-
-        return number_format($questionstat->randomguessscore * 100, 2).'%';
     }
 
     /**
@@ -315,7 +367,19 @@ class quiz_statistics_table extends flexible_table {
      * @return string contents of this table cell.
      */
     protected function col_intended_weight($questionstat) {
-        return quiz_report_scale_summarks_as_percentage($questionstat->maxmark, $this->quiz);
+        if ($this->is_calculated_question_summary($questionstat)) {
+            list($min, $max) = $questionstat->get_min_max_of('maxmark');
+
+            if (is_null($min) && is_null($max)) {
+                return '';
+            } else {
+                $min = quiz_report_scale_summarks_as_percentage($min, $this->quiz);
+                $max = quiz_report_scale_summarks_as_percentage($max, $this->quiz);
+                return $this->format_range($min, $max);
+            }
+        } else {
+            return quiz_report_scale_summarks_as_percentage($questionstat->maxmark, $this->quiz);
+        }
     }
 
     /**
@@ -327,11 +391,22 @@ class quiz_statistics_table extends flexible_table {
     protected function col_effective_weight($questionstat) {
         global $OUTPUT;
 
-        if (is_null($questionstat->effectiveweight)) {
-            return '';
-        }
+        if ($this->is_calculated_question_summary($questionstat)) {
+            list($min, $max) = $questionstat->get_min_max_of('effectiveweight');
+
+            if (is_null($min) && is_null($max)) {
+                return '';
+            } else {
+                list( , $negcovar) = $questionstat->get_min_max_of('negcovar');
+                if ($negcovar) {
+                    $min = get_string('negcovar', 'quiz_statistics');
+                }
 
-        if ($questionstat->negcovar) {
+                return $this->format_range($min, $max);
+            }
+        } else if (is_null($questionstat->effectiveweight)) {
+            return '';
+        } else if ($questionstat->negcovar) {
             $negcovar = get_string('negcovar', 'quiz_statistics');
 
             if (!$this->is_downloading()) {
@@ -341,9 +416,9 @@ class quiz_statistics_table extends flexible_table {
             }
 
             return $negcovar;
+        } else {
+            return $this->format_percentage($questionstat->effectiveweight, false);
         }
-
-        return number_format($questionstat->effectiveweight, 2) . '%';
     }
 
     /**
@@ -354,11 +429,26 @@ class quiz_statistics_table extends flexible_table {
      * @return string contents of this table cell.
      */
     protected function col_discrimination_index($questionstat) {
-        if (!is_numeric($questionstat->discriminationindex)) {
+        if ($this->is_calculated_question_summary($questionstat)) {
+            list($min, $max) = $questionstat->get_min_max_of('discriminationindex');
+
+            if (isset($max)) {
+                $min = $min ?: 0;
+            }
+
+            if (is_numeric($min)) {
+                $min = $this->format_percentage($min, false);
+            }
+            if (is_numeric($max)) {
+                $max = $this->format_percentage($max, false);
+            }
+
+            return $this->format_range($min, $max);
+        } else if (!is_numeric($questionstat->discriminationindex)) {
             return $questionstat->discriminationindex;
+        } else {
+            return $this->format_percentage($questionstat->discriminationindex, false);
         }
-
-        return number_format($questionstat->discriminationindex, 2) . '%';
     }
 
     /**
@@ -368,11 +458,19 @@ class quiz_statistics_table extends flexible_table {
      * @return string contents of this table cell.
      */
     protected function col_discriminative_efficiency($questionstat) {
-        if (!is_numeric($questionstat->discriminativeefficiency)) {
+        if ($this->is_calculated_question_summary($questionstat)) {
+            list($min, $max) = $questionstat->get_min_max_of('discriminativeefficiency');
+
+            if (!is_numeric($min) && !is_numeric($max)) {
+                return '';
+            } else {
+                return $this->format_percentage_range($min, $max, false);
+            }
+        } else if (!is_numeric($questionstat->discriminativeefficiency)) {
             return '';
+        } else {
+            return $this->format_percentage($questionstat->discriminativeefficiency);
         }
-
-        return number_format($questionstat->discriminativeefficiency, 2) . '%';
     }
 
     /**
@@ -381,24 +479,86 @@ class quiz_statistics_table extends flexible_table {
      * @return bool is this question possibly not pulling it's weight?
      */
     protected function is_dubious_question($questionstat) {
-        if (!is_numeric($questionstat->discriminativeefficiency)) {
+        if ($this->is_calculated_question_summary($questionstat)) {
+            // We only care about the minimum value here.
+            // If the minimum value is less than the threshold, then we know that there is at least one value below the threshold.
+            list($discriminativeefficiency) = $questionstat->get_min_max_of('discriminativeefficiency');
+        } else {
+            $discriminativeefficiency = $questionstat->discriminativeefficiency;
+        }
+
+        if (!is_numeric($discriminativeefficiency)) {
             return false;
         }
 
-        return $questionstat->discriminativeefficiency < 15;
+        return $discriminativeefficiency < 15;
     }
 
-    public function  wrap_html_start() {
-        // Horrible Moodle 2.0 wide-content work-around.
-        if (!$this->is_downloading()) {
-            echo html_writer::start_tag('div', array('id' => 'tablecontainer',
-                    'class' => 'statistics-tablecontainer'));
+    /**
+     * Check if the given stats object is an instance of calculated_question_summary.
+     *
+     * @param  \core_question\statistics\questions\calculated $questionstat Stats object
+     * @return bool
+     */
+    protected function is_calculated_question_summary($questionstat) {
+        return $questionstat instanceof calculated_question_summary;
+    }
+
+    /**
+     * Format inputs to represent a range between $min and $max.
+     * This function does not check if $min is less than $max or not