Skip to content

Commit 3d04e44

Browse files
WPCredits Program Manager 1.117.4: a mentored student is Current though the email join found no report record
Follows #226 (fixes #222) with the release cut from the source repository: the one predicate WPCPM_Roster_Index::is_waiting() read by the roster's groups() and the Administrators Dashboard's count; the Mentors column counts an assigned mentor whose name the reports join never brought; the mentor cell says the report record has not reached this page rather than that it was never created; the group notes name both signals; design specs 7.5 and the Administrators table row; the Administrators Dashboard suite pins the has_mentor case; version, changelog, translation template. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 20d918d commit 3d04e44

12 files changed

Lines changed: 208 additions & 138 deletions

‎Education/WordPress Education Dashboard/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Two components live here, and they are built to work together:
1212

1313
| Folder | What it is | Version |
1414
| --- | --- | --- |
15-
| [`wpcredits-program-manager/`](wpcredits-program-manager/) | The plugin. All of the program logic: roles, access levels, the Airtable sync, the Report Cards, the call calendar, the feedback surveys, email. | 1.117.3 |
15+
| [`wpcredits-program-manager/`](wpcredits-program-manager/) | The plugin. All of the program logic: roles, access levels, the Airtable sync, the Report Cards, the call calendar, the feedback surveys, email. | 1.117.4 |
1616
| [`wpcredits-theme/`](wpcredits-theme/) | The block theme. The landing page, the branded login, and the skin that turns the plugin's markup into the dashboard people actually use. Frames every page with the [official wordpress.org header and footer](../WordPress.org%20Global%20Header%20and%20Footer). | 1.24.7 |
1717

1818
The theme is the front end for the plugin and does not add program data or behaviour of its own.

‎Education/WordPress Education Dashboard/wpcredits-program-manager/bin/test-administrators-dashboard.php‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ public static function has( $id ) { return isset( $GLOBALS['inst_rows'][ (string
362362
class WPCPM_Roster_Index {
363363
const NEVER_SHOWN = array( 'SPAM', 'Duplicated' );
364364
public static function read( $id ) { return isset( $GLOBALS['rosters'][ (string) $id ] ) ? $GLOBALS['rosters'][ (string) $id ] : array( 'v' => 4, 'read' => 0, 'rows' => array() ); }
365+
// The real predicate, word for word: a tracked student is waiting only with neither a mentor
366+
// on the Students row nor a joined report record (WPCredits#222). The roster's groups() and
367+
// programs() below share it, which is what keeps the two counts from drifting apart again.
368+
public static function is_waiting( array $row ) { return empty( $row['reports'] ) && empty( $row['has_mentor'] ); }
365369
public static function rows( $id ) { return self::read( $id )['rows']; }
366370
}
367371
class WPCPM_Countries {
@@ -854,6 +858,16 @@ function capture( $fn ) { ob_start(); call_user_func( $fn ); return (string) ob_
854858
ck( 'one row per institution with somebody in progress', array_column( $programs['rows'], 'record' ), array( $A ) );
855859
ck( 'with the count, the breakdown by label, the waiting and the distinct mentors', array( $programs['rows'][0]['in_progress'], $programs['rows'][0]['by_status'], $programs['rows'][0]['waiting'], $programs['rows'][0]['mentors'] ), array( 3, array( 'WordPress Credits Program 150h' => 1, 'Developer Track' => 1, 'WordPress Credits Program 50h' => 1 ), 1, 1 ) );
856860
ck( 'the earliest and latest end among those in progress', array( $programs['rows'][0]['earliest'], $programs['rows'][0]['latest'] ), array( '2026-11-30', '2026-12-15' ) );
861+
862+
// A mentor on the Students row whose report record the email join never found (WPCredits#222):
863+
// the roster files her under Current, so this count must not call her waiting - and the
864+
// Mentors column must not read 0 beside her either. The index carries no mentor ID, so a
865+
// nameless assigned mentor counts as one mentor of her own.
866+
$GLOBALS['rosters'][ $A ]['rows'][] = array( 'record_id' => 'recS0000000000008', 'status' => 'In Sensei', 'start' => $in_early, 'end' => '2026-12-10', 'reports' => array(), 'mentor_name' => '', 'has_mentor' => true );
867+
$with_222 = WPCPM_Administrators_Cards::programs();
868+
ck( 'a mentored student the join missed is in progress and not waiting', array( $with_222['rows'][0]['in_progress'], $with_222['rows'][0]['waiting'] ), array( 4, 1 ) );
869+
ck( 'and counts as a mentor though the join never brought a name', $with_222['rows'][0]['mentors'], 2 );
870+
array_pop( $GLOBALS['rosters'][ $A ]['rows'] );
857871
ck( 'the agreement state and the latest report state ride along', array( $programs['rows'][0]['agreement'], $programs['rows'][0]['report'] ), array( 'accepted', 'draft' ) );
858872
ck( 'an institution with nobody in progress is counted, not listed', $programs['quiet'], 1 );
859873
// The oldest non-zero read, not the newest: "how old are these numbers" is honestly answered

‎Education/WordPress Education Dashboard/wpcredits-program-manager/bin/test-institution-roster-view.php‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -548,9 +548,17 @@ public static function roster_url( $cohort = '' ) {
548548
ck( 'Ada is in Current', has( group_rows( $html, 'current' ), 'Ada Example' ), true );
549549
ck( 'so is Bo', has( group_rows( $html, 'current' ), 'Bo Example' ), true );
550550
ck( 'Bo is not waiting', has( group_rows( $html, 'waiting' ), 'Bo Example' ), false );
551-
ck( 'and his row says a mentor is assigned but the report record is missing',
552-
has( group_rows( $html, 'current' ), 'A mentor is assigned. The report record has not been created yet.' ), true );
551+
// "Has not reached this page" rather than "has not been created": in the #222 shape the record
552+
// exists and the address join missed it, and telling a school it was never created is untrue.
553+
ck( 'and his row says a mentor is assigned but the report record has not reached the page',
554+
has( group_rows( $html, 'current' ), 'A mentor is assigned. The report record has not reached this page yet.' ), true );
555+
ck( 'the old sentence, which claimed the record was never created, is gone', has( $html, 'has not been created yet' ), false );
553556
ck( 'Cy is waiting with no mentor at all', has( group_rows( $html, 'waiting' ), 'No mentor yet.' ), true );
557+
// The two notes state the rule the groups are built on, both signals named: a report record
558+
// with no mentor link left on the Students row is still Current, so "with a mentor assigned"
559+
// alone would promise a name the mentor cell then denies.
560+
ck( 'the Current note names both signals', has( $html, 'On the program now, with a mentor assigned or a report record already created.' ), true );
561+
ck( 'and the Waiting note says neither is there', has( $html, 'Signed up, with no mentor assigned and no report record yet.' ), true );
554562
// Every group is a disclosure, so the same chevron is on every row of the page. The two a
555563
// school works from start open; the two it reads rarely start closed. The group, not its
556564
// students: since the roster became the Mentor Report Card's component every student is a
@@ -672,8 +680,18 @@ public static function roster_url( $cohort = '' ) {
672680
* @param string $group Which group's first card to read.
673681
* @return string|null The cell's text, or null when the card has no hours row at all.
674682
*/
675-
function hours_of( $html, $group = 'current' ) {
676-
if ( ! preg_match( '/wpcpm-roster__row--reports-hours">.*?<td class="wpcpm-mentee__value"[^>]*>(.*?)<\/td>/s', group_rows( $html, $group ), $m ) ) {
683+
function hours_of( $html, $group = 'current', $name = '' ) {
684+
$rows = group_rows( $html, $group );
685+
686+
// One student's card, when asked for by name, rather than whichever card comes first.
687+
if ( '' !== $name ) {
688+
$rows = '';
689+
foreach ( explode( '<details class="wpcpm-mentee__disclosure', group_rows( $html, $group ) ) as $card ) {
690+
if ( false !== strpos( $card, '>' . $name . '<' ) ) { $rows = $card; }
691+
}
692+
}
693+
694+
if ( ! preg_match( '/wpcpm-roster__row--reports-hours">.*?<td class="wpcpm-mentee__value"[^>]*>(.*?)<\/td>/s', $rows, $m ) ) {
677695
return null;
678696
}
679697

@@ -713,7 +731,8 @@ function ada_program( array $changes = array() ) {
713731
// target comes from has to fall back to the Students row's own status.
714732
$GLOBALS['index'][ $A ]['rows']['recSTU00000000002']['hours'] = '12';
715733

716-
ck( 'a student with no account gets their hours off the index', has( group_rows( render(), 'current' ), '12 of 150' ), true );
734+
ck( 'a student with no account gets their hours off the index', hours_of( render(), 'current', 'Bo Example' ), '12 of 150' );
735+
ck( 'and they are his, not the first card\'s', hours_of( render(), 'current', 'Ada Example' ), 'Not recorded' );
717736

718737
// The 50-hour track is a different denominator, from the same map, and the value is fractional:
719738
// 6.2 is a real count on the live base, and an intval() anywhere would print 6.

‎Education/WordPress Education Dashboard/wpcredits-program-manager/bin/test-roster-index.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,5 +451,13 @@ function row( $record_id, $email, array $extra = array() ) {
451451
ck( 'with the prefix escaped', $GLOBALS['queries'][0]['args'][0], 'wpcpm\\_roster\\_%' );
452452
ck( 'against the options table', false !== strpos( $GLOBALS['queries'][0]['sql'], 'wp_options' ), true );
453453

454+
echo "\n=== is_waiting(): either signal is a mentor (WPCredits#222) ===\n";
455+
456+
ck( 'the predicate is the index\'s, so groups() and the Administrators count read one rule', method_exists( 'WPCPM_Roster_Index', 'is_waiting' ), true );
457+
ck( 'neither a mentor nor a report record is waiting', WPCPM_Roster_Index::is_waiting( array( 'has_mentor' => false, 'reports' => array() ) ), true );
458+
ck( 'a mentor on the Students row is not, though the join found no report', WPCPM_Roster_Index::is_waiting( array( 'has_mentor' => true, 'reports' => array() ) ), false );
459+
ck( 'a joined report record is not, though the Students row names no mentor', WPCPM_Roster_Index::is_waiting( array( 'has_mentor' => false, 'reports' => array( 'recR000000000001' ) ) ), false );
460+
ck( 'a row from before the key existed falls back to the report alone', WPCPM_Roster_Index::is_waiting( array( 'reports' => array() ) ), true );
461+
454462
printf( "\n%s (%d checks)\n", $fail ? sprintf( '%d FAILURE(S)', $fail ) : 'ALL PASS', $total );
455463
exit( $fail ? 1 : 0 );

‎Education/WordPress Education Dashboard/wpcredits-program-manager/docs/specs/2026-09-02-institutions-module-design.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,8 +738,8 @@ Layout, for a settled institution: identity header from the pipeline index, fall
738738

739739
| Group | Rows | Note |
740740
| --- | --- | --- |
741-
| Current | status in `tracked_statuses()['active']` and `reports` non-empty | the account-bearing students |
742-
| Waiting for a mentor | status in `['active']` and `reports` empty | imported and legacy rows alike, with `has_mentor` so "a mentor is assigned but no report record exists" reads differently from "no mentor yet" |
741+
| Current | status in `tracked_statuses()['active']` and a mentor signal: `has_mentor` (the link on the Students row) or `reports` non-empty | since 1.117.4 (WPCredits#222): the reports list is joined by email alone, so a student whose two rows carry different addresses arrived with a mentor and an empty list and was called waiting while her own card named her mentor |
742+
| Waiting for a mentor | status in `['active']` with neither `has_mentor` nor a `reports` row, `WPCPM_Roster_Index::is_waiting()`, the one predicate the Administrators Dashboard's count reads too | imported and legacy rows alike; the mentor cell still tells "a mentor is assigned, the report record has not reached this page yet" apart from "no mentor yet" |
743743
| Finished | status in `['past']` | collapsed |
744744
| Did not start | `Not moving forward`, `Fail`, empty | counted, collapsed, named honestly: the applicants who never began, which is the question institutions ask first |
745745

‎Education/WordPress Education Dashboard/wpcredits-program-manager/docs/specs/2026-09-04-administrator-dashboard-design.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ From `WPCPM_Institution_Request::open()`: institution, the note, opened date, ag
7878
Two parts from one calculation, `WPCPM_Administrators_Dashboard::programs()`, which walks every institution in the pipeline index with a roster and, for each roster row, reads only `status`, `start`, `end` and `mentor`:
7979

8080
- **Totals strip, one tile per track** (`WPCPM_Program::track()`: `150h`, `50h`, `dev`): students in progress, signed up this semester, finished this semester. "This semester" is `WPCPM_Cohort::current()`; the numbers come from `WPCPM_Cohort::participation()` per institution, summed, so the strip and the semester report count the same thing the same way.
81-
- **One row per institution with at least one student in progress**, sorted by institution name: institution (a link to its dashboard through the switcher), students in progress with a breakdown by status label, mentors assigned (distinct mentor record IDs across the in-progress rows) and rows waiting for a mentor, earliest and latest end date among in-progress rows, agreement summary state (`WPCPM_Institution_Agreement::summary()`), latest semester report state (`draft`, `approved`, or none). Institutions with nobody in progress are counted in one closing line ("12 more institutions with no student in progress") and not listed.
81+
- **One row per institution with at least one student in progress**, sorted by institution name: institution (a link to its dashboard through the switcher), students in progress with a breakdown by status label, mentors assigned (distinct mentor names across the in-progress rows, plus one for each row whose mentor link the reports join never named, since the index carries no mentor ID) and rows waiting for a mentor (`WPCPM_Roster_Index::is_waiting()`, the roster's own rule: neither a mentor link nor a joined report record, since 1.117.4), earliest and latest end date among in-progress rows, agreement summary state (`WPCPM_Institution_Agreement::summary()`), latest semester report state (`draft`, `approved`, or none). Institutions with nobody in progress are counted in one closing line ("12 more institutions with no student in progress") and not listed.
8282

8383
The calculation reads options only, never Airtable, and prints the roster read time it found, the way the roster strip does, so a manager knows how old the numbers are.
8484

‎Education/WordPress Education Dashboard/wpcredits-program-manager/includes/class-wpcpm-roster-index.php‎

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -274,16 +274,7 @@ public static function groups( $record_id, $cohort = '' ) {
274274
}
275275

276276
if ( in_array( $status, $active, true ) ) {
277-
// **Either signal is a mentor.** `reports` is the Students Reports rows behind
278-
// this student, and the automation that creates one fires on a mentor being
279-
// assigned; `has_mentor` is the assignment itself, read off the Students row.
280-
// The reports list is joined by email alone, so a student whose two rows carry
281-
// different addresses, or whose address is filed under a second institution,
282-
// arrives here with a mentor and an empty list. Grouping on `reports` alone put
283-
// that student under "Waiting for a mentor" while her own card named her mentor
284-
// (WPCredits#222). The mentor cell still tells "no report record yet" apart.
285-
$waiting = empty( $row['reports'] ) && empty( $row['has_mentor'] );
286-
$groups[ $waiting ? 'waiting' : 'current' ][ $key ] = $row;
277+
$groups[ self::is_waiting( $row ) ? 'waiting' : 'current' ][ $key ] = $row;
287278
continue;
288279
}
289280

@@ -298,6 +289,30 @@ public static function groups( $record_id, $cohort = '' ) {
298289
return $groups;
299290
}
300291

292+
/**
293+
* Whether a tracked current student is still waiting for a mentor.
294+
*
295+
* **Either signal is a mentor.** `reports` is the Students Reports rows behind this
296+
* student, and the automation that creates one fires on a mentor being assigned;
297+
* `has_mentor` is the assignment itself, read off the Students row. The reports list is
298+
* joined by email alone, so a student whose two rows carry different addresses, or whose
299+
* address is filed under a second institution, arrives here with a mentor and an empty
300+
* list. Grouping on `reports` alone put that student under "Waiting for a mentor" while
301+
* her own card named her mentor (WPCredits#222). Waiting is having neither.
302+
*
303+
* One predicate, here, because `groups()` and the Administrators Dashboard's "Waiting for
304+
* a mentor" count both answer this question, and two copies of the rule is how two screens
305+
* come to disagree. The mentor cell still tells "a mentor is assigned, no report record has
306+
* reached this page" apart from "no mentor yet"; that is the screen's job, not this one's.
307+
*
308+
* @param array $row A row in the index shape. A row from before `has_mentor` existed reads
309+
* as having no mentor link, which is the old rule and the safe one.
310+
* @return bool
311+
*/
312+
public static function is_waiting( array $row ) {
313+
return empty( $row['reports'] ) && empty( $row['has_mentor'] );
314+
}
315+
301316
/**
302317
* The institution's students whose Students row the site could not find.
303318
*

‎Education/WordPress Education Dashboard/wpcredits-program-manager/includes/modules/class-wpcpm-administrators-cards.php‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -992,15 +992,22 @@ public static function programs() {
992992
++$tracks[ $track ]['in_progress'];
993993
}
994994

995-
// The roster's rule (`WPCPM_Roster_Index::groups()`), so the two counts agree.
996-
if ( empty( $row['reports'] ) && empty( $row['has_mentor'] ) ) {
995+
// The roster's own predicate, so this count and `groups()` cannot disagree.
996+
if ( WPCPM_Roster_Index::is_waiting( $row ) ) {
997997
++$waiting;
998998
}
999999

10001000
$mentor = isset( $row['mentor_name'] ) ? trim( (string) $row['mentor_name'] ) : '';
10011001

10021002
if ( '' !== $mentor ) {
10031003
$mentors[ $mentor ] = true;
1004+
} elseif ( ! empty( $row['has_mentor'] ) ) {
1005+
// A mentor on the Students row whose name never arrived: the name rides on
1006+
// the reports join, and the join missed (WPCredits#222). The index holds no
1007+
// mentor ID, so the row counts as one mentor of its own. "1 in progress, 0
1008+
// mentors, 0 waiting" is a contradiction; two such rows sharing a mentor is
1009+
// the rarer error, and the address fix removes it.
1010+
$mentors[ 'row:' . ( isset( $row['record_id'] ) ? (string) $row['record_id'] : (string) count( $mentors ) ) ] = true;
10041011
}
10051012

10061013
if ( '' !== $end ) {

‎Education/WordPress Education Dashboard/wpcredits-program-manager/includes/modules/class-wpcpm-institution-roster-view.php‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -723,8 +723,8 @@ private static function render_group( $key, $label, array $rows, array $columns,
723723
*/
724724
private static function group_note( $key ) {
725725
$notes = array(
726-
'current' => __( 'On the program now, with a mentor assigned.', 'wpcredits-program-manager' ),
727-
'waiting' => __( 'Signed up, with no mentor assigned yet.', 'wpcredits-program-manager' ),
726+
'current' => __( 'On the program now, with a mentor assigned or a report record already created.', 'wpcredits-program-manager' ),
727+
'waiting' => __( 'Signed up, with no mentor assigned and no report record yet.', 'wpcredits-program-manager' ),
728728
'finished' => __( 'Mentoring has finished. Their details are kept for reference.', 'wpcredits-program-manager' ),
729729
'not_started' => __( 'Applicants who never began the program.', 'wpcredits-program-manager' ),
730730
);
@@ -1199,7 +1199,10 @@ private static function days_cell( $end ) {
11991199
*
12001200
* "A mentor is assigned" and "no mentor yet" are different answers to the school's
12011201
* question, and design spec 7.5 asks for both: the first is waiting on the automation
1202-
* that creates the report record, the second is waiting on the program.
1202+
* that creates the report record, or on the sync's join by email finding one that exists
1203+
* under another address (WPCredits#222); the second is waiting on the program. Either way
1204+
* the record has not reached this page, and that is what the sentence says: "has not been
1205+
* created" claimed to know which, and for the #222 shape it was wrong.
12031206
*
12041207
* @param string $name The mentor's name from the cached card, if there is one.
12051208
* @param bool $has_mentor Whether the Students row links a mentor.
@@ -1226,7 +1229,7 @@ private static function mentor_cell( $name, $has_mentor, $has_report = false ) {
12261229
esc_html(
12271230
$has_report
12281231
? __( 'A mentor is assigned. Their name has not reached this page yet.', 'wpcredits-program-manager' )
1229-
: __( 'A mentor is assigned. The report record has not been created yet.', 'wpcredits-program-manager' )
1232+
: __( 'A mentor is assigned. The report record has not reached this page yet.', 'wpcredits-program-manager' )
12301233
)
12311234
);
12321235
}

0 commit comments

Comments
 (0)