Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/tutor/models/CourseModel.php
Keine Baseline-Datei – Diff nur gegen leer.
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
+