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
This commit was merged in pull request #152.
This commit is contained in:
2026-07-29 23:41:55 +00:00
15 changed files with 66 additions and 47 deletions
+3
View File
@@ -13,6 +13,9 @@ 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.
+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>
+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([]));
}
}