Ask some registration questions of students only
CI / Tests (PHP 8.2) (pull_request) Successful in 58s
CI / Tests (PHP 8.1) (pull_request) Successful in 58s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 2m53s
CI / PHPStan (pull_request) Successful in 3m0s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped

Every account-signup question was asked of everybody who registered, on the
same terms: "school and grade" had to be put to an adult signing themselves
up, and a question a studio needed answered for each student could only be
made required by demanding it of everyone.

A question now carries an audience — everyone, or only the students someone
registers on behalf of — and its own required flag for each side, so optional
for you and required for every student you enrol is expressible. Both settings
are account-scope only: an offering asks its questions once, about the student
being booked, so there is no second audience to differ from, and an offering
question mirrors its single "required" into both columns.

Every caller reads askedOfSelf()/isRequiredForSelf()/isRequiredForChild()
rather than the raw flags, so a students-only question can neither block the
account holder nor have an answer filed against them by a crafted post. The
family screen, which only ever adds a student, is held to the students' rule.

is_required_child arrives from dbDelta defaulting to 0, which would quietly
stop every existing required question being required of the students a
guardian registers — the case it most likely existed for. A one-time backfill
copies is_required across, guarded by its own option so a question later made
optional for students stays that way.

Closes #163

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-07-30 13:51:52 -03:00
co-authored by Claude Opus 5
parent 84378e856b
commit 434fe801ba
20 changed files with 809 additions and 85 deletions
+58 -7
View File
@@ -41,7 +41,7 @@ if (! defined('ABSPATH')) {
<?php else : ?>
<?php if ($accountScope) : ?>
<h2><?php esc_html_e('Account signup questions', 'unsupervised-schedular'); ?></h2>
<p><?php esc_html_e('Every new student answers these required-if-marked questions when they register — the account holder on the signup form itself, and once per student they are registering on behalf of.', 'unsupervised-schedular'); ?></p>
<p><?php esc_html_e('Every new student answers these questions when they register — the account holder on the signup form itself, and once per student they are registering on behalf of. Each question says who it is asked of, and can be required of the account holder, of the students, or of both.', 'unsupervised-schedular'); ?></p>
<?php else : ?>
<h2><?php echo esc_html(sprintf(/* translators: %s: offering title */ __('Questions for "%s"', 'unsupervised-schedular'), $selectedOffering->title)); ?></h2>
<?php endif; ?>
@@ -76,10 +76,31 @@ if (! defined('ABSPATH')) {
<th><label for="sort_order"><?php esc_html_e('Sort order', 'unsupervised-schedular'); ?></label></th>
<td><input type="number" name="sort_order" id="sort_order" min="0" step="1" value="0"></td>
</tr>
<tr>
<th><?php esc_html_e('Required', 'unsupervised-schedular'); ?></th>
<td><label><input type="checkbox" name="is_required" value="1"> <?php esc_html_e('Registrant must answer', 'unsupervised-schedular'); ?></label></td>
</tr>
<?php if ($accountScope) : ?>
<tr>
<th><label for="audience"><?php esc_html_e('Asked of', 'unsupervised-schedular'); ?></label></th>
<td>
<select name="audience" id="audience">
<option value="<?php echo esc_attr(Question::AUDIENCE_ALL); ?>"><?php esc_html_e('Everyone registering', 'unsupervised-schedular'); ?></option>
<option value="<?php echo esc_attr(Question::AUDIENCE_CHILD); ?>"><?php esc_html_e('Students only — not the account holder', 'unsupervised-schedular'); ?></option>
</select>
<p class="description"><?php esc_html_e('"Students only" leaves the question off the account holder\'s own section, for anything that only makes sense about a student someone is registering on behalf of.', 'unsupervised-schedular'); ?></p>
</td>
</tr>
<tr>
<th><?php esc_html_e('Required', 'unsupervised-schedular'); ?></th>
<td>
<label><input type="checkbox" name="is_required" value="1"> <?php esc_html_e('The account holder must answer for themselves', 'unsupervised-schedular'); ?></label><br>
<label><input type="checkbox" name="is_required_child" value="1"> <?php esc_html_e('Each student they register must answer', 'unsupervised-schedular'); ?></label>
<p class="description"><?php esc_html_e('Tick either, both, or neither — a question can be optional for an adult signing themselves up and still required for every student they enrol.', 'unsupervised-schedular'); ?></p>
</td>
</tr>
<?php else : ?>
<tr>
<th><?php esc_html_e('Required', 'unsupervised-schedular'); ?></th>
<td><label><input type="checkbox" name="is_required" value="1"> <?php esc_html_e('Registrant must answer', 'unsupervised-schedular'); ?></label></td>
</tr>
<?php endif; ?>
</table>
<?php submit_button(esc_html__('Add Question', 'unsupervised-schedular')); ?>
</form>
@@ -94,7 +115,12 @@ if (! defined('ABSPATH')) {
<th><?php esc_html_e('Order', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Question', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Type', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Required', 'unsupervised-schedular'); ?></th>
<?php if ($accountScope) : ?>
<th><?php esc_html_e('Asked of', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Required of', 'unsupervised-schedular'); ?></th>
<?php else : ?>
<th><?php esc_html_e('Required', 'unsupervised-schedular'); ?></th>
<?php endif; ?>
<th><?php esc_html_e('Actions', 'unsupervised-schedular'); ?></th>
</tr>
</thead>
@@ -104,7 +130,32 @@ if (! defined('ABSPATH')) {
<td><?php echo esc_html((string) $question->sortOrder); ?></td>
<td><?php echo esc_html($question->label); ?></td>
<td><?php echo esc_html($question->fieldType); ?></td>
<td><?php echo $question->isRequired ? esc_html__('Yes', 'unsupervised-schedular') : esc_html__('No', 'unsupervised-schedular'); ?></td>
<?php if ($accountScope) : ?>
<td>
<?php
echo $question->askedOfSelf()
? esc_html__('Everyone', 'unsupervised-schedular')
: esc_html__('Students only', 'unsupervised-schedular');
?>
</td>
<td>
<?php
// Named rather than two ticks, so "required of the
// students but not of you" reads as the deliberate
// setting it is rather than as a half-filled row.
$requiredOf = [];
if ($question->isRequiredForSelf()) {
$requiredOf[] = __('account holder', 'unsupervised-schedular');
}
if ($question->isRequiredForChild()) {
$requiredOf[] = __('students', 'unsupervised-schedular');
}
echo esc_html([] === $requiredOf ? __('—', 'unsupervised-schedular') : implode(', ', $requiredOf));
?>
</td>
<?php else : ?>
<td><?php echo $question->isRequired ? esc_html__('Yes', 'unsupervised-schedular') : esc_html__('No', 'unsupervised-schedular'); ?></td>
<?php endif; ?>
<td>
<form method="post" style="display:inline;">
<?php wp_nonce_field('usc_question_action'); ?>
+6 -1
View File
@@ -94,7 +94,12 @@ if (! defined('ABSPATH')) {
<?php foreach ($questions 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-family-q-' . (int) $question->id);
echo QuestionField::render(
$question,
'us_answers[' . (int) $question->id . ']',
'us-family-q-' . (int) $question->id,
isRequired: $question->isRequiredForChild()
);
?>
<?php endforeach; ?>
</fieldset>
+16 -3
View File
@@ -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, asked of every student — the account holder included when they are one.
* @var list<Question> $accountQuestions Studio-wide questions, asked of every student being registered — the account holder included when they are one, unless the question is for students only.
*/
?>
@@ -121,9 +121,21 @@ if (! defined('ABSPATH')) {
<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
// A "students only" question describes a child being registered,
// so it is never put to the account holder about themselves.
if (! $question->askedOfSelf()) {
continue;
}
?>
<?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);
echo QuestionField::render(
$question,
'us_answers[' . (int) $question->id . ']',
'us-reg-q-' . (int) $question->id,
isRequired: $question->isRequiredForSelf()
);
?>
<?php endforeach; ?>
</fieldset>
@@ -150,7 +162,8 @@ if (! defined('ABSPATH')) {
$question,
'children[0][answers][' . (int) $question->id . ']',
'us-child-0-q-' . (int) $question->id,
enforceRequired: false
enforceRequired: false,
isRequired: $question->isRequiredForChild()
);
?>
<?php endforeach; ?>