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