Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/tutor/models/CourseModel.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
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_question_ids = "'" . implode( "','", $questions_ids ) . "'";
510 + //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
511 + $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id IN({$in_question_ids}) " );
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 +