Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/add-grading-service-migration
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Internal: Integrate HPPS in grading backend.
4 changes: 4 additions & 0 deletions changelog/fix-grading-tab-counts-lesson-filter
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Fix Grading page status tab counts not updating when filtering by a specific lesson.
4 changes: 4 additions & 0 deletions changelog/fix-grading-teacher-restrictions
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Fix teachers not seeing all grading rows for their courses due to post-filter pagination mismatch.
41 changes: 30 additions & 11 deletions config/psalm/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1620,17 +1620,13 @@
</file>
<file src="includes/class-sensei-grading-main.php">
<ArgumentTypeCoercion occurrences="1"/>
<InvalidArrayAccess occurrences="6">
<code>$counts['complete']</code>
<code>$counts['failed']</code>
<code>$counts['graded']</code>
<code>$counts['in-progress']</code>
<code>$counts['passed']</code>
<code>$counts['ungraded']</code>
</InvalidArrayAccess>
<MissingParamType occurrences="1">
<code>$args</code>
</MissingParamType>
<InternalProperty occurrences="5">
<code>$item-&gt;grade</code>
<code>$item-&gt;lesson_id</code>
<code>$item-&gt;status</code>
<code>$item-&gt;updated_at</code>
<code>$item-&gt;user_id</code>
</InternalProperty>
<MissingPropertyType occurrences="6">
<code>$course_id</code>
<code>$lesson_id</code>
Expand All @@ -1642,6 +1638,9 @@
<PossiblyFalsePropertyAssignmentValue occurrences="1">
<code>$search</code>
</PossiblyFalsePropertyAssignmentValue>
<PossiblyNullArgument occurrences="1">
<code>$args</code>
</PossiblyNullArgument>
<PropertyNotSetInConstructor occurrences="8">
<code>Sensei_Grading_Main</code>
<code>Sensei_Grading_Main</code>
Expand Down Expand Up @@ -4556,6 +4555,11 @@
<code>$row-&gt;user_id</code>
</PossiblyInvalidPropertyFetch>
</file>
<file src="includes/internal/services/class-comments-based-grading-listing-service.php">
<InvalidScalarArgument occurrences="1">
<code>$comment-&gt;comment_ID</code>
</InvalidScalarArgument>
</file>
<file src="includes/internal/services/class-comments-based-progress-aggregation-service.php">
<PossiblyInvalidPropertyFetch occurrences="5">
<code>$row-&gt;days_to_complete_count</code>
Expand All @@ -4568,6 +4572,21 @@
<code>ARRAY_A</code>
</UndefinedConstant>
</file>
<file src="includes/internal/services/class-tables-based-grading-listing-service.php">
<PossiblyInvalidCast occurrences="1">
<code>$column</code>
</PossiblyInvalidCast>
<PossiblyInvalidPropertyFetch occurrences="5">
<code>$row-&gt;effective_status</code>
<code>$row-&gt;final_grade</code>
<code>$row-&gt;post_id</code>
<code>$row-&gt;updated_at</code>
<code>$row-&gt;user_id</code>
</PossiblyInvalidPropertyFetch>
<UndefinedConstant occurrences="1">
<code>ARRAY_A</code>
</UndefinedConstant>
</file>
<file src="includes/internal/services/class-tables-based-progress-aggregation-service.php">
<PossiblyInvalidPropertyFetch occurrences="5">
<code>$row-&gt;days_to_complete_count</code>
Expand Down
145 changes: 101 additions & 44 deletions includes/class-sensei-grading-main.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
exit; // Exit if accessed directly
}

use Sensei\Internal\Services\Grading_Listing_Service_Interface;
use Sensei\Internal\Services\Grading_Item;
use Sensei\Internal\Services\Progress_Query_Service_Factory;

/**
* Admin Grading Overview Data Table in Sensei.
*
Expand All @@ -19,12 +23,22 @@ class Sensei_Grading_Main extends Sensei_List_Table {
public $user_ids = false;
public $page_slug = 'sensei_grading';

/**
* The grading listing service.
*
* @var Grading_Listing_Service_Interface
*/
private Grading_Listing_Service_Interface $grading_listing_service;

/**
* Constructor
*
* @since 1.3.0
*
* @param array|null $args Constructor arguments.
* @param Grading_Listing_Service_Interface|null $grading_listing_service The grading listing service.
*/
public function __construct( $args = null ) {
public function __construct( $args = null, ?Grading_Listing_Service_Interface $grading_listing_service = null ) {

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

$this->grading_listing_service = $grading_listing_service
?? ( new Progress_Query_Service_Factory() )->create_grading_listing_service();

// Load Parent token into constructor
parent::__construct( 'grading_main' );

Expand Down Expand Up @@ -237,30 +254,45 @@ public function prepare_items() {
*/
$activity_args = apply_filters( 'sensei_grading_filter_statuses', $activity_args );

// WP_Comment_Query doesn't support SQL_CALC_FOUND_ROWS, so instead do this twice
$total_statuses = Sensei_Utils::sensei_check_for_activity(
array_merge(
$activity_args,
array(
'count' => true,
'offset' => 0,
'number' => 0,
)
)
);

// Ensure we change our range to fit (in case a search threw off the pagination) - Should this be added to all views?
if ( $total_statuses < $activity_args['offset'] ) {
$new_paged = floor( $total_statuses / $activity_args['number'] );
$activity_args['offset'] = $new_paged * $activity_args['number'];
// Apply teacher and temp-user restrictions so that both listing rows
// and cached per-status counts reflect these filters. For tables-based
// storage, these args are applied as SQL clauses. For comments-based,
// post__in flows through to WP_Comment_Query and the remaining args
// are handled by existing post-filters on sensei_check_for_activity.
$count_restrictions = apply_filters( 'sensei_count_statuses_args', array( 'type' => 'lesson' ) );

// Merge teacher's post__in restriction.
if ( ! empty( $count_restrictions['post__in'] ) ) {
if ( ! empty( $activity_args['post__in'] ) ) {
// Intersect: keep only lessons in both the course filter and teacher filter.
$intersected = array_values(
array_intersect( $activity_args['post__in'], $count_restrictions['post__in'] )
);

// Force no-results when the intersection is empty (e.g. teacher
// does not own any lessons in the selected course).
$activity_args['post__in'] = empty( $intersected ) ? array( 0 ) : $intersected;
} elseif ( ! empty( $activity_args['post_id'] ) ) {
// Validate that the specific lesson belongs to this teacher's courses.
if ( ! in_array( (int) $activity_args['post_id'], array_map( 'intval', $count_restrictions['post__in'] ), true ) ) {
$activity_args['post__in'] = array( 0 );
}
} else {
$activity_args['post__in'] = $count_restrictions['post__in'];
}
}
$statuses = Sensei_Utils::sensei_check_for_activity( $activity_args, true );
// Need to always return an array, even with only 1 item
if ( ! is_array( $statuses ) ) {
$statuses = array( $statuses );

// Pass through temp-user exclusion for the listing service.
if ( ! empty( $count_restrictions['exclude_user_login_prefixes'] ) ) {
$activity_args['exclude_user_login_prefixes'] = $count_restrictions['exclude_user_login_prefixes'];
if ( ! empty( $count_restrictions['include_statuses_override'] ) ) {
$activity_args['include_statuses_override'] = $count_restrictions['include_statuses_override'];
}
}
$this->total_items = $total_statuses;
$this->items = $statuses;

$result = $this->grading_listing_service->get_lesson_progress_items( $activity_args );
$this->total_items = $result['total_count'];
$this->items = $result['items'];

$total_items = $this->total_items;
$total_pages = ceil( $total_items / $per_page );
Expand All @@ -277,46 +309,52 @@ public function prepare_items() {
* Generates content for a single row of the table, overriding parent
*
* @since 1.7.0
* @param object $item The current item
* @param Grading_Item $item The current item.
*/
protected function get_row_data( $item ) {
global $wp_version;
$status = $item->status;
$user_id = $item->user_id;
$lesson_id = $item->lesson_id;
$updated = $item->updated_at;
$grade_val = $item->grade;

$grade_display = null !== $grade_val ? $grade_val . '%' : __( 'N/A', 'sensei-lms' );

$grade = '';
if ( 'complete' == $item->comment_approved ) {
if ( 'complete' == $status ) {
$status_html = '<span class="graded">' . esc_html__( 'Completed', 'sensei-lms' ) . '</span>';
$grade = __( 'No Grade', 'sensei-lms' );
} elseif ( 'graded' == $item->comment_approved ) {
} elseif ( 'graded' == $status ) {
$status_html = '<span class="graded">' . esc_html__( 'Graded', 'sensei-lms' ) . '</span>';
$grade = get_comment_meta( $item->comment_ID, 'grade', true ) . '%';
} elseif ( 'passed' == $item->comment_approved ) {
$grade = $grade_display;
} elseif ( 'passed' == $status ) {
$status_html = '<span class="passed">' . esc_html__( 'Passed', 'sensei-lms' ) . '</span>';
$grade = get_comment_meta( $item->comment_ID, 'grade', true ) . '%';
} elseif ( 'failed' == $item->comment_approved ) {
$grade = $grade_display;
} elseif ( 'failed' == $status ) {
$status_html = '<span class="failed">' . esc_html__( 'Failed', 'sensei-lms' ) . '</span>';
$grade = get_comment_meta( $item->comment_ID, 'grade', true ) . '%';
} elseif ( 'ungraded' == $item->comment_approved ) {
$grade = $grade_display;
} elseif ( 'ungraded' == $status ) {
$status_html = '<span class="ungraded">' . esc_html__( 'Ungraded', 'sensei-lms' ) . '</span>';
$grade = __( 'N/A', 'sensei-lms' );
} else {
$status_html = '<span class="in-progress">' . esc_html__( 'In Progress', 'sensei-lms' ) . '</span>';
$grade = __( 'N/A', 'sensei-lms' );
}

$title = Sensei_Learner::get_full_name( $item->user_id );
$title = Sensei_Learner::get_full_name( $user_id );

$quiz_id = Sensei()->lesson->lesson_quizzes( $item->comment_post_ID, 'any' );
$quiz_id = Sensei()->lesson->lesson_quizzes( $lesson_id, 'any' );
$quiz_link = add_query_arg(
array(
'page' => $this->page_slug,
'user' => $item->user_id,
'user' => $user_id,
'quiz_id' => $quiz_id,
),
admin_url( 'admin.php' )
);

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

$course_id = get_post_meta( $item->comment_post_ID, '_lesson_course', true );
$course_id = get_post_meta( $lesson_id, '_lesson_course', true );
$course_title = '';

if ( ! empty( $course_id ) ) {
Expand All @@ -347,19 +385,19 @@ protected function get_row_data( $item ) {
add_query_arg(
array(
'page' => $this->page_slug,
'lesson_id' => $item->comment_post_ID,
'lesson_id' => $lesson_id,
),
admin_url( 'admin.php' )
)
) . '">' . esc_html( get_the_title( $item->comment_post_ID ) ) . '</a>';
) . '">' . esc_html( get_the_title( $lesson_id ) ) . '</a>';

/**
* Filter columns data for the Grading list table.
*
* @hook sensei_grading_main_column_data
*
* @param {array} $column_data Column data for a row.
* @param {object} $item Activity comment object.
* @param {Grading_Item} $item Grading item for the row.
* @param {int} $course_id The course ID.
* @return {array} Filtered column data.
*/
Expand All @@ -370,14 +408,14 @@ protected function get_row_data( $item ) {
add_query_arg(
array(
'page' => $this->page_slug,
'user_id' => $item->user_id,
'user_id' => $user_id,
),
admin_url( 'admin.php' )
)
) . '">' . esc_html( $title ) . '</a></strong>',
'course' => $course_title,
'lesson' => $lesson_title,
'updated' => $item->comment_date,
'updated' => $updated,
'user_status' => $status_html,
'user_grade' => $grade,
'action' => $grade_link,
Expand Down Expand Up @@ -566,7 +604,26 @@ public function get_views() {
*/
$count_args = apply_filters( 'sensei_grading_count_statuses', $count_args );

$counts = Sensei()->grading->count_statuses( $count_args );
// Use cached per-status counts from prepare_items() when available,
// avoiding a second full-table scan with the same JOINs.
// Skip the cache if a plugin is filtering $count_args, since the
// cached counts would not reflect those modifications.
$has_count_filter = has_filter( 'sensei_grading_count_statuses' ) || has_filter( 'sensei_grading_count_statues' );
$cached_counts = $this->grading_listing_service->get_status_counts();
if ( null !== $cached_counts && ! $has_count_filter ) {
// Ensure all expected statuses exist with 0 defaults, matching
// the shape that count_statuses() returns.
$defaults = array_fill_keys(
array( 'graded', 'ungraded', 'passed', 'failed', 'in-progress', 'complete' ),
0
);
$counts = array_merge( $defaults, $cached_counts );

/** This filter is documented in includes/class-sensei-grading.php */
$counts = apply_filters( 'sensei_count_statuses', $counts, 'sensei_lesson_status' );
} else {
$counts = Sensei()->grading->count_statuses( $count_args );
}

$inprogress_lessons_count = $counts['in-progress'];
$ungraded_lessons_count = $counts['ungraded'];
Expand Down
5 changes: 3 additions & 2 deletions includes/class-sensei-grading.php
Original file line number Diff line number Diff line change
Expand Up @@ -555,9 +555,10 @@ public function get_stati( $type ) {
*/
public function count_statuses( $args = array() ) {
/**
* Filter fires inside Sensei_Grading::count_statuses
* Filter the arguments used to count progress statuses.
*
* Alter the post_in array to determine which posts the comment query should be limited to.
* Alter the query arguments (post restrictions, user exclusions) used
* to count progress statuses on the Grading page.
*
* @since 1.8.0
*
Expand Down
Loading
Loading