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