PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 2.7.7
Tutor LMS – eLearning and online course solution v2.7.7
3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / models / CourseModel.php
tutor / models Last commit date
CourseModel.php 2 years ago LessonModel.php 2 years ago QuizModel.php 2 years ago WithdrawModel.php 1 year ago
CourseModel.php
729 lines
1 <?php
2 /**
3 * Course Model
4 *
5 * @package Tutor\Models
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com
8 * @since 2.0.6
9 */
10
11 namespace Tutor\Models;
12
13 use Tutor\Helpers\QueryHelper;
14
15 /**
16 * CourseModel Class
17 *
18 * @since 2.0.6
19 */
20 class CourseModel {
21 /**
22 * WordPress course type name
23 *
24 * @var string
25 */
26 const POST_TYPE = 'courses';
27
28 const STATUS_PUBLISH = 'publish';
29 const STATUS_DRAFT = 'draft';
30 const STATUS_AUTO_DRAFT = 'auto-draft';
31 const STATUS_PENDING = 'pending';
32 const STATUS_PRIVATE = 'private';
33 const STATUS_FUTURE = 'future';
34
35 /**
36 * Course completion modes
37 */
38 const MODE_FLEXIBLE = 'flexible';
39 const MODE_STRICT = 'strict';
40
41 /**
42 * Course mapped with the product using this meta key
43 *
44 * @var string
45 */
46 const WC_PRODUCT_META_KEY = '_tutor_course_product_id';
47
48 /**
49 * Course attachment/downloadable resources meta key
50 *
51 * @var string
52 */
53 const ATTACHMENT_META_KEY = '_tutor_attachments';
54
55 /**
56 * Course benefits meta key
57 *
58 * @var string
59 */
60 const BENEFITS_META_KEY = '_tutor_course_benefits';
61
62 /**
63 * Course record count
64 *
65 * @since 2.0.7
66 *
67 * @param string $status course status.
68 * @return int
69 */
70 public static function count( $status = self::STATUS_PUBLISH ) {
71 $count_obj = wp_count_posts( self::POST_TYPE );
72 if ( 'all' === $status ) {
73 return array_sum( (array) $count_obj );
74 }
75
76 return (int) $count_obj->{$status};
77 }
78
79 /**
80 * Get courses
81 *
82 * @since 1.0.0
83 *
84 * @param array $excludes exclude course ids.
85 * @param array $post_status post status array.
86 *
87 * @return array|null|object
88 */
89 public static function get_courses( $excludes = array(), $post_status = array( 'publish' ) ) {
90 global $wpdb;
91
92 $excludes = (array) $excludes;
93 $exclude_query = '';
94
95 if ( count( $excludes ) ) {
96 $exclude_query = implode( "','", $excludes );
97 }
98
99 $post_status = array_map(
100 function ( $element ) {
101 return "'" . $element . "'";
102 },
103 $post_status
104 );
105
106 $post_status = implode( ',', $post_status );
107 $course_post_type = tutor()->course_post_type;
108
109 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
110 $query = $wpdb->get_results(
111 $wpdb->prepare(
112 "SELECT ID,
113 post_author,
114 post_title,
115 post_name,
116 post_status,
117 menu_order
118 FROM {$wpdb->posts}
119 WHERE post_status IN ({$post_status})
120 AND ID NOT IN('$exclude_query')
121 AND post_type = %s;
122 ",
123 $course_post_type
124 )
125 );
126 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
127
128 return $query;
129 }
130
131 /**
132 * Get course count by instructor
133 *
134 * @since 1.0.0
135 *
136 * @param int $instructor_id instructor ID.
137 *
138 * @return null|string
139 */
140 public static function get_course_count_by_instructor( $instructor_id ) {
141 global $wpdb;
142
143 $course_post_type = tutor()->course_post_type;
144
145 $count = $wpdb->get_var(
146 $wpdb->prepare(
147 "SELECT COUNT(ID)
148 FROM {$wpdb->posts}
149 INNER JOIN {$wpdb->usermeta}
150 ON user_id = %d
151 AND meta_key = %s
152 AND meta_value = ID
153 WHERE post_status = %s
154 AND post_type = %s;
155 ",
156 $instructor_id,
157 '_tutor_instructor_course_id',
158 'publish',
159 $course_post_type
160 )
161 );
162
163 return $count;
164 }
165
166 /**
167 * Get course by quiz
168 *
169 * @since 1.0.0
170 *
171 * @param int $quiz_id quiz id.
172 *
173 * @return array|bool|null|object|void
174 */
175 public static function get_course_by_quiz( $quiz_id ) {
176 $quiz_id = tutils()->get_post_id( $quiz_id );
177 $post = get_post( $quiz_id );
178
179 if ( $post ) {
180 $course = get_post( $post->post_parent );
181 if ( $course ) {
182 if ( tutor()->course_post_type !== $course->post_type ) {
183 $course = get_post( $course->post_parent );
184 }
185 return $course;
186 }
187 }
188
189 return false;
190 }
191
192 /**
193 * Get courses by a instructor
194 *
195 * @since 1.0.0
196 *
197 * @param integer $instructor_id instructor id.
198 * @param array|string $post_status post status.
199 * @param integer $offset offset.
200 * @param integer $limit limit.
201 * @param boolean $count_only count or not.
202 *
203 * @return array|null|object
204 */
205 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 ) {
206 global $wpdb;
207 $offset = sanitize_text_field( $offset );
208 $limit = sanitize_text_field( $limit );
209 $instructor_id = tutils()->get_user_id( $instructor_id );
210 $course_post_type = tutor()->course_post_type;
211
212 if ( empty( $post_status ) || 'any' == $post_status ) {
213 $where_post_status = '';
214 } else {
215 ! is_array( $post_status ) ? $post_status = array( $post_status ) : 0;
216 $statuses = "'" . implode( "','", $post_status ) . "'";
217 $where_post_status = "AND $wpdb->posts.post_status IN({$statuses}) ";
218 }
219
220 $select_col = $count_only ? " COUNT(DISTINCT $wpdb->posts.ID) " : " $wpdb->posts.* ";
221 $limit_offset = $count_only ? '' : " LIMIT $offset, $limit ";
222
223 //phpcs:disable
224 $query = $wpdb->prepare(
225 "SELECT $select_col
226 FROM $wpdb->posts
227 LEFT JOIN {$wpdb->usermeta}
228 ON $wpdb->usermeta.user_id = %d
229 AND $wpdb->usermeta.meta_key = %s
230 AND $wpdb->usermeta.meta_value = $wpdb->posts.ID
231 WHERE 1 = 1 {$where_post_status}
232 AND $wpdb->posts.post_type = %s
233 AND ($wpdb->posts.post_author = %d OR $wpdb->usermeta.user_id = %d)
234 ORDER BY $wpdb->posts.post_date DESC $limit_offset",
235 $instructor_id,
236 '_tutor_instructor_course_id',
237 $course_post_type,
238 $instructor_id,
239 $instructor_id
240 );
241 //phpcs:enable
242
243 //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
244 return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query, OBJECT );
245 }
246
247 /**
248 * Get courses for instructors
249 *
250 * @since 1.0.0
251 *
252 * @param int $instructor_id Instructor ID.
253 * @return array|null|object
254 */
255 public function get_courses_for_instructors( $instructor_id = 0 ) {
256 $instructor_id = tutor_utils()->get_user_id( $instructor_id );
257 $course_post_type = tutor()->course_post_type;
258
259 $courses = get_posts(
260 array(
261 'post_type' => $course_post_type,
262 'author' => $instructor_id,
263 'post_status' => array( 'publish', 'pending' ),
264 'posts_per_page' => 5,
265 )
266 );
267
268 return $courses;
269 }
270
271 /**
272 * Check a user is main instructor of a course
273 *
274 * @since 2.1.6
275 *
276 * @param integer $course_id course id.
277 * @param integer $user_id instructor id ( optional ) default: current user id.
278 *
279 * @return boolean
280 */
281 public static function is_main_instructor( $course_id, $user_id = 0 ) {
282 $course = get_post( $course_id );
283 $user_id = tutor_utils()->get_user_id( $user_id );
284
285 if ( ! $course || self::POST_TYPE !== $course->post_type || $user_id !== (int) $course->post_author ) {
286 return false;
287 }
288
289 return true;
290 }
291
292 /**
293 * Mark the course as completed
294 *
295 * @since 2.0.7
296 *
297 * @param int $course_id course id which is completed.
298 * @param int $user_id student id who completed the course.
299 *
300 * @return bool
301 */
302 public static function mark_course_as_completed( $course_id, $user_id ) {
303 if ( ! $course_id || ! $user_id ) {
304 return false;
305 }
306
307 do_action( 'tutor_course_complete_before', $course_id );
308
309 /**
310 * Marking course completed at Comment.
311 */
312 global $wpdb;
313
314 $date = date( 'Y-m-d H:i:s', tutor_time() ); //phpcs:ignore
315
316 // Making sure that, hash is unique.
317 do {
318 $hash = substr( md5( wp_generate_password( 32 ) . $date . $course_id . $user_id ), 0, 16 );
319 $has_hash = (int) $wpdb->get_var(
320 $wpdb->prepare(
321 "SELECT COUNT(comment_ID) from {$wpdb->comments}
322 WHERE comment_agent = 'TutorLMSPlugin' AND comment_type = 'course_completed' AND comment_content = %s ",
323 $hash
324 )
325 );
326
327 } while ( $has_hash > 0 );
328
329 $data = array(
330 'comment_post_ID' => $course_id,
331 'comment_author' => $user_id,
332 'comment_date' => $date,
333 'comment_date_gmt' => get_gmt_from_date( $date ),
334 'comment_content' => $hash, // Identification Hash.
335 'comment_approved' => 'approved',
336 'comment_agent' => 'TutorLMSPlugin',
337 'comment_type' => 'course_completed',
338 'user_id' => $user_id,
339 );
340
341 $wpdb->insert( $wpdb->comments, $data );
342
343 do_action( 'tutor_course_complete_after', $course_id, $user_id );
344
345 return true;
346 }
347
348 /**
349 * Delete a course by ID
350 *
351 * @since 2.0.9
352 *
353 * @param int $post_id course id that need to delete.
354 * @return bool
355 */
356 public static function delete_course( $post_id ) {
357 if ( get_post_type( $post_id ) !== tutor()->course_post_type ) {
358 return false;
359 }
360
361 wp_delete_post( $post_id, true );
362 return true;
363 }
364
365 /**
366 * Get post ids by post type and parent_id
367 *
368 * @since 1.6.6
369 *
370 * @param string $post_type post type.
371 * @param integer $post_parent post parent ID.
372 *
373 * @return array
374 */
375 private function get_post_ids( $post_type, $post_parent ) {
376 $args = array(
377 'fields' => 'ids',
378 'post_type' => $post_type,
379 'post_parent' => $post_parent,
380 'post_status' => 'any',
381 'posts_per_page' => -1,
382 );
383 return get_posts( $args );
384 }
385
386 /**
387 * Delete course data when permanently deleting a course.
388 *
389 * @since 1.6.6
390 * @since 2.0.9 updated
391 *
392 * @param integer $post_id post ID.
393 * @return bool
394 */
395 public function delete_course_data( $post_id ) {
396 $course_post_type = tutor()->course_post_type;
397 if ( get_post_type( $post_id ) !== $course_post_type ) {
398 return false;
399 }
400
401 global $wpdb;
402
403 $lesson_post_type = tutor()->lesson_post_type;
404 $assignment_post_type = tutor()->assignment_post_type;
405 $quiz_post_type = tutor()->quiz_post_type;
406
407 $topic_ids = $this->get_post_ids( 'topics', $post_id );
408
409 // Course > Topic > ( Lesson | Quiz | Assignment ).
410 if ( ! empty( $topic_ids ) ) {
411 foreach ( $topic_ids as $topic_id ) {
412 $content_post_type = array( $lesson_post_type, $assignment_post_type, $quiz_post_type );
413 $topic_content_ids = $this->get_post_ids( $content_post_type, $topic_id );
414
415 foreach ( $topic_content_ids as $content_id ) {
416 /**
417 * Delete Quiz data
418 */
419 if ( get_post_type( $content_id ) === 'tutor_quiz' ) {
420 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $content_id ) );
421 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $content_id ) );
422
423 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $content_id ) );
424 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
425 $in_question_ids = "'" . implode( "','", $questions_ids ) . "'";
426 //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
427 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id IN({$in_question_ids}) " );
428 }
429 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $content_id ) );
430 }
431
432 /**
433 * Delete assignment data ( Assignments, Assignment Submit, Assignment Evalutation )
434 *
435 * @since 2.0.9
436 */
437 if ( get_post_type( $content_id ) === $assignment_post_type ) {
438 QueryHelper::delete_comment_with_meta(
439 array(
440 'comment_type' => 'tutor_assignment',
441 'comment_post_ID' => $content_id,
442 )
443 );
444 }
445
446 wp_delete_post( $content_id, true );
447
448 }
449
450 // Delete zoom meeting.
451 $wpdb->delete(
452 $wpdb->posts,
453 array(
454 'post_parent' => $topic_id,
455 'post_type' => 'tutor_zoom_meeting',
456 )
457 );
458
459 /**
460 * Delete Google Meet Record Related to Course Topic
461 *
462 * @since 2.1.0
463 */
464 $wpdb->delete(
465 $wpdb->posts,
466 array(
467 'post_parent' => $topic_id,
468 'post_type' => 'tutor-google-meet',
469 )
470 );
471
472 wp_delete_post( $topic_id, true );
473 }
474 }
475
476 $child_post_ids = $this->get_post_ids( array( 'tutor_announcements', 'tutor_enrolled', 'tutor_zoom_meeting', 'tutor-google-meet' ), $post_id );
477 if ( ! empty( $child_post_ids ) ) {
478 foreach ( $child_post_ids as $child_post_id ) {
479 wp_delete_post( $child_post_id, true );
480 }
481 }
482
483 /**
484 * Delete earning, gradebook result, course complete data
485 *
486 * @since 2.0.9
487 */
488 $wpdb->delete( $wpdb->prefix . 'tutor_earnings', array( 'course_id' => $post_id ) );
489 $wpdb->delete( $wpdb->prefix . 'tutor_gradebooks_results', array( 'course_id' => $post_id ) );
490 $wpdb->delete(
491 $wpdb->comments,
492 array(
493 'comment_type' => 'course_completed',
494 'comment_post_ID' => $post_id,
495 )
496 );
497
498 /**
499 * Delete onsite notification record & _tutor_instructor_course_id user meta
500 *
501 * @since 2.1.0
502 */
503 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_notifications WHERE post_id=%d AND type IN ('Announcements','Q&A','Enrollments')", $post_id ) );
504 $wpdb->delete(
505 $wpdb->usermeta,
506 array(
507 'meta_key' => '_tutor_instructor_course_id',
508 'meta_value' => $post_id,
509 )
510 );
511
512 /**
513 * Delete Course rating and review
514 *
515 * @since 2.0.9
516 */
517 QueryHelper::delete_comment_with_meta(
518 array(
519 'comment_type' => 'tutor_course_rating',
520 'comment_post_ID' => $post_id,
521 )
522 );
523
524 /**
525 * Delete Q&A and its status ( read, replied etc )
526 *
527 * @since 2.0.9
528 */
529 QueryHelper::delete_comment_with_meta(
530 array(
531 'comment_type' => 'tutor_q_and_a',
532 'comment_post_ID' => $post_id,
533 )
534 );
535
536 /**
537 * Delete caches
538 */
539 $attempt_cache = new \Tutor\Cache\QuizAttempts();
540 if ( $attempt_cache->has_cache() ) {
541 $attempt_cache->delete_cache();
542 }
543
544 return true;
545 }
546
547
548 /**
549 * Get paid courses
550 *
551 * To identify course is connected with any product
552 * like WC Product or EDD product meta key will be used
553 *
554 * @since 2.2.0
555 *
556 * @param string $meta_key course product id meta key.
557 * @param array $args wp_query args.
558 *
559 * @return array
560 */
561 public static function get_paid_courses( string $meta_key, array $args = array() ): array {
562 $current_user = wp_get_current_user();
563 $default_args = array(
564 'post_type' => 'courses',
565 'post_status' => 'publish',
566 'no_found_rows' => true,
567 'posts_per_page' => -1,
568 'relation' => 'AND',
569 'meta_query' => array(
570 array(
571 'key' => sanitize_text_field( $meta_key ),
572 'value' => 0,
573 'compare' => '!=',
574 'type' => 'NUMERIC',
575 ),
576 ),
577 );
578
579 // Check if the current user is an admin.
580 if ( ! current_user_can( 'administrator' ) ) {
581 $args['author'] = $current_user->ID;
582 }
583
584 $query = new \WP_Query( wp_parse_args( $args, $default_args ) );
585
586 if ( $query->have_posts() ) {
587 return $query->posts;
588 }
589
590 return array();
591 }
592
593 /**
594 * Check the course is completeable or not
595 *
596 * @since 2.4.0
597 *
598 * @param int $course_id course id.
599 * @param int $user_id user id.
600 *
601 * @return boolean
602 */
603 public static function can_complete_course( $course_id, $user_id ) {
604 $mode = tutor_utils()->get_option( 'course_completion_process' );
605 if ( self::MODE_FLEXIBLE === $mode ) {
606 return true;
607 }
608
609 if ( self::MODE_STRICT === $mode ) {
610 $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course( $course_id, $user_id );
611 $lesson_count = tutor_utils()->get_lesson_count_by_course( $course_id, $user_id );
612
613 if ( $completed_lesson < $lesson_count ) {
614 return false;
615 }
616
617 $quizzes = array();
618 $assignments = array();
619
620 $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
621 if ( tutor_utils()->count( $course_contents ) ) {
622 foreach ( $course_contents as $content ) {
623 if ( 'tutor_quiz' === $content->post_type ) {
624 $quizzes[] = $content;
625 }
626 if ( 'tutor_assignments' === $content->post_type ) {
627 $assignments[] = $content;
628 }
629 }
630 }
631
632 foreach ( $quizzes as $row ) {
633 $result = QuizModel::get_quiz_result( $row->ID );
634 if ( 'pass' !== $result ) {
635 return false;
636 }
637 }
638
639 if ( tutor()->has_pro ) {
640 foreach ( $assignments as $row ) {
641 $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $row->ID, $user_id );
642 if ( 'pass' !== $result ) {
643 return false;
644 }
645 }
646 }
647
648 return true;
649 }
650
651 return false;
652
653 }
654
655 /**
656 * Check a course can be auto complete by an enrolled student.
657 *
658 * @since 2.4.0
659 *
660 * @param int $course_id course id.
661 * @param int $user_id user id.
662 *
663 * @return boolean
664 */
665 public static function can_autocomplete_course( $course_id, $user_id ) {
666 $auto_course_complete_option = (bool) tutor_utils()->get_option( 'auto_course_complete_on_all_lesson_completion' );
667 if ( ! $auto_course_complete_option ) {
668 return false;
669 }
670
671 $is_course_completed = tutor_utils()->is_completed_course( $course_id, $user_id );
672 if ( $is_course_completed ) {
673 return false;
674 }
675
676 $course_stats = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
677 if ( $course_stats['total_count'] && $course_stats['completed_count'] === $course_stats['total_count'] ) {
678 return self::can_complete_course( $course_id, $user_id );
679 } else {
680 return false;
681 }
682 }
683
684 /**
685 * Get review progress link when course progress 100% and
686 * User has pending or fail quiz or assignment
687 *
688 * @since 2.4.0
689 *
690 * @param int $course_id course id.
691 * @param int $user_id user id.
692 *
693 * @return string course content permalink.
694 */
695 public static function get_review_progress_link( $course_id, $user_id ) {
696 $course_progress = tutor_utils()->get_course_completed_percent( $course_id, $user_id, true );
697 $completed_percent = (int) $course_progress['completed_percent'];
698 $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
699 $permalink = '';
700
701 if ( tutor_utils()->count( $course_contents ) && 100 === $completed_percent ) {
702 foreach ( $course_contents as $content ) {
703 if ( 'tutor_quiz' === $content->post_type ) {
704 $result = QuizModel::get_quiz_result( $content->ID, $user_id );
705 if ( 'pass' !== $result ) {
706 $permalink = get_the_permalink( $content->ID );
707 break;
708 }
709 }
710
711 if ( tutor()->has_pro && 'tutor_assignments' === $content->post_type ) {
712 $result = \TUTOR_ASSIGNMENTS\Assignments::get_assignment_result( $content->ID, $user_id );
713 if ( 'pass' !== $result ) {
714 $permalink = get_the_permalink( $content->ID );
715 break;
716 }
717 }
718 }
719 }
720
721 // Fallback link.
722 if ( empty( $permalink ) ) {
723 $permalink = tutils()->get_course_first_lesson( $course_id );
724 }
725
726 return $permalink;
727 }
728 }
729