PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.0.1
Tutor LMS – eLearning and online course solution v3.0.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 2 years 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
891 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 if ( tutor()->has_pro && \TutorPro\H5P\H5P::is_enabled() ) {
451 \TutorPro\H5P\Lesson::delete_h5p_lesson_statements_by_id( $post_id, 0 );
452 }
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 if ( tutor()->has_pro && \TutorPro\H5P\H5P::is_enabled() ) {
477 \TutorPro\H5P\Utils::delete_h5p_quiz_statements_by_id( $content_id );
478 }
479
480 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $content_id ) );
481 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
482 $in_question_ids = "'" . implode( "','", $questions_ids ) . "'";
483 //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
484 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id IN({$in_question_ids}) " );
485 }
486 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $content_id ) );
487 }
488
489 /**
490 * Delete assignment data ( Assignments, Assignment Submit, Assignment Evalutation )
491 *
492 * @since 2.0.9
493 */
494 if ( get_post_type( $content_id ) === $assignment_post_type ) {
495 QueryHelper::delete_comment_with_meta(
496 array(
497 'comment_type' => 'tutor_assignment',
498 'comment_post_ID' => $content_id,
499 )
500 );
501 }
502
503 wp_delete_post( $content_id, true );
504
505 }
506
507 // Delete zoom meeting.
508 $wpdb->delete(
509 $wpdb->posts,
510 array(
511 'post_parent' => $topic_id,
512 'post_type' => 'tutor_zoom_meeting',
513 )
514 );
515
516 /**
517 * Delete Google Meet Record Related to Course Topic
518 *
519 * @since 2.1.0
520 */
521 $wpdb->delete(
522 $wpdb->posts,
523 array(
524 'post_parent' => $topic_id,
525 'post_type' => 'tutor-google-meet',
526 )
527 );
528
529 wp_delete_post( $topic_id, true );
530 }
531 }
532
533 $child_post_ids = $this->get_post_ids( array( 'tutor_announcements', 'tutor_enrolled', 'tutor_zoom_meeting', 'tutor-google-meet' ), $post_id );
534 if ( ! empty( $child_post_ids ) ) {
535 foreach ( $child_post_ids as $child_post_id ) {
536 wp_delete_post( $child_post_id, true );
537 }
538 }
539
540 /**
541 * Delete earning, gradebook result, course complete data
542 *
543 * @since 2.0.9
544 */
545 $wpdb->delete( $wpdb->prefix . 'tutor_earnings', array( 'course_id' => $post_id ) );
546 $wpdb->delete( $wpdb->prefix . 'tutor_gradebooks_results', array( 'course_id' => $post_id ) );
547 $wpdb->delete(
548 $wpdb->comments,
549 array(
550 'comment_type' => 'course_completed',
551 'comment_post_ID' => $post_id,
552 )
553 );
554
555 /**
556 * Delete onsite notification record & _tutor_instructor_course_id user meta
557 *
558 * @since 2.1.0
559 */
560 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_notifications WHERE post_id=%d AND type IN ('Announcements','Q&A','Enrollments')", $post_id ) );
561 $wpdb->delete(
562 $wpdb->usermeta,
563 array(
564 'meta_key' => '_tutor_instructor_course_id',
565 'meta_value' => $post_id,
566 )
567 );
568
569 /**
570 * Delete Course rating and review
571 *
572 * @since 2.0.9
573 */
574 QueryHelper::delete_comment_with_meta(
575 array(
576 'comment_type' => 'tutor_course_rating',
577 'comment_post_ID' => $post_id,
578 )
579 );
580
581 /**
582 * Delete Q&A and its status ( read, replied etc )
583 *
584 * @since 2.0.9
585 */
586 QueryHelper::delete_comment_with_meta(
587 array(
588 'comment_type' => 'tutor_q_and_a',
589 'comment_post_ID' => $post_id,
590 )
591 );
592
593 /**
594 * Delete caches
595 */
596 $attempt_cache = new \Tutor\Cache\QuizAttempts();
597 if ( $attempt_cache->has_cache() ) {
598 $attempt_cache->delete_cache();
599 }
600
601 return true;
602 }
603
604
605 /**
606 * Get paid courses
607 *
608 * To identify course is connected with any product
609 * like WC Product or EDD product meta key will be used
610 *
611 * @since 2.2.0
612 *
613 * @since 3.0.0
614 *
615 * Meta key removed and default meta query updated
616 *
617 * @since 3.0.1
618 * Course::COURSE_PRICE_META meta key exists clause added
619 *
620 * @param array $args wp_query args.
621 *
622 * @return \WP_Query
623 */
624 public static function get_paid_courses( array $args = array() ) {
625 $current_user = wp_get_current_user();
626
627 $default_args = array(
628 'post_type' => tutor()->course_post_type,
629 'posts_per_page' => -1,
630 'offset' => 0,
631 'post_status' => 'publish',
632 'meta_query' => array(
633 'relation' => 'AND',
634 array(
635 'key' => Course::COURSE_PRICE_TYPE_META,
636 'value' => Course::PRICE_TYPE_SUBSCRIPTION,
637 'compare' => '!=',
638 ),
639 array(
640 'key' => Course::COURSE_PRICE_META,
641 'compare' => 'EXISTS',
642 ),
643 ),
644 );
645
646 // Check if the current user is an admin.
647 if ( ! current_user_can( 'administrator' ) ) {
648 $args['author'] = $current_user->ID;
649 }
650
651 $args = wp_parse_args( $args, $default_args );
652 return new \WP_Query( $args );
653
654 }
655
656 /**
657 * Check the course is completeable or not
658 *
659 * @since 2.4.0
660 *
661 * @param int $course_id course id.
662 * @param int $user_id user id.
663 *
664 * @return boolean
665 */
666 public static function can_complete_course( $course_id, $user_id ) {
667 $mode = tutor_utils()->get_option( 'course_completion_process' );
668 if ( self::MODE_FLEXIBLE === $mode ) {
669 return true;
670 }
671
672 if ( self::MODE_STRICT === $mode ) {
673 $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course( $course_id, $user_id );
674 $lesson_count = tutor_utils()->get_lesson_count_by_course( $course_id, $user_id );
675
676 if ( $completed_lesson < $lesson_count ) {
677 return false;
678 }
679
680 $quizzes = array();
681 $assignments = array();
682
683 $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
684 if ( tutor_utils()->count( $course_contents ) ) {
685 foreach ( $course_contents as $content ) {
686 if ( 'tutor_quiz' === $content->post_type ) {
687 $quizzes[] = $content;
688 }
689 if ( 'tutor_assignments' === $content->post_type ) {
690 $assignments[] = $content;
691 }
692 }
693 }
694
695 foreach ( $quizzes as $row ) {
696 $result = QuizModel::get_quiz_result( $row->ID );
697 if ( 'pass' !== $result ) {
698 return false;
699 }
700 }
701
702 if ( tutor()->has_pro ) {
703 foreach ( $assignments as $row ) {
704 $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $row->ID, $user_id );
705 if ( 'pass' !== $result ) {
706 return false;
707 }
708 }
709 }
710
711 return true;
712 }
713
714 return false;
715 }
716
717 /**
718 * Check a course can be auto complete by an enrolled student.
719 *
720 * @since 2.4.0
721 *
722 * @param int $course_id course id.
723 * @param int $user_id user id.
724 *
725 * @return boolean
726 */
727 public static function can_autocomplete_course( $course_id, $user_id ) {
728 $auto_course_complete_option = (bool) tutor_utils()->get_option( 'auto_course_complete_on_all_lesson_completion' );
729 if ( ! $auto_course_complete_option ) {
730 return false;
731 }
732
733 $is_course_completed = tutor_utils()->is_completed_course( $course_id, $user_id );
734 if ( $is_course_completed ) {
735 return false;
736 }
737
738 $course_stats = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
739 if ( $course_stats['total_count'] && $course_stats['completed_count'] === $course_stats['total_count'] ) {
740 return self::can_complete_course( $course_id, $user_id );
741 } else {
742 return false;
743 }
744 }
745
746 /**
747 * Get review progress link when course progress 100% and
748 * User has pending or fail quiz or assignment
749 *
750 * @since 2.4.0
751 *
752 * @param int $course_id course id.
753 * @param int $user_id user id.
754 *
755 * @return string course content permalink.
756 */
757 public static function get_review_progress_link( $course_id, $user_id ) {
758 $course_progress = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
759 $completed_percent = (int) $course_progress['completed_percent'];
760 $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
761 $permalink = '';
762
763 if ( tutor_utils()->count( $course_contents ) && 100 === $completed_percent ) {
764 foreach ( $course_contents as $content ) {
765 if ( 'tutor_quiz' === $content->post_type ) {
766 $result = QuizModel::get_quiz_result( $content->ID, $user_id );
767 if ( 'pass' !== $result ) {
768 $permalink = get_the_permalink( $content->ID );
769 break;
770 }
771 }
772
773 if ( tutor()->has_pro && 'tutor_assignments' === $content->post_type ) {
774 $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $content->ID, $user_id );
775 if ( 'pass' !== $result ) {
776 $permalink = get_the_permalink( $content->ID );
777 break;
778 }
779 }
780 }
781 }
782
783 // Fallback link.
784 if ( empty( $permalink ) ) {
785 $permalink = tutils()->get_course_first_lesson( $course_id );
786 }
787
788 return $permalink;
789 }
790
791 /**
792 * Get course preview image placeholder
793 *
794 * @since 3.0.0
795 *
796 * @return string
797 */
798 public static function get_course_preview_image_placeholder() {
799 return tutor()->url . 'assets/images/placeholder.svg';
800 }
801
802 /**
803 * Retrieve the courses or course bundles that a given coupon code applies to.
804 *
805 * This function fetches published courses or course bundles from the database
806 * based on the specified type. For each course, it retrieves the course prices
807 * and the course thumbnail URL. If the user has Tutor Pro, it additionally
808 * retrieves the total number of courses in a course bundle.
809 *
810 * @since 3.0.0
811 *
812 * @param string $applies_to The type of items the coupon applies to. Accepts 'specific_courses'
813 * for individual courses or any other value for course bundles.
814 *
815 * @global wpdb $wpdb WordPress database abstraction object.
816 *
817 * @return array An array of course objects. Each course object contains:
818 * - int $id: The ID of the course.
819 * - string $title: The title of the course.
820 * - string $type: The post type of the course (e.g., 'courses', 'course-bundle').
821 * - float $price: The regular price of the course.
822 * - float $sale_price: The sale price of the course.
823 * - string $image: The URL of the course's thumbnail image.
824 * - int|null $total_courses: The total number of courses in the bundle
825 * (only if the user has Tutor Pro and the course type is 'course-bundle').
826 */
827 public function get_coupon_applies_to_courses( string $applies_to ) {
828 global $wpdb;
829
830 $post_type = 'specific_courses' === $applies_to ? 'courses' : 'course-bundle';
831
832 $where = array(
833 'post_status' => 'publish',
834 'post_type' => $post_type,
835 );
836
837 $courses = QueryHelper::get_all( $wpdb->posts, $where, 'ID' );
838
839 if ( tutor()->has_pro ) {
840 $bundle_model = new \TutorPro\CourseBundle\Models\BundleModel();
841 }
842
843 $final_data = array();
844
845 if ( ! empty( $courses ) ) {
846 foreach ( $courses as $course ) {
847 $data = new \stdClass();
848
849 if ( tutor()->has_pro && 'course-bundle' === $course->type ) {
850 $data->total_courses = count( $bundle_model->get_bundle_course_ids( $course->ID ) );
851 }
852
853 $author_name = get_the_author_meta( 'display_name', $course->post_author );
854 $course_prices = tutor_utils()->get_raw_course_price( $course->ID );
855 $data->id = (int) $course->ID;
856 $data->title = $course->post_title;
857 $data->price = $course_prices->regular_price;
858 $data->sale_price = $course_prices->sale_price;
859 $data->image = get_the_post_thumbnail_url( $course->ID );
860 $data->author = $author_name;
861
862 $final_data[] = $data;
863 }
864 }
865
866 return ! empty( $final_data ) ? $final_data : array();
867 }
868
869 /**
870 * Get course instructor IDs.
871 *
872 * @since 3.0.0
873 *
874 * @param int $course_id course id.
875 *
876 * @return array
877 */
878 public static function get_course_instructor_ids( $course_id ) {
879 global $wpdb;
880 $instructor_ids = $wpdb->get_col(
881 $wpdb->prepare(
882 "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key=%s AND meta_value=%s",
883 '_tutor_instructor_course_id',
884 $course_id
885 )
886 );
887
888 return $instructor_ids;
889 }
890 }
891