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
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:
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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' ),
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user