Skip to content
Open
Show file tree
Hide file tree
Changes from all 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