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