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