Let the account holder edit their own profile details
CI / Tests (PHP 8.1) (pull_request) Successful in 45s
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.2) (pull_request) Successful in 55s
CI / PHPStan (pull_request) Successful in 2m57s
CI / Coding Standards (pull_request) Successful in 3m3s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped

The Profile block is headed "Your profile", but the one person on it you
could not change was yourself: your name, your birth year, and whether you
take lessons yourself were fixed at whatever signup recorded, and correcting
any of them meant asking a studio admin.

A "Your details" section now opens the page, saved through the same
nonce-checked template_redirect post/redirect/get path the child rows use:

- Your name, written to display_name and nickname together, for the reason
  updateChild() does — UserName reads the nickname first, and leaving it
  behind would put the account's email address back on every screen that
  names a person.
- "I take lessons myself", the positive of us_guardian_only. This makes good
  on the claim already in bookableStudents() and the feature doc that a
  guardian-only account can put itself right from the profile page.
- Your birth year, held to the same normaliseBirthYear() rule as every other
  student.

The email is shown but not editable: it is the account's user_login as well
as its address, so changing it stays a studio-side job.

The birth-year field deliberately carries no `required` attribute. It is
asked of a student only, and this page loads no JavaScript, so a
browser-enforced `required` would leave a guardian who books solely for
other people unable to submit the form at all; handleSelf() enforces it
against the checkbox instead. Unticking the box does not clear a stored
birth year — it says who books, not "forget what is on file".

Closes #165

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-07-30 15:08:05 -03:00
co-authored by Claude Opus 5
parent 325a86f247
commit f97b8a4576
10 changed files with 443 additions and 10 deletions
+23 -4
View File
@@ -176,10 +176,26 @@ class BlockPreview {
}
/**
* Sample family (manage-children) page: two representative children and the
* add form, with the controls inert so the editor preview cannot post.
* Sample family page: the account holder's own details, two representative
* children and the add form, with the controls inert so the editor preview
* cannot post.
*/
public static function family(): string {
$self = sprintf(
'<h4>%s</h4><p class="us-family-self-email">%s <span>[email protected]</span></p>'
. '<p><label for="us-own-name">%s' . self::REQUIRED_MARK . '</label><input type="text" id="us-own-name" value="%s"></p>'
. '<p><label><input type="checkbox" checked disabled> %s</label></p>'
. '<p><label for="us-own-birth-year">%s</label><input type="number" id="us-own-birth-year" placeholder="YYYY"></p>'
. '<p><button type="button" disabled>%s</button></p>',
esc_html__( 'Your details', 'unsupervised-schedular' ),
esc_html__( 'Email', 'unsupervised-schedular' ),
esc_html__( 'Your name', 'unsupervised-schedular' ),
esc_attr__( 'Grace Hopper', 'unsupervised-schedular' ),
esc_html__( 'I take lessons myself', 'unsupervised-schedular' ),
esc_html__( 'Your birth year', 'unsupervised-schedular' ),
esc_html__( 'Save my details', 'unsupervised-schedular' )
);
$children = '';
foreach ( [ 'Ada Lovelace', 'Alan Turing' ] as $name ) {
$children .= sprintf(
@@ -202,9 +218,12 @@ class BlockPreview {
);
return sprintf(
'<div class="us-family">%s<h3>%s</h3><ul class="us-family-list">%s</ul><form class="us-family-add">%s</form></div>',
self::note( __( 'Editor preview — signed-in guardians see and manage their own students here.', 'unsupervised-schedular' ) ),
'<div class="us-family">%s<h3>%s</h3><form class="us-family-self">%s</form>'
. '<h4>%s</h4><ul class="us-family-list">%s</ul><form class="us-family-add">%s</form></div>',
self::note( __( 'Editor preview — signed-in visitors see and manage their own details and students here.', 'unsupervised-schedular' ) ),
esc_html__( 'Your profile', 'unsupervised-schedular' ),
$self,
esc_html__( 'Your students', 'unsupervised-schedular' ),
$children,
$add
);
+21 -2
View File
@@ -10,8 +10,8 @@ use Unsupervised\Schedular\Registration\QuestionRepository;
use Unsupervised\Schedular\Val;
/**
* The guardian's "my family" screen (`[us_family]`): list, add, edit and remove
* the children they book for.
* The guardian's "my family" screen (`[us_family]`): their own details, plus
* list, add, edit and remove the children they book for.
*
* Submissions are processed on `template_redirect` — before any output — and
* post/redirect/get back to the page, so a refresh cannot resubmit and add the
@@ -23,6 +23,7 @@ class FamilyPage {
private const RESULT_ADDED = 'added';
private const RESULT_UPDATED = 'updated';
private const RESULT_REMOVED = 'removed';
private const RESULT_SELF = 'self';
/**
* Error from the most recent submission processed on `template_redirect`,
@@ -58,6 +59,7 @@ class FamilyPage {
$userId = get_current_user_id();
$self = $this->guardians->accountHolder( $userId );
$children = $this->guardians->children( $userId );
$questions = $this->questions->findByScope( Question::SCOPE_ACCOUNT, activeOnly: true );
$error = $this->submitError;
@@ -99,6 +101,7 @@ class FamilyPage {
'add' => $this->handleAdd( $userId ),
'edit' => $this->handleEdit( $userId ),
'remove' => $this->handleRemove( $userId ),
'self' => $this->handleSelf( $userId ),
default => new \WP_Error( 'unknown_action', __( 'Unrecognised request.', 'unsupervised-schedular' ) ),
};
@@ -149,6 +152,21 @@ class FamilyPage {
return $error ?? self::RESULT_UPDATED;
}
/**
* Save the account holder's own details. The birth year is only asked of a
* student, so it is the checkbox — not the browser — that decides whether one
* is required; the field carries no `required` attribute, or a guardian who
* books only for other people could never submit the form at all.
*/
private function handleSelf( int $userId ): string|\WP_Error {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller.
$isStudent = isset( $_POST['is_student'] );
$error = $this->guardians->updateSelf( $userId, $this->postString( 'own_name' ), $this->postString( 'own_birth_year' ), $isStudent );
return $error ?? self::RESULT_SELF;
}
private function handleRemove( int $guardianId ): string|\WP_Error {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller.
$childId = absint( Val::int( $_POST['child_id'] ?? 0 ) );
@@ -244,6 +262,7 @@ class FamilyPage {
self::RESULT_ADDED => __( 'Student added.', 'unsupervised-schedular' ),
self::RESULT_UPDATED => __( 'Details updated.', 'unsupervised-schedular' ),
self::RESULT_REMOVED => __( 'Student removed.', 'unsupervised-schedular' ),
self::RESULT_SELF => __( 'Your details have been updated.', 'unsupervised-schedular' ),
default => '',
};
}
+66
View File
@@ -163,6 +163,52 @@ class GuardianService {
return null;
}
/**
* Update the account holder's own details from the profile screen: their
* name, whether they are a student in their own right, and — when they are —
* their birth year.
*
* `$isStudent` is the positive of what {@see META_GUARDIAN_ONLY} stores, so
* the form can ask the question the way a person would answer it and this is
* the single place the sense is flipped.
*
* Returns null on success, mirroring {@see updateChild()}.
*/
public function updateSelf( int $userId, string $name, string $birthYear, bool $isStudent ): ?\WP_Error {
$name = trim( $name );
if ( '' === $name ) {
return new \WP_Error( 'missing_name', __( 'Please give your name.', 'unsupervised-schedular' ) );
}
if ( $isStudent && 0 === self::normaliseBirthYear( $birthYear ) ) {
return new \WP_Error( 'missing_birth_year', self::ownBirthYearError() );
}
$result = wp_update_user(
[
'ID' => $userId,
'display_name' => $name,
'nickname' => $name,
]
);
if ( is_wp_error( $result ) ) {
return $result;
}
$this->setGuardianOnly( $userId, ! $isStudent );
// Only written when they are a student. Saying "I only book for other
// people" is a statement about who books, not an instruction to forget a
// year already on file — and someone who ticks the box back on the next
// visit should find their own details as they left them.
if ( $isStudent ) {
$this->setBirthYear( $userId, $birthYear );
}
return null;
}
/**
* Unlink a child and delete their account. Refused once the child has any
* lesson or enrolment history: their id is referenced by lessons, payments and
@@ -319,6 +365,26 @@ class GuardianService {
return $out;
}
/**
* The account holder's own details, as the profile screen's form needs them.
* The counterpart to {@see children()} for the person reading the page.
*
* `is_student` is the positive of {@see META_GUARDIAN_ONLY} — see
* {@see updateSelf()}, which reads it back the same way round.
*
* @return array{name: string, email: string, birth_year: string, is_student: bool}
*/
public function accountHolder( int $userId ): array {
$user = get_userdata( $userId );
return [
'name' => UserName::format( $user instanceof \WP_User ? $user : null, $userId ),
'email' => $user instanceof \WP_User ? $user->user_email : '',
'birth_year' => $this->birthYear( $userId ),
'is_student' => ! self::isGuardianOnly( $userId ),
];
}
/**
* The guardian behind a child, or null when the student books for themselves.
*