Demo follow-ups: editable policy name, one-page signup, group classes in upcoming lessons, deletion cleanup
CI / Tests (PHP 8.1) (pull_request) Successful in 1m0s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m0s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 3m8s
CI / Build Plugin Zip (pull_request) Skipped
CI / PHPStan (pull_request) Successful in 2m49s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m44s
CI / Tests (PHP 8.1) (pull_request) Successful in 1m0s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m0s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 3m8s
CI / Build Plugin Zip (pull_request) Skipped
CI / PHPStan (pull_request) Successful in 2m49s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m44s
Five items from the latest demo pass: - A policy's title can be edited from the Policies screen. Only the title moves; the slug is what the gates resolve policies by, so a rename can never detach a policy from acceptances already recorded against it. - Signup is one page again. The studio's registration questions move from a second step behind "Next" onto the main form, in an "About you" panel above the students being added, and that panel also asks an adult student for their birth year (the same us_birth_year meta a child's uses). register.js disables and hides the whole panel for a pure guardian, since the questions describe a student. - The password is re-scored on submit, not only as it is typed. zxcvbn's dictionary arrives after page load, so a password typed straight away was never scored at all and the first the student heard of it was the server rejecting the whole form. - Group-class sessions appear alongside lessons wherever upcoming lessons are listed: the [us_scheduler] panel (students and instructors) and the admin student detail page. GroupClass\SessionSchedule derives them from Offering::sessionWindows(), the same derivation the billing scan uses. They carry kind = 'group_class' and no Cancel action - a session is one date in a term, not a booked slot. - Deleting a user releases what the account was holding: each upcoming lesson is cancelled, its slot freed for rebooking, its pending payment voided, and active class enrolments cancelled. Past lessons and paid history are left alone. Tests: composer test (851), composer lint, composer cs all pass. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -22,7 +22,7 @@ if (! defined('ABSPATH')) {
|
||||
* @var string $loginUrl Where the post-confirmation sign-in link points.
|
||||
* @var string $error
|
||||
* @var list<array{policy: \Unsupervised\Schedular\Policy\Policy, version: \Unsupervised\Schedular\Policy\PolicyVersion}> $policyForms
|
||||
* @var list<Question> $accountQuestions Studio-wide questions answered as step two.
|
||||
* @var list<Question> $accountQuestions Studio-wide questions, asked of every student — the account holder included when they are one.
|
||||
*/
|
||||
|
||||
?>
|
||||
@@ -49,153 +49,140 @@ if (! defined('ABSPATH')) {
|
||||
<p class="us-error" role="alert"><?php echo esc_html($error); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php $hasQuestions = ! empty($accountQuestions); ?>
|
||||
<form method="post" action="" <?php echo $hasQuestions ? 'data-steps="1"' : ''; ?>>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('us_student_register'); ?>
|
||||
<input type="hidden" name="us_invite" value="<?php echo esc_attr($token); ?>">
|
||||
|
||||
<div class="us-reg-step" data-step="1">
|
||||
<p>
|
||||
<label for="us-reg-email"><?php esc_html_e('Email', 'unsupervised-schedular'); ?></label>
|
||||
<?php if ($inviteValid && $invite !== null && ! $invite->isGroup()) : ?>
|
||||
<input type="email" id="us-reg-email" value="<?php echo esc_attr($invite->email); ?>" readonly>
|
||||
<?php else : ?>
|
||||
<input type="email" name="email" id="us-reg-email" autocomplete="email" required>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<p>
|
||||
<label for="us-reg-name"><?php esc_html_e('Your name', 'unsupervised-schedular'); ?></label>
|
||||
<input type="text" name="display_name" id="us-reg-name" autocomplete="name" required>
|
||||
</p>
|
||||
<p>
|
||||
<label for="us-reg-pass"><?php esc_html_e('Password', 'unsupervised-schedular'); ?></label>
|
||||
<input type="password" name="password" id="us-reg-pass" autocomplete="new-password" minlength="<?php echo esc_attr((string) PasswordPolicy::MIN_LENGTH); ?>" required aria-describedby="us-reg-pass-strength">
|
||||
<?php
|
||||
/*
|
||||
* Filled in by register.js. `aria-live` announces the verdict as
|
||||
* it changes, and it starts empty so nothing is announced — or
|
||||
* takes up space — before anything has been typed.
|
||||
*/
|
||||
?>
|
||||
<span class="us-password-strength" id="us-reg-pass-strength" role="status" aria-live="polite"></span>
|
||||
</p>
|
||||
|
||||
<fieldset class="us-guardian">
|
||||
<legend><?php esc_html_e('Who are you registering?', 'unsupervised-schedular'); ?></legend>
|
||||
<?php
|
||||
/*
|
||||
* Radios, not checkboxes: the three answers are mutually
|
||||
* exclusive, and "both" only means anything as a third
|
||||
* choice alongside the other two. "Just myself" is
|
||||
* pre-selected because it is the commonest signup and the
|
||||
* one that collects the least.
|
||||
*/
|
||||
$registeringForChoices = [
|
||||
RegistrationPage::FOR_SELF => __('Just myself', 'unsupervised-schedular'),
|
||||
RegistrationPage::FOR_STUDENTS => __('On behalf of one or more students', 'unsupervised-schedular'),
|
||||
RegistrationPage::FOR_BOTH => __('Both — myself and one or more students', 'unsupervised-schedular'),
|
||||
];
|
||||
?>
|
||||
<?php foreach ($registeringForChoices as $value => $label) : ?>
|
||||
<p>
|
||||
<label>
|
||||
<input type="radio" name="us_registering_for" value="<?php echo esc_attr($value); ?>" class="us-registering-for"<?php checked($value, RegistrationPage::FOR_SELF); ?>>
|
||||
<?php echo esc_html($label); ?>
|
||||
</label>
|
||||
</p>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php /* Revealed by the two student-bearing choices; without JS it is simply always visible. */ ?>
|
||||
<div class="us-children" id="us-children">
|
||||
<p class="us-children-intro"><?php esc_html_e('Add each student you will be booking lessons for. They do not need their own login — you book and pay for them from this account.', 'unsupervised-schedular'); ?></p>
|
||||
|
||||
<?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'); ?> <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'); ?> <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
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- QuestionField::render() escapes every interpolated value.
|
||||
echo QuestionField::render(
|
||||
$question,
|
||||
'children[0][answers][' . (int) $question->id . ']',
|
||||
'us-child-0-q-' . (int) $question->id,
|
||||
enforceRequired: false
|
||||
);
|
||||
?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<button type="button" class="us-add-child"><?php esc_html_e('Add another student', 'unsupervised-schedular'); ?></button>
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<?php if (! empty($policyForms)) : ?>
|
||||
<fieldset class="us-policies">
|
||||
<legend><?php esc_html_e('Policies', 'unsupervised-schedular'); ?></legend>
|
||||
<?php foreach ($policyForms as $form) : ?>
|
||||
<div class="us-policy">
|
||||
<h4><?php echo esc_html($form['policy']->title); ?></h4>
|
||||
<div class="us-policy-body"><?php echo wp_kses_post($form['version']->bodyHtml()); ?></div>
|
||||
<label>
|
||||
<input type="checkbox" name="accept[]" value="<?php echo esc_attr((string) $form['version']->id); ?>" required>
|
||||
<?php
|
||||
/* translators: %s: policy title */
|
||||
echo esc_html(sprintf(__('I have read and agree to the %s.', 'unsupervised-schedular'), $form['policy']->title));
|
||||
?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($hasQuestions) : ?>
|
||||
<p>
|
||||
<button type="button" class="us-reg-next"><?php esc_html_e('Next', 'unsupervised-schedular'); ?></button>
|
||||
<?php
|
||||
/*
|
||||
* In parent/guardian mode the questions are asked per child,
|
||||
* up in the children section, so step two has nothing left to
|
||||
* ask and "Next" leads nowhere. This submit takes its place —
|
||||
* hidden until the guardian box is ticked (and never shown at
|
||||
* all without JS, where both steps are visible anyway and the
|
||||
* step-two submit does the job).
|
||||
*/
|
||||
?>
|
||||
<input type="submit" name="us_register" class="us-reg-submit-early" hidden value="<?php esc_attr_e('Create Account', 'unsupervised-schedular'); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="us-reg-email"><?php esc_html_e('Email', 'unsupervised-schedular'); ?></label>
|
||||
<?php if ($inviteValid && $invite !== null && ! $invite->isGroup()) : ?>
|
||||
<input type="email" id="us-reg-email" value="<?php echo esc_attr($invite->email); ?>" readonly>
|
||||
<?php else : ?>
|
||||
<p>
|
||||
<input type="submit" name="us_register" value="<?php esc_attr_e('Create Account', 'unsupervised-schedular'); ?>">
|
||||
</p>
|
||||
<input type="email" name="email" id="us-reg-email" autocomplete="email" required>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<label for="us-reg-name"><?php esc_html_e('Your name', 'unsupervised-schedular'); ?></label>
|
||||
<input type="text" name="display_name" id="us-reg-name" autocomplete="name" required>
|
||||
</p>
|
||||
<p>
|
||||
<label for="us-reg-pass"><?php esc_html_e('Password', 'unsupervised-schedular'); ?></label>
|
||||
<input type="password" name="password" id="us-reg-pass" autocomplete="new-password" minlength="<?php echo esc_attr((string) PasswordPolicy::MIN_LENGTH); ?>" required aria-describedby="us-reg-pass-strength">
|
||||
<?php
|
||||
/*
|
||||
* Filled in by register.js. `aria-live` announces the verdict as
|
||||
* it changes, and it starts empty so nothing is announced — or
|
||||
* takes up space — before anything has been typed.
|
||||
*/
|
||||
?>
|
||||
<span class="us-password-strength" id="us-reg-pass-strength" role="status" aria-live="polite"></span>
|
||||
</p>
|
||||
|
||||
<?php if ($hasQuestions) : ?>
|
||||
<div class="us-reg-step" data-step="2">
|
||||
<fieldset class="us-reg-questions">
|
||||
<legend><?php esc_html_e('Registration information', 'unsupervised-schedular'); ?></legend>
|
||||
<?php foreach ($accountQuestions as $question) : ?>
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- QuestionField::render() escapes every interpolated value.
|
||||
echo QuestionField::render($question, 'us_answers[' . (int) $question->id . ']', 'us-reg-q-' . (int) $question->id);
|
||||
?>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
<fieldset class="us-reg-group">
|
||||
<legend><?php esc_html_e('Who are you registering?', 'unsupervised-schedular'); ?></legend>
|
||||
<?php
|
||||
/*
|
||||
* Radios, not checkboxes: the three answers are mutually
|
||||
* exclusive, and "both" only means anything as a third
|
||||
* choice alongside the other two. "Just myself" is
|
||||
* pre-selected because it is the commonest signup and the
|
||||
* one that collects the least.
|
||||
*/
|
||||
$registeringForChoices = [
|
||||
RegistrationPage::FOR_SELF => __('Just myself', 'unsupervised-schedular'),
|
||||
RegistrationPage::FOR_STUDENTS => __('On behalf of one or more students', 'unsupervised-schedular'),
|
||||
RegistrationPage::FOR_BOTH => __('Both — myself and one or more students', 'unsupervised-schedular'),
|
||||
];
|
||||
?>
|
||||
<?php foreach ($registeringForChoices as $value => $label) : ?>
|
||||
<p>
|
||||
<button type="button" class="us-reg-back"><?php esc_html_e('Back', 'unsupervised-schedular'); ?></button>
|
||||
<input type="submit" name="us_register" value="<?php esc_attr_e('Create Account', 'unsupervised-schedular'); ?>">
|
||||
<label>
|
||||
<input type="radio" name="us_registering_for" value="<?php echo esc_attr($value); ?>" class="us-registering-for"<?php checked($value, RegistrationPage::FOR_SELF); ?>>
|
||||
<?php echo esc_html($label); ?>
|
||||
</label>
|
||||
</p>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
|
||||
<?php
|
||||
/*
|
||||
* The account holder's own student details, asked on the same page
|
||||
* as everything else rather than behind a "Next": what the studio
|
||||
* needs to know about them is part of registering, not a sequel to
|
||||
* it. Taken out of play by register.js when they say they are
|
||||
* registering *only* on behalf of other people — the questions
|
||||
* describe a student, and in that case they are not one.
|
||||
*/
|
||||
?>
|
||||
<fieldset class="us-reg-group us-reg-self" id="us-reg-self">
|
||||
<legend><?php esc_html_e('About you', 'unsupervised-schedular'); ?></legend>
|
||||
<p>
|
||||
<label for="us-reg-birth-year"><?php esc_html_e('Birth year', 'unsupervised-schedular'); ?> <span class="us-required" aria-hidden="true">*</span></label>
|
||||
<input type="number" name="birth_year" id="us-reg-birth-year" aria-required="true" required 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'); ?>">
|
||||
</p>
|
||||
<?php foreach ($accountQuestions as $question) : ?>
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- QuestionField::render() escapes every interpolated value.
|
||||
echo QuestionField::render($question, 'us_answers[' . (int) $question->id . ']', 'us-reg-q-' . (int) $question->id);
|
||||
?>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
|
||||
<?php /* Revealed by the two student-bearing choices; without JS it is simply always visible. */ ?>
|
||||
<fieldset class="us-reg-group us-children" id="us-children">
|
||||
<legend><?php esc_html_e('Students', 'unsupervised-schedular'); ?></legend>
|
||||
<p class="us-children-intro"><?php esc_html_e('Add each student you will be booking lessons for. They do not need their own login — you book and pay for them from this account.', 'unsupervised-schedular'); ?></p>
|
||||
|
||||
<?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'); ?> <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'); ?> <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
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- QuestionField::render() escapes every interpolated value.
|
||||
echo QuestionField::render(
|
||||
$question,
|
||||
'children[0][answers][' . (int) $question->id . ']',
|
||||
'us-child-0-q-' . (int) $question->id,
|
||||
enforceRequired: false
|
||||
);
|
||||
?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<button type="button" class="us-add-child"><?php esc_html_e('Add another student', 'unsupervised-schedular'); ?></button>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<?php if (! empty($policyForms)) : ?>
|
||||
<fieldset class="us-policies">
|
||||
<legend><?php esc_html_e('Policies', 'unsupervised-schedular'); ?></legend>
|
||||
<?php foreach ($policyForms as $form) : ?>
|
||||
<div class="us-policy">
|
||||
<h4><?php echo esc_html($form['policy']->title); ?></h4>
|
||||
<div class="us-policy-body"><?php echo wp_kses_post($form['version']->bodyHtml()); ?></div>
|
||||
<label>
|
||||
<input type="checkbox" name="accept[]" value="<?php echo esc_attr((string) $form['version']->id); ?>" required>
|
||||
<?php
|
||||
/* translators: %s: policy title */
|
||||
echo esc_html(sprintf(__('I have read and agree to the %s.', 'unsupervised-schedular'), $form['policy']->title));
|
||||
?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
<?php endif; ?>
|
||||
|
||||
<p>
|
||||
<input type="submit" name="us_register" value="<?php esc_attr_e('Create Account', 'unsupervised-schedular'); ?>">
|
||||
</p>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
Reference in New Issue
Block a user