PluginProbe
Tutor LMS – eLearning and online course solution / 2.0.8
Tutor LMS – eLearning and online course solution v2.0.8
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 3.9.15 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 All 191 releases
← All changes | models/CourseModel.php +50 -1652 4.0.02.0.8 View file →
@@ -1,168 +1,49 @@
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 2 namespace Tutor\Models;
12 3
13 -use Tutor\Helpers\DateTimeHelper;
14 -use TUTOR\Icon;
15 -use TUTOR\Course;
16 -use TUTOR\Lesson;
17 -use Tutor\Ecommerce\Tax;
18 -use InvalidArgumentException;
19 -use Tutor\Helpers\QueryHelper;
20 -use TUTOR\User;
21 -use TUTOR_ASSIGNMENTS\Assignments;
22 -
23 4 /**
24 - * CourseModel Class
25 - *
5 + * Class CourseModel
26 6 * @since 2.0.6
27 7 */
28 8 class CourseModel {
29 - /**
30 - * WordPress course type name
31 - *
32 - * @var string
33 - */
34 - const POST_TYPE = 'courses';
35 - const COURSE_CATEGORY = 'course-category';
36 - const COURSE_TAG = 'course-tag';
9 + /**
10 + * WordPress course type name
11 + * @var string
12 + */
13 + const POST_TYPE = 'courses';
37 14
38 - const STATUS_PUBLISH = 'publish';
39 - const STATUS_DRAFT = 'draft';
40 - const STATUS_AUTO_DRAFT = 'auto-draft';
41 - const STATUS_PENDING = 'pending';
42 - const STATUS_PRIVATE = 'private';
43 - const STATUS_FUTURE = 'future';
44 - const STATUS_TRASH = 'trash';
15 + const STATUS_PUBLISH = 'publish';
16 + const STATUS_DRAFT = 'draft';
17 + const STATUS_AUTO_DRAFT = 'auto-draft';
18 + const STATUS_PENDING = 'pending';
45 19
46 20 /**
47 - * Course completion modes
48 - */
49 - const MODE_FLEXIBLE = 'flexible';
50 - const MODE_STRICT = 'strict';
51 -
52 - /**
53 - * Course attachment/downloadable resources meta key
54 - *
55 - * @var string
56 - */
57 - const ATTACHMENT_META_KEY = '_tutor_attachments';
58 -
59 - /**
60 - * Course benefits meta key
61 - *
62 - * @var string
63 - */
64 - const BENEFITS_META_KEY = '_tutor_course_benefits';
65 -
66 - /**
67 - * The constant representing the status when a course is completed.
68 - *
69 - * @since 3.8.1
70 - *
71 - * @var string
72 - */
73 - const COURSE_COMPLETED = 'course_completed';
74 -
75 - /**
76 - * Get available status list.
77 - *
78 - * @since 3.0.0
79 - *
80 - * @return array
81 - */
82 - public static function get_status_list() {
83 - return array(
84 - self::STATUS_DRAFT,
85 - self::STATUS_AUTO_DRAFT,
86 - self::STATUS_PUBLISH,
87 - self::STATUS_PRIVATE,
88 - self::STATUS_FUTURE,
89 - self::STATUS_PENDING,
90 - self::STATUS_TRASH,
91 - );
92 - }
93 -
94 - /**
95 - * Check if a course is available for enrollment or purchase.
96 - *
97 - * @since 4.0.0
98 - *
99 - * @param integer $course_id the course id.
100 - *
101 - * @return bool
102 - */
103 - public static function is_course_accessible( $course_id = 0 ) {
104 - $course = get_post( $course_id );
105 - if ( ! $course || ! is_object( $course ) ) {
106 - return false;
107 - }
108 -
109 - if ( ! in_array( $course->post_type, array( tutor()->course_post_type, tutor()->bundle_post_type ), true ) ) {
110 - return false;
111 - }
112 -
113 - if ( ! in_array( $course->post_status, array( self::STATUS_PUBLISH, self::STATUS_PRIVATE ), true ) ) {
114 - return false;
115 - }
116 -
117 - return true;
118 - }
119 -
120 - /**
121 21 * Course record count
122 22 *
23 + * @return int
24 + *
123 25 * @since 2.0.7
124 - *
125 - * @since 3.6.0 $post_type param added
126 - *
127 - * @param string $status Post status.
128 - * @param string $post_type Post type.
129 - *
130 - * @return int
131 26 */
132 - public static function count( $status = self::STATUS_PUBLISH, $post_type = self::POST_TYPE ) {
133 - $count_obj = wp_count_posts( $post_type );
27 + public static function count( $status = self::STATUS_PUBLISH ) {
28 + $count_obj = wp_count_posts( self::POST_TYPE );
134 29 if ( 'all' === $status ) {
135 30 return array_sum( (array) $count_obj );
136 31 }
137 -
32 +
138 33 return (int) $count_obj->{$status};
139 34 }
140 35
141 36 /**
142 - * Get tutor post types
37 + * Get courses
38 + *
39 + * @param array $excludes exclude course ids
143 40 *
144 - * @since 3.5.0
41 + * @return array|null|object
145 42 *
146 - * @param int|\WP_POST $post the post id or object.
147 - *
148 - * @return bool
149 - */
150 - public static function get_post_types( $post ) {
151 - return apply_filters( 'tutor_check_course_post_type', self::POST_TYPE === get_post_type( $post ), get_post_type( $post ) );
152 - }
153 -
154 - /**
155 - * Get courses
156 - *
157 43 * @since 1.0.0
158 - *
159 - * @param array $excludes exclude course ids.
160 - * @param array $post_status post status array.
161 - *
162 - * @return array|null|object
163 44 */
164 - public static function get_courses( $excludes = array(), $post_status = array( 'publish' ) ) {
45 + public function get_courses( $excludes = array(), $post_status = array( 'publish' ) ) {
165 46 global $wpdb;
166 47
167 48 $excludes = (array) $excludes;
168 49 $exclude_query = '';
@@ -180,9 +61,8 @@
180 61
181 62 $post_status = implode( ',', $post_status );
182 63 $course_post_type = tutor()->course_post_type;
183 64
184 - //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
185 65 $query = $wpdb->get_results(
186 66 $wpdb->prepare(
187 67 "SELECT ID,
188 68 post_author,
@@ -197,198 +77,20 @@
197 77 ",
198 78 $course_post_type
199 79 )
200 80 );
201 - //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
202 81
203 82 return $query;
204 83 }
205 84
206 85 /**
207 - * Get courses using provided args
86 + * Get courses for instructors
87 + *
88 + * @param int $instructor_id Instructor ID
208 89 *
209 - * If user is not admin then it will return only current user's post
210 - *
211 - * @since 3.0.0
212 - *
213 - * @param array $args Args.
214 - *
215 - * @return \WP_Query
216 - */
217 - public static function get_courses_by_args( array $args = array() ) {
218 -
219 - $default_args = array(
220 - 'post_type' => tutor()->course_post_type,
221 - 'posts_per_page' => -1,
222 - 'post_status' => 'publish',
223 - );
224 -
225 - if ( ! current_user_can( 'manage_options' ) ) {
226 - $default_args['author'] = get_current_user_id();
227 - }
228 -
229 - $args = wp_parse_args( $args, apply_filters( 'tutor_get_course_list_filter_args', $default_args ) );
230 -
231 - return new \WP_Query( $args );
232 - }
233 -
234 - /**
235 - * Get course count by instructor
236 - *
237 - * @since 1.0.0
238 - *
239 - * @param int $instructor_id instructor ID.
240 - *
241 - * @return null|string
242 - */
243 - public static function get_course_count_by_instructor( $instructor_id ) {
244 - global $wpdb;
245 -
246 - $course_post_type = tutor()->course_post_type;
247 -
248 - $count = $wpdb->get_var(
249 - $wpdb->prepare(
250 - "SELECT COUNT(ID)
251 - FROM {$wpdb->posts}
252 - INNER JOIN {$wpdb->usermeta}
253 - ON user_id = %d
254 - AND meta_key = %s
255 - AND meta_value = ID
256 - WHERE post_status = %s
257 - AND post_type = %s;
258 - ",
259 - $instructor_id,
260 - '_tutor_instructor_course_id',
261 - 'publish',
262 - $course_post_type
263 - )
264 - );
265 -
266 - return $count;
267 - }
268 -
269 - /**
270 - * Get course by quiz
271 - *
272 - * @since 1.0.0
273 - *
274 - * @param int $quiz_id quiz id.
275 - *
276 - * @return array|bool|null|object|void
277 - */
278 - public static function get_course_by_quiz( $quiz_id ) {
279 - $quiz_id = tutils()->get_post_id( $quiz_id );
280 - $post = get_post( $quiz_id );
281 -
282 - if ( $post ) {
283 - $course = get_post( $post->post_parent );
284 - if ( $course ) {
285 - if ( tutor()->course_post_type !== $course->post_type ) {
286 - $course = get_post( $course->post_parent );
287 - }
288 - return $course;
289 - }
290 - }
291 -
292 - return false;
293 - }
294 -
295 - /**
296 - * Get courses by a instructor
297 - *
298 - * @since 1.0.0
299 - * @since 3.5.0 param $post_types added.
300 - * @since 4.0.0 params $search and $order added.
301 - *
302 - * @param integer $instructor_id instructor id.
303 - * @param array|string $post_status post status.
304 - * @param integer $offset offset.
305 - * @param integer $limit limit.
306 - * @param boolean $count_only count or not.
307 - * @param array $post_types array of post types.
308 - * @param string $search search keyword.
309 - * @param string $order order.
310 - *
311 90 * @return array|null|object
312 - */
313 - public static function get_courses_by_instructor(
314 - $instructor_id = 0,
315 - $post_status = array( 'publish' ),
316 - int $offset = 0,
317 - int $limit = PHP_INT_MAX,
318 - $count_only = false,
319 - $post_types = array(),
320 - $search = '',
321 - $order = 'DESC'
322 - ) {
323 - global $wpdb;
324 - $offset = sanitize_text_field( $offset );
325 - $limit = sanitize_text_field( $limit );
326 - $instructor_id = tutils()->get_user_id( $instructor_id );
327 -
328 - if ( ! count( $post_types ) ) {
329 - $post_types = array( tutor()->course_post_type );
330 - }
331 -
332 - $post_types = QueryHelper::prepare_in_clause( $post_types );
333 -
334 - if ( empty( $post_status ) || 'any' == $post_status ) {
335 - $where_post_status = '';
336 - } else {
337 - ! is_array( $post_status ) ? $post_status = array( $post_status ) : 0;
338 - $statuses = "'" . implode( "','", $post_status ) . "'";
339 - $where_post_status = "AND $wpdb->posts.post_status IN({$statuses}) ";
340 - }
341 -
342 - $search_sql = '';
343 - if ( ! empty( $search ) ) {
344 - $like = '%' . $wpdb->esc_like( $search ) . '%';
345 - $search_sql = $wpdb->prepare(
346 - " AND ( {$wpdb->posts}.post_title LIKE %s OR {$wpdb->posts}.post_excerpt LIKE %s ) ",
347 - $like,
348 - $like
349 - );
350 - }
351 -
352 - $order = strtoupper( $order ) === 'ASC' ? 'ASC' : 'DESC';
353 - $order_sql = " ORDER BY $wpdb->posts.post_date {$order} ";
354 -
355 - $select_col = $count_only ? " COUNT(DISTINCT $wpdb->posts.ID) " : " $wpdb->posts.* ";
356 - $limit_offset = $count_only ? '' : " LIMIT $offset, $limit ";
357 -
358 - //phpcs:disable
359 - $query = $wpdb->prepare(
360 - "SELECT $select_col
361 - FROM $wpdb->posts
362 - LEFT JOIN {$wpdb->usermeta}
363 - ON $wpdb->usermeta.user_id = %d
364 - AND $wpdb->usermeta.meta_key = %s
365 - AND $wpdb->usermeta.meta_value = $wpdb->posts.ID
366 - WHERE 1 = 1 {$where_post_status}
367 - AND $wpdb->posts.post_type IN ({$post_types})
368 - AND ($wpdb->posts.post_author = %d OR $wpdb->usermeta.user_id = %d)
369 - {$search_sql}
370 - {$order_sql} {$limit_offset}",
371 - $instructor_id,
372 - '_tutor_instructor_course_id',
373 - $instructor_id,
374 - $instructor_id
375 - );
376 - //phpcs:enable
377 -
378 - $query = apply_filters( 'modify_get_courses_by_instructor_query', $query, $instructor_id, $where_post_status, $post_types, $limit_offset, $select_col );
379 -
380 - //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
381 - return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query, OBJECT );
382 - }
383 -
384 - /**
385 - * Get courses for instructors
386 91 *
387 92 * @since 1.0.0
388 - *
389 - * @param int $instructor_id Instructor ID.
390 - * @return array|null|object
391 93 */
392 94 public function get_courses_for_instructors( $instructor_id = 0 ) {
393 95 $instructor_id = tutor_utils()->get_user_id( $instructor_id );
394 96 $course_post_type = tutor()->course_post_type;
@@ -404,57 +106,35 @@
404 106
405 107 return $courses;
406 108 }
407 109
408 - /**
409 - * Check a user is main instructor of a course
410 - *
411 - * @since 2.1.6
412 - *
413 - * @param integer $course_id course id.
414 - * @param integer $user_id instructor id ( optional ) default: current user id.
415 - *
416 - * @return boolean
417 - */
418 - public static function is_main_instructor( $course_id, $user_id = 0 ) {
419 - $course = get_post( $course_id );
420 - $user_id = tutor_utils()->get_user_id( $user_id );
110 + /**
111 + * Mark the course as completed
112 + *
113 + * @param int $course_id course id which is completed
114 + * @param int $user_id student id who completed the course
115 + * @return bool
116 + *
117 + * @since 2.0.7
118 + */
119 + public static function mark_course_as_completed( $course_id, $user_id ) {
120 + if ( ! $course_id || ! $user_id ) {
121 + return false;
122 + }
421 123
422 - if ( ! $course || ! self::get_post_types( $course_id ) || $user_id !== (int) $course->post_author ) {
423 - return false;
424 - }
425 -
426 - return true;
427 - }
428 -
429 - /**
430 - * Mark the course as completed
431 - *
432 - * @since 2.0.7
433 - *
434 - * @param int $course_id course id which is completed.
435 - * @param int $user_id student id who completed the course.
436 - *
437 - * @return bool
438 - */
439 - public static function mark_course_as_completed( $course_id, $user_id ) {
440 - if ( ! $course_id || ! $user_id ) {
441 - return false;
442 - }
443 -
444 - do_action( 'tutor_course_complete_before', $course_id );
445 -
446 - /**
447 - * Marking course completed at Comment.
124 + do_action( 'tutor_course_complete_before', $course_id );
125 +
126 + /**
127 + * Marking course completed at Comment
448 128 */
449 129 global $wpdb;
450 130
451 - $date = date( 'Y-m-d H:i:s', tutor_time() ); //phpcs:ignore
131 + $date = date( 'Y-m-d H:i:s', tutor_time() );
452 132
453 - // Making sure that, hash is unique.
133 + // Making sure that, hash is unique
454 134 do {
455 - $hash = substr( md5( wp_generate_password( 32 ) . $date . $course_id . $user_id ), 0, 16 );
456 - $has_hash = (int) $wpdb->get_var(
135 + $hash = substr( md5( wp_generate_password( 32 ) . $date . $course_id . $user_id ), 0, 16 );
136 + $hasHash = (int) $wpdb->get_var(
457 137 $wpdb->prepare(
458 138 "SELECT COUNT(comment_ID) from {$wpdb->comments}
459 139 WHERE comment_agent = 'TutorLMSPlugin' AND comment_type = 'course_completed' AND comment_content = %s ",
460 140 $hash
@@ -460,9 +140,9 @@
460 140 $hash
461 141 )
462 142 );
463 143
464 - } while ( $has_hash > 0 );
144 + } while ( $hasHash > 0 );
465 145
466 146 $data = array(
467 147 'comment_post_ID' => $course_id,
468 148 'comment_author' => $user_id,
@@ -467,9 +147,9 @@
467 147 'comment_post_ID' => $course_id,
468 148 'comment_author' => $user_id,
469 149 'comment_date' => $date,
470 150 'comment_date_gmt' => get_gmt_from_date( $date ),
471 - 'comment_content' => $hash, // Identification Hash.
151 + 'comment_content' => $hash, // Identification Hash
472 152 'comment_approved' => 'approved',
473 153 'comment_agent' => 'TutorLMSPlugin',
474 154 'comment_type' => 'course_completed',
475 155 'user_id' => $user_id,
@@ -477,1290 +157,8 @@
477 157
478 158 $wpdb->insert( $wpdb->comments, $data );
479 159
480 160 do_action( 'tutor_course_complete_after', $course_id, $user_id );
481 -
482 - return true;
483 - }
484 -
485 - /**
486 - * Delete a course by ID
487 - *
488 - * @since 2.0.9
489 - *
490 - * @param int $post_id course id that need to delete.
491 - * @return bool
492 - */
493 - public static function delete_course( $post_id ) {
494 - if ( ! self::get_post_types( $post_id ) ) {
495 - return false;
496 - }
497 -
498 - wp_delete_post( $post_id, true );
499 - return true;
500 - }
501 -
502 - /**
503 - * Get post ids by post type and parent_id
504 - *
505 - * @since 1.6.6
506 - *
507 - * @param string $post_type post type.
508 - * @param integer $post_parent post parent ID.
509 - *
510 - * @return array
511 - */
512 - private function get_post_ids( $post_type, $post_parent ) {
513 - $args = array(
514 - 'fields' => 'ids',
515 - 'post_type' => $post_type,
516 - 'post_parent' => $post_parent,
517 - 'post_status' => 'any',
518 - 'posts_per_page' => -1,
519 - );
520 - return get_posts( $args );
521 - }
522 -
523 - /**
524 - * Delete course data when permanently deleting a course.
525 - *
526 - * @since 1.6.6
527 - * @since 2.0.9 updated
528 - *
529 - * @param integer $post_id post ID.
530 - * @return bool
531 - */
532 - public function delete_course_data( $post_id ) {
533 - $course_post_type = tutor()->course_post_type;
534 - if ( get_post_type( $post_id ) !== $course_post_type ) {
535 - return false;
536 - }
537 -
538 - do_action( 'tutor_before_delete_course_content', $post_id, 0 );
539 -
540 - global $wpdb;
541 -
542 - $lesson_post_type = tutor()->lesson_post_type;
543 - $assignment_post_type = tutor()->assignment_post_type;
544 - $quiz_post_type = tutor()->quiz_post_type;
545 -
546 - $topic_ids = $this->get_post_ids( 'topics', $post_id );
547 -
548 - // Course > Topic > ( Lesson | Quiz | Assignment ).
549 - if ( ! empty( $topic_ids ) ) {
550 - foreach ( $topic_ids as $topic_id ) {
551 - $content_post_type = array( $lesson_post_type, $assignment_post_type, $quiz_post_type );
552 - $topic_content_ids = $this->get_post_ids( $content_post_type, $topic_id );
553 -
554 - foreach ( $topic_content_ids as $content_id ) {
555 - /**
556 - * Delete Quiz data
557 - */
558 - if ( get_post_type( $content_id ) === $quiz_post_type ) {
559 - // Collect file paths from all question types that store files before deleting rows (files deleted after DB for safety).
560 - $attempts_for_quiz = QueryHelper::get_all( 'tutor_quiz_attempts', array( 'quiz_id' => $content_id ), 'attempt_id', -1 );
561 - $attempt_file_paths = array();
562 - if ( ! empty( $attempts_for_quiz ) ) {
563 - $attempt_ids = array_map(
564 - function ( $row ) {
565 - return (int) $row->attempt_id;
566 - },
567 - $attempts_for_quiz
568 - );
569 - $attempt_file_paths = apply_filters( 'tutor_quiz/attempt_file_paths_for_deletion', array(), $attempt_ids );
570 - $attempt_file_paths = is_array( $attempt_file_paths ) ? array_values( array_filter( array_unique( $attempt_file_paths ) ) ) : array();
571 - }
572 -
573 - $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $content_id ) );
574 - $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $content_id ) );
575 -
576 - QuizModel::delete_files_by_paths( $attempt_file_paths );
577 -
578 - do_action( 'tutor_before_delete_quiz_content', $content_id, null );
579 -
580 - // Collect instructor file paths before deleting question data (e.g. draw_image / pin_image masks).
581 - /**
582 - * Filter to get file paths for quiz deletion.
583 - * Pro and other add-ons register their question types via this filter.
584 - *
585 - * @param string[] $file_paths Paths collected so far.
586 - * @param int $quiz_id Quiz post ID.
587 - */
588 - $quiz_file_paths = apply_filters( 'tutor_quiz_quiz_file_paths_for_deletion', array(), $content_id );
589 -
590 - $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $content_id ) );
591 - if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
592 - $in_clause = QueryHelper::prepare_in_clause( $questions_ids );
593 - //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
594 - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id IN({$in_clause}) " ) );
595 - }
596 - $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $content_id ) );
597 -
598 - QuizModel::delete_files_by_paths( $quiz_file_paths );
599 - }
600 -
601 - /**
602 - * Delete assignment data ( Assignments, Assignment Submit, Assignment Evalutation )
603 - *
604 - * @since 2.0.9
605 - */
606 - if ( get_post_type( $content_id ) === $assignment_post_type ) {
607 - QueryHelper::delete_comment_with_meta(
608 - array(
609 - 'comment_type' => 'tutor_assignment',
610 - 'comment_post_ID' => $content_id,
611 - )
612 - );
613 - }
614 -
615 - wp_delete_post( $content_id, true );
616 -
617 - }
618 -
619 - // Delete zoom meeting.
620 - $wpdb->delete(
621 - $wpdb->posts,
622 - array(
623 - 'post_parent' => $topic_id,
624 - 'post_type' => 'tutor_zoom_meeting',
625 - )
626 - );
627 -
628 - /**
629 - * Delete Google Meet Record Related to Course Topic
630 - *
631 - * @since 2.1.0
632 - */
633 - $wpdb->delete(
634 - $wpdb->posts,
635 - array(
636 - 'post_parent' => $topic_id,
637 - 'post_type' => 'tutor-google-meet',
638 - )
639 - );
640 -
641 - wp_delete_post( $topic_id, true );
642 - }
643 - }
644 -
645 - $child_post_ids = $this->get_post_ids( array( 'tutor_announcements', 'tutor_enrolled', 'tutor_zoom_meeting', 'tutor-google-meet' ), $post_id );
646 - if ( ! empty( $child_post_ids ) ) {
647 - foreach ( $child_post_ids as $child_post_id ) {
648 - wp_delete_post( $child_post_id, true );
649 - }
650 - }
651 -
652 - /**
653 - * Delete earning, gradebook result, course complete data
654 - *
655 - * @since 2.0.9
656 - */
657 - $wpdb->delete( $wpdb->prefix . 'tutor_earnings', array( 'course_id' => $post_id ) );
658 - $wpdb->delete( $wpdb->prefix . 'tutor_gradebooks_results', array( 'course_id' => $post_id ) );
659 - $wpdb->delete(
660 - $wpdb->comments,
661 - array(
662 - 'comment_type' => 'course_completed',
663 - 'comment_post_ID' => $post_id,
664 - )
665 - );
666 -
667 - /**
668 - * Delete onsite notification record & _tutor_instructor_course_id user meta
669 - *
670 - * @since 2.1.0
671 - */
672 - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_notifications WHERE post_id=%d AND type IN ('Announcements','Q&A','Enrollments')", $post_id ) );
673 - $wpdb->delete(
674 - $wpdb->usermeta,
675 - array(
676 - 'meta_key' => '_tutor_instructor_course_id',
677 - 'meta_value' => $post_id,
678 - )
679 - );
680 -
681 - /**
682 - * Delete Course rating and review
683 - *
684 - * @since 2.0.9
685 - */
686 - QueryHelper::delete_comment_with_meta(
687 - array(
688 - 'comment_type' => 'tutor_course_rating',
689 - 'comment_post_ID' => $post_id,
690 - )
691 - );
692 -
693 - /**
694 - * Delete Q&A and its status ( read, replied etc )
695 - *
696 - * @since 2.0.9
697 - */
698 - QueryHelper::delete_comment_with_meta(
699 - array(
700 - 'comment_type' => 'tutor_q_and_a',
701 - 'comment_post_ID' => $post_id,
702 - )
703 - );
704 -
705 - /**
706 - * Delete caches
707 - */
708 - $attempt_cache = new \Tutor\Cache\QuizAttempts();
709 - if ( $attempt_cache->has_cache() ) {
710 - $attempt_cache->delete_cache();
711 - }
712 -
713 - return true;
714 - }
715 -
716 -
717 - /**
718 - * Get paid courses
719 - *
720 - * To identify course is connected with any product
721 - * like WC Product or EDD product meta key will be used
722 - *
723 - * @since 2.2.0
724 - *
725 - * @since 3.0.0
726 - *
727 - * Meta key removed and default meta query updated
728 - *
729 - * @since 3.0.1
730 - * Course::COURSE_PRICE_META meta key exists clause added
731 - *
732 - * @param array $args wp_query args.
733 - *
734 - * @return \WP_Query
735 - */
736 - public static function get_paid_courses( array $args = array() ) {
737 - $current_user = wp_get_current_user();
738 -
739 - $default_args = array(
740 - 'post_type' => tutor()->course_post_type,
741 - 'posts_per_page' => -1,
742 - 'offset' => 0,
743 - 'post_status' => 'publish',
744 - 'meta_query' => array(
745 - 'relation' => 'AND',
746 - array(
747 - 'key' => Course::COURSE_PRICE_TYPE_META,
748 - 'value' => Course::PRICE_TYPE_SUBSCRIPTION,
749 - 'compare' => '!=',
750 - ),
751 - array(
752 - 'key' => Course::COURSE_PRICE_META,
753 - 'compare' => 'EXISTS',
754 - ),
755 - ),
756 - );
757 -
758 - // Check if the current user is an admin.
759 - if ( ! current_user_can( 'administrator' ) ) {
760 - $args['author'] = $current_user->ID;
761 - }
762 -
763 - $args = wp_parse_args( $args, $default_args );
764 - return new \WP_Query( $args );
765 - }
766 -
767 - /**
768 - * Check the course is completeable or not
769 - *
770 - * @since 2.4.0
771 - *
772 - * @param int $course_id course id.
773 - * @param int $user_id user id.
774 - *
775 - * @return boolean
776 - */
777 - public static function can_complete_course( $course_id, $user_id ) {
778 - $mode = tutor_utils()->get_option( 'course_completion_process' );
779 - if ( self::MODE_FLEXIBLE === $mode ) {
780 - return true;
781 - }
782 -
783 - if ( self::MODE_STRICT === $mode ) {
784 - $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course( $course_id, $user_id );
785 - $lesson_count = tutor_utils()->get_lesson_count_by_course( $course_id, $user_id );
786 -
787 - if ( $completed_lesson < $lesson_count ) {
788 - return false;
789 - }
790 -
791 - $quizzes = array();
792 - $assignments = array();
793 -
794 - $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
795 - if ( tutor_utils()->count( $course_contents ) ) {
796 - foreach ( $course_contents as $content ) {
797 - if ( 'tutor_quiz' === $content->post_type ) {
798 - $quizzes[] = $content;
799 - }
800 - if ( 'tutor_assignments' === $content->post_type ) {
801 - $assignments[] = $content;
802 - }
803 - }
804 - }
805 -
806 - foreach ( $quizzes as $row ) {
807 - $result = QuizModel::get_quiz_result( $row->ID );
808 - if ( 'pass' !== $result ) {
809 - return false;
810 - }
811 - }
812 -
813 - if ( tutor()->has_pro ) {
814 - foreach ( $assignments as $row ) {
815 - $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $row->ID, $user_id );
816 - if ( 'pass' !== $result ) {
817 - return false;
818 - }
819 - }
820 - }
821 -
822 - return true;
823 - }
824 -
825 - return false;
826 - }
827 -
828 - /**
829 - * Check a course can be auto complete by an enrolled student.
830 - *
831 - * @since 2.4.0
832 - *
833 - * @param int $course_id course id.
834 - * @param int $user_id user id.
835 - *
836 - * @return boolean
837 - */
838 - public static function can_autocomplete_course( $course_id, $user_id ) {
839 - $auto_course_complete_option = (bool) tutor_utils()->get_option( 'auto_course_complete_on_all_lesson_completion' );
840 - if ( ! $auto_course_complete_option ) {
841 - return false;
842 - }
843 -
844 - $is_course_completed = tutor_utils()->is_completed_course( $course_id, $user_id );
845 - if ( $is_course_completed ) {
846 - return false;
847 - }
848 -
849 - $course_stats = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
850 - if ( $course_stats['total_count'] && $course_stats['completed_count'] === $course_stats['total_count'] ) {
851 - return self::can_complete_course( $course_id, $user_id );
852 - } else {
853 - return false;
854 - }
855 - }
856 -
857 - /**
858 - * Get review progress link when course progress 100% and
859 - * User has pending or fail quiz or assignment
860 - *
861 - * @since 2.4.0
862 - *
863 - * @param int $course_id course id.
864 - * @param int $user_id user id.
865 - *
866 - * @return string course content permalink.
867 - */
868 - public static function get_review_progress_link( $course_id, $user_id ) {
869 - $course_progress = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
870 - $completed_percent = (int) $course_progress['completed_percent'];
871 - $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
872 - $permalink = '';
873 -
874 - if ( tutor_utils()->count( $course_contents ) && 100 === $completed_percent ) {
875 - foreach ( $course_contents as $content ) {
876 - if ( 'tutor_quiz' === $content->post_type ) {
877 - $result = QuizModel::get_quiz_result( $content->ID, $user_id );
878 - if ( 'pass' !== $result ) {
879 - $permalink = get_the_permalink( $content->ID );
880 - break;
881 - }
882 - }
883 -
884 - if ( tutor()->has_pro && 'tutor_assignments' === $content->post_type ) {
885 - $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $content->ID, $user_id );
886 - if ( 'pass' !== $result ) {
887 - $permalink = get_the_permalink( $content->ID );
888 - break;
889 - }
890 - }
891 - }
892 - }
893 -
894 - // Fallback link.
895 - if ( empty( $permalink ) ) {
896 - $permalink = tutils()->get_course_first_lesson( $course_id );
897 - }
898 -
899 - return $permalink;
900 - }
901 -
902 - /**
903 - * Get course preview image placeholder
904 - *
905 - * @since 3.0.0
906 - *
907 - * @return string
908 - */
909 - public static function get_course_preview_image_placeholder() {
910 - return tutor()->url . 'assets/images/placeholder.svg';
911 - }
912 -
913 - /**
914 - * Retrieve the courses or course bundles that a given coupon code applies to.
915 - *
916 - * This function fetches published courses or course bundles from the database
917 - * based on the specified type. For each course, it retrieves the course prices
918 - * and the course thumbnail URL. If the user has Tutor Pro, it additionally
919 - * retrieves the total number of courses in a course bundle.
920 - *
921 - * @since 3.0.0
922 - *
923 - * @param string $applies_to The type of items the coupon applies to. Accepts 'specific_courses'
924 - * for individual courses or any other value for course bundles.
925 - *
926 - * @global wpdb $wpdb WordPress database abstraction object.
927 - *
928 - * @return array An array of course objects. Each course object contains:
929 - * - int $id: The ID of the course.
930 - * - string $title: The title of the course.
931 - * - string $type: The post type of the course (e.g., 'courses', 'course-bundle').
932 - * - float $price: The regular price of the course.
933 - * - float $sale_price: The sale price of the course.
934 - * - string $image: The URL of the course's thumbnail image.
935 - * - int|null $total_courses: The total number of courses in the bundle
936 - * (only if the user has Tutor Pro and the course type is 'course-bundle').
937 - */
938 - public function get_coupon_applies_to_courses( string $applies_to ) {
939 - global $wpdb;
940 -
941 - $post_type = 'specific_courses' === $applies_to ? 'courses' : 'course-bundle';
942 -
943 - $where = array(
944 - 'post_status' => 'publish',
945 - 'post_type' => $post_type,
946 - );
947 -
948 - $courses = QueryHelper::get_all( $wpdb->posts, $where, 'ID' );
949 -
950 - if ( tutor()->has_pro ) {
951 - $bundle_model = new \TutorPro\CourseBundle\Models\BundleModel();
952 - }
953 -
954 - $final_data = array();
955 -
956 - if ( ! empty( $courses ) ) {
957 - foreach ( $courses as $course ) {
958 - $data = new \stdClass();
959 -
960 - if ( tutor()->has_pro && 'course-bundle' === $course->type ) {
961 - $data->total_courses = count( $bundle_model->get_bundle_course_ids( $course->ID ) );
962 - }
963 -
964 - $author_name = get_the_author_meta( 'display_name', $course->post_author );
965 - $course_prices = tutor_utils()->get_raw_course_price( $course->ID );
966 - $data->id = (int) $course->ID;
967 - $data->title = $course->post_title;
968 - $data->price = $course_prices->regular_price;
969 - $data->sale_price = $course_prices->sale_price;
970 - $data->image = get_the_post_thumbnail_url( $course->ID );
971 - $data->author = $author_name;
972 -
973 - $final_data[] = $data;
974 - }
975 - }
976 -
977 - return ! empty( $final_data ) ? $final_data : array();
978 - }
979 -
980 - /**
981 - * Get course instructor IDs.
982 - *
983 - * @since 3.0.0
984 - *
985 - * @param int $course_id course id.
986 - *
987 - * @return array
988 - */
989 - public static function get_course_instructor_ids( $course_id ) {
990 - global $wpdb;
991 - $instructor_ids = $wpdb->get_col(
992 - $wpdb->prepare(
993 - "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key=%s AND meta_value=%s",
994 - '_tutor_instructor_course_id',
995 - $course_id
996 - )
997 - );
998 -
999 - return $instructor_ids;
1000 - }
1001 -
1002 - /**
1003 - * Check tax collection is enabled for single purchase course/bundle
1004 - *
1005 - * @since 3.7.0
1006 - *
1007 - * @param int $post_id course or bundle id.
1008 - *
1009 - * @return boolean
1010 - */
1011 - public static function is_tax_enabled_for_single_purchase( $post_id ) {
1012 - if ( ! Tax::is_individual_control_enabled() ) {
1013 - return true;
1014 - }
1015 -
1016 - $data = get_post_meta( $post_id, Course::TAX_ON_SINGLE_META, true );
1017 - return ( '1' === $data || '' === $data );
1018 - }
1019 -
1020 - /**
1021 - * Count total attachments available in all courses or specific
1022 - *
1023 - * @since 3.6.0
1024 - *
1025 - * @param int|array $course_id Course id or array of ids, to get course's attachment.
1026 - *
1027 - * @return int
1028 - */
1029 - public static function count_attachment( $course_id = 0 ) {
1030 - global $wpdb;
1031 -
1032 - $total_count = 0;
1033 -
1034 - $primary_table = "$wpdb->posts p";
1035 - $joining_tables = array(
1036 - array(
1037 - 'type' => 'INNER',
1038 - 'table' => "{$wpdb->postmeta} pm",
1039 - 'on' => "p.ID = pm.post_id AND pm.meta_key = '_tutor_attachments' AND pm.meta_value != 'a:0:{}'",
1040 - ),
1041 - );
1042 -
1043 - // Prepare query.
1044 - $select = array( 'pm.meta_value' );
1045 - $where = array();
1046 - if ( $course_id ) {
1047 - $where['p.ID'] = is_array( $course_id ) ? array( 'IN', $course_id ) : $course_id;
1048 - }
1049 -
1050 - $search = array();
1051 - $limit = 0; // Get all.
1052 - $offset = 0;
1053 - $order_by = '';
1054 -
1055 - $results = QueryHelper::get_joined_data(
1056 - $primary_table,
1057 - $joining_tables,
1058 - $select,
1059 - $where,
1060 - $search,
1061 - $order_by,
1062 - $limit,
1063 - $offset
1064 - )['results'];
1065 -
1066 - if ( $results ) {
1067 - foreach ( $results as $row ) {
1068 - $attachment_ids = maybe_unserialize( $row->meta_value );
1069 - if ( ! is_array( $attachment_ids ) || empty( $attachment_ids ) ) {
1070 - continue;
1071 - }
1072 -
1073 - $attachments = get_posts(
1074 - array(
1075 - 'post_type' => 'attachment',
1076 - 'post__in' => $attachment_ids,
1077 - 'posts_per_page' => -1,
1078 - )
1079 - );
1080 -
1081 - $total_count += count( $attachments );
1082 - }
1083 - }
1084 -
1085 - return $total_count;
1086 - }
1087 -
1088 - /**
1089 - * Count total questions available in all or specific courses
1090 - *
1091 - * @since 3.7.1
1092 - *
1093 - * @param int|array $course_id Course id or array of ids, to get course's attachment.
1094 - *
1095 - * @return int
1096 - */
1097 - public static function count_questions( $course_id = 0 ) {
1098 - global $wpdb;
1099 -
1100 - $total_count = 0;
1101 - $quiz_post_type = tutor()->quiz_post_type;
1102 - $topic_post_type = tutor()->topics_post_type;
1103 - $course_post_type = tutor()->course_post_type;
1104 -
1105 - $primary_table = "{$wpdb->prefix}tutor_quiz_questions question";
1106 -
1107 - $joining_tables = array(
1108 - array(
1109 - 'type' => 'INNER',
1110 - 'table' => "{$wpdb->posts} q",
1111 - 'on' => "q.ID = question.quiz_id AND q.post_type='{$quiz_post_type}'",
1112 - ),
1113 - array(
1114 - 'type' => 'INNER',
1115 - 'table' => "{$wpdb->posts} t",
1116 - 'on' => "q.post_parent = t.ID AND t.post_type='{$topic_post_type}'",
1117 - ),
1118 - array(
1119 - 'type' => 'INNER',
1120 - 'table' => "{$wpdb->posts} c",
1121 - 'on' => "c.ID = t.post_parent AND c.post_type='{$course_post_type}'",
1122 - ),
1123 - );
1124 -
1125 - // Prepare query.
1126 - $where = array();
1127 - if ( $course_id ) {
1128 - $where['c.ID'] = is_array( $course_id ) ? array( 'IN', $course_id ) : $course_id;
1129 - }
1130 -
1131 - $search = array();
1132 -
1133 - $total_count = QueryHelper::get_joined_count(
1134 - $primary_table,
1135 - $joining_tables,
1136 - $where,
1137 - $search,
1138 - '*'
1139 - );
1140 -
1141 - return $total_count;
1142 - }
1143 -
1144 - /**
1145 - * Count course content
1146 - *
1147 - * @since 3.6.0
1148 - *
1149 - * @param string $content_type Content type.
1150 - * @param array $course_ids Course ids.
1151 - *
1152 - * @return int
1153 - */
1154 - public static function count_course_content( string $content_type, array $course_ids = array() ): int {
1155 - $total_count = 0;
1156 - switch ( $content_type ) {
1157 - case tutor()->lesson_post_type:
1158 - $total_count = tutor_utils()->get_total_lesson( $course_ids );
1159 - break;
1160 - case tutor()->quiz_post_type:
1161 - $total_count = tutor_utils()->get_total_quiz( $course_ids );
1162 - break;
1163 - case tutor()->assignment_post_type:
1164 - if ( tutor_utils()->is_addon_enabled( 'tutor-assignments' ) ) {
1165 - $total_count = ( new Assignments( false ) )->get_total_assignment( $course_ids );
1166 - }
1167 - break;
1168 - case 'attachments':
1169 - $total_count = self::count_attachment( $course_ids );
1170 - break;
1171 - case 'questions':
1172 - $total_count = self::count_questions( $course_ids );
1173 - break;
1174 - default:
1175 - break;
1176 - }
1177 -
1178 - return (int) $total_count;
1179 - }
1180 -
1181 - /**
1182 - * Get course dropdown options
1183 - *
1184 - * @since 3.7.0
1185 - *
1186 - * @return array
1187 - */
1188 - public static function get_course_dropdown_options() {
1189 - $course_options = array(
1190 - array(
1191 - 'key' => '',
1192 - 'title' => __( 'All Courses', 'tutor' ),
1193 - ),
1194 - );
1195 -
1196 - $courses = current_user_can( 'administrator' ) ? self::get_courses() : self::get_courses_by_instructor();
1197 - if ( ! empty( $courses ) ) {
1198 - foreach ( $courses as $course ) {
1199 - $course_options[] = array(
1200 - 'key' => $course->ID,
1201 - 'title' => $course->post_title,
1202 - );
1203 - }
1204 - }
1205 -
1206 - return apply_filters( 'tutor_course_dropdown_options', $course_options );
1207 - }
1208 -
1209 - /**
1210 - * Get category dropdown options
1211 - *
1212 - * @since 3.7.0
1213 - *
1214 - * @return array
1215 - */
1216 - public static function get_category_dropdown_options() {
1217 - $category_options = array(
1218 - array(
1219 - 'key' => '',
1220 - 'title' => __( 'All Categories', 'tutor' ),
1221 - ),
1222 - );
1223 -
1224 - $categories = get_terms(
1225 - array(
1226 - 'taxonomy' => self::COURSE_CATEGORY,
1227 - 'orderby' => 'term_id',
1228 - 'order' => 'DESC',
1229 - )
1230 - );
1231 - if ( ! is_wp_error( $categories ) && ! empty( $categories ) ) {
1232 - foreach ( $categories as $category ) {
1233 - $category_options[] = array(
1234 - 'key' => $category->slug,
1235 - 'title' => $category->name,
1236 - );
1237 - }
1238 - }
1239 -
1240 - return $category_options;
1241 - }
1242 -
1243 - /**
1244 - * Retrieve all course completion records for a specific course, including meta data.
1245 - *
1246 - * @since 3.8.1
1247 - *
1248 - * @param int $course_id The ID of the course.
1249 - *
1250 - * @return array Array of course completion objects with meta, empty array if none found, or on database error.
1251 - */
1252 - public function get_course_completion_data_by_course_id( int $course_id ): array {
1253 -
1254 - global $wpdb;
1255 -
1256 - $where = array(
1257 - 'comment_type' => self::COURSE_COMPLETED,
1258 - 'comment_post_ID' => $course_id,
1259 - 'comment_agent' => 'TutorLMSPlugin',
1260 - );
1261 -
1262 - $result = QueryHelper::get_all( $wpdb->comments, $where, 'comment_post_ID', -1 );
1263 -
1264 - if ( empty( $result ) ) {
1265 - return array();
1266 - }
1267 -
1268 - return array_map(
1269 - function ( $item ) {
1270 -
1271 - return array(
1272 - 'completion' => $item,
1273 - 'completion_meta' => get_comment_meta( $item->comment_ID ),
1274 - );
1275 - },
1276 - $result
1277 - );
1278 - }
1279 -
1280 - /**
1281 - * Return completed courses by user_id
1282 - *
1283 - * @since 1.0.0
1284 - *
1285 - * @param int $user_id user id.
1286 - * @param int $offset offset.
1287 - * @param int $posts_per_page posts per page.
1288 - * @param array $args Args to override the defaults.
1289 - *
1290 - * @return bool|\WP_Query
1291 - */
1292 - public static function get_completed_courses_by_user( $user_id = 0, $offset = 0, $posts_per_page = -1, $args = array() ) {
1293 - $user_id = tutor_utils()->get_user_id( $user_id );
1294 - $course_ids = tutor_utils()->get_completed_courses_ids_by_user( $user_id, false );
1295 -
1296 - if ( count( $course_ids ) ) {
1297 - $course_post_type = tutor()->course_post_type;
1298 - $course_args = array(
1299 - 'post_type' => $course_post_type,
1300 - 'post_status' => 'publish',
1301 - 'post__in' => $course_ids,
1302 - 'posts_per_page' => $posts_per_page,
1303 - 'offset' => $offset,
1304 - 'orderby' => 'post__in',
1305 - );
1306 -
1307 - $args = apply_filters( 'tutor_get_completed_courses_by_user', $args, $user_id, $course_post_type );
1308 - $course_args = wp_parse_args( $args, $course_args );
1309 -
1310 - return new \WP_Query( $course_args );
1311 - }
1312 -
1313 - return false;
1314 - }
1315 -
1316 - /**
1317 - * Get the active course by user
1318 - *
1319 - * @since 1.0.0
1320 - *
1321 - * @param int $user_id user id.
1322 - * @param int $offset offset.
1323 - * @param int $posts_per_page posts per page.
1324 - * @param array $args Args to override the defaults.
1325 - *
1326 - * @return bool|\WP_Query
1327 - */
1328 - public static function get_active_courses_by_user( $user_id = 0, $offset = 0, $posts_per_page = -1, $args = array() ) {
1329 - $user_id = tutor_utils()->get_user_id( $user_id );
1330 - $course_ids = tutor_utils()->get_completed_courses_ids_by_user( $user_id, false );
1331 - $enrolled_course_ids = tutor_utils()->get_enrolled_courses_ids_by_user( $user_id, false );
1332 - $active_courses = array_diff( $enrolled_course_ids, $course_ids );
1333 -
1334 - if ( count( $active_courses ) ) {
1335 - $course_post_type = tutor()->course_post_type;
1336 - $course_args = array(
1337 - 'post_type' => apply_filters( 'tutor_active_courses_post_types', array( $course_post_type ) ),
1338 - 'post_status' => 'publish',
1339 - 'post__in' => $active_courses,
1340 - 'posts_per_page' => $posts_per_page,
1341 - 'offset' => $offset,
1342 - 'orderby' => 'post__in',
1343 - );
1344 -
1345 - $args = apply_filters( 'tutor_get_active_courses_by_user', $args, $user_id, $course_post_type );
1346 - $course_args = wp_parse_args( $args, $course_args );
1347 -
1348 - return new \WP_Query( $course_args );
1349 - }
1350 -
1351 - return false;
1352 - }
1353 -
1354 - /**
1355 - * Get the enrolled courses by user
1356 - *
1357 - * @since 1.0.0
1358 - * @since 2.5.0 $filters param added to query enrolled courses with additional filters.
1359 - *
1360 - * @since 3.4.0 $filters replaced with $args to override the defaults.
1361 - *
1362 - * @param integer $user_id user id.
1363 - * @param mixed $post_status post status.
1364 - * @param integer $offset offset.
1365 - * @param integer $posts_per_page post per page.
1366 - * @param array $args Args to override the defaults.
1367 - *
1368 - * @return bool|\WP_Query
1369 - */
1370 - public static function get_enrolled_courses_by_user( $user_id = 0, $post_status = 'publish', $offset = 0, $posts_per_page = -1, $args = array() ) {
1371 - $user_id = tutor_utils()->get_user_id( $user_id );
1372 - $course_ids = array_unique( tutor_utils()->get_enrolled_courses_ids_by_user( $user_id, false ) );
1373 -
1374 - if ( count( $course_ids ) ) {
1375 - $course_post_type = tutor()->course_post_type;
1376 - $course_args = array(
1377 - 'post_type' => $course_post_type,
1378 - 'post_status' => $post_status,
1379 - 'post__in' => $course_ids,
1380 - 'offset' => $offset,
1381 - 'posts_per_page' => $posts_per_page,
1382 - 'orderby' => 'post__in',
1383 - );
1384 -
1385 - $args = apply_filters( 'tutor_get_enrolled_courses_by_user', $args, $user_id, $course_post_type );
1386 - $course_args = wp_parse_args( $args, $course_args );
1387 -
1388 - $result = new \WP_Query( $course_args );
1389 -
1390 - if ( is_object( $result ) && is_array( $result->posts ) ) {
1391 -
1392 - // Sort courses according to the id list.
1393 - $new_array = array();
1394 -
1395 - foreach ( $course_ids as $id ) {
1396 - foreach ( $result->posts as $post ) {
1397 - $post->ID == $id ? $new_array[] = $post : 0;
1398 - }
1399 - }
1400 -
1401 - $result->posts = $new_array;
1402 - }
1403 -
1404 - return $result;
1405 - }
1406 -
1407 - return false;
1408 - }
1409 -
1410 - /**
1411 - * Get the number of courses created by an instructor within a given date range.
1412 - *
1413 - * @since 4.0.0
1414 - *
1415 - * If no date range is provided, returns the total course count for the instructor.
1416 - * Course creation date is determined using the `_wp_old_date` meta value when available; otherwise, the course
1417 - * post date is used.
1418 - *
1419 - * @param string|null $start_date Start date in Y-m-d format.
1420 - * @param string|null $end_date End date in Y-m-d format.
1421 - * @param int $user_id Course author (user) ID.
1422 - *
1423 - * @return int Total number of matching courses.
1424 - */
1425 - public static function get_course_count_by_date( $start_date, $end_date, $user_id ) {
1426 -
1427 - $user_id = tutor_utils()->get_user_id( $user_id );
1428 -
1429 - if ( empty( $start_date ) && empty( $end_date ) ) {
1430 - return (int) self::get_courses_by_instructor( $user_id, array( 'publish', 'private' ), 0, PHP_INT_MAX, true );
1431 - }
1432 -
1433 - $courses = self::get_courses_by_instructor( $user_id, array( 'publish', 'private' ), 0, PHP_INT_MAX );
1434 -
1435 - if ( empty( $courses ) ) {
1436 - return 0;
1437 - }
1438 -
1439 - $start_date_timestamp = strtotime( $start_date . ' 00:00:00' );
1440 - $end_date_timestamp = strtotime( $end_date . ' 23:59:59' );
1441 -
1442 - $filtered = array_filter(
1443 - $courses,
1444 - function ( $course ) use ( $start_date_timestamp, $end_date_timestamp ) {
1445 - $creation_date = get_post_meta( $course->ID, '_wp_old_date', true );
1446 -
1447 - $course_creation_date = empty( $creation_date ) ? strtotime( $course->post_date_gmt ) : strtotime( $creation_date );
1448 -
1449 - return $start_date_timestamp <= $course_creation_date && $end_date_timestamp >= $course_creation_date;
1450 - }
1451 - );
1452 -
1453 - return count( $filtered );
1454 - }
1455 -
1456 - /**
1457 - * Get topic-wise progress data for a course and a student.
1458 - *
1459 - * @since 4.0.0
1460 - *
1461 - * @param int $course_id Course ID.
1462 - * @param int $student_id Student ID.
1463 - *
1464 - * @return array[] Topic progress data.
1465 - */
1466 - public static function get_course_progress_details( $course_id, $student_id ) {
1467 -
1468 - $topics_query = tutor_utils()->get_topics( $course_id );
1469 - $topic_list = array();
1470 -
1471 - if ( empty( $topics_query ) || ! $topics_query->have_posts() ) {
1472 - return $topic_list;
1473 - }
1474 -
1475 - foreach ( $topics_query->posts as $topic_post ) {
1476 - $topic_id = (int) $topic_post->ID;
1477 - $items = array();
1478 - $completion_percentage = tutor_utils()->count_completed_contents_by_topic( $topic_id, $student_id );
1479 - $contents_query = tutor_utils()->get_course_contents_by_topic( $topic_id, -1 );
1480 -
1481 - if ( ! empty( $contents_query ) && $contents_query->have_posts() ) {
1482 - foreach ( $contents_query->posts as $content_post ) {
1483 - $items[] = ( new self() )->build_course_progress_item( $content_post, $student_id, $course_id );
1484 - }
1485 - }
1486 -
1487 - $topic_list[] = array(
1488 - 'id' => $topic_id,
1489 - 'summary' => $topic_post->post_content,
1490 - 'title' => get_the_title( $topic_id ),
1491 - 'items' => $items,
1492 - 'completion_percentage' => $completion_percentage['percentage'] ?? 0,
1493 - );
1494 - }
1495 -
1496 - wp_reset_postdata();
1497 -
1498 - return $topic_list;
1499 - }
1500 -
1501 - /**
1502 - * Check if the current user has course content access
1503 - *
1504 - * @since 4.0.0
1505 - *
1506 - * @throws InvalidArgumentException If args are invalid.
1507 - *
1508 - * @param array $args Array of arguments.
1509 - *
1510 - * `$defaults = array(
1511 - * 'current_post_type' => '',
1512 - * 'current_post_id' => 0,
1513 - * 'course_id' => 0,
1514 - * 'is_public' => false,
1515 - * 'is_enrolled' => false,
1516 - * );`.
1517 - *
1518 - * @return bool
1519 - */
1520 - public static function has_course_content_access( array $args = array() ): bool {
1521 - if ( User::is_admin() ) {
1522 - return true;
1523 - }
1524 -
1525 - $defaults = array(
1526 - 'current_post_type' => '',
1527 - 'current_post_id' => 0,
1528 - 'course_id' => 0,
1529 - 'is_public' => false,
1530 - 'is_enrolled' => false,
1531 - );
1532 -
1533 - $args = wp_parse_args( $args, $defaults );
1534 -
1535 - if ( ! $args['course_id'] || ! $args['current_post_id'] || ! $args['current_post_type'] ) {
1536 - throw new InvalidArgumentException( __( 'Invalid argument passed', 'tutor' ) );
1537 - }
1538 -
1539 - $has_access = false;
1540 - $user_id = get_current_user_id();
1541 - switch ( $args['current_post_type'] ) {
1542 - case tutor()->lesson_post_type:
1543 - $is_preview = (int) get_post_meta( $args['current_post_id'], Lesson::PREVIEW_META_KEY, true );
1544 - if ( $args['is_public'] || $is_preview ) {
1545 - $has_access = true;
1546 - } elseif ( $args['is_enrolled'] || tutor_utils()->has_enrolled_content_access( 'lesson' ) ) {
1547 - $has_access = true;
1548 - }
1549 - break;
1550 - case tutor()->quiz_post_type:
1551 - case tutor()->assignment_post_type:
1552 - if ( $user_id ) {
1553 - $content_type = tutor()->quiz_post_type === $args['current_post_type'] ? 'quiz' : 'assignment';
1554 - if ( $args['is_enrolled'] || $args['is_public'] || tutor_utils()->has_enrolled_content_access( $content_type ) ) {
1555 - $has_access = true;
1556 - }
1557 - }
1558 - break;
1559 - default:
1560 - if ( $args['is_enrolled'] ) {
1561 - $has_access = true;
1562 - }
1563 - break;
1564 - }
1565 -
1566 - return apply_filters( 'tutor_course_content_access', $has_access, $args );
1567 - }
1568 -
1569 - /**
1570 - * Build course progress item data for a specific lesson, quiz, assignment, or meeting.
1571 - *
1572 - * @since 4.0.0
1573 - *
1574 - * @param \WP_Post $post The course content post object.
1575 - * @param int $user_id The user ID for whom progress is being calculated.
1576 - * @param int $course_id The course ID to which the content belongs.
1577 - *
1578 - * @return array Associative array of progress item data
1579 - */
1580 - private function build_course_progress_item( \WP_Post $post, $user_id, $course_id ) {
1581 -
1582 - $base_items = array(
1583 - 'id' => $post->ID,
1584 - 'type' => $post->post_type,
1585 - 'link' => esc_url_raw( get_permalink( $post->ID ) ),
1586 - 'title' => $post->post_title,
1587 - );
1588 -
1589 - switch ( $post->post_type ) {
1590 - case tutor()->quiz_post_type:
1591 - $quiz_status = '';
1592 - $icon_name = Icon::QUIZ_2;
1593 - $icon_class = 'tutor-text-subdued';
1594 - $last_attempt = ( new QuizModel() )->get_first_or_last_attempt( $post->ID, $user_id );
1595 -
1596 - if ( is_object( $last_attempt ) && QuizModel::ATTEMPT_STARTED !== $last_attempt->attempt_status ) {
1597 - $quiz_status = QuizModel::get_quiz_result( $post->ID, $user_id );
1598 -
1599 - $quiz_icon_map = array(
1600 - QuizModel::RESULT_FAIL => array( Icon::CROSS_COLORIZE, 'tutor-icon-critical' ),
1601 - QuizModel::RESULT_PENDING => array( Icon::INFO_COLORIZE, 'tutor-icon-warning-secondary' ),
1602 - );
1603 -
1604 - if ( isset( $quiz_icon_map[ $quiz_status ] ) ) {
1605 - list( $icon_name, $icon_class ) = $quiz_icon_map[ $quiz_status ];
1606 - }
1607 - }
1608 -
1609 - return array_merge(
1610 - $base_items,
1611 - array(
1612 - 'is_completed' => QuizModel::RESULT_PASS === $quiz_status,
1613 - 'label' => __( 'Quiz', 'tutor' ),
1614 - 'icon' => $icon_name,
1615 - 'icon_class' => $icon_class,
1616 - )
1617 - );
1618 -
1619 - case tutor()->assignment_post_type:
1620 - $status = '';
1621 -
1622 - $is_submitted = tutor_utils()->is_assignment_submitted( $post->ID, $user_id );
1623 - $status = $is_submitted ? Assignments::get_assignment_result( $post->ID, $user_id ) : $status;
1624 -
1625 - if ( ! $is_submitted && Assignments::is_expired( $post->ID, $user_id, $course_id ) ) {
1626 - $status = 'fail';
1627 - }
1628 -
1629 - $icon = Icon::BOOK_2;
1630 - $icon_class = 'tutor-text-subdued';
1631 -
1632 - $status_map = array(
1633 - 'pending' => array( Icon::INFO_COLORIZE, 'tutor-icon-warning-secondary' ),
1634 - 'fail' => array( Icon::CROSS_COLORIZE, 'tutor-icon-critical' ),
1635 - );
1636 -
1637 - if ( isset( $status_map[ $status ] ) ) {
1638 - list( $icon, $icon_class ) = $status_map[ $status ];
1639 - }
1640 -
1641 - return array_merge(
1642 - $base_items,
1643 - array(
1644 - 'is_completed' => 'pass' === $status,
1645 - 'label' => __( 'Assignment', 'tutor' ),
1646 - 'icon' => $icon,
1647 - 'icon_class' => $icon_class,
1648 - )
1649 - );
1650 -
1651 - case tutor()->zoom_post_type:
1652 - if ( ! tutor_utils()->is_addon_enabled( 'tutor-zoom' ) ) {
1653 - return $base_items;
1654 - }
1655 -
1656 - return array_merge(
1657 - $base_items,
1658 - array(
1659 - 'label' => __( 'Zoom', 'tutor' ),
1660 - 'icon' => Icon::ZOOM,
1661 - 'is_completed' => \TUTOR_ZOOM\Zoom::is_zoom_lesson_done( '', $post->ID, $user_id ),
1662 - )
1663 - );
1664 -
1665 - case tutor()->meet_post_type:
1666 - if ( ! tutor_utils()->is_addon_enabled( 'google-meet' ) ) {
1667 - return $base_items;
1668 - }
1669 -
1670 - return array_merge(
1671 - $base_items,
1672 - array(
1673 - 'label' => __( 'Google Meet', 'tutor' ),
1674 - 'icon' => Icon::GOOGLE_MEET,
1675 - 'is_completed' => \TutorPro\GoogleMeet\Frontend\Frontend::is_lesson_completed( false, $post->ID, $user_id ),
1676 - )
1677 - );
1678 -
1679 - default:
1680 - $video = tutor_utils()->get_video_info( $post->ID );
1681 -
1682 - return array_merge(
1683 - $base_items,
1684 - array(
1685 - 'video' => $video,
1686 - 'video_play_time' => isset( $video->playtime ) ? $video->playtime : '',
1687 - 'is_completed' => (bool) tutor_utils()->is_completed_lesson( $post->ID, $user_id ),
1688 - 'label' => empty( $video ) ? __( 'Reading', 'tutor' ) : __( 'Video', 'tutor' ),
1689 - 'icon' => empty( $video ) ? Icon::COURSES : Icon::VIDEO_CAMERA,
1690 - )
1691 - );
1692 - }
1693 - }
1694 -
1695 - /**
1696 - * Calculate the total course duration for a set of courses and returns the total time in hours, minutes, and
1697 - * seconds.
1698 - *
1699 - * @since 4.0.0
1700 - *
1701 - * @param array<int> | int $course_ids List of course IDs or a single course ID.
1702 - *
1703 - * @return array{
1704 - * hours: int,
1705 - * minutes: int,
1706 - * seconds: int
1707 - * } Total accumulated duration from all given courses.
1708 - */
1709 - public static function get_total_course_duration( $course_ids ): array {
1710 - $total_seconds = 0;
1711 - $course_ids = is_array( $course_ids ) ? $course_ids : array( $course_ids );
1712 - foreach ( $course_ids as $id ) {
1713 - $total_seconds += self::get_course_duration_in_seconds( $id );
1714 - }
1715 -
1716 - return DateTimeHelper::split_seconds_into_time_units( $total_seconds );
1717 - }
1718 -
1719 - /**
1720 - * Calculate the estimated time spent on courses based on completion progress.
1721 - *
1722 - * @since 4.0.0
1723 - *
1724 - * @param array<int> $course_ids List of course IDs.
1725 - *
1726 - * @return array{
1727 - * hours: int,
1728 - * minutes: int,
1729 - * seconds: int
1730 - * } Total accumulated duration from all given courses.
1731 - */
1732 - public static function get_total_estimated_time_spent( $course_ids ): array {
1733 - $total_seconds = 0;
1734 - foreach ( $course_ids as $id ) {
1735 - $completion_percentage = tutor_utils()->is_completed_course( $id )
1736 - ? 100
1737 - : (int) tutor_utils()->get_course_completed_percent( (int) $id );
1738 -
1739 - if ( 0 === $completion_percentage ) {
1740 - continue;
1741 - }
1742 - $course_duration_in_seconds = self::get_course_duration_in_seconds( $id );
1743 - // Calculate the time spent by a user based on the course completion percentage.
1744 - $total_seconds += (int) round( $course_duration_in_seconds * ( $completion_percentage / 100 ) );
1745 - }
1746 -
1747 - return DateTimeHelper::split_seconds_into_time_units( $total_seconds );
1748 - }
1749 -
1750 - /**
1751 - * Get the total duration of a course in seconds.
1752 - *
1753 - * @since 4.0.0
1754 - *
1755 - * @param int $course_id Course ID.
1756 - *
1757 - * @return int Course duration in seconds.
1758 - */
1759 - public static function get_course_duration_in_seconds( int $course_id ): int {
1760 - $duration = tutor_utils()->get_course_duration( $course_id, true );
1761 -
1762 - return ( $duration['durationHours'] * HOUR_IN_SECONDS )
1763 - + ( $duration['durationMinutes'] * MINUTE_IN_SECONDS )
1764 - + ( $duration['durationSeconds'] );
1765 - }
1766 -}
161 +
162 + return true;
163 + }
164 +}