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.
|
- Cancellation cutoff that limits how close to a lesson a student can cancel.
|
||||||
- Studio-defined account-registration questions collected during student sign-up.
|
- 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.
|
- 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
|
### Changed
|
||||||
- Plugin metadata links now point at Unsupervised and the Gitea repository.
|
- 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
|
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
|
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
|
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
|
plus the start time) and **who** teaches it (the assigned instructor's name,
|
||||||
name, surfaced as `instructor_name` on the `GET /offerings` response).
|
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
|
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;
|
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\RegistrationController;
|
||||||
use Unsupervised\Schedular\Auth\RegistrationMailer;
|
use Unsupervised\Schedular\Auth\RegistrationMailer;
|
||||||
use Unsupervised\Schedular\Auth\RoleManager;
|
use Unsupervised\Schedular\Auth\RoleManager;
|
||||||
|
use Unsupervised\Schedular\Auth\UserName;
|
||||||
use Unsupervised\Schedular\Offering\Offering;
|
use Unsupervised\Schedular\Offering\Offering;
|
||||||
use Unsupervised\Schedular\Offering\OfferingRepository;
|
use Unsupervised\Schedular\Offering\OfferingRepository;
|
||||||
use Unsupervised\Schedular\Payment\Payment;
|
use Unsupervised\Schedular\Payment\Payment;
|
||||||
@@ -73,12 +74,10 @@ class GroupClassController {
|
|||||||
|
|
||||||
$rows = array_map(
|
$rows = array_map(
|
||||||
function ( Offering $offering ): array {
|
function ( Offering $offering ): array {
|
||||||
$instructor = get_userdata( $offering->instructorId );
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $offering->id,
|
'id' => $offering->id,
|
||||||
'title' => $offering->title,
|
'title' => $offering->title,
|
||||||
'instructor' => $instructor ? $instructor->display_name : (string) $offering->instructorId,
|
'instructor' => $this->instructorName( $offering ),
|
||||||
'when' => $this->whenLabel( $offering ),
|
'when' => $this->whenLabel( $offering ),
|
||||||
'capacity' => $offering->capacity,
|
'capacity' => $offering->capacity,
|
||||||
'enrolled' => $this->enrollments->countActiveForOffering( (int) $offering->id ),
|
'enrolled' => $this->enrollments->countActiveForOffering( (int) $offering->id ),
|
||||||
@@ -196,10 +195,8 @@ class GroupClassController {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$instructor = get_userdata( $offering->instructorId );
|
|
||||||
|
|
||||||
return $this->classSummary( $offering, $enrollments ) + [
|
return $this->classSummary( $offering, $enrollments ) + [
|
||||||
'instructor' => $instructor ? $instructor->display_name : (string) $offering->instructorId,
|
'instructor' => $this->instructorName( $offering ),
|
||||||
'price' => $offering->price,
|
'price' => $offering->price,
|
||||||
'currency' => $offering->currency,
|
'currency' => $offering->currency,
|
||||||
'duration' => $offering->durationMinutes,
|
'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
|
* 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.
|
* 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;
|
namespace Unsupervised\Schedular\Offering;
|
||||||
|
|
||||||
use Unsupervised\Schedular\Auth\RoleManager;
|
use Unsupervised\Schedular\Auth\RoleManager;
|
||||||
|
use Unsupervised\Schedular\Auth\UserName;
|
||||||
use Unsupervised\Schedular\GroupClass\GroupAccessRepository;
|
use Unsupervised\Schedular\GroupClass\GroupAccessRepository;
|
||||||
use Unsupervised\Schedular\Val;
|
use Unsupervised\Schedular\Val;
|
||||||
|
|
||||||
@@ -84,8 +85,9 @@ class OfferingEndpoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A public-facing offering array with the assigned instructor's display name
|
* A public-facing offering array with the assigned instructor's name added —
|
||||||
* added (empty when the instructor account no longer exists).
|
* their real name or nickname, never the login (empty when the instructor
|
||||||
|
* account no longer exists). See {@see UserName::format()}.
|
||||||
*
|
*
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
@@ -93,7 +95,7 @@ class OfferingEndpoint {
|
|||||||
$out = $offering->toArray( includeEtransferEmail: false );
|
$out = $offering->toArray( includeEtransferEmail: false );
|
||||||
$user = get_userdata( $offering->instructorId );
|
$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;
|
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 = [];
|
$_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
|
private function offering(int $id, string $title, ?int $capacity): Offering
|
||||||
{
|
{
|
||||||
return new Offering(
|
return new Offering(
|
||||||
@@ -109,7 +127,7 @@ class GroupClassControllerTest extends TestCase
|
|||||||
|
|
||||||
public function testClassDetailListsRosterWithPaymentStatus(): void
|
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'];
|
$_GET = ['class_id' => '8'];
|
||||||
|
|
||||||
$offering = $this->offering(8, 'Choir', 10);
|
$offering = $this->offering(8, 'Choir', 10);
|
||||||
@@ -131,7 +149,7 @@ class GroupClassControllerTest extends TestCase
|
|||||||
|
|
||||||
public function testClassDetailShowsClassSettingsAndInviteControlsForInviteOnly(): void
|
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'];
|
$_GET = ['class_id' => '8'];
|
||||||
|
|
||||||
$offering = new Offering(
|
$offering = new Offering(
|
||||||
@@ -167,7 +185,7 @@ class GroupClassControllerTest extends TestCase
|
|||||||
|
|
||||||
public function testClassDetailEnrolmentCountExcludesCancelledButRosterKeepsThem(): void
|
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'];
|
$_GET = ['class_id' => '8'];
|
||||||
|
|
||||||
$offering = $this->offering(8, 'Band', null);
|
$offering = $this->offering(8, 'Band', null);
|
||||||
@@ -187,7 +205,7 @@ class GroupClassControllerTest extends TestCase
|
|||||||
|
|
||||||
public function testClassDetailFreeEnrolmentShowsDashForPayment(): void
|
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'];
|
$_GET = ['class_id' => '8'];
|
||||||
|
|
||||||
$offering = $this->offering(8, 'Theory', 5);
|
$offering = $this->offering(8, 'Theory', 5);
|
||||||
@@ -218,7 +236,7 @@ class GroupClassControllerTest extends TestCase
|
|||||||
|
|
||||||
public function testClassDetailWithNoEnrolmentsShowsEmptyMessage(): void
|
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'];
|
$_GET = ['class_id' => '8'];
|
||||||
|
|
||||||
$offering = $this->offering(8, 'Jazz', 5);
|
$offering = $this->offering(8, 'Jazz', 5);
|
||||||
@@ -243,7 +261,7 @@ class GroupClassControllerTest extends TestCase
|
|||||||
|
|
||||||
public function testStudioAdminPageSummarisesClassesNotStudents(): void
|
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(
|
$offering = new Offering(
|
||||||
instructorId: 3,
|
instructorId: 3,
|
||||||
@@ -272,7 +290,7 @@ class GroupClassControllerTest extends TestCase
|
|||||||
public function testStudioAdminCanOpenClassDetailWithInviteControls(): void
|
public function testStudioAdminCanOpenClassDetailWithInviteControls(): void
|
||||||
{
|
{
|
||||||
// A studio admin (view_all_lessons) opens a class taught by instructor 7.
|
// 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'];
|
$_GET = ['class_id' => '8'];
|
||||||
|
|
||||||
$offering = new Offering(
|
$offering = new Offering(
|
||||||
|
|||||||
@@ -92,7 +92,10 @@ class OfferingEndpointTest extends TestCase
|
|||||||
public function testIndexIncludesInstructorNameForEachOffering(): void
|
public function testIndexIncludesInstructorNameForEachOffering(): void
|
||||||
{
|
{
|
||||||
$instructor = Mockery::mock(\WP_User::class);
|
$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);
|
Functions\when('get_userdata')->justReturn($instructor);
|
||||||
|
|
||||||
$this->repository->shouldReceive('findAll')->andReturn([$this->group(1, Offering::ACCESS_PUBLIC)]);
|
$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();
|
$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']);
|
self::assertSame('Ada Lovelace', $data[0]['instructor_name']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user