Make WP admins instructors too, and add an Access toggle page
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.2) (pull_request) Successful in 41s
CI / Tests (PHP 8.3) (pull_request) Successful in 51s
CI / Tests (PHP 8.1) (pull_request) Successful in 54s
CI / Coding Standards (pull_request) Successful in 58s
CI / PHPStan (pull_request) Successful in 1m9s
CI / Build Plugin Zip (pull_request) Has been skipped

A WordPress administrator previously inherited the studio-admin
capabilities but not `manage_availability`, so the studio owner running
as an admin had no way to reach "My Availability" or act as the
instructor — breaking single-instructor businesses.

Grant the instructor capabilities to administrators as well (via the
existing `user_has_cap` filter), and make both grants — studio-admin and
instructor — independently toggleable from a new Access admin page.

- RoleManager: extract `INSTRUCTOR_CAPS`; apply studio and instructor
  cap sets to administrators, each gated on a stored toggle (default on).
- AccessSettings + templates/admin/access.php: two options
  (`us_admin_grant_studio` / `us_admin_grant_instructor`), gated on the
  core `manage_options` capability so disabling a grant can never lock an
  administrator out of re-enabling it.
- AdminMenu: register the Access page after Studio Settings; keep the
  studio sidebar separator visible for any administrator.
- Tests for the toggles and the new settings reader; docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 16:39:41 -03:00
parent 0a78f4b1ac
commit 67f8144a4a
7 changed files with 279 additions and 22 deletions
+22
View File
@@ -5,6 +5,7 @@ namespace Unsupervised\Schedular;
use Unsupervised\Schedular\Availability\AvailabilityController;
use Unsupervised\Schedular\Availability\AvailabilityRepository;
use Unsupervised\Schedular\Auth\AccessSettings;
use Unsupervised\Schedular\Auth\InviteRepository;
use Unsupervised\Schedular\Auth\RegistrationController;
use Unsupervised\Schedular\Auth\RoleManager;
@@ -39,6 +40,7 @@ class AdminMenu {
private GroupClassController $groupClassController;
private StudentController $studentController;
private StudioSettings $settings;
private AccessSettings $accessSettings;
private PaymentController $paymentController;
private PaymentReportController $paymentReportController;
@@ -52,6 +54,7 @@ class AdminMenu {
$this->groupClassController = new GroupClassController( $enrollments, $offerings );
$this->studentController = new StudentController( $bookings, $availability, $offerings, $enrollments, $resolver );
$this->settings = $settings;
$this->accessSettings = new AccessSettings();
$this->paymentController = new PaymentController( $payments, $paymentService );
$this->paymentReportController = new PaymentReportController( $payments );
}
@@ -185,6 +188,19 @@ class AdminMenu {
30
);
// Site owner: whether WordPress administrators are studio admins / instructors.
// Gated on the core manage_options capability — never the plugin's own grants —
// so an administrator can always reach it to re-enable a disabled grant.
add_menu_page(
__( 'Access', 'unsupervised-schedular' ),
__( 'Access', 'unsupervised-schedular' ),
'manage_options',
'us-access',
[ $this->accessSettings, 'renderPage' ],
'dashicons-admin-network',
30.5
);
// Instructor: view their upcoming lessons.
add_menu_page(
__( 'My Lessons', 'unsupervised-schedular' ),
@@ -234,6 +250,12 @@ class AdminMenu {
}
private function userSeesStudioMenu(): bool {
// Administrators always see the Access page, so the leading separator
// should show for them even if both capability grants are disabled.
if ( current_user_can( 'manage_options' ) ) {
return true;
}
$caps = [
RoleManager::CAP_VIEW_ALL_LESSONS,
RoleManager::CAP_VIEW_LESSONS,
+67
View File
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Unsupervised\Schedular\Auth;
/**
* Site-owner toggles for whether WordPress administrators automatically receive
* the studio-admin and/or instructor capabilities.
*
* Both default on, preserving the out-of-the-box experience where a single
* administrator runs the studio and teaches from one account. The settings page
* is gated on `manage_options` (the core WordPress administrator capability,
* which the plugin never grants or revokes) so an administrator can always reach
* it to re-enable a grant — disabling one can never lock them out.
*/
class AccessSettings {
public const OPT_GRANT_STUDIO = 'us_admin_grant_studio';
public const OPT_GRANT_INSTRUCTOR = 'us_admin_grant_instructor';
/**
* Whether WordPress administrators implicitly hold the studio-admin
* capabilities (offerings, policies, billing, reports, …).
*/
public function adminsAreStudioAdmins(): bool {
return $this->flag( self::OPT_GRANT_STUDIO );
}
/**
* Whether WordPress administrators implicitly hold the instructor
* capabilities (manage their own availability and lessons).
*/
public function adminsAreInstructors(): bool {
return $this->flag( self::OPT_GRANT_INSTRUCTOR );
}
/**
* Read a stored toggle, defaulting to on so a fresh install keeps the
* single-account behaviour.
*/
private function flag( string $option ): bool {
return '0' !== (string) get_option( $option, '1' );
}
public function renderPage(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to manage access settings.', 'unsupervised-schedular' ) );
}
if ( isset( $_POST['usc_action'] ) && check_admin_referer( 'usc_access_action' ) ) {
$this->save();
}
$adminsAreStudioAdmins = $this->adminsAreStudioAdmins();
$adminsAreInstructors = $this->adminsAreInstructors();
include USC_PLUGIN_DIR . 'templates/admin/access.php';
}
private function save(): void {
// Nonce is verified by the caller (renderPage) before this method runs.
// phpcs:disable WordPress.Security.NonceVerification.Missing
update_option( self::OPT_GRANT_STUDIO, isset( $_POST['grant_studio'] ) ? '1' : '0' );
update_option( self::OPT_GRANT_INSTRUCTOR, isset( $_POST['grant_instructor'] ) ? '1' : '0' );
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
}
+44 -14
View File
@@ -42,6 +42,26 @@ class RoleManager {
self::CAP_EXPORT_PAYMENTS,
];
/**
* Capabilities granted to the `us_instructor` role, and implicitly to any
* WordPress administrator (see {@see grantStudioCapsToAdministrators()}) so a
* single-instructor studio owner can both run the business and teach from one
* account — managing their own availability and lessons without being assigned
* a separate `us_instructor` role.
*
* @var list<string>
*/
public const INSTRUCTOR_CAPS = [
self::CAP_MANAGE_AVAILABILITY,
self::CAP_MANAGE_OFFERINGS,
self::CAP_MANAGE_QUESTIONS,
self::CAP_VIEW_LESSONS,
self::CAP_VIEW_OWN_PAYMENTS,
self::CAP_EXPORT_PAYMENTS,
];
public function __construct( private AccessSettings $access = new AccessSettings() ) {}
public function register(): void {
add_action( 'init', [ $this, 'createRoles' ] );
add_filter( 'user_has_cap', [ $this, 'grantStudioCapsToAdministrators' ], 10, 1 );
@@ -62,18 +82,15 @@ class RoleManager {
}
if ( get_role( self::INSTRUCTOR ) === null ) {
$instructorCaps = [ 'read' => true ];
foreach ( self::INSTRUCTOR_CAPS as $cap ) {
$instructorCaps[ $cap ] = true;
}
add_role(
self::INSTRUCTOR,
__( 'Instructor', 'unsupervised-schedular' ),
[
'read' => true,
self::CAP_MANAGE_AVAILABILITY => true,
self::CAP_MANAGE_OFFERINGS => true,
self::CAP_MANAGE_QUESTIONS => true,
self::CAP_VIEW_LESSONS => true,
self::CAP_VIEW_OWN_PAYMENTS => true,
self::CAP_EXPORT_PAYMENTS => true,
]
$instructorCaps
);
}
@@ -95,9 +112,14 @@ class RoleManager {
*
* The studio owner runs the site as an administrator (`manage_options`) and
* should manage offerings, questions, policies, billing, and reports without
* being assigned the separate `us_studio_admin` role. Applied dynamically via
* the `user_has_cap` filter, so nothing is persisted and the grant disappears
* when the plugin is deactivated.
* being assigned the separate `us_studio_admin` role. The instructor
* capabilities are granted too, so a single-instructor studio owner can manage
* their own availability and lessons and act as the instructor from the same
* account. Applied dynamically via the `user_has_cap` filter, so nothing is
* persisted and the grant disappears when the plugin is deactivated.
*
* Both grants are independently toggleable from the Access settings page (see
* {@see AccessSettings}); they default on, preserving the single-account setup.
*
* @param array<string, bool> $allcaps All capabilities currently held by the user.
* @return array<string, bool>
@@ -107,8 +129,16 @@ class RoleManager {
return $allcaps;
}
foreach ( self::STUDIO_ADMIN_CAPS as $cap ) {
$allcaps[ $cap ] = true;
if ( $this->access->adminsAreStudioAdmins() ) {
foreach ( self::STUDIO_ADMIN_CAPS as $cap ) {
$allcaps[ $cap ] = true;
}
}
if ( $this->access->adminsAreInstructors() ) {
foreach ( self::INSTRUCTOR_CAPS as $cap ) {
$allcaps[ $cap ] = true;
}
}
return $allcaps;