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