Show instructor real name or nickname in group-class views, not the login
CI / Tests (PHP 8.2) (pull_request) Successful in 46s
CI / Tests (PHP 8.1) (pull_request) Successful in 46s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m47s
CI / Coding Standards (pull_request) Successful in 2m51s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m41s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.2) (pull_request) Successful in 46s
CI / Tests (PHP 8.1) (pull_request) Successful in 46s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m47s
CI / Coding Standards (pull_request) Successful in 2m51s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m41s
CI / Build Plugin Zip (pull_request) Skipped
Add Auth\UserName::format(), which prefers a user's first + last name, then their nickname, avoiding display_name (which can be the login/username). Route the instructor name through it in both the front-end offerings response (instructor_name) and the back-end group-class summary and details views. Tests: composer test (513), composer lint, composer cs all pass. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
+1
-1
@@ -19,7 +19,7 @@ each change under the current top section as you work.
|
||||
- Cancellation cutoff that limits how close to a lesson a student can cancel.
|
||||
- Studio-defined account-registration questions collected during student sign-up.
|
||||
- Group classes now carry a specific class time (alongside the date and duration), and studio admins can assign the teaching instructor. Assigning an instructor clears their open booking slots at the class time and flags any already-booked lesson that clashes.
|
||||
- Students see who teaches each group class and when it meets on the enrolment page.
|
||||
- Students see who teaches each group class and when it meets on the enrolment page. Instructor names in the group-class views (front and back end) show the instructor's real name (first + last) or nickname, never their login/username.
|
||||
|
||||
### Changed
|
||||
- Plugin metadata links now point at Unsupervised and the Gitea repository.
|
||||
|
||||
@@ -22,8 +22,10 @@ A group class offering carries `term_start`/`term_end` plus a `class_time` and a
|
||||
owning `instructor_id` (see `offerings.md`): one-off classes end the day they
|
||||
start; weekly classes run a set number of sessions, all at `class_time`. The class
|
||||
card on the enrolment page shows **when** the class meets (the date or date range
|
||||
plus the start time) and **who** teaches it (the assigned instructor's display
|
||||
name, surfaced as `instructor_name` on the `GET /offerings` response).
|
||||
plus the start time) and **who** teaches it (the assigned instructor's name,
|
||||
surfaced as `instructor_name` on the `GET /offerings` response). Instructor names
|
||||
in the group-class views (front and back end) use the instructor's real name
|
||||
(first + last) or nickname, never their login — see `Auth\UserName::format()`.
|
||||
|
||||
Assigning an instructor to a scheduled class removes that instructor's open
|
||||
booking slots at the class time and flags any already-booked lesson that clashes;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
public static function format( ?\WP_User $user, int $fallbackId = 0 ): string {
|
||||
if ( ! $user instanceof \WP_User ) {
|
||||
return $fallbackId > 0 ? (string) $fallbackId : '';
|
||||
}
|
||||
|
||||
$full = trim( $user->first_name . ' ' . $user->last_name );
|
||||
if ( '' !== $full ) {
|
||||
return $full;
|
||||
}
|
||||
|
||||
$nickname = trim( $user->nickname );
|
||||
if ( '' !== $nickname ) {
|
||||
return $nickname;
|
||||
}
|
||||
|
||||
return $fallbackId > 0 ? (string) $fallbackId : '';
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use Unsupervised\Schedular\Auth\InviteRepository;
|
||||
use Unsupervised\Schedular\Auth\RegistrationController;
|
||||
use Unsupervised\Schedular\Auth\RegistrationMailer;
|
||||
use Unsupervised\Schedular\Auth\RoleManager;
|
||||
use Unsupervised\Schedular\Auth\UserName;
|
||||
use Unsupervised\Schedular\Offering\Offering;
|
||||
use Unsupervised\Schedular\Offering\OfferingRepository;
|
||||
use Unsupervised\Schedular\Payment\Payment;
|
||||
@@ -73,12 +74,10 @@ class GroupClassController {
|
||||
|
||||
$rows = array_map(
|
||||
function ( Offering $offering ): array {
|
||||
$instructor = get_userdata( $offering->instructorId );
|
||||
|
||||
return [
|
||||
'id' => $offering->id,
|
||||
'title' => $offering->title,
|
||||
'instructor' => $instructor ? $instructor->display_name : (string) $offering->instructorId,
|
||||
'instructor' => $this->instructorName( $offering ),
|
||||
'when' => $this->whenLabel( $offering ),
|
||||
'capacity' => $offering->capacity,
|
||||
'enrolled' => $this->enrollments->countActiveForOffering( (int) $offering->id ),
|
||||
@@ -196,10 +195,8 @@ class GroupClassController {
|
||||
];
|
||||
}
|
||||
|
||||
$instructor = get_userdata( $offering->instructorId );
|
||||
|
||||
return $this->classSummary( $offering, $enrollments ) + [
|
||||
'instructor' => $instructor ? $instructor->display_name : (string) $offering->instructorId,
|
||||
'instructor' => $this->instructorName( $offering ),
|
||||
'price' => $offering->price,
|
||||
'currency' => $offering->currency,
|
||||
'duration' => $offering->durationMinutes,
|
||||
@@ -211,6 +208,17 @@ class GroupClassController {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The teaching instructor's display name — their real name or nickname, never
|
||||
* the login. Falls back to the numeric id when the account is gone. See
|
||||
* {@see UserName::format()}.
|
||||
*/
|
||||
private function instructorName( Offering $offering ): string {
|
||||
$user = get_userdata( $offering->instructorId );
|
||||
|
||||
return UserName::format( $user instanceof \WP_User ? $user : null, $offering->instructorId );
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable "when" label for a class: the class date (or weekly date
|
||||
* range) and, when set, the start time. Empty when the class has no date.
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Unsupervised\Schedular\Offering;
|
||||
|
||||
use Unsupervised\Schedular\Auth\RoleManager;
|
||||
use Unsupervised\Schedular\Auth\UserName;
|
||||
use Unsupervised\Schedular\GroupClass\GroupAccessRepository;
|
||||
use Unsupervised\Schedular\Val;
|
||||
|
||||
@@ -84,8 +85,9 @@ class OfferingEndpoint {
|
||||
}
|
||||
|
||||
/**
|
||||
* A public-facing offering array with the assigned instructor's display name
|
||||
* added (empty when the instructor account no longer exists).
|
||||
* A public-facing offering array with the assigned instructor's name added —
|
||||
* their real name or nickname, never the login (empty when the instructor
|
||||
* account no longer exists). See {@see UserName::format()}.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
@@ -93,7 +95,7 @@ class OfferingEndpoint {
|
||||
$out = $offering->toArray( includeEtransferEmail: false );
|
||||
$user = get_userdata( $offering->instructorId );
|
||||
|
||||
$out['instructor_name'] = $user instanceof \WP_User ? $user->display_name : '';
|
||||
$out['instructor_name'] = UserName::format( $user instanceof \WP_User ? $user : null );
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Auth;
|
||||
|
||||
use Mockery;
|
||||
use Unsupervised\Schedular\Auth\UserName;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class UserNameTest extends TestCase
|
||||
{
|
||||
private function user(string $first, string $last, string $nickname): \WP_User
|
||||
{
|
||||
$user = Mockery::mock(\WP_User::class);
|
||||
$user->first_name = $first;
|
||||
$user->last_name = $last;
|
||||
$user->nickname = $nickname;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function testPrefersFirstAndLastName(): void
|
||||
{
|
||||
self::assertSame('Ada Lovelace', UserName::format($this->user('Ada', 'Lovelace', 'ada_login')));
|
||||
}
|
||||
|
||||
public function testUsesFirstNameAloneWhenLastNameMissing(): void
|
||||
{
|
||||
self::assertSame('Ada', UserName::format($this->user('Ada', '', 'ada_login')));
|
||||
}
|
||||
|
||||
public function testFallsBackToNicknameWhenNoRealName(): void
|
||||
{
|
||||
self::assertSame('Countess', UserName::format($this->user('', '', 'Countess')));
|
||||
}
|
||||
|
||||
public function testFallsBackToIdWhenNothingSet(): void
|
||||
{
|
||||
self::assertSame('42', UserName::format($this->user('', '', ''), 42));
|
||||
}
|
||||
|
||||
public function testReturnsFallbackIdWhenUserMissing(): void
|
||||
{
|
||||
self::assertSame('42', UserName::format(null, 42));
|
||||
self::assertSame('', UserName::format(null));
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,24 @@ class GroupClassControllerTest extends TestCase
|
||||
$_GET = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* A WP_User whose real name (and display name) is the given full name, so
|
||||
* both instructor resolution (first + last) and roster display (display name)
|
||||
* render it.
|
||||
*/
|
||||
private function userNamed(string $full): \WP_User
|
||||
{
|
||||
[$first, $last] = array_pad(explode(' ', $full, 2), 2, '');
|
||||
|
||||
$user = Mockery::mock(\WP_User::class);
|
||||
$user->first_name = $first;
|
||||
$user->last_name = $last;
|
||||
$user->nickname = $full;
|
||||
$user->display_name = $full;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function offering(int $id, string $title, ?int $capacity): Offering
|
||||
{
|
||||
return new Offering(
|
||||
@@ -109,7 +127,7 @@ class GroupClassControllerTest extends TestCase
|
||||
|
||||
public function testClassDetailListsRosterWithPaymentStatus(): void
|
||||
{
|
||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Ada Lovelace']);
|
||||
Functions\when('get_userdata')->justReturn($this->userNamed('Ada Lovelace'));
|
||||
$_GET = ['class_id' => '8'];
|
||||
|
||||
$offering = $this->offering(8, 'Choir', 10);
|
||||
@@ -131,7 +149,7 @@ class GroupClassControllerTest extends TestCase
|
||||
|
||||
public function testClassDetailShowsClassSettingsAndInviteControlsForInviteOnly(): void
|
||||
{
|
||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Ada Lovelace']);
|
||||
Functions\when('get_userdata')->justReturn($this->userNamed('Ada Lovelace'));
|
||||
$_GET = ['class_id' => '8'];
|
||||
|
||||
$offering = new Offering(
|
||||
@@ -167,7 +185,7 @@ class GroupClassControllerTest extends TestCase
|
||||
|
||||
public function testClassDetailEnrolmentCountExcludesCancelledButRosterKeepsThem(): void
|
||||
{
|
||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Grace Hopper']);
|
||||
Functions\when('get_userdata')->justReturn($this->userNamed('Grace Hopper'));
|
||||
$_GET = ['class_id' => '8'];
|
||||
|
||||
$offering = $this->offering(8, 'Band', null);
|
||||
@@ -187,7 +205,7 @@ class GroupClassControllerTest extends TestCase
|
||||
|
||||
public function testClassDetailFreeEnrolmentShowsDashForPayment(): void
|
||||
{
|
||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Alan Turing']);
|
||||
Functions\when('get_userdata')->justReturn($this->userNamed('Alan Turing'));
|
||||
$_GET = ['class_id' => '8'];
|
||||
|
||||
$offering = $this->offering(8, 'Theory', 5);
|
||||
@@ -218,7 +236,7 @@ class GroupClassControllerTest extends TestCase
|
||||
|
||||
public function testClassDetailWithNoEnrolmentsShowsEmptyMessage(): void
|
||||
{
|
||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Ada Lovelace']);
|
||||
Functions\when('get_userdata')->justReturn($this->userNamed('Ada Lovelace'));
|
||||
$_GET = ['class_id' => '8'];
|
||||
|
||||
$offering = $this->offering(8, 'Jazz', 5);
|
||||
@@ -243,7 +261,7 @@ class GroupClassControllerTest extends TestCase
|
||||
|
||||
public function testStudioAdminPageSummarisesClassesNotStudents(): void
|
||||
{
|
||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Ada Lovelace']);
|
||||
Functions\when('get_userdata')->justReturn($this->userNamed('Ada Lovelace'));
|
||||
|
||||
$offering = new Offering(
|
||||
instructorId: 3,
|
||||
@@ -272,7 +290,7 @@ class GroupClassControllerTest extends TestCase
|
||||
public function testStudioAdminCanOpenClassDetailWithInviteControls(): void
|
||||
{
|
||||
// A studio admin (view_all_lessons) opens a class taught by instructor 7.
|
||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Ada Lovelace']);
|
||||
Functions\when('get_userdata')->justReturn($this->userNamed('Ada Lovelace'));
|
||||
$_GET = ['class_id' => '8'];
|
||||
|
||||
$offering = new Offering(
|
||||
|
||||
@@ -92,7 +92,10 @@ class OfferingEndpointTest extends TestCase
|
||||
public function testIndexIncludesInstructorNameForEachOffering(): void
|
||||
{
|
||||
$instructor = Mockery::mock(\WP_User::class);
|
||||
$instructor->display_name = 'Ada Lovelace';
|
||||
$instructor->first_name = 'Ada';
|
||||
$instructor->last_name = 'Lovelace';
|
||||
$instructor->nickname = 'ada_login';
|
||||
$instructor->display_name = 'ada_login';
|
||||
Functions\when('get_userdata')->justReturn($instructor);
|
||||
|
||||
$this->repository->shouldReceive('findAll')->andReturn([$this->group(1, Offering::ACCESS_PUBLIC)]);
|
||||
@@ -100,6 +103,7 @@ class OfferingEndpointTest extends TestCase
|
||||
|
||||
$data = $this->endpoint->index(new \WP_REST_Request())->get_data();
|
||||
|
||||
// Real name is shown, not the login-style display name.
|
||||
self::assertSame('Ada Lovelace', $data[0]['instructor_name']);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user