Collect a birth year instead of a full date of birth #153
@@ -14,6 +14,7 @@ each change under the current top section as you work.
|
|||||||
## [1.3.1]
|
## [1.3.1]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- 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.
|
- 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
|
### Fixed
|
||||||
|
|||||||
@@ -492,7 +492,7 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.us-family-child-dob {
|
.us-family-child-birth-year {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ No-op when no registration page is set.
|
|||||||
|
|
||||||
## Parent/Guardian Signup
|
## Parent/Guardian Signup
|
||||||
The registration form also offers **"I'm registering as a parent or guardian"**,
|
The registration form also offers **"I'm registering as a parent or guardian"**,
|
||||||
which reveals a repeatable child block (name, date of birth, and the
|
which reveals a repeatable child block (name, birth year, and the
|
||||||
account-scope questions asked **per child**). Each child becomes a login-less
|
account-scope questions asked **per child**). Each child becomes a login-less
|
||||||
`us_student` user linked to the guardian, and the signup policies are recorded
|
`us_student` user linked to the guardian, and the signup policies are recorded
|
||||||
once per child with the guardian as the acceptor. Available on every signup path
|
once per child with the guardian as the acceptor. Available on every signup path
|
||||||
|
|||||||
@@ -67,11 +67,28 @@ requires migrating every existing row.
|
|||||||
never be linked twice.
|
never be linked twice.
|
||||||
|
|
||||||
The table is a link table, not a child record: the child's **name** is their
|
The table is a link table, not a child record: the child's **name** is their
|
||||||
`display_name` on `wp_users`, and their date of birth is the `us_date_of_birth`
|
`display_name` on `wp_users`, and their birth year is the `us_birth_year`
|
||||||
user meta. Keeping them on the user row means the admin student screens,
|
user meta. Keeping them on the user row means the admin student screens,
|
||||||
`get_users()` ordering, and every existing `student_id` lookup keep working with
|
`get_users()` ordering, and every existing `student_id` lookup keep working with
|
||||||
no special-casing.
|
no special-casing.
|
||||||
|
|
||||||
|
### The legacy `us_date_of_birth` meta
|
||||||
|
|
||||||
|
This feature originally collected a full date of birth in `us_date_of_birth`.
|
||||||
|
Nothing writes that key any more. It is handled entirely inside
|
||||||
|
`GuardianService`:
|
||||||
|
|
||||||
|
- **Read** — `birthYear()` falls back to the year of the old date when
|
||||||
|
`us_birth_year` is absent, so a child added before the change still shows one
|
||||||
|
without a migration step.
|
||||||
|
- **Write** — `setBirthYear()` deletes `us_date_of_birth` on *every* save,
|
||||||
|
including a save that clears the year. Without that the fallback would
|
||||||
|
resurrect the old date on the next read and the year could never be cleared.
|
||||||
|
|
||||||
|
The upshot is a lazy migration: a child's full date survives until their record
|
||||||
|
is next edited, then goes for good. There is no bulk purge — a site that wants
|
||||||
|
the remaining old dates gone should delete the `us_date_of_birth` meta directly.
|
||||||
|
|
||||||
v1 is deliberately **one guardian per child**: `GuardianRepository::insert()`
|
v1 is deliberately **one guardian per child**: `GuardianRepository::insert()`
|
||||||
refuses to link a child that already has a guardian. The unique key and the
|
refuses to link a child that already has a guardian. The unique key and the
|
||||||
guardian-side lookups already support many-to-many, so adding a second guardian
|
guardian-side lookups already support many-to-many, so adding a second guardian
|
||||||
@@ -130,7 +147,9 @@ least one child name.
|
|||||||
|
|
||||||
Per child the form collects:
|
Per child the form collects:
|
||||||
- **Name** (required)
|
- **Name** (required)
|
||||||
- **Date of birth** (optional, `us_date_of_birth` meta)
|
- **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.
|
||||||
- **Every account-scope registration question** (`Registration\Question`,
|
- **Every account-scope registration question** (`Registration\Question`,
|
||||||
`SCOPE_ACCOUNT`) — asked once per child, not once per guardian, because in
|
`SCOPE_ACCOUNT`) — asked once per child, not once per guardian, because in
|
||||||
practice they describe the student (instrument, level, school). The guardian
|
practice they describe the student (instrument, level, school). The guardian
|
||||||
@@ -173,12 +192,12 @@ child.
|
|||||||
## Managing children
|
## Managing children
|
||||||
|
|
||||||
`[us_family]` (block: **Profile**) 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.
|
list the children, add one, edit a name/birth year, remove one.
|
||||||
|
|
||||||
- **Add** creates another accountless child user and links it. Account-scope
|
- **Add** creates another accountless child user and links it. Account-scope
|
||||||
questions are asked here too, so a child added later carries the same
|
questions are asked here too, so a child added later carries the same
|
||||||
information as one added at signup.
|
information as one added at signup.
|
||||||
- **Edit** updates `display_name` and `us_date_of_birth`.
|
- **Edit** updates `display_name` and `us_birth_year`.
|
||||||
- **Remove** unlinks the child and **deletes the child user**, but only when the
|
- **Remove** unlinks the child and **deletes the child user**, but only when the
|
||||||
child has no lessons and no enrolments — a child with history is refused, so
|
child has no lessons and no enrolments — a child with history is refused, so
|
||||||
removing one can never orphan a lesson, payment or credit
|
removing one can never orphan a lesson, payment or credit
|
||||||
|
|||||||
@@ -477,11 +477,11 @@ class RegistrationPage {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The child blocks submitted with a guardian signup, as
|
* The child blocks submitted with a guardian signup, as
|
||||||
* `children[<n>][name|dob|answers]`. Blocks with no name are dropped rather
|
* `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",
|
* 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.
|
* and an untouched spare is not a mistake the guardian needs telling about.
|
||||||
*
|
*
|
||||||
* @return list<array{name: string, dob: string, answers: array<int, string>}>
|
* @return list<array{name: string, birth_year: string, answers: array<int, string>}>
|
||||||
*/
|
*/
|
||||||
private function submittedChildren(): array {
|
private function submittedChildren(): array {
|
||||||
// The submit nonce is verified by the caller before this runs.
|
// The submit nonce is verified by the caller before this runs.
|
||||||
@@ -508,9 +508,9 @@ class RegistrationPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$out[] = [
|
$out[] = [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'dob' => sanitize_text_field( Val::string( wp_unslash( $child['dob'] ?? '' ) ) ),
|
'birth_year' => sanitize_text_field( Val::string( wp_unslash( $child['birth_year'] ?? '' ) ) ),
|
||||||
'answers' => $answers,
|
'answers' => $answers,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,7 +528,7 @@ class RegistrationPage {
|
|||||||
* re-register and children they never confirmed, so it is undone entirely and
|
* re-register and children they never confirmed, so it is undone entirely and
|
||||||
* they simply try again.
|
* they simply try again.
|
||||||
*
|
*
|
||||||
* @param list<array{name: string, dob: string, answers: array<int, string>}> $children
|
* @param list<array{name: string, birth_year: string, answers: array<int, string>}> $children
|
||||||
* @param list<Question> $questions
|
* @param list<Question> $questions
|
||||||
* @param list<array{policy: Policy, version: \Unsupervised\Schedular\Policy\PolicyVersion}> $policyForms
|
* @param list<array{policy: Policy, version: \Unsupervised\Schedular\Policy\PolicyVersion}> $policyForms
|
||||||
*/
|
*/
|
||||||
@@ -536,7 +536,7 @@ class RegistrationPage {
|
|||||||
$created = [];
|
$created = [];
|
||||||
|
|
||||||
foreach ( $children as $child ) {
|
foreach ( $children as $child ) {
|
||||||
$childId = $this->guardians->createChild( $guardianId, $child['name'], $child['dob'] );
|
$childId = $this->guardians->createChild( $guardianId, $child['name'], $child['birth_year'] );
|
||||||
|
|
||||||
if ( $childId instanceof \WP_Error ) {
|
if ( $childId instanceof \WP_Error ) {
|
||||||
foreach ( $created as $id ) {
|
foreach ( $created as $id ) {
|
||||||
|
|||||||
@@ -187,11 +187,11 @@ class BlockPreview {
|
|||||||
|
|
||||||
$add = sprintf(
|
$add = sprintf(
|
||||||
'<h4>%s</h4><p><label for="us-child-name">%s</label><input type="text" id="us-child-name"></p>'
|
'<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><label for="us-child-birth-year">%s</label><input type="number" id="us-child-birth-year" placeholder="YYYY"></p>'
|
||||||
. '<p><button type="button" disabled>%s</button></p>',
|
. '<p><button type="button" disabled>%s</button></p>',
|
||||||
esc_html__( 'Add a student', 'unsupervised-schedular' ),
|
esc_html__( 'Add a student', 'unsupervised-schedular' ),
|
||||||
esc_html__( 'Name', 'unsupervised-schedular' ),
|
esc_html__( 'Name', 'unsupervised-schedular' ),
|
||||||
esc_html__( 'Date of birth', 'unsupervised-schedular' ),
|
esc_html__( 'Birth year', 'unsupervised-schedular' ),
|
||||||
esc_html__( 'Add student', 'unsupervised-schedular' )
|
esc_html__( 'Add student', 'unsupervised-schedular' )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class FamilyPage {
|
|||||||
*/
|
*/
|
||||||
private function handleAdd( int $guardianId ): string|\WP_Error {
|
private function handleAdd( int $guardianId ): string|\WP_Error {
|
||||||
$name = $this->postString( 'child_name' );
|
$name = $this->postString( 'child_name' );
|
||||||
$dateOfBirth = $this->postString( 'child_dob' );
|
$birthYear = $this->postString( 'child_birth_year' );
|
||||||
$relationship = $this->postString( 'child_relationship' );
|
$relationship = $this->postString( 'child_relationship' );
|
||||||
|
|
||||||
$questions = $this->questions->findByScope( Question::SCOPE_ACCOUNT, activeOnly: true );
|
$questions = $this->questions->findByScope( Question::SCOPE_ACCOUNT, activeOnly: true );
|
||||||
@@ -130,7 +130,7 @@ class FamilyPage {
|
|||||||
return $missing;
|
return $missing;
|
||||||
}
|
}
|
||||||
|
|
||||||
$childId = $this->guardians->createChild( $guardianId, $name, $dateOfBirth, $relationship );
|
$childId = $this->guardians->createChild( $guardianId, $name, $birthYear, $relationship );
|
||||||
if ( $childId instanceof \WP_Error ) {
|
if ( $childId instanceof \WP_Error ) {
|
||||||
return $childId;
|
return $childId;
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ class FamilyPage {
|
|||||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller.
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller.
|
||||||
$childId = absint( Val::int( $_POST['child_id'] ?? 0 ) );
|
$childId = absint( Val::int( $_POST['child_id'] ?? 0 ) );
|
||||||
|
|
||||||
$error = $this->guardians->updateChild( $guardianId, $childId, $this->postString( 'child_name' ), $this->postString( 'child_dob' ) );
|
$error = $this->guardians->updateChild( $guardianId, $childId, $this->postString( 'child_name' ), $this->postString( 'child_birth_year' ) );
|
||||||
|
|
||||||
return $error ?? self::RESULT_UPDATED;
|
return $error ?? self::RESULT_UPDATED;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,24 @@ class GuardianService {
|
|||||||
*/
|
*/
|
||||||
public const META_CHILD = 'us_child';
|
public const META_CHILD = 'us_child';
|
||||||
|
|
||||||
/** A child's date of birth (`Y-m-d`), collected at signup and editable after. */
|
/** A child's birth year (`YYYY`), collected at signup and editable after. */
|
||||||
|
public const META_BIRTH_YEAR = 'us_birth_year';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full date of birth this feature used to collect. Nothing writes it any
|
||||||
|
* more: it is read once, to derive a birth year for a child who predates the
|
||||||
|
* change, and cleared the moment that child's record is next saved. Kept
|
||||||
|
* public so a site that wants to purge the old dates outright can find them.
|
||||||
|
*/
|
||||||
public const META_DOB = 'us_date_of_birth';
|
public const META_DOB = 'us_date_of_birth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The earliest birth year the form will accept. Old enough for any student a
|
||||||
|
* studio will ever enrol, and late enough to reject a typo like `19` or `190`
|
||||||
|
* that would otherwise be stored as a plausible-looking year.
|
||||||
|
*/
|
||||||
|
private const MIN_BIRTH_YEAR = 1900;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain used for a child's placeholder login address. `.invalid` is reserved
|
* Domain used for a child's placeholder login address. `.invalid` is reserved
|
||||||
* by RFC 2606 and can never resolve, so a child's address is guaranteed
|
* by RFC 2606 and can never resolve, so a child's address is guaranteed
|
||||||
@@ -48,7 +63,7 @@ class GuardianService {
|
|||||||
* Returns the new user ID, or a `WP_Error` when the name is blank or WordPress
|
* Returns the new user ID, or a `WP_Error` when the name is blank or WordPress
|
||||||
* refuses the insert.
|
* refuses the insert.
|
||||||
*/
|
*/
|
||||||
public function createChild( int $guardianId, string $name, string $dateOfBirth = '', string $relationship = '' ): int|\WP_Error {
|
public function createChild( int $guardianId, string $name, string $birthYear = '', string $relationship = '' ): int|\WP_Error {
|
||||||
$name = trim( $name );
|
$name = trim( $name );
|
||||||
if ( '' === $name ) {
|
if ( '' === $name ) {
|
||||||
return new \WP_Error( 'missing_name', __( 'Please give each student a name.', 'unsupervised-schedular' ) );
|
return new \WP_Error( 'missing_name', __( 'Please give each student a name.', 'unsupervised-schedular' ) );
|
||||||
@@ -73,7 +88,7 @@ class GuardianService {
|
|||||||
$userId = (int) $userId;
|
$userId = (int) $userId;
|
||||||
|
|
||||||
update_user_meta( $userId, self::META_CHILD, '1' );
|
update_user_meta( $userId, self::META_CHILD, '1' );
|
||||||
$this->setDateOfBirth( $userId, $dateOfBirth );
|
$this->setBirthYear( $userId, $birthYear );
|
||||||
|
|
||||||
$linkId = $this->guardians->insert(
|
$linkId = $this->guardians->insert(
|
||||||
new GuardianLink(
|
new GuardianLink(
|
||||||
@@ -96,13 +111,13 @@ class GuardianService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rename a child and update their date of birth. Refuses a student the caller
|
* Rename a child and update their birth year. Refuses a student the caller
|
||||||
* is not the guardian of, so the family screen cannot be turned into an
|
* is not the guardian of, so the family screen cannot be turned into an
|
||||||
* arbitrary user editor by posting someone else's id.
|
* arbitrary user editor by posting someone else's id.
|
||||||
*
|
*
|
||||||
* Returns null on success, mirroring {@see \Unsupervised\Schedular\Registration\RegistrationGate::validate()}.
|
* Returns null on success, mirroring {@see \Unsupervised\Schedular\Registration\RegistrationGate::validate()}.
|
||||||
*/
|
*/
|
||||||
public function updateChild( int $guardianId, int $studentId, string $name, string $dateOfBirth = '' ): ?\WP_Error {
|
public function updateChild( int $guardianId, int $studentId, string $name, string $birthYear = '' ): ?\WP_Error {
|
||||||
if ( ! $this->guardians->isGuardianOf( $guardianId, $studentId ) ) {
|
if ( ! $this->guardians->isGuardianOf( $guardianId, $studentId ) ) {
|
||||||
return new \WP_Error( 'forbidden', __( 'That is not one of your students.', 'unsupervised-schedular' ) );
|
return new \WP_Error( 'forbidden', __( 'That is not one of your students.', 'unsupervised-schedular' ) );
|
||||||
}
|
}
|
||||||
@@ -124,7 +139,7 @@ class GuardianService {
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->setDateOfBirth( $studentId, $dateOfBirth );
|
$this->setBirthYear( $studentId, $birthYear );
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -235,7 +250,7 @@ class GuardianService {
|
|||||||
* A guardian's children, in link order, with the details the family and admin
|
* A guardian's children, in link order, with the details the family and admin
|
||||||
* screens display.
|
* screens display.
|
||||||
*
|
*
|
||||||
* @return list<array{id: int, name: string, date_of_birth: string, relationship: string}>
|
* @return list<array{id: int, name: string, birth_year: string, relationship: string}>
|
||||||
*/
|
*/
|
||||||
public function children( int $guardianId ): array {
|
public function children( int $guardianId ): array {
|
||||||
$out = [];
|
$out = [];
|
||||||
@@ -244,10 +259,10 @@ class GuardianService {
|
|||||||
$user = get_userdata( $link->studentId );
|
$user = get_userdata( $link->studentId );
|
||||||
|
|
||||||
$out[] = [
|
$out[] = [
|
||||||
'id' => $link->studentId,
|
'id' => $link->studentId,
|
||||||
'name' => UserName::format( $user instanceof \WP_User ? $user : null, $link->studentId ),
|
'name' => UserName::format( $user instanceof \WP_User ? $user : null, $link->studentId ),
|
||||||
'date_of_birth' => Val::string( get_user_meta( $link->studentId, self::META_DOB, true ) ),
|
'birth_year' => $this->birthYear( $link->studentId ),
|
||||||
'relationship' => $link->relationship,
|
'relationship' => $link->relationship,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,24 +342,64 @@ class GuardianService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a child's date of birth, or clear it when blank or unparseable. Kept
|
* Store a child's birth year, or clear it when blank or out of range.
|
||||||
* as `Y-m-d` so it sorts and displays consistently wherever it is read.
|
*
|
||||||
|
* Either way the legacy full date of birth goes with it. That is what makes
|
||||||
|
* the read fallback in {@see birthYear()} safe: without it, clearing the year
|
||||||
|
* on a child who predates this change would leave the old date behind for the
|
||||||
|
* fallback to resurrect on the very next read.
|
||||||
*/
|
*/
|
||||||
private function setDateOfBirth( int $userId, string $dateOfBirth ): void {
|
private function setBirthYear( int $userId, string $birthYear ): void {
|
||||||
$dateOfBirth = trim( $dateOfBirth );
|
delete_user_meta( $userId, self::META_DOB );
|
||||||
|
|
||||||
if ( '' === $dateOfBirth ) {
|
$year = $this->normaliseBirthYear( $birthYear );
|
||||||
delete_user_meta( $userId, self::META_DOB );
|
|
||||||
|
if ( 0 === $year ) {
|
||||||
|
delete_user_meta( $userId, self::META_BIRTH_YEAR );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$parsed = \DateTimeImmutable::createFromFormat( 'Y-m-d', $dateOfBirth );
|
update_user_meta( $userId, self::META_BIRTH_YEAR, (string) $year );
|
||||||
if ( false === $parsed ) {
|
}
|
||||||
delete_user_meta( $userId, self::META_DOB );
|
|
||||||
return;
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
private function normaliseBirthYear( string $birthYear ): int {
|
||||||
|
$birthYear = trim( $birthYear );
|
||||||
|
|
||||||
|
if ( '' === $birthYear || 1 !== preg_match( '/^\d{4}$/', $birthYear ) ) {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
update_user_meta( $userId, self::META_DOB, $parsed->format( 'Y-m-d' ) );
|
$year = (int) $birthYear;
|
||||||
|
|
||||||
|
if ( $year < self::MIN_BIRTH_YEAR || $year > (int) current_time( 'Y' ) ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $year;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A child's birth year, or an empty string when none is recorded.
|
||||||
|
*
|
||||||
|
* Falls back to the year of the full date of birth this feature used to
|
||||||
|
* collect, so a child added before the change still shows one. The fallback
|
||||||
|
* is read-only and one-way: {@see setBirthYear()} drops the old date as soon
|
||||||
|
* as the record is saved again.
|
||||||
|
*/
|
||||||
|
private function birthYear( int $userId ): string {
|
||||||
|
$year = Val::string( get_user_meta( $userId, self::META_BIRTH_YEAR, true ) );
|
||||||
|
if ( '' !== $year ) {
|
||||||
|
return $year;
|
||||||
|
}
|
||||||
|
|
||||||
|
$legacy = Val::string( get_user_meta( $userId, self::META_DOB, true ) );
|
||||||
|
|
||||||
|
return 1 === preg_match( '/^(\d{4})-/', $legacy, $m ) ? $m[1] : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ if (! defined('ABSPATH')) {
|
|||||||
* @var float $creditBalance Balance of the account that settles this student's charges — the guardian's for a child.
|
* @var float $creditBalance Balance of the account that settles this student's charges — the guardian's for a child.
|
||||||
* @var string $creditCurrency
|
* @var string $creditCurrency
|
||||||
* @var array{id: int, name: string, email: string}|null $guardian The parent/guardian who books for this student, or null when they book for themselves.
|
* @var array{id: int, name: string, email: string}|null $guardian The parent/guardian who books for this student, or null when they book for themselves.
|
||||||
* @var list<array{id: int, name: string, date_of_birth: string, relationship: string}> $children Children this student books for.
|
* @var list<array{id: int, name: string, birth_year: string, relationship: string}> $children Children this student books for.
|
||||||
* @var array{id: int, name: string, email: string} $payer Who is billed for this student — themselves, or their guardian.
|
* @var array{id: int, name: string, email: string} $payer Who is billed for this student — themselves, or their guardian.
|
||||||
* @var string $pageSlug
|
* @var string $pageSlug
|
||||||
* @var string $backUrl
|
* @var string $backUrl
|
||||||
@@ -131,8 +131,8 @@ $renderLessons = static function (array $rows, bool $withActions = false): void
|
|||||||
<?php foreach ($children as $child) : ?>
|
<?php foreach ($children as $child) : ?>
|
||||||
<li>
|
<li>
|
||||||
<a href="<?php echo esc_url($detailUrl($child['id'])); ?>"><?php echo esc_html($child['name']); ?></a>
|
<a href="<?php echo esc_url($detailUrl($child['id'])); ?>"><?php echo esc_html($child['name']); ?></a>
|
||||||
<?php if ($child['date_of_birth'] !== '') : ?>
|
<?php if ($child['birth_year'] !== '') : ?>
|
||||||
<span class="description"><?php echo esc_html($child['date_of_birth']); ?></span>
|
<span class="description"><?php echo esc_html($child['birth_year']); ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</li>
|
</li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ if (! defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var list<array{id: int, name: string, email: string, registered: string, upcoming: int, enrolments: int, guardian: array{id: int, name: string, email: string}|null, children: list<array{id: int, name: string, date_of_birth: string, relationship: string}>}> $students
|
* @var list<array{id: int, name: string, email: string, registered: string, upcoming: int, enrolments: int, guardian: array{id: int, name: string, email: string}|null, children: list<array{id: int, name: string, birth_year: string, relationship: string}>}> $students
|
||||||
* @var string $pageSlug
|
* @var string $pageSlug
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ if (! defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var list<array{id: int, name: string, date_of_birth: string, relationship: string}> $children
|
* @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 list<\Unsupervised\Schedular\Registration\Question> $questions Account-scope questions, asked once per child.
|
||||||
* @var string $error Validation error from the last submission, if any.
|
* @var string $error Validation error from the last submission, if any.
|
||||||
* @var string $notice Confirmation of a completed add/edit/remove, if any.
|
* @var string $notice Confirmation of a completed add/edit/remove, if any.
|
||||||
@@ -42,8 +42,8 @@ if (! defined('ABSPATH')) {
|
|||||||
<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>
|
<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>
|
||||||
<p>
|
<p>
|
||||||
<label for="us-edit-dob-<?php echo esc_attr((string) $child['id']); ?>"><?php esc_html_e('Date of birth', 'unsupervised-schedular'); ?></label>
|
<label for="us-edit-birth-year-<?php echo esc_attr((string) $child['id']); ?>"><?php esc_html_e('Birth year', 'unsupervised-schedular'); ?></label>
|
||||||
<input type="date" name="child_dob" id="us-edit-dob-<?php echo esc_attr((string) $child['id']); ?>" value="<?php echo esc_attr($child['date_of_birth']); ?>">
|
<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'); ?>">
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<button type="submit"><?php esc_html_e('Save', 'unsupervised-schedular'); ?></button>
|
<button type="submit"><?php esc_html_e('Save', 'unsupervised-schedular'); ?></button>
|
||||||
@@ -52,8 +52,8 @@ if (! defined('ABSPATH')) {
|
|||||||
</form>
|
</form>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<span class="us-family-child-name"><?php echo esc_html($child['name']); ?></span>
|
<span class="us-family-child-name"><?php echo esc_html($child['name']); ?></span>
|
||||||
<?php if ($child['date_of_birth'] !== '') : ?>
|
<?php if ($child['birth_year'] !== '') : ?>
|
||||||
<span class="us-family-child-dob"><?php echo esc_html($child['date_of_birth']); ?></span>
|
<span class="us-family-child-birth-year"><?php echo esc_html($child['birth_year']); ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<span class="us-family-child-actions">
|
<span class="us-family-child-actions">
|
||||||
<a href="<?php echo esc_url(add_query_arg('us_edit_child', $child['id'], (string) get_permalink())); ?>"><?php esc_html_e('Edit', 'unsupervised-schedular'); ?></a>
|
<a href="<?php echo esc_url(add_query_arg('us_edit_child', $child['id'], (string) get_permalink())); ?>"><?php esc_html_e('Edit', 'unsupervised-schedular'); ?></a>
|
||||||
@@ -80,8 +80,8 @@ if (! defined('ABSPATH')) {
|
|||||||
<input type="text" name="child_name" id="us-child-name" required>
|
<input type="text" name="child_name" id="us-child-name" required>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="us-child-dob"><?php esc_html_e('Date of birth', 'unsupervised-schedular'); ?></label>
|
<label for="us-child-birth-year"><?php esc_html_e('Birth year', 'unsupervised-schedular'); ?></label>
|
||||||
<input type="date" name="child_dob" id="us-child-dob">
|
<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'); ?>">
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="us-child-relationship"><?php esc_html_e('Your relationship to them', 'unsupervised-schedular'); ?></label>
|
<label for="us-child-relationship"><?php esc_html_e('Your relationship to them', 'unsupervised-schedular'); ?></label>
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ if (! defined('ABSPATH')) {
|
|||||||
<input type="text" name="children[0][name]" id="us-child-0-name">
|
<input type="text" name="children[0][name]" id="us-child-0-name">
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="us-child-0-dob"><?php esc_html_e('Date of birth', 'unsupervised-schedular'); ?></label>
|
<label for="us-child-0-birth-year"><?php esc_html_e('Birth year', 'unsupervised-schedular'); ?></label>
|
||||||
<input type="date" name="children[0][dob]" id="us-child-0-dob">
|
<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'); ?>">
|
||||||
</p>
|
</p>
|
||||||
<?php foreach ($accountQuestions as $question) : ?>
|
<?php foreach ($accountQuestions as $question) : ?>
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -618,10 +618,10 @@ class RegistrationPageTest extends TestCase
|
|||||||
'display_name' => 'Grace',
|
'display_name' => 'Grace',
|
||||||
'us_is_guardian' => '1',
|
'us_is_guardian' => '1',
|
||||||
'children' => [
|
'children' => [
|
||||||
['name' => 'Ada', 'dob' => '2015-04-02', 'answers' => [7 => 'Piano']],
|
['name' => 'Ada', 'birth_year' => '2015', 'answers' => [7 => 'Piano']],
|
||||||
['name' => 'Alan', 'dob' => '', 'answers' => [7 => 'Violin']],
|
['name' => 'Alan', 'birth_year' => '', 'answers' => [7 => 'Violin']],
|
||||||
// An untouched spare block is dropped, not rejected.
|
// An untouched spare block is dropped, not rejected.
|
||||||
['name' => ' ', 'dob' => '', 'answers' => []],
|
['name' => ' ', 'birth_year' => '', 'answers' => []],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -633,7 +633,7 @@ class RegistrationPageTest extends TestCase
|
|||||||
Functions\when('wp_insert_user')->justReturn(42);
|
Functions\when('wp_insert_user')->justReturn(42);
|
||||||
Functions\when('is_wp_error')->alias(static fn ($thing): bool => $thing instanceof \WP_Error);
|
Functions\when('is_wp_error')->alias(static fn ($thing): bool => $thing instanceof \WP_Error);
|
||||||
|
|
||||||
$this->ctx['guardians']->shouldReceive('createChild')->once()->with(42, 'Ada', '2015-04-02')->andReturn(101);
|
$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', '')->andReturn(102);
|
||||||
|
|
||||||
$recorded = [];
|
$recorded = [];
|
||||||
@@ -658,7 +658,7 @@ class RegistrationPageTest extends TestCase
|
|||||||
'password' => 'password123',
|
'password' => 'password123',
|
||||||
'display_name' => 'Grace',
|
'display_name' => 'Grace',
|
||||||
'us_is_guardian' => '1',
|
'us_is_guardian' => '1',
|
||||||
'children' => [['name' => '', 'dob' => '', 'answers' => []]],
|
'children' => [['name' => '', 'birth_year' => '', 'answers' => []]],
|
||||||
];
|
];
|
||||||
|
|
||||||
Functions\when('email_exists')->justReturn(false);
|
Functions\when('email_exists')->justReturn(false);
|
||||||
@@ -681,8 +681,8 @@ class RegistrationPageTest extends TestCase
|
|||||||
'display_name' => 'Grace',
|
'display_name' => 'Grace',
|
||||||
'us_is_guardian' => '1',
|
'us_is_guardian' => '1',
|
||||||
'children' => [
|
'children' => [
|
||||||
['name' => 'Ada', 'dob' => '', 'answers' => [7 => 'Piano']],
|
['name' => 'Ada', 'birth_year' => '', 'answers' => [7 => 'Piano']],
|
||||||
['name' => 'Alan', 'dob' => '', 'answers' => [7 => ' ']],
|
['name' => 'Alan', 'birth_year' => '', 'answers' => [7 => ' ']],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -708,8 +708,8 @@ class RegistrationPageTest extends TestCase
|
|||||||
'display_name' => 'Grace',
|
'display_name' => 'Grace',
|
||||||
'us_is_guardian' => '1',
|
'us_is_guardian' => '1',
|
||||||
'children' => [
|
'children' => [
|
||||||
['name' => 'Ada', 'dob' => '', 'answers' => []],
|
['name' => 'Ada', 'birth_year' => '', 'answers' => []],
|
||||||
['name' => 'Alan', 'dob' => '', 'answers' => []],
|
['name' => 'Alan', 'birth_year' => '', 'answers' => []],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -745,7 +745,7 @@ class RegistrationPageTest extends TestCase
|
|||||||
'display_name' => 'Grace',
|
'display_name' => 'Grace',
|
||||||
'us_is_guardian' => '1',
|
'us_is_guardian' => '1',
|
||||||
'accept' => [3],
|
'accept' => [3],
|
||||||
'children' => [['name' => 'Ada', 'dob' => '', 'answers' => []]],
|
'children' => [['name' => 'Ada', 'birth_year' => '', 'answers' => []]],
|
||||||
];
|
];
|
||||||
|
|
||||||
$version = new PolicyVersion(policyId: 1, versionNumber: 1, body: 'Terms', status: PolicyVersion::STATUS_PUBLISHED, id: 3);
|
$version = new PolicyVersion(policyId: 1, versionNumber: 1, body: 'Terms', status: PolicyVersion::STATUS_PUBLISHED, id: 3);
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ class FamilyPageTest extends TestCase
|
|||||||
Functions\when('wp_enqueue_style')->justReturn(null);
|
Functions\when('wp_enqueue_style')->justReturn(null);
|
||||||
Functions\when('wp_nonce_field')->justReturn('');
|
Functions\when('wp_nonce_field')->justReturn('');
|
||||||
Functions\when('get_permalink')->justReturn('https://studio.test/family/');
|
Functions\when('get_permalink')->justReturn('https://studio.test/family/');
|
||||||
|
// The birth-year input caps itself at the current year.
|
||||||
|
Functions\when('current_time')->justReturn('2026');
|
||||||
Functions\when('absint')->alias(static fn ($value) => abs((int) $value));
|
Functions\when('absint')->alias(static fn ($value) => abs((int) $value));
|
||||||
Functions\when('sanitize_key')->alias(static fn (string $v): string => strtolower(preg_replace('/[^a-z0-9_\-]/i', '', $v) ?? ''));
|
Functions\when('sanitize_key')->alias(static fn (string $v): string => strtolower(preg_replace('/[^a-z0-9_\-]/i', '', $v) ?? ''));
|
||||||
Functions\when('sanitize_text_field')->alias(static fn (string $v): string => trim($v));
|
Functions\when('sanitize_text_field')->alias(static fn (string $v): string => trim($v));
|
||||||
@@ -92,14 +94,14 @@ class FamilyPageTest extends TestCase
|
|||||||
public function testRenderListsTheGuardiansChildren(): void
|
public function testRenderListsTheGuardiansChildren(): void
|
||||||
{
|
{
|
||||||
$this->guardians->shouldReceive('children')->once()->with(5)->andReturn([
|
$this->guardians->shouldReceive('children')->once()->with(5)->andReturn([
|
||||||
['id' => 42, 'name' => 'Ada', 'date_of_birth' => '2015-04-02', 'relationship' => 'Parent'],
|
['id' => 42, 'name' => 'Ada', 'birth_year' => '2015', 'relationship' => 'Parent'],
|
||||||
]);
|
]);
|
||||||
$this->questions->shouldReceive('findByScope')->andReturn([]);
|
$this->questions->shouldReceive('findByScope')->andReturn([]);
|
||||||
|
|
||||||
$html = $this->page->render([]);
|
$html = $this->page->render([]);
|
||||||
|
|
||||||
self::assertStringContainsString('Ada', $html);
|
self::assertStringContainsString('Ada', $html);
|
||||||
self::assertStringContainsString('2015-04-02', $html);
|
self::assertStringContainsString('2015', $html);
|
||||||
self::assertStringContainsString('Add a student', $html);
|
self::assertStringContainsString('Add a student', $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,13 +110,13 @@ class FamilyPageTest extends TestCase
|
|||||||
$_POST = [
|
$_POST = [
|
||||||
'us_family_action' => 'add',
|
'us_family_action' => 'add',
|
||||||
'child_name' => 'Ada',
|
'child_name' => 'Ada',
|
||||||
'child_dob' => '2015-04-02',
|
'child_birth_year' => '2015',
|
||||||
'child_relationship' => 'Parent',
|
'child_relationship' => 'Parent',
|
||||||
'us_answers' => [7 => 'Piano'],
|
'us_answers' => [7 => 'Piano'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->questions->shouldReceive('findByScope')->once()->andReturn([$this->question(7, true)]);
|
$this->questions->shouldReceive('findByScope')->once()->andReturn([$this->question(7, true)]);
|
||||||
$this->guardians->shouldReceive('createChild')->once()->with(5, 'Ada', '2015-04-02', 'Parent')->andReturn(42);
|
$this->guardians->shouldReceive('createChild')->once()->with(5, 'Ada', '2015', 'Parent')->andReturn(42);
|
||||||
|
|
||||||
$this->answers->shouldReceive('insert')
|
$this->answers->shouldReceive('insert')
|
||||||
->once()
|
->once()
|
||||||
@@ -179,10 +181,10 @@ class FamilyPageTest extends TestCase
|
|||||||
'us_family_action' => 'edit',
|
'us_family_action' => 'edit',
|
||||||
'child_id' => '42',
|
'child_id' => '42',
|
||||||
'child_name' => 'Ada L',
|
'child_name' => 'Ada L',
|
||||||
'child_dob' => '2015-04-02',
|
'child_birth_year' => '2015',
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->guardians->shouldReceive('updateChild')->once()->with(5, 42, 'Ada L', '2015-04-02')->andReturn(null);
|
$this->guardians->shouldReceive('updateChild')->once()->with(5, 42, 'Ada L', '2015')->andReturn(null);
|
||||||
|
|
||||||
$captured = null;
|
$captured = null;
|
||||||
$this->capturingPage($captured)->maybeHandleSubmit();
|
$this->capturingPage($captured)->maybeHandleSubmit();
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ class GuardianServiceTest extends TestCase
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
// The birth-year range is validated against "this year", so pin it.
|
||||||
|
Functions\when('current_time')->justReturn('2026');
|
||||||
Functions\when('wp_generate_password')->justReturn('abc123def456');
|
Functions\when('wp_generate_password')->justReturn('abc123def456');
|
||||||
Functions\when('email_exists')->justReturn(false);
|
Functions\when('email_exists')->justReturn(false);
|
||||||
Functions\when('is_wp_error')->alias(static fn ($thing): bool => $thing instanceof \WP_Error);
|
Functions\when('is_wp_error')->alias(static fn ($thing): bool => $thing instanceof \WP_Error);
|
||||||
@@ -84,14 +86,14 @@ class GuardianServiceTest extends TestCase
|
|||||||
->with(Mockery::on(static fn (GuardianLink $l): bool => $l->guardianId === 5 && $l->studentId === 42 && $l->relationship === 'Parent'))
|
->with(Mockery::on(static fn (GuardianLink $l): bool => $l->guardianId === 5 && $l->studentId === 42 && $l->relationship === 'Parent'))
|
||||||
->andReturn(7);
|
->andReturn(7);
|
||||||
|
|
||||||
$result = $this->service->createChild(5, ' Ada ', '2015-04-02', 'Parent');
|
$result = $this->service->createChild(5, ' Ada ', '2015', 'Parent');
|
||||||
|
|
||||||
self::assertSame(42, $result);
|
self::assertSame(42, $result);
|
||||||
self::assertSame('Ada', $captured['display_name']);
|
self::assertSame('Ada', $captured['display_name']);
|
||||||
// The address is on the reserved .invalid TLD, so it can never receive mail.
|
// The address is on the reserved .invalid TLD, so it can never receive mail.
|
||||||
self::assertStringEndsWith('@child.invalid', $captured['user_email']);
|
self::assertStringEndsWith('@child.invalid', $captured['user_email']);
|
||||||
self::assertSame('1', $this->meta[42][GuardianService::META_CHILD]);
|
self::assertSame('1', $this->meta[42][GuardianService::META_CHILD]);
|
||||||
self::assertSame('2015-04-02', $this->meta[42][GuardianService::META_DOB]);
|
self::assertSame('2015', $this->meta[42][GuardianService::META_BIRTH_YEAR]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCreateChildRejectsABlankName(): void
|
public function testCreateChildRejectsABlankName(): void
|
||||||
@@ -117,14 +119,30 @@ class GuardianServiceTest extends TestCase
|
|||||||
self::assertInstanceOf(\WP_Error::class, $this->service->createChild(5, 'Ada'));
|
self::assertInstanceOf(\WP_Error::class, $this->service->createChild(5, 'Ada'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCreateChildClearsAnUnparseableDateOfBirth(): void
|
/**
|
||||||
|
* @dataProvider unusableBirthYears
|
||||||
|
*/
|
||||||
|
public function testCreateChildClearsAnUnusableBirthYear(string $submitted): void
|
||||||
{
|
{
|
||||||
Functions\when('wp_insert_user')->justReturn(42);
|
Functions\when('wp_insert_user')->justReturn(42);
|
||||||
$this->guardians->shouldReceive('insert')->once()->andReturn(7);
|
$this->guardians->shouldReceive('insert')->once()->andReturn(7);
|
||||||
|
|
||||||
$this->service->createChild(5, 'Ada', 'not-a-date');
|
$this->service->createChild(5, 'Ada', $submitted);
|
||||||
|
|
||||||
self::assertArrayNotHasKey(GuardianService::META_DOB, $this->meta[42] ?? []);
|
self::assertArrayNotHasKey(GuardianService::META_BIRTH_YEAR, $this->meta[42] ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{string}> */
|
||||||
|
public static function unusableBirthYears(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'not a number' => ['not-a-year'],
|
||||||
|
'a full date' => ['2015-04-02'],
|
||||||
|
'too few digits' => ['15'],
|
||||||
|
'too many digits' => ['20155'],
|
||||||
|
'before 1900' => ['1899'],
|
||||||
|
'later than today' => ['2027'],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCanActForSelfAndOwnChildOnly(): void
|
public function testCanActForSelfAndOwnChildOnly(): void
|
||||||
@@ -187,6 +205,53 @@ class GuardianServiceTest extends TestCase
|
|||||||
self::assertSame([false, false, true], array_column($students, 'is_self'));
|
self::assertSame([false, false, true], array_column($students, 'is_self'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testChildrenReportsTheStoredBirthYear(): void
|
||||||
|
{
|
||||||
|
$this->meta[42][GuardianService::META_BIRTH_YEAR] = '2015';
|
||||||
|
|
||||||
|
$this->guardians->shouldReceive('findByGuardian')->with(5)->andReturn([new GuardianLink(5, 42)]);
|
||||||
|
Functions\when('get_userdata')->justReturn($this->user(42, 'Ada', 'Lovelace'));
|
||||||
|
|
||||||
|
self::assertSame('2015', $this->service->children(5)[0]['birth_year']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A child added before this feature switched to a year has only the old full
|
||||||
|
* date on record, and must still show a birth year.
|
||||||
|
*/
|
||||||
|
public function testChildrenDerivesABirthYearFromALegacyDateOfBirth(): void
|
||||||
|
{
|
||||||
|
$this->meta[42][GuardianService::META_DOB] = '2015-04-02';
|
||||||
|
|
||||||
|
$this->guardians->shouldReceive('findByGuardian')->with(5)->andReturn([new GuardianLink(5, 42)]);
|
||||||
|
Functions\when('get_userdata')->justReturn($this->user(42, 'Ada', 'Lovelace'));
|
||||||
|
|
||||||
|
self::assertSame('2015', $this->service->children(5)[0]['birth_year']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public function testSavingAChildClearsTheLegacyDateOfBirth(): void
|
||||||
|
{
|
||||||
|
$this->meta[42][GuardianService::META_DOB] = '2015-04-02';
|
||||||
|
|
||||||
|
$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::assertArrayNotHasKey(GuardianService::META_DOB, $this->meta[42] ?? []);
|
||||||
|
self::assertArrayNotHasKey(GuardianService::META_BIRTH_YEAR, $this->meta[42] ?? []);
|
||||||
|
|
||||||
|
$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']);
|
||||||
|
}
|
||||||
|
|
||||||
public function testBookableStudentsIsJustTheUserWithoutChildren(): void
|
public function testBookableStudentsIsJustTheUserWithoutChildren(): void
|
||||||
{
|
{
|
||||||
$this->guardians->shouldReceive('findByGuardian')->with(9)->andReturn([]);
|
$this->guardians->shouldReceive('findByGuardian')->with(9)->andReturn([]);
|
||||||
@@ -228,7 +293,7 @@ class GuardianServiceTest extends TestCase
|
|||||||
self::assertInstanceOf(\WP_Error::class, $this->service->updateChild(5, 99, 'Mallory'));
|
self::assertInstanceOf(\WP_Error::class, $this->service->updateChild(5, 99, 'Mallory'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUpdateChildRenamesAndStoresTheDateOfBirth(): void
|
public function testUpdateChildRenamesAndStoresTheBirthYear(): void
|
||||||
{
|
{
|
||||||
$this->guardians->shouldReceive('isGuardianOf')->with(5, 42)->andReturn(true);
|
$this->guardians->shouldReceive('isGuardianOf')->with(5, 42)->andReturn(true);
|
||||||
Functions\expect('wp_update_user')
|
Functions\expect('wp_update_user')
|
||||||
@@ -236,8 +301,8 @@ class GuardianServiceTest extends TestCase
|
|||||||
->with(['ID' => 42, 'display_name' => 'Ada L', 'nickname' => 'Ada L'])
|
->with(['ID' => 42, 'display_name' => 'Ada L', 'nickname' => 'Ada L'])
|
||||||
->andReturn(42);
|
->andReturn(42);
|
||||||
|
|
||||||
self::assertNull($this->service->updateChild(5, 42, 'Ada L', '2015-04-02'));
|
self::assertNull($this->service->updateChild(5, 42, 'Ada L', '2015'));
|
||||||
self::assertSame('2015-04-02', $this->meta[42][GuardianService::META_DOB]);
|
self::assertSame('2015', $this->meta[42][GuardianService::META_BIRTH_YEAR]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRemoveChildUnlinksAndDeletesAChildWithNoHistory(): void
|
public function testRemoveChildUnlinksAndDeletesAChildWithNoHistory(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user