Merge pull request 'Grant studio-admin caps to administrators; re-gate Scheduler; mark 1.0.0-rc.1' (#13) from feature/admin-studio-caps into main
CI / Coding Standards (push) Successful in 48s
CI / PHPStan (push) Successful in 1m4s
CI / Tests (PHP 8.1) (push) Successful in 45s
CI / Tests (PHP 8.2) (push) Successful in 49s
CI / Tests (PHP 8.3) (push) Successful in 48s
CI / No Debug Code (push) Successful in 3s
CI / Build Plugin Zip (push) Successful in 41s
CI / Coding Standards (push) Successful in 48s
CI / PHPStan (push) Successful in 1m4s
CI / Tests (PHP 8.1) (push) Successful in 45s
CI / Tests (PHP 8.2) (push) Successful in 49s
CI / Tests (PHP 8.3) (push) Successful in 48s
CI / No Debug Code (push) Successful in 3s
CI / Build Plugin Zip (push) Successful in 41s
Reviewed-on: #13
This commit was merged in pull request #13.
This commit is contained in:
@@ -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,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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: Unsupervised Scheduler
|
* Plugin Name: Unsupervised Scheduler
|
||||||
* Plugin URI: https://unsupervised.ca
|
* Plugin URI: https://unsupervised.ca
|
||||||
* Description: Instructor/student lesson scheduling for WordPress.
|
* Description: Instructor/student lesson scheduling for WordPress.
|
||||||
* Version: 1.0.0
|
* Version: 1.0.0-rc.1
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Requires PHP: 8.1
|
* Requires PHP: 8.1
|
||||||
* Author: Unsupervised
|
* Author: Unsupervised
|
||||||
@@ -19,7 +19,7 @@ if (! defined('ABSPATH')) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
define('USC_VERSION', '1.0.0');
|
define('USC_VERSION', '1.0.0-rc.1');
|
||||||
define('USC_PLUGIN_FILE', __FILE__);
|
define('USC_PLUGIN_FILE', __FILE__);
|
||||||
define('USC_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('USC_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('USC_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('USC_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
|||||||
Reference in New Issue
Block a user