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
' . $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 00000000000..21c5b5d360f
Binary files /dev/null and b/admin/tool/policy/pix/agreedno.png differ
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 00000000000..6d1f79a535e
Binary files /dev/null and b/admin/tool/policy/pix/agreedyes.png differ
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 00000000000..dff2dc25c7f
Binary files /dev/null and b/admin/tool/policy/pix/agreedyesonbehalf.png differ
diff --git a/admin/tool/policy/pix/agreedyesonbehalf.svg b/admin/tool/policy/pix/agreedyesonbehalf.svg
new file mode 100644
index 00000000000..2b1c63d4cbd
--- /dev/null
+++ b/admin/tool/policy/pix/agreedyesonbehalf.svg
@@ -0,0 +1,3 @@
+
+]>
\ 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
{{#str}} policydocname, tool_policy {{/str}} | +{{#str}} policydocrevision, tool_policy {{/str}} | +{{#str}} agreed, tool_policy {{/str}} | +{{#str}} agreedon, tool_policy {{/str}} | + {{#hasonbehalfagreements}} +{{#str}} agreedby, tool_policy {{/str}} | +{{#str}} acceptancenote, tool_policy {{/str}} | + {{/hasonbehalfagreements}} ++ |
---|