8 Commits
Author SHA1 Message Date
thatguygriff 3a83decc82 Merge pull request 'Say "student" and "profile" in the UI, not "child" and "family"' (#152) from refactor/144-student-profile-copy into main
CI / No Debug Code (push) Successful in 2s
CI / Tests (PHP 8.1) (push) Successful in 52s
CI / Tests (PHP 8.2) (push) Successful in 51s
CI / PHPStan (push) Successful in 2m47s
CI / Coding Standards (push) Successful in 2m54s
CI / Tests (PHP 8.3) (push) Successful in 2m39s
CI / Build Plugin Zip (push) Successful in 2m50s
Reviewed-on: #152
2026-07-29 23:41:55 +00:00
thatguygriffandClaude Opus 5 76caf178f0 Say "student" and "profile" in the UI, not "child" and "family"
CI / Tests (PHP 8.1) (pull_request) Successful in 41s
CI / Tests (PHP 8.2) (pull_request) Successful in 40s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m55s
CI / PHPStan (pull_request) Successful in 3m1s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m43s
CI / Build Plugin Zip (pull_request) Skipped
Sweep the translatable strings across the frontend templates, the admin
screens, the editor previews and the block inserter entry. Nothing else
moves: the database columns, request parameters, form field names, CSS
classes, the us_family shortcode and the us-scheduler/family block name are
contracts with existing installs and with post content people have already
saved, so renaming them would break sites for no user-visible gain.

Two strings are reworded rather than swapped, because the direct
substitution reads wrong:

- The students list said "Child of Jane" and now says "Managed by Jane".
  "Student of Jane" would read as a teacher's pupil, which is exactly the
  wrong idea in a music studio.
- A managed account is now "a managed student account" rather than "a
  student account", which would not distinguish it from the account holder.

The guardian feature doc gains a short section on the split, so the next
person to work on it does not read the mismatch as drift and "fix" it.

Closes #144

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-29 20:34:55 -03:00
thatguygriff 8013d05d68 Merge pull request 'Stop upcoming lesson rows rendering on top of each other' (#151) from fix/149-upcoming-lesson-overlap into main
CI / Coding Standards (push) Successful in 2m56s
CI / Tests (PHP 8.1) (push) Successful in 51s
CI / No Debug Code (push) Successful in 2s
CI / Tests (PHP 8.2) (push) Successful in 43s
CI / PHPStan (push) Successful in 2m50s
CI / Tests (PHP 8.3) (push) Successful in 2m40s
CI / Build Plugin Zip (push) Successful in 2m51s
Reviewed-on: #151
2026-07-29 23:29:18 +00:00
thatguygriffandClaude Opus 5 6b29c0e78e Stop upcoming lesson rows rendering on top of each other
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m51s
CI / Coding Standards (pull_request) Successful in 3m6s
CI / Tests (PHP 8.1) (pull_request) Successful in 53s
CI / Tests (PHP 8.2) (pull_request) Successful in 51s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m48s
CI / Build Plugin Zip (pull_request) Skipped
The panel's row and its two columns are divs with explicit flex rules, but
the text itself still sits in inline elements. A theme is free to take those
out of normal flow, and when it does the date and time land on the lesson
title and the status pill lands on the Cancel button. Pin position, float
and margin on the leaf elements at the same id-level specificity the rest of
the panel already uses, so a theme rule cannot lift them out of the column.

The rows behind "Show all" had the same shape of problem from the other
direction: `[hidden]` is only a UA-stylesheet rule, so the `div {
display: block }` reset that many themes still ship outranks it and the
collapsed rows render anyway. An author `!important` is the only way to win
that particular cascade.

Verified with a headless-browser harness rendering the exact markup
booking.js emits against twelve theme CSS patterns at two widths: before,
five patterns overlapped text or revealed the hidden rows; after, all pass.

Closes #149

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-29 20:28:25 -03:00
thatguygriff 7ea6616ba0 Merge pull request 'Trim CLAUDE.md to what the codebase can't tell you' (#141) from docs/trim-claude-md into main
CI / Tests (PHP 8.1) (push) Successful in 50s
CI / Coding Standards (push) Successful in 2m53s
CI / No Debug Code (push) Successful in 2s
CI / Tests (PHP 8.2) (push) Successful in 42s
CI / PHPStan (push) Successful in 2m46s
CI / Tests (PHP 8.3) (push) Successful in 2m49s
CI / Build Plugin Zip (push) Successful in 2m45s
Reviewed-on: #141
2026-07-29 19:41:35 +00:00
thatguygriffandClaude Opus 5 7fdf97b073 Trim CLAUDE.md to what the codebase can't tell you
CI / Tests (PHP 8.2) (pull_request) Successful in 53s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 2m54s
CI / PHPStan (pull_request) Successful in 3m0s
CI / Tests (PHP 8.1) (pull_request) Successful in 53s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m47s
CI / Build Plugin Zip (pull_request) Skipped
Most of this file described the repo as it was around v1.0: three domain
packages, two database tables, twenty-one classes. There are now eleven
packages, fifteen tables, and well over a hundred classes, so those
sections were not just redundant with `ls` and Schema.php — they were
teaching the wrong shape of the codebase. Same for the CI section, which
had drifted past the build job.

Cut the command list (composer.json has the scripts), the bootstrap
description, the directory tree, the table list, the Key Classes table,
and the CI job summary. Kept every rule the code can't explain on its
own: package-by-domain, no $wpdb outside repositories, capability checks
rather than role names, and the Schema.php version-bump gotcha.

Moved the Brain\Monkey and Mockery gotchas to tests/CLAUDE.md, which
loads only when working under tests/ instead of in every session.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-29 16:39:43 -03:00
thatguygriff d3843186c0 Merge pull request 'Bump version to 1.3.1' (#140) from release/bump-1.3.1 into main
CI / Tests (PHP 8.2) (push) Successful in 48s
CI / No Debug Code (push) Successful in 2s
CI / PHPStan (push) Successful in 2m55s
CI / Coding Standards (push) Successful in 2m56s
CI / Tests (PHP 8.3) (push) Successful in 2m40s
CI / Build Plugin Zip (push) Successful in 2m45s
CI / Tests (PHP 8.1) (push) Successful in 43s
Reviewed-on: #140
2026-07-29 19:28:42 +00:00
Release Bot e44972abe9 Bump version to 1.3.1 and open changelog section 2026-07-29 19:22:36 +00:00
20 changed files with 130 additions and 140 deletions
+2 -1
View File
@@ -5,7 +5,8 @@
"Bash(composer lint *)",
"Bash(tea actions:*)",
"Bash(tea issue *)",
"Bash(tea label *)"
"Bash(tea label *)",
"Bash(composer cs *)"
]
}
}
+8
View File
@@ -11,6 +11,14 @@ When a `v*` tag is pushed, `.gitea/workflows/release.yml` publishes the matching
the plugin to the next patch version and adds a fresh section here for it. Record
each change under the current top section as you work.
## [1.3.1]
### Changed
- The interface now says **student** where it said "child" and **profile** where it said "family". The `[us_family]` page is headed **Your profile**, its form is **Add a student**, signup asks for a **Student's name**, and the wp-admin students list and student screen both label the relationship **Profile**. Two strings were reworded rather than swapped: the students list reads **Managed by _name_** (a bare "Student of _name_" would read as a teacher's pupil), and a managed account is described as a **managed student account** so it is not confused with the account holder. Internal names — database columns, request parameters, form field names, the `us_family` shortcode and the `us-scheduler/family` block — are unchanged, since they are contracts with existing installs and saved post content.
### Fixed
- Upcoming lesson rows no longer render on top of each other. The row's text sits in inline elements that a theme can pull out of normal flow, which dropped the date and time onto the lesson title and the status pill onto the Cancel button; those elements are now pinned into flow alongside the rest of the panel's theme-proofing. The rows held behind **Show all** also stayed visible under the `div { display: block }` reset that many themes still carry, since `[hidden]` is only a browser default — they are now hidden for real.
## [1.3.0]
### Added
+4 -88
View File
@@ -4,100 +4,23 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Commands
```bash
composer install # Install all dependencies
composer test # Run the full test suite (required after every change)
composer lint # PHPStan static analysis
composer cs # PHPCS coding standards check
composer cs:fix # Auto-fix coding standards
# Run a single test file
./vendor/bin/phpunit tests/Unit/Availability/AvailabilityRepositoryTest.php
# Run a single test by name
./vendor/bin/phpunit --filter testInsertCallsWpdbInsertAndReturnsId
```
**Run `composer test` after every code change before considering a task complete.**
## Architecture
### Plugin Bootstrap
`unsupervised-schedular.php` defines constants (`USC_VERSION`, `USC_PLUGIN_DIR`, `USC_PLUGIN_URL`), registers activation/deactivation hooks, then calls `Plugin::boot()` on `plugins_loaded`. No logic lives in the root file.
### Directory Structure
```
src/ — All plugin PHP (PSR-4 namespace: Unsupervised\Schedular\)
Availability/ — Availability slots: value object, repository, controller, REST endpoint
Booking/ — Lessons/bookings: value object, repository, controller, REST endpoint, shortcode page
Auth/ — Roles, capabilities, login page
Plugin.php — Wires all components together on plugins_loaded
Installer.php — Creates DB tables and roles on activation
Schema.php — CREATE TABLE SQL for dbDelta
AdminMenu.php — Registers wp-admin menu pages
RestRegistrar.php — Registers all REST routes under us-scheduler/v1
ShortcodeRegistrar.php — Registers [us_booking] and [us_student_login] shortcodes
BlockRegistrar.php — Registers Gutenberg dynamic-block wrappers for the shortcodes
BlockPreview.php — Static editor-preview markup for the blocks
templates/ — PHP view files included by controllers/shortcodes
assets/ — CSS and JS (vanilla JS, no build step)
tests/Unit/ — PHPUnit unit tests (PSR-4: Unsupervised\Schedular\Tests\)
Availability/ — Tests for src/Availability/
Booking/ — Tests for src/Booking/
Auth/ — Tests for src/Auth/
docs/features/ — One markdown file per feature describing data model, API, and test locations
```
**Code is organised package-by-domain** (Availability, Booking, Auth). Each domain package contains everything related to that domain: value objects, repositories, controllers, REST endpoints, and shortcode pages. Cross-cutting wiring classes (Plugin, AdminMenu, RestRegistrar, ShortcodeRegistrar, Schema) live directly under `src/`.
### Code organisation
**Code is organised package-by-domain.** Each domain package under `src/<Domain>/` contains everything related to that domain: value objects, repositories, controllers, REST endpoints, and shortcode pages. Cross-cutting wiring classes (Plugin, AdminMenu, RestRegistrar, ShortcodeRegistrar, Schema) live directly under `src/`.
### Data Storage
Two custom database tables (created via `dbDelta` on activation):
- `{prefix}us_availability` — instructor availability windows
- `{prefix}us_lessons` — booked lessons
Custom database tables are created via `dbDelta` on activation; `Schema.php` holds the SQL.
All database access goes through repository classes within their domain package. No direct `$wpdb` calls outside repositories.
### Key Classes
| Class | Responsibility |
|---|---|
| `Plugin` | Wires all components together on `plugins_loaded` |
| `Installer` | Creates DB tables and roles on activation |
| `Schema` | CREATE TABLE SQL strings for dbDelta |
| `AdminMenu` | Registers wp-admin menu pages |
| `RestRegistrar` | Registers all REST routes under `us-scheduler/v1` |
| `ShortcodeRegistrar` | Registers `[us_booking]` and `[us_student_login]` shortcodes |
| `BlockRegistrar` | Registers Gutenberg dynamic-block wrappers for the shortcodes |
| `BlockPreview` | Static editor-preview markup for the blocks |
| `Val` | Runtime coercion of untyped WP boundary values (wpdb rows, REST params, superglobals) |
| `Auth\RoleManager` | Registers `us_instructor` and `us_student` roles with custom caps |
| `Auth\LoginPage` | Renders front-end student login form |
| `Availability\AvailabilitySlot` | Immutable value object for a slot row |
| `Availability\AvailabilityRepository` | CRUD for availability slots |
| `Availability\AvailabilityController` | Instructor availability management page |
| `Availability\AvailabilityEndpoint` | REST handlers for availability CRUD |
| `Booking\Lesson` | Immutable value object for a lesson row |
| `Booking\BookingRepository` | CRUD for lesson bookings |
| `Booking\BookingEndpoint` | REST handlers for booking and status updates |
| `Booking\BookingPage` | Renders student booking UI shell (JS takes over) |
| `Booking\LessonController` | Admin and instructor lesson list pages |
### REST API Namespace
All endpoints live under `/wp-json/us-scheduler/v1/`. Permissions are enforced via `permission_callback` using capability checks (`manage_availability`, `book_lesson`), never role name checks.
### Testing Approach
Tests use [Brain\Monkey](https://brain-wp.github.io/BrainMonkey/) to stub WordPress functions without a full WP installation, and Mockery to mock `$wpdb` and other dependencies.
All test classes extend `tests/Unit/TestCase.php`, which handles `Monkey\setUp()` / `Monkey\tearDown()` and stubs all WP translation/escape functions automatically.
**Brain\Monkey API notes:**
- `Functions\when('fn')->alias(fn() => ...)` — stub with a closure (NOT `returnUsing()`)
- `Functions\when('fn')->justReturn($val)` — stub returning a fixed value
- `Functions\expect('fn')->once()->with(...)` — assert call count and arguments
- Use `Functions\when()` (not `Functions\expect()`) when you need argument-routing (e.g. `get_role` returning different values per argument) to avoid chaining ambiguity
- Mockery matchers (e.g. `\Mockery::type()`) inside plain PHP arrays do not work with `with()` — use `\Mockery::on(fn($arr) => ...)` or `\Mockery::any()` instead
- When mocking `$wpdb`, set `$mock->prefix = 'wp_'` explicitly — it is a public property, not a method
Tests stub WordPress with Brain\Monkey rather than booting a real WP install. The setup and the Brain\Monkey/Mockery API gotchas are in `tests/CLAUDE.md`.
### Adding a Feature
0. **If the feature touches `Schema.php`, bump both the `Version:` header and `USC_VERSION` in `unsupervised-schedular.php`.** `Plugin::boot()` only re-runs `Installer`/`dbDelta` when the stored `us_schedular_version` differs, so a schema change without a version bump never reaches existing sites and inserts into new columns fail silently.
@@ -106,10 +29,3 @@ All test classes extend `tests/Unit/TestCase.php`, which handles `Monkey\setUp()
3. Add template(s) under `templates/` if needed.
4. Write unit tests under `tests/Unit/<Domain>/` mirroring the `src/<Domain>/` structure.
5. Run `composer test` — all tests must pass before the feature is complete.
### CI
Gitea Actions (`.gitea/workflows/ci.yml`) runs on every push and pull request:
- **lint** — PHPCS WordPress coding standards
- **static-analysis** — PHPStan level 10
- **test** — PHPUnit on PHP 8.1, 8.2, 8.3
- **no-debug** — rejects commits with `var_dump`, `error_log`, etc. in `src/`
+37 -2
View File
@@ -99,6 +99,36 @@
align-items: center;
}
/*
* Theme-proofing for the leaf text. The row and its two columns are divs with
* explicit flex rules above, but the text itself still sits in inline elements
* a theme is free to take out of normal flow — an absolutely positioned,
* floated or negatively offset span drops the date/time on top of the title and
* the status pill on top of the Cancel button. Pinning the three properties
* that would have to change keeps the leaves in flow, at the same id-level
* specificity the rules above rely on.
*/
#us-booking-app .us-my-lesson-title,
#us-booking-app .us-my-lesson-when,
#us-booking-app .us-my-lesson-duration,
#us-booking-app .us-my-lesson-who,
#us-booking-app .us-lesson-status {
position: static;
float: none;
margin: 0;
}
/*
* The rows the "Show all" button reveals. `[hidden]` is only a UA-stylesheet
* rule, so any author rule setting a display on div beats it — the html5-reset
* `div { display: block }` is still widespread in themes — and the rows the
* button is meant to gate render anyway. An author !important is the only way
* to win that cascade.
*/
#us-booking-app [hidden] {
display: none !important;
}
#us-booking-app .us-show-all-lessons {
background: transparent;
border: 1px solid #ccc;
@@ -397,8 +427,13 @@
max-width: 100%;
}
/* Whose lesson a row in the upcoming panel is — only shown on a family account. */
.us-my-lesson-who {
/*
* Whose lesson a row in the upcoming panel is — only shown on an account that
* books for more than one person. Scoped under #us-booking-app like the rest of
* the panel; as a bare class it was the one rule in the group a theme could
* outrank on a plain span.
*/
#us-booking-app .us-my-lesson-who {
font-weight: normal;
opacity: 0.75;
}
+6 -3
View File
@@ -284,10 +284,13 @@
},
{
name: 'us-scheduler/family',
title: __('Family', 'unsupervised-schedular'),
description: __('Lets a parent or guardian add, edit and remove the children they book lessons for.', 'unsupervised-schedular'),
title: __('Profile', 'unsupervised-schedular'),
description: __('Lets a parent or guardian add, edit and remove the students they book lessons for.', 'unsupervised-schedular'),
icon: 'groups',
keywords: ['family', 'children', 'guardian', 'parent'],
// 'family' and 'children' are kept as search terms only — they are
// never displayed, and the block answered to them before it was
// renamed, so anyone reaching for the old word still finds it.
keywords: ['profile', 'students', 'family', 'children', 'guardian', 'parent'],
shortcode: 'us_family',
attributes: {
loginPageId: { type: 'number', default: 0 },
+16 -3
View File
@@ -9,6 +9,19 @@ A guardian may also be a student in their own right — they appear in their own
"who is this for?" selector alongside their children, so a parent taking lessons
next to their kids needs only the one account.
## Vocabulary: "child" in the code, "student" in the UI
The interface says **student** and **profile**; the code says **child** and
**family**. This is deliberate, not drift. Every identifier below — the
`us_guardian_links` columns, `GuardianService::createChild()`, the `children[]`
request parameters, the `child_name` form fields, the `us-scheduler/family`
block name and the `[us_family]` shortcode — is a stable contract with the
database, saved post content and existing installs, so renaming them would break
sites for no user-visible gain. Only the strings a person reads were changed.
When adding to this feature, keep the split: internal names follow the
data model, translatable strings follow the interface.
## Core Decision: children are accountless WordPress users
Every `student_id` column in `src/Schema.php` (`us_lessons`, `us_payments`,
@@ -159,7 +172,7 @@ child.
## Managing children
`[us_family]` (block: **Family**) renders the guardian's manage-children screen:
`[us_family]` (block: **Profile**) renders the guardian's manage-children screen:
list the children, add one, edit a name/date of birth, remove one.
- **Add** creates another accountless child user and links it. Account-scope
@@ -219,11 +232,11 @@ than being left as a single-student-only path.
## Admin
- **Students list** gains a **Guardian / Children** column: a child links to its
- **Students list** gains a **Profile** column: a child links to its
guardian's detail screen, a guardian lists its children as links. Children are
listed alongside every other student rather than nested, so nothing about
finding a student changes.
- **Student detail** gains a **Family** panel — the guardian (for a child) or
- **Student detail** gains a **Profile** panel — the guardian (for a child) or
the children (for a guardian), each a link to the other's screen — and the
credit balance shown is the **payer's** balance, labelled with whose it is, so
an admin looking at a child sees the family balance that will actually settle
+2 -2
View File
@@ -88,8 +88,8 @@ All actions are nonce-protected POSTs handled on the detail page:
`tests/Unit/Payment/PaymentRepositoryTest.php`
## Family Relationships
The students list gains a **Family** column — a child links to their guardian,
a guardian lists their children — and the student screen a **Family** panel. A
The students list gains a **Profile** column — a child links to their guardian,
a guardian lists their children — and the student screen a **Profile** panel. A
child's listed email is their guardian's, since a child's own address is an
undeliverable placeholder, and the credit balance shown is the payer's, labelled
with whose account holds it. See `parent-guardian-accounts.md`.
+2 -2
View File
@@ -286,14 +286,14 @@ class RegistrationPage {
// Everything is validated before a single user is created, so a bad child
// block never leaves a half-registered family behind.
if ( $isGuardian && [] === $children ) {
return esc_html__( 'Please add at least one child, or uncheck the parent/guardian option.', 'unsupervised-schedular' );
return esc_html__( 'Please add at least one student, or uncheck the parent/guardian option.', 'unsupervised-schedular' );
}
foreach ( $isGuardian ? array_column( $children, 'answers' ) : [ $answers ] as $set ) {
foreach ( $accountQuestions as $question ) {
if ( $question->isRequired && '' === trim( (string) ( $set[ (int) $question->id ] ?? '' ) ) ) {
return $isGuardian
? esc_html__( 'Please answer all required registration questions for each child.', 'unsupervised-schedular' )
? esc_html__( 'Please answer all required registration questions for each student.', 'unsupervised-schedular' )
: esc_html__( 'Please answer all required registration questions.', 'unsupervised-schedular' );
}
}
+4 -4
View File
@@ -189,16 +189,16 @@ class BlockPreview {
'<h4>%s</h4><p><label for="us-child-name">%s</label><input type="text" id="us-child-name"></p>'
. '<p><label for="us-child-dob">%s</label><input type="date" id="us-child-dob"></p>'
. '<p><button type="button" disabled>%s</button></p>',
esc_html__( 'Add a child', 'unsupervised-schedular' ),
esc_html__( 'Add a student', 'unsupervised-schedular' ),
esc_html__( 'Name', 'unsupervised-schedular' ),
esc_html__( 'Date of birth', 'unsupervised-schedular' ),
esc_html__( 'Add child', 'unsupervised-schedular' )
esc_html__( 'Add student', 'unsupervised-schedular' )
);
return sprintf(
'<div class="us-family">%s<h3>%s</h3><ul class="us-family-list">%s</ul><form class="us-family-add">%s</form></div>',
self::note( __( 'Editor preview — signed-in guardians see and manage their own children here.', 'unsupervised-schedular' ) ),
esc_html__( 'Your family', 'unsupervised-schedular' ),
self::note( __( 'Editor preview — signed-in guardians see and manage their own students here.', 'unsupervised-schedular' ) ),
esc_html__( 'Your profile', 'unsupervised-schedular' ),
$children,
$add
);
+1 -1
View File
@@ -35,7 +35,7 @@ class ChildLoginGate {
if ( $user instanceof \WP_User && GuardianService::isChild( (int) $user->ID ) ) {
return new \WP_Error(
'us_child_account',
esc_html__( 'This is a child account and cannot be signed in to. Please sign in with the parent or guardian account.', 'unsupervised-schedular' )
esc_html__( 'This is a managed student account and cannot be signed in to. Please sign in with the parent or guardian account.', 'unsupervised-schedular' )
);
}
+4 -4
View File
@@ -50,7 +50,7 @@ class FamilyPage {
'<p>%s <a href="%s">%s</a>.</p>',
esc_html__( 'Please', 'unsupervised-schedular' ),
esc_url( $this->loginUrl( $loginPageId ) ),
esc_html__( 'log in to manage your family', 'unsupervised-schedular' )
esc_html__( 'log in to manage your profile', 'unsupervised-schedular' )
);
}
@@ -168,7 +168,7 @@ class FamilyPage {
private function firstMissingAnswer( array $questions, array $answers ): ?\WP_Error {
foreach ( $questions as $question ) {
if ( $question->isRequired && '' === trim( (string) ( $answers[ (int) $question->id ] ?? '' ) ) ) {
return new \WP_Error( 'missing_answer', __( 'Please answer all required questions for this child.', 'unsupervised-schedular' ) );
return new \WP_Error( 'missing_answer', __( 'Please answer all required questions for this student.', 'unsupervised-schedular' ) );
}
}
@@ -237,9 +237,9 @@ class FamilyPage {
*/
private function noticeFor( string $result ): string {
return match ( $result ) {
self::RESULT_ADDED => __( 'Child added.', 'unsupervised-schedular' ),
self::RESULT_ADDED => __( 'Student added.', 'unsupervised-schedular' ),
self::RESULT_UPDATED => __( 'Details updated.', 'unsupervised-schedular' ),
self::RESULT_REMOVED => __( 'Child removed.', 'unsupervised-schedular' ),
self::RESULT_REMOVED => __( 'Student removed.', 'unsupervised-schedular' ),
default => '',
};
}
+6 -6
View File
@@ -51,7 +51,7 @@ class GuardianService {
public function createChild( int $guardianId, string $name, string $dateOfBirth = '', string $relationship = '' ): int|\WP_Error {
$name = trim( $name );
if ( '' === $name ) {
return new \WP_Error( 'missing_name', __( 'Please give each child a name.', 'unsupervised-schedular' ) );
return new \WP_Error( 'missing_name', __( 'Please give each student a name.', 'unsupervised-schedular' ) );
}
$email = $this->childEmail();
@@ -89,7 +89,7 @@ class GuardianService {
if ( $linkId <= 0 ) {
$this->deleteUser( $userId );
return new \WP_Error( 'link_failed', __( 'Could not add this child. Please contact the studio.', 'unsupervised-schedular' ) );
return new \WP_Error( 'link_failed', __( 'Could not add this student. Please contact the studio.', 'unsupervised-schedular' ) );
}
return $userId;
@@ -104,12 +104,12 @@ class GuardianService {
*/
public function updateChild( int $guardianId, int $studentId, string $name, string $dateOfBirth = '' ): ?\WP_Error {
if ( ! $this->guardians->isGuardianOf( $guardianId, $studentId ) ) {
return new \WP_Error( 'forbidden', __( 'That is not one of your children.', 'unsupervised-schedular' ) );
return new \WP_Error( 'forbidden', __( 'That is not one of your students.', 'unsupervised-schedular' ) );
}
$name = trim( $name );
if ( '' === $name ) {
return new \WP_Error( 'missing_name', __( 'Please give each child a name.', 'unsupervised-schedular' ) );
return new \WP_Error( 'missing_name', __( 'Please give each student a name.', 'unsupervised-schedular' ) );
}
$result = wp_update_user(
@@ -139,13 +139,13 @@ class GuardianService {
*/
public function removeChild( int $guardianId, int $studentId ): ?\WP_Error {
if ( ! $this->guardians->isGuardianOf( $guardianId, $studentId ) ) {
return new \WP_Error( 'forbidden', __( 'That is not one of your children.', 'unsupervised-schedular' ) );
return new \WP_Error( 'forbidden', __( 'That is not one of your students.', 'unsupervised-schedular' ) );
}
if ( [] !== $this->bookings->findByStudent( $studentId ) || [] !== $this->enrollments->findByStudent( $studentId ) ) {
return new \WP_Error(
'has_history',
__( 'This child has lessons or enrolments on record and cannot be removed here. Please contact the studio.', 'unsupervised-schedular' )
__( 'This student has lessons or enrolments on record and cannot be removed here. Please contact the studio.', 'unsupervised-schedular' )
);
}
+3 -3
View File
@@ -110,7 +110,7 @@ $renderLessons = static function (array $rows, bool $withActions = false): void
</form>
<?php if ($guardian !== null || ! empty($children)) : ?>
<h2><?php esc_html_e('Family', 'unsupervised-schedular'); ?></h2>
<h2><?php esc_html_e('Profile', 'unsupervised-schedular'); ?></h2>
<?php $detailUrl = static fn(int $id): string => add_query_arg(['page' => $pageSlug, 'student_id' => $id], admin_url('admin.php')); ?>
<?php if ($guardian !== null) : ?>
<p>
@@ -123,7 +123,7 @@ $renderLessons = static function (array $rows, bool $withActions = false): void
);
?>
</p>
<p class="description"><?php esc_html_e('This is a child account: it has no login of its own, and its email address is a placeholder that cannot receive mail.', 'unsupervised-schedular'); ?></p>
<p class="description"><?php esc_html_e('This is a managed student account: it has no login of its own, and its email address is a placeholder that cannot receive mail.', 'unsupervised-schedular'); ?></p>
<?php endif; ?>
<?php if (! empty($children)) : ?>
<p><?php esc_html_e('Books and pays for:', 'unsupervised-schedular'); ?></p>
@@ -293,7 +293,7 @@ $renderLessons = static function (array $rows, bool $withActions = false): void
<?php
printf(
/* translators: %s: name of the parent/guardian whose account holds the balance. */
esc_html__('Held on %ss account — the family shares one balance.', 'unsupervised-schedular'),
esc_html__('Held on %ss account — the profile shares one balance.', 'unsupervised-schedular'),
esc_html($payer['name'])
);
?>
+2 -2
View File
@@ -25,7 +25,7 @@ $familyCell = static function (array $student) use ($pageSlug): string {
if ($student['guardian'] !== null) {
return sprintf(
/* translators: %s: linked name of the parent/guardian who books for this student. */
esc_html__('Child of %s', 'unsupervised-schedular'),
esc_html__('Managed by %s', 'unsupervised-schedular'),
$link($student['guardian']['id'], $student['guardian']['name'])
);
}
@@ -51,7 +51,7 @@ $familyCell = static function (array $student) use ($pageSlug): string {
<tr>
<th><?php esc_html_e('Name', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Email', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Family', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Profile', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Registered', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Upcoming lessons', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Active enrolments', 'unsupervised-schedular'); ?></th>
+5 -5
View File
@@ -16,7 +16,7 @@ if (! defined('ABSPATH')) {
*/
?>
<div class="us-family">
<h3><?php esc_html_e('Your family', 'unsupervised-schedular'); ?></h3>
<h3><?php esc_html_e('Your profile', 'unsupervised-schedular'); ?></h3>
<?php if ($notice !== '') : ?>
<p class="us-success"><?php echo esc_html($notice); ?></p>
@@ -27,7 +27,7 @@ if (! defined('ABSPATH')) {
<?php endif; ?>
<?php if (empty($children)) : ?>
<p><?php esc_html_e('You have not added any children yet. Add one below to start booking lessons for them.', 'unsupervised-schedular'); ?></p>
<p><?php esc_html_e('You have not added any students yet. Add one below to start booking lessons for them.', 'unsupervised-schedular'); ?></p>
<?php else : ?>
<ul class="us-family-list">
<?php foreach ($children as $child) : ?>
@@ -74,7 +74,7 @@ if (! defined('ABSPATH')) {
<?php wp_nonce_field('us_family'); ?>
<input type="hidden" name="us_family_action" value="add">
<h4><?php esc_html_e('Add a child', 'unsupervised-schedular'); ?></h4>
<h4><?php esc_html_e('Add a student', 'unsupervised-schedular'); ?></h4>
<p>
<label for="us-child-name"><?php esc_html_e('Name', 'unsupervised-schedular'); ?></label>
<input type="text" name="child_name" id="us-child-name" required>
@@ -90,7 +90,7 @@ if (! defined('ABSPATH')) {
<?php if (! empty($questions)) : ?>
<fieldset class="us-reg-questions">
<legend><?php esc_html_e('About this child', 'unsupervised-schedular'); ?></legend>
<legend><?php esc_html_e('About this student', 'unsupervised-schedular'); ?></legend>
<?php foreach ($questions as $question) : ?>
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- QuestionField::render() escapes every interpolated value.
@@ -101,7 +101,7 @@ if (! defined('ABSPATH')) {
<?php endif; ?>
<p>
<button type="submit"><?php esc_html_e('Add child', 'unsupervised-schedular'); ?></button>
<button type="submit"><?php esc_html_e('Add student', 'unsupervised-schedular'); ?></button>
</p>
</form>
</div>
+5 -5
View File
@@ -75,18 +75,18 @@ if (! defined('ABSPATH')) {
<p>
<label>
<input type="checkbox" name="us_is_guardian" id="us-is-guardian" value="1">
<?php esc_html_e("I'm registering as a parent or guardian, for one or more children", 'unsupervised-schedular'); ?>
<?php esc_html_e("I'm registering as a parent or guardian, for one or more students", 'unsupervised-schedular'); ?>
</label>
</p>
<?php /* Revealed by the checkbox; without JS it is simply always visible. */ ?>
<div class="us-children" id="us-children">
<p class="us-children-intro"><?php esc_html_e('Add each child you will be booking lessons for. They do not need their own login — you book and pay for them from this account.', 'unsupervised-schedular'); ?></p>
<p class="us-children-intro"><?php esc_html_e('Add each student you will be booking lessons for. They do not need their own login — you book and pay for them from this account.', 'unsupervised-schedular'); ?></p>
<?php /* The first block is the template the "Add another child" button clones. */ ?>
<?php /* The first block is the template the "Add another student" button clones. */ ?>
<div class="us-child" data-child-index="0">
<p>
<label for="us-child-0-name"><?php esc_html_e("Child's name", 'unsupervised-schedular'); ?></label>
<label for="us-child-0-name"><?php esc_html_e("Student's name", 'unsupervised-schedular'); ?></label>
<input type="text" name="children[0][name]" id="us-child-0-name">
</p>
<p>
@@ -107,7 +107,7 @@ if (! defined('ABSPATH')) {
</div>
<p>
<button type="button" class="us-add-child"><?php esc_html_e('Add another child', 'unsupervised-schedular'); ?></button>
<button type="button" class="us-add-child"><?php esc_html_e('Add another student', 'unsupervised-schedular'); ?></button>
</p>
</div>
</fieldset>
+14
View File
@@ -0,0 +1,14 @@
# Writing tests
Tests use [Brain\Monkey](https://brain-wp.github.io/BrainMonkey/) to stub WordPress functions without a full WP installation, and Mockery to mock `$wpdb` and other dependencies.
All test classes extend `tests/Unit/TestCase.php`, which handles `Monkey\setUp()` / `Monkey\tearDown()` and stubs all WP translation/escape functions automatically.
**Brain\Monkey API notes:**
- `Functions\when('fn')->alias(fn() => ...)` — stub with a closure (NOT `returnUsing()`)
- `Functions\when('fn')->justReturn($val)` — stub returning a fixed value
- `Functions\expect('fn')->once()->with(...)` — assert call count and arguments
- Use `Functions\when()` (not `Functions\expect()`) when you need argument-routing (e.g. `get_role` returning different values per argument) to avoid chaining ambiguity
- Mockery matchers (e.g. `\Mockery::type()`) inside plain PHP arrays do not work with `with()` — use `\Mockery::on(fn($arr) => ...)` or `\Mockery::any()` instead
- When mocking `$wpdb`, set `$mock->prefix = 'wp_'` explicitly — it is a public property, not a method
+2 -2
View File
@@ -667,7 +667,7 @@ class RegistrationPageTest extends TestCase
$result = $this->submit(new Invite(email: '[email protected]', token: 'hash'), false);
self::assertStringContainsString('at least one child', $result);
self::assertStringContainsString('at least one student', $result);
}
/**
@@ -694,7 +694,7 @@ class RegistrationPageTest extends TestCase
Functions\expect('wp_insert_user')->never();
$this->ctx['guardians']->shouldNotReceive('createChild');
self::assertStringContainsString('for each child', $this->submit(new Invite(email: '[email protected]', token: 'hash'), false));
self::assertStringContainsString('for each student', $this->submit(new Invite(email: '[email protected]', token: 'hash'), false));
}
/**
+5 -5
View File
@@ -86,7 +86,7 @@ class FamilyPageTest extends TestCase
$html = $this->page->render([]);
self::assertStringContainsString('log in to manage your family', $html);
self::assertStringContainsString('log in to manage your profile', $html);
}
public function testRenderListsTheGuardiansChildren(): void
@@ -100,7 +100,7 @@ class FamilyPageTest extends TestCase
self::assertStringContainsString('Ada', $html);
self::assertStringContainsString('2015-04-02', $html);
self::assertStringContainsString('Add a child', $html);
self::assertStringContainsString('Add a student', $html);
}
public function testAddCreatesTheChildRecordsItsAnswersAndRedirects(): void
@@ -161,7 +161,7 @@ class FamilyPageTest extends TestCase
$_POST = ['us_family_action' => 'add', 'child_name' => ''];
$this->questions->shouldReceive('findByScope')->once()->andReturn([]);
$this->guardians->shouldReceive('createChild')->once()->andReturn(new \WP_Error('missing_name', 'Please give each child a name.'));
$this->guardians->shouldReceive('createChild')->once()->andReturn(new \WP_Error('missing_name', 'Please give each student a name.'));
$this->answers->shouldNotReceive('insert');
$captured = null;
@@ -207,7 +207,7 @@ class FamilyPageTest extends TestCase
$_POST = ['us_family_action' => 'remove', 'child_id' => '42'];
$this->guardians->shouldReceive('removeChild')->once()->andReturn(
new \WP_Error('has_history', 'This child has lessons or enrolments on record.')
new \WP_Error('has_history', 'This student has lessons or enrolments on record.')
);
$captured = null;
@@ -271,6 +271,6 @@ class FamilyPageTest extends TestCase
$this->guardians->shouldReceive('children')->andReturn([]);
$this->questions->shouldReceive('findByScope')->andReturn([]);
self::assertStringContainsString('Child added.', $this->page->render([]));
self::assertStringContainsString('Student added.', $this->page->render([]));
}
}
+2 -2
View File
@@ -3,7 +3,7 @@
* Plugin Name: Unsupervised Scheduler
* Plugin URI: https://git.unsupervised.ca/Unsupervised/unsupervised-scheduler
* Description: Instructor/student lesson scheduling for WordPress.
* Version: 1.3.0
* Version: 1.3.1
* Requires at least: 6.2
* Requires PHP: 8.1
* Author: Unsupervised
@@ -21,7 +21,7 @@ if (! defined('ABSPATH')) {
exit;
}
define('USC_VERSION', '1.3.0');
define('USC_VERSION', '1.3.1');
define('USC_PLUGIN_FILE', __FILE__);
define('USC_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('USC_PLUGIN_URL', plugin_dir_url(__FILE__));