MDL-51036 tool_lp: Granular permissions on user competencies

This commit is contained in:
Frederic Massart
2016-04-18 10:58:50 +08:00
parent 182f609213
commit 943989c22b
8 changed files with 450 additions and 50 deletions
+29 -35
View File
@@ -849,7 +849,9 @@ class api {
$capabilities = array('tool/lp:coursecompetencyread', 'tool/lp:coursecompetencymanage');
if (!has_any_capability($capabilities, $context)) {
throw new required_capability_exception($context, 'tool/lp:coursecompetencyread', 'nopermissions', '');
throw new required_capability_exception($context, 'tool/lp:coursecompetencyread', 'nopermissions', '');
} else if (!user_competency::can_read_user($userid, $competencyid)) {
throw new required_capability_exception($context, 'tool/lp:usercompetencyview', 'nopermissions', '');
}
// This will throw an exception if the competency does not belong to the course.
@@ -882,7 +884,9 @@ class api {
$capabilities = array('tool/lp:coursecompetencyread', 'tool/lp:coursecompetencymanage');
if (!has_any_capability($capabilities, $context)) {
throw new required_capability_exception($context, 'tool/lp:coursecompetencyread', 'nopermissions', '');
throw new required_capability_exception($context, 'tool/lp:coursecompetencyread', 'nopermissions', '');
} else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
throw new required_capability_exception($context, 'tool/lp:usercompetencyview', 'nopermissions', '');
}
// OK - all set.
@@ -2018,9 +2022,8 @@ class api {
$plan = new plan($planorid);
}
if (!$plan->can_read()) {
$context = context_user::instance($plan->get_userid());
throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
if (!user_competency::can_read_user($plan->get_userid(), $competencyid)) {
throw new required_capability_exception($plan->get_context(), 'tool/lp:usercompetencyview', 'nopermissions', '');
}
$competency = $plan->get_competency($competencyid);
@@ -2265,16 +2268,14 @@ class api {
* @return bool
*/
public static function user_competency_cancel_review_request($userid, $competencyid) {
if (!plan::can_read_user($userid)) {
$context = context_user::instance($userid);
throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
}
$context = context_user::instance($userid);
$uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
if (!$uc || $uc->get_status() != user_competency::STATUS_WAITING_FOR_REVIEW) {
if (!$uc || !$uc->can_read()) {
throw new required_capability_exception($context, 'tool/lp:usercompetencyread', 'nopermissions', '');
} else if ($uc->get_status() != user_competency::STATUS_WAITING_FOR_REVIEW) {
throw new coding_exception('The competency can not be cancel review request at this stage.');
} else if (!$uc->can_request_review()) {
throw new required_capability_exception($context, 'tool/lp:userevidencemanage', 'nopermissions', '');
throw new required_capability_exception($context, 'tool/lp:usercompetencyrequestreview', 'nopermissions', '');
}
$uc->set_status(user_competency::STATUS_IDLE);
@@ -2289,21 +2290,18 @@ class api {
* @return bool
*/
public static function user_competency_request_review($userid, $competencyid) {
if (!plan::can_read_user($userid)) {
$context = context_user::instance($userid);
throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
}
$uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
if (!$uc) {
$uc = user_competency::create_relation($userid, $competencyid);
$uc->create();
}
if ($uc->get_status() != user_competency::STATUS_IDLE) {
if (!$uc->can_read()) {
throw new required_capability_exception($uc->get_context(), 'tool/lp:usercompetencyread', 'nopermissions', '');
} else if ($uc->get_status() != user_competency::STATUS_IDLE) {
throw new coding_exception('The competency can not be sent for review at this stage.');
} else if (!$uc->can_request_review()) {
throw new required_capability_exception($context, 'tool/lp:userevidencemanage', 'nopermissions', '');
throw new required_capability_exception($uc->get_context(), 'tool/lp:usercompetencyrequestreview', 'nopermissions', '');
}
$uc->set_status(user_competency::STATUS_WAITING_FOR_REVIEW);
@@ -2320,16 +2318,14 @@ class api {
public static function user_competency_start_review($userid, $competencyid) {
global $USER;
if (!plan::can_read_user($userid)) {
$context = context_user::instance($userid);
throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
}
$context = context_user::instance($userid);
$uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
if (!$uc || $uc->get_status() != user_competency::STATUS_WAITING_FOR_REVIEW) {
if (!$uc || !$uc->can_read()) {
throw new required_capability_exception($context, 'tool/lp:usercompetencyread', 'nopermissions', '');
} else if ($uc->get_status() != user_competency::STATUS_WAITING_FOR_REVIEW) {
throw new coding_exception('The competency review can not be started at this stage.');
} else if (!$uc->can_review()) {
throw new required_capability_exception($context, 'tool/lp:competencygrade', 'nopermissions', '');
throw new required_capability_exception($context, 'tool/lp:usercompetencyreview', 'nopermissions', '');
}
$uc->set_status(user_competency::STATUS_IN_REVIEW);
@@ -2345,16 +2341,14 @@ class api {
* @return bool
*/
public static function user_competency_stop_review($userid, $competencyid) {
if (!plan::can_read_user($userid)) {
$context = context_user::instance($userid);
throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
}
$context = context_user::instance($userid);
$uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
if (!$uc || $uc->get_status() != user_competency::STATUS_IN_REVIEW) {
if (!$uc || !$uc->can_read()) {
throw new required_capability_exception($context, 'tool/lp:usercompetencyread', 'nopermissions', '');
} else if ($uc->get_status() != user_competency::STATUS_IN_REVIEW) {
throw new coding_exception('The competency review can not be stopped at this stage.');
} else if (!$uc->can_review()) {
throw new required_capability_exception($context, 'tool/lp:competencygrade', 'nopermissions', '');
throw new required_capability_exception($context, 'tool/lp:usercompetencyreview', 'nopermissions', '');
}
$uc->set_status(user_competency::STATUS_IDLE);
@@ -2785,9 +2779,9 @@ class api {
$skip = 0,
$limit = 0) {
if (!plan::can_read_user($userid)) {
if (!user_competency::can_read_user($userid, $competencyid)) {
$context = context_user::instance($userid);
throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
throw new required_capability_exception($context, 'tool/lp:usercompetencyview', 'nopermissions', '');
}
// TODO - handle archived plans.
@@ -86,6 +86,59 @@ class course_competency extends persistent {
}
}
/**
* Return the courses where both competency and user are.
*
* A user is considered being in a course when they are enrolled, the enrolment is valid,
* the enrolment instance is enabled, and the enrolment plugin is enabled..
*
* @param int $competencyid The competency ID.
* @param int $userid The user ID.
* @return array Indexed by course ID.
*/
public static function get_courses_with_competency_and_user($competencyid, $userid) {
global $CFG, $DB;
if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) {
return array();
}
$ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
list($plugins, $params) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee');
$params['competencyid'] = $competencyid;
$params['userid'] = $userid;
$params['enabled'] = ENROL_INSTANCE_ENABLED;
$params['active'] = ENROL_USER_ACTIVE;
$params['contextlevel'] = CONTEXT_COURSE;
// Heavily based on enrol_get_shared_courses().
$sql = "SELECT c.*, $ctxfields
FROM {course} c
JOIN {" . static::TABLE . "} cc
ON cc.courseid = c.id
AND cc.competencyid = :competencyid
JOIN (
SELECT DISTINCT c.id
FROM {enrol} e
JOIN {user_enrolments} ue
ON ue.enrolid = e.id
AND ue.status = :active
AND ue.userid = :userid
JOIN {course} c
ON c.id = e.courseid
WHERE e.status = :enabled
AND e.enrol $plugins
) ec ON ec.id = c.id
LEFT JOIN {context} ctx
ON ctx.instanceid = c.id
AND ctx.contextlevel = :contextlevel
ORDER BY c.id";
$courses = $DB->get_records_sql($sql, $params);
array_map('context_helper::preload_from_record', $courses);
return $courses;
}
/**
* Return a list of rules.
*
+184 -12
View File
@@ -25,6 +25,7 @@ namespace tool_lp;
defined('MOODLE_INTERNAL') || die();
use coding_exception;
use context_course;
use context_user;
use comment;
use lang_string;
@@ -89,22 +90,53 @@ class user_competency extends persistent {
);
}
/**
* Whether the current user can comment on this user competency.
*
* @return bool
*/
public function can_comment() {
return static::can_comment_user($this->get_userid(), $this->get_competencyid());
}
/**
* Whether the current user can read this user competency.
*
* @return bool
*/
public function can_read() {
return static::can_read_user($this->get_userid(), $this->get_competencyid());
}
/**
* Whether the current user can read comments on this user competency.
*
* @return bool
*/
public function can_read_comments() {
return static::can_read_comments_user($this->get_userid(), $this->get_competencyid());
}
/**
* Can the current user send the user competency for review?
*
* This also performs checks in relevant courses.
*
* @return bool
*/
public function can_request_review() {
return static::can_request_review_user($this->get_userid());
return static::can_request_review_user($this->get_userid(), $this->get_competencyid());
}
/**
* Can the current user review the user competency?
*
* This also performs checks in relevant courses.
*
* @return bool
*/
public function can_review() {
return static::can_review_user($this->get_userid());
return static::can_review_user($this->get_userid(), $this->get_competencyid());
}
/**
@@ -299,25 +331,165 @@ class user_competency extends persistent {
}
/**
* Can the current user send a user's competency for review?
* Can the current user comment on a user's competency?
*
* @param int $userid The user ID the competency belongs to.
* This follows the same philosophy as {@link self::can_read_user()}.
*
* @param int $userid The user ID the competency belongs to.
* @param int $competencyid The competency ID.
* @return bool
*/
public static function can_request_review_user($userid) {
// We can request a review when we can attach evidence of prior learning.
return user_evidence::can_manage_user($userid);
public static function can_comment_user($userid, $competencyid) {
global $USER;
$capabilities = array('tool/lp:usercompetencycomment');
if ($USER->id == $userid) {
$capabilities[] = 'tool/lp:usercompetencycommentown';
}
if (has_any_capability($capabilities, context_user::instance($userid))) {
return true;
}
$courses = course_competency::get_courses_with_competency_and_user($competencyid, $userid);
foreach ($courses as $course) {
if (has_any_capability($capabilities, context_course::instance($course->id))) {
return true;
}
}
return false;
}
/**
* Can the current user send review the user competency?
* Can the current user read the comments on a user's competency?
*
* @param int $userid The user ID the competency belongs to.
* This follows the same philosophy as {@link self::can_read_user()}.
*
* @param int $userid The user ID the competency belongs to.
* @param int $competencyid The competency ID.
* @return bool
*/
public static function can_review_user($userid) {
// We can review a competency when we can grade them.
return has_capability('tool/lp:competencygrade', context_user::instance($userid));
public static function can_read_comments_user($userid, $competencyid) {
// Everyone who can read the user competency can read the comments.
return static::can_read_user($userid, $competencyid);
}
/**
* Can the current user read the user competencies of a user in a course?
*
* This method is useful when the competency IDs are not known in advance, but the course is.
* Though this removes a level of granirality as it will not check other courses in which
* a user could have access to the user competency, therefore not following the
* philosophy "If you can view it somewhere, you can view it anywhere.".
*
* Only use this when you want to check for "all or nothing".
*
* @param int $userid The user ID the competency belongs to.
* @param int $courseid The course ID.
* @return bool
*/
public static function can_read_user_in_course($userid, $courseid) {
$capability = 'tool/lp:usercompetencyview';
return plan::can_read_user($userid)
|| has_capability($capability, context_user::instance($userid))
|| has_capability($capability, context_course::instance($courseid));
}
/**
* Can the current user read a user's competency?
*
* Following the philosophy "If you can view it somewhere, you can view it anywhere.",
* this methods checks if any of the following are true:
*
* - Can view the user's plans (user context).
* - Can view the user's user competencies (user context).
* - Can view the user's user competencies in courses where the user is enrolled (course context).
*
* Note that the competency ID is required, that's because if you're not passing it you're
* probably trying to check the wrong permissions. If you just to know if a user can read other
* users' competencies in a course, refer to {@link self::can_read_user_in_course()}, though its
* usage is not preferred. If you want to refer to a user context, for instance when viewing a plan,
* then you should simply refer to the permissions to read the plan. If you can read the plan, by
* definition you can read any of the user competencies it contains: {@link plan::can_read_user()}.
*
* @param int $userid The user ID the competency belongs to.
* @param int $competencyid The competency ID.
* @return bool
*/
public static function can_read_user($userid, $competencyid) {
$capability = 'tool/lp:usercompetencyview';
if (plan::can_read_user($userid)) {
return true;
} else if (has_capability($capability, context_user::instance($userid))) {
return true;
}
$courses = course_competency::get_courses_with_competency_and_user($competencyid, $userid);
foreach ($courses as $course) {
if (has_capability($capability, context_course::instance($course->id))) {
return true;
}
}
return false;
}
/**
* Can the current user send a user's competency for review?
*
* This follows the same philosophy as {@link self::can_read_user()}.
*
* @param int $userid The user ID the competency belongs to.
* @param int $competencyid The competency ID.
* @return bool
*/
public static function can_request_review_user($userid, $competencyid) {
global $USER;
$capabilities = array('tool/lp:usercompetencyrequestreview');
if ($USER->id == $userid) {
$capabilities[] = 'tool/lp:usercompetencyrequestreviewown';
}
if (has_any_capability($capabilities, context_user::instance($userid))) {
return true;
}
$courses = course_competency::get_courses_with_competency_and_user($competencyid, $userid);
foreach ($courses as $course) {
if (has_any_capability($capabilities, context_course::instance($course->id))) {
return true;
}
}
return false;
}
/**
* Can the current user review the user competency?
*
* This follows the same philosophy as {@link self::can_read_user()}.
*
* @param int $userid The user ID the competency belongs to.
* @param int $competencyid The competency ID.
* @return bool
*/
public static function can_review_user($userid, $competencyid) {
$capability = 'tool/lp:usercompetencyreview';
if (has_capability($capability, context_user::instance($userid))) {
return true;
}
$courses = course_competency::get_courses_with_competency_and_user($competencyid, $userid);
foreach ($courses as $course) {
if (has_capability($capability, context_course::instance($course->id))) {
return true;
}
}
return false;
}
/**
+42
View File
@@ -143,6 +143,48 @@ $capabilities = array(
),
'clonepermissionsfrom' => 'moodle/site:config'
),
// User competencies.
'tool/lp:usercompetencyview' => array(
'captype' => 'read',
'contextlevel' => CONTEXT_COURSE, // And CONTEXT_USER.
'archetypes' => array(
'editingteacher' => CAP_ALLOW
)
),
'tool/lp:usercompetencyrequestreview' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE, // And CONTEXT_USER.
'archetypes' => array(
)
),
'tool/lp:usercompetencyrequestreviewown' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE, // And CONTEXT_USER.
'archetypes' => array(
'user' => CAP_ALLOW
)
),
'tool/lp:usercompetencyreview' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE, // And CONTEXT_USER.
'archetypes' => array(
),
'clonepermissionsfrom' => 'moodle/site:config'
),
'tool/lp:usercompetencycomment' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE, // And CONTEXT_USER.
'archetypes' => array(
),
'clonepermissionsfrom' => 'moodle/site:config'
),
'tool/lp:usercompetencycommentown' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE, // And CONTEXT_USER.
'archetypes' => array(
'user' => CAP_ALLOW
),
),
// Template.
'tool/lp:templatemanage' => array(
'captype' => 'write',
+3 -2
View File
@@ -290,8 +290,9 @@ function tool_lp_comment_add($comment, $params) {
function tool_lp_comment_permissions($params) {
if ($params->commentarea == 'user_competency') {
$uc = new \tool_lp\user_competency($params->itemid);
$can = \tool_lp\plan::can_read_user($uc->get_userid());
return array('post' => $can, 'view' => $can);
if ($uc->can_read()) {
return array('post' => $uc->can_comment(), 'view' => $uc->can_read_comments());
}
}
return array('post' => false, 'view' => false);
}
@@ -0,0 +1,137 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Course competency persistent class tests.
*
* @package tool_lp
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
use tool_lp\course_competency;
/**
* Course competency persistent testcase.
*
* @package tool_lp
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_lp_course_competency_testcase extends advanced_testcase {
public function test_get_courses_with_competency_and_user() {
global $CFG, $DB;
$this->resetAfterTest(true);
$dg = $this->getDataGenerator();
$lpg = $dg->get_plugin_generator('tool_lp');
$c1 = $dg->create_course();
$c2 = $dg->create_course();
$c3 = $dg->create_course();
$c4 = $dg->create_course();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u3 = $dg->create_user();
$u4 = $dg->create_user();
$flatfileplugin = enrol_get_plugin('flatfile');
$flatfileinstanceid = $flatfileplugin->add_instance($c2);
$framework = $lpg->create_framework();
$comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); // In C1, and C2.
$comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); // In C2.
$comp3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); // In None.
$comp4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); // In C4.
$lpg->create_course_competency(array('competencyid' => $comp1->get_id(), 'courseid' => $c1->id));
$lpg->create_course_competency(array('competencyid' => $comp1->get_id(), 'courseid' => $c2->id));
$lpg->create_course_competency(array('competencyid' => $comp2->get_id(), 'courseid' => $c2->id));
$lpg->create_course_competency(array('competencyid' => $comp4->get_id(), 'courseid' => $c4->id));
// Enrol the user 1 in C1, C2, and C3.
$dg->enrol_user($u1->id, $c1->id);
$dg->enrol_user($u1->id, $c2->id);
$dg->enrol_user($u1->id, $c3->id);
// Enrol the user 2 in C4.
$dg->enrol_user($u2->id, $c4->id);
// Enrol the user 3 in C1 and C2, but non active in C2.
$dg->enrol_user($u3->id, $c1->id);
$dg->enrol_user($u3->id, $c2->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
// Enrol the user 4 with a plugin which will be enabled/disabled.
$dg->enrol_user($u4->id, $c2->id, null, 'flatfile');
// Using the competency that is not used anywhere -> no courses.
$this->assertCount(0, course_competency::get_courses_with_competency_and_user($comp3->get_id(), $u1->id));
// Using the competency that is used in a course where the user is not enrolled -> no courses.
$this->assertCount(0, course_competency::get_courses_with_competency_and_user($comp4->get_id(), $u1->id));
// Using the competency that is used in a course where the user is enrolled -> one course.
$courses = course_competency::get_courses_with_competency_and_user($comp2->get_id(), $u1->id);
$this->assertCount(1, $courses);
$this->assertArrayHasKey($c2->id, $courses);
// Using the competency used multiple times.
$courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u1->id);
$this->assertCount(2, $courses);
$this->assertArrayHasKey($c1->id, $courses);
$this->assertArrayHasKey($c2->id, $courses);
// Checking for another user where the competency is used twice, but not for them.
$courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u2->id);
$this->assertCount(0, $courses);
// Checking for another user where the competency is used in their course.
$courses = course_competency::get_courses_with_competency_and_user($comp4->get_id(), $u2->id);
$this->assertCount(1, $courses);
$this->assertArrayHasKey($c4->id, $courses);
// Checking for a user who is suspended in a course.
$courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u3->id);
$this->assertCount(1, $courses);
$this->assertArrayHasKey($c1->id, $courses);
// Check for the user with plugin enabled.
$enrolplugins = explode(',', $CFG->enrol_plugins_enabled);
$enrolplugins[] = 'flatfile';
$CFG->enrol_plugins_enabled = implode(',', array_unique($enrolplugins));
$courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u4->id);
$this->assertCount(1, $courses);
$this->assertArrayHasKey($c2->id, $courses);
// Check for the user with plugin enabled, but enrolment instance disabled.
$flatfileinstance = $DB->get_record('enrol', array('id' => $flatfileinstanceid));
$flatfileplugin->update_status($flatfileinstance, ENROL_INSTANCE_DISABLED);
$courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u4->id);
$this->assertCount(0, $courses);
$flatfileplugin->update_status($flatfileinstance, ENROL_INSTANCE_ENABLED);
// Check for the user with plugin disabled.
$enrolplugins = array_flip(explode(',', $CFG->enrol_plugins_enabled));
unset($enrolplugins['flatfile']);
$CFG->enrol_plugins_enabled = implode(',', array_keys($enrolplugins));
$courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u4->id);
$this->assertCount(0, $courses);
}
}
+1
View File
@@ -46,6 +46,7 @@ class tool_lp_lib_testcase extends advanced_testcase {
$u3 = $dg->create_user();
$reviewerroleid = $dg->create_role();
assign_capability('tool/lp:planview', CAP_ALLOW, $reviewerroleid, context_system::instance()->id, true);
assign_capability('tool/lp:usercompetencycomment', CAP_ALLOW, $reviewerroleid, context_system::instance()->id, true);
$dg->role_assign($reviewerroleid, $u2->id, context_user::instance($u1->id));
$dg->role_assign($reviewerroleid, $u3->id, context_user::instance($u1->id));
accesslib_clear_all_caches_for_unit_testing();
+1 -1
View File
@@ -25,6 +25,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2015111037; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2015111038; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2014110400; // Requires this Moodle version.
$plugin->component = 'tool_lp'; // Full name of the plugin (used for diagnostics).