Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f712f42
Query Optimization
Anindra123 Jun 12, 2026
4a06b47
Merge branch '4.0.0-dev' of https://github.com/themeum/tutor into biv…
Anindra123 Jun 12, 2026
25bd13e
Merge branch '4.0.0-dev' of https://github.com/themeum/tutor into biv…
Anindra123 Jun 15, 2026
867794a
remove student count duplicate queries
Anindra123 Jun 15, 2026
62901e8
duplicate queries for instructor home page
Anindra123 Jun 16, 2026
5c251ca
Merge branch '4.0.0-dev' of https://github.com/themeum/tutor into biv…
Anindra123 Jun 16, 2026
fef2d00
updated get_course_price is getting called two times
Anindra123 Jun 17, 2026
506c81b
student dashboard area duplicate query fixes
Anindra123 Jun 17, 2026
6afc3ed
Merge branch '4.0.0-dev' of https://github.com/themeum/tutor into biv…
Anindra123 Jun 18, 2026
0678d40
Instructor quiz attempt list, analytics and student order invoice dup…
Anindra123 Jun 18, 2026
b2259f3
Fix: tax rate duplicate query on checkout and instructor list admin p…
Anindra123 Jun 19, 2026
5ff43b7
Merge branch '4.0.0-dev' of https://github.com/themeum/tutor into biv…
Anindra123 Jun 22, 2026
757cbbb
Merge branch '4.0.0-dev' into bivas-v4
Anindra123 Jun 22, 2026
96d7bd2
Fix learning area sidebar duplicate query
Anindra123 Jun 22, 2026
350d36a
fix(types): improve config type definitions and assertions
b-l-i-n-d Jun 22, 2026
2ec33df
Merge branch '4.0.0-dev' of https://github.com/themeum/tutor into biv…
Anindra123 Jun 23, 2026
35c9d4c
Fix: My course can be accessed from instructor student view
Anindra123 Jun 24, 2026
cc42349
Merge branch '4.0.0-dev' of https://github.com/themeum/tutor into biv…
Anindra123 Jun 25, 2026
798b8dd
Merge branch '4.0.0-dev' of https://github.com/themeum/tutor into biv…
Anindra123 Jun 25, 2026
89b0062
Merge branch '4.0.0-dev' of https://github.com/themeum/tutor into biv…
Anindra123 Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions classes/Instructors_List.php
Original file line number Diff line number Diff line change
Expand Up @@ -384,16 +384,8 @@ public static function get_instructors( array $status, $offset, $per_page, $sear

$query = "SELECT
DISTINCT user.*,
ins_status.meta_value AS status,
(
SELECT
COUNT(*)
FROM {$wpdb->posts}
WHERE post_author = user.ID
AND post_type = 'courses'
) total_courses
ins_status.meta_value AS status
FROM {$wpdb->users} AS user

INNER JOIN {$wpdb->usermeta} AS ins_status
ON ( user.ID = ins_status.user_id )
AND ins_status.meta_key = '_tutor_instructor_status'
Expand Down
8 changes: 4 additions & 4 deletions classes/Quiz_Attempts_List.php
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,10 @@ public function get_quiz_attempts_nav_data( $quiz_attempts_count = 0, $url = '',
$failed_attempts = count( $quiz_model->get_formatted_quiz_attempt_list_by_quiz_id( $results, QuizModel::RESULT_FAIL ) );
$pending_attempts = count( $quiz_model->get_formatted_quiz_attempt_list_by_quiz_id( $results, QuizModel::RESULT_PENDING ) );
} else {
$all_attempts = QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, '', true, true );
$pending_attempts = QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, QuizModel::RESULT_PENDING, true, true );
$passed_attempts = QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, QuizModel::RESULT_PASS, true, true );
$failed_attempts = QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, QuizModel::RESULT_FAIL, true, true );
$all_attempts = ( '' === $result_filter ) ? (int) $quiz_attempts_count : (int) QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, '', true, true );
$pending_attempts = ( QuizModel::RESULT_PENDING === $result_filter ) ? (int) $quiz_attempts_count : (int) QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, QuizModel::RESULT_PENDING, true, true );
$passed_attempts = ( QuizModel::RESULT_PASS === $result_filter ) ? (int) $quiz_attempts_count : (int) QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, QuizModel::RESULT_PASS, true, true );
$failed_attempts = ( QuizModel::RESULT_FAIL === $result_filter ) ? (int) $quiz_attempts_count : (int) QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, QuizModel::RESULT_FAIL, true, true );
}

$filter_url = remove_query_arg( 'current_page', $url );
Expand Down
7 changes: 4 additions & 3 deletions classes/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -560,16 +560,17 @@ public function load_learning_template( string $template ): string {
*
* @since 4.0.0
*
* @param string $base_url Nav items base url.
* @param string $base_url Nav items base url.
* @param \stdClass $is_completed_course Whether course is completed.
*
* @return array
*/
public static function make_learning_area_sub_page_nav_items( $base_url = '' ): array {
public static function make_learning_area_sub_page_nav_items( $base_url = '', $is_completed_course = null ): array {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why need this dependency?
We have $tutor_is_course_completed global

if ( empty( $base_url ) ) {
$base_url = get_permalink();
}

$menu_items = apply_filters( 'tutor_learning_area_sub_page_nav_item', array(), $base_url );
$menu_items = apply_filters( 'tutor_learning_area_sub_page_nav_item', array(), $base_url, $is_completed_course );

$menu_items['course-info'] = array(
'title' => __( 'Course Info', 'tutor' ),
Expand Down
104 changes: 87 additions & 17 deletions classes/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -1470,9 +1470,13 @@ public function get_course_first_lesson( $course_id = 0, $post_type = null ) {
$course_id = $this->get_post_id( $course_id );
$user_id = get_current_user_id();

$lessons = $wpdb->get_results(
$wpdb->prepare(
"SELECT items.ID
$course_first_lesson_cache_key = 'tutor_course_first_lesson_' . $course_id;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if $post_type is provided?

$cache = TutorCache::get( $course_first_lesson_cache_key );

if ( false === $cache ) {
$lessons = $wpdb->get_results(
$wpdb->prepare(
"SELECT items.ID
FROM {$wpdb->posts} topic
INNER JOIN {$wpdb->posts} items
ON topic.ID = items.post_parent
Expand All @@ -1482,10 +1486,15 @@ public function get_course_first_lesson( $course_id = 0, $post_type = null ) {
ORDER BY topic.menu_order ASC,
items.menu_order ASC;
',
$course_id,
'publish'
)
);
$course_id,
'publish'
)
);

TutorCache::set( $course_first_lesson_cache_key, $lessons );
} else {
$lessons = $cache;
}

$first_lesson = false;

Expand Down Expand Up @@ -2108,6 +2117,12 @@ public function get_completed_courses_ids_by_user( $user_id = 0 ) {

$user_id = $this->get_user_id( $user_id );

$completed_courses_cache_key = 'tutor_completed_courses_ids_by_user_' . $user_id;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use like this pattern

$cache_key = 'tutor_completed_courses_ids_by_user_' . $user_id;
$cached    = TutorCache::get( $cache_key );
if ( false !== $cached ) {
	return $cached;
}

$cache = TutorCache::get( $completed_courses_cache_key );
if ( false !== $cache ) {
return $cache;
}

$course_ids = (array) $wpdb->get_col(
$wpdb->prepare(
"SELECT comment_post_ID AS course_id
Expand All @@ -2127,6 +2142,8 @@ public function get_completed_courses_ids_by_user( $user_id = 0 ) {
)
);

TutorCache::set( $completed_courses_cache_key, $course_ids );

return $course_ids;
}

Expand Down Expand Up @@ -2158,6 +2175,12 @@ public function get_enrolled_courses_ids_by_user( $user_id = 0, $with_bundle_enr
);
}

$enrolled_courses_cache_key = 'tutor_enrolled_courses_ids_by_user_' . $user_id . '_bundle_' . $with_bundle_enrolled_courses;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow the mentioned pattern above and use it immediate after $user_id

$cache = TutorCache::get( $enrolled_courses_cache_key );
if ( false !== $cache ) {
return $cache;
}

$course_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT DISTINCT e.post_parent, e.post_date
Expand All @@ -2174,6 +2197,8 @@ public function get_enrolled_courses_ids_by_user( $user_id = 0, $with_bundle_enr
)
);

TutorCache::set( $enrolled_courses_cache_key, (array) $course_ids );

return $course_ids;
}

Expand Down Expand Up @@ -3253,17 +3278,25 @@ function ( $instructor ) use ( $main_instructor ) {
public function get_total_students_by_instructor( $instructor_id, $args = array() ) {
global $wpdb;

$course_post_type = tutor()->course_post_type;
$enrollment_date_clause = '';
$course_post_type = tutor()->course_post_type;
$enrollment_date_clause = '';
$total_students_cache_key = __FUNCTION__ . "_{$instructor_id}";

if ( ! empty( $args['from'] ) && ! empty( $args['to'] ) ) {
$from = Input::sanitize( $args['from'] );
$to = Input::sanitize( $args['to'] );

$total_students_cache_key .= "_{$from}_{$to}";

$where['enrollment.post_date'] = array( 'BETWEEN', array( $from, $to ) );
$enrollment_date_clause = ' AND ' . QueryHelper::prepare_where_clause( $where );
}

$cached = TutorCache::get( $total_students_cache_key );
if ( false !== $cached ) {
return $cached;
}

$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT(enrollment.post_author))
Expand All @@ -3284,6 +3317,8 @@ public function get_total_students_by_instructor( $instructor_id, $args = array(
)
);

TutorCache::set( $total_students_cache_key, $count );

return (int) $count;
}

Expand Down Expand Up @@ -3484,9 +3519,17 @@ public function get_courses_by_student_instructor_id( int $student_id, int $inst
*/
public function get_completed_assignment( int $course_id, int $student_id ): int {
global $wpdb;
$course_id = sanitize_text_field( $course_id );
$student_id = sanitize_text_field( $student_id );
$count = $wpdb->get_var(

$course_id = sanitize_text_field( $course_id );
$student_id = sanitize_text_field( $student_id );
$complete_assignment_cache_key = __FUNCTION__ . "_{$course_id}_{$student_id}";

$cached = TutorCache::get( $complete_assignment_cache_key );
if ( false !== $cached ) {
return $cached;
}

$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT( DISTINCT ID ) FROM {$wpdb->posts}
INNER JOIN {$wpdb->comments} c ON c.comment_post_ID = ID AND c.user_id = %d AND c.comment_approved = %s
Expand All @@ -3503,6 +3546,8 @@ public function get_completed_assignment( int $course_id, int $student_id ): int
'publish'
)
);

TutorCache::set( $complete_assignment_cache_key, (int) $count );
return (int) $count;
}

Expand All @@ -3518,9 +3563,16 @@ public function get_completed_assignment( int $course_id, int $student_id ): int
*/
public function get_completed_quiz( int $course_id, int $student_id ): int {
global $wpdb;
$course_id = sanitize_text_field( $course_id );
$student_id = sanitize_text_field( $student_id );
$count = $wpdb->get_var(
$course_id = sanitize_text_field( $course_id );
$student_id = sanitize_text_field( $student_id );
$complete_quiz_cache_key = __FUNCTION__ . "_{$course_id}_{$student_id}";

$cached = TutorCache::get( $complete_quiz_cache_key );
if ( false !== $cached ) {
return $cached;
}

$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT quiz_id) AS total
FROM {$wpdb->prefix}tutor_quiz_attempts
Expand All @@ -3533,6 +3585,8 @@ public function get_completed_quiz( int $course_id, int $student_id ): int {
'attempt_ended'
)
);

TutorCache::set( $complete_quiz_cache_key, (int) $count );
return (int) $count;
}

Expand Down Expand Up @@ -7265,6 +7319,12 @@ public function get_student_emails_by_course_id( $course_id = 0 ) {
*/
public function get_single_comment_user_post_id( $post_id, $user_id ) {
global $wpdb;
$single_user_comment_cache_key = __FUNCTION__ . "_{$post_id}_{$user_id}";

$cached = TutorCache::get( $single_user_comment_cache_key );
if ( false !== $cached ) {
return $cached;
}
$table = $wpdb->prefix . 'comments';
$query = $wpdb->get_row(
$wpdb->prepare(
Expand All @@ -7278,6 +7338,7 @@ public function get_single_comment_user_post_id( $post_id, $user_id ) {
$user_id
)
);
TutorCache::set( $single_user_comment_cache_key, $query );
return $query ? $query : false;
}

Expand Down Expand Up @@ -8465,17 +8526,26 @@ public function has_attempted_quiz( $user_id, $quiz_id, $row = false ) {
// Sanitize data.
$user_id = sanitize_text_field( $user_id );
$quiz_id = sanitize_text_field( $quiz_id );
$cache_key = 'tutor_has_attempted_quiz_' . $user_id . '_' . $quiz_id;
$cache = TutorCache::get( $cache_key );

if ( false !== $cache ) {
return $cache ? true : false;
}

$attempted = $wpdb->get_row(
$wpdb->prepare(
"SELECT quiz_id
FROM {$wpdb->tutor_quiz_attempts}
WHERE user_id = %d
AND quiz_id = %d
",
AND quiz_id = %d",
$user_id,
$quiz_id
)
);

TutorCache::set( $cache_key, $attempted );

return $attempted ? true : false;
}

Expand Down
2 changes: 1 addition & 1 deletion ecommerce/CheckoutController.php
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ public function prepare_checkout_items( $item_ids, $order_type = OrderModel::TYP

$should_calculate_tax = Tax::should_calculate_tax();
$tax_included = Tax::is_tax_included_in_price();
$tax_rate = Tax::get_user_tax_rate();
$tax_rate = $should_calculate_tax ? Tax::get_user_tax_rate() : '';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate should not be empty string


// Keep calculated price for each item.
foreach ( $items as $item ) {
Expand Down
55 changes: 44 additions & 11 deletions models/CourseModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use TUTOR\Lesson;
use Tutor\Ecommerce\Tax;
use InvalidArgumentException;
use Tutor\Cache\TutorCache;
use Tutor\Helpers\QueryHelper;
use TUTOR\User;
use TUTOR_ASSIGNMENTS\Assignments;
Expand Down Expand Up @@ -235,16 +236,38 @@ public static function get_courses_by_args( array $args = array() ) {
* Get course count by instructor
*
* @since 1.0.0
* @since 4.0.0 param $post_type,$post_status and $search added.
*
* @param int $instructor_id instructor ID.
* @param int $instructor_id instructor ID.
* @param array $post_type the post types.
* @param array $post_status the post statuses.
* @param string $search the search term.
*
* @return null|string
* @return int
*/
public static function get_course_count_by_instructor( $instructor_id ) {
public static function get_course_count_by_instructor( $instructor_id, $post_type = array( self::POST_TYPE ), $post_status = array( self::STATUS_PUBLISH ), $search = '' ) {
global $wpdb;

$course_post_type = tutor()->course_post_type;
$cache_key = 'tutor_course_count_by_instructor_' . $instructor_id . '_' . implode( '_', $post_type ) . '_' . implode( '_', $post_status ) . '_' . $search;

$cache = TutorCache::get( $cache_key );
if ( false !== $cache ) {
return (int) $cache;
}

$course_post_type = QueryHelper::prepare_in_clause( $post_type );
$course_post_status = QueryHelper::prepare_in_clause( $post_status );
$search_sql = '';
if ( ! empty( $search ) ) {
$like = '%' . $wpdb->esc_like( $search ) . '%';
$search_sql = $wpdb->prepare(
" AND ( {$wpdb->posts}.post_title LIKE %s OR {$wpdb->posts}.post_excerpt LIKE %s ) ",
$like,
$like
);
}

// phpcs:disable
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(ID)
Expand All @@ -253,15 +276,17 @@ public static function get_course_count_by_instructor( $instructor_id ) {
ON user_id = %d
AND meta_key = %s
AND meta_value = ID
WHERE post_status = %s
AND post_type = %s;
WHERE post_status IN ({$course_post_status})
AND post_type IN ({$course_post_type})
{$search_sql}
",
$instructor_id,
'_tutor_instructor_course_id',
'publish',
$course_post_type
)
);
// phpcs:enable

TutorCache::set( $cache_key, $count );

return $count;
}
Expand Down Expand Up @@ -359,7 +384,7 @@ public static function get_courses_by_instructor(
$query = $wpdb->prepare(
"SELECT $select_col
FROM $wpdb->posts
LEFT JOIN {$wpdb->usermeta}
INNER JOIN {$wpdb->usermeta}
ON $wpdb->usermeta.user_id = %d
AND $wpdb->usermeta.meta_key = %s
AND $wpdb->usermeta.meta_value = $wpdb->posts.ID
Expand Down Expand Up @@ -1424,10 +1449,18 @@ public static function get_course_count_by_date( $start_date, $end_date, $user_i
$user_id = tutor_utils()->get_user_id( $user_id );

if ( empty( $start_date ) && empty( $end_date ) ) {
return (int) self::get_courses_by_instructor( $user_id, array( 'publish', 'private' ), 0, PHP_INT_MAX, true );
return (int) self::get_course_count_by_instructor( $user_id, array( tutor()->course_post_type ), array( 'publish', 'private' ) );
}

$courses = self::get_courses_by_instructor( $user_id, array( 'publish', 'private' ), 0, PHP_INT_MAX );
$cache_key = 'tutor_course_count_by_date_' . $user_id;

$courses = TutorCache::get( $cache_key );

if ( ! $courses ) {
// Don't need to call this twice.
$courses = self::get_courses_by_instructor( $user_id, array( 'publish', 'private' ), 0, PHP_INT_MAX );
TutorCache::set( $cache_key, $courses );
}

if ( empty( $courses ) ) {
return 0;
Expand Down
Loading
Loading