PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.7.0
Tutor LMS – eLearning and online course solution v3.7.0
3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / models / CourseModel.php
tutor / models Last commit date
BaseModel.php 11 months ago BillingModel.php 1 year ago CartModel.php 1 year ago CouponModel.php 11 months ago CourseModel.php 11 months ago LessonModel.php 11 months ago OrderActivitiesModel.php 1 year ago OrderMetaModel.php 1 year ago OrderModel.php 11 months ago QuizModel.php 11 months ago UserModel.php 1 year ago WithdrawModel.php 1 year ago
CourseModel.php
1094 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 //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
320 return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query, OBJECT );
321 }
322
323 /**
324 * Get courses for instructors
325 *
326 * @since 1.0.0
327 *
328 * @param int $instructor_id Instructor ID.
329 * @return array|null|object
330 */
331 public function get_courses_for_instructors( $instructor_id = 0 ) {
332 $instructor_id = tutor_utils()->get_user_id( $instructor_id );
333 $course_post_type = tutor()->course_post_type;
334
335 $courses = get_posts(
336 array(
337 'post_type' => $course_post_type,
338 'author' => $instructor_id,
339 'post_status' => array( 'publish', 'pending' ),
340 'posts_per_page' => 5,
341 )
342 );
343
344 return $courses;
345 }
346
347 /**
348 * Check a user is main instructor of a course
349 *
350 * @since 2.1.6
351 *
352 * @param integer $course_id course id.
353 * @param integer $user_id instructor id ( optional ) default: current user id.
354 *
355 * @return boolean
356 */
357 public static function is_main_instructor( $course_id, $user_id = 0 ) {
358 $course = get_post( $course_id );
359 $user_id = tutor_utils()->get_user_id( $user_id );
360
361 if ( ! $course || ! self::get_post_types( $course_id ) || $user_id !== (int) $course->post_author ) {
362 return false;
363 }
364
365 return true;
366 }
367
368 /**
369 * Mark the course as completed
370 *
371 * @since 2.0.7
372 *
373 * @param int $course_id course id which is completed.
374 * @param int $user_id student id who completed the course.
375 *
376 * @return bool
377 */
378 public static function mark_course_as_completed( $course_id, $user_id ) {
379 if ( ! $course_id || ! $user_id ) {
380 return false;
381 }
382
383 do_action( 'tutor_course_complete_before', $course_id );
384
385 /**
386 * Marking course completed at Comment.
387 */
388 global $wpdb;
389
390 $date = date( 'Y-m-d H:i:s', tutor_time() ); //phpcs:ignore
391
392 // Making sure that, hash is unique.
393 do {
394 $hash = substr( md5( wp_generate_password( 32 ) . $date . $course_id . $user_id ), 0, 16 );
395 $has_hash = (int) $wpdb->get_var(
396 $wpdb->prepare(
397 "SELECT COUNT(comment_ID) from {$wpdb->comments}
398 WHERE comment_agent = 'TutorLMSPlugin' AND comment_type = 'course_completed' AND comment_content = %s ",
399 $hash
400 )
401 );
402
403 } while ( $has_hash > 0 );
404
405 $data = array(
406 'comment_post_ID' => $course_id,
407 'comment_author' => $user_id,
408 'comment_date' => $date,
409 'comment_date_gmt' => get_gmt_from_date( $date ),
410 'comment_content' => $hash, // Identification Hash.
411 'comment_approved' => 'approved',
412 'comment_agent' => 'TutorLMSPlugin',
413 'comment_type' => 'course_completed',
414 'user_id' => $user_id,
415 );
416
417 $wpdb->insert( $wpdb->comments, $data );
418
419 do_action( 'tutor_course_complete_after', $course_id, $user_id );
420
421 return true;
422 }
423
424 /**
425 * Delete a course by ID
426 *
427 * @since 2.0.9
428 *
429 * @param int $post_id course id that need to delete.
430 * @return bool
431 */
432 public static function delete_course( $post_id ) {
433 if ( ! self::get_post_types( $post_id ) ) {
434 return false;
435 }
436
437 wp_delete_post( $post_id, true );
438 return true;
439 }
440
441 /**
442 * Get post ids by post type and parent_id
443 *
444 * @since 1.6.6
445 *
446 * @param string $post_type post type.
447 * @param integer $post_parent post parent ID.
448 *
449 * @return array
450 */
451 private function get_post_ids( $post_type, $post_parent ) {
452 $args = array(
453 'fields' => 'ids',
454 'post_type' => $post_type,
455 'post_parent' => $post_parent,
456 'post_status' => 'any',
457 'posts_per_page' => -1,
458 );
459 return get_posts( $args );
460 }
461
462 /**
463 * Delete course data when permanently deleting a course.
464 *
465 * @since 1.6.6
466 * @since 2.0.9 updated
467 *
468 * @param integer $post_id post ID.
469 * @return bool
470 */
471 public function delete_course_data( $post_id ) {
472 $course_post_type = tutor()->course_post_type;
473 if ( get_post_type( $post_id ) !== $course_post_type ) {
474 return false;
475 }
476
477 do_action( 'tutor_before_delete_course_content', $post_id, 0 );
478
479 global $wpdb;
480
481 $lesson_post_type = tutor()->lesson_post_type;
482 $assignment_post_type = tutor()->assignment_post_type;
483 $quiz_post_type = tutor()->quiz_post_type;
484
485 $topic_ids = $this->get_post_ids( 'topics', $post_id );
486
487 // Course > Topic > ( Lesson | Quiz | Assignment ).
488 if ( ! empty( $topic_ids ) ) {
489 foreach ( $topic_ids as $topic_id ) {
490 $content_post_type = array( $lesson_post_type, $assignment_post_type, $quiz_post_type );
491 $topic_content_ids = $this->get_post_ids( $content_post_type, $topic_id );
492
493 foreach ( $topic_content_ids as $content_id ) {
494 /**
495 * Delete Quiz data
496 */
497 if ( get_post_type( $content_id ) === 'tutor_quiz' ) {
498 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $content_id ) );
499 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $content_id ) );
500
501 do_action( 'tutor_before_delete_quiz_content', $content_id, null );
502
503 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $content_id ) );
504 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
505 $in_question_ids = "'" . implode( "','", $questions_ids ) . "'";
506 //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
507 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id IN({$in_question_ids}) " );
508 }
509 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $content_id ) );
510 }
511
512 /**
513 * Delete assignment data ( Assignments, Assignment Submit, Assignment Evalutation )
514 *
515 * @since 2.0.9
516 */
517 if ( get_post_type( $content_id ) === $assignment_post_type ) {
518 QueryHelper::delete_comment_with_meta(
519 array(
520 'comment_type' => 'tutor_assignment',
521 'comment_post_ID' => $content_id,
522 )
523 );
524 }
525
526 wp_delete_post( $content_id, true );
527
528 }
529
530 // Delete zoom meeting.
531 $wpdb->delete(
532 $wpdb->posts,
533 array(
534 'post_parent' => $topic_id,
535 'post_type' => 'tutor_zoom_meeting',
536 )
537 );
538
539 /**
540 * Delete Google Meet Record Related to Course Topic
541 *
542 * @since 2.1.0
543 */
544 $wpdb->delete(
545 $wpdb->posts,
546 array(
547 'post_parent' => $topic_id,
548 'post_type' => 'tutor-google-meet',
549 )
550 );
551
552 wp_delete_post( $topic_id, true );
553 }
554 }
555
556 $child_post_ids = $this->get_post_ids( array( 'tutor_announcements', 'tutor_enrolled', 'tutor_zoom_meeting', 'tutor-google-meet' ), $post_id );
557 if ( ! empty( $child_post_ids ) ) {
558 foreach ( $child_post_ids as $child_post_id ) {
559 wp_delete_post( $child_post_id, true );
560 }
561 }
562
563 /**
564 * Delete earning, gradebook result, course complete data
565 *
566 * @since 2.0.9
567 */
568 $wpdb->delete( $wpdb->prefix . 'tutor_earnings', array( 'course_id' => $post_id ) );
569 $wpdb->delete( $wpdb->prefix . 'tutor_gradebooks_results', array( 'course_id' => $post_id ) );
570 $wpdb->delete(
571 $wpdb->comments,
572 array(
573 'comment_type' => 'course_completed',
574 'comment_post_ID' => $post_id,
575 )
576 );
577
578 /**
579 * Delete onsite notification record & _tutor_instructor_course_id user meta
580 *
581 * @since 2.1.0
582 */
583 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_notifications WHERE post_id=%d AND type IN ('Announcements','Q&A','Enrollments')", $post_id ) );
584 $wpdb->delete(
585 $wpdb->usermeta,
586 array(
587 'meta_key' => '_tutor_instructor_course_id',
588 'meta_value' => $post_id,
589 )
590 );
591
592 /**
593 * Delete Course rating and review
594 *
595 * @since 2.0.9
596 */
597 QueryHelper::delete_comment_with_meta(
598 array(
599 'comment_type' => 'tutor_course_rating',
600 'comment_post_ID' => $post_id,
601 )
602 );
603
604 /**
605 * Delete Q&A and its status ( read, replied etc )
606 *
607 * @since 2.0.9
608 */
609 QueryHelper::delete_comment_with_meta(
610 array(
611 'comment_type' => 'tutor_q_and_a',
612 'comment_post_ID' => $post_id,
613 )
614 );
615
616 /**
617 * Delete caches
618 */
619 $attempt_cache = new \Tutor\Cache\QuizAttempts();
620 if ( $attempt_cache->has_cache() ) {
621 $attempt_cache->delete_cache();
622 }
623
624 return true;
625 }
626
627
628 /**
629 * Get paid courses
630 *
631 * To identify course is connected with any product
632 * like WC Product or EDD product meta key will be used
633 *
634 * @since 2.2.0
635 *
636 * @since 3.0.0
637 *
638 * Meta key removed and default meta query updated
639 *
640 * @since 3.0.1
641 * Course::COURSE_PRICE_META meta key exists clause added
642 *
643 * @param array $args wp_query args.
644 *
645 * @return \WP_Query
646 */
647 public static function get_paid_courses( array $args = array() ) {
648 $current_user = wp_get_current_user();
649
650 $default_args = array(
651 'post_type' => tutor()->course_post_type,
652 'posts_per_page' => -1,
653 'offset' => 0,
654 'post_status' => 'publish',
655 'meta_query' => array(
656 'relation' => 'AND',
657 array(
658 'key' => Course::COURSE_PRICE_TYPE_META,
659 'value' => Course::PRICE_TYPE_SUBSCRIPTION,
660 'compare' => '!=',
661 ),
662 array(
663 'key' => Course::COURSE_PRICE_META,
664 'compare' => 'EXISTS',
665 ),
666 ),
667 );
668
669 // Check if the current user is an admin.
670 if ( ! current_user_can( 'administrator' ) ) {
671 $args['author'] = $current_user->ID;
672 }
673
674 $args = wp_parse_args( $args, $default_args );
675 return new \WP_Query( $args );
676
677 }
678
679 /**
680 * Check the course is completeable or not
681 *
682 * @since 2.4.0
683 *
684 * @param int $course_id course id.
685 * @param int $user_id user id.
686 *
687 * @return boolean
688 */
689 public static function can_complete_course( $course_id, $user_id ) {
690 $mode = tutor_utils()->get_option( 'course_completion_process' );
691 if ( self::MODE_FLEXIBLE === $mode ) {
692 return true;
693 }
694
695 if ( self::MODE_STRICT === $mode ) {
696 $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course( $course_id, $user_id );
697 $lesson_count = tutor_utils()->get_lesson_count_by_course( $course_id, $user_id );
698
699 if ( $completed_lesson < $lesson_count ) {
700 return false;
701 }
702
703 $quizzes = array();
704 $assignments = array();
705
706 $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
707 if ( tutor_utils()->count( $course_contents ) ) {
708 foreach ( $course_contents as $content ) {
709 if ( 'tutor_quiz' === $content->post_type ) {
710 $quizzes[] = $content;
711 }
712 if ( 'tutor_assignments' === $content->post_type ) {
713 $assignments[] = $content;
714 }
715 }
716 }
717
718 foreach ( $quizzes as $row ) {
719 $result = QuizModel::get_quiz_result( $row->ID );
720 if ( 'pass' !== $result ) {
721 return false;
722 }
723 }
724
725 if ( tutor()->has_pro ) {
726 foreach ( $assignments as $row ) {
727 $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $row->ID, $user_id );
728 if ( 'pass' !== $result ) {
729 return false;
730 }
731 }
732 }
733
734 return true;
735 }
736
737 return false;
738 }
739
740 /**
741 * Check a course can be auto complete by an enrolled student.
742 *
743 * @since 2.4.0
744 *
745 * @param int $course_id course id.
746 * @param int $user_id user id.
747 *
748 * @return boolean
749 */
750 public static function can_autocomplete_course( $course_id, $user_id ) {
751 $auto_course_complete_option = (bool) tutor_utils()->get_option( 'auto_course_complete_on_all_lesson_completion' );
752 if ( ! $auto_course_complete_option ) {
753 return false;
754 }
755
756 $is_course_completed = tutor_utils()->is_completed_course( $course_id, $user_id );
757 if ( $is_course_completed ) {
758 return false;
759 }
760
761 $course_stats = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
762 if ( $course_stats['total_count'] && $course_stats['completed_count'] === $course_stats['total_count'] ) {
763 return self::can_complete_course( $course_id, $user_id );
764 } else {
765 return false;
766 }
767 }
768
769 /**
770 * Get review progress link when course progress 100% and
771 * User has pending or fail quiz or assignment
772 *
773 * @since 2.4.0
774 *
775 * @param int $course_id course id.
776 * @param int $user_id user id.
777 *
778 * @return string course content permalink.
779 */
780 public static function get_review_progress_link( $course_id, $user_id ) {
781 $course_progress = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
782 $completed_percent = (int) $course_progress['completed_percent'];
783 $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
784 $permalink = '';
785
786 if ( tutor_utils()->count( $course_contents ) && 100 === $completed_percent ) {
787 foreach ( $course_contents as $content ) {
788 if ( 'tutor_quiz' === $content->post_type ) {
789 $result = QuizModel::get_quiz_result( $content->ID, $user_id );
790 if ( 'pass' !== $result ) {
791 $permalink = get_the_permalink( $content->ID );
792 break;
793 }
794 }
795
796 if ( tutor()->has_pro && 'tutor_assignments' === $content->post_type ) {
797 $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $content->ID, $user_id );
798 if ( 'pass' !== $result ) {
799 $permalink = get_the_permalink( $content->ID );
800 break;
801 }
802 }
803 }
804 }
805
806 // Fallback link.
807 if ( empty( $permalink ) ) {
808 $permalink = tutils()->get_course_first_lesson( $course_id );
809 }
810
811 return $permalink;
812 }
813
814 /**
815 * Get course preview image placeholder
816 *
817 * @since 3.0.0
818 *
819 * @return string
820 */
821 public static function get_course_preview_image_placeholder() {
822 return tutor()->url . 'assets/images/placeholder.svg';
823 }
824
825 /**
826 * Retrieve the courses or course bundles that a given coupon code applies to.
827 *
828 * This function fetches published courses or course bundles from the database
829 * based on the specified type. For each course, it retrieves the course prices
830 * and the course thumbnail URL. If the user has Tutor Pro, it additionally
831 * retrieves the total number of courses in a course bundle.
832 *
833 * @since 3.0.0
834 *
835 * @param string $applies_to The type of items the coupon applies to. Accepts 'specific_courses'
836 * for individual courses or any other value for course bundles.
837 *
838 * @global wpdb $wpdb WordPress database abstraction object.
839 *
840 * @return array An array of course objects. Each course object contains:
841 * - int $id: The ID of the course.
842 * - string $title: The title of the course.
843 * - string $type: The post type of the course (e.g., 'courses', 'course-bundle').
844 * - float $price: The regular price of the course.
845 * - float $sale_price: The sale price of the course.
846 * - string $image: The URL of the course's thumbnail image.
847 * - int|null $total_courses: The total number of courses in the bundle
848 * (only if the user has Tutor Pro and the course type is 'course-bundle').
849 */
850 public function get_coupon_applies_to_courses( string $applies_to ) {
851 global $wpdb;
852
853 $post_type = 'specific_courses' === $applies_to ? 'courses' : 'course-bundle';
854
855 $where = array(
856 'post_status' => 'publish',
857 'post_type' => $post_type,
858 );
859
860 $courses = QueryHelper::get_all( $wpdb->posts, $where, 'ID' );
861
862 if ( tutor()->has_pro ) {
863 $bundle_model = new \TutorPro\CourseBundle\Models\BundleModel();
864 }
865
866 $final_data = array();
867
868 if ( ! empty( $courses ) ) {
869 foreach ( $courses as $course ) {
870 $data = new \stdClass();
871
872 if ( tutor()->has_pro && 'course-bundle' === $course->type ) {
873 $data->total_courses = count( $bundle_model->get_bundle_course_ids( $course->ID ) );
874 }
875
876 $author_name = get_the_author_meta( 'display_name', $course->post_author );
877 $course_prices = tutor_utils()->get_raw_course_price( $course->ID );
878 $data->id = (int) $course->ID;
879 $data->title = $course->post_title;
880 $data->price = $course_prices->regular_price;
881 $data->sale_price = $course_prices->sale_price;
882 $data->image = get_the_post_thumbnail_url( $course->ID );
883 $data->author = $author_name;
884
885 $final_data[] = $data;
886 }
887 }
888
889 return ! empty( $final_data ) ? $final_data : array();
890 }
891
892 /**
893 * Get course instructor IDs.
894 *
895 * @since 3.0.0
896 *
897 * @param int $course_id course id.
898 *
899 * @return array
900 */
901 public static function get_course_instructor_ids( $course_id ) {
902 global $wpdb;
903 $instructor_ids = $wpdb->get_col(
904 $wpdb->prepare(
905 "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key=%s AND meta_value=%s",
906 '_tutor_instructor_course_id',
907 $course_id
908 )
909 );
910
911 return $instructor_ids;
912 }
913
914 /**
915 * Check tax collection is enabled for single purchase course/bundle
916 *
917 * @since 3.7.0
918 *
919 * @param int $post_id course or bundle id.
920 *
921 * @return boolean
922 */
923 public static function is_tax_enabled_for_single_purchase( $post_id ) {
924 if ( ! Tax::is_individual_control_enabled() ) {
925 return true;
926 }
927
928 $data = get_post_meta( $post_id, Course::TAX_ON_SINGLE_META, true );
929 return ( '1' === $data || '' === $data );
930 }
931
932 /**
933 * Count total attachments available in all courses or specific
934 *
935 * @since 3.6.0
936 *
937 * @param int $course_id Course id to get only a particular course's attachment.
938 *
939 * @return int
940 */
941 public static function count_attachment( int $course_id = 0 ) {
942 global $wpdb;
943
944 $total_count = 0;
945
946 $primary_table = "$wpdb->posts p";
947 $joining_tables = array(
948 array(
949 'type' => 'INNER',
950 'table' => "{$wpdb->postmeta} pm",
951 'on' => "p.ID = pm.post_id AND pm.meta_key = '_tutor_attachments' AND pm.meta_value != 'a:0:{}'",
952 ),
953 );
954
955 // Prepare query.
956 $select = array( 'pm.meta_value' );
957 $where = array();
958 if ( $course_id ) {
959 $where['p.ID'] = $course_id;
960 }
961 $search = array();
962 $limit = 0; // Get all.
963 $offset = 0;
964 $order_by = '';
965
966 $results = QueryHelper::get_joined_data(
967 $primary_table,
968 $joining_tables,
969 $select,
970 $where,
971 $search,
972 $order_by,
973 $limit,
974 $offset
975 )['results'];
976
977 if ( $results ) {
978 foreach ( $results as $row ) {
979 $attachment_ids = maybe_unserialize( $row->meta_value );
980 if ( ! is_array( $attachment_ids ) || empty( $attachment_ids ) ) {
981 continue;
982 }
983
984 $attachments = get_posts(
985 array(
986 'post_type' => 'attachment',
987 'post__in' => $attachment_ids,
988 'posts_per_page' => -1,
989 )
990 );
991
992 $total_count += count( $attachments );
993 }
994 }
995
996 return $total_count;
997 }
998
999 /**
1000 * Count course content
1001 *
1002 * @since 3.6.0
1003 *
1004 * @param string $content_type Content type.
1005 *
1006 * @return int
1007 */
1008 public static function count_course_content( string $content_type ): int {
1009 $total_count = 0;
1010 switch ( $content_type ) {
1011 case tutor()->lesson_post_type:
1012 $total_count = tutor_utils()->get_total_lesson();
1013 break;
1014 case tutor()->quiz_post_type:
1015 $total_count = tutor_utils()->get_total_quiz();
1016 break;
1017 case tutor()->assignment_post_type:
1018 if ( tutor_utils()->is_addon_enabled( 'tutor-assignments' ) ) {
1019 $total_count = ( new Assignments( false ) )->get_total_assignment();
1020 }
1021 break;
1022 case 'attachments':
1023 $total_count = self::count_attachment();
1024 break;
1025 default:
1026 break;
1027 }
1028
1029 return (int) $total_count;
1030 }
1031
1032 /**
1033 * Get course dropdown options
1034 *
1035 * @since 3.7.0
1036 *
1037 * @return array
1038 */
1039 public static function get_course_dropdown_options() {
1040 $course_options = array(
1041 array(
1042 'key' => '',
1043 'title' => __( 'All Courses', 'tutor' ),
1044 ),
1045 );
1046
1047 $courses = current_user_can( 'administrator' ) ? self::get_courses() : self::get_courses_by_instructor();
1048 if ( ! empty( $courses ) ) {
1049 foreach ( $courses as $course ) {
1050 $course_options[] = array(
1051 'key' => $course->ID,
1052 'title' => $course->post_title,
1053 );
1054 }
1055 }
1056
1057 return $course_options;
1058 }
1059
1060 /**
1061 * Get category dropdown options
1062 *
1063 * @since 3.7.0
1064 *
1065 * @return array
1066 */
1067 public static function get_category_dropdown_options() {
1068 $category_options = array(
1069 array(
1070 'key' => '',
1071 'title' => __( 'All Categories', 'tutor' ),
1072 ),
1073 );
1074
1075 $categories = get_terms(
1076 array(
1077 'taxonomy' => self::COURSE_CATEGORY,
1078 'orderby' => 'term_id',
1079 'order' => 'DESC',
1080 )
1081 );
1082 if ( ! is_wp_error( $categories ) && ! empty( $categories ) ) {
1083 foreach ( $categories as $category ) {
1084 $category_options[] = array(
1085 'key' => $category->slug,
1086 'title' => $category->name,
1087 );
1088 }
1089 }
1090
1091 return $category_options;
1092 }
1093 }
1094