Require a name and birth year for every student #154

Merged
thatguygriff merged 1 commits from feature/148-required-name-and-birth-year into main 2026-07-30 00:04:52 +00:00
10 changed files with 215 additions and 52 deletions
+1
View File
@@ -14,6 +14,7 @@ each change under the current top section as you work.
## [1.3.1]
### Changed
- A student's **name and birth year are now required**, marked in the form the same way a required registration question is and enforced on the server whichever way they were submitted. On signup the requirement applies only once the parent/guardian box is ticked, so registering for yourself is unaffected. A student block you have started filling in is now reported back to you rather than silently dropped when the name is missing — only a completely untouched spare block is still ignored.
- Signup and the profile page now ask for a **birth year** rather than a full date of birth — a four-digit year between 1900 and the current year, with anything else discarded rather than stored. Students added before this change keep showing a birth year, derived from the date already on file; that old full date is then dropped the first time the record is saved, so the studio ends up holding only what it now asks for. No bulk purge runs, so a site wanting the remaining old dates gone should clear the `us_date_of_birth` user meta directly.
- 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.
+14
View File
@@ -111,6 +111,16 @@
function sync() {
children.hidden = !toggle.checked;
// Each student's name and birth year are required, but only once the
// block is in play: a `required` field inside a hidden container makes
// the form unsubmittable with no way to reach the offending control, so
// the attribute goes on and comes off with the block itself. The server
// enforces the same rule either way.
var required = children.querySelectorAll('[data-us-child-required]');
for (var r = 0; r < required.length; r++) {
required[r].required = toggle.checked;
}
if (!steps) {
return;
}
@@ -141,6 +151,10 @@
nextIndex += 1;
children.insertBefore(clone, addButton.parentNode);
// The clone carries the data attribute but not necessarily the
// current required state, so settle it the same way as the rest.
sync();
});
}
}
+22 -6
View File
@@ -147,20 +147,36 @@ least one child name.
Per child the form collects:
- **Name** (required)
- **Birth year** (optional, `us_birth_year` meta) — a four-digit year between
1900 and the current year. Anything else is discarded rather than stored, so
a typo cannot leave a nonsense age on the record.
- **Birth year** (required, `us_birth_year` meta) — a four-digit year between
1900 and the current year. `GuardianService::normaliseBirthYear()` is the one
definition of what counts, shared by the signup form's up-front validation and
by `createChild()`/`updateChild()` themselves, so a bad year is refused rather
than quietly discarded and a typo cannot leave a nonsense age on the record.
- **Every account-scope registration question** (`Registration\Question`,
`SCOPE_ACCOUNT`) — asked once per child, not once per guardian, because in
practice they describe the student (instrument, level, school). The guardian
answers them on the child's behalf; the answer row's `student_id` is the child.
Name and birth year are marked required in the labels the same way a required
question is, but the signup form **cannot** lean on the browser to enforce them:
the child blocks are hidden until the parent/guardian box is ticked, and a
`required` field inside a hidden container makes the form unsubmittable with no
control the user can reach to fix. `register.js` therefore puts `required` on
and takes it off along with the block itself (`[data-us-child-required]`), and
the server checks regardless — which is what makes the rule hold with
JavaScript off. The profile screen has no such problem: its forms are always
visible, so the attribute is static there.
Order of operations in `RegistrationPage::handleSubmit()`:
1. Validate the guardian's own fields (email, password, policies).
2. Validate **every** child block — a missing child name or a missing required
per-child answer fails the whole submission **before** any user is created, so
a half-registered family is never left behind.
2. Validate **every** child block — a missing name, a missing or unusable birth
year, or a missing required per-child answer fails the whole submission
**before** any user is created, so a half-registered family is never left
behind. An **entirely empty** block is dropped instead, because the form
always renders one spare for "add another"; a block with anything at all
typed into it is kept and reported on, rather than silently discarding what
the guardian entered.
3. Create the guardian user.
4. For each child: create the accountless user, link it, record its answers, and
record the signup policy acceptances **against the child** with
+28 -8
View File
@@ -289,6 +289,20 @@ class RegistrationPage {
return esc_html__( 'Please add at least one student, or uncheck the parent/guardian option.', 'unsupervised-schedular' );
}
// Name and birth year are required per student, and are checked here for
// the same reason the questions below are: the child blocks are hidden
// until the guardian box is ticked, so the browser cannot be asked to
// enforce them without blocking a signup that has no children at all.
foreach ( $children as $child ) {
if ( '' === $child['name'] ) {
return esc_html__( 'Please give each student a name.', 'unsupervised-schedular' );
}
if ( 0 === GuardianService::normaliseBirthYear( $child['birth_year'] ) ) {
return esc_html( GuardianService::birthYearError() );
}
}
foreach ( $isGuardian ? array_column( $children, 'answers' ) : [ $answers ] as $set ) {
foreach ( $accountQuestions as $question ) {
if ( $question->isRequired && '' === trim( (string) ( $set[ (int) $question->id ] ?? '' ) ) ) {
@@ -477,9 +491,13 @@ class RegistrationPage {
/**
* The child blocks submitted with a guardian signup, as
* `children[<n>][name|birth_year|answers]`. Blocks with no name are dropped rather
* than rejected — the form always renders one spare block for "add another",
* and an untouched spare is not a mistake the guardian needs telling about.
* `children[<n>][name|birth_year|answers]`.
*
* An **entirely empty** block is dropped rather than rejected — the form always
* renders one spare for "add another", and an untouched spare is not a mistake
* the guardian needs telling about. A block with anything at all filled in is
* kept, so {@see handleSubmit()} can reject it for the missing name or birth
* year rather than silently discarding what they typed.
*
* @return list<array{name: string, birth_year: string, answers: array<int, string>}>
*/
@@ -497,19 +515,21 @@ class RegistrationPage {
continue;
}
$name = sanitize_text_field( Val::string( wp_unslash( $child['name'] ?? '' ) ) );
if ( '' === trim( $name ) ) {
continue;
}
$name = trim( sanitize_text_field( Val::string( wp_unslash( $child['name'] ?? '' ) ) ) );
$birthYear = trim( sanitize_text_field( Val::string( wp_unslash( $child['birth_year'] ?? '' ) ) ) );
$answers = [];
foreach ( (array) ( $child['answers'] ?? [] ) as $questionId => $value ) {
$answers[ absint( Val::int( $questionId ) ) ] = sanitize_textarea_field( Val::string( wp_unslash( $value ) ) );
}
if ( '' === $name && '' === $birthYear && '' === trim( implode( '', $answers ) ) ) {
continue;
}
$out[] = [
'name' => $name,
'birth_year' => sanitize_text_field( Val::string( wp_unslash( $child['birth_year'] ?? '' ) ) ),
'birth_year' => $birthYear,
'answers' => $answers,
];
}
+8 -2
View File
@@ -15,6 +15,12 @@ namespace Unsupervised\Schedular;
*/
class BlockPreview {
/**
* The marker a required field's label carries, matching the one
* {@see Registration\QuestionField::render()} puts on a required question.
*/
private const REQUIRED_MARK = ' <span class="us-required" aria-hidden="true">*</span>';
/**
* Sample booking page.
*
@@ -186,8 +192,8 @@ class BlockPreview {
}
$add = sprintf(
'<h4>%s</h4><p><label for="us-child-name">%s</label><input type="text" id="us-child-name"></p>'
. '<p><label for="us-child-birth-year">%s</label><input type="number" id="us-child-birth-year" placeholder="YYYY"></p>'
'<h4>%s</h4><p><label for="us-child-name">%s' . self::REQUIRED_MARK . '</label><input type="text" id="us-child-name"></p>'
. '<p><label for="us-child-birth-year">%s' . self::REQUIRED_MARK . '</label><input type="number" id="us-child-birth-year" placeholder="YYYY"></p>'
. '<p><button type="button" disabled>%s</button></p>',
esc_html__( 'Add a student', 'unsupervised-schedular' ),
esc_html__( 'Name', 'unsupervised-schedular' ),
+28 -4
View File
@@ -60,8 +60,8 @@ class GuardianService {
* is random and discarded — it is never stored anywhere readable, emailed, or
* shown — so the account cannot be signed into even if the gate were removed.
*
* Returns the new user ID, or a `WP_Error` when the name is blank or WordPress
* refuses the insert.
* Returns the new user ID, or a `WP_Error` when the name is blank, the birth
* year is missing or unusable, or WordPress refuses the insert.
*/
public function createChild( int $guardianId, string $name, string $birthYear = '', string $relationship = '' ): int|\WP_Error {
$name = trim( $name );
@@ -69,6 +69,10 @@ class GuardianService {
return new \WP_Error( 'missing_name', __( 'Please give each student a name.', 'unsupervised-schedular' ) );
}
if ( 0 === self::normaliseBirthYear( $birthYear ) ) {
return new \WP_Error( 'missing_birth_year', self::birthYearError() );
}
$email = $this->childEmail();
$userId = wp_insert_user(
[
@@ -127,6 +131,10 @@ class GuardianService {
return new \WP_Error( 'missing_name', __( 'Please give each student a name.', 'unsupervised-schedular' ) );
}
if ( 0 === self::normaliseBirthYear( $birthYear ) ) {
return new \WP_Error( 'missing_birth_year', self::birthYearError() );
}
$result = wp_update_user(
[
'ID' => $studentId,
@@ -352,7 +360,7 @@ class GuardianService {
private function setBirthYear( int $userId, string $birthYear ): void {
delete_user_meta( $userId, self::META_DOB );
$year = $this->normaliseBirthYear( $birthYear );
$year = self::normaliseBirthYear( $birthYear );
if ( 0 === $year ) {
delete_user_meta( $userId, self::META_BIRTH_YEAR );
@@ -366,8 +374,11 @@ class GuardianService {
* A submitted birth year as an integer, or 0 when it is blank, not a number,
* or outside {@see MIN_BIRTH_YEAR}..this year. A year in the future is a typo
* every time, so it is refused rather than stored.
*
* Public and static so the signup form can reject a bad year up front, before
* it creates any users, without a second copy of the rule to keep in step.
*/
private function normaliseBirthYear( string $birthYear ): int {
public static function normaliseBirthYear( string $birthYear ): int {
$birthYear = trim( $birthYear );
if ( '' === $birthYear || 1 !== preg_match( '/^\d{4}$/', $birthYear ) ) {
@@ -383,6 +394,19 @@ class GuardianService {
return $year;
}
/**
* The message shown when a birth year is missing or unusable. One phrasing,
* shared by the signup form and the profile screen, so a guardian is told the
* same thing whichever way they got there.
*/
public static function birthYearError(): string {
return sprintf(
/* translators: %d: the earliest birth year the form accepts. */
__( 'Please give each student a birth year, as four digits from %d onwards.', 'unsupervised-schedular' ),
self::MIN_BIRTH_YEAR
);
}
/**
* A child's birth year, or an empty string when none is recorded.
*
+6 -6
View File
@@ -38,12 +38,12 @@ if (! defined('ABSPATH')) {
<input type="hidden" name="us_family_action" value="edit">
<input type="hidden" name="child_id" value="<?php echo esc_attr((string) $child['id']); ?>">
<p>
<label for="us-edit-name-<?php echo esc_attr((string) $child['id']); ?>"><?php esc_html_e('Name', 'unsupervised-schedular'); ?></label>
<label for="us-edit-name-<?php echo esc_attr((string) $child['id']); ?>"><?php esc_html_e('Name', 'unsupervised-schedular'); ?> <span class="us-required" aria-hidden="true">*</span></label>
<input type="text" name="child_name" id="us-edit-name-<?php echo esc_attr((string) $child['id']); ?>" value="<?php echo esc_attr($child['name']); ?>" required>
</p>
<p>
<label for="us-edit-birth-year-<?php echo esc_attr((string) $child['id']); ?>"><?php esc_html_e('Birth year', 'unsupervised-schedular'); ?></label>
<input type="number" name="child_birth_year" id="us-edit-birth-year-<?php echo esc_attr((string) $child['id']); ?>" value="<?php echo esc_attr($child['birth_year']); ?>" min="1900" max="<?php echo esc_attr(current_time('Y')); ?>" step="1" inputmode="numeric" placeholder="<?php esc_attr_e('YYYY', 'unsupervised-schedular'); ?>">
<label for="us-edit-birth-year-<?php echo esc_attr((string) $child['id']); ?>"><?php esc_html_e('Birth year', 'unsupervised-schedular'); ?> <span class="us-required" aria-hidden="true">*</span></label>
<input type="number" name="child_birth_year" required id="us-edit-birth-year-<?php echo esc_attr((string) $child['id']); ?>" value="<?php echo esc_attr($child['birth_year']); ?>" min="1900" max="<?php echo esc_attr(current_time('Y')); ?>" step="1" inputmode="numeric" placeholder="<?php esc_attr_e('YYYY', 'unsupervised-schedular'); ?>">
</p>
<p>
<button type="submit"><?php esc_html_e('Save', 'unsupervised-schedular'); ?></button>
@@ -76,12 +76,12 @@ if (! defined('ABSPATH')) {
<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>
<label for="us-child-name"><?php esc_html_e('Name', 'unsupervised-schedular'); ?> <span class="us-required" aria-hidden="true">*</span></label>
<input type="text" name="child_name" id="us-child-name" required>
</p>
<p>
<label for="us-child-birth-year"><?php esc_html_e('Birth year', 'unsupervised-schedular'); ?></label>
<input type="number" name="child_birth_year" id="us-child-birth-year" min="1900" max="<?php echo esc_attr(current_time('Y')); ?>" step="1" inputmode="numeric" placeholder="<?php esc_attr_e('YYYY', 'unsupervised-schedular'); ?>">
<label for="us-child-birth-year"><?php esc_html_e('Birth year', 'unsupervised-schedular'); ?> <span class="us-required" aria-hidden="true">*</span></label>
<input type="number" name="child_birth_year" id="us-child-birth-year" required min="1900" max="<?php echo esc_attr(current_time('Y')); ?>" step="1" inputmode="numeric" placeholder="<?php esc_attr_e('YYYY', 'unsupervised-schedular'); ?>">
</p>
<p>
<label for="us-child-relationship"><?php esc_html_e('Your relationship to them', 'unsupervised-schedular'); ?></label>
+4 -4
View File
@@ -86,12 +86,12 @@ if (! defined('ABSPATH')) {
<?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("Student's name", 'unsupervised-schedular'); ?></label>
<input type="text" name="children[0][name]" id="us-child-0-name">
<label for="us-child-0-name"><?php esc_html_e("Student's name", 'unsupervised-schedular'); ?> <span class="us-required" aria-hidden="true">*</span></label>
<input type="text" name="children[0][name]" id="us-child-0-name" aria-required="true" data-us-child-required>
</p>
<p>
<label for="us-child-0-birth-year"><?php esc_html_e('Birth year', 'unsupervised-schedular'); ?></label>
<input type="number" name="children[0][birth_year]" id="us-child-0-birth-year" min="1900" max="<?php echo esc_attr(current_time('Y')); ?>" step="1" inputmode="numeric" placeholder="<?php esc_attr_e('YYYY', 'unsupervised-schedular'); ?>">
<label for="us-child-0-birth-year"><?php esc_html_e('Birth year', 'unsupervised-schedular'); ?> <span class="us-required" aria-hidden="true">*</span></label>
<input type="number" name="children[0][birth_year]" id="us-child-0-birth-year" aria-required="true" data-us-child-required min="1900" max="<?php echo esc_attr(current_time('Y')); ?>" step="1" inputmode="numeric" placeholder="<?php esc_attr_e('YYYY', 'unsupervised-schedular'); ?>">
</p>
<?php foreach ($accountQuestions as $question) : ?>
<?php
+76 -10
View File
@@ -38,7 +38,11 @@ class RegistrationPageTest extends TestCase
Functions\when('sanitize_textarea_field')->alias(static fn ($v) => $v);
Functions\when('sanitize_email')->alias(static fn ($v) => $v);
Functions\when('absint')->alias(static fn ($v) => (int) $v);
Functions\when('current_time')->justReturn('2024-01-01 00:00:00');
// The birth-year check reads current_time('Y'), so answer that format
// properly rather than leaving it to cast out of the datetime string.
Functions\when('current_time')->alias(
static fn (string $type = 'mysql'): string => 'Y' === $type ? '2024' : '2024-01-01 00:00:00'
);
Functions\when('wp_enqueue_style')->justReturn(null);
Functions\when('wp_enqueue_script')->justReturn(null);
@@ -619,7 +623,7 @@ class RegistrationPageTest extends TestCase
'us_is_guardian' => '1',
'children' => [
['name' => 'Ada', 'birth_year' => '2015', 'answers' => [7 => 'Piano']],
['name' => 'Alan', 'birth_year' => '', 'answers' => [7 => 'Violin']],
['name' => 'Alan', 'birth_year' => '2017', 'answers' => [7 => 'Violin']],
// An untouched spare block is dropped, not rejected.
['name' => ' ', 'birth_year' => '', 'answers' => []],
],
@@ -634,7 +638,7 @@ class RegistrationPageTest extends TestCase
Functions\when('is_wp_error')->alias(static fn ($thing): bool => $thing instanceof \WP_Error);
$this->ctx['guardians']->shouldReceive('createChild')->once()->with(42, 'Ada', '2015')->andReturn(101);
$this->ctx['guardians']->shouldReceive('createChild')->once()->with(42, 'Alan', '')->andReturn(102);
$this->ctx['guardians']->shouldReceive('createChild')->once()->with(42, 'Alan', '2017')->andReturn(102);
$recorded = [];
$this->ctx['answers']->shouldReceive('insert')->andReturnUsing(
@@ -670,6 +674,68 @@ class RegistrationPageTest extends TestCase
self::assertStringContainsString('at least one student', $result);
}
/**
* A block the guardian actually typed into is theirs to correct, not ours to
* discard — only a wholly untouched spare is dropped. Losing the birth year
* they filled in and registering a nameless student would be worse than
* telling them what is missing.
*/
public function testGuardianSignupRejectsAHalfFilledChildRatherThanDroppingIt(): void
{
$_POST = [
'password' => 'password123',
'display_name' => 'Grace',
'us_is_guardian' => '1',
'children' => [
['name' => 'Ada', 'birth_year' => '2015', 'answers' => []],
['name' => '', 'birth_year' => '2017', 'answers' => []],
],
];
$this->ctx['questions']->shouldReceive('findByScope')->andReturn([]);
Functions\when('email_exists')->justReturn(false);
Functions\expect('wp_insert_user')->never();
$this->ctx['guardians']->shouldNotReceive('createChild');
self::assertStringContainsString(
'give each student a name',
$this->submit(new Invite(email: '[email protected]', token: 'hash'), false)
);
}
/**
* @dataProvider rejectedBirthYears
*/
public function testGuardianSignupRejectsAChildWithoutAUsableBirthYear(string $submitted): void
{
$_POST = [
'password' => 'password123',
'display_name' => 'Grace',
'us_is_guardian' => '1',
'children' => [['name' => 'Ada', 'birth_year' => $submitted, 'answers' => []]],
];
$this->ctx['questions']->shouldReceive('findByScope')->andReturn([]);
Functions\when('email_exists')->justReturn(false);
Functions\expect('wp_insert_user')->never();
$this->ctx['guardians']->shouldNotReceive('createChild');
self::assertStringContainsString(
'birth year',
$this->submit(new Invite(email: '[email protected]', token: 'hash'), false)
);
}
/** @return array<string, array{string}> */
public static function rejectedBirthYears(): array
{
return [
'left blank' => [''],
'a full date' => ['2015-04-02'],
'in the future' => ['2027'],
];
}
/**
* Required per-child answers are validated before any user exists, so a
* missing one never leaves a half-registered family behind.
@@ -681,8 +747,8 @@ class RegistrationPageTest extends TestCase
'display_name' => 'Grace',
'us_is_guardian' => '1',
'children' => [
['name' => 'Ada', 'birth_year' => '', 'answers' => [7 => 'Piano']],
['name' => 'Alan', 'birth_year' => '', 'answers' => [7 => ' ']],
['name' => 'Ada', 'birth_year' => '2015', 'answers' => [7 => 'Piano']],
['name' => 'Alan', 'birth_year' => '2017', 'answers' => [7 => ' ']],
],
];
@@ -708,8 +774,8 @@ class RegistrationPageTest extends TestCase
'display_name' => 'Grace',
'us_is_guardian' => '1',
'children' => [
['name' => 'Ada', 'birth_year' => '', 'answers' => []],
['name' => 'Alan', 'birth_year' => '', 'answers' => []],
['name' => 'Ada', 'birth_year' => '2015', 'answers' => []],
['name' => 'Alan', 'birth_year' => '2017', 'answers' => []],
],
];
@@ -717,8 +783,8 @@ class RegistrationPageTest extends TestCase
Functions\when('wp_insert_user')->justReturn(42);
Functions\when('is_wp_error')->alias(static fn ($thing): bool => $thing instanceof \WP_Error);
$this->ctx['guardians']->shouldReceive('createChild')->once()->with(42, 'Ada', '')->andReturn(101);
$this->ctx['guardians']->shouldReceive('createChild')->once()->with(42, 'Alan', '')
$this->ctx['guardians']->shouldReceive('createChild')->once()->with(42, 'Ada', '2015')->andReturn(101);
$this->ctx['guardians']->shouldReceive('createChild')->once()->with(42, 'Alan', '2017')
->andReturn(new \WP_Error('link_failed', 'Nope.'));
$deleted = [];
@@ -745,7 +811,7 @@ class RegistrationPageTest extends TestCase
'display_name' => 'Grace',
'us_is_guardian' => '1',
'accept' => [3],
'children' => [['name' => 'Ada', 'birth_year' => '', 'answers' => []]],
'children' => [['name' => 'Ada', 'birth_year' => '2015', 'answers' => []]],
];
$version = new PolicyVersion(policyId: 1, versionNumber: 1, body: 'Terms', status: PolicyVersion::STATUS_PUBLISHED, id: 3);
+28 -12
View File
@@ -116,20 +116,35 @@ class GuardianServiceTest extends TestCase
Functions\expect('wp_delete_user')->once()->with(42);
self::assertInstanceOf(\WP_Error::class, $this->service->createChild(5, 'Ada'));
self::assertInstanceOf(\WP_Error::class, $this->service->createChild(5, 'Ada', '2015'));
}
/**
* @dataProvider unusableBirthYears
*/
public function testCreateChildClearsAnUnusableBirthYear(string $submitted): void
public function testCreateChildRefusesAnUnusableBirthYear(string $submitted): void
{
Functions\when('wp_insert_user')->justReturn(42);
$this->guardians->shouldReceive('insert')->once()->andReturn(7);
// Refused before anything is written, so no orphan user is left behind.
Functions\expect('wp_insert_user')->never();
$this->guardians->shouldNotReceive('insert');
$this->service->createChild(5, 'Ada', $submitted);
$result = $this->service->createChild(5, 'Ada', $submitted);
self::assertArrayNotHasKey(GuardianService::META_BIRTH_YEAR, $this->meta[42] ?? []);
self::assertInstanceOf(\WP_Error::class, $result);
self::assertSame('missing_birth_year', $result->get_error_code());
self::assertArrayNotHasKey(42, $this->meta);
}
/** @dataProvider unusableBirthYears */
public function testUpdateChildRefusesAnUnusableBirthYear(string $submitted): void
{
$this->guardians->shouldReceive('isGuardianOf')->with(5, 42)->andReturn(true);
Functions\expect('wp_update_user')->never();
$result = $this->service->updateChild(5, 42, 'Ada', $submitted);
self::assertInstanceOf(\WP_Error::class, $result);
self::assertSame('missing_birth_year', $result->get_error_code());
}
/** @return array<string, array{string}> */
@@ -142,6 +157,7 @@ class GuardianServiceTest extends TestCase
'too many digits' => ['20155'],
'before 1900' => ['1899'],
'later than today' => ['2027'],
'left blank' => [''],
];
}
@@ -230,9 +246,8 @@ class GuardianServiceTest extends TestCase
}
/**
* Saving a child drops the legacy full date. Without that, clearing the birth
* year on a child who predates the change would leave the old date behind for
* the fallback above to resurrect on the next read.
* Saving a child drops the legacy full date, so the fallback above can never
* outrank a year the guardian has since corrected by hand.
*/
public function testSavingAChildClearsTheLegacyDateOfBirth(): void
{
@@ -241,15 +256,16 @@ class GuardianServiceTest extends TestCase
$this->guardians->shouldReceive('isGuardianOf')->with(5, 42)->andReturn(true);
Functions\when('wp_update_user')->justReturn(42);
self::assertNull($this->service->updateChild(5, 42, 'Ada L', ''));
self::assertNull($this->service->updateChild(5, 42, 'Ada L', '2016'));
self::assertArrayNotHasKey(GuardianService::META_DOB, $this->meta[42] ?? []);
self::assertArrayNotHasKey(GuardianService::META_BIRTH_YEAR, $this->meta[42] ?? []);
self::assertSame('2016', $this->meta[42][GuardianService::META_BIRTH_YEAR]);
// The corrected year is what is read back, not the year of the old date.
$this->guardians->shouldReceive('findByGuardian')->with(5)->andReturn([new GuardianLink(5, 42)]);
Functions\when('get_userdata')->justReturn($this->user(42, 'Ada', 'Lovelace'));
self::assertSame('', $this->service->children(5)[0]['birth_year']);
self::assertSame('2016', $this->service->children(5)[0]['birth_year']);
}
public function testBookableStudentsIsJustTheUserWithoutChildren(): void