Grant studio-admin caps to administrators; re-gate Scheduler; mark 1.0.0-rc.1 #13

Merged
thatguygriff merged 2 commits from feature/admin-studio-caps into main 2026-06-05 15:13:29 +00:00
6 changed files with 85 additions and 16 deletions
Showing only changes of commit b4acae34a3 - Show all commits
+1 -1
View File
@@ -53,7 +53,7 @@ Group classes follow the same registration flow but enrol against an offering of
kind `group_class`; see `group-classes.md`. kind `group_class`; see `group-classes.md`.
## Admin Interface ## Admin Interface
- **Scheduler** (`manage_options` only): all upcoming lessons across all instructors - **Scheduler** (`view_all_lessons` — studio admin / administrators): all upcoming lessons across all instructors
- **My Lessons** (`view_own_lessons`): upcoming lessons for the logged-in instructor - **My Lessons** (`view_own_lessons`): upcoming lessons for the logged-in instructor
## Frontend Shortcodes ## Frontend Shortcodes
+10 -1
View File
@@ -10,9 +10,17 @@ Runs the studio. Logs in via standard wp-admin. Can:
- Create instructor accounts and set/revoke each instructor's capabilities - Create instructor accounts and set/revoke each instructor's capabilities
- Manage offerings, intake questions, and policies - Manage offerings, intake questions, and policies
- Configure Stripe credentials and per-student billing overrides (card / e-transfer / comp) - Configure Stripe credentials and per-student billing overrides (card / e-transfer / comp)
- View the studio-wide scheduler (all upcoming lessons across instructors)
- View the all-instructor payments report and export it - View the all-instructor payments report and export it
**Capabilities:** `read`, `manage_instructors`, `manage_offerings`, `manage_questions`, `manage_policies`, `manage_billing`, `view_all_payments`, `export_payments` **Capabilities:** `read`, `manage_instructors`, `manage_offerings`, `manage_questions`, `manage_policies`, `manage_billing`, `view_all_lessons`, `view_all_payments`, `export_payments`
> Any WordPress **administrator** (`manage_options`) implicitly holds every
> studio-admin capability above, so the site owner runs the studio without being
> assigned the `us_studio_admin` role. This is applied dynamically via the
> `user_has_cap` filter (`RoleManager::grantStudioCapsToAdministrators()`) — it
> persists nothing and is removed when the plugin is deactivated. The
> `us_studio_admin` role exists for non-administrator staff who manage the studio.
### Instructor (`us_instructor`) ### Instructor (`us_instructor`)
Created by the studio admin. Logs in via standard wp-admin. Can: Created by the studio admin. Logs in via standard wp-admin. Can:
@@ -41,6 +49,7 @@ Logs in via the front-end `[us_student_login]` shortcode. Can:
| `manage_policies` | ✓ | | | Policies | | `manage_policies` | ✓ | | | Policies |
| `manage_billing` | ✓ | | | Payments (Stripe + overrides) | | `manage_billing` | ✓ | | | Payments (Stripe + overrides) |
| `book_lesson` | | | ✓ | Lesson booking / enrolment | | `book_lesson` | | | ✓ | Lesson booking / enrolment |
| `view_all_lessons` | ✓ | | | Scheduler dashboard |
| `view_own_lessons` | | ✓ | ✓ | Lesson + group views | | `view_own_lessons` | | ✓ | ✓ | Lesson + group views |
| `view_own_payments` | | ✓ | | Payment reporting | | `view_own_payments` | | ✓ | | Payment reporting |
| `view_all_payments` | ✓ | | | Payment reporting | | `view_all_payments` | ✓ | | | Payment reporting |
+2 -2
View File
@@ -32,11 +32,11 @@ class AdminMenu {
} }
public function addPages(): void { public function addPages(): void {
// Admin-only dashboard: all upcoming lessons. // Studio-wide dashboard: all upcoming lessons across instructors.
add_menu_page( add_menu_page(
__( 'Scheduler', 'unsupervised-schedular' ), __( 'Scheduler', 'unsupervised-schedular' ),
__( 'Scheduler', 'unsupervised-schedular' ), __( 'Scheduler', 'unsupervised-schedular' ),
'manage_options', RoleManager::CAP_VIEW_ALL_LESSONS,
'us-scheduler', 'us-scheduler',
[ $this->lessonController, 'renderAdminDashboard' ], [ $this->lessonController, 'renderAdminDashboard' ],
'dashicons-calendar-alt', 'dashicons-calendar-alt',
+49 -10
View File
@@ -18,29 +18,44 @@ class RoleManager {
public const CAP_MANAGE_QUESTIONS = 'manage_questions'; public const CAP_MANAGE_QUESTIONS = 'manage_questions';
public const CAP_MANAGE_POLICIES = 'manage_policies'; public const CAP_MANAGE_POLICIES = 'manage_policies';
public const CAP_MANAGE_BILLING = 'manage_billing'; public const CAP_MANAGE_BILLING = 'manage_billing';
public const CAP_VIEW_ALL_LESSONS = 'view_all_lessons';
public const CAP_VIEW_ALL_PAYMENTS = 'view_all_payments'; public const CAP_VIEW_ALL_PAYMENTS = 'view_all_payments';
public const CAP_VIEW_OWN_PAYMENTS = 'view_own_payments'; public const CAP_VIEW_OWN_PAYMENTS = 'view_own_payments';
public const CAP_EXPORT_PAYMENTS = 'export_payments'; public const CAP_EXPORT_PAYMENTS = 'export_payments';
/**
* Capabilities granted to the `us_studio_admin` role, and implicitly to any
* WordPress administrator (see {@see grantStudioCapsToAdministrators()}).
*
* @var list<string>
*/
public const STUDIO_ADMIN_CAPS = [
self::CAP_MANAGE_INSTRUCTORS,
self::CAP_MANAGE_OFFERINGS,
self::CAP_MANAGE_QUESTIONS,
self::CAP_MANAGE_POLICIES,
self::CAP_MANAGE_BILLING,
self::CAP_VIEW_ALL_LESSONS,
self::CAP_VIEW_ALL_PAYMENTS,
self::CAP_EXPORT_PAYMENTS,
];
public function register(): void { public function register(): void {
add_action( 'init', [ $this, 'createRoles' ] ); add_action( 'init', [ $this, 'createRoles' ] );
add_filter( 'user_has_cap', [ $this, 'grantStudioCapsToAdministrators' ], 10, 1 );
} }
public function createRoles(): void { public function createRoles(): void {
if ( get_role( self::STUDIO_ADMIN ) === null ) { if ( get_role( self::STUDIO_ADMIN ) === null ) {
$studioCaps = [ 'read' => true ];
foreach ( self::STUDIO_ADMIN_CAPS as $cap ) {
$studioCaps[ $cap ] = true;
}
add_role( add_role(
self::STUDIO_ADMIN, self::STUDIO_ADMIN,
__( 'Studio Admin', 'unsupervised-schedular' ), __( 'Studio Admin', 'unsupervised-schedular' ),
[ $studioCaps
'read' => true,
self::CAP_MANAGE_INSTRUCTORS => true,
self::CAP_MANAGE_OFFERINGS => true,
self::CAP_MANAGE_QUESTIONS => true,
self::CAP_MANAGE_POLICIES => true,
self::CAP_MANAGE_BILLING => true,
self::CAP_VIEW_ALL_PAYMENTS => true,
self::CAP_EXPORT_PAYMENTS => true,
]
); );
} }
@@ -72,4 +87,28 @@ class RoleManager {
); );
} }
} }
/**
* Grant every studio-admin capability to WordPress administrators.
*
* 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.
*
* @param array<string, bool> $allcaps All capabilities currently held by the user.
* @return array<string, bool>
*/
public function grantStudioCapsToAdministrators( array $allcaps ): array {
if ( empty( $allcaps['manage_options'] ) ) {
return $allcaps;
}
foreach ( self::STUDIO_ADMIN_CAPS as $cap ) {
$allcaps[ $cap ] = true;
}
return $allcaps;
}
} }
+1 -1
View File
@@ -10,7 +10,7 @@ class LessonController {
public function __construct( private BookingRepository $repository ) {} public function __construct( private BookingRepository $repository ) {}
public function renderAdminDashboard(): void { public function renderAdminDashboard(): void {
if ( ! current_user_can( 'manage_options' ) ) { if ( ! current_user_can( RoleManager::CAP_VIEW_ALL_LESSONS ) ) {
wp_die( esc_html__( 'You do not have permission to view this page.', 'unsupervised-schedular' ) ); wp_die( esc_html__( 'You do not have permission to view this page.', 'unsupervised-schedular' ) );
} }
+22 -1
View File
@@ -9,15 +9,36 @@ use Unsupervised\Schedular\Tests\Unit\TestCase;
class RoleManagerTest extends TestCase class RoleManagerTest extends TestCase
{ {
public function testRegisterAddsInitHook(): void public function testRegisterAddsInitHookAndCapFilter(): void
{ {
Functions\expect('add_action') Functions\expect('add_action')
->once() ->once()
->with('init', \Mockery::any()); ->with('init', \Mockery::any());
Functions\expect('add_filter')
->once()
->with('user_has_cap', \Mockery::any(), 10, 1);
(new RoleManager())->register(); (new RoleManager())->register();
} }
public function testGrantsStudioCapsToAdministrators(): void
{
$result = (new RoleManager())->grantStudioCapsToAdministrators(['manage_options' => true]);
foreach (RoleManager::STUDIO_ADMIN_CAPS as $cap) {
self::assertTrue($result[$cap], "administrator should be granted {$cap}");
}
}
public function testDoesNotGrantStudioCapsToNonAdministrators(): void
{
$result = (new RoleManager())->grantStudioCapsToAdministrators(['read' => true]);
self::assertArrayNotHasKey(RoleManager::CAP_MANAGE_OFFERINGS, $result);
self::assertSame(['read' => true], $result);
}
public function testCreateRolesSkipsExistingRoles(): void public function testCreateRolesSkipsExistingRoles(): void
{ {
Functions\when('get_role')->alias(static fn() => new \stdClass()); Functions\when('get_role')->alias(static fn() => new \stdClass());