From cf398020d5263b8889aec3396df2f7d1cc97f00d Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Thu, 5 Apr 2018 14:00:48 +0800 Subject: [PATCH] MDL-61864 tool_policy: user agreement reports --- admin/tool/policy/accept.php | 65 ++ admin/tool/policy/acceptances.php | 65 ++ .../amd/build/acceptances_filter.min.js | 1 + .../acceptances_filter_datasource.min.js | 1 + .../tool/policy/amd/build/acceptmodal.min.js | 1 + .../tool/policy/amd/src/acceptances_filter.js | 144 ++++ .../amd/src/acceptances_filter_datasource.js | 93 +++ admin/tool/policy/amd/src/acceptmodal.js | 244 +++++++ .../tool/policy/classes/acceptances_table.php | 648 ++++++++++++++++++ .../policy/classes/event/acceptance_base.php | 117 ++++ .../classes/event/acceptance_created.php | 73 ++ .../classes/event/acceptance_updated.php | 73 ++ .../policy/classes/form/accept_policy.php | 161 +++++ .../policy/classes/output/acceptances.php | 132 ++++ .../classes/output/acceptances_filter.php | 465 +++++++++++++ .../policy/classes/output/user_agreement.php | 114 +++ admin/tool/policy/pix/agreedno.png | Bin 0 -> 258 bytes admin/tool/policy/pix/agreedno.svg | 3 + admin/tool/policy/pix/agreedyes.png | Bin 0 -> 234 bytes admin/tool/policy/pix/agreedyes.svg | 3 + admin/tool/policy/pix/agreedyesonbehalf.png | Bin 0 -> 234 bytes admin/tool/policy/pix/agreedyesonbehalf.svg | 3 + .../policy/templates/acceptances.mustache | 149 ++++ .../templates/acceptances_filter.mustache | 67 ++ .../policy/templates/user_agreement.mustache | 65 ++ .../policy/tests/behat/acceptances.feature | 230 +++++++ .../policy/tests/behat/behat_tool_policy.php | 111 +++ .../policy/tests/behat/managepolicies.feature | 261 +++++++ admin/tool/policy/user.php | 53 ++ lib/classes/plugin_manager.php | 4 +- 30 files changed, 3344 insertions(+), 2 deletions(-) create mode 100644 admin/tool/policy/accept.php create mode 100644 admin/tool/policy/acceptances.php create mode 100644 admin/tool/policy/amd/build/acceptances_filter.min.js create mode 100644 admin/tool/policy/amd/build/acceptances_filter_datasource.min.js create mode 100644 admin/tool/policy/amd/build/acceptmodal.min.js create mode 100644 admin/tool/policy/amd/src/acceptances_filter.js create mode 100644 admin/tool/policy/amd/src/acceptances_filter_datasource.js create mode 100644 admin/tool/policy/amd/src/acceptmodal.js create mode 100644 admin/tool/policy/classes/acceptances_table.php create mode 100644 admin/tool/policy/classes/event/acceptance_base.php create mode 100644 admin/tool/policy/classes/event/acceptance_created.php create mode 100644 admin/tool/policy/classes/event/acceptance_updated.php create mode 100644 admin/tool/policy/classes/form/accept_policy.php create mode 100644 admin/tool/policy/classes/output/acceptances.php create mode 100644 admin/tool/policy/classes/output/acceptances_filter.php create mode 100644 admin/tool/policy/classes/output/user_agreement.php create mode 100644 admin/tool/policy/pix/agreedno.png create mode 100644 admin/tool/policy/pix/agreedno.svg create mode 100644 admin/tool/policy/pix/agreedyes.png create mode 100644 admin/tool/policy/pix/agreedyes.svg create mode 100644 admin/tool/policy/pix/agreedyesonbehalf.png create mode 100644 admin/tool/policy/pix/agreedyesonbehalf.svg create mode 100644 admin/tool/policy/templates/acceptances.mustache create mode 100644 admin/tool/policy/templates/acceptances_filter.mustache create mode 100644 admin/tool/policy/templates/user_agreement.mustache create mode 100644 admin/tool/policy/tests/behat/acceptances.feature create mode 100644 admin/tool/policy/tests/behat/behat_tool_policy.php create mode 100644 admin/tool/policy/tests/behat/managepolicies.feature create mode 100644 admin/tool/policy/user.php diff --git a/admin/tool/policy/accept.php b/admin/tool/policy/accept.php new file mode 100644 index 00000000000..8a7919837b3 --- /dev/null +++ b/admin/tool/policy/accept.php @@ -0,0 +1,65 @@ +. + +/** + * Accept policies on behalf of users (non-JS version) + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__.'/../../../config.php'); +require_once($CFG->dirroot.'/user/editlib.php'); + +$userids = optional_param_array('userids', null, PARAM_INT); +$versionids = optional_param_array('versionids', null, PARAM_INT); +$returnurl = optional_param('returnurl', null, PARAM_LOCALURL); + +require_login(); +if (isguestuser()) { + print_error('noguest'); +} +$context = context_system::instance(); + +$PAGE->set_context($context); +$PAGE->set_url(new moodle_url('/admin/tool/policy/accept.php')); + +if ($returnurl) { + $returnurl = new moodle_url($returnurl); +} else if (count($userids) == 1) { + $userid = reset($userids); + $returnurl = new moodle_url('/admin/tool/policy/user.php', ['userid' => $userid]); +} else { + $returnurl = new moodle_url('/admin/tool/policy/acceptances.php'); +} +// Initialise the form, this will also validate users, versions and check permission to accept policies. +$form = new \tool_policy\form\accept_policy(null, + ['versionids' => $versionids, 'userids' => $userids, 'showbuttons' => true]); +$form->set_data(['returnurl' => $returnurl]); + +if ($form->is_cancelled()) { + redirect($returnurl); +} else if ($form->get_data()) { + $form->process(); + redirect($returnurl); +} + +$output = $PAGE->get_renderer('tool_policy'); +echo $output->header(); +echo $output->heading(get_string('consentdetails', 'tool_policy')); +$form->display(); +echo $output->footer(); diff --git a/admin/tool/policy/acceptances.php b/admin/tool/policy/acceptances.php new file mode 100644 index 00000000000..dbf0dc929bd --- /dev/null +++ b/admin/tool/policy/acceptances.php @@ -0,0 +1,65 @@ +. + +/** + * View user acceptances to the policies + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__.'/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +use core\output\notification; + +$policyid = optional_param('policyid', null, PARAM_INT); +$versionid = optional_param('versionid', null, PARAM_INT); +$versionid = optional_param('versionid', null, PARAM_INT); +$filtersapplied = optional_param_array('unified-filters', [], PARAM_NOTAGS); + +$acceptancesfilter = new \tool_policy\output\acceptances_filter($policyid, $versionid, $filtersapplied); +$policyid = $acceptancesfilter->get_policy_id_filter(); +$versionid = $acceptancesfilter->get_version_id_filter(); + +// Set up the page as an admin page 'tool_policy_managedocs'. +$urlparams = ($policyid ? ['policyid' => $policyid] : []) + ($versionid ? ['versionid' => $versionid] : []); +admin_externalpage_setup('tool_policy_acceptances', '', $urlparams, + new moodle_url('/admin/tool/policy/acceptances.php')); + +$acceptancesfilter->validate_ids(); +$output = $PAGE->get_renderer('tool_policy'); +if ($acceptancesfilter->get_versions()) { + $acceptances = new \tool_policy\acceptances_table('tool_policy_user_acceptances', $acceptancesfilter, $output); + if ($acceptances->is_downloading()) { + $acceptances->download(); + } +} + +echo $output->header(); +echo $output->heading(get_string('useracceptances', 'tool_policy')); +echo $output->render($acceptancesfilter); +if (!empty($acceptances)) { + $acceptances->display(); +} else if ($acceptancesfilter->get_avaliable_policies()) { + // There are no non-guest policies. + echo $output->notification(get_string('selectpolicyandversion', 'tool_policy'), notification::NOTIFY_INFO); +} else { + // There are no non-guest policies. + echo $output->notification(get_string('nopolicies', 'tool_policy'), notification::NOTIFY_INFO); +} +echo $output->footer(); diff --git a/admin/tool/policy/amd/build/acceptances_filter.min.js b/admin/tool/policy/amd/build/acceptances_filter.min.js new file mode 100644 index 00000000000..793e5af0251 --- /dev/null +++ b/admin/tool/policy/amd/build/acceptances_filter.min.js @@ -0,0 +1 @@ +define(["jquery","core/form-autocomplete","core/str","core/notification"],function(a,b,c,d){var e={UNIFIED_FILTERS:"#unified-filters"},f=function(){var f=[{key:"filterplaceholder",component:"tool_policy"},{key:"nofiltersapplied",component:"tool_policy"}];M.util.js_pending("acceptances_filter_datasource"),c.get_strings(f).done(function(a){var c=a[0],f=a[1];b.enhance(e.UNIFIED_FILTERS,!0,"tool_policy/acceptances_filter_datasource",c,!1,!0,f,!0).then(function(){M.util.js_complete("acceptances_filter_datasource")}).fail(d.exception)}).fail(d.exception);var g=a(e.UNIFIED_FILTERS).val();a(e.UNIFIED_FILTERS).on("change",function(){var b=a(this).val(),c=[],d=[],e=!1;if(a.each(b,function(a,b){var f=b.split(":",2);if(2!==f.length)return d.push(b),!0;var g=f[0],h=f[1];return"undefined"!=typeof c[g]&&(e=!0),c[g]=h,!0}),e){var f=[];for(var h in c)f.push(h+":"+c[h]);f=f.concat(d),a(this).val(f)}g.join(",")!=b.join(",")&&this.form.submit()})},g=function(){return a(e.UNIFIED_FILTERS).closest("form")};return{init:function(){f()},getForm:function(){return g()}}}); \ No newline at end of file diff --git a/admin/tool/policy/amd/build/acceptances_filter_datasource.min.js b/admin/tool/policy/amd/build/acceptances_filter_datasource.min.js new file mode 100644 index 00000000000..567ed4aa88a --- /dev/null +++ b/admin/tool/policy/amd/build/acceptances_filter_datasource.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/notification"],function(a,b,c){return{list:function(b,c){var d=[],e=a(b),f=a(b).data("originaloptionsjson"),g=e.val();a.each(f,function(b,e){return""!==a.trim(c)&&e.label.toLocaleLowerCase().indexOf(c.toLocaleLowerCase())===-1||(a.inArray(e.value,g)>-1||(d.push(e),!0))});var h=new a.Deferred;return h.resolve(d),h.promise()},processResults:function(b,c){var d=[];return a.each(c,function(a,b){d.push({value:b.value,label:b.label})}),d},transport:function(a,b,d){this.list(a,b).then(d)["catch"](c.exception)}}}); \ No newline at end of file diff --git a/admin/tool/policy/amd/build/acceptmodal.min.js b/admin/tool/policy/amd/build/acceptmodal.min.js new file mode 100644 index 00000000000..ac65c873ba1 --- /dev/null +++ b/admin/tool/policy/amd/build/acceptmodal.min.js @@ -0,0 +1 @@ +define(["jquery","core/str","core/modal_factory","core/modal_events","core/notification","core/fragment","core/ajax","core/yui"],function(a,b,c,d,e,f,g,h){"use strict";var i=function(a){this.contextid=a,this.init()};return i.prototype.modal=null,i.prototype.contextid=-1,i.prototype.stringKeys=[{key:"consentdetails",component:"tool_policy"},{key:"iagreetothepolicy",component:"tool_policy"},{key:"selectusersforconsent",component:"tool_policy"},{key:"ok"}],i.prototype.init=function(){var c=a("a[data-action=acceptmodal]");c.on("click",function(b){b.preventDefault();var c=a(b.currentTarget).attr("href"),d=c.slice(c.indexOf("?")+1);this.showFormModal(d)}.bind(this)),c=a("form[data-action=acceptmodal]"),c.on("submit",function(d){if(d.preventDefault(),a(d.currentTarget).find('input[type=checkbox][name="userids[]"]:checked').length){var f=a(d.currentTarget).serialize();this.showFormModal(f,c)}else b.get_strings(this.stringKeys).done(function(a){e.alert("",a[2],a[3])})}.bind(this))},i.prototype.showFormModal=function(a,d){b.get_strings(this.stringKeys).done(function(b){c.create({type:c.types.SAVE_CANCEL,title:b[0],body:""},d).done(function(c){this.modal=c,this.setupFormModal(a,b[1])}.bind(this))}.bind(this)).fail(e.exception)},i.prototype.setupFormModal=function(a,b){var c=this.modal;c.setLarge(),c.setSaveButtonText(b),c.getRoot().on(d.hidden,this.destroy.bind(this)),c.setBody(this.getBody(a)),c.getRoot().on(d.save,this.submitForm.bind(this)),c.getRoot().on("submit","form",this.submitFormAjax.bind(this)),c.show()},i.prototype.getBody=function(a){"undefined"==typeof a&&(a={});var b={jsonformdata:JSON.stringify(a)};return f.loadFragment("tool_policy","accept_on_behalf",this.contextid,b)},i.prototype.submitFormAjax=function(a){a.preventDefault();var b=this.modal.getRoot().find("form").serialize(),c=g.call([{methodname:"tool_policy_submit_accept_on_behalf",args:{jsonformdata:JSON.stringify(b)}}]);c[0].done(function(a){a.validationerrors?this.modal.setBody(this.getBody(b)):this.close()}.bind(this)).fail(e.exception)},i.prototype.submitForm=function(a){a.preventDefault(),this.modal.getRoot().find("form").submit()},i.prototype.close=function(){this.destroy(),document.location.reload()},i.prototype.destroy=function(){h.use("moodle-core-formchangechecker",function(){M.core_formchangechecker.reset_form_dirty_state()}),this.modal.destroy()},{getInstance:function(a){new i(a)}}}); \ No newline at end of file diff --git a/admin/tool/policy/amd/src/acceptances_filter.js b/admin/tool/policy/amd/src/acceptances_filter.js new file mode 100644 index 00000000000..b02c64b4b2c --- /dev/null +++ b/admin/tool/policy/amd/src/acceptances_filter.js @@ -0,0 +1,144 @@ +// 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 . + +/** + * Unified filter page JS module for the course participants page. + * + * @module tool_policy/acceptances_filter + * @package tool_policy + * @copyright 2017 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'], + function($, Autocomplete, Str, Notification) { + + /** + * Selectors. + * + * @access private + * @type {{UNIFIED_FILTERS: string}} + */ + var SELECTORS = { + UNIFIED_FILTERS: '#unified-filters' + }; + + /** + * Init function. + * + * @method init + * @private + */ + var init = function() { + var stringkeys = [{ + key: 'filterplaceholder', + component: 'tool_policy' + }, { + key: 'nofiltersapplied', + component: 'tool_policy' + }]; + + M.util.js_pending('acceptances_filter_datasource'); + Str.get_strings(stringkeys).done(function(langstrings) { + var placeholder = langstrings[0]; + var noSelectionString = langstrings[1]; + Autocomplete.enhance(SELECTORS.UNIFIED_FILTERS, true, 'tool_policy/acceptances_filter_datasource', placeholder, + false, true, noSelectionString, true) + .then(function() { + M.util.js_complete('acceptances_filter_datasource'); + + return; + }) + .fail(Notification.exception); + }).fail(Notification.exception); + + var last = $(SELECTORS.UNIFIED_FILTERS).val(); + $(SELECTORS.UNIFIED_FILTERS).on('change', function() { + var current = $(this).val(); + var listoffilters = []; + var textfilters = []; + var updatedselectedfilters = false; + + $.each(current, function(index, catoption) { + var catandoption = catoption.split(':', 2); + if (catandoption.length !== 2) { + textfilters.push(catoption); + return true; // Text search filter. + } + + var category = catandoption[0]; + var option = catandoption[1]; + + // The last option (eg. 'Teacher') out of a category (eg. 'Role') in this loop is the one that was last + // selected, so we want to use that if there are multiple options from the same category. Eg. The user + // may have chosen to filter by the 'Student' role, then wanted to filter by the 'Teacher' role - the + // last option in the category to be selected (in this case 'Teacher') will come last, so will overwrite + // 'Student' (after this if). We want to let the JS know that the filters have been updated. + if (typeof listoffilters[category] !== 'undefined') { + updatedselectedfilters = true; + } + + listoffilters[category] = option; + return true; + }); + + // Check if we have something to remove from the list of filters. + if (updatedselectedfilters) { + // Go through and put the list into something we can use to update the list of filters. + var updatefilters = []; + for (var category in listoffilters) { + updatefilters.push(category + ":" + listoffilters[category]); + } + updatefilters = updatefilters.concat(textfilters); + $(this).val(updatefilters); + } + + // Prevent form from submitting unnecessarily, eg. on blur when no filter is selected. + if (last.join(',') != current.join(',')) { + this.form.submit(); + } + }); + }; + + /** + * Return the unified user filter form. + * + * @method getForm + * @return {DOMElement} + */ + var getForm = function() { + return $(SELECTORS.UNIFIED_FILTERS).closest('form'); + }; + + return /** @alias module:core/form-autocomplete */ { + /** + * Initialise the unified user filter. + * + * @method init + */ + init: function() { + init(); + }, + + /** + * Return the unified user filter form. + * + * @method getForm + * @return {DOMElement} + */ + getForm: function() { + return getForm(); + } + }; + }); diff --git a/admin/tool/policy/amd/src/acceptances_filter_datasource.js b/admin/tool/policy/amd/src/acceptances_filter_datasource.js new file mode 100644 index 00000000000..7b1f98e2b4c --- /dev/null +++ b/admin/tool/policy/amd/src/acceptances_filter_datasource.js @@ -0,0 +1,93 @@ +// 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 . + +/** + * Datasource for the tool_policy/acceptances_filter. + * + * This module is compatible with core/form-autocomplete. + * + * @package tool_policy + * @copyright 2017 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) { + + return /** @alias module:tool_policy/acceptances_filter_datasource */ { + /** + * List filter options. + * + * @param {String} selector The select element selector. + * @param {String} query The query string. + * @return {Promise} + */ + list: function(selector, query) { + var filteredOptions = []; + + var el = $(selector); + var originalOptions = $(selector).data('originaloptionsjson'); + var selectedFilters = el.val(); + $.each(originalOptions, function(index, option) { + // Skip option if it does not contain the query string. + if ($.trim(query) !== '' && option.label.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) === -1) { + return true; + } + // Skip filters that have already been selected. + if ($.inArray(option.value, selectedFilters) > -1) { + return true; + } + + filteredOptions.push(option); + return true; + }); + + var deferred = new $.Deferred(); + deferred.resolve(filteredOptions); + + return deferred.promise(); + }, + + /** + * Process the results for auto complete elements. + * + * @param {String} selector The selector of the auto complete element. + * @param {Array} results An array or results. + * @return {Array} New array of results. + */ + processResults: function(selector, results) { + var options = []; + $.each(results, function(index, data) { + options.push({ + value: data.value, + label: data.label + }); + }); + return options; + }, + + /** + * Source of data for Ajax element. + * + * @param {String} selector The selector of the auto complete element. + * @param {String} query The query string. + * @param {Function} callback A callback function receiving an array of results. + */ + /* eslint-disable promise/no-callback-in-promise */ + transport: function(selector, query, callback) { + this.list(selector, query).then(callback).catch(Notification.exception); + } + }; + +}); diff --git a/admin/tool/policy/amd/src/acceptmodal.js b/admin/tool/policy/amd/src/acceptmodal.js new file mode 100644 index 00000000000..d7ff943c252 --- /dev/null +++ b/admin/tool/policy/amd/src/acceptmodal.js @@ -0,0 +1,244 @@ +// 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 . + +/** + * Add policy consent modal to the page + * + * @module tool_policy/acceptmodal + * @class AcceptOnBehalf + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/notification', 'core/fragment', + 'core/ajax', 'core/yui'], + function($, Str, ModalFactory, ModalEvents, Notification, Fragment, Ajax, Y) { + + "use strict"; + + /** + * Constructor + * + * @param {int} contextid + * + * Each call to init gets it's own instance of this class. + */ + var AcceptOnBehalf = function(contextid) { + this.contextid = contextid; + this.init(); + }; + + /** + * @var {Modal} modal + * @private + */ + AcceptOnBehalf.prototype.modal = null; + + /** + * @var {int} contextid + * @private + */ + AcceptOnBehalf.prototype.contextid = -1; + + /** + * @var {Array} strings + * @private + */ + AcceptOnBehalf.prototype.stringKeys = [ + { + key: 'consentdetails', + component: 'tool_policy' + }, + { + key: 'iagreetothepolicy', + component: 'tool_policy' + }, + { + key: 'selectusersforconsent', + component: 'tool_policy' + }, + { + key: 'ok' + } + ]; + + /** + * Initialise the class. + * + * @private + */ + AcceptOnBehalf.prototype.init = function() { + // Initialise for links accepting policies for individual users. + var triggers = $('a[data-action=acceptmodal]'); + triggers.on('click', function(e) { + e.preventDefault(); + var href = $(e.currentTarget).attr('href'), + formData = href.slice(href.indexOf('?') + 1); + this.showFormModal(formData); + }.bind(this)); + + // Initialise for multiple users acceptance form. + triggers = $('form[data-action=acceptmodal]'); + triggers.on('submit', function(e) { + e.preventDefault(); + if ($(e.currentTarget).find('input[type=checkbox][name="userids[]"]:checked').length) { + var formData = $(e.currentTarget).serialize(); + this.showFormModal(formData, triggers); + } else { + Str.get_strings(this.stringKeys).done(function (strings) { + Notification.alert('', strings[2], strings[3]); + }); + } + }.bind(this)); + }; + + /** + * Show modal with a form + * + * @param {String} formData + * @param {object} triggerElement The trigger HTML jQuery object + */ + AcceptOnBehalf.prototype.showFormModal = function(formData, triggerElement) { + // Fetch the title string. + Str.get_strings(this.stringKeys).done(function(strings) { + // Create the modal. + ModalFactory.create({ + type: ModalFactory.types.SAVE_CANCEL, + title: strings[0], + body: '' + }, triggerElement).done(function(modal) { + this.modal = modal; + this.setupFormModal(formData, strings[1]); + }.bind(this)); + }.bind(this)) + .fail(Notification.exception); + }; + + /** + * Setup form inside a modal + * + * @param {String} formData + * @param {String} saveText + */ + AcceptOnBehalf.prototype.setupFormModal = function(formData, saveText) { + var modal = this.modal; + + modal.setLarge(); + + modal.setSaveButtonText(saveText); + + // We want to reset the form every time it is opened. + modal.getRoot().on(ModalEvents.hidden, this.destroy.bind(this)); + + modal.setBody(this.getBody(formData)); + + // We catch the modal save event, and use it to submit the form inside the modal. + // Triggering a form submission will give JS validation scripts a chance to check for errors. + modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this)); + // We also catch the form submit event and use it to submit the form with ajax. + modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this)); + + modal.show(); + }; + + /** + * Load the body of the modal (contains the form) + * + * @method getBody + * @private + * @param {String} formData + * @return {Promise} + */ + AcceptOnBehalf.prototype.getBody = function(formData) { + if (typeof formData === "undefined") { + formData = {}; + } + // Get the content of the modal. + var params = {jsonformdata: JSON.stringify(formData)}; + return Fragment.loadFragment('tool_policy', 'accept_on_behalf', this.contextid, params); + }; + + /** + * Submit the form inside the modal via AJAX request + * + * @method submitFormAjax + * @private + * @param {Event} e Form submission event. + */ + AcceptOnBehalf.prototype.submitFormAjax = function(e) { + // We don't want to do a real form submission. + e.preventDefault(); + + // Convert all the form elements values to a serialised string. + var formData = this.modal.getRoot().find('form').serialize(); + + var requests = Ajax.call([{ + methodname: 'tool_policy_submit_accept_on_behalf', + args: {jsonformdata: JSON.stringify(formData)} + }]); + requests[0].done(function(data) { + if (data.validationerrors) { + this.modal.setBody(this.getBody(formData)); + } else { + this.close(); + } + }.bind(this)).fail(Notification.exception); + }; + + /** + * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed. + * + * @method submitForm + * @param {Event} e Form submission event. + * @private + */ + AcceptOnBehalf.prototype.submitForm = function(e) { + e.preventDefault(); + this.modal.getRoot().find('form').submit(); + }; + + /** + * Close the modal + */ + AcceptOnBehalf.prototype.close = function() { + this.destroy(); + document.location.reload(); + }; + + /** + * Destroy the modal + */ + AcceptOnBehalf.prototype.destroy = function() { + Y.use('moodle-core-formchangechecker', function() { + M.core_formchangechecker.reset_form_dirty_state(); + }); + this.modal.destroy(); + }; + + return /** @alias module:tool_policy/acceptmodal */ { + // Public variables and functions. + /** + * Attach event listeners to initialise this module. + * + * @method init + * @param {string} selector The CSS selector used to find nodes that will trigger this module. + * @param {int} contextid The contextid for the course. + * @return {Promise} + */ + getInstance: function(contextid) { + new AcceptOnBehalf(contextid); + } + }; + }); diff --git a/admin/tool/policy/classes/acceptances_table.php b/admin/tool/policy/classes/acceptances_table.php new file mode 100644 index 00000000000..a7235198341 --- /dev/null +++ b/admin/tool/policy/classes/acceptances_table.php @@ -0,0 +1,648 @@ +. + +/** + * View user acceptances to the policies + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_policy; + +use tool_policy\output\acceptances_filter; +use tool_policy\output\renderer; +use tool_policy\output\user_agreement; +use core_user; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot.'/lib/tablelib.php'); + +/** + * Class acceptances_table + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class acceptances_table extends \table_sql { + + /** @var array */ + protected $versionids; + + /** @var acceptances_filter */ + protected $acceptancesfilter; + + /** @var renderer */ + protected $output; + + /** + * @var string[] The list of countries. + */ + protected $countries; + + /** @var bool are there any users that this user can agree on behalf of */ + protected $canagreeany = false; + + /** + * Constructor. + * + * @param string $uniqueid Table identifier. + * @param acceptances_filter $acceptancesfilter + * @param renderer $output + */ + public function __construct($uniqueid, acceptances_filter $acceptancesfilter, renderer $output) { + global $CFG; + parent::__construct($uniqueid); + $this->set_attribute('id', 'acceptancetable'); + $this->acceptancesfilter = $acceptancesfilter; + $this->is_downloading(optional_param('download', 0, PARAM_ALPHA), 'user_acceptances'); + $this->baseurl = $acceptancesfilter->get_url(); + $this->output = $output; + + $this->versionids = []; + $versions = $acceptancesfilter->get_versions(); + if (count($versions) > 1) { + foreach ($versions as $version) { + $this->versionids[$version->id] = $version->name; + } + } else { + $version = reset($versions); + $this->versionids[$version->id] = $version->name; + if ($version->status != policy_version::STATUS_ACTIVE) { + $this->versionids[$version->id] .= '
' . $version->revision; + } + } + + $extrafields = get_extra_user_fields(\context_system::instance()); + $userfields = \user_picture::fields('u', $extrafields); + + $this->set_sql("$userfields", + "{user} u", + 'u.id <> :siteguestid AND u.deleted = 0', + ['siteguestid' => $CFG->siteguest]); + if (!$this->is_downloading()) { + $this->add_column_header('select', get_string('select'), false, 'colselect'); + } + $this->add_column_header('fullname', get_string('fullnameuser', 'core')); + foreach ($extrafields as $field) { + $this->add_column_header($field, get_user_field_name($field)); + } + + if (!$this->is_downloading() && !has_capability('tool/policy:acceptbehalf', \context_system::instance())) { + // We will need to check capability to accept on behalf in each user's context, preload users contexts. + $this->sql->fields .= ',' . \context_helper::get_preload_record_columns_sql('ctx'); + $this->sql->from .= ' JOIN {context} ctx ON ctx.contextlevel = :usercontextlevel AND ctx.instanceid = u.id'; + $this->sql->params['usercontextlevel'] = CONTEXT_USER; + } + + if ($this->acceptancesfilter->get_single_version()) { + $this->configure_for_single_version(); + } else { + $this->configure_for_multiple_versions(); + } + + $this->build_sql_for_search_string($extrafields); + $this->build_sql_for_capability_filter(); + $this->build_sql_for_roles_filter(); + + $this->sortable(true, 'firstname'); + } + + /** + * Remove randomness from the list by always sorting by user id in the end + * + * @return array + */ + public function get_sort_columns() { + $c = parent::get_sort_columns(); + $c['u.id'] = SORT_ASC; + return $c; + } + + /** + * Allows to add only one column name and header to the table (parent class methods only allow to set all). + * + * @param string $key + * @param string $label + * @param bool $sortable + * @param string $columnclass + */ + protected function add_column_header($key, $label, $sortable = true, $columnclass = '') { + if (empty($this->columns)) { + $this->define_columns([$key]); + $this->define_headers([$label]); + } else { + $this->columns[$key] = count($this->columns); + $this->column_style[$key] = array(); + $this->column_class[$key] = $columnclass; + $this->column_suppress[$key] = false; + $this->headers[] = $label; + } + if ($columnclass !== null) { + $this->column_class($key, $columnclass); + } + if (!$sortable) { + $this->no_sorting($key); + } + } + + /** + * Helper configuration method. + */ + protected function configure_for_single_version() { + $userfieldsmod = get_all_user_name_fields(true, 'm', null, 'mod'); + $v = key($this->versionids); + $this->sql->fields .= ", $userfieldsmod, a{$v}.status AS status{$v}, a{$v}.note, ". + "a{$v}.timemodified, a{$v}.usermodified AS usermodified{$v}"; + + $join = "JOIN {tool_policy_acceptances} a{$v} ON a{$v}.userid = u.id AND a{$v}.policyversionid=:versionid{$v}"; + $filterstatus = $this->acceptancesfilter->get_status_filter(); + if ($filterstatus == 1) { + $this->sql->from .= " $join AND a{$v}.status=1"; + } else { + $this->sql->from .= " LEFT $join"; + } + + $this->sql->from .= " LEFT JOIN {user} m ON m.id = a{$v}.usermodified AND m.id <> u.id AND a{$v}.status = 1"; + + $this->sql->params['versionid' . $v] = $v; + + if ($filterstatus === 0) { + $this->sql->where .= " AND (a{$v}.status IS NULL OR a{$v}.status = 0)"; + } + + $this->add_column_header('status' . $v, get_string('agreed', 'tool_policy'), true, 'mdl-align'); + $this->add_column_header('timemodified', get_string('agreedon', 'tool_policy')); + $this->add_column_header('usermodified' . $v, get_string('agreedby', 'tool_policy')); + $this->add_column_header('note', get_string('acceptancenote', 'tool_policy'), false); + } + + /** + * Helper configuration method. + */ + protected function configure_for_multiple_versions() { + $this->add_column_header('statusall', get_string('acceptancestatusoverall', 'tool_policy')); + $filterstatus = $this->acceptancesfilter->get_status_filter(); + $statusall = []; + foreach ($this->versionids as $v => $versionname) { + $this->sql->fields .= ", a{$v}.status AS status{$v}, a{$v}.usermodified AS usermodified{$v}"; + $join = "JOIN {tool_policy_acceptances} a{$v} ON a{$v}.userid = u.id AND a{$v}.policyversionid=:versionid{$v}"; + if ($filterstatus == 1) { + $this->sql->from .= " {$join} AND a{$v}.status=1"; + } else { + $this->sql->from .= " LEFT {$join}"; + } + $this->sql->params['versionid' . $v] = $v; + $this->add_column_header('status' . $v, $versionname, true, 'mdl-align'); + $statusall[] = "COALESCE(a{$v}.status, 0)"; + } + $this->sql->fields .= ",".join('+', $statusall)." AS statusall"; + + if ($filterstatus === 0) { + $statussql = []; + foreach ($this->versionids as $v => $versionname) { + $statussql[] = "a{$v}.status IS NULL OR a{$v}.status = 0"; + } + $this->sql->where .= " AND (u.policyagreed = 0 OR ".join(" OR ", $statussql).")"; + } + } + + /** + * Download the data. + */ + public function download() { + \core\session\manager::write_close(); + $this->out(0, false); + exit; + } + + /** + * Get sql to add to where statement. + * + * @return string + */ + public function get_sql_where() { + list($where, $params) = parent::get_sql_where(); + $where = preg_replace('/firstname/', 'u.firstname', $where); + $where = preg_replace('/lastname/', 'u.lastname', $where); + return [$where, $params]; + } + + /** + * Helper SQL query builder. + * + * @param array $userfields + */ + protected function build_sql_for_search_string($userfields) { + global $DB, $USER; + + $search = $this->acceptancesfilter->get_search_strings(); + if (empty($search)) { + return; + } + + $wheres = []; + $params = []; + foreach ($search as $index => $keyword) { + $searchkey1 = 'search' . $index . '1'; + $searchkey2 = 'search' . $index . '2'; + $searchkey3 = 'search' . $index . '3'; + $searchkey4 = 'search' . $index . '4'; + $searchkey5 = 'search' . $index . '5'; + $searchkey6 = 'search' . $index . '6'; + $searchkey7 = 'search' . $index . '7'; + + $conditions = array(); + // Search by fullname. + $fullname = $DB->sql_fullname('u.firstname', 'u.lastname'); + $conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false); + + // Search by email. + $email = $DB->sql_like('u.email', ':' . $searchkey2, false, false); + if (!in_array('email', $userfields)) { + $maildisplay = 'maildisplay' . $index; + $userid1 = 'userid' . $index . '1'; + // Prevent users who hide their email address from being found by others + // who aren't allowed to see hidden email addresses. + $email = "(". $email ." AND (" . + "u.maildisplay <> :$maildisplay " . + "OR u.id = :$userid1". // User can always find himself. + "))"; + $params[$maildisplay] = core_user::MAILDISPLAY_HIDE; + $params[$userid1] = $USER->id; + } + $conditions[] = $email; + + // Search by idnumber. + $idnumber = $DB->sql_like('u.idnumber', ':' . $searchkey3, false, false); + if (!in_array('idnumber', $userfields)) { + $userid2 = 'userid' . $index . '2'; + // Users who aren't allowed to see idnumbers should at most find themselves + // when searching for an idnumber. + $idnumber = "(". $idnumber . " AND u.id = :$userid2)"; + $params[$userid2] = $USER->id; + } + $conditions[] = $idnumber; + + // Search by middlename. + $middlename = $DB->sql_like('u.middlename', ':' . $searchkey4, false, false); + $conditions[] = $middlename; + + // Search by alternatename. + $alternatename = $DB->sql_like('u.alternatename', ':' . $searchkey5, false, false); + $conditions[] = $alternatename; + + // Search by firstnamephonetic. + $firstnamephonetic = $DB->sql_like('u.firstnamephonetic', ':' . $searchkey6, false, false); + $conditions[] = $firstnamephonetic; + + // Search by lastnamephonetic. + $lastnamephonetic = $DB->sql_like('u.lastnamephonetic', ':' . $searchkey7, false, false); + $conditions[] = $lastnamephonetic; + + $wheres[] = "(". implode(" OR ", $conditions) .") "; + $params[$searchkey1] = "%$keyword%"; + $params[$searchkey2] = "%$keyword%"; + $params[$searchkey3] = "%$keyword%"; + $params[$searchkey4] = "%$keyword%"; + $params[$searchkey5] = "%$keyword%"; + $params[$searchkey6] = "%$keyword%"; + $params[$searchkey7] = "%$keyword%"; + } + + $this->sql->where .= ' AND '.join(' AND ', $wheres); + $this->sql->params += $params; + } + + /** + * If there is a filter to find users who can/cannot accept on their own behalf add it to the SQL query + */ + protected function build_sql_for_capability_filter() { + global $CFG; + $hascapability = $this->acceptancesfilter->get_capability_accept_filter(); + if ($hascapability === null) { + return; + } + + list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context(\context_system::instance(), 'tool/policy:accept'); + + if (empty($neededroles)) { + // There are no roles that allow to accept agreement on one own's behalf. + $this->sql->where .= $hascapability ? ' AND 1=0' : ''; + return; + } + + if (empty($forbiddenroles)) { + // There are no roles that prohibit to accept agreement on one own's behalf. + $this->sql->where .= ' AND ' . $this->sql_has_role($neededroles, $hascapability); + return; + } + + $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0; + if (!empty($neededroles[$defaultuserroleid])) { + // Default role allows to accept agreement. Make sure user has/does not have one of the roles prohibiting it. + $this->sql->where .= ' AND ' . $this->sql_has_role($forbiddenroles, !$hascapability); + return; + } + + if ($hascapability) { + // User has at least one role allowing to accept and no roles prohibiting. + $this->sql->where .= ' AND ' . $this->sql_has_role($neededroles); + $this->sql->where .= ' AND ' . $this->sql_has_role($forbiddenroles, false); + } else { + // Option 1: User has one of the roles prohibiting to accept. + $this->sql->where .= ' AND (' . $this->sql_has_role($forbiddenroles); + // Option 2: User has none of the roles allowing to accept. + $this->sql->where .= ' OR ' . $this->sql_has_role($neededroles, false) . ")"; + } + } + + /** + * Returns SQL snippet for users that have (do not have) one of the given roles in the system context + * + * @param array $roles list of roles indexed by role id + * @param bool $positive true: return users who HAVE roles; false: return users who DO NOT HAVE roles + * @return string + */ + protected function sql_has_role($roles, $positive = true) { + global $CFG; + if (empty($roles)) { + return $positive ? '1=0' : '1=1'; + } + $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0; + if (!empty($roles[$defaultuserroleid])) { + // No need to query, everybody has the default role. + return $positive ? '1=1' : '1=0'; + } + return "u.id " . ($positive ? "" : "NOT") . " IN ( + SELECT userid + FROM {role_assignments} + WHERE contextid = " . SYSCONTEXTID . " AND roleid IN (" . implode(',', array_keys($roles)) . ") + )"; + } + + /** + * If there is a filter by user roles add it to the SQL query. + */ + protected function build_sql_for_roles_filter() { + foreach ($this->acceptancesfilter->get_role_filters() as $roleid) { + $this->sql->where .= ' AND ' . $this->sql_has_role([$roleid => $roleid]); + } + } + + /** + * Hook that can be overridden in child classes to wrap a table in a form + * for example. Called only when there is data to display and not + * downloading. + */ + public function wrap_html_start() { + echo \html_writer::start_tag('form', + ['action' => new \moodle_url('/admin/tool/policy/accept.php'), 'data-action' => 'acceptmodal']); + echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]); + echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'returnurl', + 'value' => $this->get_return_url()]); + foreach (array_keys($this->versionids) as $versionid) { + echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => "versionids[{$versionid}]", + 'value' => $versionid]); + } + } + + /** + * Hook that can be overridden in child classes to wrap a table in a form + * for example. Called only when there is data to display and not + * downloading. + */ + public function wrap_html_finish() { + global $PAGE; + if ($this->canagreeany) { + echo \html_writer::empty_tag('input', ['type' => 'submit', + 'value' => get_string('consentbulk', 'tool_policy'), 'class' => 'btn btn-primary']); + $PAGE->requires->js_call_amd('tool_policy/acceptmodal', 'getInstance', [\context_system::instance()->id]); + } + echo "\n"; + } + + /** + * Render the table. + */ + public function display() { + $this->out(100, true); + } + + /** + * Call appropriate methods on this table class to perform any processing on values before displaying in table. + * Takes raw data from the database and process it into human readable format, perhaps also adding html linking when + * displaying table as html, adding a div wrap, etc. + * + * See for example col_fullname below which will be called for a column whose name is 'fullname'. + * + * @param array|object $row row of data from db used to make one row of the table. + * @return array one row for the table, added using add_data_keyed method. + */ + public function format_row($row) { + \context_helper::preload_from_record($row); + $row->canaccept = false; + $row->user = \user_picture::unalias($row, [], $this->useridfield); + $row->select = null; + if (!$this->is_downloading()) { + if (has_capability('tool/policy:acceptbehalf', \context_system::instance()) || + has_capability('tool/policy:acceptbehalf', \context_user::instance($row->id))) { + $row->canaccept = true; + $row->select = \html_writer::empty_tag('input', + ['type' => 'checkbox', 'name' => 'userids[]', 'value' => $row->id, 'class' => 'usercheckbox', + 'id' => 'selectuser' . $row->id]) . + \html_writer::tag('label', get_string('selectuser', 'tool_policy', $this->username($row->user, false)), + ['for' => 'selectuser' . $row->id, 'class' => 'accesshide']); + $this->canagreeany = true; + } + } + return parent::format_row($row); + } + + /** + * Get the column fullname value. + * + * @param stdClass $row + * @return string + */ + public function col_fullname($row) { + global $OUTPUT; + $userpic = $this->is_downloading() ? '' : $OUTPUT->user_picture($row->user); + return $userpic . $this->username($row->user, true); + } + + /** + * User name with a link to profile + * + * @param stdClass $user + * @param bool $profilelink show link to profile (when we are downloading never show links) + * @return string + */ + protected function username($user, $profilelink = true) { + $canviewfullnames = has_capability('moodle/site:viewfullnames', \context_system::instance()) || + has_capability('moodle/site:viewfullnames', \context_user::instance($user->id)); + $name = fullname($user, $canviewfullnames); + if (!$this->is_downloading() && $profilelink) { + $profileurl = new \moodle_url('/user/profile.php', array('id' => $user->id)); + return \html_writer::link($profileurl, $name); + } + return $name; + } + + /** + * Helper. + */ + protected function get_return_url() { + $pageurl = $this->baseurl; + if ($this->currpage) { + $pageurl = new \moodle_url($pageurl, [$this->request[TABLE_VAR_PAGE] => $this->currpage]); + } + return $pageurl; + } + + /** + * Return agreement status + * + * @param int $versionid either id of an individual version or empty for overall status + * @param stdClass $row + * @return string + */ + protected function status($versionid, $row) { + $onbehalf = false; + $versions = $versionid ? [$versionid => $this->versionids[$versionid]] : $this->versionids; // List of versions. + $accepted = []; // List of versionids that user has accepted. + + foreach ($versions as $v => $name) { + if (!empty($row->{'status' . $v})) { + $accepted[] = $v; + $agreedby = $row->{'usermodified' . $v}; + if ($agreedby && $agreedby != $row->id) { + $onbehalf = true; + } + } + } + + if ($versionid) { + $str = new \lang_string($accepted ? 'yes' : 'no'); + } else { + $str = new \lang_string('acceptancecount', 'tool_policy', (object)[ + 'agreedcount' => count($accepted), + 'policiescount' => count($versions) + ]); + } + + if ($this->is_downloading()) { + return $str->out(); + } else { + $s = $this->output->render(new user_agreement($row->id, $accepted, $this->get_return_url(), + $versions, $onbehalf, $row->canaccept)); + if (!$versionid) { + $s .= '
' . \html_writer::link(new \moodle_url('/admin/tool/policy/user.php', + ['userid' => $row->id, 'returnurl' => $this->get_return_url()]), $str); + } + return $s; + } + } + + /** + * Get the column timemodified value. + * + * @param stdClass $row + * @return string + */ + public function col_timemodified($row) { + if ($row->timemodified) { + if ($this->is_downloading()) { + // Use timestamp format readable for both machines and humans. + return date_format_string($row->timemodified, '%Y-%m-%d %H:%M:%S %Z'); + } else { + // Use localised calendar format. + return userdate($row->timemodified, get_string('strftimedatetime')); + } + } else { + return null; + } + } + + /** + * Get the column note value. + * + * @param stdClass $row + * @return string + */ + public function col_note($row) { + if ($this->is_downloading()) { + return $row->note; + } else { + return format_text($row->note, FORMAT_MOODLE); + } + } + + /** + * Get the column statusall value. + * + * @param stdClass $row + * @return string + */ + public function col_statusall($row) { + return $this->status(0, $row); + } + + /** + * Generate the country column. + * + * @param \stdClass $data + * @return string + */ + public function col_country($data) { + if ($data->country && $this->countries === null) { + $this->countries = get_string_manager()->get_list_of_countries(); + } + if (!empty($this->countries[$data->country])) { + return $this->countries[$data->country]; + } + return ''; + } + + /** + * You can override this method in a child class. See the description of + * build_table which calls this method. + * + * @param string $column + * @param stdClass $row + * @return string + */ + public function other_cols($column, $row) { + if (preg_match('/^status([\d]+)$/', $column, $matches)) { + $versionid = $matches[1]; + return $this->status($versionid, $row); + } + if (preg_match('/^usermodified([\d]+)$/', $column, $matches)) { + if ($row->$column && $row->$column != $row->id) { + $user = (object)['id' => $row->$column]; + username_load_fields_from_object($user, $row, 'mod'); + return $this->username($user, true); + } + return ''; // User agreed by themselves. + } + return null; + } +} \ No newline at end of file diff --git a/admin/tool/policy/classes/event/acceptance_base.php b/admin/tool/policy/classes/event/acceptance_base.php new file mode 100644 index 00000000000..1ecf6c9a40e --- /dev/null +++ b/admin/tool/policy/classes/event/acceptance_base.php @@ -0,0 +1,117 @@ +. + +/** + * Provides {@link tool_policy\event\acceptance_base} class. + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_policy\event; + +use core\event\base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Base class for acceptance_created and acceptance_updated events. + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class acceptance_base extends base { + + /** + * Initialise the event. + */ + protected function init() { + $this->data['objecttable'] = 'tool_policy_acceptances'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Create event from record. + * + * @param stdClass $record + * @return acceptance_created + */ + public static function create_from_record($record) { + $event = static::create([ + 'objectid' => $record->id, + 'relateduserid' => $record->userid, + 'context' => \context_user::instance($record->userid), + 'other' => [ + 'policyversionid' => $record->policyversionid, + 'note' => $record->note, + 'status' => $record->status, + ], + ]); + $event->add_record_snapshot($event->objecttable, $record); + return $event; + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/admin/tool/policy/acceptance.php', array('userid' => $this->relateduserid, + 'versionid' => $this->other['policyversionid'])); + } + + /** + * Get the object ID mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return array('db' => 'tool_policy', 'restore' => \core\event\base::NOT_MAPPED); + } + + /** + * Custom validation. + * + * @throws \coding_exception + */ + protected function validate_data() { + parent::validate_data(); + + if (empty($this->other['policyversionid'])) { + throw new \coding_exception('The \'policyversionid\' value must be set'); + } + + if (!isset($this->other['status'])) { + throw new \coding_exception('The \'status\' value must be set'); + } + + if (empty($this->relateduserid)) { + throw new \coding_exception('The \'relateduserid\' must be set.'); + } + } + + /** + * No mapping required for this event because this event is not backed up. + * + * @return bool + */ + public static function get_other_mapping() { + return false; + } +} \ No newline at end of file diff --git a/admin/tool/policy/classes/event/acceptance_created.php b/admin/tool/policy/classes/event/acceptance_created.php new file mode 100644 index 00000000000..b81b9d185d4 --- /dev/null +++ b/admin/tool/policy/classes/event/acceptance_created.php @@ -0,0 +1,73 @@ +. + +/** + * Provides {@link tool_policy\event\acceptance_created} class. + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_policy\event; + +use core\event\base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Event acceptance_created + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class acceptance_created extends acceptance_base { + + /** + * Initialise the event. + */ + protected function init() { + parent::init(); + $this->data['crud'] = 'c'; + } + + /** + * Returns event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_acceptance_created', 'tool_policy'); + } + + /** + * Get the event description. + * + * @return string + */ + public function get_description() { + if ($this->other['status'] == 1) { + $action = 'added consent to'; + } else if ($this->other['status'] == -1) { + $action = 'revoked consent to'; + } else { + $action = 'created an empty consent record for'; + } + return "The user with id '{$this->userid}' $action the policy with revision {$this->other['policyversionid']} ". + "for the user with id '{$this->relateduserid}'"; + } +} \ No newline at end of file diff --git a/admin/tool/policy/classes/event/acceptance_updated.php b/admin/tool/policy/classes/event/acceptance_updated.php new file mode 100644 index 00000000000..4bec14f3b99 --- /dev/null +++ b/admin/tool/policy/classes/event/acceptance_updated.php @@ -0,0 +1,73 @@ +. + +/** + * Provides {@link tool_policy\event\acceptance_updated} class. + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_policy\event; + +use core\event\base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Event acceptance_updated + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class acceptance_updated extends acceptance_base { + + /** + * Initialise the event. + */ + protected function init() { + parent::init(); + $this->data['crud'] = 'u'; + } + + /** + * Returns event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_acceptance_updated', 'tool_policy'); + } + + /** + * Get the event description. + * + * @return string + */ + public function get_description() { + if ($this->other['status'] == 1) { + $action = 'added consent to'; + } else if ($this->other['status'] == -1) { + $action = 'revoked consent to'; + } else { + $action = 'updated consent to'; + } + return "The user with id '{$this->userid}' $action the policy with revision {$this->other['policyversionid']} ". + "for the user with id '{$this->relateduserid}'"; + } +} \ No newline at end of file diff --git a/admin/tool/policy/classes/form/accept_policy.php b/admin/tool/policy/classes/form/accept_policy.php new file mode 100644 index 00000000000..5b54e2c6472 --- /dev/null +++ b/admin/tool/policy/classes/form/accept_policy.php @@ -0,0 +1,161 @@ +. + +/** + * Provides {@link tool_policy\form\accept_policy} class. + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_policy\form; + +use tool_policy\api; +use tool_policy\policy_version; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/lib/formslib.php'); + +/** + * Represents the form for accepting a policy. + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class accept_policy extends \moodleform { + + /** + * Defines the form fields. + */ + public function definition() { + global $PAGE; + $mform = $this->_form; + + if (empty($this->_customdata['userids']) || !is_array($this->_customdata['userids'])) { + throw new \moodle_exception('missingparam', 'error', '', 'userids'); + } + if (empty($this->_customdata['versionids']) || !is_array($this->_customdata['versionids'])) { + throw new \moodle_exception('missingparam', '', '', 'versionids'); + } + $userids = clean_param_array($this->_customdata['userids'], PARAM_INT); + $versionids = clean_param_array($this->_customdata['versionids'], PARAM_INT); + $usernames = $this->validate_and_get_users($userids); + $versionnames = $this->validate_and_get_versions($versionids); + + foreach ($usernames as $userid => $name) { + $mform->addElement('hidden', 'userids['.$userid.']', $userid); + $mform->setType('userids['.$userid.']', PARAM_INT); + } + + foreach ($versionnames as $versionid => $name) { + $mform->addElement('hidden', 'versionids['.$versionid.']', $versionid); + $mform->setType('versionids['.$versionid.']', PARAM_INT); + } + + $mform->addElement('hidden', 'returnurl'); + $mform->setType('returnurl', PARAM_LOCALURL); + + $mform->addElement('static', 'user', get_string('acceptanceusers', 'tool_policy'), join(', ', $usernames)); + $mform->addElement('static', 'policy', get_string('acceptancepolicies', 'tool_policy'), + join(', ', $versionnames)); + + $mform->addElement('static', 'ack', '', get_string('acceptanceacknowledgement', 'tool_policy')); + + $mform->addElement('textarea', 'note', get_string('acceptancenote', 'tool_policy')); + $mform->setType('note', PARAM_NOTAGS); + + if (!empty($this->_customdata['showbuttons'])) { + $this->add_action_buttons(true, get_string('iagreetothepolicy', 'tool_policy')); + } + + $PAGE->requires->js_call_amd('tool_policy/policyactions', 'init'); + } + + /** + * Validate userids and return usernames + * + * @param array $userids + * @return array (userid=>username) + */ + protected function validate_and_get_users($userids) { + global $DB, $USER; + $usernames = []; + list($sql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + $params['usercontextlevel'] = CONTEXT_USER; + $users = $DB->get_records_sql("SELECT u.id, " . get_all_user_name_fields(true, 'u') . ", " . + \context_helper::get_preload_record_columns_sql('ctx') . + " FROM {user} u JOIN {context} ctx ON ctx.contextlevel=:usercontextlevel AND ctx.instanceid = u.id + WHERE u.id " . $sql, $params); + + $acceptany = has_capability('tool/policy:acceptbehalf', \context_system::instance()); + foreach ($userids as $userid) { + if (!isset($users[$userid])) { + throw new \dml_missing_record_exception('user', 'id=?', [$userid]); + } + $user = $users[$userid]; + if (isguestuser($user)) { + throw new \moodle_exception('noguest'); + } + if ($userid == $USER->id) { + require_capability('tool/policy:accept', \context_system::instance()); + } else if (!$acceptany) { + \context_helper::preload_from_record($user); + require_capability('tool/policy:acceptbehalf', \context_user::instance($userid)); + } + $usernames[$userid] = fullname($user); + } + return $usernames; + } + + /** + * Validate versionids and return their names + * + * @param array $versionids + * @return array (versionid=>name) + */ + protected function validate_and_get_versions($versionids) { + $versionnames = []; + $policies = api::list_policies(); + foreach ($versionids as $versionid) { + $version = api::get_policy_version($versionid, $policies); + if ($version->audience == policy_version::AUDIENCE_GUESTS) { + throw new \moodle_exception('errorpolicyversionnotfound', 'tool_policy'); + } + $url = new \moodle_url('/admin/tool/policy/view.php', ['versionid' => $version->id]); + $policyname = $version->name; + if ($version->status != policy_version::STATUS_ACTIVE) { + $policyname .= ' ' . $version->revision; + } + $versionnames[$version->id] = \html_writer::link($url, $policyname, + ['data-action' => 'view', 'data-versionid' => $version->id]); + } + return $versionnames; + } + + /** + * Process form submission + */ + public function process() { + if ($data = $this->get_data()) { + foreach ($data->userids as $userid) { + \tool_policy\api::accept_policies($data->versionids, $userid, $data->note); + } + } + } +} \ No newline at end of file diff --git a/admin/tool/policy/classes/output/acceptances.php b/admin/tool/policy/classes/output/acceptances.php new file mode 100644 index 00000000000..ace885d3cfe --- /dev/null +++ b/admin/tool/policy/classes/output/acceptances.php @@ -0,0 +1,132 @@ +. + +/** + * Provides {@link tool_policy\output\acceptances} class. + * + * @package tool_policy + * @category output + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_policy\output; + +use tool_policy\api; + +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; +use renderable; +use renderer_base; +use single_button; +use templatable; +use tool_policy\policy_version; + +/** + * List of users and their acceptances + * + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class acceptances implements renderable, templatable { + + /** @var id */ + protected $userid; + + /** @var moodle_url */ + protected $returnurl; + + /** + * Contructor. + * + * @param int $userid + */ + public function __construct($userid, $returnurl = null) { + $this->userid = $userid; + $this->returnurl = $returnurl ? (new moodle_url($returnurl))->out(false) : null; + } + + /** + * Export the page data for the mustache template. + * + * @param renderer_base $output renderer to be used to render the page elements. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = (object)[]; + $data->hasonbehalfagreements = false; + $data->pluginbaseurl = (new moodle_url('/admin/tool/policy'))->out(false); + $data->returnurl = $this->returnurl; + + // Get the list of policies and versions that current user is able to see + // and the respective acceptance records for the selected user. + $policies = api::get_policies_with_acceptances($this->userid); + + $canviewfullnames = has_capability('moodle/site:viewfullnames', \context_system::instance()); + foreach ($policies as $policy) { + + foreach ($policy->versions as $version) { + unset($version->summary); + unset($version->content); + $version->iscurrent = ($version->status == policy_version::STATUS_ACTIVE); + $version->name = $version->name; + $version->revision = $version->revision; + $returnurl = new moodle_url('/admin/tool/policy/user.php', ['userid' => $this->userid]); + $version->viewurl = (new moodle_url('/admin/tool/policy/view.php', [ + 'policyid' => $policy->id, + 'versionid' => $version->id, + 'returnurl' => $returnurl->out(false), + ]))->out(false); + + if (!empty($version->acceptance->status)) { + $acceptance = $version->acceptance; + $version->timeaccepted = userdate($acceptance->timemodified, get_string('strftimedatetime')); + $onbehalf = $acceptance->usermodified && $acceptance->usermodified != $this->userid; + $version->agreement = new user_agreement($this->userid, [$version->id], $returnurl, + [$version->id => $version->name], $onbehalf); + if ($onbehalf) { + $usermodified = (object)['id' => $acceptance->usermodified]; + username_load_fields_from_object($usermodified, $acceptance, 'mod'); + $profileurl = new \moodle_url('/user/profile.php', array('id' => $usermodified->id)); + $version->acceptedby = \html_writer::link($profileurl, fullname($usermodified, $canviewfullnames || + has_capability('moodle/site:viewfullnames', \context_user::instance($acceptance->usermodified)))); + $data->hasonbehalfagreements = true; + } + $version->note = format_text($acceptance->note); + } else if ($version->iscurrent) { + $version->agreement = new user_agreement($this->userid, [], $returnurl, [$version->id => $version->name]); + } + if (isset($version->agreement)) { + $version->agreement = $version->agreement->export_for_template($output); + } + } + + if ($policy->versions[0]->status != policy_version::STATUS_ACTIVE) { + // Add an empty "currentversion" on top. + $policy->versions = [0 => (object)[]] + $policy->versions; + } + + $policy->versioncount = count($policy->versions); + $policy->versions = array_values($policy->versions); + $policy->versions[0]->isfirst = 1; + $policy->versions[0]->hasarchived = (count($policy->versions) > 1); + } + + $data->policies = array_values($policies); + return $data; + } +} diff --git a/admin/tool/policy/classes/output/acceptances_filter.php b/admin/tool/policy/classes/output/acceptances_filter.php new file mode 100644 index 00000000000..347a509dc6a --- /dev/null +++ b/admin/tool/policy/classes/output/acceptances_filter.php @@ -0,0 +1,465 @@ +. + +/** + * Provides {@link tool_policy\output\acceptances_filter} class. + * + * @package tool_policy + * @category output + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_policy\output; + +use tool_policy\api; +use tool_policy\policy_version; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Implements the widget allowing to filter the acceptance records. + * + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class acceptances_filter implements \templatable, \renderable { + + /** @var array $filtersapplied The list of selected filter options. */ + protected $filtersapplied; + + /** @var string $searchstring */ + protected $searchstrings; + + /** @var array list of available versions */ + protected $versions = null; + + /** @var array list of available roles for the filter */ + protected $roles; + + /** @var array cached list of all available policies, to retrieve use {@link self::get_avaliable_policies()} */ + protected $policies; + + /** @var int */ + const FILTER_SEARCH_STRING = 0; + + /** @var int */ + const FILTER_POLICYID = 1; + + /** @var int */ + const FILTER_VERSIONID = 2; + + /** @var int */ + const FILTER_CAPABILITY_ACCEPT = 3; + + /** @var int */ + const FILTER_STATUS = 4; + + /** @var int */ + const FILTER_ROLE = 5; + + /** + * Constructor. + * + * @param array $policyid Specified policy id + * @param array $versionid Specified version id + * @param array $filtersapplied The list of selected filter option values. + */ + public function __construct($policyid, $versionid, $filtersapplied) { + $this->filtersapplied = []; + $this->roles = get_assignable_roles(\context_system::instance()); + if ($policyid) { + $this->add_filter(self::FILTER_POLICYID, $policyid); + } + if ($versionid) { + $this->add_filter(self::FILTER_VERSIONID, $versionid); + } + foreach ($filtersapplied as $filter) { + if (preg_match('/^([1-9]\d*):(\d+)$/', $filter, $parts)) { + // This is a pre-set filter (policy, version, status, etc.). + $allowmultiple = false; + switch ((int)$parts[1]) { + case self::FILTER_POLICYID: + case self::FILTER_VERSIONID: + $value = (int)$parts[2]; + break; + case self::FILTER_CAPABILITY_ACCEPT: + case self::FILTER_STATUS: + $value = (int)(bool)$parts[2]; + break; + case self::FILTER_ROLE: + $value = (int)$parts[2]; + if (!array_key_exists($value, $this->roles)) { + continue 2; + } + $allowmultiple = true; + break; + default: + // Unrecognised filter. + continue 2; + } + + $this->add_filter((int)$parts[1], $value, $allowmultiple); + } else if (trim($filter) !== '') { + // This is a search string. + $this->add_filter(self::FILTER_SEARCH_STRING, trim($filter), true); + } + } + } + + /** + * Adds an applied filter + * + * @param mixed $key + * @param mixed $value + * @param bool $allowmultiple + */ + protected function add_filter($key, $value, $allowmultiple = false) { + if ($allowmultiple || empty($this->get_filter_values($key))) { + $this->filtersapplied[] = [$key, $value]; + } + } + + /** + * Is there a filter by policy + * + * @return null|int null if there is no filter, otherwise the policy id + */ + public function get_policy_id_filter() { + return $this->get_filter_value(self::FILTER_POLICYID); + } + + /** + * Is there a filter by version + * + * @return null|int null if there is no filter, otherwise the version id + */ + public function get_version_id_filter() { + return $this->get_filter_value(self::FILTER_VERSIONID); + } + + /** + * Are there filters by search strings + * + * @return string[] array of string filters + */ + public function get_search_strings() { + return $this->get_filter_values(self::FILTER_SEARCH_STRING); + } + + /** + * Is there a filter by status (agreed/not agreed). + * + * @return null|0|1 null if there is no filter, 0/1 if there is a filter by status + */ + public function get_status_filter() { + return $this->get_filter_value(self::FILTER_STATUS); + } + + /** + * Are there filters by role + * + * @return array list of role ids + */ + public function get_role_filters() { + return $this->get_filter_values(self::FILTER_ROLE); + } + + /** + * Is there a filter by capability (can accept/cannot accept). + * + * @return null|0|1 null if there is no filter, 0/1 if there is a filter by capability + */ + public function get_capability_accept_filter() { + return $this->get_filter_value(self::FILTER_CAPABILITY_ACCEPT); + } + + /** + * Get all values of the applied filter + * + * @param string $filtername + * @return array + */ + protected function get_filter_values($filtername) { + $values = []; + foreach ($this->filtersapplied as $filter) { + if ($filter[0] == $filtername) { + $values[] = $filter[1]; + } + } + return $values; + } + + /** + * Get one value of the applied filter + * + * @param string $filtername + * @param string $default + * @return mixed + */ + protected function get_filter_value($filtername, $default = null) { + if ($values = $this->get_filter_values($filtername)) { + $value = reset($values); + return $value; + } + return $default; + } + + /** + * Returns all policies that have versions with possible acceptances (excl. drafts and guest-only versions) + * + * @return array|null + */ + public function get_avaliable_policies() { + if ($this->policies === null) { + $this->policies = []; + foreach (\tool_policy\api::list_policies() as $policy) { + // Make a list of all versions that are not draft and are not guest-only. + $policy->versions = []; + if ($policy->currentversion && $policy->currentversion->audience != policy_version::AUDIENCE_GUESTS) { + $policy->versions[$policy->currentversion->id] = $policy->currentversion; + } else { + $policy->currentversion = null; + } + foreach ($policy->archivedversions as $version) { + if ($version->audience != policy_version::AUDIENCE_GUESTS) { + $policy->versions[$version->id] = $version; + } + } + if ($policy->versions) { + $this->policies[$policy->id] = $policy; + } + } + } + return $this->policies; + } + + /** + * List of policies that match current filters + * + * @return array of versions to display indexed by versionid + */ + public function get_versions() { + if ($this->versions === null) { + $policyid = $this->get_policy_id_filter(); + $versionid = $this->get_version_id_filter(); + $this->versions = []; + foreach ($this->get_avaliable_policies() as $policy) { + if ($policyid && $policy->id != $policyid) { + continue; + } + if ($versionid) { + if (array_key_exists($versionid, $policy->versions)) { + $this->versions[$versionid] = $policy->versions[$versionid]; + break; // No need to keep searching. + } + } else if ($policy->currentversion) { + $this->versions[$policy->currentversion->id] = $policy->currentversion; + } + } + } + return $this->versions; + } + + /** + * Validates if policyid and versionid are valid (if specified) + */ + public function validate_ids() { + $policyid = $this->get_policy_id_filter(); + $versionid = $this->get_version_id_filter(); + if ($policyid || $versionid) { + $found = array_filter($this->get_avaliable_policies(), function($policy) use ($policyid, $versionid) { + return (!$policyid || $policy->id == $policyid) && + (!$versionid || array_key_exists($versionid, $policy->versions)); + }); + if (!$found) { + // Throw exception that policy/version is not found. + throw new \moodle_exception('errorpolicyversionnotfound', 'tool_policy'); + } + } + } + + /** + * If policyid or versionid is specified return one single policy that needs to be shown + * + * If neither policyid nor versionid is specified this method returns null. + * + * When versionid is specified this method will always return an object (this is validated in {@link self::validate_ids()} + * When only policyid is specified this method either returns the current version of the policy or null if there is + * no current version (for example, it is an old policy). + * + * @return mixed|null + */ + public function get_single_version() { + if ($this->get_version_id_filter() || $this->get_policy_id_filter()) { + $versions = $this->get_versions(); + return reset($versions); + } + return null; + } + + /** + * Returns URL of the acceptances page with all current filters applied + * + * @return \moodle_url + */ + public function get_url() { + $urlparams = []; + if ($policyid = $this->get_policy_id_filter()) { + $urlparams['policyid'] = $policyid; + } + if ($versionid = $this->get_version_id_filter()) { + $urlparams['versionid'] = $versionid; + } + $i = 0; + foreach ($this->filtersapplied as $filter) { + if ($filter[0] != self::FILTER_POLICYID && $filter[0] != self::FILTER_VERSIONID) { + if ($filter[0] == self::FILTER_SEARCH_STRING) { + $urlparams['unified-filters['.($i++).']'] = $filter[1]; + } else { + $urlparams['unified-filters['.($i++).']'] = join(':', $filter); + } + } + } + return new \moodle_url('/admin/tool/policy/acceptances.php', $urlparams); + } + + /** + * Creates an option name for the smart select for the version + * + * @param \stdClass $version + * @return string + */ + protected function get_version_option_for_filter($version) { + if ($version->status == policy_version::STATUS_ACTIVE) { + $a = (object)[ + 'name' => format_string($version->revision), + 'status' => get_string('status'.policy_version::STATUS_ACTIVE, 'tool_policy'), + ]; + return get_string('filterrevisionstatus', 'tool_policy', $a); + } else { + return get_string('filterrevision', 'tool_policy', $version->revision); + } + } + + /** + * Build list of filters available for this page + * + * @return array [$availablefilters, $selectedoptions] + */ + protected function build_available_filters() { + $selectedoptions = []; + $availablefilters = []; + + $versionid = $this->get_version_id_filter(); + $policyid = $versionid ? $this->get_single_version()->policyid : $this->get_policy_id_filter(); + + // Policies. + $policies = $this->get_avaliable_policies(); + if ($policyid) { + // If policy is selected, display only the current policy in the selector. + $selectedoptions[] = $key = self::FILTER_POLICYID . ':' . $policyid; + $version = $versionid ? $policies[$policyid]->versions[$versionid] : reset($policies[$policyid]->versions); + $availablefilters[$key] = get_string('filterpolicy', 'tool_policy', $version->name); + } else { + // If no policy/version is selected display the list of all policies. + foreach ($policies as $policy) { + $firstversion = reset($policy->versions); + $key = self::FILTER_POLICYID . ':' . $policy->id; + $availablefilters[$key] = get_string('filterpolicy', 'tool_policy', $firstversion->name); + } + } + + // Versions. + if ($versionid) { + $singleversion = $this->get_single_version(); + $selectedoptions[] = $key = self::FILTER_VERSIONID . ':' . $singleversion->id; + $availablefilters[$key] = $this->get_version_option_for_filter($singleversion); + } else if ($policyid) { + foreach ($policies[$policyid]->versions as $version) { + $key = self::FILTER_VERSIONID . ':' . $version->id; + $availablefilters[$key] = $this->get_version_option_for_filter($version); + } + } + + // Permissions. + $permissions = [ + self::FILTER_CAPABILITY_ACCEPT . ':1' => get_string('filtercapabilityyes', 'tool_policy'), + self::FILTER_CAPABILITY_ACCEPT . ':0' => get_string('filtercapabilityno', 'tool_policy'), + ]; + if (($currentpermission = $this->get_capability_accept_filter()) !== null) { + $selectedoptions[] = $key = self::FILTER_CAPABILITY_ACCEPT . ':' . $currentpermission; + $permissions = array_intersect_key($permissions, [$key => true]); + } + $availablefilters += $permissions; + + // Status. + $statuses = [ + self::FILTER_STATUS.':1' => get_string('filterstatusyes', 'tool_policy'), + self::FILTER_STATUS.':0' => get_string('filterstatusno', 'tool_policy'), + ]; + if (($currentstatus = $this->get_status_filter()) !== null) { + $selectedoptions[] = $key = self::FILTER_STATUS . ':' . $currentstatus; + $statuses = array_intersect_key($statuses, [$key => true]); + } + $availablefilters += $statuses; + + // Roles. + $currentroles = $this->get_role_filters(); + foreach ($this->roles as $roleid => $rolename) { + $key = self::FILTER_ROLE . ':' . $roleid; + $availablefilters[$key] = get_string('filterrole', 'tool_policy', $rolename); + if (in_array($roleid, $currentroles)) { + $selectedoptions[] = $key; + } + } + + // Search string. + foreach ($this->get_search_strings() as $str) { + $selectedoptions[] = $str; + $availablefilters[$str] = $str; + } + + return [$availablefilters, $selectedoptions]; + } + + /** + * Function to export the renderer data in a format that is suitable for a mustache template. + * + * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return \stdClass|array + */ + public function export_for_template(\renderer_base $output) { + $data = new \stdClass(); + $data->action = (new \moodle_url('/admin/tool/policy/acceptances.php'))->out(false); + + $data->filteroptions = []; + $originalfilteroptions = []; + list($avilablefilters, $selectedoptions) = $this->build_available_filters(); + foreach ($avilablefilters as $value => $label) { + $selected = in_array($value, $selectedoptions); + $filteroption = (object)[ + 'value' => $value, + 'label' => $label + ]; + $originalfilteroptions[] = $filteroption; + $filteroption->selected = $selected; + $data->filteroptions[] = $filteroption; + } + $data->originaloptionsjson = json_encode($originalfilteroptions); + return $data; + } +} \ No newline at end of file diff --git a/admin/tool/policy/classes/output/user_agreement.php b/admin/tool/policy/classes/output/user_agreement.php new file mode 100644 index 00000000000..8dd9030fbf3 --- /dev/null +++ b/admin/tool/policy/classes/output/user_agreement.php @@ -0,0 +1,114 @@ +. + +/** + * Provides {@link tool_policy\output\user_agreement} class. + * + * @package tool_policy + * @category output + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_policy\output; + +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; +use renderable; +use renderer_base; +use single_button; +use templatable; + +/** + * List of users and their acceptances + * + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_agreement implements \templatable, \renderable { + + /** @var int */ + protected $userid; + + /** @var bool */ + protected $onbehalf; + + /** @var moodle_url */ + protected $pageurl; + + /** @var array */ + protected $versions; + + /** @var array */ + protected $accepted; + + /** @var bool */ + protected $canaccept; + + /** + * user_agreement constructor + * + * @param int $userid + * @param array $accepted list of ids of accepted versions + * @param moodle_url $pageurl + * @param array $versions list of versions (id=>name) + * @param bool $onbehalf whether at least one version was accepted by somebody else on behalf of the user + * @param bool $canaccept does the current user have permission to accept the policy on behalf of user $userid + */ + public function __construct($userid, $accepted, moodle_url $pageurl, $versions, $onbehalf = false, $canaccept = null) { + $this->userid = $userid; + $this->onbehalf = $onbehalf; + $this->pageurl = $pageurl; + $this->versions = $versions; + $this->accepted = $accepted; + $this->canaccept = $canaccept; + if (count($this->accepted) < count($this->versions) && $canaccept === null) { + $this->canaccept = (has_capability('tool/policy:acceptbehalf', \context_system::instance()) || + has_capability('tool/policy:acceptbehalf', \context_user::instance($this->userid))); + } + } + + /** + * Export data to be rendered. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(\renderer_base $output) { + $data = [ + 'status' => count($this->accepted) == count($this->versions), + 'onbehalf' => $this->onbehalf, + 'canaccept' => $this->canaccept, + ]; + if (!$data['status'] && $this->canaccept) { + $linkparams = ['userids[0]' => $this->userid]; + foreach (array_diff(array_keys($this->versions), $this->accepted) as $versionid) { + $linkparams["versionids[{$versionid}]"] = $versionid; + } + $linkparams['returnurl'] = $this->pageurl->out_as_local_url(false); + $link = new \moodle_url('/admin/tool/policy/accept.php', $linkparams); + $data['acceptlink'] = $link->out(false); + $data['acceptmodaldata'] = $link->get_query_string(false); // TODO not needed? + } + $data['singleversion'] = count($this->versions) == 1; + if ($data['singleversion']) { + $firstversion = reset($this->versions); + $data['versionname'] = $firstversion; + } + return $data; + } +} \ No newline at end of file diff --git a/admin/tool/policy/pix/agreedno.png b/admin/tool/policy/pix/agreedno.png new file mode 100644 index 0000000000000000000000000000000000000000..21c5b5d360f54e730fe884a3fd7adef0b1b509a0 GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFP2=EDU1=9Z=Y=}pBA8%X+>ftX5 z@(X4VP%v-^NGNFNpTBc$iTpM;rb8FM;BPvD2PqC z(IB|X(YlFIrYC_}xu8Up*+b`nlC6N<`@k(zMAP#7!n>-sEw0gSPT!@rpZB!N&V?3g z@41ehl$+wo{p=+3oL$eD|6Vz~#^7znxrpmL@jDLm-D6bau1)yL{(i5Pj^`7R&p_K5 NJYD@<);T3K0RRw?W4HhS literal 0 HcmV?d00001 diff --git a/admin/tool/policy/pix/agreedno.svg b/admin/tool/policy/pix/agreedno.svg new file mode 100644 index 00000000000..0185d868b95 --- /dev/null +++ b/admin/tool/policy/pix/agreedno.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/admin/tool/policy/pix/agreedyes.png b/admin/tool/policy/pix/agreedyes.png new file mode 100644 index 0000000000000000000000000000000000000000..6d1f79a535eb23a24e7a0d296b84cfbd28cdabe2 GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPHF4e$wZ1=2Im7!!#^^sIgYHSm=L z`2{lwC>S^-6!gzuzyJLG_a`r3*bkJ6^K@|xsbEZQU@Fx(TO)aDgKh)Y9nl7Zl=)5> z$72m29qxs(we2u>nD=Ej!?J1x)!+AQpH{I4J^KAF_U>+$34N9C@9kxsedyb} ryWAaVv&;Jz9$U-2s&xkYCP9WzO|oBhxg;M3+Qi`L>gTe~DWM4fqtjjB literal 0 HcmV?d00001 diff --git a/admin/tool/policy/pix/agreedyes.svg b/admin/tool/policy/pix/agreedyes.svg new file mode 100644 index 00000000000..714d4c73c79 --- /dev/null +++ b/admin/tool/policy/pix/agreedyes.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/admin/tool/policy/pix/agreedyesonbehalf.png b/admin/tool/policy/pix/agreedyesonbehalf.png new file mode 100644 index 0000000000000000000000000000000000000000..dff2dc25c7f065e215fec5a2694fbf350a1ee485 GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPHF4e$wZ1=9a_xDbiHoIkM-sDZB} z$S;^dK*7Kvp`d^M`u*qczdw2T!hWDkoTrOpNCjhZ15>HS*&4}H8+04E?ua%Rq|A59 zI9AK9z|&XG=x{HLt!;<7!@MuM8J1NmsQ$iZ`?QKZ=+W_gw) r-R15`n_b?&@Yq`BRjo7FHwiL)YLfl3%O&|R&?W{?S3j3^P6 +]> \ No newline at end of file diff --git a/admin/tool/policy/templates/acceptances.mustache b/admin/tool/policy/templates/acceptances.mustache new file mode 100644 index 00000000000..63a7335ae80 --- /dev/null +++ b/admin/tool/policy/templates/acceptances.mustache @@ -0,0 +1,149 @@ +{{! + 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 . +}} +{{! + @template tool_policy/acceptances + + Template for the user acceptances page. + + Classes required for JS: + - + + Data attributes required for JS: + - + + Context variables required for this template: + * policies + + Example context (json): + { + "hasonbehalfagreements": true, + "policies": [ + { + "versions": [ + { + "isfirst": true, + "policyid": 1, + "viewurl": "/", + "name": "Terms & conditions", + "revision": "2.0", + "hasarchived": true, + "timeaccepted": "1 Mar 2018", + "agreement": { + "onbehalf": false, + "status": false, + "canaccept": true, + "acceptlink": "#" + } + }, + { + "isfirst": false, + "policyid": 1, + "viewurl": "/", + "name": "Terms & conditions", + "revision": "1.0-beta", + "acceptedby": "Mary Smith", + "note": "Based on parent's agreement via email", + "hasarchived": false, + "timeaccepted": "15 Feb 2018", + "agreement": { + "onbehalf": true, + "status": true, + "canaccept": false + } + } + ] + } + ] + } +}} + + + + + + + + {{#hasonbehalfagreements}} + + + {{/hasonbehalfagreements}} + + + + + {{#policies}} + {{#versions}} + + + + + + {{#hasonbehalfagreements}} + + + {{/hasonbehalfagreements}} + + + {{/versions}} + {{/policies}} + +
{{#str}} policydocname, tool_policy {{/str}}{{#str}} policydocrevision, tool_policy {{/str}}{{#str}} agreed, tool_policy {{/str}}{{#str}} agreedon, tool_policy {{/str}}{{#str}} agreedby, tool_policy {{/str}}{{#str}} acceptancenote, tool_policy {{/str}}
+ {{^isfirst}} +
+ {{#pix}} level, tool_policy {{/pix}} +
+ {{/isfirst}} + +
+ {{{revision}}} + {{#iscurrent}}{{#str}} status1, tool_policy {{/str}}{{/iscurrent}} + + {{#agreement}} + {{>tool_policy/user_agreement}} + {{/agreement}} + + {{timeaccepted}}{{{acceptedby}}}{{{note}}} + {{#hasarchived}} + +
{{#pix}}t/more, moodle, {{#str}}detailedmore, moodle{{/str}}{{/pix}}
+
{{/hasarchived}} +
+{{#returnurl}} + +{{/returnurl}} +{{#js}} + require(['jquery'], function($) { + $('body').on('click', '.showarchived', function(e) { + e.preventDefault(); + var target = $(this).attr('data-target'), + status = $(this).attr('data-status'); + if (status === 'hidden') { + $(target).show(); + $(this).attr('data-status', 'shown'); + $(this).find('.toggleoff').show(); + $(this).find('.toggleon').hide(); + } else { + $(target).hide(); + $(this).attr('data-status', 'hidden'); + $(this).find('.toggleon').show(); + $(this).find('.toggleoff').hide(); + } + }); + }); +{{/js}} diff --git a/admin/tool/policy/templates/acceptances_filter.mustache b/admin/tool/policy/templates/acceptances_filter.mustache new file mode 100644 index 00000000000..27ee8124856 --- /dev/null +++ b/admin/tool/policy/templates/acceptances_filter.mustache @@ -0,0 +1,67 @@ +{{! + 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 . +}} +{{! + @template tool_policy/acceptances_filter + + Template for the unified filter element. + + Context variables required for this template: + * action string - The action URL for the form. + * filteroptions - Array of filter options. + * value string - The option value. + * label string - The option label. + * selected boolean - Whether the option is selected + + Example context (json): + { + "action": "/admin/tool/policy/acceptances.php", + "filteroptions": [ + { + "value": "1", + "label": "Option 1" + }, + { + "value": "2", + "label": "Option 2", + "selected": true + }, + { + "value": "3", + "label": "Option 3", + "selected": true + }, + { + "value": "4", + "label": "Option 4" + } + ] + } +}} + +{{#js}} + require(['tool_policy/acceptances_filter'], function(Filter) { + Filter.init(); + }); +{{/js}} diff --git a/admin/tool/policy/templates/user_agreement.mustache b/admin/tool/policy/templates/user_agreement.mustache new file mode 100644 index 00000000000..684a7cb0d58 --- /dev/null +++ b/admin/tool/policy/templates/user_agreement.mustache @@ -0,0 +1,65 @@ +{{! + 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 . +}} +{{! + @template tool_policy/user_agreement + + Template for the user agreement icon. + + Classes required for JS: + - + + Data attributes required for JS: + - + + Context variables required for this template: + * status + * onbehalf + * canaccept + * acceptlink + + Example context (json): + { + "status": false, + "onbehalf": false, + "canaccept": true, + "acceptlink": "/", + "singleversion": false, + "versionname": "" + } +}} +{{#status}} + + {{#onbehalf}} + {{#pix}}agreedyesonbehalf, tool_policy, {{#str}} agreedyesonbehalf, tool_policy {{/str}}{{/pix}} + {{/onbehalf}} + {{^onbehalf}} + {{#pix}}agreedyes, tool_policy, {{#str}} agreedyes, tool_policy {{/str}}{{/pix}} + {{/onbehalf}} +{{/status}} +{{^status}} + {{#canaccept}} + {{#singleversion}} + {{#pix}}agreedno, tool_policy, {{#str}} agreednowithlink, tool_policy, {{{versionname}}} {{/str}}{{/pix}} + {{/singleversion}} + {{^singleversion}} + {{#pix}}agreedno, tool_policy, {{#str}} agreednowithlinkall, tool_policy {{/str}}{{/pix}} + {{/singleversion}} + {{/canaccept}} + {{^canaccept}} + {{#pix}}agreedno, tool_policy, {{#str}} agreedno, tool_policy {{/str}}{{/pix}} + {{/canaccept}} +{{/status}} diff --git a/admin/tool/policy/tests/behat/acceptances.feature b/admin/tool/policy/tests/behat/acceptances.feature new file mode 100644 index 00000000000..cfca9378a3d --- /dev/null +++ b/admin/tool/policy/tests/behat/acceptances.feature @@ -0,0 +1,230 @@ +@tool @tool_policy +Feature: Viewing acceptances reports and accepting on behalf of other users + In order to manage user acceptances + As a manager + I need to be able to view acceptances and accept on behalf of other users + + Background: + Given the following config values are set as admin: + | sitepolicyhandler | tool_policy | + And the following policies exist: + | Name | Revision | Content | Summary | Status | + | This site policy | | full text2 | short text2 | active | + | This privacy policy | | full text3 | short text3 | draft | + And the following "users" exist: + | username | firstname | lastname | email | + | user1 | User | One | one@example.com | + | user2 | User | Two | two@example.com | + | manager | Max | Manager | man@example.com | + And the following "role assigns" exist: + | user | role | contextlevel | reference | + | manager | manager | System | | + And the following "courses" exist: + | fullname | shortname | + | Course1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | user1 | C1 | student | + | user2 | C1 | student | + + Scenario: View acceptances made by users on their own, single policy + When I log in as "user1" + Then I should see "This site policy" + And I should not see "Course overview" + And I press "Next" + And I set the field "I agree to the This site policy" to "1" + And I press "Next" + And I should see "Course overview" + And I log out + And I log in as "manager" + And I press "Next" + And I set the field "I agree to the This site policy" to "1" + And I press "Next" + And I navigate to "Privacy and policies > User agreements" in site administration + And "Agreed" "icon" should exist in the "User One" "table_row" + And "Agreed" "icon" should exist in the "Max Manager" "table_row" + And "Not agreed" "icon" should exist in the "User Two" "table_row" + + Scenario: Agree on behalf of another user as a manager, single policy, javascript off + Given I log in as "admin" + And I set the following system permissions of "Manager" role: + | capability | permission | + | tool/policy:acceptbehalf | Allow | + And I log out + When I log in as "manager" + And I press "Next" + And I set the field "I agree to the This site policy" to "1" + And I press "Next" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row" + And I click on "Not agreed" "link" in the "User One" "table_row" + Then I should see "Consent details" + And I should see "User One" + And I should see "This site policy" + And I should see "I acknowledge that consents to these policies have been acquired" + And I set the field "Remarks" to "Consent received from a parent" + And I press "I agree to the policy" + And "Agreed on behalf of" "icon" should exist in the "User One" "table_row" + And "Max Manager" "link" should exist in the "User One" "table_row" + And "Consent received from a parent" "text" should exist in the "User One" "table_row" + And "Not agreed" "icon" should exist in the "User Two" "table_row" + + @javascript + Scenario: Agree on behalf of another user as a manager, single policy, javascript on + Given I log in as "admin" + And I set the following system permissions of "Manager" role: + | capability | permission | + | tool/policy:acceptbehalf | Allow | + And I log out + When I log in as "manager" + And I press "Next" + And I set the field "I agree to the This site policy" to "1" + And I press "Next" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row" + And I click on "Not agreed" "link" in the "User One" "table_row" + Then I should see "Consent details" + And I should see "User One" + And I should see "This site policy" + And I should see "I acknowledge that consents to these policies have been acquired" + And I set the field "Remarks" to "Consent received from a parent" + And I press "I agree to the policy" + And "Agreed on behalf of" "icon" should exist in the "User One" "table_row" + And "Max Manager" "link" should exist in the "User One" "table_row" + And "Consent received from a parent" "text" should exist in the "User One" "table_row" + And "Not agreed" "icon" should exist in the "User Two" "table_row" + + Scenario: View acceptances made by users on their own, multiple policies + Given I log in as "admin" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I open the action menu in "This privacy policy" "table_row" + And I click on "Set status to \"Active\"" "link" in the "This privacy policy" "table_row" + And I press "Continue" + And I log out + When I log in as "user1" + Then I should see "This site policy" + And I press "Next" + And I should see "This privacy policy" + And I press "Next" + And I set the field "I agree to the This site policy" to "1" + And I set the field "I agree to the This privacy policy" to "1" + And I press "Next" + And I should see "Course overview" + And I log out + And I log in as "manager" + And I press "Next" + And I press "Next" + And I set the field "I agree to the This site policy" to "1" + And I set the field "I agree to the This privacy policy" to "1" + And I press "Next" + And I navigate to "Privacy and policies > User agreements" in site administration + And "Agreed" "icon" should exist in the "User One" "table_row" + And "Not agreed" "icon" should not exist in the "User One" "table_row" + And "Agreed" "icon" should exist in the "Max Manager" "table_row" + And "Not agreed" "icon" should exist in the "User Two" "table_row" + And "Agreed" "icon" should not exist in the "User Two" "table_row" + And I click on "2 of 2" "link" in the "User One" "table_row" + And "Agreed" "icon" should exist in the "This site policy" "table_row" + And "Agreed" "icon" should exist in the "This privacy policy" "table_row" + And I am on site homepage + And I navigate to "Privacy and policies > User agreements" in site administration + And I click on "0 of 2" "link" in the "User Two" "table_row" + And "Not agreed" "icon" should exist in the "This site policy" "table_row" + And "Not agreed" "icon" should exist in the "This privacy policy" "table_row" + + Scenario: Agree on behalf of another user as a manager, multiple policies, javascript off + Given I log in as "admin" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I open the action menu in "This privacy policy" "table_row" + And I click on "Set status to \"Active\"" "link" in the "This privacy policy" "table_row" + And I press "Continue" + And I set the following system permissions of "Manager" role: + | capability | permission | + | tool/policy:acceptbehalf | Allow | + And I log out + When I log in as "manager" + And I press "Next" + And I press "Next" + And I set the field "I agree to the This site policy" to "1" + And I set the field "I agree to the This privacy policy" to "1" + And I press "Next" + And I navigate to "Privacy and policies > User agreements" in site administration + And I click on "Not agreed, click to agree to \"This site policy\"" "link" in the "User One" "table_row" + Then I should see "Consent details" + And I should see "User One" + And I should see "This site policy" + And I should see "I acknowledge that consents to these policies have been acquired" + And I set the field "Remarks" to "Consent received from a parent" + And I press "I agree to the policy" + And "Agreed on behalf of" "icon" should exist in the "User One" "table_row" + And "Not agreed, click to agree to \"This privacy policy\"" "icon" should exist in the "User One" "table_row" + And I click on "1 of 2" "link" in the "User One" "table_row" + And "Agreed on behalf of" "icon" should exist in the "This site policy" "table_row" + And "Max Manager" "link" should exist in the "This site policy" "table_row" + And "Consent received from a parent" "text" should exist in the "This site policy" "table_row" + And "Not agreed" "icon" should exist in the "This privacy policy" "table_row" + + @javascript + Scenario: Agree on behalf of another user as a manager, multiple policies, javascript on + Given I log in as "admin" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I open the action menu in "This privacy policy" "table_row" + And I click on "Set status to \"Active\"" "link" in the "This privacy policy" "table_row" + And I press "Activate" + And I set the following system permissions of "Manager" role: + | capability | permission | + | tool/policy:acceptbehalf | Allow | + And I log out + When I log in as "manager" + And I press "Next" + And I press "Next" + And I set the field "I agree to the This site policy" to "1" + And I set the field "I agree to the This privacy policy" to "1" + And I press "Next" + And I navigate to "Privacy and policies > User agreements" in site administration + And I click on "Not agreed, click to agree to \"This site policy\"" "link" in the "User One" "table_row" + Then I should see "Consent details" + And I should see "User One" + And I should see "This site policy" + And I should see "I acknowledge that consents to these policies have been acquired" + And I set the field "Remarks" to "Consent received from a parent" + And I press "I agree to the policy" + And "Agreed on behalf of" "icon" should exist in the "User One" "table_row" + And "Not agreed, click to agree to \"This privacy policy\"" "icon" should exist in the "User One" "table_row" + And I click on "1 of 2" "link" in the "User One" "table_row" + And "Agreed on behalf of" "icon" should exist in the "This site policy" "table_row" + And "Max Manager" "link" should exist in the "This site policy" "table_row" + And "Consent received from a parent" "text" should exist in the "This site policy" "table_row" + And "Not agreed" "icon" should exist in the "This privacy policy" "table_row" + + Scenario: Policies and agreements profile link visible for current user + Given I log in as "user1" + And I press "Next" + And I set the field "I agree to the This site policy" to "1" + And I press "Next" + When I follow "Profile" in the user menu + # User can see his own agreements link in the profile. + Then I should see "Policies and agreements" + And I follow "Policies and agreements" + And "Agreed" "icon" should exist in the "This site policy" "table_row" + # User can't see agreements link in other user profiles. + And I am on "Course1" course homepage + And I navigate to course participants + And I follow "User Two" + And I should not see "Policies and agreements" + + Scenario: Policies and agreements profile link visible also for users who can access on behaf of others + Given I log in as "admin" + And I set the following system permissions of "Manager" role: + | capability | permission | + | tool/policy:acceptbehalf | Allow | + And I log out + And I log in as "manager" + And I press "Next" + And I set the field "I agree to the This site policy" to "1" + And I press "Next" + # User can see agreements link in other user profiles because has the capability for accepting on behalf of them. + When I am on "Course1" course homepage + And I navigate to course participants + And I follow "User Two" + Then I should see "Policies and agreements" diff --git a/admin/tool/policy/tests/behat/behat_tool_policy.php b/admin/tool/policy/tests/behat/behat_tool_policy.php new file mode 100644 index 00000000000..e2882ac3dae --- /dev/null +++ b/admin/tool/policy/tests/behat/behat_tool_policy.php @@ -0,0 +1,111 @@ +. + +/** + * Step definition for tool_policy + * + * @package tool_policy + * @category test + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); + +use Behat\Gherkin\Node\TableNode as TableNode; + +/** + * Step definition for tool_policy + * + * @package tool_policy + * @category test + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_tool_policy extends behat_base { + + /** + * Click on an entry in the edit menu. + * + * @Given /^the following policies exist:$/ + * + * @param TableNode $data + */ + public function the_following_policies_exist(TableNode $data) { + global $CFG; + if (empty($CFG->sitepolicyhandler) || $CFG->sitepolicyhandler !== 'tool_policy') { + throw new Exception('Site policy handler is not set to "tool_policy"'); + } + + $fields = [ + 'name', // Policy name (required). + 'revision', // Revision name. + 'policy', // Any policy identifier, can be used to generate multiple versions of the same policy. + 'status', // Version status: 'draft', 'active', 'archived'. By default 'active'. + 'audience', // Audience: 'guest', 'all', 'loggedin' (by default 'all'). + 'type', // 0 (default) - Site policy, 1 - Privacy policy, 2 - Third party policy, 99 - Other . + 'content', + 'summary', + ]; + + // Associative array "policy identifier" => id in the database . + $policies = []; + + foreach ($data->getHash() as $elementdata) { + $data = (object)[ + 'audience' => \tool_policy\policy_version::AUDIENCE_ALL, + 'archived' => 0, + 'type' => 0 + ]; + $elementdata = array_change_key_case($elementdata, CASE_LOWER); + foreach ($elementdata as $key => $value) { + if ($key === 'policy') { + if (array_key_exists($value, $policies)) { + $data->policyid = $policies[$value]; + } + } else if ($key === 'status') { + $data->archived = ($value === 'archived'); + } else if ($key === 'audience') { + if ($value === 'guest') { + $data->audience = \tool_policy\policy_version::AUDIENCE_GUESTS; + } else if ($value === 'loggedin') { + $data->audience = \tool_policy\policy_version::AUDIENCE_LOGGEDIN; + } + } else if (($key === 'summary' || $key === 'content') && !empty($value)) { + $data->{$key.'_editor'} = ['text' => $value, 'format' => FORMAT_MOODLE]; + } else if (in_array($key, $fields) && $value !== '') { + $data->$key = $value; + } + } + if (empty($data->name) || empty($data->content_editor) || empty($data->summary_editor)) { + throw new Exception('Policy is missing at least one of the required fields: name, content, summary'); + } + + if (!empty($data->policyid)) { + $version = tool_policy\api::form_policydoc_update_new($data); + } else { + $version = \tool_policy\api::form_policydoc_add($data); + } + + if (!empty($elementdata['policy'])) { + $policies[$elementdata['policy']] = $version->get('policyid'); + } + if (empty($elementdata['status']) || $elementdata['status'] === 'active') { + \tool_policy\api::make_current($version->get('id')); + } + } + } +} diff --git a/admin/tool/policy/tests/behat/managepolicies.feature b/admin/tool/policy/tests/behat/managepolicies.feature new file mode 100644 index 00000000000..5088f895caa --- /dev/null +++ b/admin/tool/policy/tests/behat/managepolicies.feature @@ -0,0 +1,261 @@ +@tool @tool_policy +Feature: Manage policies + In order to manage policies + As a manager + I need to be able to create and edit site policies + + Background: + Given the following config values are set as admin: + | sitepolicyhandler | tool_policy | + And the following "users" exist: + | username | firstname | lastname | email | + | user1 | User | One | one@example.com | + | user2 | User | Two | two@example.com | + | manager | Max | Manager | man@example.com | + And the following "role assigns" exist: + | user | role | contextlevel | reference | + | manager | manager | System | | + + Scenario: Create new policy and save as draft + When I log in as "manager" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I follow "New policy" + And I set the following fields to these values: + | Name | Policy1 | + | Version | v1 | + | Summary | Policy summary | + | Full policy | Full text | + And the field "Type" matches value "Site policy" + And the field "User consent" matches value "All users" + And the field "status" matches value "0" + And "Draft" "field" should exist + And "Active" "field" should exist + And "Minor change" "field" should not exist + And I should not see "Minor change" + And "Save as draft" "button" should not exist + And I press "Save" + Then the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy1 Site policy, All users | Draft | v1 | N/A | + And I log out + + Scenario: Create new policy and save as active + When I log in as "admin" + # TODO MDL-61844 change to manager! + And I navigate to "Privacy and policies > Manage policies" in site administration + And I follow "New policy" + And I set the following fields to these values: + | Name | Policy1 | + | Version | v1 | + | Summary | Policy summary | + | Full policy | Full text | + | Active | 1 | + And I press "Save" + Then the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy1 Site policy, All users | Active | v1 | 0 of 4 (0%) | + And I log out + + Scenario: Edit active policy and save as minor change + Given the following policies exist: + | Name | Revision | Content | Summary | Status | + | Policy1 | v1 | full text2 | short text2 | active | + And I log in as "manager" + And I press "Next" + And I set the field "I agree to the Policy1" to "1" + And I press "Next" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I open the action menu in "Policy1" "table_row" + And I click on "Edit" "link" in the "Policy1" "table_row" + And "Draft" "field" should not exist + And "Active" "field" should not exist + And "Minor change" "field" should exist + And "Save as draft" "button" should exist + And I set the field "Version" to "v1 amended" + And I set the field "Minor change" to "1" + And I press "Save" + Then the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy1 Site policy, All users | Active | v1 amended | 1 of 4 (25%) | + And I log out + + Scenario: Edit active policy and save as draft + Given the following policies exist: + | Name | Revision | Content | Summary | Status | + | Policy1 | v1 | full text2 | short text2 | active | + And I log in as "manager" + And I press "Next" + And I set the field "I agree to the Policy1" to "1" + And I press "Next" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I open the action menu in "Policy1" "table_row" + And I click on "Edit" "link" in the "Policy1" "table_row" + And I set the field "Version" to "v2" + And I press "Save as draft" + Then the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy1 Site policy, All users | Active | v1 | 1 of 4 (25%) | + | Policy1 Site policy, All users | Draft | v2 | N/A | + And I log out + + Scenario: Edit active policy and save as new active version + Given the following policies exist: + | Name | Revision | Content | Summary | Status | + | Policy1 | v1 | full text2 | short text2 | active | + And I log in as "manager" + And I press "Next" + And I set the field "I agree to the Policy1" to "1" + And I press "Next" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I open the action menu in "Policy1" "table_row" + And I click on "Edit" "link" in the "Policy1" "table_row" + And I set the field "Name" to "Policy2" + And I set the field "Version" to "v2" + And I press "Save" + Then the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy2 Site policy, All users | Active | v2 | 0 of 4 (0%) | + And I should not see "Policy1" + And I should not see "v1" + And I open the action menu in "Policy2" "table_row" + And I click on "View previous versions" "link" in the "Policy2" "table_row" + And I should see "Policy2 previous versions" + And I should not see "v2" + Then the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy1 Site policy, All users | Inactive | v1 | 1 of 4 (25%) | + And I log out + + Scenario: Edit draft policy and save as draft + Given the following policies exist: + | Name | Revision | Content | Summary | Status | + | Policy1 | v1 | full text2 | short text2 | draft | + And I log in as "manager" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I open the action menu in "Policy1" "table_row" + And I click on "Edit" "link" in the "Policy1" "table_row" + And I set the field "Version" to "v2" + And "Draft" "field" should exist + And "Active" "field" should exist + And "Minor change" "field" should not exist + And I should not see "Minor change" + And "Save as draft" "button" should not exist + And I press "Save" + Then the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy1 Site policy, All users | Draft | v2 | N/A | + And I should not see "v1" + And I open the action menu in "Policy1" "table_row" + And "View previous versions" "link" should not exist + And I log out + + Scenario: Edit draft policy and save as active + Given the following policies exist: + | Name | Revision | Content | Summary | Status | + | Policy1 | v1 | full text2 | short text2 | draft | + And I log in as "admin" + # TODO MDL-61844 change to manager! + And I navigate to "Privacy and policies > Manage policies" in site administration + And I open the action menu in "Policy1" "table_row" + And I click on "Edit" "link" in the "Policy1" "table_row" + And I set the field "Version" to "v2" + And I set the field "Active" to "1" + And I press "Save" + Then the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy1 Site policy, All users | Active | v2 | 0 of 4 (0%) | + And I should not see "v1" + And I open the action menu in "Policy1" "table_row" + And "View previous versions" "link" should not exist + And I log out + + Scenario: Activate draft policy + Given the following policies exist: + | Name | Revision | Content | Summary | Status | + | Policy1 | v1 | full text2 | short text2 | draft | + And I log in as "admin" + # TODO MDL-61844 change to manager! + And I navigate to "Privacy and policies > Manage policies" in site administration + And I open the action menu in "Policy1" "table_row" + And I click on "Set status to \"Active\"" "link" in the "Policy1" "table_row" + Then I should see "All users will be required to accept this new policy version to be able to use the site" + And I press "Continue" + And the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy1 Site policy, All users | Active | v1 | 0 of 4 (0%) | + And I open the action menu in "Policy1" "table_row" + And "View previous versions" "link" should not exist + And I log out + + Scenario: Edit archived policy and save as draft + Given the following policies exist: + | Name | Revision | Content | Summary | Status | + | Policy1 | v1 | full text2 | short text2 | active | + And I log in as "manager" + And I press "Next" + And I set the field "I agree to the Policy1" to "1" + And I press "Next" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I open the action menu in "Policy1" "table_row" + And I click on "Set status to \"Inactive\"" "link" in the "Policy1" "table_row" + Then I should see "The policy will not apply until some version is made the current one" + And I press "Continue" + And the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy1 Site policy, All users | Inactive | v1 | 1 of 4 (25%) | + And I open the action menu in "Policy1" "table_row" + And I click on "Create a new \"Draft\"" "link" in the "Policy1" "table_row" + And I set the field "Version" to "v2" + And I set the field "Name" to "Policy2" + And the field "status" matches value "0" + And "Draft" "field" should exist + And "Active" "field" should exist + And "Minor change" "field" should not exist + And I should not see "Minor change" + And "Save as draft" "button" should not exist + And I press "Save" + And the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy2 Site policy, All users | Draft | v2 | N/A | + And I should not see "v1" + And I should not see "Policy1" + And I open the action menu in "Policy2" "table_row" + And I click on "View previous versions" "link" in the "Policy2" "table_row" + And I should see "Policy2 previous versions" + And the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy1 Site policy, All users | Inactive | v1 | 1 of 4 (25%) | + And I should not see "v2" + And I log out + + Scenario: Edit archived policy and save as active + Given the following policies exist: + | Name | Revision | Content | Summary | Status | + | Policy1 | v1 | full text2 | short text2 | active | + And I log in as "manager" + And I press "Next" + And I set the field "I agree to the Policy1" to "1" + And I press "Next" + And I navigate to "Privacy and policies > Manage policies" in site administration + And I open the action menu in "Policy1" "table_row" + And I click on "Set status to \"Inactive\"" "link" in the "Policy1" "table_row" + And I press "Continue" + And I open the action menu in "Policy1" "table_row" + And I click on "Create a new \"Draft\"" "link" in the "Policy1" "table_row" + And I set the field "Version" to "v2" + And I set the field "Name" to "Policy2" + And I set the field "Active" to "1" + And I press "Save" + And the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy2 Site policy, All users | Active | v2 | 0 of 4 (0%) | + And I should not see "v1" + And I should not see "Policy1" + And I open the action menu in "Policy2" "table_row" + And I click on "View previous versions" "link" in the "Policy2" "table_row" + And I should see "Policy2 previous versions" + And the following should exist in the "tool-policy-managedocs-wrapper" table: + | Name | Policy status | Version | Agreements | + | Policy1 Site policy, All users | Inactive | v1 | 1 of 4 (25%) | + And I should not see "v2" + And I log out diff --git a/admin/tool/policy/user.php b/admin/tool/policy/user.php new file mode 100644 index 00000000000..889934c69ab --- /dev/null +++ b/admin/tool/policy/user.php @@ -0,0 +1,53 @@ +. + +/** + * View user acceptances to the policies + * + * @package tool_policy + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__.'/../../../config.php'); +require_once($CFG->dirroot.'/user/editlib.php'); + +$userid = optional_param('userid', null, PARAM_INT); +$returnurl = optional_param('returnurl', null, PARAM_LOCALURL); + +require_login(); +$userid = $userid ?: $USER->id; +if (isguestuser() || isguestuser($userid)) { + print_error('noguest'); +} +$context = context_user::instance($userid); +if ($userid != $USER->id) { + // Check capability to view acceptances. No capability is needed to view your own acceptances. + if (!has_capability('tool/policy:acceptbehalf', $context)) { + require_capability('tool/policy:viewacceptances', $context); + } +} + +$PAGE->set_context($context); +$PAGE->set_url(new moodle_url('/admin/tool/policy/user.php', ['userid' => $userid])); + +$output = $PAGE->get_renderer('tool_policy'); +echo $output->header(); +echo $output->heading(get_string('policiesagreements', 'tool_policy')); +$acceptances = new \tool_policy\output\acceptances($userid, $returnurl); +echo $output->render($acceptances); +$PAGE->requires->js_call_amd('tool_policy/acceptmodal', 'getInstance', [context_system::instance()->id]); +echo $output->footer(); diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index cb6c2fe2899..09a7ed112dd 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1907,8 +1907,8 @@ class core_plugin_manager { 'analytics', 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'cohortroles', 'customlang', 'dbtransfer', 'filetypes', 'generator', 'health', 'httpsreplace', 'innodb', 'installaddon', 'langimport', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound', 'mobile', 'multilangupgrade', - 'monitor', 'oauth2', 'phpunit', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary', - 'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb' + 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task', + 'templatelibrary', 'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb' ), 'webservice' => array(