Name people by their name, not their email address
CI / Tests (PHP 8.2) (pull_request) Successful in 58s
CI / Tests (PHP 8.1) (pull_request) Successful in 59s
CI / Coding Standards (pull_request) Successful in 2m51s
CI / PHPStan (pull_request) Successful in 3m1s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / No Debug Code (pull_request) Successful in 3s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.2) (pull_request) Successful in 58s
CI / Tests (PHP 8.1) (pull_request) Successful in 59s
CI / Coding Standards (pull_request) Successful in 2m51s
CI / PHPStan (pull_request) Successful in 3m1s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / No Debug Code (pull_request) Successful in 3s
CI / Build Plugin Zip (pull_request) Skipped
Anywhere the plugin named a person it could show their email instead — "Managed by [email protected]" in the students table, the same under Booked by, instructor names on the class pages. WordPress defaults a new account's `nickname` to its `user_login`, and signup uses the email address as the login. So every self-registered account carried its own address as its nickname, and UserName::format() fell straight through to it. The name they typed was in `display_name` all along. Accounts created by a guardian were never affected — GuardianService::createChild() sets `nickname` outright, which is exactly why children read correctly and their parents did not. UserName::format() now walks nickname then display name, skipping either when it is really the login or the email, so existing accounts read correctly with nothing to migrate. An identifier still never reaches the screen: an account with nothing but its address on file falls back to the id, as before. Signup also sets `nickname` at insert, so new accounts are right at the source rather than relying on the fallback. Tests: composer test (866), composer lint, composer cs all pass. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -21,6 +21,7 @@ each change under the current top section as you work.
|
||||
- **A policy can be renamed.** The title was fixed at creation, so a typo or a change of wording meant creating a second policy and re-collecting everyone's acceptance. Renaming changes only what students read above the policy text: the slug stays put, so every version already accepted stays attached.
|
||||
|
||||
### Fixed
|
||||
- **People are named by their name again, not their email address.** Anywhere the plugin named a person it could show their email instead — "Managed by grace@example.com" in the students table, the same under **Booked by**, and instructor names on the class pages. WordPress starts a new account's nickname off as its username, and signup uses the email address as the username, so the address became the nickname of every self-registered account; the name they had typed was sitting in the account's display name the whole time. Names are now read from there when the nickname turns out to be an address, so existing accounts read correctly with nothing to fix by hand, and new signups store the name properly in the first place. Students added by a parent were never affected.
|
||||
- **Deleting a parent now removes the students they booked for.** A managed student account has no login of its own and exists only so its parent has somebody to book for — with the parent gone nobody can reach it, book for it, or be billed for it, so it was left stranded on the roster still holding lesson times. Deleting a parent now releases each of their students' upcoming lessons and enrolments on the same terms as their own, and deletes the accounts. Removing a student from the family screen is unchanged and still refuses one with lessons on record.
|
||||
- **The upcoming-lessons panel no longer collapses onto itself in some themes.** Rows could render on top of one another and the status badge's colour could stop short of the text inside it. Both came from the same thing: the panel never stated its own line spacing, so a theme setting a line height of zero anywhere above it — a common icon-font reset — was inherited straight through, leaving each line of text taller than the space allotted to it. The panel now sets its own.
|
||||
- **Deleting a student now gives back what they had booked.** WordPress deletes a user without knowing anything about lessons, so their bookings were left behind: the times stayed marked as booked and nobody else could take them, the lessons stayed on the instructor's schedule under a name that no longer resolved, and a group class kept a seat filled by nobody. Deleting an account now cancels each of its upcoming lessons, frees the time for rebooking, cancels its active class enrolments, and voids any payment still pending on them. Past lessons are left exactly as they are — they happened, and the payment report has to keep adding up. A paid lesson is not credited back: a credit could only be spent on the account being deleted, so a refund owed to someone who has left stays the studio's decision to make.
|
||||
|
||||
@@ -377,12 +377,21 @@ class RegistrationPage {
|
||||
return esc_html__( 'An account already exists for this email.', 'unsupervised-schedular' );
|
||||
}
|
||||
|
||||
// Nickname as well as display name. WordPress defaults nickname to
|
||||
// `user_login`, which here is the email address — so without this the
|
||||
// account's own address became its nickname, and every screen that names
|
||||
// a person through `UserName` showed the address instead of the name they
|
||||
// had just typed. `UserName` copes with the accounts already created that
|
||||
// way; this stops any more of them.
|
||||
$name = '' !== $displayName ? $displayName : $email;
|
||||
|
||||
$userId = wp_insert_user(
|
||||
[
|
||||
'user_login' => $email,
|
||||
'user_email' => $email,
|
||||
'user_pass' => $password,
|
||||
'display_name' => '' !== $displayName ? $displayName : $email,
|
||||
'display_name' => $name,
|
||||
'nickname' => $name,
|
||||
'role' => $inviteValid ? $invite->role : RoleManager::STUDENT,
|
||||
]
|
||||
);
|
||||
|
||||
+32
-8
@@ -5,15 +5,25 @@ namespace Unsupervised\Schedular\Auth;
|
||||
|
||||
/**
|
||||
* Resolves a person's public-facing name for display. Prefers their real name
|
||||
* (first + last), then their nickname — deliberately avoiding the account's
|
||||
* login/username, which `display_name` can otherwise expose.
|
||||
* (first + last), then their nickname, then the display name — skipping any of
|
||||
* them that is really the account's login or email address, which is the thing
|
||||
* this class exists to keep off the screen.
|
||||
*/
|
||||
class UserName {
|
||||
|
||||
/**
|
||||
* The display name for a user: "First Last" when a real name is set,
|
||||
* otherwise the WordPress nickname. Falls back to the numeric id (or an empty
|
||||
* string when none is given) when the user cannot be loaded or has no name.
|
||||
* The display name for a user: "First Last" when a real name is set, else the
|
||||
* first of nickname / display name that is an actual name. Falls back to the
|
||||
* numeric id (or an empty string when none is given) when the user cannot be
|
||||
* loaded or has nothing but identifiers on file.
|
||||
*
|
||||
* Display name is consulted at all because WordPress defaults **nickname** to
|
||||
* `user_login`, and signup uses the email address as the login — so a
|
||||
* self-registered account carries its own email as its nickname, and every
|
||||
* screen naming that person showed the address instead. The name they typed
|
||||
* was on file the whole time, in `display_name`. (Accounts created by a
|
||||
* guardian never hit this: `GuardianService::createChild()` sets `nickname`
|
||||
* outright, which is why children read correctly and their parents did not.)
|
||||
*/
|
||||
public static function format( ?\WP_User $user, int $fallbackId = 0 ): string {
|
||||
if ( ! $user instanceof \WP_User ) {
|
||||
@@ -25,11 +35,25 @@ class UserName {
|
||||
return $full;
|
||||
}
|
||||
|
||||
$nickname = trim( $user->nickname );
|
||||
if ( '' !== $nickname ) {
|
||||
return $nickname;
|
||||
foreach ( [ $user->nickname, $user->display_name ] as $candidate ) {
|
||||
$candidate = trim( (string) $candidate );
|
||||
|
||||
if ( '' !== $candidate && ! self::isIdentifier( $candidate, $user ) ) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $fallbackId > 0 ? (string) $fallbackId : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a candidate name is really the account's login or email address
|
||||
* wearing a name's clothing — the case this class must never pass through.
|
||||
*/
|
||||
private static function isIdentifier( string $candidate, \WP_User $user ): bool {
|
||||
$candidate = strtolower( $candidate );
|
||||
|
||||
return strtolower( (string) $user->user_login ) === $candidate
|
||||
|| strtolower( (string) $user->user_email ) === $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,21 @@ use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class UserNameTest extends TestCase
|
||||
{
|
||||
private function user(string $first, string $last, string $nickname): \WP_User
|
||||
{
|
||||
private function user(
|
||||
string $first,
|
||||
string $last,
|
||||
string $nickname,
|
||||
string $displayName = '',
|
||||
string $login = 'ada_login',
|
||||
string $email = '[email protected]',
|
||||
): \WP_User {
|
||||
$user = Mockery::mock(\WP_User::class);
|
||||
$user->first_name = $first;
|
||||
$user->last_name = $last;
|
||||
$user->nickname = $nickname;
|
||||
$user->display_name = $displayName;
|
||||
$user->user_login = $login;
|
||||
$user->user_email = $email;
|
||||
|
||||
return $user;
|
||||
}
|
||||
@@ -34,6 +43,60 @@ class UserNameTest extends TestCase
|
||||
self::assertSame('Countess', UserName::format($this->user('', '', 'Countess')));
|
||||
}
|
||||
|
||||
/**
|
||||
* WordPress defaults a new account's nickname to its `user_login`, and signup
|
||||
* uses the email address as the login — so a self-registered account carried
|
||||
* its own address as its nickname and every screen naming that person showed
|
||||
* the address. The name they typed was in `display_name` all along.
|
||||
*/
|
||||
public function testSkipsANicknameThatIsReallyTheLoginAndUsesTheDisplayName(): void
|
||||
{
|
||||
$user = $this->user(
|
||||
'',
|
||||
'',
|
||||
'[email protected]',
|
||||
'Grace Hopper',
|
||||
login: '[email protected]',
|
||||
email: '[email protected]',
|
||||
);
|
||||
|
||||
self::assertSame('Grace Hopper', UserName::format($user, 42));
|
||||
}
|
||||
|
||||
public function testSkipsANicknameThatIsReallyTheEmailAddress(): void
|
||||
{
|
||||
$user = $this->user(
|
||||
'',
|
||||
'',
|
||||
'[email protected]',
|
||||
'Grace Hopper',
|
||||
login: 'gracehopper',
|
||||
email: '[email protected]',
|
||||
);
|
||||
|
||||
// Case-insensitively: the address is the address however it was typed.
|
||||
self::assertSame('Grace Hopper', UserName::format($user, 42));
|
||||
}
|
||||
|
||||
/**
|
||||
* The whole point of the class: an identifier never reaches the screen, even
|
||||
* when it is the only thing on file. Someone who registered without giving a
|
||||
* name is shown as their id rather than as their email address.
|
||||
*/
|
||||
public function testNeverFallsThroughToAnIdentifier(): void
|
||||
{
|
||||
$user = $this->user(
|
||||
'',
|
||||
'',
|
||||
'[email protected]',
|
||||
'[email protected]',
|
||||
login: '[email protected]',
|
||||
email: '[email protected]',
|
||||
);
|
||||
|
||||
self::assertSame('42', UserName::format($user, 42));
|
||||
}
|
||||
|
||||
public function testFallsBackToIdWhenNothingSet(): void
|
||||
{
|
||||
self::assertSame('42', UserName::format($this->user('', '', ''), 42));
|
||||
|
||||
Reference in New Issue
Block a user