PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.3.7
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.3.7
4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 4.2.1 All 138 releases
learnpress / inc / Models / UserItems / UserCourseModel.php

UserCourseModel.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.3.7, at inc/Models/UserItems/UserCourseModel.php

1,137 lines 30.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Class UserItemModel
5 *
6 * @package LearnPress/Classes
7 * @version 1.0.1
8 * @since 4.2.5
9 */
10
11 namespace LearnPress\Models\UserItems;
12
13 use Exception;
14 use LearnPress\Filters\UserItemsFilter;
15 use LearnPress\Models\CourseModel;
16 use LearnPress\Models\CoursePostModel;
17 use LearnPress\Models\QuizPostModel;
18 use LP_Cache;
19 use LP_Course_Cache;
20 use LP_Course_Item;
21 use LP_Courses_Cache;
22 use LP_Datetime;
23 use LP_User;
24 use LP_User_Item_Course;
25 use LP_User_Items_Cache;
26 use LP_User_Items_DB;
27 use LP_User_Items_Filter;
28 use LP_User_Items_Result_DB;
29 use stdClass;
30 use Throwable;
31 use WP_Error;
32
33 class UserCourseModel extends UserItemModel {
34 /**
35 * Item type Course
36 *
37 * @var string Item type
38 */
39 public $item_type = LP_COURSE_CPT;
40 /**
41 * Ref type Order
42 *
43 * @var string
44 */
45 public $ref_type = LP_ORDER_CPT;
46
47 // Constants status
48 const STATUS_PURCHASED = 'purchased';
49
50 /**
51 * Constant key meta
52 */
53 const META_KEY_RETAKEN_COUNT = '_lp_retaken_count';
54
55 public function __construct( $data = null ) {
56 parent::__construct( $data );
57
58 if ( $data ) {
59 $this->get_course_model();
60 }
61 }
62
63 /**
64 * Get course model
65 *
66 * @return bool|CourseModel
67 */
68 public function get_course_model() {
69 return CourseModel::find( $this->item_id, true );
70 }
71
72 /**
73 * Find User Item by user_id, item_id, item_type.
74 *
75 * @param int $user_id
76 * @param int $course_id
77 * @param bool $check_cache
78 *
79 * @return false|static
80 * @since 4.2.7
81 * @version 1.0.2
82 */
83 public static function find( int $user_id, int $course_id, bool $check_cache = false ) {
84 static $staticData = [];
85
86 $filter = new LP_User_Items_Filter();
87 $filter->user_id = $user_id;
88 $filter->item_id = $course_id;
89 $filter->item_type = LP_COURSE_CPT;
90 $key_cache = "userCourseModel/find/{$user_id}/{$course_id}/{$filter->item_type}";
91 $lpUserCourseCache = new LP_Cache();
92
93 // Check cache
94 if ( $check_cache ) {
95 $userCourseModel = $lpUserCourseCache->get_cache( $key_cache );
96 if ( $userCourseModel instanceof UserCourseModel ) {
97 return $userCourseModel;
98 }
99
100 if ( isset( $staticData[ $key_cache ] ) ) {
101 return $staticData[ $key_cache ];
102 }
103 }
104
105 $userCourseModel = static::get_user_item_model_from_db( $filter );
106
107 // Set cache
108 if ( $userCourseModel instanceof UserCourseModel ) {
109 $lpUserCourseCache->set_cache( $key_cache, $userCourseModel );
110 }
111
112 $staticData[ $key_cache ] = $userCourseModel;
113
114 return $userCourseModel;
115 }
116
117 /**
118 * Get user_item is child of user course.
119 *
120 * @param int $item_id
121 * @param string $item_type
122 *
123 * @return false|UserItemModel|UserQuizModel|mixed
124 * @since 4.2.5
125 * @version 1.0.1
126 */
127 public function get_item_attend( int $item_id, string $item_type = '' ) {
128 $item = false;
129
130 try {
131 $item = UserItemModel::find_user_item(
132 $this->user_id,
133 $item_id,
134 $item_type,
135 $this->item_id,
136 $this->item_type,
137 true
138 );
139
140 if ( $item ) {
141 switch ( $item_type ) {
142 case LP_LESSON_CPT:
143 $item = new UserLessonModel( $item );
144 break;
145 case LP_QUIZ_CPT:
146 $item = new UserQuizModel( $item );
147 break;
148 default:
149 $item = apply_filters( 'learn-press/userCourseModel/get-item-attend', $item, $this, $item_id, $item_type );
150 break;
151 }
152 }
153 } catch ( Throwable $e ) {
154 error_log( __METHOD__ . ': ' . $e->getMessage() );
155 }
156
157 return $item;
158 }
159
160 /**
161 * Get item continue to learn.
162 *
163 * @return mixed|null
164 * @since 4.2.7.6
165 * @version 1.0.0
166 */
167 public function get_item_continue() {
168 if ( isset( $this->meta_data->item_continue ) ) {
169 return $this->meta_data->item_continue;
170 }
171
172 $itemModel = null;
173
174 try {
175 $courseModel = $this->get_course_model();
176 $totalItemsObj = $courseModel->count_items();
177 if ( empty( $totalItemsObj ) ) {
178 return $itemModel;
179 }
180
181 $item_id = 0;
182 $item_type = '';
183 $sections_items = $courseModel->get_section_items();
184 foreach ( $sections_items as $section_items ) {
185 if ( $itemModel ) {
186 break;
187 }
188
189 foreach ( $section_items->items as $item ) {
190 $item_id = $item->id ?? $item->item_id;
191 $item_type = $item->type ?? $item->item_type;
192
193 $userItemModel = UserItemModel::find_user_item(
194 $this->user_id,
195 $item_id,
196 $item_type,
197 $courseModel->get_id(),
198 LP_COURSE_CPT,
199 true
200 );
201 if ( ! $userItemModel || $userItemModel->get_status() !== LP_ITEM_COMPLETED ) {
202 $itemModel = $courseModel->get_item_model( $item_id, $item_type );
203 break;
204 }
205 }
206 }
207
208 // For all items are completed
209 if ( empty( $itemModel ) && ! empty( $item_id ) && ! empty( $item_type ) ) {
210 $itemModel = $courseModel->get_item_model( $item_id, $item_type );
211 }
212
213 $this->meta_data->item_continue = $itemModel;
214 } catch ( Throwable $e ) {
215 error_log( __METHOD__ . ': ' . $e->getMessage() );
216 }
217
218 return $itemModel;
219 }
220
221 /**
222 * Find next item when have just completed lesson.
223 * Logic: find item next not completed, circle to find next item, if not found return current item.
224 *
225 * @param int $item_id_current
226 *
227 * @since 4.3.2
228 * @version 1.0.0
229 * @return mixed|null
230 */
231 public function get_item_next_when_complete_lesson( int $item_id_current ) {
232 $itemModel = null;
233 $item_before_found = 0;
234 $item_after_found = 0;
235
236 try {
237 $courseModel = $this->get_course_model();
238 $course_items = $courseModel->get_only_items();
239 $course_item_keys = array_keys( $course_items );
240
241 $index_of_current = array_search( $item_id_current, $course_item_keys, true );
242
243 foreach ( $course_item_keys as $index => $item_id ) {
244 $course_item = $course_items[ $item_id ];
245 $item_type = $course_item->item_type;
246
247 $userItemModel = UserItemModel::find_user_item(
248 $this->user_id,
249 $item_id,
250 $item_type,
251 $courseModel->get_id(),
252 LP_COURSE_CPT,
253 true
254 );
255 $status_compare = apply_filters(
256 'learn-press/user-course-model/get-item-next-when-complete-lesson/status-compare',
257 [
258 UserItemModel::STATUS_COMPLETED,
259 ]
260 );
261
262 // Found item before current item not completed
263 if ( $index < $index_of_current ) {
264 if ( ! $item_before_found &&
265 ( ! $userItemModel || ! in_array( $userItemModel->get_status(), $status_compare ) ) ) {
266 $item_before_found = $course_item;
267 }
268 } elseif ( $index > $index_of_current ) {
269 // Found item after current item not completed
270 if ( ! $item_after_found &&
271 ( ! $userItemModel || ! in_array( $userItemModel->get_status(), $status_compare ) ) ) {
272 $item_after_found = $course_item;
273 break;
274 }
275 }
276 }
277
278 if ( $item_after_found ) {
279 $itemModel = $courseModel->get_item_model( $item_after_found->item_id, $item_after_found->item_type );
280 } elseif ( $item_before_found ) {
281 $itemModel = $courseModel->get_item_model( $item_before_found->item_id, $item_before_found->item_type );
282 } else {
283 $itemModel = $courseModel->get_item_model( $item_id_current, LP_LESSON_CPT );
284 }
285 } catch ( Throwable $e ) {
286 error_log( __METHOD__ . ': ' . $e->getMessage() );
287 }
288
289 return $itemModel;
290 }
291
292 /**
293 * Count students.
294 *
295 * @param LP_User_Items_Filter|UserItemsFilter $filter
296 *
297 * @return int
298 * @since 4.2.5.4
299 * @version 1.0.0
300 */
301 public static function count_students( $filter ): int {
302 // Check cache
303 $key_cache = 'count-courses-student-' . md5( json_encode( $filter ) );
304 $count = LP_Cache::cache_load_first( 'get', $key_cache );
305 if ( false !== $count ) {
306 return $count;
307 }
308
309 $lp_courses_cache = new LP_Courses_Cache( true );
310 $count = $lp_courses_cache->get_cache( $key_cache );
311 if ( false !== $count ) {
312 LP_Cache::cache_load_first( 'set', $key_cache, $count );
313
314 return $count;
315 }
316
317 $lp_user_items_db = LP_User_Items_DB::getInstance();
318 $count = $lp_user_items_db->count_students( $filter );
319
320 // Set cache
321 $lp_courses_cache->set_cache( $key_cache, $count );
322 $lp_courses_cache_keys = new LP_Courses_Cache( true );
323 $lp_courses_cache_keys->save_cache_keys_count_student_courses( $key_cache );
324 LP_Cache::cache_load_first( 'set', $key_cache, $count );
325
326 return $count;
327 }
328
329 /**
330 * Check course of user is enrolled or finished
331 *
332 * @return bool
333 */
334 public function has_enrolled_or_finished(): bool {
335 return $this->has_enrolled() || $this->has_finished();
336 }
337
338 /**
339 * Check user has enrolled course
340 *
341 * @return bool
342 */
343 public function has_enrolled(): bool {
344 return $this->status === UserItemModel::STATUS_ENROLLED;
345 }
346
347 /**
348 * Check user has purchased course
349 *
350 * @return bool
351 */
352 public function has_purchased(): bool {
353 return $this->status === UserItemModel::STATUS_PURCHASED;
354 }
355
356 /**
357 * Check user has finished course
358 *
359 * @return bool
360 */
361 public function has_finished(): bool {
362 return $this->status === UserItemModel::STATUS_FINISHED;
363 }
364
365 /**
366 * Check course has finished or not.
367 *
368 * @return bool
369 * @move from class-lp-user-item-course.php
370 * @since 3.0.0
371 * @version 1.0.2
372 */
373 public function is_finished(): bool {
374 return $this->has_finished();
375 }
376
377 /**
378 * Calculate course result
379 *
380 * @move from class-lp-user-item-course.php
381 *
382 * @param bool $force_cache
383 *
384 * @return array [ 'count_items' => int, 'completed_items' => int, 'items' => array, 'evaluate_type' => string, 'pass' => int, 'result' => float ]
385 * @since 4.1.4
386 * @version 1.0.3
387 */
388 public function calculate_course_results( bool $force_cache = false ): array {
389 $items = array();
390 $results = array(
391 'count_items' => 0,
392 'completed_items' => 0,
393 'items' => array(),
394 'evaluate_type' => '',
395 'pass' => 0,
396 'result' => 0,
397 );
398
399 try {
400 $courseModel = $this->get_course_model();
401 if ( ! $courseModel instanceof CourseModel ) {
402 throw new Exception( 'Course invalid!' );
403 }
404
405 $key_first_cache = 'calculate_course/' . $this->user_id . '/' . $courseModel->get_id();
406 $results_cache = LP_Cache::cache_load_first( 'get', $key_first_cache );
407 if ( false !== $results_cache && ! $force_cache ) {
408 return $results_cache;
409 }
410
411 if ( $this->is_finished() ) {
412 // Get result from lp_user_item_results
413 // Todo: tungnx - set cache
414 return LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
415 }
416
417 if ( $this->status !== LP_COURSE_ENROLLED ) {
418 return $results;
419 }
420
421 $count_items = $courseModel->count_items();
422 $count_items_completed = $this->count_items_completed();
423
424 $evaluate_type = $courseModel->get_meta_value_by_key( CoursePostModel::META_KEY_EVALUATION_TYPE, 'evaluate_lesson' );
425 switch ( $evaluate_type ) {
426 case 'evaluate_lesson':
427 $results_evaluate = $this->evaluate_course_by_lesson( $count_items_completed, $courseModel->count_items( LP_LESSON_CPT ) );
428 break;
429 case 'evaluate_final_quiz':
430 $results_evaluate = $this->evaluate_course_by_final_quiz();
431 break;
432 case 'evaluate_quiz':
433 $results_evaluate = $this->evaluate_course_by_quizzes_passed( $count_items_completed, $courseModel->count_items( LP_QUIZ_CPT ) );
434 break;
435 case 'evaluate_questions':
436 case 'evaluate_mark':
437 $results_evaluate = $this->evaluate_course_by_question( $evaluate_type );
438 break;
439 default:
440 // Old Hook
441 if ( has_filter( 'learn-press/evaluate_passed_conditions' ) ) {
442 $user_course_old = new LP_User_Item_Course( $this );
443 $results = apply_filters( 'learn-press/evaluate_passed_conditions', $results, $evaluate_type, $user_course_old );
444 }
445
446 $results_evaluate = apply_filters( 'learn-press/evaluate/calculate', $results, $evaluate_type, $this );
447 break;
448 }
449
450 if ( ! is_array( $results_evaluate ) ) {
451 $results_evaluate = array(
452 'result' => 0,
453 'pass' => 0,
454 );
455 }
456
457 $results_evaluate['result'] = round( $results_evaluate['result'], 2 );
458
459 $completed_items = intval( $count_items_completed->count_status ?? 0 );
460
461 $item_types = CourseModel::item_types_support();
462 foreach ( $item_types as $item_type ) {
463 $item_type_key = str_replace( 'lp_', '', $item_type );
464
465 $items[ $item_type_key ] = array(
466 'completed' => $count_items_completed->{$item_type . '_status_completed'} ?? 0,
467 'passed' => $count_items_completed->{$item_type . '_graduation_passed'} ?? 0,
468 'total' => $courseModel->count_items( $item_type ),
469 );
470 }
471
472 $results = array_merge(
473 $results_evaluate,
474 compact( 'count_items', 'completed_items', 'items', 'evaluate_type' )
475 );
476
477 $results = apply_filters(
478 'learn-press/update-course-results',
479 $results,
480 $this->item_id,
481 $this->user_id,
482 $this
483 );
484
485 LP_Cache::cache_load_first( 'set', $key_first_cache, $results );
486 } catch ( Throwable $e ) {
487 error_log( __METHOD__ . ': ' . $e->getMessage() );
488 }
489
490 return $results;
491 }
492
493 /**
494 * Check user can retake course or not.
495 *
496 * @move from class-lp-user.php method can_retry_course since 4.0.0
497 * @use LP_User::can_retry_course
498 * @return int|mixed|null
499 * @since 4.2.7.3
500 * @version 1.0.0
501 */
502 public function can_retake() {
503 $flag = 0;
504
505 try {
506 $course = $this->get_course_model();
507 if ( ! $course ) {
508 return $flag;
509 }
510
511 $retake_option = (int) $course->get_meta_value_by_key( CoursePostModel::META_KEY_RETAKE_COUNT, 0 );
512 if ( $retake_option > 0 ) {
513 /**
514 * Check course is finished
515 * Check duration is blocked
516 */
517 if ( ! $this->has_finished() ) {
518 if ( 0 !== $this->timestamp_remaining_duration() ) {
519 throw new Exception();
520 }
521 }
522
523 $retaken = $this->get_retaken_count();
524 $can_retake_times = $retake_option - $retaken;
525
526 if ( $can_retake_times > 0 ) {
527 $flag = $can_retake_times;
528 }
529 }
530 } catch ( Exception $e ) {
531
532 }
533
534 return apply_filters( 'learn-press/user/course/can-retake', $flag, $this );
535 }
536
537 /**
538 * Get retaken count of user attend course.
539 *
540 * @return int
541 */
542 public function get_retaken_count(): int {
543 return (int) $this->get_meta_value_from_key( self::META_KEY_RETAKEN_COUNT, 0 );
544 }
545
546 /**
547 * Check time remaining course when enable duration expire
548 * Value: -1 is no limit (default)
549 * Value: 0 is block
550 * Administrator || (is instructor && is author course) will be not block.
551 *
552 * @return int second
553 * @since 4.0.0
554 * @author tungnx
555 * @version 1.0.1
556 * @depecated 4.2.7.6, instead of get_time_remaining
557 */
558 public function timestamp_remaining_duration(): int {
559 $timestamp_remaining = - 1;
560 $user = $this->get_user_model();
561 /**
562 * @var CourseModel $course
563 */
564 $course = $this->get_course_model();
565 if ( ! $user || ! $course ) {
566 return $timestamp_remaining;
567 }
568
569 $author = $course->get_author_model();
570 if ( ! $author ) {
571 return $timestamp_remaining;
572 }
573
574 $user_id = $user->get_id();
575
576 if ( current_user_can( 'administrator' ) ||
577 ( current_user_can( LP_TEACHER_ROLE ) && $author->get_id() === $user_id ) ) {
578 return $timestamp_remaining;
579 }
580
581 if ( 0 === (int) $course->get_duration() ) {
582 return $timestamp_remaining;
583 }
584
585 if ( ! $course->enable_block_when_expire() ) {
586 return $timestamp_remaining;
587 }
588
589 $course_start_time_str = $this->get_start_time();
590 $course_start_time = new LP_Datetime( $course_start_time_str );
591 $course_start_time = $course_start_time->get_raw_date();
592 $duration = $course->get_duration();
593 $timestamp_expire = strtotime( $course_start_time . ' +' . $duration );
594 $timestamp_current = time();
595 $timestamp_remaining = $timestamp_expire - $timestamp_current;
596
597 if ( $timestamp_remaining < 0 ) {
598 $timestamp_remaining = 0;
599 }
600
601 return apply_filters( 'learnpress/course/block_duration_expire/timestamp_remaining', $timestamp_remaining );
602 }
603
604 /**
605 * Check time remaining course when enable duration expire
606 * Value: -1 is no limit (default)
607 * Value: 0 is block
608 * Administrator || (is instructor && is author course) will be not block.
609 *
610 * @return int second
611 * @since 4.2.7.6
612 * @version 1.0.0
613 */
614 public function get_time_remaining(): int {
615 $timestamp_remaining = - 1;
616 $userModel = $this->get_user_model();
617 $courseModel = $this->get_course_model();
618 if ( ! $userModel || ! $courseModel ) {
619 return $timestamp_remaining;
620 }
621
622 $author = $courseModel->get_author_model();
623 if ( ! $author ) {
624 return $timestamp_remaining;
625 }
626
627 /*$user_id = $userModel->get_id();
628 $user_wp = new \WP_User( $userModel );
629 if ( user_can( $user_wp, ADMIN_ROLE ) ||
630 ( user_can( $user_wp, LP_TEACHER_ROLE ) && $author->get_id() === $user_id ) ) {
631 return $timestamp_remaining;
632 }*/
633
634 if ( 0 === (int) $courseModel->get_duration() ) {
635 return $timestamp_remaining;
636 }
637
638 if ( ! $courseModel->enable_block_when_expire() ) {
639 return $timestamp_remaining;
640 }
641
642 $course_start_time = new LP_Datetime( $this->get_start_time() );
643 $course_start_time = $course_start_time->get_raw_date();
644 $duration = $courseModel->get_duration();
645 $timestamp_expire = strtotime( $course_start_time . ' +' . $duration );
646 $timestamp_current = time();
647 $timestamp_remaining = $timestamp_expire - $timestamp_current;
648
649 if ( $timestamp_remaining < 0 ) {
650 $timestamp_remaining = 0;
651 }
652
653 return apply_filters( 'learnpress/course/block_duration_expire/timestamp_remaining', $timestamp_remaining );
654 }
655
656 /**
657 * Get completed items.
658 *
659 * @return object
660 * @modify 4.1.4.1
661 * @since 4.0.0
662 * @version 4.0.3
663 */
664 public function count_items_completed() {
665 $lp_user_items_db = LP_User_Items_DB::getInstance();
666 $count_items_completed = new stdClass();
667
668 try {
669 $key_cache_first = "userCourseModel/count_items_completed/{$this->user_id}/{$this->item_id}";
670 $count_items_completed = LP_Cache::cache_load_first( 'get', $key_cache_first );
671 if ( false !== $count_items_completed ) {
672 return $count_items_completed;
673 }
674
675 $filter_count = new LP_User_Items_Filter();
676 $filter_count->parent_id = $this->get_user_item_id();
677 $filter_count->item_id = $this->item_id;
678 $filter_count->user_id = $this->user_id;
679 $filter_count->status = LP_ITEM_COMPLETED;
680 $filter_count->graduation = LP_COURSE_GRADUATION_PASSED;
681 $count_items_completed = $lp_user_items_db->count_items_of_course_with_status( $filter_count );
682
683 // Set cache first
684 LP_Cache::cache_load_first( 'set', $key_cache_first, $count_items_completed );
685 } catch ( Throwable $e ) {
686 error_log( __METHOD__ . ': ' . $e->getMessage() );
687 }
688
689 return $count_items_completed;
690 }
691
692 /**
693 * Get graduation of course.
694 *
695 * @return string
696 * @since 4.2.7.2
697 * @version 1.0.1
698 */
699 public function get_graduation(): string {
700 return (string) $this->graduation;
701 }
702
703 /**
704 * Check course is passed or not.
705 *
706 * @return bool
707 * @since 4.2.7.2
708 * @version 1.0.0
709 */
710 public function is_passed(): bool {
711 return $this->graduation === UserItemModel::GRADUATION_PASSED;
712 }
713
714 /**
715 * Check course is canceled or not.
716 *
717 * @return bool
718 * @since 4.2.7.6
719 * @version 1.0.0
720 */
721 public function has_canceled(): bool {
722 return $this->status === self::STATUS_CANCEL;
723 }
724
725 /**
726 * Evaluate course result by lessons.
727 *
728 * @param $count_items_completed
729 * @param int $total_item_lesson
730 *
731 * @return array
732 * @author tungnx
733 * @since 4.1.4.1
734 * @version 1.0.0
735 */
736 protected function evaluate_course_by_lesson( $count_items_completed, int $total_item_lesson = 0 ): array {
737 $evaluate = array(
738 'result' => 0,
739 'pass' => 0,
740 );
741
742 $count_items_completed = intval( $count_items_completed->{LP_LESSON_CPT . '_status_completed'} ?? 0 );
743
744 if ( $total_item_lesson && $count_items_completed ) {
745 $evaluate['result'] = $count_items_completed * 100 / $total_item_lesson;
746 }
747
748 $passing_condition = $this->get_course_model()->get_passing_condition();
749 if ( $evaluate['result'] >= $passing_condition ) {
750 $evaluate['pass'] = 1;
751 }
752
753 return $evaluate;
754 }
755
756 /**
757 * Evaluate course result by final quiz.
758 *
759 * @return array
760 */
761 protected function evaluate_course_by_final_quiz(): array {
762 $lp_user_items_db = LP_User_Items_DB::getInstance();
763 $lp_user_item_result_db = LP_User_Items_Result_DB::instance();
764 $evaluate = array(
765 'result' => 0,
766 'pass' => 0,
767 );
768
769 try {
770 $courseModel = $this->get_course_model();
771 if ( ! $courseModel ) {
772 return $evaluate;
773 }
774
775 $quiz_final_id = $courseModel->get_meta_value_by_key( CoursePostModel::META_KEY_FINAL_QUIZ, 0 );
776 if ( ! $quiz_final_id ) {
777 return $evaluate;
778 }
779
780 $quizPostModel = QuizPostModel::find( $quiz_final_id, true );
781 if ( ! $quizPostModel ) {
782 return $evaluate;
783 }
784
785 $filter = new LP_User_Items_Filter();
786 $filter->query_type = 'get_row';
787 $filter->parent_id = $this->get_user_item_id();
788 $filter->item_type = LP_QUIZ_CPT;
789 $filter->item_id = $quiz_final_id;
790 $user_quiz = $lp_user_items_db->get_user_course_items_by_item_type( $filter );
791 if ( ! $user_quiz ) {
792 throw new Exception();
793 }
794
795 // Get result did quiz
796 $quiz_result = $lp_user_item_result_db->get_result( $user_quiz->user_item_id );
797
798 if ( $quiz_result ) {
799 if ( ! isset( $quiz_result['result'] ) ) {
800 $evaluate['result'] = $quiz_result['user_mark'] * 100 / $quiz_result['mark'];
801 } else {
802 $evaluate['result'] = $quiz_result['result'];
803 }
804
805 $passing_condition = $quizPostModel->get_passing_grade();
806 if ( $evaluate['result'] >= $passing_condition ) {
807 $evaluate['pass'] = 1;
808 }
809 }
810 } catch ( Throwable $e ) {
811
812 }
813
814 return $evaluate;
815 }
816
817 /**
818 * Evaluate course results by count quizzes passed/all quizzes.
819 *
820 * @author tungnx
821 * @since 4.1.4.1
822 * @version 1.0.0
823 */
824 protected function evaluate_course_by_quizzes_passed( $count_items_completed, $total_item_quizzes ): array {
825 $evaluate = array(
826 'result' => 0,
827 'pass' => 0,
828 );
829
830 $count_items_completed = intval( $count_items_completed->{LP_QUIZ_CPT . '_graduation_passed'} ?? 0 );
831
832 if ( $total_item_quizzes && $count_items_completed ) {
833 $evaluate['result'] = $count_items_completed * 100 / $total_item_quizzes;
834
835 $passing_condition = $this->get_course_model()->get_passing_condition();
836 if ( $evaluate['result'] >= $passing_condition ) {
837 $evaluate['pass'] = 1;
838 }
839 }
840
841 return $evaluate;
842 }
843
844 /**
845 * Evaluate course results by count questions true/all questions.
846 *
847 * @author tungnx
848 * @since 4.1.4.1
849 * @version 1.0.0
850 */
851 private function evaluate_course_by_questions( &$evaluate, $lp_quizzes, $total_questions ) {
852 $lp_user_item_results_db = LP_User_Items_Result_DB::instance();
853 $count_questions_correct = 0;
854
855 // get questions correct
856 foreach ( $lp_quizzes as $lp_quiz ) {
857 $lp_quiz_result = $lp_user_item_results_db->get_result( $lp_quiz->user_item_id );
858 if ( $lp_quiz_result ) {
859 $count_questions_correct += $lp_quiz_result['question_correct'];
860 }
861 }
862
863 if ( $total_questions && $count_questions_correct ) {
864 $evaluate['result'] = $count_questions_correct * 100 / $total_questions;
865
866 $passing_condition = $this->get_course_model()->get_passing_condition();
867 if ( $evaluate['result'] >= $passing_condition ) {
868 $evaluate['pass'] = 1;
869 }
870 }
871 }
872
873 /**
874 * Evaluate course results by total mark of questions.
875 *
876 * @author tungnx
877 * @since 4.1.4.1
878 * @version 1.0.0
879 */
880 private function evaluate_course_by_mark( &$evaluate, $lp_quizzes, $total_mark_questions ) {
881 $lp_user_item_results_db = LP_User_Items_Result_DB::instance();
882 $count_mark_questions_receiver = 0;
883
884 foreach ( $lp_quizzes as $lp_quiz ) {
885 $lp_quiz_result = $lp_user_item_results_db->get_result( $lp_quiz->user_item_id );
886 if ( $lp_quiz_result ) {
887 $count_mark_questions_receiver += $lp_quiz_result['user_mark'];
888 }
889 }
890
891 if ( $count_mark_questions_receiver && $total_mark_questions ) {
892 $evaluate['result'] = $count_mark_questions_receiver * 100 / $total_mark_questions;
893
894 $passing_condition = $this->get_course_model()->get_passing_condition();
895 if ( $evaluate['result'] >= $passing_condition ) {
896 $evaluate['pass'] = 1;
897 }
898 }
899 }
900
901 /**
902 * Evaluate course results by total mark of questions.
903 *
904 * @author tungnx
905 * @since 4.1.4.1
906 * @version 1.0.2
907 */
908 protected function evaluate_course_by_question( string $evaluate_type ): array {
909 $lp_user_items_db = LP_User_Items_DB::getInstance();
910 $evaluate = array(
911 'result' => 0,
912 'pass' => 0,
913 );
914
915 try {
916 // get quiz_ids
917 $filter_get_quiz_ids = new LP_User_Items_Filter();
918 $filter_get_quiz_ids->parent_id = $this->get_user_item_id();
919 $filter_get_quiz_ids->item_type = LP_QUIZ_CPT;
920 $lp_quizzes = $lp_user_items_db->get_user_course_items_by_item_type( $filter_get_quiz_ids );
921
922 // Get total questions, mark
923 $course = $this->get_course_model();
924
925 $total_questions = 0;
926 $total_mark_question = 0;
927
928 // Get all items' course
929 $sections_items = $course->get_section_items();
930
931 foreach ( $sections_items as $section_items ) {
932 foreach ( $section_items->items as $item ) {
933 $itemObj = LP_Course_Item::get_item( $item->id );
934 if ( ! $itemObj instanceof LP_Course_Item ) {
935 continue;
936 }
937
938 if ( $item->type == LP_QUIZ_CPT ) {
939 $total_questions += count( $itemObj->get_question_ids() );
940 $total_mark_question += $itemObj->get_mark();
941 }
942 }
943 }
944 // End get total questions, mark
945
946 switch ( $evaluate_type ) {
947 case 'evaluate_questions':
948 $this->evaluate_course_by_questions( $evaluate, $lp_quizzes, $total_questions );
949 break;
950 case 'evaluate_mark':
951 $this->evaluate_course_by_mark( $evaluate, $lp_quizzes, $total_mark_question );
952 break;
953 default:
954 break;
955 }
956
957 // Get results of each quiz - has questions
958 } catch ( Throwable $e ) {
959 error_log( __FUNCTION__ . ': ' . $e->getMessage() );
960 }
961
962 return $evaluate;
963 }
964
965 protected function count_item_completed() {
966 }
967
968 /**
969 * Check user can finish course or not.
970 *
971 * @return bool|WP_Error
972 *
973 * @since 4.2.7.5
974 * @version 1.0.0
975 */
976 public function can_finish() {
977 $can_finish = true;
978
979 try {
980 $courseModel = $this->get_course_model();
981 if ( ! $courseModel ) {
982 throw new Exception( __( 'Course not exists!', 'learnpress' ) );
983 }
984
985 $can_impact_item = $this->can_impact_item();
986 if ( $can_impact_item instanceof WP_Error ) {
987 throw new Exception( $can_impact_item->get_error_message() );
988 }
989
990 $course_results = $this->calculate_course_results();
991 if ( empty( $course_results['pass'] ) ) {
992 $allow_finish_when_all_item_completed = $courseModel->get_meta_value_by_key(
993 CoursePostModel::META_KEY_HAS_FINISH,
994 'yes'
995 );
996 if ( $allow_finish_when_all_item_completed ) {
997 $course_total_items_obj = $courseModel->get_total_items();
998 if ( $course_total_items_obj && $course_results['completed_items'] < $course_total_items_obj->count_items ) {
999 throw new Exception( __( 'You must complete all items in course', 'learnpress' ) );
1000 }
1001 } else {
1002 throw new Exception( __( 'You must passed course', 'learnpress' ) );
1003 }
1004 }
1005 } catch ( Throwable $e ) {
1006 $can_finish = new WP_Error( 'lp_user_course_can_finish_err', $e->getMessage() );
1007 }
1008
1009 return apply_filters( 'learn-press/user-course/can-finish', $can_finish, $this );
1010 }
1011
1012 /**
1013 * Handle finish course.
1014 *
1015 * @throws Exception
1016 * @version 1.0.1
1017 * @since 4.2.7.6
1018 */
1019 public function handle_finish() {
1020 $can_finish = $this->can_finish();
1021 if ( is_wp_error( $can_finish ) ) {
1022 throw new Exception( $can_finish->get_error_message() );
1023 }
1024
1025 $course_results = $this->calculate_course_results();
1026 $this->graduation = $course_results['pass'] ? LP_COURSE_GRADUATION_PASSED : LP_COURSE_GRADUATION_FAILED;
1027 $this->status = LP_COURSE_FINISHED;
1028 $this->end_time = gmdate( LP_Datetime::$format, time() );
1029 $this->save();
1030
1031 // Save result for course
1032 LP_User_Items_Result_DB::instance()->update( $this->get_user_item_id(), wp_json_encode( $course_results ) );
1033
1034 do_action( 'learn-press/user-course-finished', $this->item_id, $this->user_id, $this->get_user_item_id() );
1035 do_action( 'learn-press/user-course/finished', $this );
1036 }
1037
1038 /**
1039 * Handle retake course
1040 *
1041 * @throws Exception
1042 * @since 4.2.7.6
1043 * @version 1.0.0
1044 */
1045 public function handle_retake() {
1046 $remaining_retake = $this->can_retake();
1047 if ( $remaining_retake === 0 ) {
1048 throw new Exception( __( 'You can not retake this course!', 'learnpress' ) );
1049 }
1050
1051 $this->status = self::STATUS_ENROLLED;
1052 $this->graduation = self::GRADUATION_IN_PROGRESS;
1053 $this->start_time = gmdate( LP_Datetime::$format, time() );
1054 $this->end_time = null;
1055 $this->set_meta_value_for_key( self::META_KEY_RETAKEN_COUNT, $this->get_retaken_count() + 1 );
1056 $this->save();
1057
1058 $courseModel = $this->get_course_model();
1059
1060 if ( $courseModel->count_items() > 0 ) {
1061 // Remove items' course user learned.
1062 $filter_remove = new LP_User_Items_Filter();
1063 $filter_remove->parent_id = $this->get_user_item_id();
1064 $filter_remove->user_id = $this->user_id;
1065 $filter_remove->limit = - 1;
1066 LP_User_Items_DB::getInstance()->remove_items_of_user_course( $filter_remove );
1067
1068 // Clean cache items.
1069 foreach ( $courseModel->get_section_items() as $section_items ) {
1070 foreach ( $section_items->items as $item ) {
1071 $key_cache = "userItemModel/find/{$this->user_id}/{$item->id}/{$item->type}/{$this->item_id}/" . LP_COURSE_CPT;
1072 $lpUserItemCache = new LP_User_Items_Cache();
1073 $lpUserItemCache->clear( $key_cache );
1074 }
1075 }
1076 }
1077
1078 // Create new result in table learnpress_user_item_results.
1079 LP_User_Items_Result_DB::instance()->insert( $this->get_user_item_id() );
1080 }
1081
1082 /**
1083 * Check can impact item.
1084 *
1085 * @return bool|WP_Error
1086 * @since 4.2.7.6
1087 * @version 1.0.1
1088 */
1089 public function can_impact_item() {
1090 $can_impact_item = true;
1091
1092 // For case user Guest
1093 $userModel = $this->get_user_model();
1094 if ( ! $userModel ) {
1095 $can_impact_item = new WP_Error( 'user_not_exists', __( 'User not exists!', 'learnpress' ) );
1096 }
1097
1098 $status = $this->get_status();
1099 if ( $this->has_canceled() || ! $this->has_enrolled() ) {
1100 $can_impact_item = new WP_Error( 'user_not_enroll_course', __( 'You have not enroll this course!', 'learnpress' ) );
1101 }
1102
1103 if ( $this->has_finished() ) {
1104 $can_impact_item = new WP_Error( 'user_finished_course', __( 'You have finished this course!', 'learnpress' ) );
1105 }
1106
1107 if ( $this->get_time_remaining() === 0 ) {
1108 $can_impact_item = new WP_Error( 'course_is_blocked', __( 'Course was blocked by expire!', 'learnpress' ) );
1109 }
1110
1111 return apply_filters( 'learn-press/user-course/can-impact-item', $can_impact_item, $this );
1112 }
1113
1114 /**
1115 * Clean caches.
1116 *
1117 * @return void
1118 *
1119 * @since 4.2.5.4
1120 * @version 1.0.1
1121 */
1122 public function clean_caches() {
1123 $key_cache = "userCourseModel/find/{$this->user_id}/{$this->item_id}/{$this->item_type}";
1124 $lpUserCourseCache = new LP_Cache();
1125 $lpUserCourseCache->clear( $key_cache );
1126
1127 parent::clean_caches();
1128 // Clear cache total students enrolled of a course.
1129 $lp_course_cache = new LP_Course_Cache( true );
1130 $lp_course_cache->clean_total_students_enrolled( $this->item_id );
1131 $lp_course_cache->clean_total_students_enrolled_or_purchased( $this->item_id );
1132 // Clear cache count students of many course
1133 $lp_courses_cache = new LP_Courses_Cache( true );
1134 $lp_courses_cache->clear_cache_on_group( LP_Courses_Cache::KEYS_COUNT_STUDENT_COURSES );
1135 }
1136 }
1137