Merge pull request 'Add Instructors admin page (create + per-capability access)' (#30) from feature/instructor-management into main
CI / No Debug Code (push) Successful in 3s
CI / Tests (PHP 8.3) (push) Successful in 45s
CI / Tests (PHP 8.1) (push) Successful in 47s
CI / Tests (PHP 8.2) (push) Successful in 50s
CI / Coding Standards (push) Successful in 54s
CI / PHPStan (push) Successful in 1m5s
CI / Build Plugin Zip (push) Successful in 50s
CI / No Debug Code (push) Successful in 3s
CI / Tests (PHP 8.3) (push) Successful in 45s
CI / Tests (PHP 8.1) (push) Successful in 47s
CI / Tests (PHP 8.2) (push) Successful in 50s
CI / Coding Standards (push) Successful in 54s
CI / PHPStan (push) Successful in 1m5s
CI / Build Plugin Zip (push) Successful in 50s
Reviewed-on: #30
This commit was merged in pull request #30.
This commit is contained in:
@@ -68,10 +68,21 @@ Logs in via the front-end `[us_student_login]` shortcode. Can:
|
||||
|
||||
## Instructor Management
|
||||
The studio admin gets an **Instructors** admin page (gated by `manage_instructors`)
|
||||
to add an instructor — creating the WP user with the `us_instructor` role — and to
|
||||
toggle that instructor's per-capability access (e.g. whether they may manage their
|
||||
own offerings/questions or export payments). The studio admin cannot grant a
|
||||
capability it does not itself hold.
|
||||
to add an instructor — creating the WP user with the `us_instructor` role and
|
||||
emailing them a set-password link — and to toggle that instructor's per-capability
|
||||
access. The managed capabilities are `manage_offerings`, `manage_questions`,
|
||||
`view_own_payments`, and `export_payments`; `manage_availability` and
|
||||
`view_own_lessons` are core to every instructor and are not managed here. The
|
||||
`us_instructor` role grants the managed capabilities by default, and the page
|
||||
stores per-user overrides on top of the role.
|
||||
|
||||
A studio admin cannot grant a capability it does not itself hold: only
|
||||
capabilities the acting user has (checked via `current_user_can()`, so an
|
||||
administrator's dynamic grant counts) are offered as toggles, and on creation any
|
||||
managed capability the admin lacks is denied on the new instructor so they never
|
||||
exceed the creator. The grantable/assignment rules live in the pure, unit-tested
|
||||
`Auth\InstructorCapabilities`; `InstructorController` applies the result to the
|
||||
`WP_User`.
|
||||
|
||||
## Implementation
|
||||
- Class: `Unsupervised\Schedular\Auth\RoleManager`
|
||||
@@ -81,4 +92,4 @@ capability it does not itself hold.
|
||||
|
||||
## Tests
|
||||
- `tests/Unit/Auth/RoleManagerTest.php`
|
||||
- `tests/Unit/Auth/InstructorControllerTest.php`
|
||||
- `tests/Unit/Auth/InstructorCapabilitiesTest.php`
|
||||
|
||||
+16
-1
@@ -6,6 +6,7 @@ namespace Unsupervised\Schedular;
|
||||
use Unsupervised\Schedular\Availability\AvailabilityController;
|
||||
use Unsupervised\Schedular\Availability\AvailabilityRepository;
|
||||
use Unsupervised\Schedular\Auth\AccessSettings;
|
||||
use Unsupervised\Schedular\Auth\InstructorController;
|
||||
use Unsupervised\Schedular\Auth\InviteRepository;
|
||||
use Unsupervised\Schedular\Auth\RegistrationController;
|
||||
use Unsupervised\Schedular\Auth\RoleManager;
|
||||
@@ -39,6 +40,7 @@ class AdminMenu {
|
||||
private RegistrationController $registrationController;
|
||||
private GroupClassController $groupClassController;
|
||||
private StudentController $studentController;
|
||||
private InstructorController $instructorController;
|
||||
private StudioSettings $settings;
|
||||
private AccessSettings $accessSettings;
|
||||
private PaymentController $paymentController;
|
||||
@@ -53,6 +55,7 @@ class AdminMenu {
|
||||
$this->registrationController = new RegistrationController( $invites );
|
||||
$this->groupClassController = new GroupClassController( $enrollments, $offerings );
|
||||
$this->studentController = new StudentController( $bookings, $availability, $offerings, $enrollments, $resolver );
|
||||
$this->instructorController = new InstructorController();
|
||||
$this->settings = $settings;
|
||||
$this->accessSettings = new AccessSettings();
|
||||
$this->paymentController = new PaymentController( $payments, $paymentService );
|
||||
@@ -143,6 +146,17 @@ class AdminMenu {
|
||||
36
|
||||
);
|
||||
|
||||
// Studio admin: create instructors and manage their capabilities.
|
||||
add_menu_page(
|
||||
__( 'Instructors', 'unsupervised-schedular' ),
|
||||
__( 'Instructors', 'unsupervised-schedular' ),
|
||||
RoleManager::CAP_MANAGE_INSTRUCTORS,
|
||||
InstructorController::PAGE_SLUG,
|
||||
[ $this->instructorController, 'renderPage' ],
|
||||
'dashicons-businessperson',
|
||||
34.5
|
||||
);
|
||||
|
||||
// Studio admin: browse students and their activity.
|
||||
add_menu_page(
|
||||
__( 'Students', 'unsupervised-schedular' ),
|
||||
@@ -244,7 +258,8 @@ class AdminMenu {
|
||||
}
|
||||
|
||||
private function userSeesPeopleSection(): bool {
|
||||
return current_user_can( RoleManager::CAP_MANAGE_STUDENTS )
|
||||
return current_user_can( RoleManager::CAP_MANAGE_INSTRUCTORS )
|
||||
|| current_user_can( RoleManager::CAP_MANAGE_STUDENTS )
|
||||
|| current_user_can( RoleManager::CAP_VIEW_ALL_LESSONS )
|
||||
|| current_user_can( RoleManager::CAP_MANAGE_BILLING );
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Auth;
|
||||
|
||||
/**
|
||||
* Pure capability logic for the Instructors admin page: which instructor
|
||||
* capabilities a studio admin may toggle per instructor, and how a submitted form
|
||||
* maps onto capability assignments. Kept free of WordPress so it can be unit
|
||||
* tested; {@see InstructorController} applies the result to a `WP_User`.
|
||||
*/
|
||||
class InstructorCapabilities {
|
||||
|
||||
/**
|
||||
* Instructor capabilities a studio admin can switch on or off per instructor.
|
||||
* The `us_instructor` role grants these by default; the remaining instructor
|
||||
* capabilities (`manage_availability`, `view_own_lessons`) are core to being an
|
||||
* instructor and are not managed here.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public const MANAGED = [
|
||||
RoleManager::CAP_MANAGE_OFFERINGS,
|
||||
RoleManager::CAP_MANAGE_QUESTIONS,
|
||||
RoleManager::CAP_VIEW_OWN_PAYMENTS,
|
||||
RoleManager::CAP_EXPORT_PAYMENTS,
|
||||
];
|
||||
|
||||
/**
|
||||
* The managed capabilities the acting studio admin is allowed to assign — a
|
||||
* studio admin can never grant a capability it does not itself hold.
|
||||
*
|
||||
* @param array<string, bool> $adminCaps The acting user's capabilities (cap => held).
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function grantable( array $adminCaps ): array {
|
||||
return array_values(
|
||||
array_filter(
|
||||
self::MANAGED,
|
||||
static fn( string $cap ): bool => ! empty( $adminCaps[ $cap ] )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map submitted checkbox capability names onto the assignment to persist. Only
|
||||
* grantable capabilities are considered, so a request can neither grant a
|
||||
* capability the admin lacks nor one outside the managed set. Every grantable
|
||||
* capability is returned (true when submitted, false otherwise) so the caller
|
||||
* can apply each as an explicit per-user grant or denial.
|
||||
*
|
||||
* @param list<string> $submitted Capability names checked in the form.
|
||||
* @param list<string> $grantable Capabilities the admin may assign.
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
public static function resolve( array $submitted, array $grantable ): array {
|
||||
$resolved = [];
|
||||
foreach ( $grantable as $cap ) {
|
||||
$resolved[ $cap ] = in_array( $cap, $submitted, true );
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Auth;
|
||||
|
||||
/**
|
||||
* Studio-admin **Instructors** page: create instructor accounts and toggle each
|
||||
* instructor's managed capabilities. Gated on `manage_instructors`. A studio
|
||||
* admin can never grant a capability it does not itself hold; the testable rules
|
||||
* for that live in {@see InstructorCapabilities}.
|
||||
*/
|
||||
class InstructorController {
|
||||
|
||||
public const PAGE_SLUG = 'us-instructors';
|
||||
|
||||
public function renderPage(): void {
|
||||
if ( ! current_user_can( RoleManager::CAP_MANAGE_INSTRUCTORS ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to manage instructors.', 'unsupervised-schedular' ) );
|
||||
}
|
||||
|
||||
$notice = '';
|
||||
if ( isset( $_POST['usc_action'] ) && check_admin_referer( 'usc_instructor_action' ) ) {
|
||||
$notice = $this->handleFormAction();
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only instructor selector.
|
||||
$instructorId = absint( $_GET['instructor_id'] ?? 0 );
|
||||
$instructor = $instructorId > 0 ? get_userdata( $instructorId ) : false;
|
||||
|
||||
if ( $instructor && in_array( RoleManager::INSTRUCTOR, (array) $instructor->roles, true ) ) {
|
||||
$grantable = $this->grantableCaps();
|
||||
|
||||
$capabilities = [];
|
||||
foreach ( InstructorCapabilities::MANAGED as $cap ) {
|
||||
$capabilities[ $cap ] = [
|
||||
'granted' => user_can( $instructor, $cap ),
|
||||
'grantable' => in_array( $cap, $grantable, true ),
|
||||
];
|
||||
}
|
||||
|
||||
$backUrl = admin_url( 'admin.php?page=' . self::PAGE_SLUG );
|
||||
include USC_PLUGIN_DIR . 'templates/admin/instructor-detail.php';
|
||||
return;
|
||||
}
|
||||
|
||||
$instructors = array_map(
|
||||
static fn( \WP_User $user ): array => [
|
||||
'id' => (int) $user->ID,
|
||||
'name' => $user->display_name,
|
||||
'email' => $user->user_email,
|
||||
'registered' => $user->user_registered,
|
||||
],
|
||||
get_users(
|
||||
[
|
||||
'role' => RoleManager::INSTRUCTOR,
|
||||
'orderby' => 'display_name',
|
||||
'order' => 'ASC',
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$pageSlug = self::PAGE_SLUG;
|
||||
include USC_PLUGIN_DIR . 'templates/admin/instructors.php';
|
||||
}
|
||||
|
||||
private function handleFormAction(): string {
|
||||
// Nonce is verified by the caller (renderPage) before this method runs.
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$action = sanitize_key( wp_unslash( $_POST['usc_action'] ?? '' ) );
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
|
||||
if ( 'create' === $action ) {
|
||||
return $this->createInstructor();
|
||||
}
|
||||
|
||||
if ( 'update_caps' === $action ) {
|
||||
return $this->updateCaps();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function createInstructor(): string {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
|
||||
$name = sanitize_text_field( wp_unslash( $_POST['display_name'] ?? '' ) );
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
|
||||
if ( ! is_email( $email ) ) {
|
||||
return __( 'Enter a valid email address.', 'unsupervised-schedular' );
|
||||
}
|
||||
|
||||
if ( false !== email_exists( $email ) ) {
|
||||
return __( 'A user with that email already exists.', 'unsupervised-schedular' );
|
||||
}
|
||||
|
||||
$userId = wp_insert_user(
|
||||
[
|
||||
'user_login' => $email,
|
||||
'user_email' => $email,
|
||||
'display_name' => '' !== $name ? $name : $email,
|
||||
'user_pass' => wp_generate_password( 24 ),
|
||||
'role' => RoleManager::INSTRUCTOR,
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $userId ) ) {
|
||||
return __( 'Could not create the instructor account.', 'unsupervised-schedular' );
|
||||
}
|
||||
|
||||
// Never let a new instructor exceed the creating admin's own capabilities:
|
||||
// the role grants every managed capability, so deny any the admin lacks.
|
||||
$grantable = $this->grantableCaps();
|
||||
$user = new \WP_User( $userId );
|
||||
foreach ( InstructorCapabilities::MANAGED as $cap ) {
|
||||
if ( ! in_array( $cap, $grantable, true ) ) {
|
||||
$user->add_cap( $cap, false );
|
||||
}
|
||||
}
|
||||
|
||||
wp_new_user_notification( $userId, null, 'user' );
|
||||
|
||||
return __( 'Instructor created and emailed a link to set their password.', 'unsupervised-schedular' );
|
||||
}
|
||||
|
||||
private function updateCaps(): string {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$instructorId = absint( $_POST['instructor_id'] ?? 0 );
|
||||
$submitted = array_map( 'sanitize_key', (array) wp_unslash( $_POST['capabilities'] ?? [] ) );
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
|
||||
$instructor = $instructorId > 0 ? get_userdata( $instructorId ) : false;
|
||||
if ( ! $instructor || ! in_array( RoleManager::INSTRUCTOR, (array) $instructor->roles, true ) ) {
|
||||
return __( 'Instructor not found.', 'unsupervised-schedular' );
|
||||
}
|
||||
|
||||
foreach ( InstructorCapabilities::resolve( $submitted, $this->grantableCaps() ) as $cap => $granted ) {
|
||||
$instructor->add_cap( $cap, $granted );
|
||||
}
|
||||
|
||||
return __( 'Capabilities updated.', 'unsupervised-schedular' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Managed capabilities the acting user may assign. Uses `current_user_can()`
|
||||
* so capabilities the administrator holds only via the dynamic studio grant
|
||||
* still count.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function grantableCaps(): array {
|
||||
$caps = [];
|
||||
foreach ( InstructorCapabilities::MANAGED as $cap ) {
|
||||
$caps[ $cap ] = current_user_can( $cap );
|
||||
}
|
||||
|
||||
return InstructorCapabilities::grantable( $caps );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (! defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var \WP_User $instructor
|
||||
* @var array<string, array{granted: bool, grantable: bool}> $capabilities
|
||||
* @var string $backUrl
|
||||
* @var string $notice
|
||||
*/
|
||||
|
||||
$capabilityLabels = [
|
||||
'manage_offerings' => __('Manage their own offerings', 'unsupervised-schedular'),
|
||||
'manage_questions' => __('Manage their own intake questions', 'unsupervised-schedular'),
|
||||
'view_own_payments' => __('View their own payments report', 'unsupervised-schedular'),
|
||||
'export_payments' => __('Export their own payments', 'unsupervised-schedular'),
|
||||
];
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php echo esc_html($instructor->display_name); ?></h1>
|
||||
<p><a href="<?php echo esc_url($backUrl); ?>">← <?php esc_html_e('Back to instructors', 'unsupervised-schedular'); ?></a></p>
|
||||
|
||||
<?php if ('' !== $notice) : ?>
|
||||
<div class="notice notice-info inline"><p><?php echo esc_html($notice); ?></p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p><?php echo esc_html($instructor->user_email); ?></p>
|
||||
|
||||
<h2><?php esc_html_e('Capabilities', 'unsupervised-schedular'); ?></h2>
|
||||
<p class="description"><?php esc_html_e('Every instructor manages their own availability and sees their own lessons. The options below are set per instructor — ones you do not hold yourself cannot be changed.', 'unsupervised-schedular'); ?></p>
|
||||
<form method="post">
|
||||
<?php wp_nonce_field('usc_instructor_action'); ?>
|
||||
<input type="hidden" name="usc_action" value="update_caps">
|
||||
<input type="hidden" name="instructor_id" value="<?php echo esc_attr((string) $instructor->ID); ?>">
|
||||
<table class="form-table">
|
||||
<?php foreach ($capabilities as $cap => $state) : ?>
|
||||
<tr>
|
||||
<th scope="row"><?php echo esc_html($capabilityLabels[$cap] ?? $cap); ?></th>
|
||||
<td>
|
||||
<?php if ($state['grantable']) : ?>
|
||||
<label>
|
||||
<input type="checkbox" name="capabilities[]" value="<?php echo esc_attr($cap); ?>" <?php checked($state['granted']); ?>>
|
||||
<?php esc_html_e('Allowed', 'unsupervised-schedular'); ?>
|
||||
</label>
|
||||
<?php else : ?>
|
||||
<span class="description">
|
||||
<?php echo $state['granted'] ? esc_html__('Allowed', 'unsupervised-schedular') : esc_html__('Not allowed', 'unsupervised-schedular'); ?>
|
||||
— <?php esc_html_e('you cannot change this because you do not hold this capability yourself.', 'unsupervised-schedular'); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php submit_button(esc_html__('Save Capabilities', 'unsupervised-schedular')); ?>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (! defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var list<array{id: int, name: string, email: string, registered: string}> $instructors
|
||||
* @var string $pageSlug
|
||||
* @var string $notice
|
||||
*/
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e('Instructors', 'unsupervised-schedular'); ?></h1>
|
||||
|
||||
<?php if ('' !== $notice) : ?>
|
||||
<div class="notice notice-info inline"><p><?php echo esc_html($notice); ?></p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h2><?php esc_html_e('Add an instructor', 'unsupervised-schedular'); ?></h2>
|
||||
<form method="post">
|
||||
<?php wp_nonce_field('usc_instructor_action'); ?>
|
||||
<input type="hidden" name="usc_action" value="create">
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="display_name"><?php esc_html_e('Name', 'unsupervised-schedular'); ?></label></th>
|
||||
<td><input type="text" name="display_name" id="display_name" class="regular-text"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="email"><?php esc_html_e('Email', 'unsupervised-schedular'); ?></label></th>
|
||||
<td>
|
||||
<input type="email" name="email" id="email" class="regular-text" required>
|
||||
<p class="description"><?php esc_html_e('Used as the login. The instructor is emailed a link to set their password.', 'unsupervised-schedular'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php submit_button(esc_html__('Add Instructor', 'unsupervised-schedular')); ?>
|
||||
</form>
|
||||
|
||||
<h2><?php esc_html_e('Current instructors', 'unsupervised-schedular'); ?></h2>
|
||||
<?php if (empty($instructors)) : ?>
|
||||
<p><?php esc_html_e('No instructors yet.', 'unsupervised-schedular'); ?></p>
|
||||
<?php else : ?>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e('Name', 'unsupervised-schedular'); ?></th>
|
||||
<th><?php esc_html_e('Email', 'unsupervised-schedular'); ?></th>
|
||||
<th><?php esc_html_e('Registered', 'unsupervised-schedular'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($instructors as $instructor) : ?>
|
||||
<?php $detailUrl = add_query_arg(['page' => $pageSlug, 'instructor_id' => $instructor['id']], admin_url('admin.php')); ?>
|
||||
<tr>
|
||||
<td><a href="<?php echo esc_url($detailUrl); ?>"><?php echo esc_html($instructor['name']); ?></a></td>
|
||||
<td><?php echo esc_html($instructor['email']); ?></td>
|
||||
<td><?php echo esc_html($instructor['registered']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Auth;
|
||||
|
||||
use Unsupervised\Schedular\Auth\InstructorCapabilities;
|
||||
use Unsupervised\Schedular\Auth\RoleManager;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class InstructorCapabilitiesTest extends TestCase
|
||||
{
|
||||
public function testGrantableReturnsOnlyManagedCapsTheAdminHolds(): void
|
||||
{
|
||||
$grantable = InstructorCapabilities::grantable([
|
||||
RoleManager::CAP_MANAGE_OFFERINGS => true,
|
||||
RoleManager::CAP_EXPORT_PAYMENTS => true,
|
||||
// A non-instructor cap the admin holds is irrelevant here.
|
||||
RoleManager::CAP_MANAGE_POLICIES => true,
|
||||
]);
|
||||
|
||||
self::assertContains(RoleManager::CAP_MANAGE_OFFERINGS, $grantable);
|
||||
self::assertContains(RoleManager::CAP_EXPORT_PAYMENTS, $grantable);
|
||||
self::assertNotContains(RoleManager::CAP_MANAGE_QUESTIONS, $grantable);
|
||||
self::assertNotContains(RoleManager::CAP_MANAGE_POLICIES, $grantable);
|
||||
}
|
||||
|
||||
public function testGrantableExcludesCapsTheAdminLacksOrHasDisabled(): void
|
||||
{
|
||||
self::assertSame([], InstructorCapabilities::grantable([
|
||||
RoleManager::CAP_MANAGE_OFFERINGS => false,
|
||||
]));
|
||||
|
||||
self::assertSame([], InstructorCapabilities::grantable([]));
|
||||
}
|
||||
|
||||
public function testResolveMapsEachGrantableCapToWhetherItWasSubmitted(): void
|
||||
{
|
||||
$resolved = InstructorCapabilities::resolve(
|
||||
[RoleManager::CAP_MANAGE_OFFERINGS],
|
||||
[RoleManager::CAP_MANAGE_OFFERINGS, RoleManager::CAP_EXPORT_PAYMENTS]
|
||||
);
|
||||
|
||||
self::assertTrue($resolved[RoleManager::CAP_MANAGE_OFFERINGS]);
|
||||
self::assertFalse($resolved[RoleManager::CAP_EXPORT_PAYMENTS]);
|
||||
}
|
||||
|
||||
public function testResolveIgnoresSubmittedCapsOutsideTheGrantableSet(): void
|
||||
{
|
||||
// The form posts a cap the admin may not grant; it must be dropped, so a
|
||||
// studio admin can never assign a capability it does not itself hold.
|
||||
$resolved = InstructorCapabilities::resolve(
|
||||
[RoleManager::CAP_MANAGE_QUESTIONS, RoleManager::CAP_MANAGE_POLICIES],
|
||||
[RoleManager::CAP_MANAGE_OFFERINGS]
|
||||
);
|
||||
|
||||
self::assertSame([RoleManager::CAP_MANAGE_OFFERINGS => false], $resolved);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user