Source: includes/reports/overview/list-table/class-sensei-reports-overview-list-table-courses.php

<?php
/**
 * File containing the Sensei_Reports_Overview_List_Table_Courses class.
 *
 * @package sensei
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

use Sensei\Internal\Services\Progress_Aggregation_Service_Interface;
use Sensei\Internal\Services\Utils;

/**
 * Courses overview list table class.
 *
 * @since 4.3.0
 */
class Sensei_Reports_Overview_List_Table_Courses extends Sensei_Reports_Overview_List_Table_Abstract {
	/**
	 * Sensei reports courses service.
	 *
	 * @var Sensei_Reports_Overview_Service_Courses
	 */
	private $reports_overview_service_courses;

	/**
	 * The progress aggregation service.
	 *
	 * @var Progress_Aggregation_Service_Interface
	 */
	private $aggregation_service;

	/**
	 * Per-course completion-day averages for the current result set.
	 *
	 * @var array<int, float>
	 */
	private $average_days_to_completion_by_course = array();

	/**
	 * Per-course average-progress cache for the current result set.
	 *
	 * @var float[]
	 */
	private $average_progress_by_course = array();

	/**
	 * Eligible completion counts for the current page or CSV export.
	 *
	 * @var array<int, int>
	 */
	private array $completion_counts_by_course = array();

	/**
	 * Unrounded average grades for the current page or CSV export.
	 *
	 * @var array<int, float>
	 */
	private array $average_grades_by_course = array();

	/**
	 * Constructor
	 *
	 * @param Sensei_Grading                                  $grading Unused. Retained for backward compatibility.
	 * @param Sensei_Course                                   $course Unused. Retained for backward compatibility.
	 * @param Sensei_Reports_Overview_Data_Provider_Interface $data_provider Report data provider.
	 * @param Sensei_Reports_Overview_Service_Courses         $reports_overview_service_courses reports courses service.
	 * @param Progress_Aggregation_Service_Interface          $aggregation_service The progress aggregation service.
	 */
	public function __construct( Sensei_Grading $grading, Sensei_Course $course, Sensei_Reports_Overview_Data_Provider_Interface $data_provider, Sensei_Reports_Overview_Service_Courses $reports_overview_service_courses, Progress_Aggregation_Service_Interface $aggregation_service ) {
		// Load Parent token into constructor.
		parent::__construct( 'courses', $data_provider );

		$this->reports_overview_service_courses = $reports_overview_service_courses;
		$this->aggregation_service              = $aggregation_service;
	}

	/**
	 * Prepare the table items and prime the per-course aggregate caches for the current page.
	 */
	public function prepare_items() {
		parent::prepare_items();
		$this->prime_row_aggregates( $this->items );
	}

	/**
	 * Prime the per-course aggregate caches before generating CSV report rows.
	 *
	 * @param array $items The courses that will be exported.
	 */
	protected function before_generate_report_rows( array $items ) {
		$this->prime_row_aggregates( $items );
	}

	/**
	 * Define the columns that are going to be used in the table
	 *
	 * @return array The array of columns to use with the table
	 */
	public function get_columns() {
		if ( $this->columns ) {
			return $this->columns;
		}

		$all_course_ids   = $this->get_all_item_ids();
		$total_completion = 0;
		if ( ! empty( $all_course_ids ) ) {
			$counts           = $this->aggregation_service->count_statuses_by_post(
				$all_course_ids,
				array(
					'exclude_user_login_prefixes' => Utils::REPORTS_EXCLUDED_USER_LOGIN_PREFIXES,
				)
			);
			$total_completion = (int) array_sum( array_column( $counts, 'complete' ) );
		}

		$total_average_progress = $this->reports_overview_service_courses->get_total_average_progress( $all_course_ids );
		$total_enrolled         = $this->reports_overview_service_courses->get_total_enrollments( $all_course_ids );

		$columns = array(
			'title'              => sprintf(
			// translators: Placeholder value is the number of courses.
				__( 'Course (%d)', 'sensei-lms' ),
				esc_html( count( $all_course_ids ) )
			),
			'last_activity'      => __( 'Last Activity', 'sensei-lms' ),
			'enrolled'           => sprintf(
			// translators: Placeholder value is the total number of enrollments across all courses.
				__( 'Enrolled (%d)', 'sensei-lms' ),
				$total_enrolled
			),

			'completions'        => sprintf(
			// translators: Placeholder value represents the total number of enrollments that have completed courses..
				__( 'Completions (%s)', 'sensei-lms' ),
				$total_completion
			),
			'completion_rate'    => sprintf(
			// translators: Placeholder value represents the % of enrolled students that completed the course.
				__( 'Completion Rate (%s)', 'sensei-lms' ),
				$this->get_completion_rate( $total_enrolled, $total_completion )
			),
			'average_progress'   => sprintf(
			// translators: Placeholder value is the total average progress for all courses.
				__( 'Average Progress (%s)', 'sensei-lms' ),
				esc_html( sprintf( '%d%%', $total_average_progress ) )
			),
			'average_percent'    => sprintf(
			// translators: Placeholder value is the average grade of all courses.
				__( 'Average Grade (%s%%)', 'sensei-lms' ),
				esc_html( ceil( $this->reports_overview_service_courses->get_courses_average_grade( $all_course_ids ) ) )
			),
			'days_to_completion' => sprintf(
			// translators: Placeholder value is average days to completion.
				__( 'Days to Completion (%d)', 'sensei-lms' ),
				ceil( $this->reports_overview_service_courses->get_average_days_to_completion( $all_course_ids ) )
			),
		);

		// Backwards compatible filter name, moving forward should have single filter name.
		/**
		 * Filter the columns for the courses overview report.
		 *
		 * @hook sensei_analysis_overview_courses_columns
		 *
		 * @param {array} $columns The columns for the courses overview report.
		 * @param {Sensei_Reports_Overview_List_Table_Courses} $this The current instance of the class.
		 * @return {array} The filtered columns.
		 */
		$columns = apply_filters( 'sensei_analysis_overview_courses_columns', $columns, $this );

		/**
		 * Filter the columns for the courses overview report.
		 *
		 * @hook sensei_analysis_overview_columns
		 *
		 * @param {array} $columns The columns for the courses overview report.
		 * @param {Sensei_Reports_Overview_List_Table_Courses} $this The current instance of the class.
		 * @return {array} The filtered columns.
		 */
		$columns = apply_filters( 'sensei_analysis_overview_columns', $columns, $this );

		$this->columns = $columns;

		return $this->columns;
	}

	/**
	 * Define the columns that are going to be used in the table
	 *
	 * @return array The array of columns to use with the table
	 */
	public function get_sortable_columns() {
		$columns = array(
			'title' => array( 'title', false ),
		);

		// Backwards compatible filter name, moving forward should have single filter name.
		/**
		 * Filter the sortable columns for the courses overview report.
		 *
		 * @hook sensei_analysis_overview_courses_columns_sortable
		 *
		 * @param {array} $columns The sortable columns for the courses overview report.
		 * @param {Sensei_Reports_Overview_List_Table_Courses} $this The current instance of the class.
		 * @return {array} The filtered sortable columns.
		 */
		$columns = apply_filters( 'sensei_analysis_overview_courses_columns_sortable', $columns, $this );

		/**
		 * Filter the sortable columns for the courses overview report.
		 *
		 * @hook sensei_analysis_overview_columns_sortable
		 *
		 * @param {array} $columns The sortable columns for the courses overview report.
		 * @param {Sensei_Reports_Overview_List_Table_Courses} $this The current instance of the class.
		 * @return {array} The filtered sortable columns.
		 */
		$columns = apply_filters( 'sensei_analysis_overview_columns_sortable', $columns, $this );

		return $columns;
	}

	/**
	 * Generates the overall array for a single item in the display
	 *
	 * @param object $item The current item.
	 *
	 * @return array Report row data.
	 * @throws Exception If date-time conversion fails.
	 */
	protected function get_row_data( $item ) {
		$course_completions   = $this->completion_counts_by_course[ (int) $item->ID ] ?? 0;
		$course_average_grade = $this->average_grades_by_course[ (int) $item->ID ] ?? null;
		$average_grade        = null === $course_average_grade
			? __( 'N/A', 'sensei-lms' )
			: Sensei_Utils::as_absolute_rounded_number( $course_average_grade, 2 ) . '%';

		$average_completion_days = $this->average_days_to_completion_by_course[ (int) $item->ID ] ?? __( 'N/A', 'sensei-lms' );

		// Output course data.
		$course_title   = apply_filters( 'the_title', $item->post_title, $item->ID ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals
		$total_enrolled = $this->reports_overview_service_courses->get_total_enrollments( array( $item->ID ) );

		if ( ! $this->csv_output ) {
			$url = add_query_arg(
				array(
					'page'      => $this->page_slug,
					'course_id' => $item->ID,
				),
				admin_url( 'admin.php' )
			);

			$course_title = '<strong><a class="row-title" href="' . esc_url( $url ) . '">' . $course_title . '</a></strong>';
		}

		$average_course_progress = $this->get_average_progress_for_courses_table( $item->ID );

		/**
		 * Filter the row data for the Analysis Overview list table.
		 *
		 * @hook sensei_analysis_overview_column_data
		 *
		 * @param {array} $column_data Array of column data for the report table.
		 * @param {object|WP_Post|WP_User} $item Current row object.
		 * @param {Sensei_Reports_Overview_List_Table_Courses} $this Current instance of the list table.
		 * @return {array} Filtered array of column data for the report table.
		 */
		$column_data = apply_filters(
			'sensei_analysis_overview_column_data',
			array(
				'title'              => $course_title,
				'last_activity'      => $item->last_activity_date ? Sensei_Utils::format_last_activity_date( $item->last_activity_date ) : __( 'N/A', 'sensei-lms' ),
				'enrolled'           => $total_enrolled,
				'completions'        => $course_completions,
				'completion_rate'    => $this->get_completion_rate( $total_enrolled, $course_completions ),
				'average_progress'   => $average_course_progress,
				'average_percent'    => $average_grade,
				'days_to_completion' => $average_completion_days,
			),
			$item,
			$this
		);

		$escaped_column_data = array();

		foreach ( $column_data as $key => $data ) {
			$escaped_column_data[ $key ] = wp_kses_post( $data );
		}

		return $escaped_column_data;
	}

	/**
	 * Get completion rate for a lesson.
	 *
	 * @since 4.15.1
	 *
	 * @param int $total_enrollments Total of enrollments in a course.
	 * @param int $total_completion Total of students who completed the course.
	 *
	 * @return string The completion rate or 'N/A' if there are no enrollment.
	 */
	private function get_completion_rate( int $total_enrollments, int $total_completion ): string {
		if ( 0 >= $total_enrollments ) {
			return __( 'N/A', 'sensei-lms' );
		}

		return Sensei_Utils::quotient_as_absolute_rounded_percentage( $total_completion, $total_enrollments ) . '%';
	}

	/**
	 * Calculate average lesson progress per student for course.
	 *
	 * @since 4.3.0
	 *
	 * @param int $course_id Id of the course for which average progress is calculated.
	 *
	 * @return string The average progress for the course, or N/A if none.
	 */
	private function get_average_progress_for_courses_table( $course_id ) {
		$average_course_progress = $this->average_progress_by_course[ (int) $course_id ] ?? null;

		if ( null === $average_course_progress ) {
			return __( 'N/A', 'sensei-lms' );
		}

		return esc_html( sprintf( '%d%%', round( $average_course_progress ) ) );
	}

	/**
	 * The text for the search button.
	 *
	 * @return string
	 */
	public function search_button() {
		return __( 'Search Courses', 'sensei-lms' );
	}

	/**
	 * Return additional filters for current report.
	 *
	 * @return array
	 */
	protected function get_additional_filters(): array {
		return array(
			'last_activity_date_from' => $this->get_start_date_and_time(),
			'last_activity_date_to'   => $this->get_end_date_and_time(),
		);
	}

	/**
	 * Prime the per-course aggregate caches for the given report items.
	 *
	 * @param array $items Course report items with an ID.
	 */
	private function prime_row_aggregates( array $items ) {
		$course_ids = array_map(
			static function ( $item ) {
				return (int) $item->ID;
			},
			$items
		);

		$this->average_days_to_completion_by_course = $this->reports_overview_service_courses->get_average_days_to_completion_by_course( $course_ids );
		$this->average_progress_by_course           = $this->reports_overview_service_courses->get_average_progress_by_course( $course_ids );
		$this->completion_counts_by_course          = $this->reports_overview_service_courses->get_completion_counts_by_course( $course_ids );
		$this->average_grades_by_course             = $this->reports_overview_service_courses->get_average_grades_by_course( $course_ids );
	}
}