PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.7
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.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.4.7, at inc/Models/UserItems/UserCourseModel.php

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