Require a name and birth year for every student
CI / Tests (PHP 8.1) (pull_request) Successful in 42s
CI / Coding Standards (pull_request) Successful in 2m56s
CI / PHPStan (pull_request) Successful in 2m56s
CI / Tests (PHP 8.2) (pull_request) Successful in 50s
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m45s
CI / Build Plugin Zip (pull_request) Skipped

Both fields are marked in their labels the same way a required registration
question is, and enforced on the server whichever form they arrive from:
GuardianService::createChild() and updateChild() now refuse a blank name or
an unusable birth year, and the signup form checks the same rule up front,
before it creates a single user, so a bad block never leaves a
half-registered family behind. normaliseBirthYear() became public and static
so both paths share one definition of what a usable year is.

The signup form cannot lean on the browser here. Its child blocks are hidden
until the parent/guardian box is ticked, and a `required` field inside a
hidden container makes the whole form unsubmittable with no control the user
can reach to fix — the same trap the guardian's own question panel already
sidesteps by disabling rather than hiding. So register.js puts `required` on
and takes it off along with the block itself, and the server 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.

One behaviour change beyond the requirement: a child block with anything
typed into it is now reported back instead of dropped. Previously any block
without a name was silently discarded, which would now mean losing a birth
year the guardian had filled in. A wholly untouched spare block — the one
the form always renders for "add another" — is still ignored.

Verified the required-toggling in a headless browser: unticked submits,
ticked blocks an empty block, a cloned block inherits the requirement, and
re-unticking leaves nothing behind to block a non-guardian signup.

Closes #148

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-07-29 21:00:45 -03:00
co-authored by Claude Opus 5
parent 2878beb221
commit 1d2f95d388
10 changed files with 215 additions and 52 deletions
+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.
*