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
← All changes | inc/Models/CourseModel.php +1559 -1528 4.3.84.4.7 View file →
@@ -1,1528 +1,1559 @@
1 -<?php
2 -
3 -/**
4 - * Class Course Model
5 - * Purpose: Use to map property separate table learnpress_course
6 - * Field json for store all value of single course.
7 - * Another fields for query list courses faster
8 - *
9 - * @package LearnPress/Classes
10 - * @version 1.0.5
11 - * @since 4.2.6.9
12 - */
13 -
14 -namespace LearnPress\Models;
15 -
16 -use Exception;
17 -use LearnPress\Databases\Course\CourseSectionItemsDB;
18 -use LearnPress\Filters\PostFilter;
19 -use LearnPress\Models\UserItems\UserCourseModel;
20 -use LearnPress\Models\UserItems\UserItemModel;
21 -use LP_Course_Cache;
22 -use LP_Course_DB;
23 -use LP_Course_Item;
24 -use LP_Course_JSON_DB;
25 -use LP_Course_JSON_Filter;
26 -use LP_Datetime;
27 -use LP_Debug;
28 -use LP_Helper;
29 -use LP_Section_Items_Filter;
30 -use LP_Settings;
31 -use stdClass;
32 -use Throwable;
33 -use WP_Error;
34 -use WP_Post;
35 -
36 -class CourseModel {
37 - /**
38 - * Auto increment, Primary key
39 - *
40 - * @var int
41 - */
42 - public $ID = 0;
43 - /**
44 - * @var string author id, foreign key
45 - */
46 - public $post_author = 0;
47 - /**
48 - * @var string post date gmt
49 - */
50 - public $post_date_gmt = null;
51 - /**
52 - * @var string post content
53 - */
54 - public $post_content = '';
55 - /**
56 - * @var string Post title
57 - */
58 - public $post_title = '';
59 - /**
60 - * @var string Post Status (publish, draft, ...)
61 - */
62 - public $post_status = '';
63 - /**
64 - * @var string Post name (slug for link)
65 - */
66 - public $post_name = '';
67 - /**
68 - * @var float price only using for filter courses, don't use for course detail
69 - * Because price can change by date if set schedule sale
70 - */
71 - public $price_to_sort = 0;
72 - public $is_sale = 0;
73 - /**
74 - * @var string JSON Store all data a single course
75 - */
76 - public $json = null; // Only set when save, don't set when get
77 - /**
78 - * @var string lang of Course
79 - */
80 - public $lang = null;
81 - /********** Field not on table **********/
82 - /**
83 - * @var stdClass all meta data
84 - */
85 - public $meta_data = null;
86 - public $image_url = null;
87 - public $permalink = '';
88 - public $categories;
89 - public $tags;
90 - private $price; // Not save in database, must auto reload calculate
91 - private $passing_condition = '';
92 - public $post_excerpt = '';
93 - /**
94 - * @var int ID of first item
95 - */
96 - public $first_item_id;
97 - /**
98 - * @var null|object info total items {'count_items': 20, 'lp_lesson': 10, 'lp_quiz': 10, ...}
99 - */
100 - public $total_items;
101 - /**
102 - * @var array list sections items
103 - */
104 - public $sections_items;
105 - /**
106 - * @var array|null cached items of course (per instance)
107 - */
108 - protected array $only_items;
109 -
110 - /**
111 - * If data get from database, map to object.
112 - * Else create new object to save data to database.
113 - *
114 - * @param array|object|mixed $data
115 - */
116 - public function __construct( $data = null ) {
117 - if ( $data ) {
118 - $this->map_to_object( $data );
119 - }
120 -
121 - if ( is_null( $this->meta_data ) ) {
122 - $this->meta_data = new stdClass();
123 - }
124 - }
125 -
126 - /**
127 - * Map array, object data to CourseModel.
128 - * Use for data get from database.
129 - *
130 - * @param array|object|mixed $data
131 - *
132 - * @return CourseModel
133 - */
134 - public function map_to_object( $data ): CourseModel {
135 - foreach ( $data as $key => $value ) {
136 - if ( property_exists( $this, $key ) ) {
137 - $this->{$key} = $value;
138 - }
139 - }
140 -
141 - return $this;
142 - }
143 -
144 - /**
145 - * Get course id
146 - *
147 - * @return int
148 - */
149 - public function get_id(): int {
150 - return $this->ID;
151 - }
152 -
153 - /**
154 - * Get post model
155 - *
156 - * @return CoursePostModel
157 - * @since 4.3.6
158 - * @version 1.0.0
159 - */
160 - public function get_post_model(): CoursePostModel {
161 - return new CoursePostModel( $this );
162 - }
163 -
164 - public function get_title(): string {
165 - $course_post = new CoursePostModel( $this );
166 -
167 - return $course_post->get_the_title();
168 - }
169 -
170 - /**
171 - * Get image url
172 - * if not check get from Post
173 - *
174 - * @param string|int[] $size
175 - *
176 - * @return string
177 - * @since 4.2.6.9
178 - * @version 1.0.2
179 - */
180 - public function get_image_url( $size = 'post-thumbnail' ): string {
181 - /**
182 - * Comment code isset( $this->image_url )
183 - * To apply for many size on a course
184 - * To apply cache need handle cache before, where set size for image.
185 - */
186 - /*if ( isset( $this->image_url ) ) {
187 - return $this->image_url;
188 - }*/
189 -
190 - $post = new CoursePostModel( $this );
191 - $image_url = $post->get_image_url( $size );
192 -
193 - $this->image_url = $image_url;
194 -
195 - return $image_url;
196 - }
197 -
198 - /**
199 - * Get author model
200 - * Check has data on table learnpress_courses return
201 - * if not check get from Post
202 - *
203 - * @return UserModel|false
204 - */
205 - public function get_author_model() {
206 - $post = new CoursePostModel( $this );
207 -
208 - return $post->get_author_model();
209 - }
210 -
211 - /**
212 - * Get status of course
213 - *
214 - * @return string
215 - * @since 4.2.7.3
216 - * @version 1.0.0
217 - */
218 - public function get_status(): string {
219 - return $this->post_status;
220 - }
221 -
222 - /**
223 - * Get categories
224 - * Check has data on table learnpress_courses return
225 - * if not check get from Post
226 - *
227 - * @return array
228 - */
229 - public function get_categories(): array {
230 - if ( isset( $this->categories ) ) {
231 - return $this->categories;
232 - }
233 -
234 - $post = new PostModel( $this );
235 - $categories = $post->get_categories();
236 -
237 - $this->categories = $categories;
238 -
239 - return $this->categories;
240 - }
241 -
242 - /**
243 - * Get tags of course.
244 - *
245 - * @return array
246 - * @since 4.2.7.2
247 - * @version 1.0.0
248 - */
249 - public function get_tags(): array {
250 - if ( isset( $this->tags ) ) {
251 - return $this->tags;
252 - }
253 -
254 - $post = new PostModel( $this );
255 - $tags = $post->get_tags();
256 -
257 - $this->tags = $tags;
258 -
259 - return $this->tags;
260 - }
261 -
262 - /**
263 - * Get price
264 - *
265 - * @return float
266 - */
267 - public function get_price(): float {
268 - /*if ( ! empty( $this->price ) ) {
269 - return $this->price;
270 - }*/
271 -
272 - if ( $this->has_sale_price() ) {
273 - $price = $this->get_sale_price();
274 - } else {
275 - $price = $this->get_regular_price();
276 - }
277 -
278 - $this->price = (float) $price;
279 - $this->meta_data->{CoursePostModel::META_KEY_PRICE} = (float) $price;
280 -
281 - return apply_filters( 'learnPress/course/price', (float) $price, $this->get_id() );
282 - }
283 -
284 - /**
285 - * Get regular price
286 - * Check has data on table learnpress_courses return
287 - * if not check get from Post
288 - * Value can be string empty if not set
289 - *
290 - * @return float|string
291 - */
292 - public function get_regular_price() {
293 - $key = CoursePostModel::META_KEY_REGULAR_PRICE;
294 - if ( $this->meta_data && isset( $this->meta_data->{$key} ) ) {
295 - return $this->meta_data->{$key};
296 - }
297 -
298 - $coursePost = new CoursePostModel( $this );
299 - $this->meta_data->{$key} = $coursePost->get_regular_price();
300 -
301 - return $this->meta_data->{$key};
302 - }
303 -
304 - /**
305 - * Get sale price
306 - * Sale price can is string empty if not set
307 - * Sale price set if is number >= 0
308 - * Check has data on table learnpress_courses return
309 - * if not check get from Post
310 - *
311 - * @return float|string
312 - */
313 - public function get_sale_price() {
314 - $key = CoursePostModel::META_KEY_SALE_PRICE;
315 - if ( $this->meta_data && isset( $this->meta_data->{$key} ) ) {
316 - return $this->meta_data->{$key};
317 - }
318 -
319 - $coursePost = new CoursePostModel( $this );
320 - $sale_price = $coursePost->get_sale_price();
321 - $this->meta_data->{$key} = $sale_price;
322 -
323 - return $this->meta_data->{$key};
324 - }
325 -
326 - /**
327 - * Check course has 'sale price'
328 - *
329 - * @return bool
330 - */
331 - public function has_sale_price(): bool {
332 - $has_sale_price = false;
333 - $regular_price = $this->get_regular_price();
334 - $sale_price = $this->get_sale_price();
335 - $start_date = $this->get_sale_start();
336 - $end_date = $this->get_sale_end();
337 -
338 - if ( $sale_price !== '' && (float) $regular_price > (float) $sale_price ) {
339 - $has_sale_price = true;
340 - }
341 -
342 - // Check in days sale
343 - if ( $has_sale_price && ! empty( $start_date ) && ! empty( $end_date ) ) {
344 - $nowObj = new LP_Datetime();
345 - // Compare via timezone WP
346 - $nowStr = $nowObj->toSql( true );
347 - $now = strtotime( $nowStr );
348 - $end = strtotime( $end_date );
349 - $start = strtotime( $start_date );
350 -
351 - $has_sale_price = $now >= $start && $now <= $end;
352 - }
353 -
354 - return apply_filters( 'learnPress/course/has-sale-price', $has_sale_price, $this );
355 - }
356 -
357 - /**
358 - * Get date sale start
359 - *
360 - * @return mixed
361 - */
362 - public function get_sale_start() {
363 - return $this->get_meta_value_by_key( CoursePostModel::META_KEY_SALE_START );
364 - }
365 -
366 - /**
367 - * Get date sale end
368 - *
369 - * @return mixed
370 - */
371 - public function get_sale_end() {
372 - return $this->get_meta_value_by_key( CoursePostModel::META_KEY_SALE_END );
373 - }
374 -
375 - /**
376 - * Check if a course is Free
377 - *
378 - * @return bool
379 - */
380 - public function is_free(): bool {
381 - return $this->get_price() == 0;
382 - }
383 -
384 - /**
385 - * Check if a course is enabled Offline
386 - *
387 - * @return bool
388 - */
389 - public function is_offline(): bool {
390 - return $this->get_meta_value_by_key( CoursePostModel::META_KEY_OFFLINE_COURSE, 'no' ) === 'yes';
391 - }
392 -
393 - /**
394 - * Check option "Block course when expire" enable.
395 - *
396 - * @return bool
397 - * @since 4.2.7.3
398 - * @version 1.0.0
399 - */
400 - public function enable_block_when_expire(): bool {
401 - return $this->get_meta_value_by_key( CoursePostModel::META_KEY_BLOCK_EXPIRE_DURATION, 'no' ) === 'yes';
402 - }
403 -
404 - /**
405 - * Check option "Block course when finished" enable.
406 - *
407 - * @return bool
408 - * @since 4.2.7.6
409 - * @version 1.0.0
410 - */
411 - public function enable_block_when_finished(): bool {
412 - return $this->get_meta_value_by_key( CoursePostModel::META_KEY_BLOCK_FINISH, 'no' ) === 'yes';
413 - }
414 -
415 - /**
416 - * Get first item of course
417 - *
418 - * @return int
419 - */
420 - public function get_first_item_id(): int {
421 - if ( isset( $this->first_item_id ) ) {
422 - return $this->first_item_id;
423 - }
424 -
425 - try {
426 - $this->first_item_id = LP_Course_DB::getInstance()->get_first_item_id( $this->get_id() );
427 - } catch ( Throwable $e ) {
428 - $this->first_item_id = 0;
429 - }
430 -
431 - return $this->first_item_id;
432 - }
433 -
434 - /**
435 - * Get total items of course
436 - *
437 - * @return null|object
438 - */
439 - public function get_total_items() {
440 - if ( isset( $this->total_items ) ) {
441 - return $this->total_items;
442 - }
443 -
444 - try {
445 - $this->total_items = LP_Course_DB::getInstance()->get_total_items( $this->get_id() );
446 - } catch ( Throwable $e ) {
447 - $this->total_items = null;
448 - }
449 -
450 - return $this->total_items;
451 - }
452 -
453 - /**
454 - * Get total sections of course
455 - *
456 - * @return int
457 - * @since 4.2.7.6
458 - * @version 1.0.0
459 - */
460 - public function get_total_sections(): int {
461 - $section_items = $this->get_section_items();
462 -
463 - return count( $section_items );
464 - }
465 -
466 - /**
467 - * Get total items of course
468 - *
469 - * @return array
470 - */
471 - public function get_section_items(): array {
472 - if ( isset( $this->sections_items ) ) {
473 - return $this->sections_items;
474 - }
475 -
476 - try {
477 - $this->sections_items = $this->get_sections_and_items_course_from_db_and_sort();
478 - } catch ( Throwable $e ) {
479 - $this->sections_items = [];
480 - }
481 -
482 - return $this->sections_items;
483 - }
484 -
485 - /**
486 - * Get only items of course
487 - *
488 - * @return array
489 - * @since 4.3.2
490 - * @version 1.0.1
491 - */
492 - public function get_only_items(): array {
493 - if ( isset( $this->only_items ) ) {
494 - return $this->only_items;
495 - }
496 -
497 - $this->only_items = [];
498 -
499 - foreach ( $this->get_section_items() as $section ) {
500 - foreach ( $section->items as $item ) {
501 - $this->only_items[ $item->item_id ] = $item;
502 - }
503 - }
504 -
505 - return $this->only_items;
506 - }
507 -
508 - /**
509 - * Get section id of item
510 - *
511 - * @param int $item_id
512 - *
513 - * @return int
514 - * @since 4.2.8
515 - * @version 1.0.0
516 - */
517 - public function get_section_of_item( int $item_id ): int {
518 - $section_id = 0;
519 -
520 - $section_items = $this->get_section_items();
521 - foreach ( $section_items as $section ) {
522 - foreach ( $section->items as $item ) {
523 - $item_id_check = (int) ( $item->item_id ?? $item->id ?? 0 );
524 - if ( $item_id_check === $item_id ) {
525 - $section_id = $section->section_id ?? $section->id ?? 0;
526 - break;
527 - }
528 - }
529 - }
530 -
531 - return (int) $section_id;
532 - }
533 -
534 - /**
535 - * Get course Evaluation type.
536 - *
537 - * @return string
538 - * @since 4.2.7.3
539 - * @version 1.0.1
540 - */
541 - public function get_evaluation_type(): string {
542 - return $this->get_meta_value_by_key( CoursePostModel::META_KEY_EVALUATION_TYPE, 'evaluate_lesson' );
543 - }
544 -
545 - /**
546 - * Get course Evaluation type.
547 - *
548 - * @param string $type
549 - *
550 - * @return array
551 - * @since 4.3.2.6
552 - * @version 1.0.0
553 - */
554 - public static function get_evaluation_types( string $type = '' ): array {
555 - if ( has_filter( 'learnpress/course-evaluation/methods' ) ) {
556 - $methods = apply_filters( 'learnpress/course-evaluation/methods', [], 0 );
557 - }
558 -
559 - $types = apply_filters(
560 - 'learn-press/course/evaluation-types',
561 - array(
562 - 'evaluate_lesson' => [
563 - 'label' => __( 'Evaluate via completed lessons', 'learnpress' ),
564 - 'tip' => __( 'Course will be completed when all lessons are completed.', 'learnpress' ),
565 - ],
566 - 'evaluate_final_quiz' => [
567 - 'label' => __( 'Evaluate via final quiz', 'learnpress' ),
568 - 'tip' => __( 'Course will be completed when the final quiz is passed.', 'learnpress' ),
569 - ],
570 - 'evaluate_quiz' => [
571 - 'label' => __( 'Evaluate via quizzes', 'learnpress' ),
572 - 'tip' => __( 'Course will be completed when all quizzes are passed.', 'learnpress' ),
573 - ],
574 - 'evaluate_questions' => [
575 - 'label' => __( 'Evaluate via questions', 'learnpress' ),
576 - 'tip' => __( 'Course will be completed when all questions are answered correctly.', 'learnpress' ),
577 - ],
578 - 'evaluate_mark' => [
579 - 'label' => __( 'Evaluate via marks', 'learnpress' ),
580 - 'tip' => __( 'Course will be completed when the passing mark is achieved.', 'learnpress' ),
581 - ],
582 - ),
583 - );
584 -
585 - if ( empty( $type ) ) {
586 - return $types;
587 - } elseif ( empty( $types[ $type ] ) && ! empty( $methods[ $type ] ) ) {
588 - return $methods[ $type ];
589 - }
590 -
591 - return $types[ $type ] ?? [];
592 - }
593 -
594 - /**
595 - * Get course passing condition value.
596 - *
597 - * @return float
598 - * @since 4.2.7.3
599 - * @version 1.0.0
600 - */
601 - public function get_passing_condition(): float {
602 - return (float) $this->get_meta_value_by_key( CoursePostModel::META_KEY_PASSING_CONDITION, 80 );
603 - }
604 -
605 - /**
606 - * Get final quiz id
607 - *
608 - * @return int
609 - */
610 - public function get_final_quiz(): int {
611 - $key = '_lp_final_quiz';
612 - if ( ! empty( $this->meta_data->{$key} ) ) {
613 - return $this->meta_data->$key;
614 - }
615 -
616 - $final_quiz = 0;
617 -
618 - // Not use array_reverse, it's make change object
619 - $section_items = $this->get_section_items();
620 - $found = 0;
621 - for ( $i = count( $section_items ); $i > 0; $i-- ) {
622 - $section = $section_items[ $i - 1 ];
623 - for ( $j = count( $section->items ); $j > 0; $j-- ) {
624 - $item = $section->items[ $j - 1 ];
625 - if ( learn_press_get_post_type( $item->id ) === LP_QUIZ_CPT ) {
626 - $final_quiz = $item->id;
627 - $found = 1;
628 - break;
629 - }
630 - }
631 -
632 - if ( $found ) {
633 - break;
634 - }
635 - }
636 -
637 - $evaluation_type = $this->get_evaluation_type();
638 - if ( $evaluation_type === 'evaluate_final_quiz' ) {
639 - if ( isset( $final_quiz ) ) {
640 - update_post_meta( $this->ID, $key, $final_quiz );
641 - } else {
642 - delete_post_meta( $this->ID, $key );
643 - }
644 - }
645 -
646 - $this->meta_data->{$key} = $final_quiz;
647 -
648 - return $final_quiz;
649 - }
650 -
651 - /**
652 - * Get all sections and items from database, then handle sort
653 - * Only call when data change or not set
654 - *
655 - * @return array
656 - * @since 4.1.6.9
657 - * @version 1.0.4
658 - * @author tungnx
659 - */
660 - private function get_sections_and_items_course_from_db_and_sort(): array {
661 - $sections_items = [];
662 - $course_id = $this->get_id();
663 - $lp_course_db = LP_Course_DB::getInstance();
664 -
665 - try {
666 - $sections_results = $lp_course_db->get_sections( $course_id );
667 - $sections_items_results = $lp_course_db->get_full_sections_and_items_course( $course_id );
668 - $count_items = count( $sections_items_results );
669 - $index_items_last = $count_items - 1;
670 - $section_current = 0;
671 -
672 - /**
673 - * @var $section_order_plus int
674 - * @var $item_order_plus int
675 - * To fixed case: section order start from 0, item order start from 0
676 - */
677 - $section_order_plus = 0;
678 - $item_order_plus = 0;
679 - foreach ( $sections_items_results as $index => $sections_item ) {
680 - $section_new = $sections_item->section_id;
681 - $section_order = (int) $sections_item->section_order;
682 - if ( $index === 0 && $section_order === 0 ) {
683 - $section_order_plus = 1;
684 - }
685 -
686 - $section_order += $section_order_plus;
687 - $item = new stdClass();
688 - $item->id = $sections_item->item_id;
689 - $item->item_id = $sections_item->item_id;
690 - $item_order = (int) $sections_item->item_order;
691 - if ( $index === 0 && $item_order === 0 ) {
692 - $item_order_plus = 1;
693 - }
694 -
695 - $item_order += $item_order_plus;
696 - $item->order = $item_order;
697 - $item->item_order = $item_order;
698 - $item->type = $sections_item->item_type;
699 - $item->item_type = $sections_item->item_type;
700 - $item_tmp = LP_Course_Item::get_item( $item->id );
701 - if ( $item_tmp ) {
702 - $item->title = html_entity_decode( $item_tmp->get_title() );
703 - $item->preview = $item_tmp->is_preview();
704 - }
705 -
706 - if ( $section_new != $section_current ) {
707 - $sections_items[ $section_new ] = new stdClass();
708 - $sections_items[ $section_new ]->id = $section_new; // old field will be deprecated in future
709 - $sections_items[ $section_new ]->section_id = $section_new; // new field
710 - $sections_items[ $section_new ]->order = $section_order; // old field will be deprecated in future
711 - $sections_items[ $section_new ]->section_order = $section_order; // new field
712 - $sections_items[ $section_new ]->title = html_entity_decode( $sections_item->section_name ); // old field will be deprecated in future
713 - $sections_items[ $section_new ]->section_name = html_entity_decode( $sections_item->section_name ); // new field
714 - $sections_items[ $section_new ]->description = html_entity_decode( $sections_item->section_description ); // old field will be deprecated in future
715 - $sections_items[ $section_new ]->section_description = html_entity_decode( $sections_item->section_description ); // new field
716 - $sections_items[ $section_new ]->items = [];
717 -
718 - // Sort item by item_order
719 - if ( $section_current != 0 ) {
720 - usort(
721 - $sections_items[ $section_current ]->items,
722 - function ( $item1, $item2 ) {
723 - return $item1->item_order - $item2->item_order;
724 - }
725 - );
726 - }
727 -
728 - $section_current = $section_new;
729 - }
730 -
731 - $sections_items[ $section_new ]->items[ $item->item_id ] = $item;
732 -
733 - if ( $index_items_last === $index ) {
734 - usort(
735 - $sections_items[ $section_current ]->items,
736 - function ( $item1, $item2 ) {
737 - return $item1->item_order - $item2->item_order;
738 - }
739 - );
740 - }
741 - }
742 -
743 - // Check case if section empty items
744 - foreach ( $sections_results as $section ) {
745 - $section_id = $section->section_id;
746 - if ( isset( $sections_items[ $section_id ] ) ) {
747 - continue;
748 - }
749 -
750 - $section_obj = new stdClass();
751 - $section_obj->id = $section_id;
752 - $section_obj->section_id = $section_id;
753 - $section_obj->order = $section->section_order;
754 - $section_obj->section_order = $section->section_order;
755 - $section_obj->title = html_entity_decode( $section->section_name );
756 - $section_obj->section_name = html_entity_decode( $section->section_name );
757 - $section_obj->description = html_entity_decode( $section->section_description );
758 - $section_obj->section_description = html_entity_decode( $section->section_description );
759 - $section_obj->items = [];
760 - $sections_items[ $section_id ] = $section_obj;
761 - }
762 -
763 - // Sort section by section_order
764 - usort(
765 - $sections_items,
766 - function ( $section1, $section2 ) {
767 - return $section1->section_order - $section2->section_order;
768 - }
769 - );
770 - } catch ( Throwable $e ) {
771 - LP_Debug::error_log( $e );
772 - }
773 -
774 - return $sections_items;
775 - }
776 -
777 - /**
778 - * Get permalink course
779 - *
780 - * @return string
781 - */
782 - public function get_permalink(): string {
783 - if ( ! empty( $this->permalink ) ) {
784 - return $this->permalink;
785 - }
786 -
787 - try {
788 - $coursePostModel = new CoursePostModel( $this );
789 - $this->permalink = $coursePostModel->get_permalink();
790 - } catch ( Throwable $e ) {
791 - $this->permalink = '';
792 - }
793 -
794 - return $this->permalink;
795 - }
796 -
797 - /**
798 - * Get value option No enroll requirement
799 - *
800 - * @return mixed
801 - */
802 - public function get_no_enroll_requirement() {
803 - return $this->get_meta_value_by_key( CoursePostModel::META_KEY_NO_REQUIRED_ENROLL, 'no' );
804 - }
805 -
806 - /**
807 - * Get description of Course
808 - *
809 - * @return string
810 - */
811 - public function get_description(): string {
812 - $course_post = new CoursePostModel( $this );
813 -
814 - return $course_post->get_the_content();
815 - }
816 -
817 - /**
818 - * Get short description of Course
819 - *
820 - * @return string
821 - */
822 - public function get_short_description(): string {
823 - $course_post = new CoursePostModel( $this );
824 -
825 - return $course_post->get_the_excerpt();
826 - }
827 -
828 - /**
829 - * Get value option No enroll requirement
830 - *
831 - * @return bool
832 - */
833 - public function has_no_enroll_requirement(): bool {
834 - return $this->get_no_enroll_requirement() === 'yes';
835 - }
836 -
837 - /**
838 - * Get value from meta data by key
839 - *
840 - * @param string $key
841 - * @param mixed|false $default_value
842 - * @param bool $single
843 - *
844 - * @return false|mixed
845 - * @since 4.2.6.9
846 - * @version 1.0.2
847 - */
848 - public function get_meta_value_by_key( string $key, $default_value = false, bool $single = true ) {
849 - if ( $this->meta_data instanceof stdClass && isset( $this->meta_data->{$key} ) ) {
850 - $value = maybe_unserialize( $this->meta_data->{$key} );
851 - } else {
852 - $coursePost = new CoursePostModel( $this );
853 - $value = $coursePost->get_meta_value_by_key( $key, $default_value, $single );
854 - }
855 -
856 - $value = maybe_unserialize( $value );
857 - $this->meta_data->{$key} = $value;
858 -
859 - return $value;
860 - }
861 -
862 - /**
863 - * Check course is in stock
864 - * True is in stock, False is out of stock
865 - *
866 - * @return mixed
867 - * @since 3.0.0
868 - * @version 1.0.1
869 - */
870 - public function is_in_stock() {
871 - $in_stock = true;
872 - $max_allowed = (int) $this->get_meta_value_by_key( CoursePostModel::META_KEY_MAX_STUDENTS, 0 );
873 -
874 - if ( $max_allowed ) {
875 - $in_stock = $max_allowed > $this->get_total_user_enrolled_or_purchased();
876 - }
877 -
878 - return apply_filters( 'learn-press/is-in-stock', $in_stock, $this->get_id() );
879 - }
880 -
881 - /**
882 - * Check course is enable repurchase
883 - *
884 - * @return bool
885 - * @since 4.2.7.2
886 - * @version 1.0.1
887 - */
888 - public function enable_allow_repurchase(): bool {
889 - $enable = $this->get_meta_value_by_key( CoursePostModel::META_KEY_ALLOW_COURSE_REPURCHASE, 'no' );
890 -
891 - return 'yes' === $enable;
892 - }
893 -
894 - /**
895 - * Type repurchase
896 - *
897 - * @return string
898 - * @since 4.2.7.3
899 - * @version 1.0.0
900 - */
901 - public function get_type_repurchase(): string {
902 - return $this->get_meta_value_by_key( CoursePostModel::META_KEY_COURSE_REPURCHASE_OPTION, 'reset' );
903 - }
904 -
905 - /**
906 - * Get external link
907 - *
908 - * @return string
909 - */
910 - public function get_external_link(): string {
911 - return esc_url_raw(
912 - $this->get_meta_value_by_key( CoursePostModel::META_KEY_EXTERNAL_LINK_BY_COURSE, '' )
913 - );
914 - }
915 -
916 - /**
917 - * Get item's link
918 - * @move from LP_Abstract_Course
919 - *
920 - * @param int $item_id
921 - * @param string $item_type
922 - *
923 - * @return string
924 - * @since 3.0.0
925 - * @version 1.0.2
926 - */
927 - public function get_item_link( int $item_id, string $item_type = '' ): string {
928 - if ( empty( $item_type ) ) {
929 - $item_type = get_post_type( $item_id );
930 - }
931 - $course_permalink = trailingslashit( $this->get_permalink() );
932 - $item_slug = get_post_field( 'post_name', $item_id );
933 -
934 - $slug_prefixes = apply_filters(
935 - 'learn-press/course/custom-item-prefixes',
936 - array(
937 - LP_QUIZ_CPT => sanitize_title_with_dashes( LP_Settings::get_option( 'quiz_slug', 'quizzes' ) ),
938 - LP_LESSON_CPT => sanitize_title_with_dashes( LP_Settings::get_option( 'lesson_slug', 'lessons' ) ),
939 - ),
940 - $this->get_id()
941 - );
942 -
943 - $slug_prefix = trailingslashit( $slug_prefixes[ $item_type ] ?? '' );
944 - $item_link = trailingslashit( $course_permalink . $slug_prefix . $item_slug );
945 -
946 - return apply_filters( 'learn-press/course/item-link', $item_link, $item_id, $this );
947 - }
948 -
949 - /**
950 - * Get total user enrolled, purchased or finished
951 - *
952 - * @move from LP_Abstract_Course
953 - * @return int
954 - * @version 1.0.1
955 - * @since 4.1.4
956 - */
957 - public function get_total_user_enrolled_or_purchased(): int {
958 - $total = 0;
959 - $lp_course_cache = new LP_Course_Cache( true );
960 -
961 - try {
962 - $total = $lp_course_cache->get_total_students_enrolled_or_purchased( $this->get_id() );
963 - if ( false !== $total ) {
964 - return $total;
965 - }
966 -
967 - $lp_course_db = LP_Course_DB::getInstance();
968 - $total = $lp_course_db->get_total_user_enrolled_or_purchased( $this->get_id() );
969 - $lp_course_cache->set_total_students_enrolled_or_purchased( $this->get_id(), $total );
970 - } catch ( Throwable $e ) {
971 - LP_Debug::error_log( $e );
972 - }
973 -
974 - return $total;
975 - }
976 -
977 - /**
978 - * Get fake students.
979 - *
980 - * @return int
981 - */
982 - public function get_fake_students(): int {
983 - return (int) $this->get_meta_value_by_key( CoursePostModel::META_KEY_STUDENTS, 0 );
984 - }
985 -
986 - /**
987 - * Count number of students enrolled course.
988 - * Check global settings `enrolled_students_number`
989 - * and add the fake value if both are set.
990 - *
991 - * @return int
992 - * @move from LP_Abstract_Course
993 - */
994 - public function count_students(): int {
995 - $total = $this->get_total_user_enrolled_or_purchased();
996 - $total += $this->get_fake_students();
997 -
998 - return $total;
999 - }
1000 -
1001 - /**
1002 - * Count total items in Course
1003 - * item_type empty will return all items if exists.
1004 - *
1005 - * @param string $item_type
1006 - *
1007 - * @return int
1008 - * @since 4.2.7.3
1009 - * @version 1.0.1
1010 - */
1011 - public function count_items( string $item_type = '' ): int {
1012 - $count = 0;
1013 -
1014 - $total_items = $this->get_total_items();
1015 - if ( empty( $item_type ) ) {
1016 - $count = $total_items->count_items ?? 0;
1017 - } elseif ( isset( $total_items->{$item_type} ) ) {
1018 - return $total_items->{$item_type};
1019 - }
1020 -
1021 - return $count;
1022 - }
1023 -
1024 - /**
1025 - * Get Duration of course
1026 - * Timestamp in second
1027 - *
1028 - * @return string
1029 - */
1030 - public function get_duration(): string {
1031 - return $this->get_meta_value_by_key( CoursePostModel::META_KEY_DURATION, '10 week' );
1032 - }
1033 -
1034 - /**
1035 - * Check user can enroll course.
1036 - * @move from can_enroll_course method of LP_User class, since 4.1.1
1037 - *
1038 - * @param UserModel|false $user
1039 - *
1040 - * @return bool|WP_Error
1041 - * @since 4.2.7.3
1042 - * @version 1.0.2
1043 - */
1044 - public function can_enroll( $user ) {
1045 - $can_enroll = true;
1046 - $error_code = '';
1047 -
1048 - $user_id = 0;
1049 - if ( $user instanceof UserModel ) {
1050 - $user_id = $user->get_id();
1051 - }
1052 -
1053 - try {
1054 - if ( ! in_array( $this->post_status, [ 'publish', 'private' ] ) ) {
1055 - $error_code = 'course_not_publish';
1056 - throw new Exception( __( 'The course is not public', 'learnpress' ) );
1057 - }
1058 -
1059 - $userCourseModel = UserCourseModel::find( $user_id, $this->get_id(), true );
1060 - $enable_no_required_enroll = $this->has_no_enroll_requirement();
1061 - $out_of_stock = ! $this->is_in_stock();
1062 -
1063 - // Case user can retake course.
1064 - if ( $userCourseModel && $userCourseModel->can_retake() ) {
1065 - $error_code = 'course_can_retry';
1066 - throw new Exception( esc_html__( 'Course can retake.', 'learnpress' ) );
1067 - }
1068 -
1069 - // Case course is out of stock, show message when user is not login or user_item not exits
1070 - if ( $out_of_stock &&
1071 - ( ! $user || ! $userCourseModel || ! $userCourseModel->has_enrolled_or_finished() ) ) {
1072 - $error_code = 'course_out_of_stock';
1073 - throw new Exception( __( 'The course is full of students.', 'learnpress' ) );
1074 - }
1075 -
1076 - // Case user is logged in and user_item exists
1077 - if ( $userCourseModel && $user ) {
1078 - if ( $userCourseModel->has_enrolled() ) {
1079 - $error_code = 'course_is_enrolled';
1080 - throw new Exception( __( 'This course is already enrolled!', 'learnpress' ) );
1081 - } elseif ( $userCourseModel->has_finished() ) {
1082 - $error_code = 'course_is_finished';
1083 - throw new Exception( __( 'The course is finished.', 'learnpress' ) );
1084 - }
1085 - }
1086 -
1087 - if ( $enable_no_required_enroll ) {
1088 - if ( ! $user ) {
1089 - $error_code = 'course_is_no_required_enroll_not_login';
1090 - throw new Exception(
1091 - __(
1092 - 'Enrollment in the course is not mandatory. You can access course for learning now.',
1093 - 'learnpress'
1094 - )
1095 - );
1096 - } else {
1097 -
1098 - }
1099 - } else {
1100 - if ( ! empty( $this->get_external_link() )
1101 - && ( ! $user || ! $userCourseModel
1102 - || $userCourseModel->get_status() === UserItemModel::STATUS_CANCEL )
1103 - && ! $this->is_offline() ) {
1104 - $error_code = 'course_is_external';
1105 - throw new Exception( __( 'The course is external', 'learnpress' ) );
1106 - }
1107 -
1108 - if ( ! $this->is_free() ) {
1109 - if ( ! $user ) {
1110 - $error_code = 'course_is_not_purchased_not_login';
1111 - throw new Exception( __( 'The course is not purchased.', 'learnpress' ) );
1112 - } elseif ( ! $userCourseModel || ! $userCourseModel->has_purchased() ) {
1113 - $error_code = 'course_is_not_purchased';
1114 - throw new Exception( __( 'The course is not purchased.', 'learnpress' ) );
1115 - }
1116 - }
1117 - }
1118 - } catch ( Throwable $e ) {
1119 - if ( empty( $error_code ) ) {
1120 - $error_code = 'course_can_not_enroll';
1121 - }
1122 - $can_enroll = new WP_Error( $error_code, $e->getMessage() );
1123 - }
1124 -
1125 - // Hook old
1126 - if ( has_filter( 'learn-press/user/can-enroll-course' ) ) {
1127 - $output = new stdClass();
1128 - $output->check = true;
1129 - $output->message = '';
1130 - if ( $can_enroll instanceof WP_Error ) {
1131 - $output->check = false;
1132 - $output->message = $can_enroll->get_error_message();
1133 - }
1134 -
1135 - $course_old = learn_press_get_course( $this->get_id() );
1136 - $user_old = learn_press_get_user( $user_id );
1137 - $output = apply_filters( 'learn-press/user/can-enroll-course', $output, $course_old, false, $user_old );
1138 - if ( $output === false ) {
1139 - $can_enroll = new WP_Error( '', '' );
1140 - } elseif ( ! $output->check && $output->message ) {
1141 - $can_enroll = new WP_Error( 'error_custom', $output->message );
1142 - }
1143 - //_deprecated_function( 'The learn-press/user/can-enroll-course filter', '4.2.7.3', 'learn-press/user/can-enroll/course' );
1144 - }
1145 -
1146 - return apply_filters( 'learn-press/user/can-enroll/course', $can_enroll, $this, $user );
1147 - }
1148 -
1149 - /**
1150 - * Check user can purchase course.
1151 - * @move from can_purchase_course method of LP_User class, since 4.0.8
1152 - * @use LP_User::can_purchase_course
1153 - *
1154 - * @param UserModel|false $user
1155 - *
1156 - * @return bool|WP_Error
1157 - * @since 4.2.7.3
1158 - * @version 1.0.0
1159 - */
1160 - public function can_purchase( $user ) {
1161 - $can_purchase = true;
1162 - $error_code = '';
1163 -
1164 - $user_id = 0;
1165 - if ( $user instanceof UserModel ) {
1166 - $user_id = $user->get_id();
1167 - }
1168 -
1169 - try {
1170 - $can_enroll = $this->can_enroll( $user );
1171 - if ( $can_enroll instanceof WP_Error ) {
1172 - $error_code_return = [
1173 - 'course_is_not_purchased_not_login',
1174 - 'course_is_not_purchased',
1175 - 'course_is_enrolled',
1176 - 'course_is_finished',
1177 - ];
1178 - if ( ! in_array( $can_enroll->get_error_code(), $error_code_return ) ) {
1179 - $error_code = $can_enroll->get_error_code();
1180 - throw new Exception( $can_enroll->get_error_message() );
1181 - }
1182 - }
1183 -
1184 - if ( $this->is_free() ) {
1185 - $error_code = 'course_is_free';
1186 - throw new Exception( __( 'The course is free.', 'learnpress' ) );
1187 - }
1188 -
1189 - $enable_no_required_enroll = $this->has_no_enroll_requirement();
1190 - if ( $enable_no_required_enroll ) {
1191 - $error_code = 'course_is_no_required_enroll';
1192 - throw new Exception(
1193 - __( 'Enrollment in the course is not mandatory. You can access course for learning now.', 'learnpress' )
1194 - );
1195 - }
1196 -
1197 - $userCourseModel = UserCourseModel::find( $user_id, $this->get_id(), true );
1198 - if ( $user ) {
1199 - if ( $userCourseModel ) {
1200 - if ( $userCourseModel->has_purchased() ) {
1201 - $error_code = 'course_purchased';
1202 - throw new Exception( __( 'Course is purchased', 'learnpress' ) );
1203 - }
1204 -
1205 - if ( $this->enable_allow_repurchase() ) {
1206 - if ( $userCourseModel->has_enrolled() && $userCourseModel->timestamp_remaining_duration() !== 0 ) {
1207 - $error_code = 'course_is_enrolled';
1208 - throw new Exception( 'This course is already enrolled!' );
1209 - }
1210 - } else {
1211 - if ( $userCourseModel->has_enrolled_or_finished() ) {
1212 - $error_code = 'course_is_enrolled_or_finished';
1213 - throw new Exception( __( 'Course is enrolled or finished', 'learnpress' ) );
1214 - }
1215 - }
1216 - }
1217 - }
1218 - } catch ( Throwable $e ) {
1219 - if ( empty( $error_code ) ) {
1220 - $error_code = 'course_can_not_purchase';
1221 - }
1222 - $can_purchase = new WP_Error( $error_code, $e->getMessage() );
1223 - }
1224 -
1225 - // Hook old
1226 - if ( has_filter( 'learn-press/user/can-purchase-course' ) ) {
1227 - $can_purchase = apply_filters( 'learn-press/user/can-purchase-course', $can_purchase, $user_id, $this->get_id() );
1228 - if ( $can_purchase === false ) {
1229 - $can_purchase = new WP_Error( '', '' );
1230 - }
1231 - //_deprecated_function( 'The learn-press/user/can-purchase-course filter', '4.2.7.3', 'learn-press/user/can-purchase/course' );
1232 - }
1233 -
1234 - return apply_filters( 'learn-press/user/can-purchase/course', $can_purchase, $this, $user );
1235 - }
1236 -
1237 - /**
1238 - * Check user is author or co-in of course.
1239 - *
1240 - * @param UserModel|false $userModel
1241 - *
1242 - * @return bool
1243 - * @since 4.2.7.6
1244 - * @version 1.0.1
1245 - */
1246 - public function check_user_is_author( $userModel ): bool {
1247 - $is_author = false;
1248 -
1249 - if ( $userModel instanceof UserModel
1250 - && $userModel->get_id() === $this->post_author ) {
1251 - $is_author = true;
1252 - }
1253 -
1254 - return apply_filters( 'learn-press/course/is-author', $is_author, $this, $userModel );
1255 - }
1256 -
1257 - /**
1258 - * Get item model assigned to this course
1259 - *
1260 - * @return mixed|false|null|WP_Post|PostModel
1261 - * @since v4.2.7.6
1262 - * @version 1.0.1
1263 - */
1264 - public function get_item_model( int $item_id, string $item_type ) {
1265 - try {
1266 - $item = false;
1267 -
1268 - switch ( $item_type ) {
1269 - case LP_LESSON_CPT:
1270 - $item = LessonPostModel::find( $item_id, true );
1271 - break;
1272 - case LP_QUIZ_CPT:
1273 - $item = QuizPostModel::find( $item_id, true );
1274 - break;
1275 - case LP_QUESTION_CPT:
1276 - break;
1277 - default:
1278 - $item = apply_filters( 'learn-press/course/get-item-model', $item, $item_id, $item_type, $this );
1279 - break;
1280 - }
1281 -
1282 - // If not defined class, get post default
1283 - if ( ! $item ) {
1284 - $filter = new PostFilter();
1285 - $filter->ID = $item_id;
1286 - $filter->post_type = $item_type;
1287 - $item = PostModel::get_item_model_from_db( $filter );
1288 - }
1289 - } catch ( Exception $e ) {
1290 - LP_Debug::error_log( $e );
1291 - }
1292 -
1293 - return $item;
1294 - }
1295 -
1296 - /**
1297 - * Get item model if query success.
1298 - * If not exists, return false.
1299 - * If exists, return PostModel.
1300 - *
1301 - * @param LP_Course_JSON_Filter $filter
1302 - *
1303 - * @return CourseModel|false|static
1304 - * @since 4.2.6.9
1305 - * @version 1.0.2
1306 - */
1307 - public static function get_item_model_from_db( LP_Course_JSON_Filter $filter ) {
1308 - $course_model = false;
1309 -
1310 - try {
1311 - $filter->only_fields = [ 'json', 'post_content' ];
1312 -
1313 - $course_rs = self::get_course_from_db( $filter );
1314 - if ( $course_rs instanceof stdClass && isset( $course_rs->json ) ) {
1315 - $course_obj = LP_Helper::json_decode( $course_rs->json );
1316 - $course_model = new static( $course_obj );
1317 - //$course_model->json = $course_rs->json;
1318 - $course_model->post_content = $course_rs->post_content;
1319 - $course_model->get_author_model();
1320 - }
1321 - } catch ( Throwable $e ) {
1322 - LP_Debug::error_log( $e );
1323 - }
1324 -
1325 - return $course_model;
1326 - }
1327 -
1328 - /**
1329 - * Get course by ID
1330 - *
1331 - * @param int $course_id
1332 - * @param bool $check_cache
1333 - *
1334 - * @return false|CourseModel|static
1335 - * @since 4.2.6.9
1336 - * @version 1.0.2
1337 - */
1338 - public static function find( int $course_id, bool $check_cache = false ) {
1339 - if ( ! $course_id ) {
1340 - return false;
1341 - }
1342 -
1343 - $filter_course = new LP_Course_JSON_Filter();
1344 - $filter_course->ID = $course_id;
1345 - $key_cache = "courseModel/find/id/{$course_id}";
1346 - $lp_course_cache = new LP_Course_Cache();
1347 -
1348 - // Check cache
1349 - if ( $check_cache ) {
1350 - $course_model = $lp_course_cache->get_cache( $key_cache );
1351 - if ( $course_model instanceof CourseModel ) {
1352 - return $course_model;
1353 - }
1354 - }
1355 -
1356 - // Query database no cache.
1357 - $course_model = self::get_item_model_from_db( $filter_course );
1358 - if ( false === $course_model ) { // Find on table posts
1359 - $course_rs = CoursePostModel::find( $course_id );
1360 - if ( $course_rs instanceof CoursePostModel ) {
1361 - $course_model = new static( $course_rs );
1362 - }
1363 - }
1364 -
1365 - // Set cache
1366 - if ( $course_model instanceof CourseModel ) {
1367 - $lp_course_cache->set_cache( $key_cache, $course_model );
1368 - }
1369 -
1370 - return $course_model;
1371 - }
1372 -
1373 - /**
1374 - * Get course from table learnpress_courses
1375 - *
1376 - * @return array|object|stdClass|null
1377 - * @throws Exception
1378 - */
1379 - private static function get_course_from_db( LP_Course_JSON_Filter $filter ) {
1380 - $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1381 - $lp_course_json_db->get_query_single_row( $filter );
1382 - $query_single_row = $lp_course_json_db->get_courses( $filter );
1383 -
1384 - return $lp_course_json_db->wpdb->get_row( $query_single_row );
1385 - }
1386 -
1387 - /**
1388 - * Save course data to table learnpress_courses.
1389 - *
1390 - * @param bool $force_save
1391 - *
1392 - * @return CourseModel
1393 - * @throws Exception
1394 - * @since 4.2.6.9
1395 - * @version 1.0.3
1396 - */
1397 - public function save( bool $force_save = false ): CourseModel {
1398 - // Check permission
1399 - if ( ! $force_save ) {
1400 - $this->check_permission();
1401 - }
1402 -
1403 - $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1404 -
1405 - $courseObjToJSON = clone $this;
1406 - unset( $courseObjToJSON->post_content );
1407 - unset( $courseObjToJSON->json );
1408 - $this->json = json_encode( $courseObjToJSON, JSON_UNESCAPED_UNICODE );
1409 - $data = get_object_vars( $this );
1410 -
1411 - if ( ! isset( $data['ID'] ) ) {
1412 - throw new Exception( 'Course ID is invalid!' );
1413 - }
1414 -
1415 - $filter = new LP_Course_JSON_Filter();
1416 - $filter->ID = $this->ID;
1417 - $filter->only_fields = [ 'ID' ];
1418 - $course_rs = self::get_course_from_db( $filter );
1419 - // Check if exists course id.
1420 - if ( empty( $course_rs ) ) { // Insert data.
1421 - $lp_course_json_db->insert_data( $data );
1422 - } else { // Update data.
1423 - $lp_course_json_db->update_data( $data );
1424 - }
1425 -
1426 - // Clear cache
1427 - $this->clean_caches();
1428 -
1429 - return $this;
1430 - }
1431 -
1432 - /**
1433 - * Delete row
1434 - *
1435 - * @throws Exception
1436 - */
1437 - public function delete() {
1438 - $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1439 - $filter = new LP_Course_JSON_Filter();
1440 - $filter->where[] = $lp_course_json_db->wpdb->prepare( 'AND ID = %d', $this->ID );
1441 - $filter->collection = $lp_course_json_db->tb_lp_courses;
1442 - $lp_course_json_db->delete_execute( $filter );
1443 -
1444 - // Clear cache
1445 - $this->clean_caches();
1446 - }
1447 -
1448 - /**
1449 - * Clean caches
1450 - *
1451 - * @return void
1452 - * @version 1.0.0
1453 - * @since 4.2.7.4
1454 - */
1455 - public function clean_caches() {
1456 - $key_cache = "courseModel/find/id/{$this->ID}";
1457 - $lp_course_cache = new LP_Course_Cache();
1458 - $lp_course_cache->clear( $key_cache );
1459 -
1460 - // Clear cache image urls, store with many sizes
1461 - $img_urls_key_cache = "image_urls/{$this->ID}";
1462 - $lp_course_cache->clear_cache_on_group( $img_urls_key_cache );
1463 - }
1464 -
1465 - /**
1466 - * Check permission to update/create course
1467 - *
1468 - * @throws Exception
1469 - */
1470 - public function check_permission() {
1471 - $coursePostModel = new CoursePostModel( $this );
1472 - if ( ! $coursePostModel->check_capabilities_update() ) {
1473 - throw new Exception( 'You do not have permission to update this course!' );
1474 - }
1475 - }
1476 -
1477 - /**
1478 - * Return course's items support.
1479 - * To replace learn_press_course_get_support_item_types()
1480 - * Should add hook on addons before use this function.
1481 - *
1482 - * @return array
1483 - * @since 4.2.7.4
1484 - * @version 1.0.1
1485 - */
1486 - public static function item_types_support(): array {
1487 - $item_types = [
1488 - LP_LESSON_CPT,
1489 - LP_QUIZ_CPT,
1490 - ];
1491 -
1492 - // Hook old
1493 - if ( has_filter( 'learn-press/course-item-type' ) ) {
1494 - $item_types = apply_filters( 'learn-press/course-item-type', $item_types );
1495 - }
1496 -
1497 - $item_types = apply_filters( 'learn-press/course/item-types-support', $item_types );
1498 -
1499 - // set types unique
1500 - return array_unique( $item_types );
1501 - }
1502 -
1503 - /**
1504 - * Get label of item type
1505 - *
1506 - * @param string $item_type
1507 - *
1508 - * @return string
1509 - * @since 4.2.8.6
1510 - * @version 1.0.0
1511 - */
1512 - public static function item_types_label( string $item_type = '' ): string {
1513 - switch ( $item_type ) {
1514 - case LP_LESSON_CPT:
1515 - $label = __( 'Lesson', 'learnpress' );
1516 - break;
1517 - case LP_QUIZ_CPT:
1518 - $label = __( 'Quiz', 'learnpress' );
1519 - break;
1520 - default:
1521 - $label = ucfirst( strtolower( str_replace( [ 'lp_', '_' ], '', $item_type ) ) );
1522 - $label = apply_filters( 'learn-press/course/item-type-label', $label, $item_type );
1523 - break;
1524 - }
1525 -
1526 - return $label;
1527 - }
1528 -}
1 +<?php
2 +
3 +/**
4 + * Class Course Model
5 + * Purpose: Use to map property separate table learnpress_course
6 + * Field json for store all value of single course.
7 + * Another fields for query list courses faster
8 + *
9 + * @package LearnPress/Classes
10 + * @version 1.0.5
11 + * @since 4.2.6.9
12 + */
13 +
14 +namespace LearnPress\Models;
15 +
16 +use Exception;
17 +use LearnPress\Databases\Course\CourseSectionItemsDB;
18 +use LearnPress\Filters\PostFilter;
19 +use LearnPress\Models\UserItems\UserCourseModel;
20 +use LearnPress\Models\UserItems\UserItemModel;
21 +use LP_Course_Cache;
22 +use LP_Course_DB;
23 +use LP_Course_Item;
24 +use LP_Course_JSON_DB;
25 +use LP_Course_JSON_Filter;
26 +use LP_Datetime;
27 +use LP_Debug;
28 +use LP_Helper;
29 +use LP_Section_Items_Filter;
30 +use LP_Settings;
31 +use stdClass;
32 +use Throwable;
33 +use WP_Error;
34 +use WP_Post;
35 +
36 +class CourseModel {
37 + /**
38 + * Auto increment, Primary key
39 + *
40 + * @var int
41 + */
42 + public $ID = 0;
43 + /**
44 + * @var string author id, foreign key
45 + */
46 + public $post_author = 0;
47 + /**
48 + * @var string post date gmt
49 + */
50 + public $post_date_gmt = null;
51 + /**
52 + * @var string post content
53 + */
54 + public $post_content = '';
55 + /**
56 + * @var string Post title
57 + */
58 + public $post_title = '';
59 + /**
60 + * @var string Post Status (publish, draft, ...)
61 + */
62 + public $post_status = '';
63 + /**
64 + * @var string Post name (slug for link)
65 + */
66 + public $post_name = '';
67 + /**
68 + * @var float price only using for filter courses, don't use for course detail
69 + * Because price can change by date if set schedule sale
70 + */
71 + public $price_to_sort = 0;
72 + public $is_sale = 0;
73 + /**
74 + * @var string JSON Store all data a single course
75 + */
76 + public $json = null; // Only set when save, don't set when get
77 + /**
78 + * @var string lang of Course
79 + */
80 + public $lang = null;
81 + /********** Field not on table **********/
82 + /**
83 + * @var stdClass all meta data
84 + */
85 + public $meta_data = null;
86 + public $image_url = null;
87 + public $permalink = '';
88 + public $categories;
89 + public $tags;
90 + private $price; // Not save in database, must auto reload calculate
91 + private $passing_condition = '';
92 + public $post_excerpt = '';
93 + /**
94 + * @var int ID of first item
95 + */
96 + public $first_item_id;
97 + /**
98 + * @var null|object info total items {'count_items': 20, 'lp_lesson': 10, 'lp_quiz': 10, ...}
99 + */
100 + public $total_items;
101 + /**
102 + * @var array list sections items
103 + */
104 + public $sections_items;
105 + /**
106 + * @var array|null cached items of course (per instance)
107 + */
108 + protected array $only_items;
109 +
110 + /**
111 + * If data get from database, map to object.
112 + * Else create new object to save data to database.
113 + *
114 + * @param array|object|mixed $data
115 + */
116 + public function __construct( $data = null ) {
117 + if ( $data ) {
118 + $this->map_to_object( $data );
119 + }
120 +
121 + if ( is_null( $this->meta_data ) ) {
122 + $this->meta_data = new stdClass();
123 + }
124 + }
125 +
126 + /**
127 + * Map array, object data to CourseModel.
128 + * Use for data get from database.
129 + *
130 + * @param array|object|mixed $data
131 + *
132 + * @return CourseModel
133 + */
134 + public function map_to_object( $data ): CourseModel {
135 + foreach ( $data as $key => $value ) {
136 + if ( property_exists( $this, $key ) ) {
137 + $this->{$key} = $value;
138 + }
139 + }
140 +
141 + return $this;
142 + }
143 +
144 + /**
145 + * Get course id
146 + *
147 + * @return int
148 + */
149 + public function get_id(): int {
150 + return $this->ID;
151 + }
152 +
153 + /**
154 + * Get post model
155 + *
156 + * @return CoursePostModel
157 + * @since 4.3.6
158 + * @version 1.0.0
159 + */
160 + public function get_post_model(): CoursePostModel {
161 + return new CoursePostModel( $this );
162 + }
163 +
164 + public function get_title(): string {
165 + $course_post = new CoursePostModel( $this );
166 +
167 + return $course_post->get_the_title();
168 + }
169 +
170 + /**
171 + * Get image url
172 + * if not check get from Post
173 + *
174 + * @param string|int[] $size
175 + *
176 + * @return string
177 + * @since 4.2.6.9
178 + * @version 1.0.2
179 + */
180 + public function get_image_url( $size = 'post-thumbnail' ): string {
181 + /**
182 + * Comment code isset( $this->image_url )
183 + * To apply for many size on a course
184 + * To apply cache need handle cache before, where set size for image.
185 + */
186 + /*if ( isset( $this->image_url ) ) {
187 + return $this->image_url;
188 + }*/
189 +
190 + $post = new CoursePostModel( $this );
191 + $image_url = $post->get_image_url( $size );
192 +
193 + $this->image_url = $image_url;
194 +
195 + return $image_url;
196 + }
197 +
198 + /**
199 + * Get author model
200 + * Check has data on table learnpress_courses return
201 + * if not check get from Post
202 + *
203 + * @return UserModel|false
204 + */
205 + public function get_author_model() {
206 + $post = new CoursePostModel( $this );
207 +
208 + return $post->get_author_model();
209 + }
210 +
211 + /**
212 + * Get status of course
213 + *
214 + * @return string
215 + * @since 4.2.7.3
216 + * @version 1.0.0
217 + */
218 + public function get_status(): string {
219 + return $this->post_status;
220 + }
221 +
222 + /**
223 + * Get categories
224 + * Check has data on table learnpress_courses return
225 + * if not check get from Post
226 + *
227 + * @return array
228 + */
229 + public function get_categories(): array {
230 + if ( isset( $this->categories ) ) {
231 + return $this->categories;
232 + }
233 +
234 + $post = new PostModel( $this );
235 + $categories = $post->get_categories();
236 +
237 + $this->categories = $categories;
238 +
239 + return $this->categories;
240 + }
241 +
242 + /**
243 + * Get tags of course.
244 + *
245 + * @return array
246 + * @since 4.2.7.2
247 + * @version 1.0.0
248 + */
249 + public function get_tags(): array {
250 + if ( isset( $this->tags ) ) {
251 + return $this->tags;
252 + }
253 +
254 + $post = new PostModel( $this );
255 + $tags = $post->get_tags();
256 +
257 + $this->tags = $tags;
258 +
259 + return $this->tags;
260 + }
261 +
262 + /**
263 + * Get price
264 + *
265 + * @return float
266 + */
267 + public function get_price(): float {
268 + /*if ( ! empty( $this->price ) ) {
269 + return $this->price;
270 + }*/
271 +
272 + if ( $this->has_sale_price() ) {
273 + $price = $this->get_sale_price();
274 + } else {
275 + $price = $this->get_regular_price();
276 + }
277 +
278 + $this->price = (float) $price;
279 + $this->meta_data->{CoursePostModel::META_KEY_PRICE} = (float) $price;
280 +
281 + return apply_filters( 'learnPress/course/price', (float) $price, $this->get_id() );
282 + }
283 +
284 + /**
285 + * Get regular price
286 + * Check has data on table learnpress_courses return
287 + * if not check get from Post
288 + * Value can be string empty if not set
289 + *
290 + * @return float|string
291 + */
292 + public function get_regular_price() {
293 + $key = CoursePostModel::META_KEY_REGULAR_PRICE;
294 + if ( $this->meta_data && isset( $this->meta_data->{$key} ) ) {
295 + return $this->meta_data->{$key};
296 + }
297 +
298 + $coursePost = new CoursePostModel( $this );
299 + $this->meta_data->{$key} = $coursePost->get_regular_price();
300 +
301 + return $this->meta_data->{$key};
302 + }
303 +
304 + /**
305 + * Get sale price
306 + * Sale price can is string empty if not set
307 + * Sale price set if is number >= 0
308 + * Check has data on table learnpress_courses return
309 + * if not check get from Post
310 + *
311 + * @return float|string
312 + */
313 + public function get_sale_price() {
314 + $key = CoursePostModel::META_KEY_SALE_PRICE;
315 + if ( $this->meta_data && isset( $this->meta_data->{$key} ) ) {
316 + return $this->meta_data->{$key};
317 + }
318 +
319 + $coursePost = new CoursePostModel( $this );
320 + $sale_price = $coursePost->get_sale_price();
321 + $this->meta_data->{$key} = $sale_price;
322 +
323 + return $this->meta_data->{$key};
324 + }
325 +
326 + /**
327 + * Check course has 'sale price'
328 + *
329 + * @return bool
330 + */
331 + public function has_sale_price(): bool {
332 + $has_sale_price = false;
333 + $regular_price = $this->get_regular_price();
334 + $sale_price = $this->get_sale_price();
335 + $start_date = $this->get_sale_start();
336 + $end_date = $this->get_sale_end();
337 +
338 + if ( $sale_price !== '' && (float) $regular_price > (float) $sale_price ) {
339 + $has_sale_price = true;
340 + }
341 +
342 + // Check in days sale
343 + if ( $has_sale_price && ! empty( $start_date ) && ! empty( $end_date ) ) {
344 + $nowObj = new LP_Datetime();
345 + // Compare via timezone WP
346 + $nowStr = $nowObj->toSql( true );
347 + $now = strtotime( $nowStr );
348 + $end = strtotime( $end_date );
349 + $start = strtotime( $start_date );
350 +
351 + $has_sale_price = $now >= $start && $now <= $end;
352 + }
353 +
354 + return apply_filters( 'learnPress/course/has-sale-price', $has_sale_price, $this );
355 + }
356 +
357 + /**
358 + * Get date sale start
359 + *
360 + * @return mixed
361 + */
362 + public function get_sale_start() {
363 + return $this->get_meta_value_by_key( CoursePostModel::META_KEY_SALE_START );
364 + }
365 +
366 + /**
367 + * Get date sale end
368 + *
369 + * @return mixed
370 + */
371 + public function get_sale_end() {
372 + return $this->get_meta_value_by_key( CoursePostModel::META_KEY_SALE_END );
373 + }
374 +
375 + /**
376 + * Check if a course is Free
377 + *
378 + * @return bool
379 + */
380 + public function is_free(): bool {
381 + return $this->get_price() == 0;
382 + }
383 +
384 + /**
385 + * Check if a course is enabled Offline
386 + *
387 + * @return bool
388 + */
389 + public function is_offline(): bool {
390 + return $this->get_meta_value_by_key( CoursePostModel::META_KEY_OFFLINE_COURSE, 'no' ) === 'yes';
391 + }
392 +
393 + /**
394 + * Check option "Block course when expire" enable.
395 + *
396 + * @return bool
397 + * @since 4.2.7.3
398 + * @version 1.0.0
399 + */
400 + public function enable_block_when_expire(): bool {
401 + return $this->get_meta_value_by_key( CoursePostModel::META_KEY_BLOCK_EXPIRE_DURATION, 'no' ) === 'yes';
402 + }
403 +
404 + /**
405 + * Check option "Block course when finished" enable.
406 + *
407 + * @return bool
408 + * @since 4.2.7.6
409 + * @version 1.0.0
410 + */
411 + public function enable_block_when_finished(): bool {
412 + return $this->get_meta_value_by_key( CoursePostModel::META_KEY_BLOCK_FINISH, 'no' ) === 'yes';
413 + }
414 +
415 + /**
416 + * Get first item of course
417 + *
418 + * @return int
419 + */
420 + public function get_first_item_id(): int {
421 + if ( isset( $this->first_item_id ) ) {
422 + return $this->first_item_id;
423 + }
424 +
425 + try {
426 + $this->first_item_id = LP_Course_DB::getInstance()->get_first_item_id( $this->get_id() );
427 + } catch ( Throwable $e ) {
428 + $this->first_item_id = 0;
429 + }
430 +
431 + return $this->first_item_id;
432 + }
433 +
434 + /**
435 + * Get total items of course
436 + *
437 + * @return null|object
438 + */
439 + public function get_total_items() {
440 + if ( isset( $this->total_items ) ) {
441 + return $this->total_items;
442 + }
443 +
444 + try {
445 + $this->total_items = LP_Course_DB::getInstance()->get_total_items( $this->get_id() );
446 + } catch ( Throwable $e ) {
447 + $this->total_items = null;
448 + }
449 +
450 + return $this->total_items;
451 + }
452 +
453 + /**
454 + * Get total sections of course
455 + *
456 + * @return int
457 + * @since 4.2.7.6
458 + * @version 1.0.0
459 + */
460 + public function get_total_sections(): int {
461 + $section_items = $this->get_section_items();
462 +
463 + return count( $section_items );
464 + }
465 +
466 + /**
467 + * Get total items of course
468 + *
469 + * @return array
470 + */
471 + public function get_section_items(): array {
472 + if ( isset( $this->sections_items ) ) {
473 + return $this->sections_items;
474 + }
475 +
476 + try {
477 + $this->sections_items = $this->get_sections_and_items_course_from_db_and_sort();
478 + } catch ( Throwable $e ) {
479 + $this->sections_items = [];
480 + }
481 +
482 + return $this->sections_items;
483 + }
484 +
485 + /**
486 + * Get only items of course
487 + *
488 + * @return array
489 + * @since 4.3.2
490 + * @version 1.0.1
491 + */
492 + public function get_only_items(): array {
493 + if ( isset( $this->only_items ) ) {
494 + return $this->only_items;
495 + }
496 +
497 + $this->only_items = [];
498 +
499 + foreach ( $this->get_section_items() as $section ) {
500 + foreach ( $section->items as $item ) {
501 + $this->only_items[ $item->item_id ] = $item;
502 + }
503 + }
504 +
505 + return $this->only_items;
506 + }
507 +
508 + /**
509 + * Get section id of item
510 + *
511 + * A curriculum item's identity is the (item_id, item_type) pair, not the numeric
512 + * ID alone: once items live in per-type tables a lesson and a quiz can share the
513 + * same ID. Pass $item_type to match the composite identity.
514 + *
515 + * @param int $item_id
516 + * @param string $item_type Optional. Curriculum item type to match as well. Empty
517 + * keeps the legacy ID-only match for callers not yet migrated.
518 + *
519 + * @return int
520 + * @since 4.2.8
521 + * @version 1.1.0
522 + */
523 + public function get_section_of_item( int $item_id, string $item_type = '' ): int {
524 + $section_items = $this->get_section_items();
525 +
526 + foreach ( $section_items as $section ) {
527 + if ( empty( $section->items ) ) {
528 + continue;
529 + }
530 +
531 + foreach ( $section->items as $item ) {
532 + $item_id_check = (int) ( $item->item_id ?? $item->id ?? 0 );
533 + if ( $item_id_check !== $item_id ) {
534 + continue;
535 + }
536 +
537 + if ( '' !== $item_type ) {
538 + $item_type_check = (string) ( $item->item_type ?? $item->type ?? '' );
539 + if ( $item_type_check !== $item_type ) {
540 + continue;
541 + }
542 + }
543 +
544 + return (int) ( $section->section_id ?? $section->id ?? 0 );
545 + }
546 + }
547 +
548 + return 0;
549 + }
550 +
551 + /**
552 + * Get course Evaluation type.
553 + *
554 + * @return string
555 + * @since 4.2.7.3
556 + * @version 1.0.1
557 + */
558 + public function get_evaluation_type(): string {
559 + return $this->get_meta_value_by_key( CoursePostModel::META_KEY_EVALUATION_TYPE, 'evaluate_lesson' );
560 + }
561 +
562 + /**
563 + * Get course Evaluation type.
564 + *
565 + * @param string $type
566 + *
567 + * @return array
568 + * @since 4.3.2.6
569 + * @version 1.0.1
570 + */
571 + public static function get_evaluation_types( string $type = '' ): array {
572 + if ( has_filter( 'learnpress/course-evaluation/methods' ) ) {
573 + $methods = apply_filters( 'learnpress/course-evaluation/methods', [], 0 );
574 + }
575 +
576 + $types = apply_filters(
577 + 'learn-press/course/evaluation-types',
578 + array(
579 + 'evaluate_lesson' => [
580 + 'label' => __( 'Evaluate via completed lessons', 'learnpress' ),
581 + 'tip' => __( 'Course will be completed when all lessons are completed.', 'learnpress' ),
582 + ],
583 + 'evaluate_final_quiz' => [
584 + 'label' => __( 'Evaluate via final quiz', 'learnpress' ),
585 + 'tip' => __( 'Course will be completed when the final quiz is passed.', 'learnpress' ),
586 + ],
587 + 'evaluate_quiz' => [
588 + 'label' => __( 'Evaluate via quizzes', 'learnpress' ),
589 + 'tip' => __( 'Course will be completed when all quizzes are passed.', 'learnpress' ),
590 + ],
591 + 'evaluate_questions' => [
592 + 'label' => __( 'Evaluate via questions', 'learnpress' ),
593 + 'tip' => __( 'Course will be completed when all questions are answered correctly.', 'learnpress' ),
594 + ],
595 + 'evaluate_mark' => [
596 + 'label' => __( 'Evaluate via marks', 'learnpress' ),
597 + 'tip' => __( 'Course will be completed when the passing mark is achieved.', 'learnpress' ),
598 + ],
599 + ),
600 + );
601 +
602 + if ( empty( $type ) ) {
603 + return $types;
604 + } elseif ( empty( $types[ $type ] ) && ! empty( $methods[ $type ] ) ) {
605 + $types[ $type ] = [
606 + 'label' => strip_tags( $methods[ $type ] ),
607 + 'tip' => strip_tags( $methods[ $type ] ),
608 + ];
609 + }
610 +
611 + return $types[ $type ] ?? [];
612 + }
613 +
614 + /**
615 + * Get course passing condition value.
616 + *
617 + * @return float
618 + * @since 4.2.7.3
619 + * @version 1.0.0
620 + */
621 + public function get_passing_condition(): float {
622 + return (float) $this->get_meta_value_by_key( CoursePostModel::META_KEY_PASSING_CONDITION, 80 );
623 + }
624 +
625 + /**
626 + * Get final quiz id
627 + *
628 + * @return int
629 + */
630 + public function get_final_quiz(): int {
631 + $key = '_lp_final_quiz';
632 + if ( ! empty( $this->meta_data->{$key} ) ) {
633 + return $this->meta_data->$key;
634 + }
635 +
636 + $final_quiz = 0;
637 +
638 + // Not use array_reverse, it's make change object
639 + $section_items = $this->get_section_items();
640 + $found = 0;
641 + for ( $i = count( $section_items ); $i > 0; $i-- ) {
642 + $section = $section_items[ $i - 1 ];
643 + for ( $j = count( $section->items ); $j > 0; $j-- ) {
644 + $item = $section->items[ $j - 1 ];
645 + if ( learn_press_get_post_type( $item->id ) === LP_QUIZ_CPT ) {
646 + $final_quiz = $item->id;
647 + $found = 1;
648 + break;
649 + }
650 + }
651 +
652 + if ( $found ) {
653 + break;
654 + }
655 + }
656 +
657 + $evaluation_type = $this->get_evaluation_type();
658 + if ( $evaluation_type === 'evaluate_final_quiz' ) {
659 + if ( isset( $final_quiz ) ) {
660 + update_post_meta( $this->ID, $key, $final_quiz );
661 + } else {
662 + delete_post_meta( $this->ID, $key );
663 + }
664 + }
665 +
666 + $this->meta_data->{$key} = $final_quiz;
667 +
668 + return $final_quiz;
669 + }
670 +
671 + /**
672 + * Get all sections and items from database, then handle sort
673 + * Only call when data change or not set
674 + *
675 + * @return array
676 + * @since 4.1.6.9
677 + * @version 1.0.4
678 + * @author tungnx
679 + */
680 + private function get_sections_and_items_course_from_db_and_sort(): array {
681 + $sections_items = [];
682 + $course_id = $this->get_id();
683 + $lp_course_db = LP_Course_DB::getInstance();
684 +
685 + try {
686 + $sections_results = $lp_course_db->get_sections( $course_id );
687 + $sections_items_results = $lp_course_db->get_full_sections_and_items_course( $course_id );
688 + $count_items = count( $sections_items_results );
689 + $index_items_last = $count_items - 1;
690 + $section_current = 0;
691 +
692 + /**
693 + * @var $section_order_plus int
694 + * @var $item_order_plus int
695 + * To fixed case: section order start from 0, item order start from 0
696 + */
697 + $section_order_plus = 0;
698 + $item_order_plus = 0;
699 + foreach ( $sections_items_results as $index => $sections_item ) {
700 + $section_new = $sections_item->section_id;
701 + $section_order = (int) $sections_item->section_order;
702 + if ( $index === 0 && $section_order === 0 ) {
703 + $section_order_plus = 1;
704 + }
705 +
706 + $section_order += $section_order_plus;
707 + $item = new stdClass();
708 + $item->id = $sections_item->item_id;
709 + $item->item_id = $sections_item->item_id;
710 + $item_order = (int) $sections_item->item_order;
711 + if ( $index === 0 && $item_order === 0 ) {
712 + $item_order_plus = 1;
713 + }
714 +
715 + $item_order += $item_order_plus;
716 + $item->order = $item_order;
717 + $item->item_order = $item_order;
718 + $item->type = $sections_item->item_type;
719 + $item->item_type = $sections_item->item_type;
720 + $item_tmp = LP_Course_Item::get_item( $item->id );
721 + if ( $item_tmp ) {
722 + $item->title = html_entity_decode( $item_tmp->get_title() );
723 + $item->preview = $item_tmp->is_preview();
724 + }
725 +
726 + if ( $section_new != $section_current ) {
727 + $sections_items[ $section_new ] = new stdClass();
728 + $sections_items[ $section_new ]->id = $section_new; // old field will be deprecated in future
729 + $sections_items[ $section_new ]->section_id = $section_new; // new field
730 + $sections_items[ $section_new ]->order = $section_order; // old field will be deprecated in future
731 + $sections_items[ $section_new ]->section_order = $section_order; // new field
732 + $sections_items[ $section_new ]->title = html_entity_decode( $sections_item->section_name ); // old field will be deprecated in future
733 + $sections_items[ $section_new ]->section_name = html_entity_decode( $sections_item->section_name ); // new field
734 + $sections_items[ $section_new ]->description = html_entity_decode( $sections_item->section_description ); // old field will be deprecated in future
735 + $sections_items[ $section_new ]->section_description = html_entity_decode( $sections_item->section_description ); // new field
736 + $sections_items[ $section_new ]->items = [];
737 +
738 + // Sort item by item_order
739 + if ( $section_current != 0 ) {
740 + usort(
741 + $sections_items[ $section_current ]->items,
742 + function ( $item1, $item2 ) {
743 + return $item1->item_order - $item2->item_order;
744 + }
745 + );
746 + }
747 +
748 + $section_current = $section_new;
749 + }
750 +
751 + $sections_items[ $section_new ]->items[ $item->item_id ] = $item;
752 +
753 + if ( $index_items_last === $index ) {
754 + usort(
755 + $sections_items[ $section_current ]->items,
756 + function ( $item1, $item2 ) {
757 + return $item1->item_order - $item2->item_order;
758 + }
759 + );
760 + }
761 + }
762 +
763 + // Check case if section empty items
764 + foreach ( $sections_results as $section ) {
765 + $section_id = $section->section_id;
766 + if ( isset( $sections_items[ $section_id ] ) ) {
767 + continue;
768 + }
769 +
770 + $section_obj = new stdClass();
771 + $section_obj->id = $section_id;
772 + $section_obj->section_id = $section_id;
773 + $section_obj->order = $section->section_order;
774 + $section_obj->section_order = $section->section_order;
775 + $section_obj->title = html_entity_decode( $section->section_name );
776 + $section_obj->section_name = html_entity_decode( $section->section_name );
777 + $section_obj->description = html_entity_decode( $section->section_description );
778 + $section_obj->section_description = html_entity_decode( $section->section_description );
779 + $section_obj->items = [];
780 + $sections_items[ $section_id ] = $section_obj;
781 + }
782 +
783 + // Sort section by section_order
784 + usort(
785 + $sections_items,
786 + function ( $section1, $section2 ) {
787 + return $section1->section_order - $section2->section_order;
788 + }
789 + );
790 + } catch ( Throwable $e ) {
791 + LP_Debug::error_log( $e );
792 + }
793 +
794 + return $sections_items;
795 + }
796 +
797 + /**
798 + * Get permalink course
799 + *
800 + * @return string
801 + */
802 + public function get_permalink(): string {
803 + if ( ! empty( $this->permalink ) ) {
804 + return $this->permalink;
805 + }
806 +
807 + try {
808 + $coursePostModel = new CoursePostModel( $this );
809 + $this->permalink = $coursePostModel->get_permalink();
810 + } catch ( Throwable $e ) {
811 + $this->permalink = '';
812 + }
813 +
814 + return $this->permalink;
815 + }
816 +
817 + /**
818 + * Get value option No enroll requirement
819 + *
820 + * @return mixed
821 + */
822 + public function get_no_enroll_requirement() {
823 + return $this->get_meta_value_by_key( CoursePostModel::META_KEY_NO_REQUIRED_ENROLL, 'no' );
824 + }
825 +
826 + /**
827 + * Get description of Course
828 + *
829 + * @return string
830 + */
831 + public function get_description(): string {
832 + $course_post = new CoursePostModel( $this );
833 +
834 + return $course_post->get_the_content();
835 + }
836 +
837 + /**
838 + * Get short description of Course
839 + *
840 + * @return string
841 + */
842 + public function get_short_description(): string {
843 + $course_post = new CoursePostModel( $this );
844 +
845 + return $course_post->get_the_excerpt();
846 + }
847 +
848 + /**
849 + * Get value option No enroll requirement
850 + *
851 + * @return bool
852 + */
853 + public function has_no_enroll_requirement(): bool {
854 + return $this->get_no_enroll_requirement() === 'yes';
855 + }
856 +
857 + /**
858 + * Get value from meta data by key
859 + *
860 + * @param string $key
861 + * @param mixed|false $default_value
862 + * @param bool $single
863 + *
864 + * @return false|mixed
865 + * @since 4.2.6.9
866 + * @version 1.0.2
867 + */
868 + public function get_meta_value_by_key( string $key, $default_value = false, bool $single = true ) {
869 + if ( $this->meta_data instanceof stdClass && isset( $this->meta_data->{$key} ) ) {
870 + $value = maybe_unserialize( $this->meta_data->{$key} );
871 + } else {
872 + $coursePost = new CoursePostModel( $this );
873 + $value = $coursePost->get_meta_value_by_key( $key, $default_value, $single );
874 + }
875 +
876 + $value = maybe_unserialize( $value );
877 + $this->meta_data->{$key} = $value;
878 +
879 + return $value;
880 + }
881 +
882 + /**
883 + * Check course is in stock
884 + * True is in stock, False is out of stock
885 + *
886 + * @return mixed
887 + * @since 3.0.0
888 + * @version 1.0.1
889 + */
890 + public function is_in_stock() {
891 + $in_stock = true;
892 + $max_allowed = (int) $this->get_meta_value_by_key( CoursePostModel::META_KEY_MAX_STUDENTS, 0 );
893 +
894 + if ( $max_allowed ) {
895 + $in_stock = $max_allowed > $this->get_total_user_enrolled_or_purchased();
896 + }
897 +
898 + return apply_filters( 'learn-press/is-in-stock', $in_stock, $this->get_id() );
899 + }
900 +
901 + /**
902 + * Check course is enable repurchase
903 + *
904 + * @return bool
905 + * @since 4.2.7.2
906 + * @version 1.0.1
907 + */
908 + public function enable_allow_repurchase(): bool {
909 + $enable = $this->get_meta_value_by_key( CoursePostModel::META_KEY_ALLOW_COURSE_REPURCHASE, 'no' );
910 +
911 + return 'yes' === $enable;
912 + }
913 +
914 + /**
915 + * Type repurchase
916 + *
917 + * @return string
918 + * @since 4.2.7.3
919 + * @version 1.0.0
920 + */
921 + public function get_type_repurchase(): string {
922 + return $this->get_meta_value_by_key( CoursePostModel::META_KEY_COURSE_REPURCHASE_OPTION, 'reset' );
923 + }
924 +
925 + /**
926 + * Get external link
927 + *
928 + * @return string
929 + */
930 + public function get_external_link(): string {
931 + return esc_url_raw(
932 + $this->get_meta_value_by_key( CoursePostModel::META_KEY_EXTERNAL_LINK_BY_COURSE, '' )
933 + );
934 + }
935 +
936 + /**
937 + * Get item's link
938 + * @move from LP_Abstract_Course
939 + *
940 + * @param int $item_id
941 + * @param string $item_type
942 + *
943 + * @return string
944 + * @since 3.0.0
945 + * @version 1.0.2
946 + */
947 + public function get_item_link( int $item_id, string $item_type = '' ): string {
948 + if ( empty( $item_type ) ) {
949 + $item_type = get_post_type( $item_id );
950 + }
951 + $course_permalink = trailingslashit( $this->get_permalink() );
952 + $item_slug = get_post_field( 'post_name', $item_id );
953 +
954 + $slug_prefixes = apply_filters(
955 + 'learn-press/course/custom-item-prefixes',
956 + array(
957 + LP_QUIZ_CPT => sanitize_title_with_dashes( LP_Settings::get_option( 'quiz_slug', 'quizzes' ) ),
958 + LP_LESSON_CPT => sanitize_title_with_dashes( LP_Settings::get_option( 'lesson_slug', 'lessons' ) ),
959 + ),
960 + $this->get_id()
961 + );
962 +
963 + $slug_prefix = trailingslashit( $slug_prefixes[ $item_type ] ?? '' );
964 + $item_link = trailingslashit( $course_permalink . $slug_prefix . $item_slug );
965 +
966 + return apply_filters( 'learn-press/course/item-link', $item_link, $item_id, $this );
967 + }
968 +
969 + /**
970 + * Get total user enrolled, purchased or finished
971 + *
972 + * @move from LP_Abstract_Course
973 + * @return int
974 + * @version 1.0.1
975 + * @since 4.1.4
976 + */
977 + public function get_total_user_enrolled_or_purchased(): int {
978 + $total = 0;
979 + $lp_course_cache = new LP_Course_Cache( true );
980 +
981 + try {
982 + $total = $lp_course_cache->get_total_students_enrolled_or_purchased( $this->get_id() );
983 + if ( false !== $total ) {
984 + return $total;
985 + }
986 +
987 + $lp_course_db = LP_Course_DB::getInstance();
988 + $total = $lp_course_db->get_total_user_enrolled_or_purchased( $this->get_id() );
989 + $lp_course_cache->set_total_students_enrolled_or_purchased( $this->get_id(), $total );
990 + } catch ( Throwable $e ) {
991 + LP_Debug::error_log( $e );
992 + }
993 +
994 + return $total;
995 + }
996 +
997 + /**
998 + * Get fake students.
999 + *
1000 + * @return int
1001 + */
1002 + public function get_fake_students(): int {
1003 + return (int) $this->get_meta_value_by_key( CoursePostModel::META_KEY_STUDENTS, 0 );
1004 + }
1005 +
1006 + /**
1007 + * Count number of students enrolled course.
1008 + * Check global settings `enrolled_students_number`
1009 + * and add the fake value if both are set.
1010 + *
1011 + * @return int
1012 + * @move from LP_Abstract_Course
1013 + */
1014 + public function count_students(): int {
1015 + $total = $this->get_total_user_enrolled_or_purchased();
1016 + $total += $this->get_fake_students();
1017 +
1018 + return $total;
1019 + }
1020 +
1021 + /**
1022 + * Count total items in Course
1023 + * item_type empty will return all items if exists.
1024 + *
1025 + * @param string $item_type
1026 + *
1027 + * @return int
1028 + * @since 4.2.7.3
1029 + * @version 1.0.1
1030 + */
1031 + public function count_items( string $item_type = '' ): int {
1032 + $count = 0;
1033 +
1034 + $total_items = $this->get_total_items();
1035 + if ( empty( $item_type ) ) {
1036 + $count = $total_items->count_items ?? 0;
1037 + } elseif ( isset( $total_items->{$item_type} ) ) {
1038 + return $total_items->{$item_type};
1039 + }
1040 +
1041 + return $count;
1042 + }
1043 +
1044 + /**
1045 + * Get Duration of course
1046 + * Timestamp in second
1047 + *
1048 + * @return string
1049 + */
1050 + public function get_duration(): string {
1051 + return $this->get_meta_value_by_key( CoursePostModel::META_KEY_DURATION, '10 week' );
1052 + }
1053 +
1054 + /**
1055 + * Check user can enroll course.
1056 + * @move from can_enroll_course method of LP_User class, since 4.1.1
1057 + *
1058 + * @param UserModel|false $user
1059 + *
1060 + * @return bool|WP_Error
1061 + * @since 4.2.7.3
1062 + * @version 1.0.2
1063 + */
1064 + public function can_enroll( $user ) {
1065 + $can_enroll = true;
1066 + $error_code = '';
1067 +
1068 + $user_id = 0;
1069 + if ( $user instanceof UserModel ) {
1070 + $user_id = $user->get_id();
1071 + }
1072 +
1073 + try {
1074 + if ( ! in_array( $this->post_status, [ 'publish', 'private' ] ) ) {
1075 + $error_code = 'course_not_publish';
1076 + throw new Exception( __( 'The course is not public', 'learnpress' ) );
1077 + }
1078 +
1079 + $userCourseModel = UserCourseModel::find( $user_id, $this->get_id(), true );
1080 + $enable_no_required_enroll = $this->has_no_enroll_requirement();
1081 + $out_of_stock = ! $this->is_in_stock();
1082 +
1083 + // Case user can retake course.
1084 + if ( $userCourseModel && $userCourseModel->can_retake() ) {
1085 + $error_code = 'course_can_retry';
1086 + throw new Exception( esc_html__( 'Course can retake.', 'learnpress' ) );
1087 + }
1088 +
1089 + // Case course is out of stock, show message when user is not login or user_item not exits
1090 + if ( $out_of_stock &&
1091 + ( ! $user || ! $userCourseModel || ! $userCourseModel->has_enrolled_or_finished() ) ) {
1092 + $error_code = 'course_out_of_stock';
1093 + throw new Exception( __( 'The course is full of students.', 'learnpress' ) );
1094 + }
1095 +
1096 + // Case user is logged in and user_item exists
1097 + if ( $userCourseModel && $user ) {
1098 + if ( $userCourseModel->has_enrolled() ) {
1099 + $error_code = 'course_is_enrolled';
1100 + throw new Exception( __( 'This course is already enrolled!', 'learnpress' ) );
1101 + } elseif ( $userCourseModel->has_finished() ) {
1102 + $error_code = 'course_is_finished';
1103 + throw new Exception( __( 'The course is finished.', 'learnpress' ) );
1104 + }
1105 + }
1106 +
1107 + if ( $enable_no_required_enroll ) {
1108 + if ( ! $user ) {
1109 + $error_code = 'course_is_no_required_enroll_not_login';
1110 + throw new Exception(
1111 + __(
1112 + 'Enrollment in the course is not mandatory. You can access course for learning now.',
1113 + 'learnpress'
1114 + )
1115 + );
1116 + } else {
1117 +
1118 + }
1119 + } else {
1120 + if ( ! empty( $this->get_external_link() )
1121 + && ( ! $user || ! $userCourseModel
1122 + || $userCourseModel->get_status() === UserItemModel::STATUS_CANCEL )
1123 + && ! $this->is_offline() ) {
1124 + $error_code = 'course_is_external';
1125 + throw new Exception( __( 'The course is external', 'learnpress' ) );
1126 + }
1127 +
1128 + if ( ! $this->is_free() ) {
1129 + if ( ! $user ) {
1130 + $error_code = 'course_is_not_purchased_not_login';
1131 + throw new Exception( __( 'The course is not purchased.', 'learnpress' ) );
1132 + } elseif ( ! $userCourseModel || ! $userCourseModel->has_purchased() ) {
1133 + $error_code = 'course_is_not_purchased';
1134 + throw new Exception( __( 'The course is not purchased.', 'learnpress' ) );
1135 + }
1136 + }
1137 + }
1138 + } catch ( Throwable $e ) {
1139 + if ( empty( $error_code ) ) {
1140 + $error_code = 'course_can_not_enroll';
1141 + }
1142 + $can_enroll = new WP_Error( $error_code, $e->getMessage() );
1143 + }
1144 +
1145 + // Hook old
1146 + if ( has_filter( 'learn-press/user/can-enroll-course' ) ) {
1147 + $output = new stdClass();
1148 + $output->check = true;
1149 + $output->message = '';
1150 + if ( $can_enroll instanceof WP_Error ) {
1151 + $output->check = false;
1152 + $output->message = $can_enroll->get_error_message();
1153 + }
1154 +
1155 + $course_old = learn_press_get_course( $this->get_id() );
1156 + $user_old = learn_press_get_user( $user_id );
1157 + $output = apply_filters( 'learn-press/user/can-enroll-course', $output, $course_old, false, $user_old );
1158 + if ( $output === false ) {
1159 + $can_enroll = new WP_Error( '', '' );
1160 + } elseif ( ! $output->check && $output->message ) {
1161 + $can_enroll = new WP_Error( 'error_custom', $output->message );
1162 + }
1163 + //_deprecated_function( 'The learn-press/user/can-enroll-course filter', '4.2.7.3', 'learn-press/user/can-enroll/course' );
1164 + }
1165 +
1166 + return apply_filters( 'learn-press/user/can-enroll/course', $can_enroll, $this, $user );
1167 + }
1168 +
1169 + /**
1170 + * Check user can purchase course.
1171 + * @move from can_purchase_course method of LP_User class, since 4.0.8
1172 + * @use LP_User::can_purchase_course
1173 + *
1174 + * @param UserModel|false $user
1175 + *
1176 + * @return bool|WP_Error
1177 + * @since 4.2.7.3
1178 + * @version 1.0.0
1179 + */
1180 + public function can_purchase( $user ) {
1181 + $can_purchase = true;
1182 + $error_code = '';
1183 +
1184 + $user_id = 0;
1185 + if ( $user instanceof UserModel ) {
1186 + $user_id = $user->get_id();
1187 + }
1188 +
1189 + try {
1190 + $can_enroll = $this->can_enroll( $user );
1191 + if ( $can_enroll instanceof WP_Error ) {
1192 + $error_code_return = [
1193 + 'course_is_not_purchased_not_login',
1194 + 'course_is_not_purchased',
1195 + 'course_is_enrolled',
1196 + 'course_is_finished',
1197 + ];
1198 + if ( ! in_array( $can_enroll->get_error_code(), $error_code_return ) ) {
1199 + $error_code = $can_enroll->get_error_code();
1200 + throw new Exception( $can_enroll->get_error_message() );
1201 + }
1202 + }
1203 +
1204 + if ( $this->is_free() ) {
1205 + $error_code = 'course_is_free';
1206 + throw new Exception( __( 'The course is free.', 'learnpress' ) );
1207 + }
1208 +
1209 + $enable_no_required_enroll = $this->has_no_enroll_requirement();
1210 + if ( $enable_no_required_enroll ) {
1211 + $error_code = 'course_is_no_required_enroll';
1212 + throw new Exception(
1213 + __( 'Enrollment in the course is not mandatory. You can access course for learning now.', 'learnpress' )
1214 + );
1215 + }
1216 +
1217 + $userCourseModel = UserCourseModel::find( $user_id, $this->get_id(), true );
1218 + if ( $user ) {
1219 + if ( $userCourseModel ) {
1220 + if ( $userCourseModel->has_purchased() ) {
1221 + $error_code = 'course_purchased';
1222 + throw new Exception( __( 'Course is purchased', 'learnpress' ) );
1223 + }
1224 +
1225 + if ( $this->enable_allow_repurchase() ) {
1226 + if ( $userCourseModel->has_enrolled() && $userCourseModel->timestamp_remaining_duration() !== 0 ) {
1227 + $error_code = 'course_is_enrolled';
1228 + throw new Exception( 'This course is already enrolled!' );
1229 + }
1230 + } else {
1231 + if ( $userCourseModel->has_enrolled_or_finished() ) {
1232 + $error_code = 'course_is_enrolled_or_finished';
1233 + throw new Exception( __( 'Course is enrolled or finished', 'learnpress' ) );
1234 + }
1235 + }
1236 + }
1237 + }
1238 + } catch ( Throwable $e ) {
1239 + if ( empty( $error_code ) ) {
1240 + $error_code = 'course_can_not_purchase';
1241 + }
1242 + $can_purchase = new WP_Error( $error_code, $e->getMessage() );
1243 + }
1244 +
1245 + // Hook old
1246 + if ( has_filter( 'learn-press/user/can-purchase-course' ) ) {
1247 + $can_purchase = apply_filters( 'learn-press/user/can-purchase-course', $can_purchase, $user_id, $this->get_id() );
1248 + if ( $can_purchase === false ) {
1249 + $can_purchase = new WP_Error( '', '' );
1250 + }
1251 + //_deprecated_function( 'The learn-press/user/can-purchase-course filter', '4.2.7.3', 'learn-press/user/can-purchase/course' );
1252 + }
1253 +
1254 + return apply_filters( 'learn-press/user/can-purchase/course', $can_purchase, $this, $user );
1255 + }
1256 +
1257 + /**
1258 + * Check user is author or co-in of course.
1259 + *
1260 + * @param UserModel|false $userModel
1261 + *
1262 + * @return bool
1263 + * @since 4.2.7.6
1264 + * @version 1.0.1
1265 + */
1266 + public function check_user_is_author( $userModel ): bool {
1267 + $is_author = false;
1268 +
1269 + if ( $userModel instanceof UserModel
1270 + && $userModel->get_id() === $this->post_author ) {
1271 + $is_author = true;
1272 + }
1273 +
1274 + return apply_filters( 'learn-press/course/is-author', $is_author, $this, $userModel );
1275 + }
1276 +
1277 + /**
1278 + * Get item model assigned to this course
1279 + *
1280 + * @param int $item_id
1281 + * @param string $item_type
1282 + * @param bool $check_assign | default true check assign item to course
1283 + *
1284 + * @return mixed|false|null|WP_Post|PostModel
1285 + * @since v4.2.7.6
1286 + * @version 1.0.3
1287 + */
1288 + public function get_item_model( int $item_id, string $item_type, bool $check_assign = true ) {
1289 + try {
1290 + $item = false;
1291 +
1292 + // Find item has in section. Match the composite (item_id, item_type) identity,
1293 + // so a quiz ID can never resolve through a lesson request and vice versa.
1294 + $section_id = $this->get_section_of_item( $item_id, $item_type );
1295 + if ( $check_assign && ! $section_id ) {
1296 + return $item;
1297 + }
1298 +
1299 + switch ( $item_type ) {
1300 + case LP_LESSON_CPT:
1301 + $item = LessonPostModel::find( $item_id, true );
1302 + break;
1303 + case LP_QUIZ_CPT:
1304 + $item = QuizPostModel::find( $item_id, true );
1305 + break;
1306 + case LP_QUESTION_CPT:
1307 + break;
1308 + default:
1309 + $item = apply_filters( 'learn-press/course/get-item-model', $item, $item_id, $item_type, $this );
1310 + break;
1311 + }
1312 +
1313 + // If not defined class, get post default
1314 + if ( ! $item ) {
1315 + $filter = new PostFilter();
1316 + $filter->ID = $item_id;
1317 + $filter->post_type = $item_type;
1318 + $item = PostModel::get_item_model_from_db( $filter );
1319 + }
1320 + } catch ( Exception $e ) {
1321 + LP_Debug::error_log( $e );
1322 + }
1323 +
1324 + return $item;
1325 + }
1326 +
1327 + /**
1328 + * Get item model if query success.
1329 + * If not exists, return false.
1330 + * If exists, return PostModel.
1331 + *
1332 + * @param LP_Course_JSON_Filter $filter
1333 + *
1334 + * @return CourseModel|false|static
1335 + * @since 4.2.6.9
1336 + * @version 1.0.2
1337 + */
1338 + public static function get_item_model_from_db( LP_Course_JSON_Filter $filter ) {
1339 + $course_model = false;
1340 +
1341 + try {
1342 + $filter->only_fields = [ 'json', 'post_content' ];
1343 +
1344 + $course_rs = self::get_course_from_db( $filter );
1345 + if ( $course_rs instanceof stdClass && isset( $course_rs->json ) ) {
1346 + $course_obj = LP_Helper::json_decode( $course_rs->json );
1347 + $course_model = new static( $course_obj );
1348 + //$course_model->json = $course_rs->json;
1349 + $course_model->post_content = $course_rs->post_content;
1350 + $course_model->get_author_model();
1351 + }
1352 + } catch ( Throwable $e ) {
1353 + LP_Debug::error_log( $e );
1354 + }
1355 +
1356 + return $course_model;
1357 + }
1358 +
1359 + /**
1360 + * Get course by ID
1361 + *
1362 + * @param int $course_id
1363 + * @param bool $check_cache
1364 + *
1365 + * @return false|CourseModel|static
1366 + * @since 4.2.6.9
1367 + * @version 1.0.2
1368 + */
1369 + public static function find( int $course_id, bool $check_cache = false ) {
1370 + if ( ! $course_id ) {
1371 + return false;
1372 + }
1373 +
1374 + $filter_course = new LP_Course_JSON_Filter();
1375 + $filter_course->ID = $course_id;
1376 + $key_cache = "courseModel/find/id/{$course_id}";
1377 + $lp_course_cache = new LP_Course_Cache();
1378 +
1379 + // Check cache
1380 + if ( $check_cache ) {
1381 + $course_model = $lp_course_cache->get_cache( $key_cache );
1382 + if ( $course_model instanceof CourseModel ) {
1383 + return $course_model;
1384 + }
1385 + }
1386 +
1387 + // Query database no cache.
1388 + $course_model = self::get_item_model_from_db( $filter_course );
1389 + if ( false === $course_model ) { // Find on table posts
1390 + $course_rs = CoursePostModel::find( $course_id );
1391 + if ( $course_rs instanceof CoursePostModel ) {
1392 + $course_model = new static( $course_rs );
1393 + }
1394 + }
1395 +
1396 + // Set cache
1397 + if ( $course_model instanceof CourseModel ) {
1398 + $lp_course_cache->set_cache( $key_cache, $course_model );
1399 + }
1400 +
1401 + return $course_model;
1402 + }
1403 +
1404 + /**
1405 + * Get course from table learnpress_courses
1406 + *
1407 + * @return array|object|stdClass|null
1408 + * @throws Exception
1409 + */
1410 + private static function get_course_from_db( LP_Course_JSON_Filter $filter ) {
1411 + $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1412 + $lp_course_json_db->get_query_single_row( $filter );
1413 + $query_single_row = $lp_course_json_db->get_courses( $filter );
1414 +
1415 + return $lp_course_json_db->wpdb->get_row( $query_single_row );
1416 + }
1417 +
1418 + /**
1419 + * Save course data to table learnpress_courses.
1420 + *
1421 + * @param bool $force_save
1422 + *
1423 + * @return CourseModel
1424 + * @throws Exception
1425 + * @since 4.2.6.9
1426 + * @version 1.0.3
1427 + */
1428 + public function save( bool $force_save = false ): CourseModel {
1429 + // Check permission
1430 + if ( ! $force_save ) {
1431 + $this->check_permission();
1432 + }
1433 +
1434 + $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1435 +
1436 + $courseObjToJSON = clone $this;
1437 + unset( $courseObjToJSON->post_content );
1438 + unset( $courseObjToJSON->json );
1439 + $this->json = json_encode( $courseObjToJSON, JSON_UNESCAPED_UNICODE );
1440 + $data = get_object_vars( $this );
1441 +
1442 + if ( ! isset( $data['ID'] ) ) {
1443 + throw new Exception( 'Course ID is invalid!' );
1444 + }
1445 +
1446 + $filter = new LP_Course_JSON_Filter();
1447 + $filter->ID = $this->ID;
1448 + $filter->only_fields = [ 'ID' ];
1449 + $course_rs = self::get_course_from_db( $filter );
1450 + // Check if exists course id.
1451 + if ( empty( $course_rs ) ) { // Insert data.
1452 + $lp_course_json_db->insert_data( $data );
1453 + } else { // Update data.
1454 + $lp_course_json_db->update_data( $data );
1455 + }
1456 +
1457 + // Clear cache
1458 + $this->clean_caches();
1459 +
1460 + return $this;
1461 + }
1462 +
1463 + /**
1464 + * Delete row
1465 + *
1466 + * @throws Exception
1467 + */
1468 + public function delete() {
1469 + $lp_course_json_db = LP_Course_JSON_DB::getInstance();
1470 + $filter = new LP_Course_JSON_Filter();
1471 + $filter->where[] = $lp_course_json_db->wpdb->prepare( 'AND ID = %d', $this->ID );
1472 + $filter->collection = $lp_course_json_db->tb_lp_courses;
1473 + $lp_course_json_db->delete_execute( $filter );
1474 +
1475 + // Clear cache
1476 + $this->clean_caches();
1477 + }
1478 +
1479 + /**
1480 + * Clean caches
1481 + *
1482 + * @return void
1483 + * @version 1.0.0
1484 + * @since 4.2.7.4
1485 + */
1486 + public function clean_caches() {
1487 + $key_cache = "courseModel/find/id/{$this->ID}";
1488 + $lp_course_cache = new LP_Course_Cache();
1489 + $lp_course_cache->clear( $key_cache );
1490 +
1491 + // Clear cache image urls, store with many sizes
1492 + $img_urls_key_cache = "image_urls/{$this->ID}";
1493 + $lp_course_cache->clear_cache_on_group( $img_urls_key_cache );
1494 + }
1495 +
1496 + /**
1497 + * Check permission to update/create course
1498 + *
1499 + * @throws Exception
1500 + */
1501 + public function check_permission() {
1502 + $coursePostModel = new CoursePostModel( $this );
1503 + if ( ! $coursePostModel->check_capabilities_update() ) {
1504 + throw new Exception( 'You do not have permission to update this course!' );
1505 + }
1506 + }
1507 +
1508 + /**
1509 + * Return course's items support.
1510 + * To replace learn_press_course_get_support_item_types()
1511 + * Should add hook on addons before use this function.
1512 + *
1513 + * @return array
1514 + * @since 4.2.7.4
1515 + * @version 1.0.1
1516 + */
1517 + public static function item_types_support(): array {
1518 + $item_types = [
1519 + LP_LESSON_CPT,
1520 + LP_QUIZ_CPT,
1521 + ];
1522 +
1523 + // Hook old
1524 + if ( has_filter( 'learn-press/course-item-type' ) ) {
1525 + $item_types = apply_filters( 'learn-press/course-item-type', $item_types );
1526 + }
1527 +
1528 + $item_types = apply_filters( 'learn-press/course/item-types-support', $item_types );
1529 +
1530 + // set types unique
1531 + return array_unique( $item_types );
1532 + }
1533 +
1534 + /**
1535 + * Get label of item type
1536 + *
1537 + * @param string $item_type
1538 + *
1539 + * @return string
1540 + * @since 4.2.8.6
1541 + * @version 1.0.0
1542 + */
1543 + public static function item_types_label( string $item_type = '' ): string {
1544 + switch ( $item_type ) {
1545 + case LP_LESSON_CPT:
1546 + $label = __( 'Lesson', 'learnpress' );
1547 + break;
1548 + case LP_QUIZ_CPT:
1549 + $label = __( 'Quiz', 'learnpress' );
1550 + break;
1551 + default:
1552 + $label = ucfirst( strtolower( str_replace( [ 'lp_', '_' ], '', $item_type ) ) );
1553 + $label = apply_filters( 'learn-press/course/item-type-label', $label, $item_type );
1554 + break;
1555 + }
1556 +
1557 + return $label;
1558 + }
1559 +}