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