diff --git a/changelog/fix-reports-overview-courses-aggregates-hpps b/changelog/fix-reports-overview-courses-aggregates-hpps new file mode 100644 index 0000000000..f8391f23e6 --- /dev/null +++ b/changelog/fix-reports-overview-courses-aggregates-hpps @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Reports Overview courses aggregates now read HPPS progress tables diff --git a/includes/internal/services/class-comments-based-progress-aggregation-service.php b/includes/internal/services/class-comments-based-progress-aggregation-service.php index 4900e0dad6..01b97b86e2 100644 --- a/includes/internal/services/class-comments-based-progress-aggregation-service.php +++ b/includes/internal/services/class-comments-based-progress-aggregation-service.php @@ -222,6 +222,129 @@ public function count_ungraded_quizzes( array $args = array() ): int { return $count; } + /** + * Count progress records grouped by post and status. + * + * @since $$next-version$$ + * + * @param array $args Same shape as count_statuses(); 'type' and 'post__in' honored. + * @return array> Map of post_id => [ status => count ]. + */ + public function count_statuses_by_post( array $args ): array { + if ( empty( $args['type'] ) || ! in_array( $args['type'], array( 'course', 'lesson' ), true ) ) { + _doing_it_wrong( __METHOD__, 'The "type" argument must be "course" or "lesson".', '$$next-version$$' ); + return array(); + } + + $wpdb = $this->wpdb; + $comment_type = 'course' === $args['type'] ? 'sensei_course_status' : 'sensei_lesson_status'; + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from wpdb. + $query = $wpdb->prepare( "SELECT comment_post_ID, comment_approved, COUNT(*) AS total FROM {$wpdb->comments} INNER JOIN {$wpdb->posts} ON {$wpdb->posts}.ID = {$wpdb->comments}.comment_post_ID AND {$wpdb->posts}.post_status IN ( 'publish', 'private' ) WHERE comment_type = %s", $comment_type ); + $query .= $this->build_post_filter_clause( $args ); + $query .= $this->build_user_filter_clause( $args ); + $query .= $this->build_user_exclusion_clause( $args ); + $query .= ' GROUP BY comment_post_ID, comment_approved'; + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL prepared in advance. Caching handled by callers. + $results = (array) $wpdb->get_results( $query, ARRAY_A ); + Utils::log_query_error( $wpdb, 'Comments-based status counts by post' ); + + $counts = array(); + foreach ( $results as $row ) { + $counts[ (int) $row['comment_post_ID'] ][ $row['comment_approved'] ] = (int) $row['total']; + } + + return $counts; + } + + /** + * Count completed lesson progress per lesson. + * + * @since $$next-version$$ + * + * @param int[] $lesson_ids Lesson post IDs. + * @return array Map of lesson_id => completion count. + */ + public function get_lesson_completion_counts( array $lesson_ids ): array { + if ( empty( $lesson_ids ) ) { + return array(); + } + + $wpdb = $this->wpdb; + $placeholders = implode( ', ', array_fill( 0, count( $lesson_ids ), '%d' ) ); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Table names from wpdb. Placeholders created dynamically. + $query = $wpdb->prepare( + "SELECT wcom.comment_post_id AS lesson_id, COUNT(*) AS completion_count + FROM {$wpdb->comments} wcom + WHERE wcom.comment_approved IN ('graded', 'ungraded', 'passed', 'failed','complete') + AND comment_type IN ('sensei_lesson_status') + AND wcom.comment_post_ID IN ( $placeholders ) + AND wcom.comment_post_ID IN + ( + SELECT wpm.post_id FROM {$wpdb->posts} wpc + JOIN {$wpdb->postmeta} wpm ON wpm.meta_value = wpc.id + WHERE wpm.meta_key = '_lesson_course' + AND wpc.post_status IN ('publish','private') + ) + GROUP BY wcom.comment_post_id", + $lesson_ids + ); + // phpcs:enable + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL prepared in advance. Caching handled by callers. + $results = (array) $wpdb->get_results( $query, ARRAY_A ); + Utils::log_query_error( $wpdb, 'Comments-based lesson completion counts' ); + + $counts = array(); + foreach ( $results as $row ) { + $counts[ (int) $row['lesson_id'] ] = (int) $row['completion_count']; + } + + return $counts; + } + + /** + * Average days-to-completion across the given courses (AVG of per-course averages). + * + * @since $$next-version$$ + * + * @param int[] $course_ids Course post IDs. + * @return float + */ + public function get_courses_average_days_to_completion( array $course_ids ): float { + if ( empty( $course_ids ) ) { + return 0.0; + } + + $wpdb = $this->wpdb; + $placeholders = implode( ', ', array_fill( 0, count( $course_ids ), '%d' ) ); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Table names from wpdb. Placeholders created dynamically. Date format string passed as %s to avoid conflicting with prepare(). + $query = $wpdb->prepare( + "SELECT AVG( aggregated.days_to_completion ) + FROM ( + SELECT CEIL( SUM( ABS( DATEDIFF( {$wpdb->comments}.comment_date, STR_TO_DATE( {$wpdb->commentmeta}.meta_value, %s ) ) ) + 1 ) / COUNT({$wpdb->commentmeta}.comment_id) ) AS days_to_completion + FROM {$wpdb->comments} + LEFT JOIN {$wpdb->commentmeta} ON {$wpdb->comments}.comment_ID = {$wpdb->commentmeta}.comment_id + AND {$wpdb->commentmeta}.meta_key = 'start' + WHERE {$wpdb->comments}.comment_type = 'sensei_course_status' + AND {$wpdb->comments}.comment_approved = 'complete' + AND {$wpdb->comments}.comment_post_ID IN ( $placeholders ) + GROUP BY {$wpdb->comments}.comment_post_ID + ) AS aggregated", + array_merge( array( '%Y-%m-%d %H:%i:%s' ), $course_ids ) + ); + // phpcs:enable + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL prepared in advance. Caching handled by callers. + $result = $wpdb->get_var( $query ); + Utils::log_query_error( $wpdb, 'Comments-based courses average days to completion' ); + + return (float) $result; + } + /** * Build SQL clause for filtering by post ID(s). * diff --git a/includes/internal/services/class-progress-aggregation-service-interface.php b/includes/internal/services/class-progress-aggregation-service-interface.php index a906a4e2eb..9421eeea24 100644 --- a/includes/internal/services/class-progress-aggregation-service-interface.php +++ b/includes/internal/services/class-progress-aggregation-service-interface.php @@ -82,4 +82,34 @@ public function get_lesson_totals( array $lesson_ids ): array; * @return int Number of ungraded quiz submissions for live (publish or private) lessons. */ public function count_ungraded_quizzes( array $args = array() ): int; + + /** + * Count progress records grouped by post and status. + * + * @since $$next-version$$ + * + * @param array $args Same shape as count_statuses(); 'type' and 'post__in' honored. + * @return array> Map of post_id => [ status => count ]. + */ + public function count_statuses_by_post( array $args ): array; + + /** + * Count completed lesson progress per lesson. + * + * @since $$next-version$$ + * + * @param int[] $lesson_ids Lesson post IDs. + * @return array Map of lesson_id => completion count. + */ + public function get_lesson_completion_counts( array $lesson_ids ): array; + + /** + * Average days-to-completion across the given courses (AVG of per-course averages). + * + * @since $$next-version$$ + * + * @param int[] $course_ids Course post IDs. + * @return float + */ + public function get_courses_average_days_to_completion( array $course_ids ): float; } diff --git a/includes/internal/services/class-tables-based-progress-aggregation-service.php b/includes/internal/services/class-tables-based-progress-aggregation-service.php index 5c221246c7..5f4016dd88 100644 --- a/includes/internal/services/class-tables-based-progress-aggregation-service.php +++ b/includes/internal/services/class-tables-based-progress-aggregation-service.php @@ -315,6 +315,190 @@ private function count_course_statuses( array $args ): array { return $counts; } + /** + * Count progress records grouped by post and status. + * + * @since $$next-version$$ + * + * @param array $args Same shape as count_statuses(); 'type' and 'post__in' honored. + * @return array> Map of post_id => [ status => count ]. + */ + public function count_statuses_by_post( array $args ): array { + if ( empty( $args['type'] ) || ! in_array( $args['type'], array( 'course', 'lesson' ), true ) ) { + _doing_it_wrong( __METHOD__, 'The "type" argument must be "course" or "lesson".', '$$next-version$$' ); + return array(); + } + + if ( 'lesson' === $args['type'] ) { + return $this->count_lesson_statuses_with_quiz_by_post( $args ); + } + + return $this->count_course_statuses_by_post( $args ); + } + + /** + * Count completed lesson progress per lesson. + * + * @since $$next-version$$ + * + * @param int[] $lesson_ids Lesson post IDs. + * @return array Map of lesson_id => completion count. + */ + public function get_lesson_completion_counts( array $lesson_ids ): array { + if ( empty( $lesson_ids ) ) { + return array(); + } + + $wpdb = $this->wpdb; + $table = $this->get_progress_table_name(); + $placeholders = implode( ', ', array_fill( 0, count( $lesson_ids ), '%d' ) ); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Table names from wpdb prefix. Placeholders created dynamically. + $query = $wpdb->prepare( + "SELECT p.post_id AS lesson_id, COUNT(*) AS completion_count + FROM {$table} p + LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.post_id AND pm.meta_key = '_lesson_quiz' AND pm.meta_value > 0 + LEFT JOIN {$table} q ON q.post_id = pm.meta_value AND q.user_id = p.user_id AND q.type = 'quiz' + WHERE p.type = 'lesson' + AND p.post_id IN ( $placeholders ) + AND COALESCE( q.status, p.status ) IN ('graded', 'ungraded', 'passed', 'failed', 'complete') + GROUP BY p.post_id", + $lesson_ids + ); + // phpcs:enable + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL prepared in advance. Caching handled by callers. + $results = (array) $wpdb->get_results( $query, ARRAY_A ); + Utils::log_query_error( $wpdb, 'Tables-based lesson completion counts' ); + + $counts = array(); + foreach ( $results as $row ) { + $counts[ (int) $row['lesson_id'] ] = (int) $row['completion_count']; + } + + return $counts; + } + + /** + * Average days-to-completion across the given courses (AVG of per-course averages). + * + * @since $$next-version$$ + * + * @param int[] $course_ids Course post IDs. + * @return float + */ + public function get_courses_average_days_to_completion( array $course_ids ): float { + if ( empty( $course_ids ) ) { + return 0.0; + } + + $wpdb = $this->wpdb; + $table = $this->get_progress_table_name(); + $placeholders = implode( ', ', array_fill( 0, count( $course_ids ), '%d' ) ); + $utc_offset = Utils::get_utc_offset_string(); + + // Convert to site-local time before DATEDIFF so results match the comments-based + // implementation, which reads already-local comment_date / commentmeta 'start' values. + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Table name from wpdb prefix. Placeholders created dynamically. + $query = $wpdb->prepare( + "SELECT AVG( aggregated.days_to_completion ) + FROM ( + SELECT CEIL( SUM( ABS( DATEDIFF( CONVERT_TZ( p.completed_at, '+00:00', '$utc_offset' ), CONVERT_TZ( p.started_at, '+00:00', '$utc_offset' ) ) ) + 1 ) / COUNT(*) ) AS days_to_completion + FROM {$table} p + WHERE p.type = 'course' + AND p.status = 'complete' + AND p.post_id IN ( $placeholders ) + GROUP BY p.post_id + ) AS aggregated", + $course_ids + ); + // phpcs:enable + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL prepared in advance. Caching handled by callers. + $result = $wpdb->get_var( $query ); + Utils::log_query_error( $wpdb, 'Tables-based courses average days to completion' ); + + return (float) $result; + } + + /** + * Count course progress grouped by post and status. + * + * @since $$next-version$$ + * + * @param array $args Query arguments (see count_statuses). + * @return array> Map of post_id => [ status => count ]. + */ + private function count_course_statuses_by_post( array $args ): array { + $wpdb = $this->wpdb; + $table = $this->get_progress_table_name(); + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from wpdb prefix. + $query = "SELECT p.post_id, p.status, COUNT(*) AS total FROM {$table} p"; + $query .= " INNER JOIN {$wpdb->posts} post ON post.ID = p.post_id AND post.post_status IN ( 'publish', 'private' )"; + + $query .= $wpdb->prepare( ' WHERE p.type = %s', $args['type'] ); + $query .= $this->build_post_filter_clause( $args ); + $query .= $this->build_user_filter_clause( $args ); + $query .= $this->build_user_exclusion_clause( $args ); + + $query .= ' GROUP BY p.post_id, p.status'; + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL prepared in advance. Caching handled by callers. + $results = (array) $wpdb->get_results( $query, ARRAY_A ); + Utils::log_query_error( $wpdb, 'Tables-based course status counts by post' ); + + $counts = array(); + foreach ( $results as $row ) { + $counts[ (int) $row['post_id'] ][ $row['status'] ] = (int) $row['total']; + } + + return $counts; + } + + /** + * Count lesson statuses grouped by post, using quiz status when a quiz exists. + * + * See count_lesson_statuses_with_quiz() for the rationale behind using + * COALESCE(q.status, p.status). + * + * @since $$next-version$$ + * + * @param array $args Query arguments (see count_statuses). + * @return array> Map of post_id => [ status => count ]. + */ + private function count_lesson_statuses_with_quiz_by_post( array $args ): array { + $wpdb = $this->wpdb; + $table = $this->get_progress_table_name(); + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from wpdb prefix. + $query = "SELECT p.post_id, COALESCE( q.status, p.status ) AS effective_status, COUNT( * ) AS total FROM {$table} p"; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from wpdb prefix. + $query .= " INNER JOIN {$wpdb->posts} post ON post.ID = p.post_id AND post.post_status IN ( 'publish', 'private' )"; + $query .= " LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.post_id AND pm.meta_key = '_lesson_quiz' AND pm.meta_value > 0"; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from wpdb prefix. + $query .= " LEFT JOIN {$table} q ON q.post_id = pm.meta_value AND q.user_id = p.user_id AND q.type = 'quiz'"; + + $query .= $wpdb->prepare( ' WHERE p.type = %s', 'lesson' ); + + $query .= $this->build_post_filter_clause( $args ); + $query .= $this->build_user_filter_clause( $args ); + $query .= $this->build_user_exclusion_clause( $args, 'COALESCE( q.status, p.status )' ); + + $query .= ' GROUP BY p.post_id, effective_status'; + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL prepared in advance. Caching handled by callers. + $results = (array) $wpdb->get_results( $query, ARRAY_A ); + Utils::log_query_error( $wpdb, 'Tables-based lesson status counts by post' ); + + $counts = array(); + foreach ( $results as $row ) { + $counts[ (int) $row['post_id'] ][ $row['effective_status'] ] = (int) $row['total']; + } + + return $counts; + } + /** * Build SQL clause for filtering by post ID(s). * diff --git a/includes/reports/overview/services/class-sensei-reports-overview-service-courses.php b/includes/reports/overview/services/class-sensei-reports-overview-service-courses.php index 7b73685c3b..10ba0a252f 100644 --- a/includes/reports/overview/services/class-sensei-reports-overview-service-courses.php +++ b/includes/reports/overview/services/class-sensei-reports-overview-service-courses.php @@ -32,7 +32,14 @@ public function get_total_average_progress( array $course_ids ): float { return 0.0; } $lessons_count_per_courses = $this->get_lessons_in_courses( $course_ids ); - $lessons_completions = $this->get_lessons_completions(); + + $all_lesson_ids = array(); + foreach ( $lessons_count_per_courses as $course_lessons ) { + $all_lesson_ids = array_merge( $all_lesson_ids, array_map( 'intval', explode( ',', $course_lessons->lessons ) ) ); + } + $all_lesson_ids = array_unique( $all_lesson_ids ); + + $lessons_completions = $this->get_lessons_completions( $all_lesson_ids ); $student_count_per_courses = $this->get_students_count_in_courses( $course_ids ); $total_average_progress = 0; @@ -107,24 +114,10 @@ public function get_average_days_to_completion( array $course_ids ): float { if ( empty( $course_ids ) ) { return 0; } - global $wpdb; - $query = " - SELECT AVG( aggregated.days_to_completion ) - FROM ( - SELECT CEIL( SUM( ABS( DATEDIFF( {$wpdb->comments}.comment_date, STR_TO_DATE( {$wpdb->commentmeta}.meta_value, '%Y-%m-%d %H:%i:%s' ) ) ) + 1 ) / COUNT({$wpdb->commentmeta}.comment_id) ) AS days_to_completion - FROM {$wpdb->comments} - LEFT JOIN {$wpdb->commentmeta} ON {$wpdb->comments}.comment_ID = {$wpdb->commentmeta}.comment_id - AND {$wpdb->commentmeta}.meta_key = 'start' - WHERE {$wpdb->comments}.comment_type = 'sensei_course_status' - AND {$wpdb->comments}.comment_approved = 'complete' - AND {$wpdb->comments}.comment_post_ID IN ( " . implode( ',', $course_ids ) . ' )' // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - . " GROUP BY {$wpdb->comments}.comment_post_ID - ) AS aggregated - "; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching -- Performance improvement. - return (float) $wpdb->get_var( $query ); + return ( new Progress_Query_Service_Factory() ) + ->create_aggregation_service() + ->get_courses_average_days_to_completion( $course_ids ); } @@ -154,31 +147,31 @@ public function get_total_enrollments( $course_ids ): int { } /** - * Get all lessons completions. + * Get lessons completions. * * @since 4.4.1 * + * @param array $lesson_ids The list of lesson ids to get completions for. * @return array lessons completions. */ - private function get_lessons_completions(): array { + private function get_lessons_completions( array $lesson_ids ): array { + if ( empty( $lesson_ids ) ) { + return array(); + } - global $wpdb; - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe direct sql. - return $wpdb->get_results( - "SELECT wcom.comment_post_id lesson_id, COUNT(*) completion_count - FROM {$wpdb->comments} wcom - WHERE wcom.comment_approved IN ('graded', 'ungraded', 'passed', 'failed','complete') - AND comment_type IN ('sensei_lesson_status') - AND wcom.comment_post_ID IN - ( - SELECT wpm.post_id lesson_id from {$wpdb->posts} wpc - JOIN {$wpdb->postmeta} wpm on wpm.meta_value = wpc.id - WHERE wpm.meta_key = '_lesson_course' - AND wpc.post_status in ('publish','private') - ) - GROUP BY wcom.comment_post_id", - 'OBJECT_K' - ); + $counts = ( new Progress_Query_Service_Factory() ) + ->create_aggregation_service() + ->get_lesson_completion_counts( $lesson_ids ); + + $result = array(); + foreach ( $counts as $lesson_id => $completion_count ) { + $result[ $lesson_id ] = (object) array( + 'lesson_id' => $lesson_id, + 'completion_count' => $completion_count, + ); + } + + return $result; } /** @@ -212,17 +205,27 @@ private function get_lessons_in_courses( $course_ids ): array { * @return array students in courses. */ private function get_students_count_in_courses( array $course_ids ): array { + if ( empty( $course_ids ) ) { + return array(); + } - global $wpdb; - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe direct sql. - return $wpdb->get_results( - "SELECT c.comment_post_ID as course_id, count(c.comment_post_ID) as students_count - FROM {$wpdb->comments} c - WHERE c.comment_post_ID IN ( " . implode( ',', $course_ids ) . ' )' // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - . " AND c.comment_type = 'sensei_course_status' - AND c.comment_approved IN ( 'in-progress', 'complete' ) - GROUP BY c.comment_post_ID", - 'OBJECT_K' - ); + $by_post = ( new Progress_Query_Service_Factory() ) + ->create_aggregation_service() + ->count_statuses_by_post( + array( + 'type' => 'course', + 'post__in' => $course_ids, + ) + ); + + $result = array(); + foreach ( $by_post as $course_id => $statuses ) { + $result[ $course_id ] = (object) array( + 'course_id' => $course_id, + 'students_count' => ( $statuses['in-progress'] ?? 0 ) + ( $statuses['complete'] ?? 0 ), + ); + } + + return $result; } } diff --git a/tests/unit-tests/internal/services/test-class-comments-based-progress-aggregation-service.php b/tests/unit-tests/internal/services/test-class-comments-based-progress-aggregation-service.php index 225f4a6f85..2199732668 100644 --- a/tests/unit-tests/internal/services/test-class-comments-based-progress-aggregation-service.php +++ b/tests/unit-tests/internal/services/test-class-comments-based-progress-aggregation-service.php @@ -681,4 +681,155 @@ public function testCountStatusesByUser_InvalidType_ReturnsEmptyArray(): void { /* Assert. */ $this->assertSame( array(), $result ); } + + public function testCountStatusesByPost_CourseType_ReturnsCountsGroupedByPost(): void { + /* Arrange. */ + global $wpdb; + + $user1 = $this->sensei_factory->user->create(); + $user2 = $this->sensei_factory->user->create(); + $course_id1 = $this->sensei_factory->course->create(); + $course_id2 = $this->sensei_factory->course->create(); + + \Sensei_Utils::update_course_status( $user1, $course_id1, 'complete' ); + \Sensei_Utils::update_course_status( $user2, $course_id1, 'in-progress' ); + \Sensei_Utils::update_course_status( $user1, $course_id2, 'in-progress' ); + + $service = new Comments_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->count_statuses_by_post( + array( + 'type' => 'course', + 'post__in' => array( $course_id1, $course_id2 ), + ) + ); + + /* Assert. */ + $this->assertSame( 1, $result[ $course_id1 ]['complete'] ); + $this->assertSame( 1, $result[ $course_id1 ]['in-progress'] ); + $this->assertSame( 1, $result[ $course_id2 ]['in-progress'] ); + $this->assertArrayNotHasKey( 'complete', $result[ $course_id2 ] ); + } + + public function testCountStatusesByPost_InvalidType_ReturnsEmptyArray(): void { + /* Arrange. */ + global $wpdb; + + $this->setExpectedIncorrectUsage( 'Sensei\Internal\Services\Comments_Based_Progress_Aggregation_Service::count_statuses_by_post' ); + + $service = new Comments_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->count_statuses_by_post( array( 'type' => 'invalid' ) ); + + /* Assert. */ + $this->assertSame( array(), $result ); + } + + public function testGetLessonCompletionCounts_ReturnsCorrectPerLessonCounts(): void { + /* Arrange. */ + global $wpdb; + + $user1 = $this->sensei_factory->user->create(); + $user2 = $this->sensei_factory->user->create(); + $course_id = $this->sensei_factory->course->create(); + + $ungraded_lesson = $this->sensei_factory->lesson->create( + array( 'meta_input' => array( '_lesson_course' => $course_id ) ) + ); + $complete_lesson = $this->sensei_factory->lesson->create( + array( 'meta_input' => array( '_lesson_course' => $course_id ) ) + ); + $progress_lesson = $this->sensei_factory->lesson->create( + array( 'meta_input' => array( '_lesson_course' => $course_id ) ) + ); + + \Sensei_Utils::update_lesson_status( $user1, $ungraded_lesson, 'ungraded' ); + \Sensei_Utils::update_lesson_status( $user1, $complete_lesson, 'complete' ); + \Sensei_Utils::update_lesson_status( $user2, $complete_lesson, 'complete' ); + \Sensei_Utils::update_lesson_status( $user1, $progress_lesson, 'in-progress' ); + + $service = new Comments_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->get_lesson_completion_counts( array( $ungraded_lesson, $complete_lesson, $progress_lesson ) ); + + /* Assert. */ + $this->assertSame( 1, $result[ $ungraded_lesson ], 'Ungraded lesson status should be counted as a completion.' ); + $this->assertSame( 2, $result[ $complete_lesson ], 'Both completions for the complete lesson should be counted.' ); + $this->assertArrayNotHasKey( $progress_lesson, $result, 'In-progress lesson should not be counted as a completion.' ); + } + + public function testGetLessonCompletionCounts_WithEmptyLessonIds_ReturnsEmptyArray(): void { + /* Arrange. */ + global $wpdb; + + $service = new Comments_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->get_lesson_completion_counts( array() ); + + /* Assert. */ + $this->assertSame( array(), $result ); + } + + public function testGetCoursesAverageDaysToCompletion_ReturnsCorrectAverage(): void { + /* Arrange. */ + global $wpdb; + + $user1 = $this->sensei_factory->user->create(); + $user2 = $this->sensei_factory->user->create(); + $course_id1 = $this->sensei_factory->course->create(); + $course_id2 = $this->sensei_factory->course->create(); + + $comment1_id = \Sensei_Utils::update_course_status( $user1, $course_id1, 'complete' ); + wp_update_comment( + array( + 'comment_ID' => $comment1_id, + 'comment_date' => '2022-03-11 23:29:06', + ) + ); + update_comment_meta( $comment1_id, 'start', '2022-03-11 23:27:51' ); + + $comment2_id = \Sensei_Utils::update_course_status( $user2, $course_id1, 'complete' ); + wp_update_comment( + array( + 'comment_ID' => $comment2_id, + 'comment_date' => '2022-03-14 21:34:37', + ) + ); + update_comment_meta( $comment2_id, 'start', '2022-03-14 21:34:27' ); + + $comment3_id = \Sensei_Utils::update_course_status( $user1, $course_id2, 'complete' ); + wp_update_comment( + array( + 'comment_ID' => $comment3_id, + 'comment_date' => '2022-03-12 00:22:37', + ) + ); + update_comment_meta( $comment3_id, 'start', '2022-03-09 00:22:34' ); + + $service = new Comments_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->get_courses_average_days_to_completion( array( $course_id1, $course_id2 ) ); + + /* Assert. */ + // Course 1 average: (1 + 1) / 2 = 1. Course 2 average: 4 / 1 = 4. Total: (1 + 4) / 2 = 2.5. + $this->assertSame( 2.5, $result ); + } + + public function testGetCoursesAverageDaysToCompletion_WithEmptyCourseIds_ReturnsZero(): void { + /* Arrange. */ + global $wpdb; + + $service = new Comments_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->get_courses_average_days_to_completion( array() ); + + /* Assert. */ + $this->assertSame( 0.0, $result ); + } } diff --git a/tests/unit-tests/internal/services/test-class-tables-based-progress-aggregation-service.php b/tests/unit-tests/internal/services/test-class-tables-based-progress-aggregation-service.php index 4d5e0f350c..fab533c84c 100644 --- a/tests/unit-tests/internal/services/test-class-tables-based-progress-aggregation-service.php +++ b/tests/unit-tests/internal/services/test-class-tables-based-progress-aggregation-service.php @@ -1108,4 +1108,200 @@ public function testCountStatusesByUser_InvalidType_ReturnsEmptyArray(): void { /* Assert. */ $this->assertSame( array(), $result ); } + + public function testCountStatusesByPost_CourseType_ReturnsCountsGroupedByPost(): void { + /* Arrange. */ + global $wpdb; + + $user1 = $this->sensei_factory->user->create(); + $user2 = $this->sensei_factory->user->create(); + $course_id1 = $this->sensei_factory->course->create(); + $course_id2 = $this->sensei_factory->course->create(); + + $this->insert_progress( $course_id1, $user1, 'course', 'complete' ); + $this->insert_progress( $course_id1, $user2, 'course', 'in-progress' ); + $this->insert_progress( $course_id2, $user1, 'course', 'in-progress' ); + + $service = new Tables_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->count_statuses_by_post( + array( + 'type' => 'course', + 'post__in' => array( $course_id1, $course_id2 ), + ) + ); + + /* Assert. */ + $this->assertSame( 1, $result[ $course_id1 ]['complete'] ); + $this->assertSame( 1, $result[ $course_id1 ]['in-progress'] ); + $this->assertSame( 1, $result[ $course_id2 ]['in-progress'] ); + $this->assertArrayNotHasKey( 'complete', $result[ $course_id2 ] ); + } + + public function testCountStatusesByPost_InvalidType_ReturnsEmptyArray(): void { + /* Arrange. */ + global $wpdb; + + $this->setExpectedIncorrectUsage( 'Sensei\Internal\Services\Tables_Based_Progress_Aggregation_Service::count_statuses_by_post' ); + + $service = new Tables_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->count_statuses_by_post( array( 'type' => 'invalid' ) ); + + /* Assert. */ + $this->assertSame( array(), $result ); + } + + public function testGetLessonCompletionCounts_ReturnsCorrectPerLessonCounts(): void { + /* Arrange. */ + global $wpdb; + + $user1 = $this->sensei_factory->user->create(); + $user2 = $this->sensei_factory->user->create(); + $course_id = $this->sensei_factory->course->create(); + + $ungraded_lesson = $this->sensei_factory->lesson->create( + array( 'meta_input' => array( '_lesson_course' => $course_id ) ) + ); + $complete_lesson = $this->sensei_factory->lesson->create( + array( 'meta_input' => array( '_lesson_course' => $course_id ) ) + ); + $progress_lesson = $this->sensei_factory->lesson->create( + array( 'meta_input' => array( '_lesson_course' => $course_id ) ) + ); + + $this->insert_progress( $ungraded_lesson, $user1, 'lesson', 'ungraded' ); + $this->insert_progress( $complete_lesson, $user1, 'lesson', 'complete' ); + $this->insert_progress( $complete_lesson, $user2, 'lesson', 'complete' ); + $this->insert_progress( $progress_lesson, $user1, 'lesson', 'in-progress' ); + + $service = new Tables_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->get_lesson_completion_counts( array( $ungraded_lesson, $complete_lesson, $progress_lesson ) ); + + /* Assert. */ + $this->assertSame( 1, $result[ $ungraded_lesson ], 'Ungraded lesson status should be counted as a completion.' ); + $this->assertSame( 2, $result[ $complete_lesson ], 'Both completions for the complete lesson should be counted.' ); + $this->assertArrayNotHasKey( $progress_lesson, $result, 'In-progress lesson should not be counted as a completion.' ); + } + + public function testGetLessonCompletionCounts_LessonWithQuiz_UsesQuizStatus(): void { + /* Arrange. */ + global $wpdb; + + $user_id = $this->sensei_factory->user->create(); + $course_id = $this->sensei_factory->course->create(); + $lesson_id = $this->sensei_factory->lesson->create( + array( 'meta_input' => array( '_lesson_course' => $course_id ) ) + ); + $quiz_id = $this->sensei_factory->quiz->create( + array( + 'post_parent' => $lesson_id, + 'meta_input' => array( '_quiz_lesson' => $lesson_id ), + ) + ); + update_post_meta( $lesson_id, '_lesson_quiz', $quiz_id ); + + // Lesson row is only in-progress, but quiz progress is 'ungraded' — the + // effective (quiz-aware) status should be counted as a completion. + $this->insert_progress( $lesson_id, $user_id, 'lesson', 'in-progress' ); + $this->insert_progress( $quiz_id, $user_id, 'quiz', 'ungraded' ); + + $service = new Tables_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->get_lesson_completion_counts( array( $lesson_id ) ); + + /* Assert. */ + $this->assertSame( 1, $result[ $lesson_id ], 'Quiz-derived ungraded status should be counted as a completion.' ); + } + + public function testGetLessonCompletionCounts_WithEmptyLessonIds_ReturnsEmptyArray(): void { + /* Arrange. */ + global $wpdb; + + $service = new Tables_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->get_lesson_completion_counts( array() ); + + /* Assert. */ + $this->assertSame( array(), $result ); + } + + public function testGetCoursesAverageDaysToCompletion_ReturnsCorrectAverage(): void { + /* Arrange. */ + global $wpdb; + + $user1 = $this->sensei_factory->user->create(); + $user2 = $this->sensei_factory->user->create(); + $course_id1 = $this->sensei_factory->course->create(); + $course_id2 = $this->sensei_factory->course->create(); + + $this->insert_progress_with_dates( $course_id1, $user1, 'course', 'complete', '2022-03-11 23:27:51', '2022-03-11 23:29:06' ); + $this->insert_progress_with_dates( $course_id1, $user2, 'course', 'complete', '2022-03-14 21:34:27', '2022-03-14 21:34:37' ); + $this->insert_progress_with_dates( $course_id2, $user1, 'course', 'complete', '2022-03-09 00:22:34', '2022-03-12 00:22:37' ); + + $service = new Tables_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->get_courses_average_days_to_completion( array( $course_id1, $course_id2 ) ); + + /* Assert. */ + // Course 1 average: (1 + 1) / 2 = 1. Course 2 average: 4 / 1 = 4. Total: (1 + 4) / 2 = 2.5. + $this->assertSame( 2.5, $result ); + } + + public function testGetCoursesAverageDaysToCompletion_WithUtcDatesNearMidnight_ConvertsToLocalTimeBeforeDatediff(): void { + /* Arrange. */ + global $wpdb; + + // Set site timezone to UTC-5 (e.g. America/New_York EST). + $original_offset = get_option( 'gmt_offset' ); + $original_timezone = get_option( 'timezone_string' ); + update_option( 'gmt_offset', -5 ); + update_option( 'timezone_string', '' ); + + $user_id = $this->sensei_factory->user->create(); + $course_id = $this->sensei_factory->course->create(); + + // UTC dates span two days (Jan 1 05:00 -> Jan 2 04:00), so a naive + // DATEDIFF on the raw UTC columns would compute 1 day difference (+1 = 2 days). + // In local time (UTC-5) they fall on the same day (Jan 1 00:00 -> Jan 1 23:00), + // which the comments-based implementation would report as 1 day, since + // comment_date/commentmeta 'start' are already stored in site-local time. + $started_at = '2024-01-01 05:00:00'; + $completed_at = '2024-01-02 04:00:00'; + $this->insert_progress_with_dates( $course_id, $user_id, 'course', 'complete', $started_at, $completed_at ); + + $service = new Tables_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->get_courses_average_days_to_completion( array( $course_id ) ); + + /* Assert. */ + // This pins parity with the comments-based implementation, which would + // compute 1 day for the equivalent site-local timestamps. + $this->assertSame( 1.0, $result, 'UTC dates spanning midnight should be converted to local time (1 day) before DATEDIFF, matching the comments-based implementation.' ); + + /* Cleanup. */ + update_option( 'gmt_offset', $original_offset ); + update_option( 'timezone_string', $original_timezone ); + } + + public function testGetCoursesAverageDaysToCompletion_WithEmptyCourseIds_ReturnsZero(): void { + /* Arrange. */ + global $wpdb; + + $service = new Tables_Based_Progress_Aggregation_Service( $wpdb ); + + /* Act. */ + $result = $service->get_courses_average_days_to_completion( array() ); + + /* Assert. */ + $this->assertSame( 0.0, $result ); + } } diff --git a/tests/unit-tests/reports/overview/services/test-class-sensei-reports-overview-service-courses.php b/tests/unit-tests/reports/overview/services/test-class-sensei-reports-overview-service-courses.php index 91dec01f9b..63f9702ad0 100644 --- a/tests/unit-tests/reports/overview/services/test-class-sensei-reports-overview-service-courses.php +++ b/tests/unit-tests/reports/overview/services/test-class-sensei-reports-overview-service-courses.php @@ -6,6 +6,7 @@ * @covers Sensei_Reports_Overview_Service_Courses */ class Sensei_Reports_Overview_Service_Courses_Test extends WP_UnitTestCase { + use Sensei_HPPS_Helpers; private static $initial_hook_suffix; @@ -60,14 +61,16 @@ public function testTotalAverageProgressForCoursesSingleCourse() { //Add 2 lessons to the course $lesson_1 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course_id ] ] + array( 'meta_input' => array( '_lesson_course' => $course_id ) ) ); $lesson_2 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course_id ] ] + array( 'meta_input' => array( '_lesson_course' => $course_id ) ) ); $service = new Sensei_Reports_Overview_Service_Courses(); + $this->maybe_enable_hpps_tables_repository(); + // Complete lesson 1 and lesson 2 with user_1. Sensei_Utils::sensei_start_lesson( $lesson_1, $user_id_1, true ); Sensei_Utils::sensei_start_lesson( $lesson_2, $user_id_1, true ); @@ -79,9 +82,11 @@ public function testTotalAverageProgressForCoursesSingleCourse() { /* Assert. */ $this->assertEquals( 50, - $service->get_total_average_progress( [ $course_id ] ), + $service->get_total_average_progress( array( $course_id ) ), 'Find totals of lessons completed single course.' ); + + $this->maybe_reset_hpps_repository(); } /** @@ -102,19 +107,22 @@ public function testTotalAverageProgressForCoursesMultipleCourses() { //Add 2 lessons to the course 1 $lesson_1 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course_id_1 ] ] + array( 'meta_input' => array( '_lesson_course' => $course_id_1 ) ) ); $lesson_2 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course_id_1 ] ] + array( 'meta_input' => array( '_lesson_course' => $course_id_1 ) ) ); //Add 2 lessons to the course 2 $lesson_3 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course_id_2 ] ] + array( 'meta_input' => array( '_lesson_course' => $course_id_2 ) ) ); $lesson_4 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course_id_2 ] ] + array( 'meta_input' => array( '_lesson_course' => $course_id_2 ) ) ); $service = new Sensei_Reports_Overview_Service_Courses(); + + $this->maybe_enable_hpps_tables_repository(); + // Complete lesson 1 and lesson 2 with user_1. Sensei_Utils::sensei_start_lesson( $lesson_1, $user_id_1, true ); Sensei_Utils::sensei_start_lesson( $lesson_2, $user_id_1, true ); @@ -133,9 +141,11 @@ public function testTotalAverageProgressForCoursesMultipleCourses() { /* Assert. */ $this->assertEquals( 38, - $service->get_total_average_progress( [ $course_id_1, $course_id_2 ] ), + $service->get_total_average_progress( array( $course_id_1, $course_id_2 ) ), 'Find totals of lessons completed multiple courses.' ); + + $this->maybe_reset_hpps_repository(); } @@ -154,13 +164,15 @@ public function testTotalAverageProgressForCoursesProgressZero() { //Add 2 lessons to the course 1 $lesson_1 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course_id_1 ] ] + array( 'meta_input' => array( '_lesson_course' => $course_id_1 ) ) ); $lesson_2 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course_id_1 ] ] + array( 'meta_input' => array( '_lesson_course' => $course_id_1 ) ) ); $service = new Sensei_Reports_Overview_Service_Courses(); + $this->maybe_enable_hpps_tables_repository(); + // Enroll student 2 to the course and lessons, but don't complete the lessons. Sensei_Utils::sensei_start_lesson( $lesson_1, $user_id_2 ); Sensei_Utils::sensei_start_lesson( $lesson_2, $user_id_2 ); @@ -168,9 +180,11 @@ public function testTotalAverageProgressForCoursesProgressZero() { /* Assert. */ $this->assertEquals( 0, - $service->get_total_average_progress( [ $course_id_1 ] ), + $service->get_total_average_progress( array( $course_id_1 ) ), 'Find average progress total is 0 when no lesson is completed' ); + + $this->maybe_reset_hpps_repository(); } @@ -188,7 +202,7 @@ public function testTotalAverageProgressForCoursesProgressZeroNoStudents() { /* Assert. */ $this->assertEquals( 0, - $service->get_total_average_progress( [] ), + $service->get_total_average_progress( array() ), 'Average of progress total is zero when no lessons or students.' ); } @@ -212,18 +226,20 @@ public function testTotalAverageProgressCompletedForMultipleStudents() { //Add 2 lessons to the first course $lesson_1 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course_id_1 ] ] + array( 'meta_input' => array( '_lesson_course' => $course_id_1 ) ) ); $lesson_2 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course_id_1 ] ] + array( 'meta_input' => array( '_lesson_course' => $course_id_1 ) ) ); // Add 1 lesson to the second course $lesson_3 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course_id_2 ] ] + array( 'meta_input' => array( '_lesson_course' => $course_id_2 ) ) ); $service = new Sensei_Reports_Overview_Service_Courses(); + $this->maybe_enable_hpps_tables_repository(); + // Complete lesson 1 and lesson 2 with user_1. Sensei_Utils::sensei_start_lesson( $lesson_1, $user_id_1, true ); Sensei_Utils::sensei_start_lesson( $lesson_2, $user_id_1, true ); @@ -238,11 +254,42 @@ public function testTotalAverageProgressCompletedForMultipleStudents() { /* Assert. */ $this->assertEquals( 100, - $service->get_total_average_progress( [ $course_id_1, $course_id_2 ] ), + $service->get_total_average_progress( array( $course_id_1, $course_id_2 ) ), 'Find totals of lessons completed single course.' ); + + $this->maybe_reset_hpps_repository(); } + /** + * Seed a completed course status with fixed start/completion dates, using + * whichever storage backend is active for the current test run so that the + * seeded fixture is readable by the aggregation service under test. + * + * @param int $course_id Course ID. + * @param int $user_id User ID. + * @param string $started_at Start date/time string (site-local). + * @param string $completed_at Completion date/time string (site-local). + */ + private function seed_course_completion_with_dates( int $course_id, int $user_id, string $started_at, string $completed_at ): void { + if ( self::is_hpps_tables_mode() ) { + $timezone = wp_timezone(); + $course_progress = Sensei()->course_progress_repository->create( $course_id, $user_id ); + $course_progress->start( new DateTimeImmutable( $started_at, $timezone ) ); + $course_progress->complete( new DateTimeImmutable( $completed_at, $timezone ) ); + Sensei()->course_progress_repository->save( $course_progress ); + return; + } + + $comment_id = Sensei_Utils::update_course_status( $user_id, $course_id, 'complete' ); + wp_update_comment( + array( + 'comment_ID' => $comment_id, + 'comment_date' => $completed_at, + ) + ); + update_comment_meta( $comment_id, 'start', $started_at ); + } public function testGetAverageDaysToCompletionWhenOneCourseExistsReturnsMatchingValue() { $user1_id = $this->factory->user->create(); @@ -250,35 +297,14 @@ public function testGetAverageDaysToCompletionWhenOneCourseExistsReturnsMatching $user3_id = $this->factory->user->create(); $course_id = $this->factory->course->create(); - $comment1_id = Sensei_Utils::update_course_status( $user1_id, $course_id, 'complete' ); - wp_update_comment( - [ - 'comment_ID' => $comment1_id, - 'comment_date' => '2022-01-07 00:00:00', - ] - ); - update_comment_meta( $comment1_id, 'start', '2022-01-01 00:00:01' ); + $this->maybe_enable_hpps_tables_repository(); - $comment2_id = Sensei_Utils::update_course_status( $user2_id, $course_id, 'complete' ); - wp_update_comment( - [ - 'comment_ID' => $comment2_id, - 'comment_date' => '2022-01-10 00:00:00', - ] - ); - update_comment_meta( $comment2_id, 'start', '2022-01-01 00:00:01' ); - - $comment3_id = Sensei_Utils::update_course_status( $user3_id, $course_id, 'complete' ); - wp_update_comment( - [ - 'comment_ID' => $comment3_id, - 'comment_date' => '2022-01-30 00:00:00', - ] - ); - update_comment_meta( $comment3_id, 'start', '2022-01-01 00:00:01' ); + $this->seed_course_completion_with_dates( $course_id, $user1_id, '2022-01-01 00:00:01', '2022-01-07 00:00:00' ); + $this->seed_course_completion_with_dates( $course_id, $user2_id, '2022-01-01 00:00:01', '2022-01-10 00:00:00' ); + $this->seed_course_completion_with_dates( $course_id, $user3_id, '2022-01-01 00:00:01', '2022-01-30 00:00:00' ); $instance = new Sensei_Reports_Overview_Service_Courses(); - $actual = $instance->get_average_days_to_completion( [ $course_id ] ); + $actual = $instance->get_average_days_to_completion( array( $course_id ) ); // 2022-01-07 00:00:00 - 2022-01-01 00:00:01 + 1 = 7 days. // 2022-01-10 00:00:00 - 2022-01-01 00:00:01 + 1 = 10 days. @@ -286,6 +312,8 @@ public function testGetAverageDaysToCompletionWhenOneCourseExistsReturnsMatching // As these completions are for the single course: // ceil(7 + 10 + 30/ 3) = 16 days. self::assertSame( 16.0, $actual ); + + $this->maybe_reset_hpps_repository(); } public function testGetAverageDaysToCompletionWhenMoreThanOneCourseExistReturnsMatchingValue() { @@ -294,40 +322,21 @@ public function testGetAverageDaysToCompletionWhenMoreThanOneCourseExistReturnsM $course1_id = $this->factory->course->create(); $course2_id = $this->factory->course->create(); - $comment1_id = Sensei_Utils::update_course_status( $user1_id, $course1_id, 'complete' ); - wp_update_comment( - [ - 'comment_ID' => $comment1_id, - 'comment_date' => '2022-03-11 23:29:06', - ] - ); - update_comment_meta( $comment1_id, 'start', '2022-03-11 23:27:51' ); + $this->maybe_enable_hpps_tables_repository(); - $comment2_id = Sensei_Utils::update_course_status( $user2_id, $course1_id, 'complete' ); - wp_update_comment( - [ - 'comment_ID' => $comment2_id, - 'comment_date' => '2022-03-14 21:34:37', - ] - ); - update_comment_meta( $comment2_id, 'start', '2022-03-14 21:34:27' ); - - $comment3_id = Sensei_Utils::update_course_status( $user1_id, $course2_id, 'complete' ); - wp_update_comment( - [ - 'comment_ID' => $comment3_id, - 'comment_date' => '2022-03-12 00:22:37', - ] - ); - update_comment_meta( $comment3_id, 'start', '2022-03-09 00:22:34' ); + $this->seed_course_completion_with_dates( $course1_id, $user1_id, '2022-03-11 23:27:51', '2022-03-11 23:29:06' ); + $this->seed_course_completion_with_dates( $course1_id, $user2_id, '2022-03-14 21:34:27', '2022-03-14 21:34:37' ); + $this->seed_course_completion_with_dates( $course2_id, $user1_id, '2022-03-09 00:22:34', '2022-03-12 00:22:37' ); $instance = new Sensei_Reports_Overview_Service_Courses(); - $actual = $instance->get_average_days_to_completion( [ $course1_id, $course2_id ] ); + $actual = $instance->get_average_days_to_completion( array( $course1_id, $course2_id ) ); // Average for the first course: (1 + 1) / 2 = 1. // Average for the second course: 4 / 1 = 4. // Total: (1 + 4) / 2 = 2.5. self::assertSame( 2.5, $actual ); + + $this->maybe_reset_hpps_repository(); } public function testGetTotalTotalEnrollments_WhenThereWereNoEnrolledStudents_ReturnsZero() { @@ -336,7 +345,7 @@ public function testGetTotalTotalEnrollments_WhenThereWereNoEnrolledStudents_Ret $instance = new Sensei_Reports_Overview_Service_Courses(); /* Act. */ - $actual = $instance->get_total_enrollments( [] ); + $actual = $instance->get_total_enrollments( array() ); /* Assert. */ self::assertSame( 0, $actual ); @@ -350,12 +359,14 @@ public function testGetTotalTotalEnrollments_WhenThereWereSameStudentsInDifferen // Add 2 lessons to the course. $lesson_course_1 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course1_id ] ] + array( 'meta_input' => array( '_lesson_course' => $course1_id ) ) ); $lesson_course_2 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course2_id ] ] + array( 'meta_input' => array( '_lesson_course' => $course2_id ) ) ); + $this->maybe_enable_hpps_tables_repository(); + // Enroll student 2 to the course and lessons, but don't complete the lessons. Sensei_Utils::sensei_start_lesson( $lesson_course_1, $user1_id ); Sensei_Utils::sensei_start_lesson( $lesson_course_2, $user1_id ); @@ -363,10 +374,12 @@ public function testGetTotalTotalEnrollments_WhenThereWereSameStudentsInDifferen $instance = new Sensei_Reports_Overview_Service_Courses(); /* Act. */ - $actual = $instance->get_total_enrollments( [ $course1_id, $course2_id ] ); + $actual = $instance->get_total_enrollments( array( $course1_id, $course2_id ) ); /* Assert. */ self::assertSame( 2, $actual ); + + $this->maybe_reset_hpps_repository(); } @@ -381,12 +394,14 @@ public function testGetTotalTotalEnrollments_WhenThereWereStudentsInDifferentCou // Add 2 lessons to the course. $lesson_course_1 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course1_id ] ] + array( 'meta_input' => array( '_lesson_course' => $course1_id ) ) ); $lesson_course_2 = $this->factory->lesson->create( - [ 'meta_input' => [ '_lesson_course' => $course2_id ] ] + array( 'meta_input' => array( '_lesson_course' => $course2_id ) ) ); + $this->maybe_enable_hpps_tables_repository(); + // Enroll student 2 to the course and lessons, but don't complete the lessons. Sensei_Utils::sensei_start_lesson( $lesson_course_1, $user1_id ); Sensei_Utils::sensei_start_lesson( $lesson_course_2, $user2_id ); @@ -394,10 +409,12 @@ public function testGetTotalTotalEnrollments_WhenThereWereStudentsInDifferentCou $instance = new Sensei_Reports_Overview_Service_Courses(); /* Act. */ - $actual = $instance->get_total_enrollments( [ $course1_id, $course2_id ] ); + $actual = $instance->get_total_enrollments( array( $course1_id, $course2_id ) ); /* Assert. */ self::assertSame( 2, $actual ); + + $this->maybe_reset_hpps_repository(); } /** @@ -411,7 +428,7 @@ public function testGetCoursesAverageGrade_WhenNoGradedQuizzes_ReturnsZero() { $instance = new Sensei_Reports_Overview_Service_Courses(); /* Act. */ - $actual = $instance->get_courses_average_grade( [ $course_id ] ); + $actual = $instance->get_courses_average_grade( array( $course_id ) ); /* Assert. */ self::assertSame( 0.0, $actual, 'Average grade should be zero when there are no graded quizzes.' ); @@ -419,7 +436,7 @@ public function testGetCoursesAverageGrade_WhenNoGradedQuizzes_ReturnsZero() { public function testGetAverageDaysToCompletionTotalWithoutCompletionsReturnsZero() { $instance = new Sensei_Reports_Overview_Service_Courses(); - $actual = $instance->get_average_days_to_completion( [] ); + $actual = $instance->get_average_days_to_completion( array() ); self::assertSame( 0.0, $actual ); }