PluginProbe
Tutor LMS – eLearning and online course solution / 2.2.1
Tutor LMS – eLearning and online course solution v2.2.1
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 3.9.15 3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 All 191 releases
← All changes | models/CourseModel.php +73 -1245 4.0.22.2.1 View file →
@@ -9,17 +9,9 @@
9 9 */
10 10
11 11 namespace Tutor\Models;
12 12
13 -use Tutor\Helpers\DateTimeHelper;
14 -use TUTOR\Icon;
15 -use TUTOR\Course;
16 -use TUTOR\Lesson;
17 -use Tutor\Ecommerce\Tax;
18 -use InvalidArgumentException;
19 13 use Tutor\Helpers\QueryHelper;
20 -use TUTOR\User;
21 -use TUTOR_ASSIGNMENTS\Assignments;
22 14
23 15 /**
24 16 * CourseModel Class
25 17 *
@@ -30,25 +22,21 @@
30 22 * WordPress course type name
31 23 *
32 24 * @var string
33 25 */
34 - const POST_TYPE = 'courses';
35 - const COURSE_CATEGORY = 'course-category';
36 - const COURSE_TAG = 'course-tag';
26 + const POST_TYPE = 'courses';
37 27
38 28 const STATUS_PUBLISH = 'publish';
39 29 const STATUS_DRAFT = 'draft';
40 30 const STATUS_AUTO_DRAFT = 'auto-draft';
41 31 const STATUS_PENDING = 'pending';
42 - const STATUS_PRIVATE = 'private';
43 - const STATUS_FUTURE = 'future';
44 - const STATUS_TRASH = 'trash';
45 32
46 33 /**
47 - * Course completion modes
34 + * Course mapped with the product using this meta key
35 + *
36 + * @var string
48 37 */
49 - const MODE_FLEXIBLE = 'flexible';
50 - const MODE_STRICT = 'strict';
38 + const WC_PRODUCT_META_KEY = '_tutor_course_product_id';
51 39
52 40 /**
53 41 * Course attachment/downloadable resources meta key
54 42 *
@@ -63,75 +51,17 @@
63 51 */
64 52 const BENEFITS_META_KEY = '_tutor_course_benefits';
65 53
66 54 /**
67 - * The constant representing the status when a course is completed.
68 - *
69 - * @since 3.8.1
70 - *
71 - * @var string
72 - */
73 - const COURSE_COMPLETED = 'course_completed';
74 -
75 - /**
76 - * Get available status list.
77 - *
78 - * @since 3.0.0
79 - *
80 - * @return array
81 - */
82 - public static function get_status_list() {
83 - return array(
84 - self::STATUS_DRAFT,
85 - self::STATUS_AUTO_DRAFT,
86 - self::STATUS_PUBLISH,
87 - self::STATUS_PRIVATE,
88 - self::STATUS_FUTURE,
89 - self::STATUS_PENDING,
90 - self::STATUS_TRASH,
91 - );
92 - }
93 -
94 - /**
95 - * Check if a course is available for enrollment or purchase.
96 - *
97 - * @since 4.0.0
98 - *
99 - * @param integer $course_id the course id.
100 - *
101 - * @return bool
102 - */
103 - public static function is_course_accessible( $course_id = 0 ) {
104 - $course = get_post( $course_id );
105 - if ( ! $course || ! is_object( $course ) ) {
106 - return false;
107 - }
108 -
109 - if ( ! in_array( $course->post_type, array( tutor()->course_post_type, tutor()->bundle_post_type ), true ) ) {
110 - return false;
111 - }
112 -
113 - if ( ! in_array( $course->post_status, array( self::STATUS_PUBLISH, self::STATUS_PRIVATE ), true ) ) {
114 - return false;
115 - }
116 -
117 - return true;
118 - }
119 -
120 - /**
121 55 * Course record count
122 56 *
123 57 * @since 2.0.7
124 58 *
125 - * @since 3.6.0 $post_type param added
126 - *
127 - * @param string $status Post status.
128 - * @param string $post_type Post type.
129 - *
59 + * @param string $status course status.
130 60 * @return int
131 61 */
132 - public static function count( $status = self::STATUS_PUBLISH, $post_type = self::POST_TYPE ) {
133 - $count_obj = wp_count_posts( $post_type );
62 + public static function count( $status = self::STATUS_PUBLISH ) {
63 + $count_obj = wp_count_posts( self::POST_TYPE );
134 64 if ( 'all' === $status ) {
135 65 return array_sum( (array) $count_obj );
136 66 }
137 67
@@ -138,21 +68,8 @@
138 68 return (int) $count_obj->{$status};
139 69 }
140 70
141 71 /**
142 - * Get tutor post types
143 - *
144 - * @since 3.5.0
145 - *
146 - * @param int|\WP_POST $post the post id or object.
147 - *
148 - * @return bool
149 - */
150 - public static function get_post_types( $post ) {
151 - return apply_filters( 'tutor_check_course_post_type', self::POST_TYPE === get_post_type( $post ), get_post_type( $post ) );
152 - }
153 -
154 - /**
155 72 * Get courses
156 73 *
157 74 * @since 1.0.0
158 75 *
@@ -203,41 +120,13 @@
203 120 return $query;
204 121 }
205 122
206 123 /**
207 - * Get courses using provided args
208 - *
209 - * If user is not admin then it will return only current user's post
210 - *
211 - * @since 3.0.0
212 - *
213 - * @param array $args Args.
214 - *
215 - * @return \WP_Query
216 - */
217 - public static function get_courses_by_args( array $args = array() ) {
218 -
219 - $default_args = array(
220 - 'post_type' => tutor()->course_post_type,
221 - 'posts_per_page' => -1,
222 - 'post_status' => 'publish',
223 - );
224 -
225 - if ( ! current_user_can( 'manage_options' ) ) {
226 - $default_args['author'] = get_current_user_id();
227 - }
228 -
229 - $args = wp_parse_args( $args, apply_filters( 'tutor_get_course_list_filter_args', $default_args ) );
230 -
231 - return new \WP_Query( $args );
232 - }
233 -
234 - /**
235 124 * Get course count by instructor
236 125 *
237 126 * @since 1.0.0
238 127 *
239 - * @param int $instructor_id instructor ID.
128 + * @param $instructor_id
240 129 *
241 130 * @return null|string
242 131 */
243 132 public static function get_course_count_by_instructor( $instructor_id ) {
@@ -270,56 +159,50 @@
270 159 * Get course by quiz
271 160 *
272 161 * @since 1.0.0
273 162 *
274 - * @param int $quiz_id quiz id.
163 + * @param $quiz_id quiz id.
275 164 *
276 - * @return mixed WP_Post|null
165 + * @return array|bool|null|object|void
277 166 */
278 167 public static function get_course_by_quiz( $quiz_id ) {
279 - return get_post_parent( get_post_parent( $quiz_id ) );
168 + $quiz_id = tutils()->get_post_id( $quiz_id );
169 + $post = get_post( $quiz_id );
170 +
171 + if ( $post ) {
172 + $course = get_post( $post->post_parent );
173 + if ( $course ) {
174 + if ( $course->post_type !== tutor()->course_post_type ) {
175 + $course = get_post( $course->post_parent );
176 + }
177 + return $course;
178 + }
179 + }
180 +
181 + return false;
280 182 }
281 183
282 - /**
283 - * Get courses by a instructor
284 - *
285 - * @since 1.0.0
286 - * @since 3.5.0 param $post_types added.
287 - * @since 4.0.0 params $search and $order added.
288 - *
289 - * @param integer $instructor_id instructor id.
290 - * @param array|string $post_status post status.
291 - * @param integer $offset offset.
292 - * @param integer $limit limit.
293 - * @param boolean $count_only count or not.
294 - * @param array $post_types array of post types.
295 - * @param string $search search keyword.
296 - * @param string $order order.
297 - *
298 - * @return array|null|object
299 - */
300 - public static function get_courses_by_instructor(
301 - $instructor_id = 0,
302 - $post_status = array( 'publish' ),
303 - int $offset = 0,
304 - int $limit = PHP_INT_MAX,
305 - $count_only = false,
306 - $post_types = array(),
307 - $search = '',
308 - $order = 'DESC'
309 - ) {
184 + /**
185 + * Get courses by a instructor
186 + *
187 + * @since 1.0.0
188 + *
189 + * @param integer $instructor_id
190 + * @param array|string $post_status
191 + * @param integer $offset
192 + * @param integer $limit
193 + * @param boolean $count_only
194 + *
195 + * @return array|null|object
196 + */
197 + public static function get_courses_by_instructor( $instructor_id = 0, $post_status = array( 'publish' ), int $offset = 0, int $limit = PHP_INT_MAX, $count_only = false ) {
310 198 global $wpdb;
311 - $offset = sanitize_text_field( $offset );
312 - $limit = sanitize_text_field( $limit );
313 - $instructor_id = tutils()->get_user_id( $instructor_id );
199 + $offset = sanitize_text_field( $offset );
200 + $limit = sanitize_text_field( $limit );
201 + $instructor_id = tutils()->get_user_id( $instructor_id );
202 + $course_post_type = tutor()->course_post_type;
314 203
315 - if ( ! count( $post_types ) ) {
316 - $post_types = array( tutor()->course_post_type );
317 - }
318 -
319 - $post_types = QueryHelper::prepare_in_clause( $post_types );
320 -
321 - if ( empty( $post_status ) || 'any' == $post_status ) {
204 + if ( empty( $post_status ) || $post_status == 'any' ) {
322 205 $where_post_status = '';
323 206 } else {
324 207 ! is_array( $post_status ) ? $post_status = array( $post_status ) : 0;
325 208 $statuses = "'" . implode( "','", $post_status ) . "'";
@@ -325,47 +208,29 @@
325 208 $statuses = "'" . implode( "','", $post_status ) . "'";
326 209 $where_post_status = "AND $wpdb->posts.post_status IN({$statuses}) ";
327 210 }
328 211
329 - $search_sql = '';
330 - if ( ! empty( $search ) ) {
331 - $like = '%' . $wpdb->esc_like( $search ) . '%';
332 - $search_sql = $wpdb->prepare(
333 - " AND ( {$wpdb->posts}.post_title LIKE %s OR {$wpdb->posts}.post_excerpt LIKE %s ) ",
334 - $like,
335 - $like
336 - );
337 - }
338 -
339 - $order = strtoupper( $order ) === 'ASC' ? 'ASC' : 'DESC';
340 - $order_sql = " ORDER BY $wpdb->posts.post_date {$order} ";
341 -
342 212 $select_col = $count_only ? " COUNT(DISTINCT $wpdb->posts.ID) " : " $wpdb->posts.* ";
343 213 $limit_offset = $count_only ? '' : " LIMIT $offset, $limit ";
344 214
345 - //phpcs:disable
346 215 $query = $wpdb->prepare(
347 216 "SELECT $select_col
348 - FROM $wpdb->posts
217 + FROM $wpdb->posts
349 218 LEFT JOIN {$wpdb->usermeta}
350 - ON $wpdb->usermeta.user_id = %d
351 - AND $wpdb->usermeta.meta_key = %s
352 - AND $wpdb->usermeta.meta_value = $wpdb->posts.ID
353 - WHERE 1 = 1 {$where_post_status}
354 - AND $wpdb->posts.post_type IN ({$post_types})
219 + ON $wpdb->usermeta.user_id = %d
220 + AND $wpdb->usermeta.meta_key = %s
221 + AND $wpdb->usermeta.meta_value = $wpdb->posts.ID
222 + WHERE 1 = 1 {$where_post_status}
223 + AND $wpdb->posts.post_type = %s
355 224 AND ($wpdb->posts.post_author = %d OR $wpdb->usermeta.user_id = %d)
356 - {$search_sql}
357 - {$order_sql} {$limit_offset}",
225 + ORDER BY $wpdb->posts.post_date DESC $limit_offset",
358 226 $instructor_id,
359 227 '_tutor_instructor_course_id',
228 + $course_post_type,
360 229 $instructor_id,
361 230 $instructor_id
362 231 );
363 - //phpcs:enable
364 232
365 - $query = apply_filters( 'modify_get_courses_by_instructor_query', $query, $instructor_id, $where_post_status, $post_types, $limit_offset, $select_col );
366 -
367 - //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
368 233 return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query, OBJECT );
369 234 }
370 235
371 236 /**
@@ -405,9 +270,9 @@
405 270 public static function is_main_instructor( $course_id, $user_id = 0 ) {
406 271 $course = get_post( $course_id );
407 272 $user_id = tutor_utils()->get_user_id( $user_id );
408 273
409 - if ( ! $course || ! self::get_post_types( $course_id ) || $user_id !== (int) $course->post_author ) {
274 + if ( ! $course || self::POST_TYPE !== $course->post_type || $user_id !== (int) $course->post_author ) {
410 275 return false;
411 276 }
412 277
413 278 return true;
@@ -434,9 +299,9 @@
434 299 * Marking course completed at Comment.
435 300 */
436 301 global $wpdb;
437 302
438 - $date = date( 'Y-m-d H:i:s', tutor_time() ); //phpcs:ignore
303 + $date = date( 'Y-m-d H:i:s', tutor_time() );
439 304
440 305 // Making sure that, hash is unique.
441 306 do {
442 307 $hash = substr( md5( wp_generate_password( 32 ) . $date . $course_id . $user_id ), 0, 16 );
@@ -477,9 +342,9 @@
477 342 * @param int $post_id course id that need to delete.
478 343 * @return bool
479 344 */
480 345 public static function delete_course( $post_id ) {
481 - if ( ! self::get_post_types( $post_id ) ) {
346 + if ( get_post_type( $post_id ) !== tutor()->course_post_type ) {
482 347 return false;
483 348 }
484 349
485 350 wp_delete_post( $post_id, true );
@@ -521,10 +386,8 @@
521 386 if ( get_post_type( $post_id ) !== $course_post_type ) {
522 387 return false;
523 388 }
524 389
525 - do_action( 'tutor_before_delete_course_content', $post_id, 0 );
526 -
527 390 global $wpdb;
528 391
529 392 $lesson_post_type = tutor()->lesson_post_type;
530 393 $assignment_post_type = tutor()->assignment_post_type;
@@ -541,49 +404,19 @@
541 404 foreach ( $topic_content_ids as $content_id ) {
542 405 /**
543 406 * Delete Quiz data
544 407 */
545 - if ( get_post_type( $content_id ) === $quiz_post_type ) {
546 - // Collect file paths from all question types that store files before deleting rows (files deleted after DB for safety).
547 - $attempts_for_quiz = QueryHelper::get_all( 'tutor_quiz_attempts', array( 'quiz_id' => $content_id ), 'attempt_id', -1 );
548 - $attempt_file_paths = array();
549 - if ( ! empty( $attempts_for_quiz ) ) {
550 - $attempt_ids = array_map(
551 - function ( $row ) {
552 - return (int) $row->attempt_id;
553 - },
554 - $attempts_for_quiz
555 - );
556 - $attempt_file_paths = apply_filters( 'tutor_quiz/attempt_file_paths_for_deletion', array(), $attempt_ids );
557 - $attempt_file_paths = is_array( $attempt_file_paths ) ? array_values( array_filter( array_unique( $attempt_file_paths ) ) ) : array();
558 - }
559 -
408 + if ( get_post_type( $content_id ) === 'tutor_quiz' ) {
560 409 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $content_id ) );
561 410 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $content_id ) );
562 411
563 - QuizModel::delete_files_by_paths( $attempt_file_paths );
564 -
565 - do_action( 'tutor_before_delete_quiz_content', $content_id, null );
566 -
567 - // Collect instructor file paths before deleting question data (e.g. draw_image / pin_image masks).
568 - /**
569 - * Filter to get file paths for quiz deletion.
570 - * Pro and other add-ons register their question types via this filter.
571 - *
572 - * @param string[] $file_paths Paths collected so far.
573 - * @param int $quiz_id Quiz post ID.
574 - */
575 - $quiz_file_paths = apply_filters( 'tutor_quiz_quiz_file_paths_for_deletion', array(), $content_id );
576 -
577 412 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $content_id ) );
578 413 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
579 - $in_clause = QueryHelper::prepare_in_clause( $questions_ids );
414 + $in_question_ids = "'" . implode( "','", $questions_ids ) . "'";
580 415 //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
581 - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id IN({$in_clause}) " ) );
416 + $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id IN({$in_question_ids}) " );
582 417 }
583 418 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $content_id ) );
584 -
585 - QuizModel::delete_files_by_paths( $quiz_file_paths );
586 419 }
587 420
588 421 /**
589 422 * Delete assignment data ( Assignments, Assignment Submit, Assignment Evalutation )
@@ -708,38 +541,28 @@
708 541 * like WC Product or EDD product meta key will be used
709 542 *
710 543 * @since 2.2.0
711 544 *
712 - * @since 3.0.0
545 + * @param string $meta_key course product id meta key.
546 + * @param array $args wp_query args.
713 547 *
714 - * Meta key removed and default meta query updated
715 - *
716 - * @since 3.0.1
717 - * Course::COURSE_PRICE_META meta key exists clause added
718 - *
719 - * @param array $args wp_query args.
720 - *
721 - * @return \WP_Query
548 + * @return array
722 549 */
723 - public static function get_paid_courses( array $args = array() ) {
550 + public static function get_paid_courses( string $meta_key, array $args = array() ): array {
724 551 $current_user = wp_get_current_user();
725 -
726 552 $default_args = array(
727 - 'post_type' => tutor()->course_post_type,
553 + 'post_type' => 'courses',
554 + 'post_status' => 'publish',
555 + 'no_found_rows' => true,
728 556 'posts_per_page' => -1,
729 - 'offset' => 0,
730 - 'post_status' => 'publish',
557 + 'relation' => 'AND',
731 558 'meta_query' => array(
732 - 'relation' => 'AND',
733 559 array(
734 - 'key' => Course::COURSE_PRICE_TYPE_META,
735 - 'value' => Course::PRICE_TYPE_SUBSCRIPTION,
560 + 'key' => sanitize_text_field( $meta_key ),
561 + 'value' => 0,
736 562 'compare' => '!=',
563 + 'type' => 'NUMERIC',
737 564 ),
738 - array(
739 - 'key' => Course::COURSE_PRICE_META,
740 - 'compare' => 'EXISTS',
741 - ),
742 565 ),
743 566 );
744 567
745 568 // Check if the current user is an admin.
@@ -746,1008 +569,13 @@
746 569 if ( ! current_user_can( 'administrator' ) ) {
747 570 $args['author'] = $current_user->ID;
748 571 }
749 572
750 - $args = wp_parse_args( $args, $default_args );
751 - return new \WP_Query( $args );
752 - }
573 + $query = new \WP_Query( wp_parse_args( $args, $default_args ) );
753 574
754 - /**
755 - * Check the course is completeable or not
756 - *
757 - * @since 2.4.0
758 - *
759 - * @param int $course_id course id.
760 - * @param int $user_id user id.
761 - *
762 - * @return boolean
763 - */
764 - public static function can_complete_course( $course_id, $user_id ) {
765 - $mode = tutor_utils()->get_option( 'course_completion_process' );
766 - if ( self::MODE_FLEXIBLE === $mode ) {
767 - return true;
575 + if ( $query->have_posts() ) {
576 + return $query->posts;
768 577 }
769 578
770 - if ( self::MODE_STRICT === $mode ) {
771 - $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course( $course_id, $user_id );
772 - $lesson_count = tutor_utils()->get_lesson_count_by_course( $course_id, $user_id );
773 -
774 - if ( $completed_lesson < $lesson_count ) {
775 - return false;
776 - }
777 -
778 - $quizzes = array();
779 - $assignments = array();
780 -
781 - $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
782 - if ( tutor_utils()->count( $course_contents ) ) {
783 - foreach ( $course_contents as $content ) {
784 - if ( 'tutor_quiz' === $content->post_type ) {
785 - $quizzes[] = $content;
786 - }
787 - if ( 'tutor_assignments' === $content->post_type ) {
788 - $assignments[] = $content;
789 - }
790 - }
791 - }
792 -
793 - foreach ( $quizzes as $row ) {
794 - $result = QuizModel::get_quiz_result( $row->ID );
795 - if ( 'pass' !== $result ) {
796 - return false;
797 - }
798 - }
799 -
800 - if ( tutor()->has_pro ) {
801 - foreach ( $assignments as $row ) {
802 - $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $row->ID, $user_id );
803 - if ( 'pass' !== $result ) {
804 - return false;
805 - }
806 - }
807 - }
808 -
809 - return true;
810 - }
811 -
812 - return false;
813 - }
814 -
815 - /**
816 - * Check a course can be auto complete by an enrolled student.
817 - *
818 - * @since 2.4.0
819 - *
820 - * @param int $course_id course id.
821 - * @param int $user_id user id.
822 - *
823 - * @return boolean
824 - */
825 - public static function can_autocomplete_course( $course_id, $user_id ) {
826 - $auto_course_complete_option = (bool) tutor_utils()->get_option( 'auto_course_complete_on_all_lesson_completion' );
827 - if ( ! $auto_course_complete_option ) {
828 - return false;
829 - }
830 -
831 - $is_course_completed = tutor_utils()->is_completed_course( $course_id, $user_id );
832 - if ( $is_course_completed ) {
833 - return false;
834 - }
835 -
836 - $course_stats = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
837 - if ( $course_stats['total_count'] && $course_stats['completed_count'] === $course_stats['total_count'] ) {
838 - return self::can_complete_course( $course_id, $user_id );
839 - } else {
840 - return false;
841 - }
842 - }
843 -
844 - /**
845 - * Get review progress link when course progress 100% and
846 - * User has pending or fail quiz or assignment
847 - *
848 - * @since 2.4.0
849 - *
850 - * @param int $course_id course id.
851 - * @param int $user_id user id.
852 - *
853 - * @return string course content permalink.
854 - */
855 - public static function get_review_progress_link( $course_id, $user_id ) {
856 - $course_progress = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
857 - $completed_percent = (int) $course_progress['completed_percent'];
858 - $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
859 - $permalink = '';
860 -
861 - if ( tutor_utils()->count( $course_contents ) && 100 === $completed_percent ) {
862 - foreach ( $course_contents as $content ) {
863 - if ( 'tutor_quiz' === $content->post_type ) {
864 - $result = QuizModel::get_quiz_result( $content->ID, $user_id );
865 - if ( 'pass' !== $result ) {
866 - $permalink = get_the_permalink( $content->ID );
867 - break;
868 - }
869 - }
870 -
871 - if ( tutor()->has_pro && 'tutor_assignments' === $content->post_type ) {
872 - $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $content->ID, $user_id );
873 - if ( 'pass' !== $result ) {
874 - $permalink = get_the_permalink( $content->ID );
875 - break;
876 - }
877 - }
878 - }
879 - }
880 -
881 - // Fallback link.
882 - if ( empty( $permalink ) ) {
883 - $permalink = tutils()->get_course_first_lesson( $course_id );
884 - }
885 -
886 - return $permalink;
887 - }
888 -
889 - /**
890 - * Get course preview image placeholder
891 - *
892 - * @since 3.0.0
893 - *
894 - * @return string
895 - */
896 - public static function get_course_preview_image_placeholder() {
897 - return tutor()->url . 'assets/images/placeholder.svg';
898 - }
899 -
900 - /**
901 - * Retrieve the courses or course bundles that a given coupon code applies to.
902 - *
903 - * This function fetches published courses or course bundles from the database
904 - * based on the specified type. For each course, it retrieves the course prices
905 - * and the course thumbnail URL. If the user has Tutor Pro, it additionally
906 - * retrieves the total number of courses in a course bundle.
907 - *
908 - * @since 3.0.0
909 - *
910 - * @param string $applies_to The type of items the coupon applies to. Accepts 'specific_courses'
911 - * for individual courses or any other value for course bundles.
912 - *
913 - * @global wpdb $wpdb WordPress database abstraction object.
914 - *
915 - * @return array An array of course objects. Each course object contains:
916 - * - int $id: The ID of the course.
917 - * - string $title: The title of the course.
918 - * - string $type: The post type of the course (e.g., 'courses', 'course-bundle').
919 - * - float $price: The regular price of the course.
920 - * - float $sale_price: The sale price of the course.
921 - * - string $image: The URL of the course's thumbnail image.
922 - * - int|null $total_courses: The total number of courses in the bundle
923 - * (only if the user has Tutor Pro and the course type is 'course-bundle').
924 - */
925 - public function get_coupon_applies_to_courses( string $applies_to ) {
926 - global $wpdb;
927 -
928 - $post_type = 'specific_courses' === $applies_to ? 'courses' : 'course-bundle';
929 -
930 - $where = array(
931 - 'post_status' => 'publish',
932 - 'post_type' => $post_type,
933 - );
934 -
935 - $courses = QueryHelper::get_all( $wpdb->posts, $where, 'ID' );
936 -
937 - if ( tutor()->has_pro ) {
938 - $bundle_model = new \TutorPro\CourseBundle\Models\BundleModel();
939 - }
940 -
941 - $final_data = array();
942 -
943 - if ( ! empty( $courses ) ) {
944 - foreach ( $courses as $course ) {
945 - $data = new \stdClass();
946 -
947 - if ( tutor()->has_pro && 'course-bundle' === $course->type ) {
948 - $data->total_courses = count( $bundle_model->get_bundle_course_ids( $course->ID ) );
949 - }
950 -
951 - $author_name = get_the_author_meta( 'display_name', $course->post_author );
952 - $course_prices = tutor_utils()->get_raw_course_price( $course->ID );
953 - $data->id = (int) $course->ID;
954 - $data->title = $course->post_title;
955 - $data->price = $course_prices->regular_price;
956 - $data->sale_price = $course_prices->sale_price;
957 - $data->image = get_the_post_thumbnail_url( $course->ID );
958 - $data->author = $author_name;
959 -
960 - $final_data[] = $data;
961 - }
962 - }
963 -
964 - return ! empty( $final_data ) ? $final_data : array();
965 - }
966 -
967 - /**
968 - * Get course instructor IDs.
969 - *
970 - * @since 3.0.0
971 - *
972 - * @param int $course_id course id.
973 - *
974 - * @return array
975 - */
976 - public static function get_course_instructor_ids( $course_id ) {
977 - global $wpdb;
978 - $instructor_ids = $wpdb->get_col(
979 - $wpdb->prepare(
980 - "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key=%s AND meta_value=%s",
981 - '_tutor_instructor_course_id',
982 - $course_id
983 - )
984 - );
985 -
986 - return $instructor_ids;
987 - }
988 -
989 - /**
990 - * Check tax collection is enabled for single purchase course/bundle
991 - *
992 - * @since 3.7.0
993 - *
994 - * @param int $post_id course or bundle id.
995 - *
996 - * @return boolean
997 - */
998 - public static function is_tax_enabled_for_single_purchase( $post_id ) {
999 - if ( ! Tax::is_individual_control_enabled() ) {
1000 - return true;
1001 - }
1002 -
1003 - $data = get_post_meta( $post_id, Course::TAX_ON_SINGLE_META, true );
1004 - return ( '1' === $data || '' === $data );
1005 - }
1006 -
1007 - /**
1008 - * Count total attachments available in all courses or specific
1009 - *
1010 - * @since 3.6.0
1011 - *
1012 - * @param int|array $course_id Course id or array of ids, to get course's attachment.
1013 - *
1014 - * @return int
1015 - */
1016 - public static function count_attachment( $course_id = 0 ) {
1017 - global $wpdb;
1018 -
1019 - $total_count = 0;
1020 -
1021 - $primary_table = "$wpdb->posts p";
1022 - $joining_tables = array(
1023 - array(
1024 - 'type' => 'INNER',
1025 - 'table' => "{$wpdb->postmeta} pm",
1026 - 'on' => "p.ID = pm.post_id AND pm.meta_key = '_tutor_attachments' AND pm.meta_value != 'a:0:{}'",
1027 - ),
1028 - );
1029 -
1030 - // Prepare query.
1031 - $select = array( 'pm.meta_value' );
1032 - $where = array();
1033 - if ( $course_id ) {
1034 - $where['p.ID'] = is_array( $course_id ) ? array( 'IN', $course_id ) : $course_id;
1035 - }
1036 -
1037 - $search = array();
1038 - $limit = 0; // Get all.
1039 - $offset = 0;
1040 - $order_by = '';
1041 -
1042 - $results = QueryHelper::get_joined_data(
1043 - $primary_table,
1044 - $joining_tables,
1045 - $select,
1046 - $where,
1047 - $search,
1048 - $order_by,
1049 - $limit,
1050 - $offset
1051 - )['results'];
1052 -
1053 - if ( $results ) {
1054 - foreach ( $results as $row ) {
1055 - $attachment_ids = maybe_unserialize( $row->meta_value );
1056 - if ( ! is_array( $attachment_ids ) || empty( $attachment_ids ) ) {
1057 - continue;
1058 - }
1059 -
1060 - $attachments = get_posts(
1061 - array(
1062 - 'post_type' => 'attachment',
1063 - 'post__in' => $attachment_ids,
1064 - 'posts_per_page' => -1,
1065 - )
1066 - );
1067 -
1068 - $total_count += count( $attachments );
1069 - }
1070 - }
1071 -
1072 - return $total_count;
1073 - }
1074 -
1075 - /**
1076 - * Count total questions available in all or specific courses
1077 - *
1078 - * @since 3.7.1
1079 - *
1080 - * @param int|array $course_id Course id or array of ids, to get course's attachment.
1081 - *
1082 - * @return int
1083 - */
1084 - public static function count_questions( $course_id = 0 ) {
1085 - global $wpdb;
1086 -
1087 - $total_count = 0;
1088 - $quiz_post_type = tutor()->quiz_post_type;
1089 - $topic_post_type = tutor()->topics_post_type;
1090 - $course_post_type = tutor()->course_post_type;
1091 -
1092 - $primary_table = "{$wpdb->prefix}tutor_quiz_questions question";
1093 -
1094 - $joining_tables = array(
1095 - array(
1096 - 'type' => 'INNER',
1097 - 'table' => "{$wpdb->posts} q",
1098 - 'on' => "q.ID = question.quiz_id AND q.post_type='{$quiz_post_type}'",
1099 - ),
1100 - array(
1101 - 'type' => 'INNER',
1102 - 'table' => "{$wpdb->posts} t",
1103 - 'on' => "q.post_parent = t.ID AND t.post_type='{$topic_post_type}'",
1104 - ),
1105 - array(
1106 - 'type' => 'INNER',
1107 - 'table' => "{$wpdb->posts} c",
1108 - 'on' => "c.ID = t.post_parent AND c.post_type='{$course_post_type}'",
1109 - ),
1110 - );
1111 -
1112 - // Prepare query.
1113 - $where = array();
1114 - if ( $course_id ) {
1115 - $where['c.ID'] = is_array( $course_id ) ? array( 'IN', $course_id ) : $course_id;
1116 - }
1117 -
1118 - $search = array();
1119 -
1120 - $total_count = QueryHelper::get_joined_count(
1121 - $primary_table,
1122 - $joining_tables,
1123 - $where,
1124 - $search,
1125 - '*'
1126 - );
1127 -
1128 - return $total_count;
1129 - }
1130 -
1131 - /**
1132 - * Count course content
1133 - *
1134 - * @since 3.6.0
1135 - *
1136 - * @param string $content_type Content type.
1137 - * @param array $course_ids Course ids.
1138 - *
1139 - * @return int
1140 - */
1141 - public static function count_course_content( string $content_type, array $course_ids = array() ): int {
1142 - $total_count = 0;
1143 - switch ( $content_type ) {
1144 - case tutor()->lesson_post_type:
1145 - $total_count = tutor_utils()->get_total_lesson( $course_ids );
1146 - break;
1147 - case tutor()->quiz_post_type:
1148 - $total_count = tutor_utils()->get_total_quiz( $course_ids );
1149 - break;
1150 - case tutor()->assignment_post_type:
1151 - if ( tutor_utils()->is_addon_enabled( 'tutor-assignments' ) ) {
1152 - $total_count = ( new Assignments( false ) )->get_total_assignment( $course_ids );
1153 - }
1154 - break;
1155 - case 'attachments':
1156 - $total_count = self::count_attachment( $course_ids );
1157 - break;
1158 - case 'questions':
1159 - $total_count = self::count_questions( $course_ids );
1160 - break;
1161 - default:
1162 - break;
1163 - }
1164 -
1165 - return (int) $total_count;
1166 - }
1167 -
1168 - /**
1169 - * Get course dropdown options
1170 - *
1171 - * @since 3.7.0
1172 - *
1173 - * @return array
1174 - */
1175 - public static function get_course_dropdown_options() {
1176 - $course_options = array(
1177 - array(
1178 - 'key' => '',
1179 - 'title' => __( 'All Courses', 'tutor' ),
1180 - ),
1181 - );
1182 -
1183 - $courses = current_user_can( 'administrator' ) ? self::get_courses() : self::get_courses_by_instructor();
1184 - if ( ! empty( $courses ) ) {
1185 - foreach ( $courses as $course ) {
1186 - $course_options[] = array(
1187 - 'key' => $course->ID,
1188 - 'title' => $course->post_title,
1189 - );
1190 - }
1191 - }
1192 -
1193 - return apply_filters( 'tutor_course_dropdown_options', $course_options );
1194 - }
1195 -
1196 - /**
1197 - * Get category dropdown options
1198 - *
1199 - * @since 3.7.0
1200 - *
1201 - * @return array
1202 - */
1203 - public static function get_category_dropdown_options() {
1204 - $category_options = array(
1205 - array(
1206 - 'key' => '',
1207 - 'title' => __( 'All Categories', 'tutor' ),
1208 - ),
1209 - );
1210 -
1211 - $categories = get_terms(
1212 - array(
1213 - 'taxonomy' => self::COURSE_CATEGORY,
1214 - 'orderby' => 'term_id',
1215 - 'order' => 'DESC',
1216 - )
1217 - );
1218 - if ( ! is_wp_error( $categories ) && ! empty( $categories ) ) {
1219 - foreach ( $categories as $category ) {
1220 - $category_options[] = array(
1221 - 'key' => $category->slug,
1222 - 'title' => $category->name,
1223 - );
1224 - }
1225 - }
1226 -
1227 - return $category_options;
1228 - }
1229 -
1230 - /**
1231 - * Retrieve all course completion records for a specific course, including meta data.
1232 - *
1233 - * @since 3.8.1
1234 - *
1235 - * @param int $course_id The ID of the course.
1236 - *
1237 - * @return array Array of course completion objects with meta, empty array if none found, or on database error.
1238 - */
1239 - public function get_course_completion_data_by_course_id( int $course_id ): array {
1240 -
1241 - global $wpdb;
1242 -
1243 - $where = array(
1244 - 'comment_type' => self::COURSE_COMPLETED,
1245 - 'comment_post_ID' => $course_id,
1246 - 'comment_agent' => 'TutorLMSPlugin',
1247 - );
1248 -
1249 - $result = QueryHelper::get_all( $wpdb->comments, $where, 'comment_post_ID', -1 );
1250 -
1251 - if ( empty( $result ) ) {
1252 - return array();
1253 - }
1254 -
1255 - return array_map(
1256 - function ( $item ) {
1257 -
1258 - return array(
1259 - 'completion' => $item,
1260 - 'completion_meta' => get_comment_meta( $item->comment_ID ),
1261 - );
1262 - },
1263 - $result
1264 - );
1265 - }
1266 -
1267 - /**
1268 - * Return completed courses by user_id
1269 - *
1270 - * @since 1.0.0
1271 - *
1272 - * @param int $user_id user id.
1273 - * @param int $offset offset.
1274 - * @param int $posts_per_page posts per page.
1275 - * @param array $args Args to override the defaults.
1276 - *
1277 - * @return bool|\WP_Query
1278 - */
1279 - public static function get_completed_courses_by_user( $user_id = 0, $offset = 0, $posts_per_page = -1, $args = array() ) {
1280 - $user_id = tutor_utils()->get_user_id( $user_id );
1281 - $course_ids = tutor_utils()->get_completed_courses_ids_by_user( $user_id, false );
1282 -
1283 - if ( count( $course_ids ) ) {
1284 - $course_post_type = tutor()->course_post_type;
1285 - $course_args = array(
1286 - 'post_type' => $course_post_type,
1287 - 'post_status' => 'publish',
1288 - 'post__in' => $course_ids,
1289 - 'posts_per_page' => $posts_per_page,
1290 - 'offset' => $offset,
1291 - 'orderby' => 'post__in',
1292 - );
1293 -
1294 - $args = apply_filters( 'tutor_get_completed_courses_by_user', $args, $user_id, $course_post_type );
1295 - $course_args = wp_parse_args( $args, $course_args );
1296 -
1297 - return new \WP_Query( $course_args );
1298 - }
1299 -
1300 - return false;
1301 - }
1302 -
1303 - /**
1304 - * Get the active course by user
1305 - *
1306 - * @since 1.0.0
1307 - *
1308 - * @param int $user_id user id.
1309 - * @param int $offset offset.
1310 - * @param int $posts_per_page posts per page.
1311 - * @param array $args Args to override the defaults.
1312 - *
1313 - * @return bool|\WP_Query
1314 - */
1315 - public static function get_active_courses_by_user( $user_id = 0, $offset = 0, $posts_per_page = -1, $args = array() ) {
1316 - $user_id = tutor_utils()->get_user_id( $user_id );
1317 - $course_ids = tutor_utils()->get_completed_courses_ids_by_user( $user_id, false );
1318 - $enrolled_course_ids = tutor_utils()->get_enrolled_courses_ids_by_user( $user_id, false );
1319 - $active_courses = array_diff( $enrolled_course_ids, $course_ids );
1320 -
1321 - if ( count( $active_courses ) ) {
1322 - $course_post_type = tutor()->course_post_type;
1323 - $course_args = array(
1324 - 'post_type' => apply_filters( 'tutor_active_courses_post_types', array( $course_post_type ) ),
1325 - 'post_status' => 'publish',
1326 - 'post__in' => $active_courses,
1327 - 'posts_per_page' => $posts_per_page,
1328 - 'offset' => $offset,
1329 - 'orderby' => 'post__in',
1330 - );
1331 -
1332 - $args = apply_filters( 'tutor_get_active_courses_by_user', $args, $user_id, $course_post_type );
1333 - $course_args = wp_parse_args( $args, $course_args );
1334 -
1335 - return new \WP_Query( $course_args );
1336 - }
1337 -
1338 - return false;
1339 - }
1340 -
1341 - /**
1342 - * Get the enrolled courses by user
1343 - *
1344 - * @since 1.0.0
1345 - * @since 2.5.0 $filters param added to query enrolled courses with additional filters.
1346 - *
1347 - * @since 3.4.0 $filters replaced with $args to override the defaults.
1348 - *
1349 - * @param integer $user_id user id.
1350 - * @param mixed $post_status post status.
1351 - * @param integer $offset offset.
1352 - * @param integer $posts_per_page post per page.
1353 - * @param array $args Args to override the defaults.
1354 - *
1355 - * @return bool|\WP_Query
1356 - */
1357 - public static function get_enrolled_courses_by_user( $user_id = 0, $post_status = 'publish', $offset = 0, $posts_per_page = -1, $args = array() ) {
1358 - $user_id = tutor_utils()->get_user_id( $user_id );
1359 - $course_ids = array_unique( tutor_utils()->get_enrolled_courses_ids_by_user( $user_id, false ) );
1360 -
1361 - if ( count( $course_ids ) ) {
1362 - $course_post_type = tutor()->course_post_type;
1363 - $course_args = array(
1364 - 'post_type' => $course_post_type,
1365 - 'post_status' => $post_status,
1366 - 'post__in' => $course_ids,
1367 - 'offset' => $offset,
1368 - 'posts_per_page' => $posts_per_page,
1369 - 'orderby' => 'post__in',
1370 - );
1371 -
1372 - $args = apply_filters( 'tutor_get_enrolled_courses_by_user', $args, $user_id, $course_post_type );
1373 - $course_args = wp_parse_args( $args, $course_args );
1374 -
1375 - $result = new \WP_Query( $course_args );
1376 -
1377 - if ( is_object( $result ) && is_array( $result->posts ) ) {
1378 -
1379 - // Sort courses according to the id list.
1380 - $new_array = array();
1381 -
1382 - foreach ( $course_ids as $id ) {
1383 - foreach ( $result->posts as $post ) {
1384 - $post->ID == $id ? $new_array[] = $post : 0;
1385 - }
1386 - }
1387 -
1388 - $result->posts = $new_array;
1389 - }
1390 -
1391 - return $result;
1392 - }
1393 -
1394 - return false;
1395 - }
1396 -
1397 - /**
1398 - * Get the number of courses created by an instructor within a given date range.
1399 - *
1400 - * @since 4.0.0
1401 - *
1402 - * If no date range is provided, returns the total course count for the instructor.
1403 - * Course creation date is determined using the `_wp_old_date` meta value when available; otherwise, the course
1404 - * post date is used.
1405 - *
1406 - * @param string|null $start_date Start date in Y-m-d format.
1407 - * @param string|null $end_date End date in Y-m-d format.
1408 - * @param int $user_id Course author (user) ID.
1409 - *
1410 - * @return int Total number of matching courses.
1411 - */
1412 - public static function get_course_count_by_date( $start_date, $end_date, $user_id ) {
1413 -
1414 - $user_id = tutor_utils()->get_user_id( $user_id );
1415 -
1416 - if ( empty( $start_date ) && empty( $end_date ) ) {
1417 - return (int) self::get_courses_by_instructor( $user_id, array( 'publish', 'private' ), 0, PHP_INT_MAX, true );
1418 - }
1419 -
1420 - $courses = self::get_courses_by_instructor( $user_id, array( 'publish', 'private' ), 0, PHP_INT_MAX );
1421 -
1422 - if ( empty( $courses ) ) {
1423 - return 0;
1424 - }
1425 -
1426 - $start_date_timestamp = strtotime( $start_date . ' 00:00:00' );
1427 - $end_date_timestamp = strtotime( $end_date . ' 23:59:59' );
1428 -
1429 - $filtered = array_filter(
1430 - $courses,
1431 - function ( $course ) use ( $start_date_timestamp, $end_date_timestamp ) {
1432 - $creation_date = get_post_meta( $course->ID, '_wp_old_date', true );
1433 -
1434 - $course_creation_date = empty( $creation_date ) ? strtotime( $course->post_date_gmt ) : strtotime( $creation_date );
1435 -
1436 - return $start_date_timestamp <= $course_creation_date && $end_date_timestamp >= $course_creation_date;
1437 - }
1438 - );
1439 -
1440 - return count( $filtered );
1441 - }
1442 -
1443 - /**
1444 - * Get topic-wise progress data for a course and a student.
1445 - *
1446 - * @since 4.0.0
1447 - *
1448 - * @param int $course_id Course ID.
1449 - * @param int $student_id Student ID.
1450 - *
1451 - * @return array[] Topic progress data.
1452 - */
1453 - public static function get_course_progress_details( $course_id, $student_id ) {
1454 -
1455 - $topics_query = tutor_utils()->get_topics( $course_id );
1456 - $topic_list = array();
1457 -
1458 - if ( empty( $topics_query ) || ! $topics_query->have_posts() ) {
1459 - return $topic_list;
1460 - }
1461 -
1462 - foreach ( $topics_query->posts as $topic_post ) {
1463 - $topic_id = (int) $topic_post->ID;
1464 - $items = array();
1465 - $completion_percentage = tutor_utils()->count_completed_contents_by_topic( $topic_id, $student_id );
1466 - $contents_query = tutor_utils()->get_course_contents_by_topic( $topic_id, -1 );
1467 -
1468 - if ( ! empty( $contents_query ) && $contents_query->have_posts() ) {
1469 - foreach ( $contents_query->posts as $content_post ) {
1470 - $items[] = ( new self() )->build_course_progress_item( $content_post, $student_id, $course_id );
1471 - }
1472 - }
1473 -
1474 - $topic_list[] = array(
1475 - 'id' => $topic_id,
1476 - 'summary' => $topic_post->post_content,
1477 - 'title' => get_the_title( $topic_id ),
1478 - 'items' => $items,
1479 - 'completion_percentage' => $completion_percentage['percentage'] ?? 0,
1480 - );
1481 - }
1482 -
1483 - wp_reset_postdata();
1484 -
1485 - return $topic_list;
1486 - }
1487 -
1488 - /**
1489 - * Check if the current user has course content access
1490 - *
1491 - * @since 4.0.0
1492 - *
1493 - * @throws InvalidArgumentException If args are invalid.
1494 - *
1495 - * @param array $args Array of arguments.
1496 - *
1497 - * `$defaults = array(
1498 - * 'current_post_type' => '',
1499 - * 'current_post_id' => 0,
1500 - * 'course_id' => 0,
1501 - * 'is_public' => false,
1502 - * 'is_enrolled' => false,
1503 - * );`.
1504 - *
1505 - * @return bool
1506 - */
1507 - public static function has_course_content_access( array $args = array() ): bool {
1508 - if ( User::is_admin() ) {
1509 - return true;
1510 - }
1511 -
1512 - $defaults = array(
1513 - 'current_post_type' => '',
1514 - 'current_post_id' => 0,
1515 - 'course_id' => 0,
1516 - 'is_public' => false,
1517 - 'is_enrolled' => false,
1518 - );
1519 -
1520 - $args = wp_parse_args( $args, $defaults );
1521 -
1522 - if ( ! $args['course_id'] || ! $args['current_post_id'] || ! $args['current_post_type'] ) {
1523 - throw new InvalidArgumentException( __( 'Invalid argument passed', 'tutor' ) );
1524 - }
1525 -
1526 - $has_access = false;
1527 - $user_id = get_current_user_id();
1528 - switch ( $args['current_post_type'] ) {
1529 - case tutor()->lesson_post_type:
1530 - $is_preview = (int) get_post_meta( $args['current_post_id'], Lesson::PREVIEW_META_KEY, true );
1531 - if ( $args['is_public'] || $is_preview ) {
1532 - $has_access = true;
1533 - } elseif ( $args['is_enrolled'] || tutor_utils()->has_enrolled_content_access( 'lesson' ) ) {
1534 - $has_access = true;
1535 - }
1536 - break;
1537 - case tutor()->quiz_post_type:
1538 - case tutor()->assignment_post_type:
1539 - if ( $user_id ) {
1540 - $content_type = tutor()->quiz_post_type === $args['current_post_type'] ? 'quiz' : 'assignment';
1541 - if ( $args['is_enrolled'] || $args['is_public'] || tutor_utils()->has_enrolled_content_access( $content_type ) ) {
1542 - $has_access = true;
1543 - }
1544 - }
1545 - break;
1546 - default:
1547 - if ( $args['is_enrolled'] ) {
1548 - $has_access = true;
1549 - }
1550 - break;
1551 - }
1552 -
1553 - return apply_filters( 'tutor_course_content_access', $has_access, $args );
1554 - }
1555 -
1556 - /**
1557 - * Build course progress item data for a specific lesson, quiz, assignment, or meeting.
1558 - *
1559 - * @since 4.0.0
1560 - *
1561 - * @param \WP_Post $post The course content post object.
1562 - * @param int $user_id The user ID for whom progress is being calculated.
1563 - * @param int $course_id The course ID to which the content belongs.
1564 - *
1565 - * @return array Associative array of progress item data
1566 - */
1567 - private function build_course_progress_item( \WP_Post $post, $user_id, $course_id ) {
1568 -
1569 - $base_items = array(
1570 - 'id' => $post->ID,
1571 - 'type' => $post->post_type,
1572 - 'link' => esc_url_raw( get_permalink( $post->ID ) ),
1573 - 'title' => $post->post_title,
1574 - );
1575 -
1576 - switch ( $post->post_type ) {
1577 - case tutor()->quiz_post_type:
1578 - $quiz_status = '';
1579 - $icon_name = Icon::QUIZ_2;
1580 - $icon_class = 'tutor-text-subdued';
1581 - $last_attempt = ( new QuizModel() )->get_first_or_last_attempt( $post->ID, $user_id );
1582 -
1583 - if ( is_object( $last_attempt ) && QuizModel::ATTEMPT_STARTED !== $last_attempt->attempt_status ) {
1584 - $quiz_status = QuizModel::get_quiz_result( $post->ID, $user_id );
1585 -
1586 - $quiz_icon_map = array(
1587 - QuizModel::RESULT_FAIL => array( Icon::CROSS_COLORIZE, 'tutor-icon-critical' ),
1588 - QuizModel::RESULT_PENDING => array( Icon::INFO_COLORIZE, 'tutor-icon-warning-secondary' ),
1589 - );
1590 -
1591 - if ( isset( $quiz_icon_map[ $quiz_status ] ) ) {
1592 - list( $icon_name, $icon_class ) = $quiz_icon_map[ $quiz_status ];
1593 - }
1594 - }
1595 -
1596 - return array_merge(
1597 - $base_items,
1598 - array(
1599 - 'is_completed' => QuizModel::RESULT_PASS === $quiz_status,
1600 - 'label' => __( 'Quiz', 'tutor' ),
1601 - 'icon' => $icon_name,
1602 - 'icon_class' => $icon_class,
1603 - )
1604 - );
1605 -
1606 - case tutor()->assignment_post_type:
1607 - $status = '';
1608 -
1609 - $is_submitted = tutor_utils()->is_assignment_submitted( $post->ID, $user_id );
1610 - $status = $is_submitted ? Assignments::get_assignment_result( $post->ID, $user_id ) : $status;
1611 -
1612 - if ( ! $is_submitted && Assignments::is_expired( $post->ID, $user_id, $course_id ) ) {
1613 - $status = 'fail';
1614 - }
1615 -
1616 - $icon = Icon::BOOK_2;
1617 - $icon_class = 'tutor-text-subdued';
1618 -
1619 - $status_map = array(
1620 - 'pending' => array( Icon::INFO_COLORIZE, 'tutor-icon-warning-secondary' ),
1621 - 'fail' => array( Icon::CROSS_COLORIZE, 'tutor-icon-critical' ),
1622 - );
1623 -
1624 - if ( isset( $status_map[ $status ] ) ) {
1625 - list( $icon, $icon_class ) = $status_map[ $status ];
1626 - }
1627 -
1628 - return array_merge(
1629 - $base_items,
1630 - array(
1631 - 'is_completed' => 'pass' === $status,
1632 - 'label' => __( 'Assignment', 'tutor' ),
1633 - 'icon' => $icon,
1634 - 'icon_class' => $icon_class,
1635 - )
1636 - );
1637 -
1638 - case tutor()->zoom_post_type:
1639 - if ( ! tutor_utils()->is_addon_enabled( 'tutor-zoom' ) ) {
1640 - return $base_items;
1641 - }
1642 -
1643 - return array_merge(
1644 - $base_items,
1645 - array(
1646 - 'label' => __( 'Zoom', 'tutor' ),
1647 - 'icon' => Icon::ZOOM,
1648 - 'is_completed' => \TUTOR_ZOOM\Zoom::is_zoom_lesson_done( '', $post->ID, $user_id ),
1649 - )
1650 - );
1651 -
1652 - case tutor()->meet_post_type:
1653 - if ( ! tutor_utils()->is_addon_enabled( 'google-meet' ) ) {
1654 - return $base_items;
1655 - }
1656 -
1657 - return array_merge(
1658 - $base_items,
1659 - array(
1660 - 'label' => __( 'Google Meet', 'tutor' ),
1661 - 'icon' => Icon::GOOGLE_MEET,
1662 - 'is_completed' => \TutorPro\GoogleMeet\Frontend\Frontend::is_lesson_completed( false, $post->ID, $user_id ),
1663 - )
1664 - );
1665 -
1666 - default:
1667 - $video = tutor_utils()->get_video_info( $post->ID );
1668 -
1669 - return array_merge(
1670 - $base_items,
1671 - array(
1672 - 'video' => $video,
1673 - 'video_play_time' => isset( $video->playtime ) ? $video->playtime : '',
1674 - 'is_completed' => (bool) tutor_utils()->is_completed_lesson( $post->ID, $user_id ),
1675 - 'label' => empty( $video ) ? __( 'Reading', 'tutor' ) : __( 'Video', 'tutor' ),
1676 - 'icon' => empty( $video ) ? Icon::COURSES : Icon::VIDEO_CAMERA,
1677 - )
1678 - );
1679 - }
1680 - }
1681 -
1682 - /**
1683 - * Calculate the total course duration for a set of courses and returns the total time in hours, minutes, and
1684 - * seconds.
1685 - *
1686 - * @since 4.0.0
1687 - *
1688 - * @param array<int> | int $course_ids List of course IDs or a single course ID.
1689 - *
1690 - * @return array{
1691 - * hours: int,
1692 - * minutes: int,
1693 - * seconds: int
1694 - * } Total accumulated duration from all given courses.
1695 - */
1696 - public static function get_total_course_duration( $course_ids ): array {
1697 - $total_seconds = 0;
1698 - $course_ids = is_array( $course_ids ) ? $course_ids : array( $course_ids );
1699 - foreach ( $course_ids as $id ) {
1700 - $total_seconds += self::get_course_duration_in_seconds( $id );
1701 - }
1702 -
1703 - return DateTimeHelper::split_seconds_into_time_units( $total_seconds );
1704 - }
1705 -
1706 - /**
1707 - * Calculate the estimated time spent on courses based on completion progress.
1708 - *
1709 - * @since 4.0.0
1710 - *
1711 - * @param array<int> $course_ids List of course IDs.
1712 - *
1713 - * @return array{
1714 - * hours: int,
1715 - * minutes: int,
1716 - * seconds: int
1717 - * } Total accumulated duration from all given courses.
1718 - */
1719 - public static function get_total_estimated_time_spent( $course_ids ): array {
1720 - $total_seconds = 0;
1721 - foreach ( $course_ids as $id ) {
1722 - $completion_percentage = tutor_utils()->is_completed_course( $id )
1723 - ? 100
1724 - : (int) tutor_utils()->get_course_completed_percent( (int) $id );
1725 -
1726 - if ( 0 === $completion_percentage ) {
1727 - continue;
1728 - }
1729 - $course_duration_in_seconds = self::get_course_duration_in_seconds( $id );
1730 - // Calculate the time spent by a user based on the course completion percentage.
1731 - $total_seconds += (int) round( $course_duration_in_seconds * ( $completion_percentage / 100 ) );
1732 - }
1733 -
1734 - return DateTimeHelper::split_seconds_into_time_units( $total_seconds );
1735 - }
1736 -
1737 - /**
1738 - * Get the total duration of a course in seconds.
1739 - *
1740 - * @since 4.0.0
1741 - *
1742 - * @param int $course_id Course ID.
1743 - *
1744 - * @return int Course duration in seconds.
1745 - */
1746 - public static function get_course_duration_in_seconds( int $course_id ): int {
1747 - $duration = tutor_utils()->get_course_duration( $course_id, true );
1748 -
1749 - return ( $duration['durationHours'] * HOUR_IN_SECONDS )
1750 - + ( $duration['durationMinutes'] * MINUTE_IN_SECONDS )
1751 - + ( $duration['durationSeconds'] );
579 + return array();
1752 580 }
1753 581 }