Skip to content

Commit 98ed4a4

Browse files
donnapepclaude
andcommitted
Migrate Grading backend to HPPS service layer
Add Grading_Listing_Service_Interface with comments-based and tables-based implementations. Refactor Sensei_Grading_Main to use the listing service for prepare_items() and get_row_data(). Wire Sensei_Grading::count_statuses() through the aggregation service. Apply teacher and temp-user restrictions to tables-based grading. Add per-status count caching to avoid duplicate queries for tab counts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0a44259 commit 98ed4a4

15 files changed

+1571
-137
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Internal: Integrate HPPS in grading backend.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
Hide lessons with no quiz answers from the Grading page, including completed lessons where the student never submitted the quiz and orphaned records with quiz-derived statuses but no answer data.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
Fix Grading page status tab counts not updating when filtering by a specific lesson.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
Fix teachers not seeing all grading rows for their courses due to post-filter pagination mismatch.

config/psalm/psalm-baseline.xml

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,17 +1620,13 @@
16201620
</file>
16211621
<file src="includes/class-sensei-grading-main.php">
16221622
<ArgumentTypeCoercion occurrences="1"/>
1623-
<InvalidArrayAccess occurrences="6">
1624-
<code>$counts['complete']</code>
1625-
<code>$counts['failed']</code>
1626-
<code>$counts['graded']</code>
1627-
<code>$counts['in-progress']</code>
1628-
<code>$counts['passed']</code>
1629-
<code>$counts['ungraded']</code>
1630-
</InvalidArrayAccess>
1631-
<MissingParamType occurrences="1">
1632-
<code>$args</code>
1633-
</MissingParamType>
1623+
<InternalProperty occurrences="5">
1624+
<code>$item-&gt;grade</code>
1625+
<code>$item-&gt;lesson_id</code>
1626+
<code>$item-&gt;status</code>
1627+
<code>$item-&gt;updated_at</code>
1628+
<code>$item-&gt;user_id</code>
1629+
</InternalProperty>
16341630
<MissingPropertyType occurrences="6">
16351631
<code>$course_id</code>
16361632
<code>$lesson_id</code>
@@ -1642,6 +1638,9 @@
16421638
<PossiblyFalsePropertyAssignmentValue occurrences="1">
16431639
<code>$search</code>
16441640
</PossiblyFalsePropertyAssignmentValue>
1641+
<PossiblyNullArgument occurrences="1">
1642+
<code>$args</code>
1643+
</PossiblyNullArgument>
16451644
<PropertyNotSetInConstructor occurrences="8">
16461645
<code>Sensei_Grading_Main</code>
16471646
<code>Sensei_Grading_Main</code>
@@ -4556,6 +4555,11 @@
45564555
<code>$row-&gt;user_id</code>
45574556
</PossiblyInvalidPropertyFetch>
45584557
</file>
4558+
<file src="includes/internal/services/class-comments-based-grading-listing-service.php">
4559+
<InvalidScalarArgument occurrences="1">
4560+
<code>$comment-&gt;comment_ID</code>
4561+
</InvalidScalarArgument>
4562+
</file>
45594563
<file src="includes/internal/services/class-comments-based-progress-aggregation-service.php">
45604564
<PossiblyInvalidPropertyFetch occurrences="5">
45614565
<code>$row-&gt;days_to_complete_count</code>
@@ -4568,6 +4572,21 @@
45684572
<code>ARRAY_A</code>
45694573
</UndefinedConstant>
45704574
</file>
4575+
<file src="includes/internal/services/class-tables-based-grading-listing-service.php">
4576+
<PossiblyInvalidCast occurrences="1">
4577+
<code>$column</code>
4578+
</PossiblyInvalidCast>
4579+
<PossiblyInvalidPropertyFetch occurrences="5">
4580+
<code>$row-&gt;effective_status</code>
4581+
<code>$row-&gt;final_grade</code>
4582+
<code>$row-&gt;post_id</code>
4583+
<code>$row-&gt;updated_at</code>
4584+
<code>$row-&gt;user_id</code>
4585+
</PossiblyInvalidPropertyFetch>
4586+
<UndefinedConstant occurrences="1">
4587+
<code>ARRAY_A</code>
4588+
</UndefinedConstant>
4589+
</file>
45714590
<file src="includes/internal/services/class-tables-based-progress-aggregation-service.php">
45724591
<PossiblyInvalidPropertyFetch occurrences="5">
45734592
<code>$row-&gt;days_to_complete_count</code>

includes/class-sensei-grading-main.php

Lines changed: 100 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
exit; // Exit if accessed directly
44
}
55

6+
use Sensei\Internal\Services\Grading_Listing_Service_Interface;
7+
use Sensei\Internal\Services\Grading_Item;
8+
use Sensei\Internal\Services\Progress_Query_Service_Factory;
9+
610
/**
711
* Admin Grading Overview Data Table in Sensei.
812
*
@@ -19,12 +23,22 @@ class Sensei_Grading_Main extends Sensei_List_Table {
1923
public $user_ids = false;
2024
public $page_slug = 'sensei_grading';
2125

26+
/**
27+
* The grading listing service.
28+
*
29+
* @var Grading_Listing_Service_Interface
30+
*/
31+
private Grading_Listing_Service_Interface $grading_listing_service;
32+
2233
/**
2334
* Constructor
2435
*
2536
* @since 1.3.0
37+
*
38+
* @param array|null $args Constructor arguments.
39+
* @param Grading_Listing_Service_Interface|null $grading_listing_service The grading listing service.
2640
*/
27-
public function __construct( $args = null ) {
41+
public function __construct( $args = null, ?Grading_Listing_Service_Interface $grading_listing_service = null ) {
2842

2943
$defaults = array(
3044
'course_id' => 0,
@@ -44,6 +58,9 @@ public function __construct( $args = null ) {
4458
$this->view = $args['view'];
4559
}
4660

61+
$this->grading_listing_service = $grading_listing_service
62+
?? ( new Progress_Query_Service_Factory() )->create_grading_listing_service();
63+
4764
// Load Parent token into constructor
4865
parent::__construct( 'grading_main' );
4966

@@ -237,30 +254,45 @@ public function prepare_items() {
237254
*/
238255
$activity_args = apply_filters( 'sensei_grading_filter_statuses', $activity_args );
239256

240-
// WP_Comment_Query doesn't support SQL_CALC_FOUND_ROWS, so instead do this twice
241-
$total_statuses = Sensei_Utils::sensei_check_for_activity(
242-
array_merge(
243-
$activity_args,
244-
array(
245-
'count' => true,
246-
'offset' => 0,
247-
'number' => 0,
248-
)
249-
)
250-
);
251-
252-
// Ensure we change our range to fit (in case a search threw off the pagination) - Should this be added to all views?
253-
if ( $total_statuses < $activity_args['offset'] ) {
254-
$new_paged = floor( $total_statuses / $activity_args['number'] );
255-
$activity_args['offset'] = $new_paged * $activity_args['number'];
257+
// Apply teacher and temp-user restrictions so that both listing rows
258+
// and cached per-status counts reflect these filters. For tables-based
259+
// storage, these args are applied as SQL clauses. For comments-based,
260+
// post__in flows through to WP_Comment_Query and the remaining args
261+
// are handled by existing post-filters on sensei_check_for_activity.
262+
$count_restrictions = apply_filters( 'sensei_count_statuses_args', array( 'type' => 'lesson' ) );
263+
264+
// Merge teacher's post__in restriction.
265+
if ( ! empty( $count_restrictions['post__in'] ) ) {
266+
if ( ! empty( $activity_args['post__in'] ) ) {
267+
// Intersect: keep only lessons in both the course filter and teacher filter.
268+
$intersected = array_values(
269+
array_intersect( $activity_args['post__in'], $count_restrictions['post__in'] )
270+
);
271+
272+
// Force no-results when the intersection is empty (e.g. teacher
273+
// does not own any lessons in the selected course).
274+
$activity_args['post__in'] = empty( $intersected ) ? array( 0 ) : $intersected;
275+
} elseif ( ! empty( $activity_args['post_id'] ) ) {
276+
// Validate that the specific lesson belongs to this teacher's courses.
277+
if ( ! in_array( (int) $activity_args['post_id'], array_map( 'intval', $count_restrictions['post__in'] ), true ) ) {
278+
$activity_args['post__in'] = array( 0 );
279+
}
280+
} else {
281+
$activity_args['post__in'] = $count_restrictions['post__in'];
282+
}
256283
}
257-
$statuses = Sensei_Utils::sensei_check_for_activity( $activity_args, true );
258-
// Need to always return an array, even with only 1 item
259-
if ( ! is_array( $statuses ) ) {
260-
$statuses = array( $statuses );
284+
285+
// Pass through temp-user exclusion for the listing service.
286+
if ( ! empty( $count_restrictions['exclude_user_login_prefixes'] ) ) {
287+
$activity_args['exclude_user_login_prefixes'] = $count_restrictions['exclude_user_login_prefixes'];
288+
if ( ! empty( $count_restrictions['include_statuses_override'] ) ) {
289+
$activity_args['include_statuses_override'] = $count_restrictions['include_statuses_override'];
290+
}
261291
}
262-
$this->total_items = $total_statuses;
263-
$this->items = $statuses;
292+
293+
$result = $this->grading_listing_service->get_lesson_progress_items( $activity_args );
294+
$this->total_items = $result['total_count'];
295+
$this->items = $result['items'];
264296

265297
$total_items = $this->total_items;
266298
$total_pages = ceil( $total_items / $per_page );
@@ -277,46 +309,52 @@ public function prepare_items() {
277309
* Generates content for a single row of the table, overriding parent
278310
*
279311
* @since 1.7.0
280-
* @param object $item The current item
312+
* @param Grading_Item $item The current item.
281313
*/
282314
protected function get_row_data( $item ) {
283-
global $wp_version;
315+
$status = $item->status;
316+
$user_id = $item->user_id;
317+
$lesson_id = $item->lesson_id;
318+
$updated = $item->updated_at;
319+
$grade_val = $item->grade;
320+
321+
$grade_display = null !== $grade_val ? $grade_val . '%' : __( 'N/A', 'sensei-lms' );
284322

285323
$grade = '';
286-
if ( 'complete' == $item->comment_approved ) {
324+
if ( 'complete' == $status ) {
287325
$status_html = '<span class="graded">' . esc_html__( 'Completed', 'sensei-lms' ) . '</span>';
288326
$grade = __( 'No Grade', 'sensei-lms' );
289-
} elseif ( 'graded' == $item->comment_approved ) {
327+
} elseif ( 'graded' == $status ) {
290328
$status_html = '<span class="graded">' . esc_html__( 'Graded', 'sensei-lms' ) . '</span>';
291-
$grade = get_comment_meta( $item->comment_ID, 'grade', true ) . '%';
292-
} elseif ( 'passed' == $item->comment_approved ) {
329+
$grade = $grade_display;
330+
} elseif ( 'passed' == $status ) {
293331
$status_html = '<span class="passed">' . esc_html__( 'Passed', 'sensei-lms' ) . '</span>';
294-
$grade = get_comment_meta( $item->comment_ID, 'grade', true ) . '%';
295-
} elseif ( 'failed' == $item->comment_approved ) {
332+
$grade = $grade_display;
333+
} elseif ( 'failed' == $status ) {
296334
$status_html = '<span class="failed">' . esc_html__( 'Failed', 'sensei-lms' ) . '</span>';
297-
$grade = get_comment_meta( $item->comment_ID, 'grade', true ) . '%';
298-
} elseif ( 'ungraded' == $item->comment_approved ) {
335+
$grade = $grade_display;
336+
} elseif ( 'ungraded' == $status ) {
299337
$status_html = '<span class="ungraded">' . esc_html__( 'Ungraded', 'sensei-lms' ) . '</span>';
300338
$grade = __( 'N/A', 'sensei-lms' );
301339
} else {
302340
$status_html = '<span class="in-progress">' . esc_html__( 'In Progress', 'sensei-lms' ) . '</span>';
303341
$grade = __( 'N/A', 'sensei-lms' );
304342
}
305343

306-
$title = Sensei_Learner::get_full_name( $item->user_id );
344+
$title = Sensei_Learner::get_full_name( $user_id );
307345

308-
$quiz_id = Sensei()->lesson->lesson_quizzes( $item->comment_post_ID, 'any' );
346+
$quiz_id = Sensei()->lesson->lesson_quizzes( $lesson_id, 'any' );
309347
$quiz_link = add_query_arg(
310348
array(
311349
'page' => $this->page_slug,
312-
'user' => $item->user_id,
350+
'user' => $user_id,
313351
'quiz_id' => $quiz_id,
314352
),
315353
admin_url( 'admin.php' )
316354
);
317355

318356
$grade_link = '';
319-
switch ( $item->comment_approved ) {
357+
switch ( $status ) {
320358
case 'ungraded':
321359
$grade_link = '<a class="button-primary button" href="' . esc_url( $quiz_link ) . '">' . esc_html__( 'Grade quiz', 'sensei-lms' ) . '</a>';
322360
break;
@@ -328,7 +366,7 @@ protected function get_row_data( $item ) {
328366
break;
329367
}
330368

331-
$course_id = get_post_meta( $item->comment_post_ID, '_lesson_course', true );
369+
$course_id = get_post_meta( $lesson_id, '_lesson_course', true );
332370
$course_title = '';
333371

334372
if ( ! empty( $course_id ) ) {
@@ -347,19 +385,19 @@ protected function get_row_data( $item ) {
347385
add_query_arg(
348386
array(
349387
'page' => $this->page_slug,
350-
'lesson_id' => $item->comment_post_ID,
388+
'lesson_id' => $lesson_id,
351389
),
352390
admin_url( 'admin.php' )
353391
)
354-
) . '">' . esc_html( get_the_title( $item->comment_post_ID ) ) . '</a>';
392+
) . '">' . esc_html( get_the_title( $lesson_id ) ) . '</a>';
355393

356394
/**
357395
* Filter columns data for the Grading list table.
358396
*
359397
* @hook sensei_grading_main_column_data
360398
*
361399
* @param {array} $column_data Column data for a row.
362-
* @param {object} $item Activity comment object.
400+
* @param {Grading_Item} $item Grading item for the row.
363401
* @param {int} $course_id The course ID.
364402
* @return {array} Filtered column data.
365403
*/
@@ -370,14 +408,14 @@ protected function get_row_data( $item ) {
370408
add_query_arg(
371409
array(
372410
'page' => $this->page_slug,
373-
'user_id' => $item->user_id,
411+
'user_id' => $user_id,
374412
),
375413
admin_url( 'admin.php' )
376414
)
377415
) . '">' . esc_html( $title ) . '</a></strong>',
378416
'course' => $course_title,
379417
'lesson' => $lesson_title,
380-
'updated' => $item->comment_date,
418+
'updated' => $updated,
381419
'user_status' => $status_html,
382420
'user_grade' => $grade,
383421
'action' => $grade_link,
@@ -500,7 +538,8 @@ public function get_views() {
500538

501539
// Setup counters.
502540
$count_args = array(
503-
'type' => 'lesson',
541+
'type' => 'lesson',
542+
'exclude_unsubmitted_quiz_completions' => true,
504543
);
505544
$query_args = array(
506545
'page' => $this->page_slug,
@@ -566,7 +605,23 @@ public function get_views() {
566605
*/
567606
$count_args = apply_filters( 'sensei_grading_count_statuses', $count_args );
568607

569-
$counts = Sensei()->grading->count_statuses( $count_args );
608+
// Use cached per-status counts from prepare_items() when available,
609+
// avoiding a second full-table scan with the same JOINs.
610+
$cached_counts = $this->grading_listing_service->get_status_counts();
611+
if ( null !== $cached_counts ) {
612+
// Ensure all expected statuses exist with 0 defaults, matching
613+
// the shape that count_statuses() returns.
614+
$defaults = array_fill_keys(
615+
array( 'graded', 'ungraded', 'passed', 'failed', 'in-progress', 'complete' ),
616+
0
617+
);
618+
$counts = array_merge( $defaults, $cached_counts );
619+
620+
/** This filter is documented in includes/class-sensei-grading.php */
621+
$counts = apply_filters( 'sensei_count_statuses', $counts, 'sensei_lesson_status' );
622+
} else {
623+
$counts = Sensei()->grading->count_statuses( $count_args );
624+
}
570625

571626
$inprogress_lessons_count = $counts['in-progress'];
572627
$ungraded_lessons_count = $counts['ungraded'];

0 commit comments

Comments
 (0)