Merge pull request 'Let the account holder edit their own details on the profile page' (#166) from feature/165-editable-own-profile into main
CI / Tests (PHP 8.1) (push) Successful in 46s
CI / Tests (PHP 8.2) (push) Successful in 55s
CI / No Debug Code (push) Successful in 3s
CI / PHPStan (push) Successful in 2m57s
CI / Coding Standards (push) Successful in 3m3s
CI / Tests (PHP 8.3) (push) Successful in 2m48s
CI / Build Plugin Zip (push) Successful in 2m50s
CI / Tests (PHP 8.1) (push) Successful in 46s
CI / Tests (PHP 8.2) (push) Successful in 55s
CI / No Debug Code (push) Successful in 3s
CI / PHPStan (push) Successful in 2m57s
CI / Coding Standards (push) Successful in 3m3s
CI / Tests (PHP 8.3) (push) Successful in 2m48s
CI / Build Plugin Zip (push) Successful in 2m50s
Reviewed-on: #166
This commit was merged in pull request #166.
This commit is contained in:
@@ -15,6 +15,8 @@ each change under the current top section as you work.
|
||||
|
||||
### Added
|
||||
- **A registration question can now be asked of students only, and can be required of a student without being required of the account holder.** Every account-signup question was asked of everybody who registered, on the same terms — so "School and grade" had to be put to the adult signing themselves up, and a question a studio needed answered for a child could only be made required by demanding it of everyone. Each question now says who it is asked of — everyone, or only the students you register on behalf of — and carries its own **Required** setting for each: optional for you, required for every student you enrol, is now a thing a studio can ask for. Existing questions are untouched: they stay asked of everyone, and one that was required stays required of everyone.
|
||||
- **You can now edit your own details on the profile page**, not just your students'. The page is called **Your profile**, and until now the one person on it you could not change was yourself: a mistyped name at signup, or a name that had since changed, meant asking the studio to fix it. **Your details** now sits at the top of the page with your name, your birth year, and whether you take lessons yourself. Your email address is shown but not editable — it is also how you sign in, so changing it stays a studio-side job.
|
||||
- **"I take lessons myself" can be corrected after signup.** Signup asks whether you are registering just yourself, only on behalf of students, or both, and the answer decides whether you are offered as a student when booking. Choosing wrongly — or taking up lessons later alongside the children you book for — used to leave you asking the studio to change it. Ticking the box makes you bookable again and asks for your birth year like any other student; unticking it takes you back off the list without discarding the birth year you already gave, so ticking it back on costs you nothing.
|
||||
|
||||
## [1.4.1]
|
||||
|
||||
|
||||
@@ -521,6 +521,32 @@
|
||||
}
|
||||
|
||||
/* The guardian's manage-children screen ([us_family]). */
|
||||
|
||||
/*
|
||||
* The account holder's own details, set off from the students below so the two
|
||||
* halves of the page do not read as one long form.
|
||||
*/
|
||||
.us-family-self {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.us-family-self-email span {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/*
|
||||
* Guidance under a control, not a label: it explains when a field matters
|
||||
* rather than naming it, so it is sized down and reads after the input.
|
||||
*/
|
||||
.us-field-hint {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 0.9em;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.us-family-list {
|
||||
margin: 0 0 20px;
|
||||
padding: 0;
|
||||
|
||||
@@ -187,7 +187,8 @@ than writing `0`, so "not set" stays the one spelling of "yes, a student".
|
||||
|
||||
One guard: a guardian-only account with **nobody linked to it** is still offered
|
||||
itself, because an empty picker is no way to book at all. They can put the
|
||||
account right from the profile page.
|
||||
account right from the profile page — see **Your details** below, where the flag
|
||||
is editable as "I take lessons myself".
|
||||
|
||||
Per child the form collects:
|
||||
- **Name** (required)
|
||||
@@ -267,9 +268,51 @@ Booking-scope policies are accepted at booking time by whoever is signed in;
|
||||
`BookingEndpoint` passes the same `accepted_by` when a guardian books for a
|
||||
child.
|
||||
|
||||
## Managing children
|
||||
## Managing the account
|
||||
|
||||
`[us_family]` (block: **Profile**) renders the guardian's manage-children screen:
|
||||
### Your details
|
||||
|
||||
The profile screen opens with the account holder's own record, because the
|
||||
alternative was a page called **Your profile** on which the one person who could
|
||||
not be edited was you. The form saves through
|
||||
`GuardianService::updateSelf()` and holds:
|
||||
|
||||
- **Your name** — `display_name` and `nickname`, written together for the reason
|
||||
`updateChild()` does: `UserName` reads the nickname first, and leaving it
|
||||
behind would put the account's email address back on every screen that names a
|
||||
person.
|
||||
- **I take lessons myself** — the positive of `us_guardian_only`, so the form
|
||||
asks the question the way a person answers it and `updateSelf()` is the one
|
||||
place the sense is flipped. This is what makes good on "they can put the
|
||||
account right from the profile page": an account that registered as a pure
|
||||
guardian and later took up lessons — or ticked the wrong radio at signup — can
|
||||
now correct itself instead of asking the studio to.
|
||||
- **Your birth year** — `us_birth_year`, the same meta and the same
|
||||
`normaliseBirthYear()` rule every student is held to.
|
||||
|
||||
Two decisions worth keeping:
|
||||
|
||||
- The birth-year field carries **no `required` attribute**. It is asked of a
|
||||
student only, and the family screen loads no JavaScript, so a browser-enforced
|
||||
`required` would leave a guardian who books solely for other people unable to
|
||||
submit the form at all. `FamilyPage::handleSelf()` enforces it against the
|
||||
checkbox instead, which is where the condition actually lives.
|
||||
- Unticking **I take lessons myself** does **not** clear a stored birth year.
|
||||
The box says who books, not "forget what you know about me", and someone who
|
||||
ticks it back on the next visit should find their details as they left them.
|
||||
|
||||
The **email** is shown but not editable: it is the account's `user_login` as
|
||||
well as its address, so changing it is a studio-side job rather than a
|
||||
profile-screen one.
|
||||
|
||||
The account-scope questions are not re-asked here, in either direction — the
|
||||
child rows do not offer them on edit either, and a studio that needs a newly
|
||||
self-declared student's answers asks for them the same way it would for any
|
||||
other change of circumstance.
|
||||
|
||||
### Managing children
|
||||
|
||||
The rest of `[us_family]` (block: **Profile**) is the manage-children screen:
|
||||
list the children, add one, edit a name/birth year, remove one.
|
||||
|
||||
- **Add** creates another accountless child user and links it. Account-scope
|
||||
@@ -366,7 +409,8 @@ need — via `GuardianService::contactFor()`.
|
||||
- Models: `Unsupervised\Schedular\Guardian\GuardianLink`
|
||||
- Repository: `Unsupervised\Schedular\Guardian\GuardianRepository`
|
||||
- Service: `Unsupervised\Schedular\Guardian\GuardianService` (child creation,
|
||||
`canActFor()`, `payerFor()`, `contactFor()`, removal rules)
|
||||
`canActFor()`, `payerFor()`, `contactFor()`, removal rules, and the account
|
||||
holder's own record via `accountHolder()`/`updateSelf()`)
|
||||
- Login block: `Unsupervised\Schedular\Guardian\ChildLoginGate`
|
||||
- Frontend: `Unsupervised\Schedular\Guardian\FamilyPage` (`[us_family]`)
|
||||
- Shared question field: `Unsupervised\Schedular\Registration\QuestionField`
|
||||
|
||||
+23
-4
@@ -176,10 +176,26 @@ class BlockPreview {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample family (manage-children) page: two representative children and the
|
||||
* add form, with the controls inert so the editor preview cannot post.
|
||||
* Sample family page: the account holder's own details, two representative
|
||||
* children and the add form, with the controls inert so the editor preview
|
||||
* cannot post.
|
||||
*/
|
||||
public static function family(): string {
|
||||
$self = sprintf(
|
||||
'<h4>%s</h4><p class="us-family-self-email">%s <span>[email protected]</span></p>'
|
||||
. '<p><label for="us-own-name">%s' . self::REQUIRED_MARK . '</label><input type="text" id="us-own-name" value="%s"></p>'
|
||||
. '<p><label><input type="checkbox" checked disabled> %s</label></p>'
|
||||
. '<p><label for="us-own-birth-year">%s</label><input type="number" id="us-own-birth-year" placeholder="YYYY"></p>'
|
||||
. '<p><button type="button" disabled>%s</button></p>',
|
||||
esc_html__( 'Your details', 'unsupervised-schedular' ),
|
||||
esc_html__( 'Email', 'unsupervised-schedular' ),
|
||||
esc_html__( 'Your name', 'unsupervised-schedular' ),
|
||||
esc_attr__( 'Grace Hopper', 'unsupervised-schedular' ),
|
||||
esc_html__( 'I take lessons myself', 'unsupervised-schedular' ),
|
||||
esc_html__( 'Your birth year', 'unsupervised-schedular' ),
|
||||
esc_html__( 'Save my details', 'unsupervised-schedular' )
|
||||
);
|
||||
|
||||
$children = '';
|
||||
foreach ( [ 'Ada Lovelace', 'Alan Turing' ] as $name ) {
|
||||
$children .= sprintf(
|
||||
@@ -202,9 +218,12 @@ class BlockPreview {
|
||||
);
|
||||
|
||||
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 students here.', 'unsupervised-schedular' ) ),
|
||||
'<div class="us-family">%s<h3>%s</h3><form class="us-family-self">%s</form>'
|
||||
. '<h4>%s</h4><ul class="us-family-list">%s</ul><form class="us-family-add">%s</form></div>',
|
||||
self::note( __( 'Editor preview — signed-in visitors see and manage their own details and students here.', 'unsupervised-schedular' ) ),
|
||||
esc_html__( 'Your profile', 'unsupervised-schedular' ),
|
||||
$self,
|
||||
esc_html__( 'Your students', 'unsupervised-schedular' ),
|
||||
$children,
|
||||
$add
|
||||
);
|
||||
|
||||
@@ -10,8 +10,8 @@ use Unsupervised\Schedular\Registration\QuestionRepository;
|
||||
use Unsupervised\Schedular\Val;
|
||||
|
||||
/**
|
||||
* The guardian's "my family" screen (`[us_family]`): list, add, edit and remove
|
||||
* the children they book for.
|
||||
* The guardian's "my family" screen (`[us_family]`): their own details, plus
|
||||
* list, add, edit and remove the children they book for.
|
||||
*
|
||||
* Submissions are processed on `template_redirect` — before any output — and
|
||||
* post/redirect/get back to the page, so a refresh cannot resubmit and add the
|
||||
@@ -23,6 +23,7 @@ class FamilyPage {
|
||||
private const RESULT_ADDED = 'added';
|
||||
private const RESULT_UPDATED = 'updated';
|
||||
private const RESULT_REMOVED = 'removed';
|
||||
private const RESULT_SELF = 'self';
|
||||
|
||||
/**
|
||||
* Error from the most recent submission processed on `template_redirect`,
|
||||
@@ -58,6 +59,7 @@ class FamilyPage {
|
||||
|
||||
$userId = get_current_user_id();
|
||||
|
||||
$self = $this->guardians->accountHolder( $userId );
|
||||
$children = $this->guardians->children( $userId );
|
||||
$questions = $this->questions->findByScope( Question::SCOPE_ACCOUNT, activeOnly: true );
|
||||
$error = $this->submitError;
|
||||
@@ -99,6 +101,7 @@ class FamilyPage {
|
||||
'add' => $this->handleAdd( $userId ),
|
||||
'edit' => $this->handleEdit( $userId ),
|
||||
'remove' => $this->handleRemove( $userId ),
|
||||
'self' => $this->handleSelf( $userId ),
|
||||
default => new \WP_Error( 'unknown_action', __( 'Unrecognised request.', 'unsupervised-schedular' ) ),
|
||||
};
|
||||
|
||||
@@ -149,6 +152,21 @@ class FamilyPage {
|
||||
return $error ?? self::RESULT_UPDATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the account holder's own details. The birth year is only asked of a
|
||||
* student, so it is the checkbox — not the browser — that decides whether one
|
||||
* is required; the field carries no `required` attribute, or a guardian who
|
||||
* books only for other people could never submit the form at all.
|
||||
*/
|
||||
private function handleSelf( int $userId ): string|\WP_Error {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller.
|
||||
$isStudent = isset( $_POST['is_student'] );
|
||||
|
||||
$error = $this->guardians->updateSelf( $userId, $this->postString( 'own_name' ), $this->postString( 'own_birth_year' ), $isStudent );
|
||||
|
||||
return $error ?? self::RESULT_SELF;
|
||||
}
|
||||
|
||||
private function handleRemove( int $guardianId ): string|\WP_Error {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller.
|
||||
$childId = absint( Val::int( $_POST['child_id'] ?? 0 ) );
|
||||
@@ -244,6 +262,7 @@ class FamilyPage {
|
||||
self::RESULT_ADDED => __( 'Student added.', 'unsupervised-schedular' ),
|
||||
self::RESULT_UPDATED => __( 'Details updated.', 'unsupervised-schedular' ),
|
||||
self::RESULT_REMOVED => __( 'Student removed.', 'unsupervised-schedular' ),
|
||||
self::RESULT_SELF => __( 'Your details have been updated.', 'unsupervised-schedular' ),
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -163,6 +163,52 @@ class GuardianService {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the account holder's own details from the profile screen: their
|
||||
* name, whether they are a student in their own right, and — when they are —
|
||||
* their birth year.
|
||||
*
|
||||
* `$isStudent` is the positive of what {@see META_GUARDIAN_ONLY} stores, so
|
||||
* the form can ask the question the way a person would answer it and this is
|
||||
* the single place the sense is flipped.
|
||||
*
|
||||
* Returns null on success, mirroring {@see updateChild()}.
|
||||
*/
|
||||
public function updateSelf( int $userId, string $name, string $birthYear, bool $isStudent ): ?\WP_Error {
|
||||
$name = trim( $name );
|
||||
if ( '' === $name ) {
|
||||
return new \WP_Error( 'missing_name', __( 'Please give your name.', 'unsupervised-schedular' ) );
|
||||
}
|
||||
|
||||
if ( $isStudent && 0 === self::normaliseBirthYear( $birthYear ) ) {
|
||||
return new \WP_Error( 'missing_birth_year', self::ownBirthYearError() );
|
||||
}
|
||||
|
||||
$result = wp_update_user(
|
||||
[
|
||||
'ID' => $userId,
|
||||
'display_name' => $name,
|
||||
'nickname' => $name,
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$this->setGuardianOnly( $userId, ! $isStudent );
|
||||
|
||||
// Only written when they are a student. Saying "I only book for other
|
||||
// people" is a statement about who books, not an instruction to forget a
|
||||
// year already on file — and someone who ticks the box back on the next
|
||||
// visit should find their own details as they left them.
|
||||
if ( $isStudent ) {
|
||||
$this->setBirthYear( $userId, $birthYear );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink a child and delete their account. Refused once the child has any
|
||||
* lesson or enrolment history: their id is referenced by lessons, payments and
|
||||
@@ -319,6 +365,26 @@ class GuardianService {
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The account holder's own details, as the profile screen's form needs them.
|
||||
* The counterpart to {@see children()} for the person reading the page.
|
||||
*
|
||||
* `is_student` is the positive of {@see META_GUARDIAN_ONLY} — see
|
||||
* {@see updateSelf()}, which reads it back the same way round.
|
||||
*
|
||||
* @return array{name: string, email: string, birth_year: string, is_student: bool}
|
||||
*/
|
||||
public function accountHolder( int $userId ): array {
|
||||
$user = get_userdata( $userId );
|
||||
|
||||
return [
|
||||
'name' => UserName::format( $user instanceof \WP_User ? $user : null, $userId ),
|
||||
'email' => $user instanceof \WP_User ? $user->user_email : '',
|
||||
'birth_year' => $this->birthYear( $userId ),
|
||||
'is_student' => ! self::isGuardianOnly( $userId ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The guardian behind a child, or null when the student books for themselves.
|
||||
*
|
||||
|
||||
@@ -8,6 +8,7 @@ if (! defined('ABSPATH')) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array{name: string, email: string, birth_year: string, is_student: bool} $self The account holder's own details.
|
||||
* @var list<array{id: int, name: string, birth_year: string, relationship: string}> $children
|
||||
* @var list<\Unsupervised\Schedular\Registration\Question> $questions Account-scope questions, asked once per child.
|
||||
* @var string $error Validation error from the last submission, if any.
|
||||
@@ -26,6 +27,52 @@ if (! defined('ABSPATH')) {
|
||||
<p class="us-error" role="alert"><?php echo esc_html($error); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="" class="us-family-self">
|
||||
<?php wp_nonce_field('us_family'); ?>
|
||||
<input type="hidden" name="us_family_action" value="self">
|
||||
|
||||
<h4><?php esc_html_e('Your details', 'unsupervised-schedular'); ?></h4>
|
||||
|
||||
<?php if ($self['email'] !== '') : ?>
|
||||
<?php /* Shown, not editable: the address is the account's login, and changing it is a studio-side job. */ ?>
|
||||
<p class="us-family-self-email">
|
||||
<?php esc_html_e('Email', 'unsupervised-schedular'); ?>
|
||||
<span><?php echo esc_html($self['email']); ?></span>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<p>
|
||||
<label for="us-own-name"><?php esc_html_e('Your name', 'unsupervised-schedular'); ?> <span class="us-required" aria-hidden="true">*</span></label>
|
||||
<input type="text" name="own_name" id="us-own-name" autocomplete="name" required value="<?php echo esc_attr($self['name']); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="is_student" value="1"<?php checked($self['is_student']); ?>>
|
||||
<?php esc_html_e('I take lessons myself', 'unsupervised-schedular'); ?>
|
||||
</label>
|
||||
<span class="us-field-hint"><?php esc_html_e('Leave this unticked if you only book for the students below — you will not be offered as a student yourself.', 'unsupervised-schedular'); ?></span>
|
||||
</p>
|
||||
<?php
|
||||
/*
|
||||
* Asked of a student only, so `required` is deliberately absent: the box
|
||||
* above is what decides, and the browser cannot be told to enforce a
|
||||
* field conditionally without JavaScript this page does not load.
|
||||
* `handleSelf()` enforces it on the server either way.
|
||||
*/
|
||||
?>
|
||||
<p>
|
||||
<label for="us-own-birth-year"><?php esc_html_e('Your birth year', 'unsupervised-schedular'); ?></label>
|
||||
<input type="number" name="own_birth_year" id="us-own-birth-year" value="<?php echo esc_attr($self['birth_year']); ?>" min="1900" max="<?php echo esc_attr(current_time('Y')); ?>" step="1" inputmode="numeric" autocomplete="bday-year" placeholder="<?php esc_attr_e('YYYY', 'unsupervised-schedular'); ?>" aria-describedby="us-own-birth-year-hint">
|
||||
<span class="us-field-hint" id="us-own-birth-year-hint"><?php esc_html_e('Needed only if you take lessons yourself.', 'unsupervised-schedular'); ?></span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<button type="submit"><?php esc_html_e('Save my details', 'unsupervised-schedular'); ?></button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<h4><?php esc_html_e('Your students', 'unsupervised-schedular'); ?></h4>
|
||||
|
||||
<?php if (empty($children)) : ?>
|
||||
<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 : ?>
|
||||
|
||||
@@ -74,6 +74,23 @@ class BlockPreviewTest extends TestCase
|
||||
self::assertStringContainsString('us-editor-note', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* The preview is what someone placing the block styles against, so it has to
|
||||
* show both halves of the page — your own details as well as your students'.
|
||||
*/
|
||||
public function testFamilyPreviewShowsTheAccountHoldersDetailsAndTheirStudents(): void
|
||||
{
|
||||
$html = BlockPreview::family();
|
||||
|
||||
self::assertStringContainsString('class="us-family-self"', $html);
|
||||
self::assertStringContainsString('id="us-own-name"', $html);
|
||||
self::assertStringContainsString('id="us-own-birth-year"', $html);
|
||||
self::assertStringContainsString('I take lessons myself', $html);
|
||||
self::assertStringContainsString('class="us-family-list"', $html);
|
||||
self::assertStringContainsString('class="us-family-add"', $html);
|
||||
self::assertStringContainsString('us-editor-note', $html);
|
||||
}
|
||||
|
||||
public function testRegistrationPreviewShowsADisabledSampleForm(): void
|
||||
{
|
||||
$html = BlockPreview::registration();
|
||||
|
||||
@@ -76,6 +76,21 @@ class FamilyPageTest extends TestCase
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* The account holder's own details, which every render reads.
|
||||
*
|
||||
* @param array{name?: string, email?: string, birth_year?: string, is_student?: bool} $overrides
|
||||
*/
|
||||
private function expectAccountHolder(array $overrides = []): void
|
||||
{
|
||||
$this->guardians->shouldReceive('accountHolder')->with(5)->andReturn($overrides + [
|
||||
'name' => 'Grace',
|
||||
'email' => '[email protected]',
|
||||
'birth_year' => '1984',
|
||||
'is_student' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A question required of everyone, or of nobody — the shape every question
|
||||
* had before the account holder and the students could differ, and the shape
|
||||
@@ -105,6 +120,7 @@ class FamilyPageTest extends TestCase
|
||||
|
||||
public function testRenderListsTheGuardiansChildren(): void
|
||||
{
|
||||
$this->expectAccountHolder();
|
||||
$this->guardians->shouldReceive('children')->once()->with(5)->andReturn([
|
||||
['id' => 42, 'name' => 'Ada', 'birth_year' => '2015', 'relationship' => 'Parent'],
|
||||
]);
|
||||
@@ -117,6 +133,101 @@ class FamilyPageTest extends TestCase
|
||||
self::assertStringContainsString('Add a student', $html);
|
||||
}
|
||||
|
||||
public function testRenderShowsTheAccountHoldersOwnDetails(): void
|
||||
{
|
||||
$this->expectAccountHolder();
|
||||
$this->guardians->shouldReceive('children')->andReturn([]);
|
||||
$this->questions->shouldReceive('findByScope')->andReturn([]);
|
||||
|
||||
$html = $this->page->render([]);
|
||||
|
||||
self::assertStringContainsString('Your details', $html);
|
||||
self::assertStringContainsString('value="Grace"', $html);
|
||||
self::assertStringContainsString('[email protected]', $html);
|
||||
self::assertStringContainsString('value="1984"', $html);
|
||||
// A student in their own right has the box ticked.
|
||||
self::assertStringContainsString("checked='checked'", $html);
|
||||
}
|
||||
|
||||
public function testAGuardianOnlyAccountRendersTheStudentBoxUnticked(): void
|
||||
{
|
||||
$this->expectAccountHolder(['is_student' => false]);
|
||||
$this->guardians->shouldReceive('children')->andReturn([]);
|
||||
$this->questions->shouldReceive('findByScope')->andReturn([]);
|
||||
|
||||
$html = $this->page->render([]);
|
||||
|
||||
self::assertStringContainsString('name="is_student"', $html);
|
||||
self::assertStringNotContainsString("checked='checked'", $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* The birth-year field must not carry `required`: it is asked of a student
|
||||
* only, and the browser would otherwise block a guardian who books solely
|
||||
* for other people from ever saving the form.
|
||||
*/
|
||||
public function testTheOwnBirthYearFieldIsNotBrowserRequired(): void
|
||||
{
|
||||
$this->expectAccountHolder(['is_student' => false, 'birth_year' => '']);
|
||||
$this->guardians->shouldReceive('children')->andReturn([]);
|
||||
$this->questions->shouldReceive('findByScope')->andReturn([]);
|
||||
|
||||
$html = $this->page->render([]);
|
||||
|
||||
self::assertMatchesRegularExpression('/<input[^>]*name="own_birth_year"(?![^>]*\brequired\b)[^>]*>/', $html);
|
||||
}
|
||||
|
||||
public function testSavingOwnDetailsDelegatesToTheServiceAndRedirects(): void
|
||||
{
|
||||
$_POST = [
|
||||
'us_family_action' => 'self',
|
||||
'own_name' => 'Grace H',
|
||||
'own_birth_year' => '1984',
|
||||
'is_student' => '1',
|
||||
];
|
||||
|
||||
$this->guardians->shouldReceive('updateSelf')->once()->with(5, 'Grace H', '1984', true)->andReturn(null);
|
||||
|
||||
$captured = null;
|
||||
$this->capturingPage($captured)->maybeHandleSubmit();
|
||||
|
||||
self::assertSame('https://studio.test/family/?us_family=self', $captured);
|
||||
}
|
||||
|
||||
/** An unticked checkbox is simply absent from the post — that is the "no". */
|
||||
public function testAnAbsentStudentBoxSavesTheAccountAsGuardianOnly(): void
|
||||
{
|
||||
$_POST = [
|
||||
'us_family_action' => 'self',
|
||||
'own_name' => 'Grace H',
|
||||
'own_birth_year' => '',
|
||||
];
|
||||
|
||||
$this->guardians->shouldReceive('updateSelf')->once()->with(5, 'Grace H', '', false)->andReturn(null);
|
||||
|
||||
$captured = null;
|
||||
$this->capturingPage($captured)->maybeHandleSubmit();
|
||||
|
||||
self::assertSame('https://studio.test/family/?us_family=self', $captured);
|
||||
}
|
||||
|
||||
public function testOwnDetailsRefusalIsShownRatherThanRedirected(): void
|
||||
{
|
||||
$_POST = ['us_family_action' => 'self', 'own_name' => 'Grace', 'is_student' => '1'];
|
||||
|
||||
$this->guardians->shouldReceive('updateSelf')->once()->andReturn(
|
||||
new \WP_Error('missing_birth_year', 'Please give your birth year.')
|
||||
);
|
||||
|
||||
$captured = null;
|
||||
$page = $this->capturingPage($captured);
|
||||
$page->shouldNotReceive('redirect');
|
||||
|
||||
$page->maybeHandleSubmit();
|
||||
|
||||
self::assertNull($captured);
|
||||
}
|
||||
|
||||
public function testAddCreatesTheChildRecordsItsAnswersAndRedirects(): void
|
||||
{
|
||||
$_POST = [
|
||||
@@ -347,6 +458,7 @@ class FamilyPageTest extends TestCase
|
||||
{
|
||||
$_GET = ['us_family' => 'added'];
|
||||
|
||||
$this->expectAccountHolder();
|
||||
$this->guardians->shouldReceive('children')->andReturn([]);
|
||||
$this->questions->shouldReceive('findByScope')->andReturn([]);
|
||||
|
||||
|
||||
@@ -321,6 +321,87 @@ class GuardianServiceTest extends TestCase
|
||||
self::assertSame('2015', $this->meta[42][GuardianService::META_BIRTH_YEAR]);
|
||||
}
|
||||
|
||||
public function testUpdateSelfRenamesAndStoresTheBirthYear(): void
|
||||
{
|
||||
$this->meta[5][GuardianService::META_GUARDIAN_ONLY] = '1';
|
||||
|
||||
Functions\expect('wp_update_user')
|
||||
->once()
|
||||
->with(['ID' => 5, 'display_name' => 'Grace H', 'nickname' => 'Grace H'])
|
||||
->andReturn(5);
|
||||
|
||||
self::assertNull($this->service->updateSelf(5, 'Grace H', '1984', true));
|
||||
|
||||
self::assertSame('1984', $this->meta[5][GuardianService::META_BIRTH_YEAR]);
|
||||
// Saying they take lessons makes them a bookable student again.
|
||||
self::assertFalse(GuardianService::isGuardianOnly(5));
|
||||
}
|
||||
|
||||
public function testUpdateSelfMarksTheAccountGuardianOnly(): void
|
||||
{
|
||||
Functions\when('wp_update_user')->justReturn(5);
|
||||
|
||||
self::assertNull($this->service->updateSelf(5, 'Grace H', '', false));
|
||||
|
||||
self::assertSame('1', $this->meta[5][GuardianService::META_GUARDIAN_ONLY]);
|
||||
}
|
||||
|
||||
/**
|
||||
* "I only book for other people" says who books, not "forget my birth year" —
|
||||
* ticking the box back on should not have cost them what was on file.
|
||||
*/
|
||||
public function testUpdateSelfKeepsAStoredBirthYearWhenTheyAreNoLongerAStudent(): void
|
||||
{
|
||||
$this->meta[5][GuardianService::META_BIRTH_YEAR] = '1984';
|
||||
|
||||
Functions\when('wp_update_user')->justReturn(5);
|
||||
|
||||
self::assertNull($this->service->updateSelf(5, 'Grace H', '', false));
|
||||
|
||||
self::assertSame('1984', $this->meta[5][GuardianService::META_BIRTH_YEAR]);
|
||||
}
|
||||
|
||||
public function testUpdateSelfRejectsABlankName(): void
|
||||
{
|
||||
Functions\expect('wp_update_user')->never();
|
||||
|
||||
self::assertInstanceOf(\WP_Error::class, $this->service->updateSelf(5, ' ', '1984', true));
|
||||
}
|
||||
|
||||
/**
|
||||
* The browser cannot enforce the year conditionally, so the server is the
|
||||
* only thing standing between a student and a nonsense age on their record.
|
||||
*
|
||||
* @dataProvider unusableBirthYears
|
||||
*/
|
||||
public function testUpdateSelfRefusesAnUnusableBirthYearFromAStudent(string $submitted): void
|
||||
{
|
||||
Functions\expect('wp_update_user')->never();
|
||||
|
||||
self::assertInstanceOf(\WP_Error::class, $this->service->updateSelf(5, 'Grace H', $submitted, true));
|
||||
}
|
||||
|
||||
public function testAccountHolderReportsTheirOwnDetails(): void
|
||||
{
|
||||
$this->meta[5][GuardianService::META_BIRTH_YEAR] = '1984';
|
||||
|
||||
Functions\when('get_userdata')->justReturn($this->user(5, 'Grace', 'Hopper', email: '[email protected]'));
|
||||
|
||||
self::assertSame(
|
||||
['name' => 'Grace Hopper', 'email' => '[email protected]', 'birth_year' => '1984', 'is_student' => true],
|
||||
$this->service->accountHolder(5)
|
||||
);
|
||||
}
|
||||
|
||||
public function testAccountHolderReportsAGuardianOnlyAccountAsNotAStudent(): void
|
||||
{
|
||||
$this->meta[5][GuardianService::META_GUARDIAN_ONLY] = '1';
|
||||
|
||||
Functions\when('get_userdata')->justReturn($this->user(5, 'Grace', 'Hopper', email: '[email protected]'));
|
||||
|
||||
self::assertFalse($this->service->accountHolder(5)['is_student']);
|
||||
}
|
||||
|
||||
public function testRemoveChildUnlinksAndDeletesAChildWithNoHistory(): void
|
||||
{
|
||||
$this->guardians->shouldReceive('isGuardianOf')->with(5, 42)->andReturn(true);
|
||||
|
||||
Reference in New Issue
Block a user