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

<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * Analysis Overview Data Table.
 *
 * @package Analytics
 * @author Automattic
 *
 * @since 1.2.0
 * @deprecated 4.3.0 Use Sensei_Reports_Overview_List_Table_Factory to create proper instances.
 */
class Sensei_Analysis_Overview_List_Table extends Sensei_List_Table {

	public $type;
	public $page_slug;

	/**
	 * Constructor
	 *
	 * @param string $type Report type.
	 *
	 * @since  1.2.0
	 */
	public function __construct( $type = 'users' ) {
		_deprecated_function( __METHOD__, '4.3.0', 'Sensei_Reports_Overview_List_Table_Factory::create()' );

		$this->type      = in_array( $type, array( 'courses', 'lessons', 'users' ), true ) ? $type : 'users';
		$this->page_slug = Sensei_Analysis::PAGE_SLUG;

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

		// Actions.
		add_action( 'sensei_before_list_table', array( $this, 'output_top_filters' ) );
		add_action( 'sensei_after_list_table', array( $this, 'data_table_footer' ) );
		add_filter( 'sensei_list_table_search_button_text', array( $this, 'search_button' ) );
		add_filter( 'sensei_analysis_overview_columns', array( $this, 'add_totals_to_report_column_headers' ) );
	}

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

		switch ( $this->type ) {
			case 'courses':
				$total_completions = Sensei_Utils::sensei_check_for_activity(
					array(
						'type'   => 'sensei_course_status',
						'status' => 'complete',
					)
				);
				$columns           = array(
					'title'              => sprintf(
						// translators: Placeholder value is the number of courses.
						__( 'Course (%d)', 'sensei-lms' ),
						esc_html( $this->total_items )
					),
					'last_activity'      => __( 'Last Activity', 'sensei-lms' ),
					'completions'        => sprintf(
						// translators: Placeholder value is the number of completed courses.
						__( 'Completed (%d)', 'sensei-lms' ),
						esc_html( $total_completions )
					),
					'average_progress'   => __( 'Average Progress', 'sensei-lms' ),
					'average_grade'      => sprintf(
					// translators: Placeholder value is the average grade of all courses.
						__( 'Average Grade (%s%%)', 'sensei-lms' ),
						esc_html( ceil( Sensei()->grading->get_courses_average_grade() ) )
					),
					'days_to_completion' => sprintf(
						// translators: Placeholder value is average days to completion.
						__( 'Days to Completion (%d)', 'sensei-lms' ),
						ceil( Sensei()->course->get_average_days_to_completion() )
					),
				);
				break;

			case 'lessons':
				$columns = array(
					'title'              => __( 'Lesson', 'sensei-lms' ),
					'students'           => __( 'Students', 'sensei-lms' ),
					'last_activity'      => __( 'Last Activity', 'sensei-lms' ),
					'completions'        => __( 'Completed', 'sensei-lms' ),
					'completion_rate'    => __( 'Completion Rate', 'sensei-lms' ),
					'days_to_completion' => __( 'Days to Completion', 'sensei-lms' ),
				);
				break;

			case 'users':
			default:
				// Get total value for Courses Completed column in users table.
				$course_args_completed   = array(
					'type'   => 'sensei_course_status',
					'status' => 'complete',
				);
				$total_completed_courses = Sensei_Utils::sensei_check_for_activity( $course_args_completed );

				// Get the number of the courses that users have started.
				$course_args_started   = array(
					'type'   => 'sensei_course_status',
					'status' => 'any',
				);
				$total_courses_started = Sensei_Utils::sensei_check_for_activity( $course_args_started );

				// Get total average students grade.
				$total_average_grade = Sensei()->grading->get_graded_lessons_average_grade();

				$columns = array(
					// translators: Placeholder value is total count of students.
					'title'             => sprintf( __( 'Student (%d)', 'sensei-lms' ), esc_html( $this->total_items ) ),
					'email'             => __( 'Email', 'sensei-lms' ),
					'date_registered'   => __( 'Date Registered', 'sensei-lms' ),
					'last_activity'     => __( 'Last Activity', 'sensei-lms' ),
					// translators: Placeholder value is all active courses.
					'active_courses'    => sprintf( __( 'Active Courses (%d)', 'sensei-lms' ), esc_html( $total_courses_started - $total_completed_courses ) ),
					// translators: Placeholder value is all completed courses.
					'completed_courses' => sprintf( __( 'Completed Courses (%d)', 'sensei-lms' ), esc_html( $total_completed_courses ) ),
					// translators: Placeholder value is graded average value.
					'average_grade'     => sprintf( __( 'Average Grade (%d%%)', 'sensei-lms' ), esc_html( $total_average_grade ) ),
				);
				break;
		}

		/**
		 * Filter the columns that are going to be used in the Analysis Overview list table.
		 *
		 * Backwards compatible filter name, moving forward should have single filter name.
		 *
		 * @deprecated 4.19.0 Use sensei_analysis_overview_columns instead.
		 *
		 * @hook sensei_analysis_overview_{type}_columns
		 *
		 * @param {array} $columns Array of columns for the report table.
		 * @param {Sensei_Analysis_Overview_List_Table} $this Current instance of the list table.
		 * @return {array} Filtered array of columns for the report table.
		 */
		$columns = apply_filters_deprecated(
			'sensei_analysis_overview_' . $this->type . '_columns',
			array( $columns, $this ),
			'4.19.0',
			'sensei_analysis_overview_columns'
		);

		/**
		 * Filter the columns that are going to be used in the Analysis Overview list table.
		 *
		 * @hook sensei_analysis_overview_columns
		 *
		 * @param {array} $columns Array of columns for the report table.
		 * @param {Sensei_Analysis_Overview_List_Table} $this Current instance of the list table.
		 * @return {array} Filtered array of columns for the report table.
		 */
		$columns = apply_filters( 'sensei_analysis_overview_columns', $columns, $this );

		$this->columns = $columns;

		return $this->columns;
	}
	/**
	 * Append the count value to column headers where applicable
	 *
	 * @since  4.2.0
	 * @access private
	 *
	 * @param array $columns Array of columns for the report table.
	 * @return array The array of columns to use with the table with columns appended to their title
	 */
	public function add_totals_to_report_column_headers( array $columns ) {
		if ( 'lessons' !== $this->type || ! $this->get_course_filter_value() ) {
			return $columns;
		}

		$total_counts                           = $this->get_totals_for_lesson_report_column_headers( $this->get_course_filter_value() );
		$column_value_map                       = array();
		$column_value_map['title']              = $total_counts->lesson_count;
		$column_value_map['lesson_module']      = $total_counts->unique_module_count;
		$column_value_map['students']           = $total_counts->unique_student_count;
		$column_value_map['completions']        = $total_counts->lesson_completed_count > 0 && $total_counts->lesson_count > 0
			? ceil( $total_counts->lesson_completed_count / $total_counts->lesson_count )
			: 0;
		$column_value_map['days_to_completion'] = $total_counts->lesson_completed_count > 0
			? ceil( $total_counts->days_to_complete_sum / $total_counts->lesson_completed_count )
			: __( 'N/A', 'sensei-lms' );
		$column_value_map['completion_rate']    = $total_counts->lesson_start_count > 0
			? Sensei_Utils::quotient_as_absolute_rounded_percentage( $total_counts->lesson_completed_count, $total_counts->lesson_start_count ) . '%'
			: '0%';

		foreach ( $column_value_map as $key => $value ) {
			if ( array_key_exists( $key, $columns ) ) {
				$columns[ $key ] = $columns[ $key ] . ' (' . esc_html( $value ) . ')';
			}
		}
		return $columns;
	}
	/**
	 * Define the columns that are going to be used in the table
	 *
	 * @since  1.7.0
	 * @return array $columns, the array of columns to use with the table
	 */
	function get_sortable_columns() {

		switch ( $this->type ) {
			case 'courses':
				$columns = array(
					'title'       => array( 'title', false ),
					'completions' => array( 'count_of_completions', false ),
				);
				break;

			case 'lessons':
				$columns = array(
					'title' => array( 'title', false ),
				);
				break;

			case 'users':
			default:
				$columns = array(
					'title'           => array( 'display_name', false ),
					'email'           => array( 'user_email', false ),
					'date_registered' => array( 'user_registered', false ),
					'last_activity'   => array( 'last_activity_date', false ),
				);
				break;
		}

		/**
		 * Filter the sortable columns that are going to be used in the Analysis Overview list table.
		 *
		 * Backwards compatible filter name, moving forward should have single filter name.
		 *
		 * @deprecated 4.19.0 Use sensei_analysis_overview_columns_sortable instead.
		 *
		 * @hook sensei_analysis_overview_{type}_columns_sortable
		 *
		 * @param {array} $columns Array of sortable columns for the report table.
		 * @param {Sensei_Analysis_Overview_List_Table} $this Current instance of the list table.
		 * @return {array} Filtered array of sortable columns for the report table.
		 */
		$columns = apply_filters_deprecated(
			'sensei_analysis_overview_' . $this->type . '_columns_sortable',
			array( $columns, $this ),
			'4.19.0',
			'sensei_analysis_overview_columns_sortable'
		);

		/**
		 * Filter the sortable columns that are going to be used in the Analysis Overview list table.
		 *
		 * @hook sensei_analysis_overview_columns_sortable
		 *
		 * @param {array} $columns Array of sortable columns for the report table.
		 * @param {Sensei_Analysis_Overview_List_Table} $this Current instance of the list table.
		 * @return {array} Filtered array of sortable columns for the report table.
		 */
		$columns = apply_filters( 'sensei_analysis_overview_columns_sortable', $columns, $this );

		return $columns;
	}

	/**
	 * Prepare the table with different parameters, pagination, columns and table elements
	 *
	 * @since  1.7.0
	 * @return void
	 */
	public function prepare_items() {
		// Handle orderby.
		$orderby = '';
		// phpcs:ignore WordPress.Security.NonceVerification
		if ( ! empty( $_GET['orderby'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
			$orderby = esc_html( $_GET['orderby'] );
		}

		// Handle order.
		$order = 'ASC';
		// phpcs:ignore WordPress.Security.NonceVerification
		if ( ! empty( $_GET['order'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification,Universal.Operators.StrictComparisons.LooseComparison,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$order = ( 'ASC' == strtoupper( $_GET['order'] ) ) ? 'ASC' : 'DESC';
		}

		$per_page = $this->get_items_per_page( 'sensei_comments_per_page' );

		/**
		 * Filter the number of items per page.
		 *
		 * @hook sensei_comments_per_page
		 *
		 * @param {int} $per_page The number of items per page.
		 * @param {string} $type The type of items.
		 * @return {int} The number of items per page.
		 */
		$per_page = apply_filters( 'sensei_comments_per_page', $per_page, 'sensei_comments' );

		$paged  = $this->get_pagenum();
		$offset = 0;
		if ( ! empty( $paged ) ) {
			$offset = $per_page * ( $paged - 1 );
		}

		$args = array(
			'number'  => $per_page,
			'offset'  => $offset,
			'orderby' => $orderby,
			'order'   => $order,
		);

		// Handle search.
		// phpcs:ignore WordPress.Security.NonceVerification
		if ( isset( $_GET['s'] ) && ! empty( $_GET['s'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$args['search'] = esc_html( wp_unslash( $_GET['s'] ) );
		}

		switch ( $this->type ) {
			case 'courses':
				$this->items = $this->get_courses( $args );
				break;

			case 'lessons':
				$this->items = $this->get_lessons( $args, $this->get_course_filter_value() );
				break;

			case 'users':
			default:
				$this->items = $this->get_learners( $args );
				break;
		}

		$total_items = $this->total_items;
		$total_pages = ceil( $total_items / $per_page );
		$this->set_pagination_args(
			array(
				'total_items' => $total_items,
				'total_pages' => $total_pages,
				'per_page'    => $per_page,
			)
		);
	}

	// phpcs:ignore Squiz.Commenting.FunctionComment.MissingParamTag
	/**
	 * Generate a csv report with different parameters, pagination, columns and table elements
	 *
	 * @since  1.7.0
	 * @return data
	 */
	public function generate_report( $report ) {
		$data = array();

		$this->csv_output = true;

		// Handle orderby.
		$orderby = '';
		// phpcs:ignore WordPress.Security.NonceVerification
		if ( ! empty( $_GET['orderby'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$orderby = esc_html( $_GET['orderby'] );
		}

		// Handle order.
		$order = 'ASC';
		//phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! empty( $_GET['order'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification,Universal.Operators.StrictComparisons.LooseComparison,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$order = ( 'ASC' == strtoupper( $_GET['order'] ) ) ? 'ASC' : 'DESC';
		}

		$args = array(
			'number'  => -1,
			'offset'  => 0,
			'orderby' => $orderby,
			'order'   => $order,
		);

		// Handle search.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['s'] ) && ! empty( $_GET['s'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$args['search'] = esc_html( $_GET['s'] );
		}

		switch ( $this->type ) {
			case 'courses':
				$this->items = $this->get_courses( $args );
				break;

			case 'lessons':
				$this->items = $this->get_lessons( $args, $this->get_course_filter_value() );
				break;

			case 'users':
			default:
				$this->items = $this->get_learners( $args );
				break;
		}

		// Start the CSV with the column headings.
		$column_headers = array();
		$columns        = $this->get_columns();

		foreach ( $columns as $key => $title ) {
			$column_headers[] = $title;
		}

		$data[] = $column_headers;

		// Process each row.
		foreach ( $this->items as $item ) {
			$data[] = array_replace( $columns, $this->get_row_data( $item ) );
		}

		return $data;
	}

	/**
	 * Generates the overall array for a single item in the display
	 *
	 * @since  1.7.0
	 * @param object $item The current item.
	 * @return array $column_data;
	 */
	protected function get_row_data( $item ) {

		switch ( $this->type ) {
			case 'courses':
				// Last Activity.
				$last_activity_date = __( 'N/A', 'sensei-lms' );
				$lessons            = Sensei()->course->course_lessons( $item->ID, 'any', 'ids' );

				if ( $lessons ) {
					$last_activity_date = $this->get_last_activity_date( array( 'post__in' => $lessons ) );
				}

				// Get Course Completions.
				$course_args = array(
					'post_id' => $item->ID,
					'type'    => 'sensei_course_status',
					'status'  => 'complete',
				);

				/**
				 * Filter the course completions query arguments.
				 *
				 * @hook sensei_analysis_course_completions
				 *
				 * @param {array} $course_args Array of query arguments for course completions.
				 * @param {WP_Post} $item Current course post object.
				 * @return {array} Filtered array of query arguments for course completions.
				 */
				$course_args = apply_filters( 'sensei_analysis_course_completions', $course_args, $item );

				$course_completions = Sensei_Utils::sensei_check_for_activity( $course_args );

				// Average Grade will be N/A if the course has no lessons or quizzes, if none of the lessons
				// have a status of 'graded', 'passed' or 'failed', or if none of the quizzes have grades.
				$average_grade = __( 'N/A', 'sensei-lms' );

				// Get grades only if the course has lessons and quizzes.
				if ( ! empty( $lessons ) && Sensei()->course->course_quizzes( $item->ID, true ) ) {
					$grade_args = array(
						'post__in' => $lessons,
						'type'     => 'sensei_lesson_status',
						'status'   => array( 'graded', 'passed', 'failed' ),
						'meta_key' => 'grade',
					);

					/**
					 * Filter the course completion percentage query arguments.
					 *
					 * @hook sensei_analysis_course_percentage
					 *
					 * @param {array} $grade_args Array of query arguments for course percentage.
					 * @param {WP_Post} $item Current course post object.
					 * @return {array} Filtered array of query arguments for course percentage.
					 */
					$grade_args = apply_filters( 'sensei_analysis_course_percentage', $grade_args, $item );

					$percent_count = Sensei_Utils::sensei_check_for_activity( $grade_args, false );
					$percent_total = Sensei_Grading::get_course_users_grades_sum( $item->ID );

					if ( $percent_count > 0 && $percent_total >= 0 ) {
						$average_grade = Sensei_Utils::quotient_as_absolute_rounded_number( $percent_total, $percent_count, 2 ) . '%';
					}
				}

				// Properties `count_of_completions` and `days_to_completion` where added to items in
				// `Sensei_Analysis_Overview_List_Table::add_days_to_completion_to_courses_queries`.
				// We made it due to improve performance of the report. Don't try to access these properties outside.
				$average_completion_days = $item->count_of_completions > 0 ? ceil( $item->days_to_completion / $item->count_of_completions ) : __( 'N/A', 'sensei-lms' );

				// Output course data
				if ( $this->csv_output ) {
					$course_title = apply_filters( 'the_title', $item->post_title, $item->ID );
				} else {
					$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 ) . '">' . apply_filters( 'the_title', $item->post_title, $item->ID ) . '</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_Analysis_Overview_List_Table} $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'      => $last_activity_date,
						'completions'        => $course_completions,
						'average_progress'   => $average_course_progress,
						'average_grade'      => $average_grade,
						'days_to_completion' => $average_completion_days,
					),
					$item,
					$this
				);
				break;

			case 'lessons':
				// Get Learners (i.e. those who have started).
				$lesson_args = array(
					'post_id' => $item->ID,
					'type'    => 'sensei_lesson_status',
					'status'  => 'any',
				);

				/**
				 * Filter the lesson learners progress query arguments.
				 *
				 * @hook sensei_analysis_lesson_learners
				 *
				 * @param {array} $lesson_args Array of query arguments for lesson learners.
				 * @param {WP_Post} $item Current lesson post object.
				 * @return {array} Filtered array of query arguments for lesson learners.
				 */
				$lesson_args = apply_filters( 'sensei_analysis_lesson_learners', $lesson_args, $item );

				$lesson_students = Sensei_Utils::sensei_check_for_activity( $lesson_args );

				// Get Course Completions.
				$lesson_args = array(
					'post_id' => $item->ID,
					'type'    => 'sensei_lesson_status',
					'status'  => array( 'complete', 'graded', 'passed', 'failed', 'ungraded' ),
					'count'   => true,
				);

				/**
				 * Filter the lesson completions query arguments.
				 *
				 * @hook sensei_analysis_lesson_completions
				 *
				 * @param {array} $lesson_args Array of query arguments for lesson completions.
				 * @param {WP_Post} $item Current lesson post object.
				 * @return {array} Filtered array of query arguments for lesson completions.
				 */
				$lesson_args = apply_filters( 'sensei_analysis_lesson_completions', $lesson_args, $item );

				$lesson_completions = Sensei_Utils::sensei_check_for_activity( $lesson_args );

				// Taking the ceiling value for the average.
				$average_completion_days = $lesson_completions > 0 ? ceil( $item->days_to_complete / $lesson_completions ) : __( 'N/A', 'sensei-lms' );

				// Output lesson data.
				if ( $this->csv_output ) {
					$lesson_title = apply_filters( 'the_title', $item->post_title, $item->ID );
				} else {
					$url          = add_query_arg(
						array(
							'page'      => $this->page_slug,
							'lesson_id' => $item->ID,
						),
						admin_url( 'admin.php' )
					);
					$lesson_title = '<strong><a class="row-title" href="' . esc_url( $url ) . '">' . apply_filters( 'the_title', $item->post_title, $item->ID ) . '</a></strong>';

				}

				/**
				 * 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_Analysis_Overview_List_Table} $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'              => $lesson_title,
						'students'           => $lesson_students,
						'last_activity'      => $this->get_last_activity_date( array( 'post_id' => $item->ID ) ),
						'completions'        => $lesson_completions,
						'completion_rate'    => $this->get_completion_rate( $lesson_completions, $lesson_students ),
						'days_to_completion' => $average_completion_days,
					),
					$item,
					$this
				);
				break;

			case 'users':
			default:
				// Get Started Courses.
				$course_args = array(
					'user_id' => $item->ID,
					'type'    => 'sensei_course_status',
					'status'  => 'any',
				);

				/**
				 * Filter user progress query arguments for courses: find started courses.
				 *
				 * @hook sensei_analysis_user_courses_started
				 *
				 * @param {array} $course_args Array of query arguments for started user courses.
				 * @param {WP_User} $item Current user object.
				 * @return {array} Filtered array of query arguments for started user courses.
				 */
				$course_args = apply_filters( 'sensei_analysis_user_courses_started', $course_args, $item );

				$user_courses_started = Sensei_Utils::sensei_check_for_activity( $course_args );

				// Get Completed Courses.
				$course_args = array(
					'user_id' => $item->ID,
					'type'    => 'sensei_course_status',
					'status'  => 'complete',
				);

				/**
				 * Filter user progress query arguments for courses: find completed courses.
				 *
				 * @hook sensei_analysis_user_courses_ended
				 *
				 * @param {array} $course_args Array of query arguments for ended user courses.
				 * @param {WP_User} $item Current user object.
				 * @return {array} Filtered array of query arguments for ended user courses.
				 */
				$course_args = apply_filters( 'sensei_analysis_user_courses_ended', $course_args, $item );

				$user_courses_ended = Sensei_Utils::sensei_check_for_activity( $course_args );

				// Get Quiz Grades.
				$grade_args = array(
					'user_id'  => $item->ID,
					'type'     => 'sensei_lesson_status',
					'status'   => 'any',
					'meta_key' => 'grade',
				);

				/**
				 * Filter user progress query arguments for lessons: find graded lessons.
				 *
				 * @hook sensei_analysis_user_lesson_grades
				 *
				 * @param {array} $grade_args Array of query arguments for graded user lessons.
				 * @param {WP_User} $item Current user object.
				 * @return {array} Filtered array of query arguments for graded user lessons.
				 */
				$grade_args = apply_filters( 'sensei_analysis_user_lesson_grades', $grade_args, $item );

				$grade_count        = Sensei_Utils::sensei_check_for_activity( $grade_args, false );
				$grade_total        = Sensei_Grading::get_user_graded_lessons_sum( $item->ID );
				$user_average_grade = 0;

				if ( $grade_total > 0 && $grade_count > 0 ) {
					$user_average_grade = Sensei_Utils::quotient_as_absolute_rounded_number( $grade_total, $grade_count, 2 );
				}

				$user_email = $item->user_email;

				// Output the users data.
				if ( $this->csv_output ) {
					$user_name = Sensei_Learner::get_full_name( $item->ID );
				} else {
					$url                 = add_query_arg(
						array(
							'page'    => $this->page_slug,
							'user_id' => $item->ID,
						),
						admin_url( 'admin.php' )
					);
					$user_name           = '<strong><a class="row-title" href="' . esc_url( $url ) . '">' . esc_html( $item->display_name ) . '</a></strong>';
					$user_average_grade .= '%';
				}
				$last_activity_date = __( 'N/A', 'sensei-lms' );
				if ( $item->last_activity_date ) {
					$last_activity_date = $this->csv_output ? $item->last_activity_date : Sensei_Utils::format_last_activity_date( $item->last_activity_date );
				}

				/**
				 * 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_Analysis_Overview_List_Table} $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'             => $user_name,
						'email'             => $user_email,
						'date_registered'   => $this->format_date_registered( $item->user_registered ),
						'last_activity'     => $last_activity_date,
						'active_courses'    => ( $user_courses_started - $user_courses_ended ),
						'completed_courses' => $user_courses_ended,
						'average_grade'     => $user_average_grade,
					),
					$item,
					$this
				);
				break;
		}

		$escaped_column_data = array();

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

		return $escaped_column_data;
	}


	/**
	 * 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 ) {
		// Fetch learners in course.
		$course_args = array(
			'post_id' => $course_id,
			'type'    => 'sensei_course_status',
			'status'  => array( 'in-progress', 'complete' ),
		);

		$course_students_count = Sensei_Utils::sensei_check_for_activity( $course_args );

		// Get all course lessons.
		$lessons        = Sensei()->course->course_lessons( $course_id, 'publish', 'ids' );
		$course_lessons = is_array( $lessons ) ? $lessons : array( $lessons );
		$total_lessons  = count( $course_lessons );

		// Get all completed lessons.
		$lesson_args     = array(
			'post__in' => $course_lessons,
			'type'     => 'sensei_lesson_status',
			'status'   => array( 'graded', 'ungraded', 'passed', 'failed', 'complete' ),
			'count'    => true,
		);
		$completed_count = (int) Sensei_Utils::sensei_check_for_activity( $lesson_args );
		// Calculate average progress.
		$average_course_progress = __( 'N/A', 'sensei-lms' );
		if ( $course_students_count && $total_lessons ) {
			// Average course progress is calculated based on lessons completed for the course
			// divided by the total possible lessons completed.
			$average_course_progress_value = $completed_count / ( $course_students_count * $total_lessons ) * 100;
			$average_course_progress       = esc_html(
				sprintf( '%d%%', round( $average_course_progress_value ) )
			);
		}
		return $average_course_progress;
	}


	/**
	 * Format the registration date.
	 *
	 * @since 4.3.0
	 *
	 * @param string $date Registration date.
	 *
	 * @return string Formatted registration date.
	 */
	private function format_date_registered( string $date ) {
		$timezone = new DateTimeZone( 'GMT' );
		$date     = new DateTime( $date, $timezone );

		return wp_date( get_option( 'date_format' ), $date->getTimestamp(), $timezone );
	}

	/**
	 * Get the date on which the last lesson was marked complete.
	 *
	 * @since 4.2.0
	 *
	 * @param array $args Array of arguments to pass to comments query.
	 *
	 * @return string The last activity date, or N/A if none.
	 */
	private function get_last_activity_date( array $args ): string {
		$default_args  = array(
			'number' => 1,
			'type'   => 'sensei_lesson_status',
			'status' => [ 'complete', 'passed', 'graded' ],
		);
		$args          = wp_parse_args( $args, $default_args );
		$last_activity = Sensei_Utils::sensei_check_for_activity( $args, true );

		if ( ! $last_activity ) {
			return __( 'N/A', 'sensei-lms' );
		}

		return $this->csv_output ? $last_activity->comment_date_gmt : Sensei_Utils::format_last_activity_date( $last_activity->comment_date_gmt );
	}

	/**
	 * Return array of course
	 *
	 * @since  1.7.0
	 * @return array courses
	 */
	private function get_courses( $args ) {
		$course_args = array(
			'post_type'        => 'course',
			'post_status'      => array( 'publish', 'private' ),
			'posts_per_page'   => $args['number'],
			'offset'           => $args['offset'],
			'orderby'          => $args['orderby'],
			'order'            => $args['order'],
			'suppress_filters' => 0,
		);

		if ( isset( $args['search'] ) ) {
			$course_args['s'] = $args['search'];
		}

		add_filter( 'posts_clauses', [ $this, 'filter_courses_by_last_activity' ] );
		add_filter( 'posts_clauses', [ $this, 'add_days_to_completion_to_courses_queries' ] );
		if ( isset( $args['orderby'] ) && ( 'count_of_completions' === $args['orderby'] ) ) {
			add_filter( 'posts_orderby', array( $this, 'add_orderby_custom_field_to_non_user_query' ), 10, 2 );
		}

		/**
		 * Filter the query arguments for courses in the Analysis Overview list table.
		 *
		 * @hook sensei_analysis_overview_filter_courses
		 *
		 * @param {array} $course_args Array of query arguments for courses.
		 * @return {array} Filtered array of query arguments for courses.
		 */
		$course_args = apply_filters( 'sensei_analysis_overview_filter_courses', $course_args );

		$courses_query = new WP_Query( $course_args );

		remove_filter( 'posts_orderby', array( $this, 'add_orderby_custom_field_to_non_user_query' ), 10, 2 );
		remove_filter( 'posts_clauses', [ $this, 'filter_courses_by_last_activity' ] );
		remove_filter( 'posts_clauses', [ $this, 'add_days_to_completion_to_courses_queries' ] );

		$this->total_items = $courses_query->found_posts;

		return $courses_query->posts;

	}

	/**
	 * Return array of lessons.
	 *
	 * @since  1.7.0
	 *
	 * @param array $args      The query arguments.
	 * @param int   $course_id The selected course ID.
	 *
	 * @return array Lesson posts or empty array if no course is selected.
	 */
	private function get_lessons( array $args, int $course_id ): array {

		if ( ! $course_id ) {
			return [];
		}
		// Fetching the lesson ids beforehand because joining both postmeta and comment + commentmeta makes WP_Query very slow.
		$course_lessons = Sensei()->course->course_lessons( $course_id, 'any', 'ids' );
		$lessons_args   = array(
			'post_type'        => 'lesson',
			'post_status'      => array( 'publish', 'private' ),
			'posts_per_page'   => $args['number'],
			'offset'           => $args['offset'],
			'orderby'          => $args['orderby'],
			'order'            => $args['order'],
			'post__in'         => $course_lessons,
			'suppress_filters' => 0,
		);

		if ( isset( $args['search'] ) ) {
			$lessons_args['s'] = $args['search'];
		}

		add_filter( 'posts_clauses', [ $this, 'add_days_to_complete_to_lessons_query' ] );

		/**
		 * Filter the query arguments for lessons in the Analysis Overview list table.
		 *
		 * @hook sensei_analysis_overview_filter_lessons
		 *
		 * @param {array} $lessons_args Array of query arguments for lessons.
		 * @return {array} Filtered array of query arguments for lessons.
		 */
		$lessons_args = apply_filters( 'sensei_analysis_overview_filter_lessons', $lessons_args );

		// Using WP_Query as get_posts() doesn't support 'found_posts'.
		$lessons_query = new WP_Query( $lessons_args );
		remove_filter( 'posts_clauses', [ $this, 'add_days_to_complete_to_lessons_query' ] );
		$this->total_items = $lessons_query->found_posts;
		return $lessons_query->posts;
	}

	/**
	 * Return array of learners
	 *
	 * @since  1.7.0
	 * @return array learners
	 */
	private function get_learners( $args ) {

		if ( ! empty( $args['search'] ) ) {
			$args = array(
				'search' => '*' . trim( $args['search'], '*' ) . '*',
			);
		}

		// This stops the full meta data of each user being loaded
		$args['fields'] = array( 'ID', 'user_login', 'user_email', 'user_registered', 'display_name' );

		/**
		 * Filter the query arguments for users in the Analysis Overview list table.
		 *
		 * @hook sensei_analysis_overview_filter_users
		 *
		 * @param {array} $args Array of query arguments for users.
		 * @return {array} Filtered array of query arguments for users.
		 */
		$args = apply_filters( 'sensei_analysis_overview_filter_users', $args );

		add_action( 'pre_user_query', [ $this, 'add_last_activity_to_user_query' ] );
		add_action( 'pre_user_query', [ $this, 'filter_users_by_last_activity' ] );

		if ( isset( $args['orderby'] ) && 'last_activity_date' === $args['orderby'] ) {
			add_action( 'pre_user_query', [ $this, 'add_orderby_custom_field_to_query' ] );
		}

		$wp_user_search = new WP_User_Query( $args );
		remove_action( 'pre_user_query', [ $this, 'add_orderby_custom_field_to_query' ] );
		remove_action( 'pre_user_query', [ $this, 'add_last_activity_to_user_query' ] );
		remove_action( 'pre_user_query', [ $this, 'filter_users_by_last_activity' ] );

		$learners          = $wp_user_search->get_results();
		$this->total_items = $wp_user_search->get_total();

		return $learners;
	}

	/**
	 * Sets the stats boxes to render
	 *
	 * @since      1.2.0
	 * @deprecated 4.2.0
	 * @return     array $stats_to_render of stats boxes and values
	 */
	public function stats_boxes() {

		_deprecated_function( __METHOD__, '4.2.0' );

		// Get the data required
		$user_count          = count_users();
		$user_count          = apply_filters( 'sensei_analysis_total_users', $user_count['total_users'], $user_count );
		$total_courses       = Sensei()->course->course_count( array( 'publish', 'private' ) );
		$total_lessons       = Sensei()->lesson->lesson_count( array( 'publish', 'private' ) );
		$total_grade_count   = Sensei_Grading::get_graded_lessons_count();
		$total_grade_total   = Sensei_Grading::get_graded_lessons_sum();
		$total_average_grade = 0;

		if ( $total_grade_total > 0 && $total_grade_count > 0 ) {
			$total_average_grade = Sensei_Utils::quotient_as_absolute_rounded_number( $total_grade_total, $total_grade_count, 2 );
		}

		$course_args                 = array(
			'type'   => 'sensei_course_status',
			'status' => 'any',
		);
		$total_courses_started       = Sensei_Utils::sensei_check_for_activity( apply_filters( 'sensei_analysis_total_courses_started', $course_args ) );
		$course_args                 = array(
			'type'   => 'sensei_course_status',
			'status' => 'complete',
		);
		$total_courses_ended         = Sensei_Utils::sensei_check_for_activity( apply_filters( 'sensei_analysis_total_courses_ended', $course_args ) );
		$average_courses_per_learner = Sensei_Utils::quotient_as_absolute_rounded_number( $total_courses_started, $user_count, 2 );

		// Setup the boxes to render.
		$stats_to_render = array(
			__( 'Total Courses', 'sensei-lms' )           => $total_courses,
			__( 'Total Lessons', 'sensei-lms' )           => $total_lessons,
			__( 'Total Students', 'sensei-lms' )          => $user_count,
			__( 'Average Courses per Student', 'sensei-lms' ) => $average_courses_per_learner,
			__( 'Average Grade', 'sensei-lms' )           => $total_average_grade . '%',
			__( 'Total Completed Courses', 'sensei-lms' ) => $total_courses_ended,
		);
		return apply_filters( 'sensei_analysis_stats_boxes', $stats_to_render );
	}

	/**
	 * Get completion rate for a lesson.
	 *
	 * @since 4.2.0
	 *
	 * @param int $lesson_completion_count Number of students who has completed this lesson.
	 * @param int $lesson_student_count Number of students who has started this lesson.
	 *
	 * @return string The completion rate or 'N/A' if there are no students.
	 */
	private function get_completion_rate( int $lesson_completion_count, int $lesson_student_count ): string {
		if ( 0 >= $lesson_student_count ) {
			return __( 'N/A', 'sensei-lms' );
		}
		return Sensei_Utils::quotient_as_absolute_rounded_percentage( $lesson_completion_count, $lesson_student_count ) . '%';
	}

	/**
	 * Sets output when no items are found
	 * Overloads the parent method
	 *
	 * @since  1.2.0
	 */
	public function no_items() {

		if ( 'lessons' === $this->type && ! $this->get_course_filter_value() ) {
			$message = __( 'View your Lessons data by first selecting a course.', 'sensei-lms' );
		} else {
			if ( ! $this->type || 'users' === $this->type ) {
				$type = __( 'students', 'sensei-lms' );
			} else {
				$type = $this->type;
			}

			// translators: Placeholders %1$s and %3$s are opening and closing <em> tags, %2$s is the view type.
			$message = sprintf( __( '%1$sNo %2$s found%3$s', 'sensei-lms' ), '<em>', $type, '</em>' );
		}

		?>
		<div class="sensei-analysis__no-items-message">
			<?php echo wp_kses_post( $message ); ?>
		</div>
		<?php
	}

	/**
	 * Output top filter form.
	 *
	 * @since  4.2.0
	 * @access private
	 */
	public function output_top_filters() {
		?>
		<form class="sensei-analysis__top-filters">
			<?php Sensei_Utils::output_query_params_as_inputs( [ 'course_filter', 'start_date', 'end_date', 's' ] ); ?>

			<?php if ( 'lessons' === $this->type ) : ?>
				<label for="sensei-course-filter">
					<?php esc_html_e( 'Course', 'sensei-lms' ); ?>:
				</label>

				<?php $this->output_course_select_input(); ?>
			<?php endif ?>

			<?php if ( in_array( $this->type, [ 'courses', 'users' ], true ) ) : ?>
				<label for="sensei-start-date-filter">
					<?php esc_html_e( 'Last Activity', 'sensei-lms' ); ?>:
				</label>

				<input
					class="sensei-date-picker"
					id="sensei-start-date-filter"
					name="start_date"
					type="text"
					autocomplete="off"
					placeholder="<?php echo esc_attr( __( 'Start Date', 'sensei-lms' ) ); ?>"
					value="<?php echo esc_attr( $this->get_start_date_filter_value() ); ?>"
				/>

				<input
					class="sensei-date-picker"
					id="sensei-end-date-filter"
					name="end_date"
					type="text"
					autocomplete="off"
					placeholder="<?php echo esc_attr( __( 'End Date', 'sensei-lms' ) ); ?>"
					value="<?php echo esc_attr( $this->get_end_date_filter_value() ); ?>"
				/>
			<?php endif ?>

			<?php submit_button( __( 'Filter', 'sensei-lms' ), '', '', false ); ?>
		</form>
		<?php
	}

	/**
	 * Output the course filter select input.
	 *
	 * @since 4.2.0
	 */
	private function output_course_select_input() {
		$courses            = Sensei_Course::get_all_courses();
		$selected_course_id = $this->get_course_filter_value();

		?>
		<select name="course_filter" id="sensei-course-filter">
			<option>
				<?php esc_html_e( 'Select a course', 'sensei-lms' ); ?>
			</option>
			<?php foreach ( $courses as $course ) : ?>
				<option
					value="<?php echo esc_attr( $course->ID ); ?>"
					<?php echo $selected_course_id === $course->ID ? 'selected' : ''; ?>
				>
					<?php echo esc_html( get_the_title( $course ) ); ?>
				</option>
			<?php endforeach; ?>
		</select>
		<?php
	}

	/**
	 * Output for table heading
	 *
	 * @since  1.2.0
	 * @deprecated 4.2.0
	 * @return void
	 */
	public function data_table_header() {
		_deprecated_function( __METHOD__, '4.2.0' );

		$menu = array();

		$query_args     = array(
			'page' => $this->page_slug,
		);
		$learners_class = $courses_class = $lessons_class = '';
		switch ( $this->type ) {
			case 'courses':
				$courses_class = 'current';
				break;

			case 'lessons':
				$lessons_class = 'current';
				break;

			default:
				$learners_class = 'current';
				break;
		}
		$learner_args         = $lesson_args = $courses_args = $query_args;
		$learner_args['view'] = 'users';
		$lesson_args['view']  = 'lessons';
		$courses_args['view'] = 'courses';

		$menu['learners'] = '<a class="' . esc_attr( $learners_class ) . '" href="' . esc_url( add_query_arg( $learner_args, admin_url( 'admin.php' ) ) ) . '">' . esc_html__( 'Students', 'sensei-lms' ) . '</a>';
		$menu['courses']  = '<a class="' . esc_attr( $courses_class ) . '" href="' . esc_url( add_query_arg( $courses_args, admin_url( 'admin.php' ) ) ) . '">' . esc_html__( 'Courses', 'sensei-lms' ) . '</a>';
		$menu['lessons']  = '<a class="' . esc_attr( $lessons_class ) . '" href="' . esc_url( add_query_arg( $lesson_args, admin_url( 'admin.php' ) ) ) . '">' . esc_html__( 'Lessons', 'sensei-lms' ) . '</a>';

		$menu = apply_filters( 'sensei_analysis_overview_sub_menu', $menu );
		if ( ! empty( $menu ) ) {
			echo '<ul class="subsubsub">' . "\n";
			foreach ( $menu as $class => $item ) {
				$menu[ $class ] = "\t<li class='$class'>$item";
			}
			echo wp_kses_post( implode( " |</li>\n", $menu ) ) . "</li>\n";
			echo '</ul>' . "\n";
		}
	}

	/**
	 * Output for table footer
	 *
	 * @since  1.7.0
	 * @return void
	 */
	public function data_table_footer() {
		switch ( $this->type ) {
			case 'courses':
				$report = 'courses-overview';
				break;

			case 'lessons':
				$report = 'lessons-overview';
				break;

			case 'users':
			default:
				$report = 'user-overview';
				break;
		}

		$url = add_query_arg(
			array(
				'page'                   => $this->page_slug,
				'view'                   => $this->type,
				'sensei_report_download' => $report,
				'course_filter'          => $this->get_course_filter_value(),
				'start_date'             => $this->get_start_date_filter_value(),
				'end_date'               => $this->get_end_date_filter_value(),
			),
			admin_url( 'admin.php' )
		);

		echo '<a class="button button-primary" href="' . esc_url( wp_nonce_url( $url, 'sensei_csv_download', '_sdl_nonce' ) ) . '">' . esc_html__( 'Export all rows (CSV)', 'sensei-lms' ) . '</a>';
	}

	/**
	 * The text for the search button
	 *
	 * @since  1.7.0
	 * @return string $text
	 */
	public function search_button( $text = '' ) {
		switch ( $this->type ) {
			case 'courses':
				$text = __( 'Search Courses', 'sensei-lms' );
				break;

			case 'lessons':
				$text = __( 'Search Lessons', 'sensei-lms' );
				break;

			case 'users':
			default:
				$text = __( 'Search Students', 'sensei-lms' );
				break;
		}

		return $text;
	}

	/**
	 * Add the sum of days taken by each student to complete a lesson with returning lesson row.
	 *
	 * @since  4.2.0
	 * @access private
	 *
	 * @param array $clauses Associative array of the clauses for the query.
	 *
	 * @return array Modified associative array of the clauses for the query.
	 */
	public function add_days_to_complete_to_lessons_query( $clauses ) {
		global $wpdb;

		$clauses['fields'] .= ", (SELECT SUM( ABS( DATEDIFF( STR_TO_DATE( {$wpdb->commentmeta}.meta_value, '%Y-%m-%d %H:%i:%s' ), {$wpdb->comments}.comment_date )) + 1 ) as days_to_complete";
		$clauses['fields'] .= " FROM {$wpdb->comments}";
		$clauses['fields'] .= " INNER JOIN {$wpdb->commentmeta} ON {$wpdb->comments}.comment_ID = {$wpdb->commentmeta}.comment_id";
		$clauses['fields'] .= " WHERE {$wpdb->comments}.comment_post_ID = {$wpdb->posts}.ID";
		$clauses['fields'] .= " AND {$wpdb->comments}.comment_type IN ('sensei_lesson_status')";
		$clauses['fields'] .= " AND {$wpdb->comments}.comment_approved IN ( 'complete', 'graded', 'passed', 'failed', 'ungraded' )";
		$clauses['fields'] .= " AND {$wpdb->commentmeta}.meta_key = 'start') as days_to_complete";

		return $clauses;
	}

	/**
	 * Filter the courses by last activity start/end date.
	 *
	 * @since  4.2.0
	 * @access private
	 *
	 * @param array $clauses Associative array of the clauses for the query.
	 *
	 * @return array Modified associative array of the clauses for the query.
	 */
	public function filter_courses_by_last_activity( array $clauses ): array {
		global $wpdb;

		$start_date = $this->get_start_date_and_time();
		$end_date   = $this->get_end_date_and_time();

		if ( ! $start_date && ! $end_date ) {
			return $clauses;
		}
		// Fetch the lessons within the expected last activity range.
		$lessons_query = "SELECT cm.comment_post_id lesson_id, MAX(cm.comment_date_gmt) as comment_date_gmt
			FROM {$wpdb->comments} cm
			WHERE cm.comment_approved IN ('complete', 'passed', 'graded')
			AND cm.comment_type = 'sensei_lesson_status'";
		// Filter by start date.
		if ( $start_date ) {
			$lessons_query .= $wpdb->prepare(
				' AND cm.comment_date_gmt >= %s',
				$start_date
			);
		}
		$lessons_query .= ' GROUP BY cm.comment_post_id';
		// Fetch the course IDs associated with those lessons.
		$course_query = "SELECT DISTINCT(pm.meta_value) course_id
		FROM {$wpdb->postmeta} pm JOIN ({$lessons_query}) cm
		ON cm.lesson_id = pm.post_id
		AND pm.meta_key = '_lesson_course'
		GROUP BY pm.meta_value
		";
		// Filter by end date.
		if ( $end_date ) {
			$course_query .= $wpdb->prepare(
				' HAVING MAX(cm.comment_date_gmt) <= %s',
				$end_date
			);
		}
		$clauses['where'] .= " AND {$wpdb->posts}.ID IN ({$course_query})";

		return $clauses;
	}

	/**
	 * Add the `last_activity` field to the user query.
	 *
	 * @since  4.3.0
	 * @access private
	 *
	 * @param WP_User_Query $query The user query.
	 */
	public function add_last_activity_to_user_query( WP_User_Query $query ) {
		global $wpdb;

		$query->query_fields .= ", (
			SELECT MAX({$wpdb->comments}.comment_date_gmt)
			FROM {$wpdb->comments}
			WHERE {$wpdb->comments}.user_id = {$wpdb->users}.ID
			AND {$wpdb->comments}.comment_approved IN ('complete', 'passed', 'graded')
			AND {$wpdb->comments}.comment_type = 'sensei_lesson_status'
		) AS last_activity_date";
	}

	/**
	 * Order query based on the custom field.
	 *
	 * @since  4.3.0
	 * @access private
	 *
	 * @param WP_User_Query $query The user query.
	 */
	public function add_orderby_custom_field_to_query( WP_User_Query $query ) {
		$query->query_orderby = 'ORDER BY ' . $query->query_vars['orderby'] . ' ' . $query->query_vars['order'];
	}

	/**
	 * Order query based on the custom field.
	 *
	 * @since  4.3.0
	 * @access private
	 *
	 * @param array  $args Arguments Old orderby arguments.
	 * @param object $query Query.
	 */
	public function add_orderby_custom_field_to_non_user_query( $args, $query ) {
		return $query->query_vars['orderby'] . ' ' . $query->query_vars['order'];
	}

	/**
	 * Filter the users by last activity start/end date.
	 *
	 * This action should be called after `Sensei_Analysis_Overview_List_Table::add_last_activity_to_user_query`.
	 *
	 * @since  4.2.0
	 * @access private
	 *
	 * @param WP_User_Query $query The user query.
	 */
	public function filter_users_by_last_activity( WP_User_Query $query ) {
		global $wpdb;

		$start_date = $this->get_start_date_and_time();
		$end_date   = $this->get_end_date_and_time();

		if ( ! $start_date && ! $end_date ) {
			return;
		}

		$query->query_where .= ' HAVING 1 = 1';

		// Filter by start date.
		if ( $start_date ) {
			$query->query_where .= $wpdb->prepare(
				' AND last_activity_date >= %s',
				$start_date
			);
		}

		// Filter by end date.
		if ( $end_date ) {
			$query->query_where .= $wpdb->prepare(
				' AND last_activity_date <= %s',
				$end_date
			);
		}
	}

	/**
	 * Add the sum of days taken by each student to complete a course and the number of completions for each course.
	 *
	 * @since  4.2.0
	 * @access private
	 *
	 * @param array $clauses Associative array of the clauses for the query.
	 *
	 * @return array Modified associative array of the clauses for the query.
	 */
	public function add_days_to_completion_to_courses_queries( $clauses ) {
		global $wpdb;

		// Get the number of days to complete a course: `days to complete = complete date - start date + 1`.
		$clauses['fields'] .= ", SUM(  ABS( DATEDIFF( {$wpdb->comments}.comment_date, STR_TO_DATE( {$wpdb->commentmeta}.meta_value, '%Y-%m-%d %H:%i:%s' ) ) ) + 1 ) AS days_to_completion";
		// We consider the course as completed if there is a comment and corresponding meta for it.
		$clauses['fields']  .= ", COUNT({$wpdb->commentmeta}.comment_id) AS count_of_completions";
		$clauses['join']    .= " LEFT JOIN {$wpdb->comments} ON {$wpdb->comments}.comment_post_ID = {$wpdb->posts}.ID";
		$clauses['join']    .= " AND {$wpdb->comments}.comment_type IN ('sensei_course_status')";
		$clauses['join']    .= " AND {$wpdb->comments}.comment_approved IN ( 'complete' )";
		$clauses['join']    .= " AND {$wpdb->comments}.comment_post_ID = {$wpdb->posts}.ID";
		$clauses['join']    .= " LEFT JOIN {$wpdb->commentmeta} ON {$wpdb->comments}.comment_ID = {$wpdb->commentmeta}.comment_id";
		$clauses['join']    .= " AND {$wpdb->commentmeta}.meta_key = 'start'";
		$clauses['groupby'] .= " {$wpdb->posts}.ID";

		return $clauses;
	}

	/**
	 * Get the selected course ID.
	 *
	 * @return int The course ID or 0 if none is selected.
	 */
	private function get_course_filter_value(): int {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Arguments used for filtering.
		return isset( $_GET['course_filter'] ) ? (int) $_GET['course_filter'] : 0;
	}

	/**
	 * Get the start date filter value.
	 *
	 * @return string The start date.
	 */
	private function get_start_date_filter_value(): string {
		$default = gmdate( 'Y-m-d', strtotime( '-30 days' ) );

		// phpcs:ignore WordPress.Security -- The date is sanitized by DateTime.
		$start_date = $_GET['start_date'] ?? $default;

		return DateTime::createFromFormat( 'Y-m-d', $start_date ) ? $start_date : '';
	}

	/**
	 * Get the start date filter value including the time.
	 *
	 * @return string The start date including the time or empty string if none.
	 */
	private function get_start_date_and_time(): string {
		$start_date = DateTime::createFromFormat( 'Y-m-d', $this->get_start_date_filter_value() );

		if ( ! $start_date ) {
			return '';
		}

		$start_date->setTime( 0, 0, 0 );

		return $start_date->format( 'Y-m-d H:i:s' );
	}

	/**
	 * Get the end date filter value.
	 *
	 * @return string The end date or empty string if none.
	 */
	private function get_end_date_filter_value(): string {
		// phpcs:ignore WordPress.Security -- The date is sanitized by DateTime.
		$end_date = $_GET['end_date'] ?? '';

		return DateTime::createFromFormat( 'Y-m-d', $end_date ) ? $end_date : '';
	}

	/**
	 * Get the end date filter value including the time.
	 *
	 * @return string The end date including the time or empty string if none.
	 */
	private function get_end_date_and_time(): string {
		$end_date = DateTime::createFromFormat( 'Y-m-d', $this->get_end_date_filter_value() );

		if ( ! $end_date ) {
			return '';
		}

		$end_date->setTime( 23, 59, 59 );

		return $end_date->format( 'Y-m-d H:i:s' );
	}
	/**
	 * Fetch the values required for the total counts added to column headers in lesson reports.
	 *
	 * @since  4.2.0
	 * @access private
	 *
	 * @param int $course_id Course Id to filter lessons with.
	 *
	 * @return object Object containing the required totals for column header.
	 */
	private function get_totals_for_lesson_report_column_headers( int $course_id ) {
		global $wpdb;
		$lessons      = Sensei()->course->course_lessons( $course_id, array( 'publish', 'private' ), 'ids' );
		$lesson_ids   = '0';
		$lesson_count = is_countable( $lessons ) ? count( $lessons ) : 0;
		if ( 0 < $lesson_count ) {
			$lesson_ids = implode( ',', $lessons );
		};

		$default_args  = array(
			'fields' => 'ids',
		);
		$modules       = wp_get_object_terms( $lessons, 'module', $default_args );
		$modules_count = is_countable( $modules ) ? count( $modules ) : 0;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Performance improvement.
		$lesson_completion_info                      = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT COUNT(DISTINCT(lesson_students.user_id)) unique_student_count
			, COUNT(lesson_students.comment_id) lesson_start_count
			, SUM(IF(lesson_students.`comment_approved` IN ('graded','passed','complete','failed', 'ungraded' ), 1, 0)) lesson_completed_count
			, SUM(IF(lesson_students.`comment_approved` IN ('graded','passed','complete','failed', 'ungraded' ), ABS( DATEDIFF( STR_TO_DATE( lesson_start.meta_value, %s ), lesson_students.comment_date ) ) + 1, 0)) days_to_complete_sum
			FROM $wpdb->comments lesson_students
			LEFT JOIN $wpdb->commentmeta lesson_start ON lesson_start.comment_id = lesson_students.comment_id
			WHERE lesson_start.meta_key = 'start' AND lesson_students.comment_post_id IN ( $lesson_ids )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				'%Y-%m-%d %H:%i:%s'
			)
		);
		$lesson_completion_info->lesson_count        = $lesson_count;
		$lesson_completion_info->unique_module_count = $modules_count;
		return $lesson_completion_info;
	}
}

/**
 * Class WooThemes_Sensei_Analysis_Overview_List_Table
 *
 * @ignore only for backward compatibility
 * @since 1.9.0
 */
class WooThemes_Sensei_Analysis_Overview_List_Table extends Sensei_Analysis_Overview_List_Table {}