From a6e7237adc2a3c7fa2107a930a45034d29eea40c Mon Sep 17 00:00:00 2001 From: tjhunt Date: Wed, 12 Nov 2008 07:55:09 +0000 Subject: [PATCH] roles explanation: MDL-13538 Make a table showing the result of has_capability for every capabiltiy in a context. This implements the page showing the table. It is not yet integrated into the tab bar. To try this, go to an Assign roles page (one with URL .../admin/roles/assign.php?contextid=...) and change the 'assign' to 'explain'. --- admin/roles/explain.php | 215 +++++++++++++++++++++++++++ admin/roles/explainhascapabiltiy.php | 61 +++++++- lib/adminlib.php | 131 ++++++++++++++++ theme/standard/styles_color.css | 16 +- theme/standard/styles_layout.css | 31 ++-- user/selector/lib.php | 20 ++- user/selector/script.js | 2 + 7 files changed, 459 insertions(+), 17 deletions(-) create mode 100755 admin/roles/explain.php diff --git a/admin/roles/explain.php b/admin/roles/explain.php new file mode 100755 index 0000000000..e2c4a989be --- /dev/null +++ b/admin/roles/explain.php @@ -0,0 +1,215 @@ +libdir.'/adminlib.php'); + require_once($CFG->dirroot.'/user/selector/lib.php'); + + $contextid = required_param('contextid',PARAM_INT); + $contextuserid = optional_param('userid', 0, PARAM_INT); // needed for user tabs + $courseid = optional_param('courseid', 0, PARAM_INT); // needed for user tabs + + if (! $context = get_context_instance_by_id($contextid)) { + print_error('wrongcontextid', 'error'); + } + $isfrontpage = $context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID; + $contextname = print_context_name($context); + + if ($context->contextlevel == CONTEXT_COURSE) { + $courseid = $context->instanceid; + if (!$course = $DB->get_record('course', array('id'=>$courseid))) { + print_error('invalidcourse', 'error'); + } + + } else if (!empty($courseid)){ // we need this for user tabs in user context + if (!$course = $DB->get_record('course', array('id'=>$courseid))) { + print_error('invalidcourse', 'error'); + } + + } else { + $courseid = SITEID; + $course = clone($SITE); + } + +/// Check login and permissions. + require_login($course); + $canview = has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride', + 'moodle/role:override', 'moodle/role:manage'), $context); + if (!$canview) { + print_error('nopermissions', 'error', '', get_string('explainpermissions', 'role')); + } + +/// These are needed early because of tabs.php + $assignableroles = get_assignable_roles($context, ROLENAME_BOTH); + $overridableroles = get_overridable_roles($context, ROLENAME_BOTH); + +/// Get the user_selector we will need. +/// Teachers within a course just get to see the same list of people they can +/// assign roles to. Admins (people with moodle/role:manage) can run this report for any user. + $options = array('context' => $context, 'roleid' => 0); + if ($context->contextlevel > CONTEXT_COURSE && !is_inside_frontpage($context) && !has_capability('moodle/role:manage', $context)) { + $userselector = new potential_assignees_below_course('reportuser', $options); + } else { + $userselector = new potential_assignees_course_and_above('reportuser', $options); + } + $userselector->set_multiselect(false); + $userselector->set_rows(10); + +/// Work out an appropriate page title. + $title = get_string('explainpermissionsin', 'role', $contextname); + $straction = get_string('explainpermissions', 'role'); // Used by tabs.php + +/// Print the header and tabs + if ($context->contextlevel == CONTEXT_USER) { + $contextuser = $DB->get_record('user', array('id' => $contextuserid)); + $fullname = fullname($contextuser, has_capability('moodle/site:viewfullnames', $context)); + + /// course header + $navlinks = array(); + if ($courseid != SITEID) { + if (has_capability('moodle/course:viewparticipants', get_context_instance(CONTEXT_COURSE, $courseid))) { + $navlinks[] = array('name' => get_string('participants'), 'link' => "$CFG->wwwroot/user/index.php?id=$courseid", 'type' => 'misc'); + } + $navlinks[] = array('name' => $fullname, 'link' => "$CFG->wwwroot/user/view.php?id=$contextuserid&course=$courseid", 'type' => 'misc'); + $navlinks[] = array('name' => $straction, 'link' => null, 'type' => 'misc'); + $navigation = build_navigation($navlinks); + + print_header($title, $fullname, $navigation, '', '', true, ' ', navmenu($course)); + + /// site header + } else { + $navlinks[] = array('name' => $fullname, 'link' => "$CFG->wwwroot/user/view.php?id=$contextuserid&course=$courseid", 'type' => 'misc'); + $navlinks[] = array('name' => $straction, 'link' => null, 'type' => 'misc'); + $navigation = build_navigation($navlinks); + print_header($title, $course->fullname, $navigation, "", "", true, " ", navmenu($course)); + } + + $showroles = 1; + $currenttab = 'explain'; + include_once($CFG->dirroot.'/user/tabs.php'); + + } else if ($context->contextlevel == CONTEXT_SYSTEM) { + admin_externalpage_setup('assignroles'); +// admin_externalpage_setup('explainpermissions'); + admin_externalpage_print_header(); + + } else if ($context->contextlevel == CONTEXT_COURSE and $context->instanceid == SITEID) { + admin_externalpage_setup('explainfrontpagepermissions'); + admin_externalpage_print_header(); + $currenttab = 'explain'; + include_once('tabs.php'); + + } else { + $currenttab = 'explain'; + include_once('tabs.php'); + } + +/// Print heading. + print_heading_with_help($title, 'explainpermissions'); + +/// If a user has been chosen, show all the permissions for this user. + $user = $userselector->get_selected_user(); + if (!is_null($user)) { + + /// Class for rendering the table. + class explain_cabability_table extends cabability_table_base { + protected $user; + protected $fullname; + protected $baseurl; + protected $stryes; + protected $strno; + protected $strexplanation; + private $hascap; + public function __construct($context, $user) { + global $CFG; + parent::__construct($context, 'explaincaps'); + $this->user = $user; + $this->fullname = fullname($user); + $this->baseurl = $CFG->wwwroot . '/' . $CFG->admin . + '/roles/explainhascapabiltiy.php?user=' . $user->id . + '&contextid=' . $context->id . '&capability='; + $this->stryes = get_string('yes'); + $this->strno = get_string('no'); + $this->strexplanation = get_string('explanation'); + } + protected function add_header_cells() { + echo '' . get_string('allowed', 'role') . ''; + echo '' . $this->strexplanation . ''; + } + protected function num_extra_columns() { + return 2; + } + protected function skip_row($capability) { + return $capability->name != 'moodle/site:doanything' && is_legacy($capability->name); + } + protected function get_row_classes($capability) { + $this->hascap = has_capability($capability->name, $this->context, $this->user->id); + if ($this->hascap) { + return array('yes'); + } else { + return array('no'); + } + } + protected function add_row_cells($capability) { + if ($this->hascap) { + $result = $this->stryes; + $tooltip = 'explainwhyhascap'; + } else { + $result = $this->strno; + $tooltip = 'explainwhyhasnotcap'; + } + $a = new stdClass; + $a->username = $this->fullname; + $a->capability = $capability->name; + echo '' . $result . ''; + echo ''; + link_to_popup_window($this->baseurl . $capability->name, 'hascapabilityexplanation', + $this->strexplanation, 600, 600, get_string($tooltip, 'role', $a)); + echo ''; + } + } + + require_js(array('yui_yahoo', 'yui_dom', 'yui_event')); + require_js($CFG->admin . '/roles/roles.js'); + print_box_start('generalbox boxaligncenter'); + print_heading(get_string('permissionsforuser', 'role', fullname($user)), '', 3); + + $table = new explain_cabability_table($context, $user); + $table->display(); + print_box_end(); + + $selectheading = get_string('selectanotheruser', 'role'); + } else { + $selectheading = get_string('selectauser', 'role'); + } + +/// Show UI for choosing a user to report on. + print_box_start('generalbox boxwidthnormal boxaligncenter', 'chooseuser'); + echo '
'; + +/// Hidden fields. + echo ''; + if (!empty($contextuserid)) { + echo ''; + } + if ($courseid && $courseid != SITEID) { + echo ''; + } + +/// User selector. + print_heading('', '', 3); + $userselector->display(); + +/// Submit button and the end of the form. + echo '

'; + echo '
'; + print_box_end(); + +/// Appropriate back link. + if (!$isfrontpage && ($url = get_context_url($context))) { + echo ''; + } + + print_footer($course); +?> diff --git a/admin/roles/explainhascapabiltiy.php b/admin/roles/explainhascapabiltiy.php index 6af2c13e06..12280173f2 100644 --- a/admin/roles/explainhascapabiltiy.php +++ b/admin/roles/explainhascapabiltiy.php @@ -76,6 +76,47 @@ foreach ($roles as $role) { } $rolenames = role_fix_names($rolenames, $context); +// Pass over the data once, to find the cell that determines the result. +$userhascapability = has_capability($capability, $context, $userid, false); +$areprohibits = false; +$decisiveassigncon = 0; +$decisiveoverridecon = 0; +foreach ($contexts as $con) { + if (!empty($accessdata['ra'][$con->path])) { + // The array_unique here is to work around bug MDL-14817. Once that bug is + // fixed, it can be removed + $ras = array_unique($accessdata['ra'][$con->path]); + } else { + $ras = array(); + } + foreach ($contexts as $ocon) { + $summedpermission = 0; + foreach ($ras as $roleid) { + if (isset($accessdata['rdef'][$ocon->path . ':' . $roleid][$capability])) { + $perm = $accessdata['rdef'][$ocon->path . ':' . $roleid][$capability]; + } else { + $perm = CAP_INHERIT; + } + if ($perm == CAP_PROHIBIT) { + $areprohibits = true; + $decisiveassigncon = 0; + $decisiveoverridecon = 0; + break 3; + } + $summedpermission += $perm; + } + if ($summedpermission) { + $decisiveassigncon = $con->id; + $decisiveoverridecon = $ocon->id; + break 2; + } + } +} +if (!$areprohibits && !$decisiveassigncon) { + $decisiveassigncon = SYSCONTEXTID; + $decisiveoverridecon = SYSCONTEXTID; +} + // Make a fake role to simplify rendering the table below. $rolenames[0] = get_string('none'); @@ -124,7 +165,9 @@ echo ''; // Now print the bulk of the table. foreach ($contexts as $con) { if (!empty($accessdata['ra'][$con->path])) { - $ras = $accessdata['ra'][$con->path]; + // The array_unique here is to work around bug MDL-14817. Once that bug is + // fixed, it can be removed + $ras = array_unique($accessdata['ra'][$con->path]); } else { $ras = array(0); } @@ -151,7 +194,16 @@ foreach ($contexts as $con) { } else { $permission = $strperm[$perm]; } - echo '' . $permission . ''; + $classes = $cssclasses[$perm]; + if (!$areprohibits && $decisiveassigncon == $con->id && $decisiveoverridecon == $ocon->id) { + $classes .= ' decisive'; + if ($userhascapability) { + $classes .= ' has'; + } else { + $classes .= ' hasnot'; + } + } + echo '' . $permission . ''; } echo ''; $firstcell = ''; @@ -162,9 +214,8 @@ echo ''; // Finish the page. echo get_string('explainpermissionsinfo', 'role'); -if ($userid && $capability != 'moodle/site:doanything' && - has_capability('moodle/site:doanything', $context, $userid) && - !has_capability($capability, $context, $userid, false)) { +if ($userid && $capability != 'moodle/site:doanything' && !$userhascapability && + has_capability('moodle/site:doanything', $context, $userid)) { echo '

' . get_string('explainpermissionsdoanything', 'role', $capability) . '

'; } close_window_button(); diff --git a/lib/adminlib.php b/lib/adminlib.php index 9326535e9c..df8c8ab59e 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -6115,3 +6115,134 @@ class admin_setting_managerepository extends admin_setting { return highlight($query, $output); } } + +/** + * This class represents a table with one row for each of a list of capabilities + * where the first cell in the row contains the capability name, and there is + * arbitrary stuff in the rest of the row. This class is used by + * admin/roles/manage.php, override.php and explain.php. There is also some + * ajaxy search UI shown at the top, if JavaScript is on. + */ +abstract class cabability_table_base { + protected $context; + protected $capabilities = array(); + protected $id; + protected $classes = array('rolecap'); + const NUM_CAPS_FOR_SEARCH = 12; + + /** + * Constructor + * @param object $context the context this table relates to. + * @param string $id what to put in the id="" attribute. + */ + public function __construct($context, $id) { + $this->context = $context; + $this->capabilities = fetch_context_capabilities($context); + $this->id = $id; + } + + /** + * Use this to add class="" attributes to the table. You get the rolecap by + * default. + * @param array $classnames of class names. + */ + public function add_classes($classnames) { + $this->classes = array_unique(array_merge($this->classes, $classnames)); + } + + /** + * Display the table. + */ + public function display() { + echo '' . "\n\n"; + echo ''; + $this->add_header_cells(); + echo "\n\n\n"; + + /// Loop over capabilities. + $contextlevel = 0; + $component = ''; + foreach ($this->capabilities as $capability) { + if ($this->skip_row($capability)) { + continue; + } + + /// Prints a breaker if component or name or context level has changed + if (component_level_changed($capability, $component, $contextlevel)) { + $this->print_heading_row($capability); + } + $contextlevel = $capability->contextlevel; + $component = $capability->component; + + /// Start the row. + echo ''; + + /// Table cell for the capability name. + echo ''; + + /// Add the cells specific to this table. + $this->add_row_cells($capability); + + /// End the row. + echo "\n"; + } + + /// End of the table. + echo "\n
' . get_string('capability','role') . '
' . get_capability_docs_link($capability) . + '' . $capability->name . '
\n"; + if (count($this->capabilities) > cabability_table_base::NUM_CAPS_FOR_SEARCH) { + print_js_call('cap_table_filter.init', + array($this->id, get_string('search'), get_string('clear'))); + } + } + + /** + * Used to output a heading rows when the context level or component changes. + * @param object $capability gives the new component and contextlevel. + */ + protected function print_heading_row($capability) { + echo '' . + get_component_string($capability->component, $capability->contextlevel) . + ''; + + } + + /** For subclasses to override, output header cells, after the initial capability one. */ + protected abstract function add_header_cells(); + + /** For subclasses to override, return the number of cells that add_header_cells/add_row_cells output. */ + protected abstract function num_extra_columns(); + + /** + * For subclasses to override. Allows certain capabilties (e.g. legacy capabilities) + * to be left out of the table. + * + * @param object $capability the capability this row relates to. + * @return boolean. If true, this row is omitted from the table. + */ + protected function skip_row($capability) { + return false; + } + + /** + * For subclasses to override. A change to reaturn class names that are added + * to the class="" attribute on the <tr> for this capability. + * + * @param object $capability the capability this row relates to. + * @return array of class name strings. + */ + protected function get_row_classes($capability) { + return array(); + } + + /** + * For subclasses to override. Output the data cells for this capability. The + * capability name cell will already have been output. + * + * You can rely on get_row_classes always being called before add_row_cells. + * + * @param object $capability the capability this row relates to. + */ + protected abstract function add_row_cells($capability); +} \ No newline at end of file diff --git a/theme/standard/styles_color.css b/theme/standard/styles_color.css index ff24cbd0e6..87812b7463 100644 --- a/theme/standard/styles_color.css +++ b/theme/standard/styles_color.css @@ -1169,7 +1169,21 @@ table.quizreviewsummary td.cell { background-color:#FFFFFF; border: 1px solid #cecece; } - +table.explainpermissions .decisive.has, +#explaincaps .rolecap.yes { + background-color: #ddffdd; +} +table.explainpermissions .decisive.hasnot, +#explaincaps .rolecap.no { + background-color: #ffdddd; +} +table.explainpermissions .prohibit { + background-color: #ffbbff; + font-weight: bold; +} +table.explainpermissions .decisive { + font-weight: bold; +} #admin-roles-manage .capdefault { background-color:#dddddd; border: 1px solid #cecece; diff --git a/theme/standard/styles_layout.css b/theme/standard/styles_layout.css index 44a02fc14b..4f99e6a5c9 100644 --- a/theme/standard/styles_layout.css +++ b/theme/standard/styles_layout.css @@ -582,6 +582,11 @@ div.hide { #userselector_options .collapsibleregioncaption { font-weight: bold; } +#userselector_options p { + margin:0.2em 0pt; + text-align:left; +} + /*** *** Forms ***/ @@ -1106,13 +1111,13 @@ body#admin-modules table.generaltable td.c0 font-weight: normal; } -#admin-roles-manage table.roledesc, -#admin-roles-override table.roledesc { +table.roledesc { margin-left:auto; margin-right:auto; } #admin-roles-manage .backlink, +#admin-roles-explain .backlink, #admin-roles-assign .backlink, #admin-roles-override .backlink { text-align: right; @@ -1120,8 +1125,15 @@ body#admin-modules table.generaltable td.c0 margin: 2em auto 1em; } -#admin-roles-manage table.rolecap, -#admin-roles-override table.rolecap { +#admin-roles-explain #chooseuser h3 { + margin-top: 0; +} +#admin-roles-explain #chooseusersubmit { + margin: 1em 0 0; + text-align: center; +} + +table.rolecap { margin-left:auto; margin-right:auto; } @@ -1134,15 +1146,14 @@ table.rolecap .hiddenrow { display: none; } -.rolecap .inherit, -.rolecap .allow, -.rolecap .prevent, -.rolecap .prohibit { +table.rolecap .inherit, +table.rolecap .allow, +table.rolecap .prevent, +table.rolecap .prohibit { text-align:center; } -#admin-roles-manage .rolecap .cap-desc .cap-name, -#admin-roles-override .rolecap .cap-desc .cap-name { +table.rolecap .cap-desc .cap-name { display: block; } diff --git a/user/selector/lib.php b/user/selector/lib.php index ecf4d36e08..dde9405e81 100644 --- a/user/selector/lib.php +++ b/user/selector/lib.php @@ -144,6 +144,24 @@ abstract class user_selector_base { return $this->selected; } + /** + * Convenience method for when multiselect is false (throws an exception if not). + * @return object the selected user object, or null if none. + */ + public function get_selected_user() { + if ($this->multiselect) { + throw new moodle_exception('cannotcallusgetselecteduser'); + } + $users = $this->get_selected_users(); + if (count($users) == 1) { + return reset($users); + } else if (count($users) == 0) { + return null; + } else { + throw new moodle_exception('userselectortoomany'); + } + } + /** * If you update the database in such a way that it is likely to change the * list of users that this component is allowed to select from, then you @@ -721,7 +739,7 @@ class potential_assignees_below_course extends role_assign_user_selector_base { $order = ' ORDER BY lastname ASC, firstname ASC'; $params[] = $this->context->id; - $params[] = $this->roleid; + $params[] = $this->roleid; // Check to see if there are too many to show sensibly. if (!$this->is_validating()) { diff --git a/user/selector/script.js b/user/selector/script.js index 0e8a292741..0ced0bffbc 100644 --- a/user/selector/script.js +++ b/user/selector/script.js @@ -496,6 +496,8 @@ YAHOO.lang.augmentProto(user_selector, YAHOO.util.EventProvider); */ function user_selector_options_tracker() { var oself = this; + + // Add event listeners. YAHOO.util.Event.addListener('userselector_preserveselectedid', 'click', function(e) { oself.handle_option_change('userselector_preserveselected') }); YAHOO.util.Event.addListener('userselector_autoselectuniqueid', 'click', -- 2.39.5