PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.3.1
Tutor LMS – eLearning and online course solution v3.3.1
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 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / models / CourseModel.php
tutor / models Last commit date
BillingModel.php 1 year ago CartModel.php 1 year ago CouponModel.php 1 year ago CourseModel.php 1 year ago LessonModel.php 1 year ago OrderActivitiesModel.php 1 year ago OrderMetaModel.php 1 year ago OrderModel.php 1 year ago QuizModel.php 1 year ago UserModel.php 1 year ago WithdrawModel.php 1 year ago
CourseModel.php
889 lines
1 <?php
2 /**
3 * Course Model
4 *
5 * @package Tutor\Models
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com
8 * @since 2.0.6
9 */
10
11 namespace Tutor\Models;
12
13 use TUTOR\Course;
14 use Tutor\Helpers\QueryHelper;
15
16 /**
17 * CourseModel Class
18 *
19 * @since 2.0.6
20 */
21 class CourseModel {
22 /**
23 * WordPress course type name
24 *
25 * @var string
26 */
27 const POST_TYPE = 'courses';
28 const COURSE_CATEGORY = 'course-category';
29 const COURSE_TAG = 'course-tag';
30
31 const STATUS_PUBLISH = 'publish';
32 const STATUS_DRAFT = 'draft';
33 const STATUS_AUTO_DRAFT = 'auto-draft';
34 const STATUS_PENDING = 'pending';
35 const STATUS_PRIVATE = 'private';
36 const STATUS_FUTURE = 'future';
37 const STATUS_TRASH = 'trash';
38
39 /**
40 * Course completion modes
41 */
42 const MODE_FLEXIBLE = 'flexible';
43 const MODE_STRICT = 'strict';
44
45 /**
46 * Course mapped with the product using this meta key
47 *
48 * @var string
49 */
50 const WC_PRODUCT_META_KEY = '_tutor_course_product_id';
51
52 /**
53 * Course attachment/downloadable resources meta key
54 *
55 * @var string
56 */
57 const ATTACHMENT_META_KEY = '_tutor_attachments';
58
59 /**
60 * Course benefits meta key
61 *
62 * @var string
63 */
64 const BENEFITS_META_KEY = '_tutor_course_benefits';
65
66 /**
67 * Get available status list.
68 *
69 * @since 3.0.0
70 *
71 * @return array
72 */
73 public static function get_status_list() {
74 return array(
75 self::STATUS_DRAFT,
76 self::STATUS_AUTO_DRAFT,
77 self::STATUS_PUBLISH,
78 self::STATUS_PRIVATE,
79 self::STATUS_FUTURE,
80 self::STATUS_PENDING,
81 self::STATUS_TRASH,
82 );
83 }
84
85 /**
86 * Course record count
87 *
88 * @since 2.0.7
89 *
90 * @param string $status course status.
91 * @return int
92 */
93 public static function count( $status = self::STATUS_PUBLISH ) {
94 $count_obj = wp_count_posts( self::POST_TYPE );
95 if ( 'all' === $status ) {
96 return array_sum( (array) $count_obj );
97 }
98
99 return (int) $count_obj->{$status};
100 }
101
102 /**
103 * Get courses
104 *
105 * @since 1.0.0
106 *
107 * @param array $excludes exclude course ids.
108 * @param array $post_status post status array.
109 *
110 * @return array|null|object
111 */
112 public static function get_courses( $excludes = array(), $post_status = array( 'publish' ) ) {
113 global $wpdb;
114
115 $excludes = (array) $excludes;
116 $exclude_query = '';
117
118 if ( count( $excludes ) ) {
119 $exclude_query = implode( "','", $excludes );
120 }
121
122 $post_status = array_map(
123 function ( $element ) {
124 return "'" . $element . "'";
125 },
126 $post_status
127 );
128
129 $post_status = implode( ',', $post_status );
130 $course_post_type = tutor()->course_post_type;
131
132 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
133 $query = $wpdb->get_results(
134 $wpdb->prepare(
135 "SELECT ID,
136 post_author,
137 post_title,
138 post_name,
139 post_status,
140 menu_order
141 FROM {$wpdb->posts}
142 WHERE post_status IN ({$post_status})
143 AND ID NOT IN('$exclude_query')
144 AND post_type = %s;
145 ",
146 $course_post_type
147 )
148 );
149 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
150
151 return $query;
152 }
153
154 /**
155 * Get courses using provided args
156 *
157 * If user is not admin then it will return only current user's post
158 *
159 * @since 3.0.0
160 *
161 * @param array $args Args.
162 *
163 * @return \WP_Query
164 */
165 public static function get_courses_by_args( array $args = array() ) {
166
167 $default_args = array(
168 'post_type' => tutor()->course_post_type,
169 'posts_per_page' => -1,
170 'post_status' => 'publish',
171 );
172
173 if ( ! current_user_can( 'manage_options' ) ) {
174 $default_args['author'] = get_current_user_id();
175 }
176
177 $args = wp_parse_args( $args, apply_filters( 'tutor_get_course_list_filter_args', $default_args ) );
178
179 return new \WP_Query( $args );
180 }
181
182 /**
183 * Get course count by instructor
184 *
185 * @since 1.0.0
186 *
187 * @param int $instructor_id instructor ID.
188 *
189 * @return null|string
190 */
191 public static function get_course_count_by_instructor( $instructor_id ) {
192 global $wpdb;
193
194 $course_post_type = tutor()->course_post_type;
195
196 $count = $wpdb->get_var(
197 $wpdb->prepare(
198 "SELECT COUNT(ID)
199 FROM {$wpdb->posts}
200 INNER JOIN {$wpdb->usermeta}
201 ON user_id = %d
202 AND meta_key = %s
203 AND meta_value = ID
204 WHERE post_status = %s
205 AND post_type = %s;
206 ",
207 $instructor_id,
208 '_tutor_instructor_course_id',
209 'publish',
210 $course_post_type
211 )
212 );
213
214 return $count;
215 }
216
217 /**
218 * Get course by quiz
219 *
220 * @since 1.0.0
221 *
222 * @param int $quiz_id quiz id.
223 *
224 * @return array|bool|null|object|void
225 */
226 public static function get_course_by_quiz( $quiz_id ) {
227 $quiz_id = tutils()->get_post_id( $quiz_id );
228 $post = get_post( $quiz_id );
229
230 if ( $post ) {
231 $course = get_post( $post->post_parent );
232 if ( $course ) {
233 if ( tutor()->course_post_type !== $course->post_type ) {
234 $course = get_post( $course->post_parent );
235 }
236 return $course;
237 }
238 }
239
240 return false;
241 }
242
243 /**
244 * Get courses by a instructor
245 *
246 * @since 1.0.0
247 *
248 * @param integer $instructor_id instructor id.
249 * @param array|string $post_status post status.
250 * @param integer $offset offset.
251 * @param integer $limit limit.
252 * @param boolean $count_only count or not.
253 *
254 * @return array|null|object
255 */
256 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 ) {
257 global $wpdb;
258 $offset = sanitize_text_field( $offset );
259 $limit = sanitize_text_field( $limit );
260 $instructor_id = tutils()->get_user_id( $instructor_id );
261 $course_post_type = tutor()->course_post_type;
262
263 if ( empty( $post_status ) || 'any' == $post_status ) {
264 $where_post_status = '';
265 } else {
266 ! is_array( $post_status ) ? $post_status = array( $post_status ) : 0;
267 $statuses = "'" . implode( "','", $post_status ) . "'";
268 $where_post_status = "AND $wpdb->posts.post_status IN({$statuses}) ";
269 }
270
271 $select_col = $count_only ? " COUNT(DISTINCT $wpdb->posts.ID) " : " $wpdb->posts.* ";
272 $limit_offset = $count_only ? '' : " LIMIT $offset, $limit ";
273
274 //phpcs:disable
275 $query = $wpdb->prepare(
276 "SELECT $select_col
277 FROM $wpdb->posts
278 LEFT JOIN {$wpdb->usermeta}
279 ON $wpdb->usermeta.user_id = %d
280 AND $wpdb->usermeta.meta_key = %s
281 AND $wpdb->usermeta.meta_value = $wpdb->posts.ID
282 WHERE 1 = 1 {$where_post_status}
283 AND $wpdb->posts.post_type = %s
284 AND ($wpdb->posts.post_author = %d OR $wpdb->usermeta.user_id = %d)
285 ORDER BY $wpdb->posts.post_date DESC $limit_offset",
286 $instructor_id,
287 '_tutor_instructor_course_id',
288 $course_post_type,
289 $instructor_id,
290 $instructor_id
291 );
292 //phpcs:enable
293
294 //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
295 return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query, OBJECT );
296 }
297
298 /**
299 * Get courses for instructors
300 *
301 * @since 1.0.0
302 *
303 * @param int $instructor_id Instructor ID.
304 * @return array|null|object
305 */
306 public function get_courses_for_instructors( $instructor_id = 0 ) {
307 $instructor_id = tutor_utils()->get_user_id( $instructor_id );
308 $course_post_type = tutor()->course_post_type;
309
310 $courses = get_posts(
311 array(
312 'post_type' => $course_post_type,
313 'author' => $instructor_id,
314 'post_status' => array( 'publish', 'pending' ),
315 'posts_per_page' => 5,
316 )
317 );
318
319 return $courses;
320 }
321
322 /**
323 * Check a user is main instructor of a course
324 *
325 * @since 2.1.6
326 *
327 * @param integer $course_id course id.
328 * @param integer $user_id instructor id ( optional ) default: current user id.
329 *
330 * @return boolean
331 */
332 public static function is_main_instructor( $course_id, $user_id = 0 ) {
333 $course = get_post( $course_id );
334 $user_id = tutor_utils()->get_user_id( $user_id );
335
336 if ( ! $course || self::POST_TYPE !== $course->post_type || $user_id !== (int) $course->post_author ) {
337 return false;
338 }
339
340 return true;
341 }
342
343 /**
344 * Mark the course as completed
345 *
346 * @since 2.0.7
347 *
348 * @param int $course_id course id which is completed.
349 * @param int $user_id student id who completed the course.
350 *
351 * @return bool
352 */
353 public static function mark_course_as_completed( $course_id, $user_id ) {
354 if ( ! $course_id || ! $user_id ) {
355 return false;
356 }
357
358 do_action( 'tutor_course_complete_before', $course_id );
359
360 /**
361 * Marking course completed at Comment.
362 */
363 global $wpdb;
364
365 $date = date( 'Y-m-d H:i:s', tutor_time() ); //phpcs:ignore
366
367 // Making sure that, hash is unique.
368 do {
369 $hash = substr( md5( wp_generate_password( 32 ) . $date . $course_id . $user_id ), 0, 16 );
370 $has_hash = (int) $wpdb->get_var(
371 $wpdb->prepare(
372 "SELECT COUNT(comment_ID) from {$wpdb->comments}
373 WHERE comment_agent = 'TutorLMSPlugin' AND comment_type = 'course_completed' AND comment_content = %s ",
374 $hash
375 )
376 );
377
378 } while ( $has_hash > 0 );
379
380 $data = array(
381 'comment_post_ID' => $course_id,
382 'comment_author' => $user_id,
383 'comment_date' => $date,
384 'comment_date_gmt' => get_gmt_from_date( $date ),
385 'comment_content' => $hash, // Identification Hash.
386 'comment_approved' => 'approved',
387 'comment_agent' => 'TutorLMSPlugin',
388 'comment_type' => 'course_completed',
389 'user_id' => $user_id,
390 );
391
392 $wpdb->insert( $wpdb->comments, $data );
393
394 do_action( 'tutor_course_complete_after', $course_id, $user_id );
395
396 return true;
397 }
398
399 /**
400 * Delete a course by ID
401 *
402 * @since 2.0.9
403 *
404 * @param int $post_id course id that need to delete.
405 * @return bool
406 */
407 public static function delete_course( $post_id ) {
408 if ( get_post_type( $post_id ) !== tutor()->course_post_type ) {
409 return false;
410 }
411
412 wp_delete_post( $post_id, true );
413 return true;
414 }
415
416 /**
417 * Get post ids by post type and parent_id
418 *
419 * @since 1.6.6
420 *
421 * @param string $post_type post type.
422 * @param integer $post_parent post parent ID.
423 *
424 * @return array
425 */
426 private function get_post_ids( $post_type, $post_parent ) {
427 $args = array(
428 'fields' => 'ids',
429 'post_type' => $post_type,
430 'post_parent' => $post_parent,
431 'post_status' => 'any',
432 'posts_per_page' => -1,
433 );
434 return get_posts( $args );
435 }
436
437 /**
438 * Delete course data when permanently deleting a course.
439 *
440 * @since 1.6.6
441 * @since 2.0.9 updated
442 *
443 * @param integer $post_id post ID.
444 * @return bool
445 */
446 public function delete_course_data( $post_id ) {
447 $course_post_type = tutor()->course_post_type;
448 if ( get_post_type( $post_id ) !== $course_post_type ) {
449 return false;
450 }
451
452 do_action( 'tutor_before_delete_course_content', $post_id, 0 );
453
454 global $wpdb;
455
456 $lesson_post_type = tutor()->lesson_post_type;
457 $assignment_post_type = tutor()->assignment_post_type;
458 $quiz_post_type = tutor()->quiz_post_type;
459
460 $topic_ids = $this->get_post_ids( 'topics', $post_id );
461
462 // Course > Topic > ( Lesson | Quiz | Assignment ).
463 if ( ! empty( $topic_ids ) ) {
464 foreach ( $topic_ids as $topic_id ) {
465 $content_post_type = array( $lesson_post_type, $assignment_post_type, $quiz_post_type );
466 $topic_content_ids = $this->get_post_ids( $content_post_type, $topic_id );
467
468 foreach ( $topic_content_ids as $content_id ) {
469 /**
470 * Delete Quiz data
471 */
472 if ( get_post_type( $content_id ) === 'tutor_quiz' ) {
473 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $content_id ) );
474 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $content_id ) );
475
476 do_action( 'tutor_before_delete_quiz_content', $content_id, null );
477
478 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $content_id ) );
479 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
480 $in_question_ids = "'" . implode( "','", $questions_ids ) . "'";
481 //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
482 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id IN({$in_question_ids}) " );
483 }
484 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $content_id ) );
485 }
486
487 /**
488 * Delete assignment data ( Assignments, Assignment Submit, Assignment Evalutation )
489 *
490 * @since 2.0.9
491 */
492 if ( get_post_type( $content_id ) === $assignment_post_type ) {
493 QueryHelper::delete_comment_with_meta(
494 array(
495 'comment_type' => 'tutor_assignment',
496 'comment_post_ID' => $content_id,
497 )
498 );
499 }
500
501 wp_delete_post( $content_id, true );
502
503 }
504
505 // Delete zoom meeting.
506 $wpdb->delete(
507 $wpdb->posts,
508 array(
509 'post_parent' => $topic_id,
510 'post_type' => 'tutor_zoom_meeting',
511 )
512 );
513
514 /**
515 * Delete Google Meet Record Related to Course Topic
516 *
517 * @since 2.1.0
518 */
519 $wpdb->delete(
520 $wpdb->posts,
521 array(
522 'post_parent' => $topic_id,
523 'post_type' => 'tutor-google-meet',
524 )
525 );
526
527 wp_delete_post( $topic_id, true );
528 }
529 }
530
531 $child_post_ids = $this->get_post_ids( array( 'tutor_announcements', 'tutor_enrolled', 'tutor_zoom_meeting', 'tutor-google-meet' ), $post_id );
532 if ( ! empty( $child_post_ids ) ) {
533 foreach ( $child_post_ids as $child_post_id ) {
534 wp_delete_post( $child_post_id, true );
535 }
536 }
537
538 /**
539 * Delete earning, gradebook result, course complete data
540 *
541 * @since 2.0.9
542 */
543 $wpdb->delete( $wpdb->prefix . 'tutor_earnings', array( 'course_id' => $post_id ) );
544 $wpdb->delete( $wpdb->prefix . 'tutor_gradebooks_results', array( 'course_id' => $post_id ) );
545 $wpdb->delete(
546 $wpdb->comments,
547 array(
548 'comment_type' => 'course_completed',
549 'comment_post_ID' => $post_id,
550 )
551 );
552
553 /**
554 * Delete onsite notification record & _tutor_instructor_course_id user meta
555 *
556 * @since 2.1.0
557 */
558 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_notifications WHERE post_id=%d AND type IN ('Announcements','Q&A','Enrollments')", $post_id ) );
559 $wpdb->delete(
560 $wpdb->usermeta,
561 array(
562 'meta_key' => '_tutor_instructor_course_id',
563 'meta_value' => $post_id,
564 )
565 );
566
567 /**
568 * Delete Course rating and review
569 *
570 * @since 2.0.9
571 */
572 QueryHelper::delete_comment_with_meta(
573 array(
574 'comment_type' => 'tutor_course_rating',
575 'comment_post_ID' => $post_id,
576 )
577 );
578
579 /**
580 * Delete Q&A and its status ( read, replied etc )
581 *
582 * @since 2.0.9
583 */
584 QueryHelper::delete_comment_with_meta(
585 array(
586 'comment_type' => 'tutor_q_and_a',
587 'comment_post_ID' => $post_id,
588 )
589 );
590
591 /**
592 * Delete caches
593 */
594 $attempt_cache = new \Tutor\Cache\QuizAttempts();
595 if ( $attempt_cache->has_cache() ) {
596 $attempt_cache->delete_cache();
597 }
598
599 return true;
600 }
601
602
603 /**
604 * Get paid courses
605 *
606 * To identify course is connected with any product
607 * like WC Product or EDD product meta key will be used
608 *
609 * @since 2.2.0
610 *
611 * @since 3.0.0
612 *
613 * Meta key removed and default meta query updated
614 *
615 * @since 3.0.1
616 * Course::COURSE_PRICE_META meta key exists clause added
617 *
618 * @param array $args wp_query args.
619 *
620 * @return \WP_Query
621 */
622 public static function get_paid_courses( array $args = array() ) {
623 $current_user = wp_get_current_user();
624
625 $default_args = array(
626 'post_type' => tutor()->course_post_type,
627 'posts_per_page' => -1,
628 'offset' => 0,
629 'post_status' => 'publish',
630 'meta_query' => array(
631 'relation' => 'AND',
632 array(
633 'key' => Course::COURSE_PRICE_TYPE_META,
634 'value' => Course::PRICE_TYPE_SUBSCRIPTION,
635 'compare' => '!=',
636 ),
637 array(
638 'key' => Course::COURSE_PRICE_META,
639 'compare' => 'EXISTS',
640 ),
641 ),
642 );
643
644 // Check if the current user is an admin.
645 if ( ! current_user_can( 'administrator' ) ) {
646 $args['author'] = $current_user->ID;
647 }
648
649 $args = wp_parse_args( $args, $default_args );
650 return new \WP_Query( $args );
651
652 }
653
654 /**
655 * Check the course is completeable or not
656 *
657 * @since 2.4.0
658 *
659 * @param int $course_id course id.
660 * @param int $user_id user id.
661 *
662 * @return boolean
663 */
664 public static function can_complete_course( $course_id, $user_id ) {
665 $mode = tutor_utils()->get_option( 'course_completion_process' );
666 if ( self::MODE_FLEXIBLE === $mode ) {
667 return true;
668 }
669
670 if ( self::MODE_STRICT === $mode ) {
671 $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course( $course_id, $user_id );
672 $lesson_count = tutor_utils()->get_lesson_count_by_course( $course_id, $user_id );
673
674 if ( $completed_lesson < $lesson_count ) {
675 return false;
676 }
677
678 $quizzes = array();
679 $assignments = array();
680
681 $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
682 if ( tutor_utils()->count( $course_contents ) ) {
683 foreach ( $course_contents as $content ) {
684 if ( 'tutor_quiz' === $content->post_type ) {
685 $quizzes[] = $content;
686 }
687 if ( 'tutor_assignments' === $content->post_type ) {
688 $assignments[] = $content;
689 }
690 }
691 }
692
693 foreach ( $quizzes as $row ) {
694 $result = QuizModel::get_quiz_result( $row->ID );
695 if ( 'pass' !== $result ) {
696 return false;
697 }
698 }
699
700 if ( tutor()->has_pro ) {
701 foreach ( $assignments as $row ) {
702 $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $row->ID, $user_id );
703 if ( 'pass' !== $result ) {
704 return false;
705 }
706 }
707 }
708
709 return true;
710 }
711
712 return false;
713 }
714
715 /**
716 * Check a course can be auto complete by an enrolled student.
717 *
718 * @since 2.4.0
719 *
720 * @param int $course_id course id.
721 * @param int $user_id user id.
722 *
723 * @return boolean
724 */
725 public static function can_autocomplete_course( $course_id, $user_id ) {
726 $auto_course_complete_option = (bool) tutor_utils()->get_option( 'auto_course_complete_on_all_lesson_completion' );
727 if ( ! $auto_course_complete_option ) {
728 return false;
729 }
730
731 $is_course_completed = tutor_utils()->is_completed_course( $course_id, $user_id );
732 if ( $is_course_completed ) {
733 return false;
734 }
735
736 $course_stats = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
737 if ( $course_stats['total_count'] && $course_stats['completed_count'] === $course_stats['total_count'] ) {
738 return self::can_complete_course( $course_id, $user_id );
739 } else {
740 return false;
741 }
742 }
743
744 /**
745 * Get review progress link when course progress 100% and
746 * User has pending or fail quiz or assignment
747 *
748 * @since 2.4.0
749 *
750 * @param int $course_id course id.
751 * @param int $user_id user id.
752 *
753 * @return string course content permalink.
754 */
755 public static function get_review_progress_link( $course_id, $user_id ) {
756 $course_progress = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
757 $completed_percent = (int) $course_progress['completed_percent'];
758 $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
759 $permalink = '';
760
761 if ( tutor_utils()->count( $course_contents ) && 100 === $completed_percent ) {
762 foreach ( $course_contents as $content ) {
763 if ( 'tutor_quiz' === $content->post_type ) {
764 $result = QuizModel::get_quiz_result( $content->ID, $user_id );
765 if ( 'pass' !== $result ) {
766 $permalink = get_the_permalink( $content->ID );
767 break;
768 }
769 }
770
771 if ( tutor()->has_pro && 'tutor_assignments' === $content->post_type ) {
772 $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $content->ID, $user_id );
773 if ( 'pass' !== $result ) {
774 $permalink = get_the_permalink( $content->ID );
775 break;
776 }
777 }
778 }
779 }
780
781 // Fallback link.
782 if ( empty( $permalink ) ) {
783 $permalink = tutils()->get_course_first_lesson( $course_id );
784 }
785
786 return $permalink;
787 }
788
789 /**
790 * Get course preview image placeholder
791 *
792 * @since 3.0.0
793 *
794 * @return string
795 */
796 public static function get_course_preview_image_placeholder() {
797 return tutor()->url . 'assets/images/placeholder.svg';
798 }
799
800 /**
801 * Retrieve the courses or course bundles that a given coupon code applies to.
802 *
803 * This function fetches published courses or course bundles from the database
804 * based on the specified type. For each course, it retrieves the course prices
805 * and the course thumbnail URL. If the user has Tutor Pro, it additionally
806 * retrieves the total number of courses in a course bundle.
807 *
808 * @since 3.0.0
809 *
810 * @param string $applies_to The type of items the coupon applies to. Accepts 'specific_courses'
811 * for individual courses or any other value for course bundles.
812 *
813 * @global wpdb $wpdb WordPress database abstraction object.
814 *
815 * @return array An array of course objects. Each course object contains:
816 * - int $id: The ID of the course.
817 * - string $title: The title of the course.
818 * - string $type: The post type of the course (e.g., 'courses', 'course-bundle').
819 * - float $price: The regular price of the course.
820 * - float $sale_price: The sale price of the course.
821 * - string $image: The URL of the course's thumbnail image.
822 * - int|null $total_courses: The total number of courses in the bundle
823 * (only if the user has Tutor Pro and the course type is 'course-bundle').
824 */
825 public function get_coupon_applies_to_courses( string $applies_to ) {
826 global $wpdb;
827
828 $post_type = 'specific_courses' === $applies_to ? 'courses' : 'course-bundle';
829
830 $where = array(
831 'post_status' => 'publish',
832 'post_type' => $post_type,
833 );
834
835 $courses = QueryHelper::get_all( $wpdb->posts, $where, 'ID' );
836
837 if ( tutor()->has_pro ) {
838 $bundle_model = new \TutorPro\CourseBundle\Models\BundleModel();
839 }
840
841 $final_data = array();
842
843 if ( ! empty( $courses ) ) {
844 foreach ( $courses as $course ) {
845 $data = new \stdClass();
846
847 if ( tutor()->has_pro && 'course-bundle' === $course->type ) {
848 $data->total_courses = count( $bundle_model->get_bundle_course_ids( $course->ID ) );
849 }
850
851 $author_name = get_the_author_meta( 'display_name', $course->post_author );
852 $course_prices = tutor_utils()->get_raw_course_price( $course->ID );
853 $data->id = (int) $course->ID;
854 $data->title = $course->post_title;
855 $data->price = $course_prices->regular_price;
856 $data->sale_price = $course_prices->sale_price;
857 $data->image = get_the_post_thumbnail_url( $course->ID );
858 $data->author = $author_name;
859
860 $final_data[] = $data;
861 }
862 }
863
864 return ! empty( $final_data ) ? $final_data : array();
865 }
866
867 /**
868 * Get course instructor IDs.
869 *
870 * @since 3.0.0
871 *
872 * @param int $course_id course id.
873 *
874 * @return array
875 */
876 public static function get_course_instructor_ids( $course_id ) {
877 global $wpdb;
878 $instructor_ids = $wpdb->get_col(
879 $wpdb->prepare(
880 "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key=%s AND meta_value=%s",
881 '_tutor_instructor_course_id',
882 $course_id
883 )
884 );
885
886 return $instructor_ids;
887 }
888 }
889