PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.8
4.4.8 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 All 139 releases
← All changes | inc/Models/UserItems/UserQuizModel.php +624 -639 4.3.94.4.8 View file →
@@ -1,639 +1,624 @@
1 -<?php
2 -
3 -/**
4 - * Class UserQuizModel
5 - *
6 - * @package LearnPress/Classes
7 - * @version 1.0.2
8 - * @since 4.2.5
9 - */
10 -
11 -namespace LearnPress\Models\UserItems;
12 -
13 -use Exception;
14 -use LearnPress\Models\CourseModel;
15 -use LearnPress\Models\QuizPostModel;
16 -use LearnPress\Models\UserItemMeta\UserItemMetaModel;
17 -use LearnPress\Models\UserItemMeta\UserQuizMetaModel;
18 -use LearnPress\Models\UserModel;
19 -use LearnPressAssignment\Models\AssignmentPostModel;
20 -use LP_Course;
21 -use LP_Datetime;
22 -use LP_Helper;
23 -use LP_Question;
24 -use LP_Quiz;
25 -use LP_Quiz_CURD;
26 -use LP_User;
27 -use LP_User_Items_Result_DB;
28 -use Throwable;
29 -use WP_Error;
30 -
31 -class UserQuizModel extends UserItemModel {
32 - /**
33 - * Item type Course
34 - *
35 - * @var string Item type
36 - */
37 - public $item_type = LP_QUIZ_CPT;
38 - /**
39 - * Ref type Order
40 - *
41 - * @var string
42 - */
43 - public $ref_type = LP_COURSE_CPT;
44 -
45 - public const STATUS_STARTED = 'started';
46 -
47 - public function __construct( $data = null ) {
48 - parent::__construct( $data );
49 - }
50 -
51 - /**
52 - * Get quiz model
53 - *
54 - * @return bool|QuizPostModel
55 - * @since 4.2.5
56 - * @version 1.0.1
57 - */
58 - public function get_quiz_post_model() {
59 - return QuizPostModel::find( $this->item_id, true );
60 - }
61 -
62 - /**
63 - * Get course model
64 - *
65 - * @return false|CourseModel
66 - * @since 4.2.7.6
67 - * @version 1.0.0
68 - */
69 - public function get_course_model() {
70 - return CourseModel::find( $this->ref_id, true );
71 - }
72 -
73 - /**
74 - * Get user course model
75 - *
76 - * @return false|UserCourseModel
77 - * @since 4.2.7.6
78 - * @version 1.0.0
79 - */
80 - public function get_user_course_model() {
81 - return UserCourseModel::find( $this->user_id, $this->ref_id, true );
82 - }
83 -
84 - /**
85 - * Get status
86 - *
87 - * @return string
88 - */
89 - public function get_status(): string {
90 - return $this->status;
91 - }
92 -
93 - /**
94 - * Get all question's ids of the quiz.
95 - *
96 - * @param string $context
97 - *
98 - * @move from LP_Quiz
99 - * @return int[]
100 - * @editor tungnx
101 - * @version 1.0.1
102 - * @since 3.2.0
103 - */
104 - public function get_question_ids( string $context = 'display' ): array {
105 - $curd = new LP_Quiz_CURD();
106 - $question_ids = $curd->read_question_ids( $this->item_id, $context );
107 - $question_ids = apply_filters( 'learn-press/quiz/get-question-ids', $question_ids, $this->item_id, $this->ref_id, $context );
108 - if ( ! is_array( $question_ids ) ) {
109 - $question_ids = array();
110 - }
111 -
112 - return $question_ids;
113 - }
114 -
115 - /**
116 - * Get time remaining when user doing assignment.
117 - *
118 - * @return int seconds
119 - * @since 4.2.7.6
120 - * @version 1.0.0
121 - */
122 - public function get_time_remaining(): int {
123 - $time_remaining = 0;
124 - $quizPostModel = $this->get_quiz_post_model();
125 - if ( ! $quizPostModel instanceof QuizPostModel ) {
126 - return $time_remaining;
127 - }
128 -
129 - $duration = $quizPostModel->get_duration();
130 - $start_time_stamp = strtotime( $this->get_start_time() );
131 - $expire_time_stamp = strtotime( '+' . $duration, $start_time_stamp );
132 - $time_remaining = $expire_time_stamp - time();
133 -
134 - if ( $time_remaining < 0 ) {
135 - $time_remaining = 0;
136 - }
137 -
138 - return $time_remaining;
139 - }
140 -
141 - /**
142 - * Start quiz.
143 - *
144 - * @throws Exception
145 - * @since 4.2.5
146 - * @version 1.0.0
147 - */
148 - public function start_quiz() {
149 - $can_start = $this->check_can_start();
150 - if ( is_wp_error( $can_start ) ) {
151 - /**
152 - * @var WP_Error $can_start
153 - */
154 - throw new Exception( $can_start->get_error_message() );
155 - }
156 -
157 - $this->status = self::STATUS_STARTED;
158 - $this->graduation = LP_COURSE_GRADUATION_IN_PROGRESS;
159 - $this->save();
160 -
161 - // Hook old - random quiz using.
162 - do_action( 'learn-press/user/quiz-started', $this->item_id, $this->ref_id, $this->user_id );
163 - // Hook new
164 - do_action( 'learn-press/user/quiz/started', $this );
165 - }
166 -
167 - /**
168 - * Retake quiz.
169 - *
170 - * @throws Exception
171 - */
172 - public function retake() {
173 - $can_retake = $this->check_can_retake();
174 - if ( is_wp_error( $can_retake ) ) {
175 - /**
176 - * @var WP_Error $can_retake
177 - */
178 - throw new Exception( $can_retake->get_error_message() );
179 - }
180 -
181 - // Update retaken count.
182 - $user_quiz_retaken = $this->get_meta_model_from_key( UserQuizMetaModel::KEY_RETAKEN_COUNT );
183 - if ( $user_quiz_retaken instanceof UserItemMetaModel ) {
184 - $number_retaken = absint( $user_quiz_retaken->meta_value );
185 - ++$number_retaken;
186 - $user_quiz_retaken->meta_value = $number_retaken;
187 - $user_quiz_retaken->save();
188 - } else {
189 - $user_quiz_retaken_new = new UserQuizMetaModel();
190 - $user_quiz_retaken_new->learnpress_user_item_id = $this->get_user_item_id();
191 - $user_quiz_retaken_new->meta_key = UserQuizMetaModel::KEY_RETAKEN_COUNT;
192 - $user_quiz_retaken_new->meta_value = 1;
193 - $user_quiz_retaken_new->save();
194 - }
195 -
196 - //Todo: rewrite by object.
197 - //Create new result in table learnpress_user_item_results.
198 - LP_User_Items_Result_DB::instance()->insert( $this->get_user_item_id() );
199 - // Remove user_item_meta.
200 - learn_press_delete_user_item_meta( $this->get_user_item_id(), '_lp_question_checked' );
201 -
202 - $this->status = self::STATUS_STARTED;
203 - $this->start_time = gmdate( LP_Datetime::$format, time() );
204 - $this->end_time = null;
205 - $this->graduation = LP_COURSE_GRADUATION_IN_PROGRESS;
206 - $this->save();
207 -
208 - // Hook old - random quiz using.
209 - do_action( 'learn-press/user/quiz-retried', $this->item_id, $this->ref_id, $this->user_id );
210 - // Hook new
211 - do_action( 'learn-press/user/quiz/retake', $this );
212 - }
213 -
214 - /**
215 - * Check user can start quiz.
216 - * If user can start quiz, return true, else return WP_Error.
217 - * Set user, quiz, course, user_course and parent_id for this object.
218 - *
219 - * @throws Exception
220 - * return bool|WP_Error
221 - *
222 - * @since 4.2.5
223 - * @version 1.0.2
224 - */
225 - public function check_can_start() {
226 - $can_start = true;
227 -
228 - $userModel = $this->get_user_model();
229 - if ( ! $userModel instanceof UserModel ) {
230 - $can_start = new WP_Error( 'user_invalid', __( 'User is invalid.', 'learnpress' ) );
231 - }
232 -
233 - $quizPostModel = $this->get_quiz_post_model();
234 - if ( ! $quizPostModel instanceof QuizPostModel ) {
235 - $can_start = new WP_Error( 'quiz_invalid', __( 'Quiz is invalid.', 'learnpress' ) );
236 - }
237 -
238 - $courseModel = $this->get_course_model();
239 - if ( ! $courseModel instanceof CourseModel ) {
240 - $can_start = new WP_Error( 'course_invalid', __( 'Course is invalid.', 'learnpress' ) );
241 - }
242 -
243 - $userQuizModel = UserQuizModel::find_user_item(
244 - $this->user_id,
245 - $this->item_id,
246 - $this->item_type,
247 - $this->ref_id,
248 - $this->ref_type,
249 - true
250 - );
251 - if ( $userQuizModel instanceof UserQuizModel ) {
252 - $can_start = new WP_Error( 'started_quiz', __( 'You have already started the quiz.', 'learnpress' ) );
253 - }
254 -
255 - // Check user, course of quiz is enrolled.
256 - $userCourseModel = $this->get_user_course_model();
257 - if ( ! $userCourseModel instanceof UserCourseModel
258 - || $userCourseModel->status !== LP_COURSE_ENROLLED ) {
259 - $can_start = new WP_Error( 'not_errol_course', __( 'Please enroll in the course before starting the quiz.', 'learnpress' ) );
260 - } elseif ( $userCourseModel->status === LP_COURSE_FINISHED ) {
261 - $can_start = new WP_Error( 'finished_course', __( 'You have already finished the course of this quiz.', 'learnpress' ) );
262 - } else {
263 - // Set Parent id for user quiz to save DB.
264 - $this->parent_id = $userCourseModel->get_user_item_id();
265 - }
266 -
267 - // Hook old.
268 - if ( has_filter( 'learn-press/can-start-quiz' ) ) {
269 - try {
270 - $can_start = apply_filters(
271 - 'learn-press/can-start-quiz',
272 - $can_start,
273 - $userQuizModel->item_id,
274 - $userQuizModel->ref_id,
275 - $userQuizModel->user_id
276 - );
277 - } catch ( Throwable $e ) {
278 - $can_start = new WP_Error( 'can_not_start_quiz', $e->getMessage() );
279 - }
280 - }
281 -
282 - // Hook can start quiz
283 - return apply_filters(
284 - 'learn-press/user/can-start-quiz',
285 - $can_start,
286 - $this
287 - );
288 - }
289 -
290 - /**
291 - * Check can retake quiz.
292 - *
293 - * @throws Exception
294 - * @since 4.2.5
295 - * @version 1.0.1
296 - */
297 - public function check_can_retake() {
298 - $can_retake = true;
299 -
300 - $userModel = $this->get_user_model();
301 - if ( ! $userModel instanceof UserModel ) {
302 - $can_retake = new WP_Error( 'user_invalid', __( 'User is invalid.', 'learnpress' ) );
303 - }
304 -
305 - $quizPostModel = $this->get_quiz_post_model();
306 - if ( ! $quizPostModel instanceof QuizPostModel ) {
307 - $can_retake = new WP_Error( 'quiz_invalid', __( 'Quiz is invalid.', 'learnpress' ) );
308 - }
309 -
310 - $course = CourseModel::find( $this->ref_id, true );
311 - if ( ! $course instanceof CourseModel ) {
312 - $can_retake = new WP_Error( 'course_invalid', __( 'Course is invalid.', 'learnpress' ) );
313 - }
314 -
315 - // Check user, course of quiz is enrolled.
316 - $userCourseModel = $this->get_user_course_model();
317 - if ( ! $userCourseModel instanceof UserCourseModel
318 - || $userCourseModel->get_graduation() !== LP_COURSE_GRADUATION_IN_PROGRESS ) {
319 - $can_retake = new WP_Error(
320 - 'not_errol_course',
321 - __( 'Please enroll in the course before starting the quiz.', 'learnpress' )
322 - );
323 - } elseif ( $userCourseModel->get_status() === LP_COURSE_FINISHED ) {
324 - $can_retake = new WP_Error(
325 - 'finished_course',
326 - __( 'You have already finished the course of this quiz.', 'learnpress' )
327 - );
328 - }
329 -
330 - // Check user quiz start and completed?.
331 - if ( $this->get_status() !== LP_ITEM_COMPLETED ) {
332 - $can_retake = new WP_Error( 'not_completed_quiz', __( 'You have not completed the quiz.', 'learnpress' ) );
333 - }
334 -
335 - // Check retaken count.
336 - $retake_max = $quizPostModel->get_retake_count();
337 - if ( $retake_max != -1 && $this->get_remaining_retake() === 0 ) {
338 - $can_retake = new WP_Error( 'exceed_retaken_count', __( 'You have exceeded the number of retakes.', 'learnpress' ) );
339 - }
340 -
341 - // Hook can retake quiz
342 - return apply_filters(
343 - 'learn-press/user/can-retake-quiz',
344 - $can_retake,
345 - $this
346 - );
347 - }
348 -
349 - /**
350 - * Get number retake remaining.
351 - *
352 - * @return int
353 - * @since 4.2.7.6
354 - * @version 1.0.0
355 - */
356 - public function get_remaining_retake(): int {
357 - $quizPostModel = $this->get_quiz_post_model();
358 - $retake_max = $quizPostModel->get_retake_count();
359 - $retaken_count = $this->get_retaken_count();
360 - $remaining_retake = $retake_max - $retaken_count;
361 - if ( $remaining_retake <= 0 ) {
362 - $remaining_retake = 0;
363 - }
364 -
365 - return $remaining_retake;
366 - }
367 -
368 - /**
369 - * Get all attempts of a quiz.
370 - *
371 - * @move from LP_Quiz
372 - *
373 - * @return array
374 - */
375 - public function get_attempts( $limit = 3 ) {
376 - $limit = $limit ?? 3;
377 -
378 - $limit = absint( apply_filters( 'lp/quiz/get-attempts/limit', $limit ) );
379 -
380 - $results = LP_User_Items_Result_DB::instance()->get_results( $this->get_user_item_id(), $limit, true );
381 - $output = array();
382 -
383 - if ( ! empty( $results ) ) {
384 - foreach ( $results as $result ) {
385 - if ( $result && is_string( $result ) ) {
386 - $result = json_decode( $result );
387 -
388 - unset( $result->questions );
389 -
390 - $output[] = $result;
391 - }
392 - }
393 - }
394 -
395 - return $output;
396 - }
397 -
398 - /**
399 - * Get number retaken count.
400 - * @move from LP_Quiz
401 - *
402 - * @return integer
403 - */
404 - public function get_retaken_count(): int {
405 - return absint( $this->get_meta_value_from_key( UserQuizMetaModel::KEY_RETAKEN_COUNT, 0 ) );
406 - }
407 -
408 - /**
409 - * Get all questions user has already used "Check"
410 - * @move from LP_Quiz
411 - *
412 - * @return array
413 - */
414 - public function get_checked_questions(): array {
415 - $value_str = $this->get_meta_value_from_key( UserQuizMetaModel::KEY_QUESTION_CHECKED, [] );
416 - $value = maybe_unserialize( $value_str );
417 -
418 - if ( $value ) {
419 - $value = (array) $value;
420 - } else {
421 - $value = array();
422 - }
423 -
424 - return $value;
425 - }
426 -
427 - /**
428 - * Get result when user completed quiz.
429 - *
430 - * @move from LP_Quiz
431 - * @return array
432 - */
433 - public function get_result(): array {
434 - $result = array(
435 - 'questions' => array(),
436 - 'mark' => 0,
437 - 'user_mark' => 0,
438 - 'minus_point' => 0,
439 - 'question_count' => 0,
440 - 'question_empty' => 0,
441 - 'question_answered' => 0,
442 - 'question_wrong' => 0,
443 - 'question_correct' => 0,
444 - 'status' => '',
445 - 'result' => 0,
446 - 'time_spend' => '',
447 - 'passing_grade' => '',
448 - 'pass' => 0,
449 - );
450 -
451 - try {
452 - $result_tmp = LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
453 - if ( $result_tmp ) {
454 - if ( isset( $result_tmp['user_mark'] ) && $result_tmp['user_mark'] < 0 ) {
455 - $result_tmp['user_mark'] = 0;
456 - }
457 -
458 - $result = $result_tmp;
459 - }
460 -
461 - return $result;
462 - } catch ( Throwable $e ) {
463 - error_log( __METHOD__ . ': ' . $e->getMessage() );
464 - }
465 -
466 - return $result;
467 - }
468 -
469 - /**
470 - * Calculate result of quiz.
471 - * @move from LP_Quiz
472 - * @param array $answered [question_id => answered, 'instant_check' => 0]
473 - *
474 - * @return array
475 - * @since 4.2.5
476 - * @version 1.0.0
477 - */
478 - public function calculate_quiz_result( array $answered ): array {
479 - $result = array(
480 - 'questions' => array(),
481 - 'mark' => 0,
482 - 'user_mark' => 0,
483 - 'minus_point' => 0,
484 - 'question_count' => 0,
485 - 'question_empty' => 0,
486 - 'question_answered' => 0,
487 - 'question_wrong' => 0,
488 - 'question_correct' => 0,
489 - 'status' => '',
490 - 'result' => 0,
491 - 'time_spend' => '',
492 - 'passing_grade' => '',
493 - 'pass' => 0,
494 - );
495 -
496 - $quizPostModel = $this->get_quiz_post_model();
497 - if ( ! $quizPostModel instanceof QuizPostModel ) {
498 - return $result;
499 - }
500 -
501 - $question_ids = $quizPostModel->get_question_ids();
502 - $result['mark'] = $quizPostModel->get_mark();
503 - $result['question_count'] = $quizPostModel->count_questions();
504 - $result['time_spend'] = $this->get_time_spend();
505 - $result['passing_grade'] = $quizPostModel->get_passing_grade();
506 - $checked_questions = $this->get_checked_questions();
507 -
508 - foreach ( $question_ids as $question_id ) {
509 - $question = LP_Question::get_question( $question_id );
510 - $point = floatval( $question->get_mark() );
511 -
512 - $result['questions'][ $question_id ] = array();
513 - $result['questions'][ $question_id ]['answered'] = $answered[ $question_id ] ?? '';
514 -
515 - if ( isset( $answered[ $question_id ] ) ) { // User's answer
516 - ++$result['question_answered'];
517 -
518 - $check = $question->check( $answered[ $question_id ] );
519 - $point = apply_filters( 'learn-press/user/calculate-quiz-result/point', $point, $question, $check );
520 - if ( $check['correct'] ) {
521 - ++$result['question_correct'];
522 - $result['user_mark'] += $point;
523 -
524 - $result['questions'][ $question_id ]['correct'] = true;
525 - $result['questions'][ $question_id ]['mark'] = $point;
526 - } else {
527 - if ( $quizPostModel->has_negative_marking() ) {
528 - $result['user_mark'] -= $point;
529 - $result['minus_point'] += $point;
530 - }
531 - ++$result['question_wrong'];
532 -
533 - $result['questions'][ $question_id ]['correct'] = false;
534 - $result['questions'][ $question_id ]['mark'] = 0;
535 - }
536 - } elseif ( ! array_key_exists( 'instant_check', $answered ) ) { // User skip question
537 - if ( $quizPostModel->has_minus_skip_questions() ) {
538 - $result['user_mark'] -= $point;
539 - $result['minus_point'] += $point;
540 - }
541 - ++$result['question_empty'];
542 -
543 - $result['questions'][ $question_id ]['correct'] = false;
544 - $result['questions'][ $question_id ]['mark'] = 0;
545 - }
546 -
547 - if ( $quizPostModel->has_instant_check() && ! array_key_exists( 'instant_check', $answered ) ) {
548 - $result['questions'][ $question_id ]['explanation'] = $question->get_explanation();
549 - $result['questions'][ $question_id ]['options'] = learn_press_get_question_options_for_js(
550 - $question,
551 - array(
552 - 'include_is_true' => in_array( $question_id, $checked_questions ) || $quizPostModel->has_show_correct_review(),
553 - 'answer' => $answered[ $question_id ] ?? '',
554 - )
555 - );
556 - }
557 - }
558 -
559 - if ( $result['user_mark'] < 0 ) {
560 - $result['user_mark'] = 0;
561 - }
562 -
563 - if ( $result['user_mark'] > 0 && $result['mark'] > 0 ) {
564 - $result['result'] = round( $result['user_mark'] * 100 / $result['mark'], 2, PHP_ROUND_HALF_DOWN );
565 - }
566 -
567 - $passing_grade = $quizPostModel->get_passing_grade();
568 - if ( $result['result'] >= $passing_grade ) {
569 - $result['pass'] = 1;
570 - } else {
571 - $result['pass'] = 0;
572 - }
573 -
574 - return $result;
575 - }
576 -
577 - /**
578 - * Get string time spend.
579 - *
580 - * @return string
581 - */
582 - public function get_time_spend(): string {
583 - $interval = $this->get_total_timestamp_completed();
584 - if ( empty( $interval ) ) {
585 - return '--:--';
586 - }
587 -
588 - $duration = new LP_Datetime( $interval );
589 - return $duration->format( 'H:i:s' );
590 - }
591 -
592 - /**
593 - * Get history user did quiz.
594 - *
595 - * @param int $limit
596 - *
597 - * @return array
598 - * @version 1.0.0
599 - * @since 4.2.7.6
600 - */
601 - public function get_history( int $limit = 3 ): array {
602 - $history = array();
603 -
604 - try {
605 - $results = LP_User_Items_Result_DB::instance()->get_results( $this->get_user_item_id(), $limit, true );
606 -
607 - if ( ! empty( $results ) ) {
608 - foreach ( $results as $result ) {
609 - if ( $result && is_string( $result ) ) {
610 - $result = LP_Helper::json_decode( $result );
611 -
612 - unset( $result->questions );
613 -
614 - $history[] = $result;
615 - }
616 - }
617 - }
618 - } catch ( Throwable $e ) {
619 - error_log( __METHOD__ . ': ' . $e->getMessage() );
620 - }
621 -
622 - return $history;
623 - }
624 - public function get_status_label( $status = '' ) {
625 - $statuses = array(
626 - 'started' => __( 'In Progress', 'learnpress' ),
627 - 'in-progress' => __( 'In Progress', 'learnpress' ),
628 - 'completed' => __( 'Completed', 'learnpress' ),
629 - 'passed' => __( 'Passed', 'learnpress' ),
630 - 'failed' => __( 'Failed', 'learnpress' ),
631 - );
632 -
633 - if ( ! $status ) {
634 - $status = $this->get_status();
635 - }
636 -
637 - return ! empty( $statuses[ $status ] ) ? $statuses[ $status ] : __( 'Not Started', 'learnpress' );
638 - }
639 -}
1 +<?php
2 +
3 +/**
4 + * Class UserQuizModel
5 + *
6 + * @package LearnPress/Classes
7 + * @version 1.0.2
8 + * @since 4.2.5
9 + */
10 +
11 +namespace LearnPress\Models\UserItems;
12 +
13 +use Exception;
14 +use LearnPress\Models\CourseModel;
15 +use LearnPress\Models\QuizPostModel;
16 +use LearnPress\Models\UserItemMeta\UserItemMetaModel;
17 +use LearnPress\Models\UserItemMeta\UserQuizMetaModel;
18 +use LearnPress\Models\UserModel;
19 +use LearnPressAssignment\Models\AssignmentPostModel;
20 +use LP_Course;
21 +use LP_Datetime;
22 +use LP_Helper;
23 +use LP_Question;
24 +use LP_Quiz;
25 +use LP_Quiz_CURD;
26 +use LP_User;
27 +use LP_User_Items_Result_DB;
28 +use Throwable;
29 +use WP_Error;
30 +
31 +class UserQuizModel extends UserItemModel {
32 + /**
33 + * Item type Course
34 + *
35 + * @var string Item type
36 + */
37 + public $item_type = LP_QUIZ_CPT;
38 + /**
39 + * Ref type Order
40 + *
41 + * @var string
42 + */
43 + public $ref_type = LP_COURSE_CPT;
44 +
45 + public const STATUS_STARTED = 'started';
46 +
47 + public function __construct( $data = null ) {
48 + parent::__construct( $data );
49 + }
50 +
51 + /**
52 + * Get quiz model
53 + *
54 + * @return bool|QuizPostModel
55 + * @since 4.2.5
56 + * @version 1.0.1
57 + */
58 + public function get_quiz_post_model() {
59 + return QuizPostModel::find( $this->item_id, true );
60 + }
61 +
62 + /**
63 + * Get course model
64 + *
65 + * @return false|CourseModel
66 + * @since 4.2.7.6
67 + * @version 1.0.0
68 + */
69 + public function get_course_model() {
70 + return CourseModel::find( $this->ref_id, true );
71 + }
72 +
73 + /**
74 + * Get user course model
75 + *
76 + * @return false|UserCourseModel
77 + * @since 4.2.7.6
78 + * @version 1.0.0
79 + */
80 + public function get_user_course_model() {
81 + return UserCourseModel::find( $this->user_id, $this->ref_id, true );
82 + }
83 +
84 + /**
85 + * Get status
86 + *
87 + * @return string
88 + */
89 + public function get_status(): string {
90 + return $this->status;
91 + }
92 +
93 + /**
94 + * Get all question's ids of the quiz.
95 + *
96 + * @param string $context
97 + *
98 + * @move from LP_Quiz
99 + * @return int[]
100 + * @editor tungnx
101 + * @version 1.0.1
102 + * @since 3.2.0
103 + */
104 + public function get_question_ids( string $context = 'display' ): array {
105 + $curd = new LP_Quiz_CURD();
106 + $question_ids = $curd->read_question_ids( $this->item_id, $context );
107 + $question_ids = apply_filters( 'learn-press/quiz/get-question-ids', $question_ids, $this->item_id, $this->ref_id, $context );
108 + if ( ! is_array( $question_ids ) ) {
109 + $question_ids = array();
110 + }
111 +
112 + return $question_ids;
113 + }
114 +
115 + /**
116 + * Get time remaining when user doing assignment.
117 + *
118 + * @return int seconds
119 + * @since 4.2.7.6
120 + * @version 1.0.0
121 + */
122 + public function get_time_remaining(): int {
123 + $time_remaining = 0;
124 + $quizPostModel = $this->get_quiz_post_model();
125 + if ( ! $quizPostModel instanceof QuizPostModel ) {
126 + return $time_remaining;
127 + }
128 +
129 + $duration = $quizPostModel->get_duration();
130 + $start_time_stamp = strtotime( $this->get_start_time() );
131 + $expire_time_stamp = strtotime( '+' . $duration, $start_time_stamp );
132 + $time_remaining = $expire_time_stamp - time();
133 +
134 + if ( $time_remaining < 0 ) {
135 + $time_remaining = 0;
136 + }
137 +
138 + return $time_remaining;
139 + }
140 +
141 + /**
142 + * Start quiz.
143 + *
144 + * @throws Exception
145 + * @since 4.2.5
146 + * @version 1.0.0
147 + */
148 + public function start_quiz() {
149 + $can_start = $this->check_can_start();
150 + if ( is_wp_error( $can_start ) ) {
151 + /**
152 + * @var WP_Error $can_start
153 + */
154 + throw new Exception( $can_start->get_error_message() );
155 + }
156 +
157 + $this->status = self::STATUS_STARTED;
158 + $this->graduation = LP_COURSE_GRADUATION_IN_PROGRESS;
159 + $this->save();
160 +
161 + // Hook old - random quiz using.
162 + do_action( 'learn-press/user/quiz-started', $this->item_id, $this->ref_id, $this->user_id );
163 + // Hook new
164 + do_action( 'learn-press/user/quiz/started', $this );
165 + }
166 +
167 + /**
168 + * Retake quiz.
169 + *
170 + * @throws Exception
171 + */
172 + public function retake() {
173 + $can_retake = $this->check_can_retake();
174 + if ( is_wp_error( $can_retake ) ) {
175 + /**
176 + * @var WP_Error $can_retake
177 + */
178 + throw new Exception( $can_retake->get_error_message() );
179 + }
180 +
181 + // Update retaken count.
182 + $user_quiz_retaken = $this->get_meta_model_from_key( UserQuizMetaModel::KEY_RETAKEN_COUNT );
183 + if ( $user_quiz_retaken instanceof UserItemMetaModel ) {
184 + $number_retaken = absint( $user_quiz_retaken->meta_value );
185 + ++$number_retaken;
186 + $user_quiz_retaken->meta_value = $number_retaken;
187 + $user_quiz_retaken->save();
188 + } else {
189 + $user_quiz_retaken_new = new UserQuizMetaModel();
190 + $user_quiz_retaken_new->learnpress_user_item_id = $this->get_user_item_id();
191 + $user_quiz_retaken_new->meta_key = UserQuizMetaModel::KEY_RETAKEN_COUNT;
192 + $user_quiz_retaken_new->meta_value = 1;
193 + $user_quiz_retaken_new->save();
194 + }
195 +
196 + //Todo: rewrite by object.
197 + //Create new result in table learnpress_user_item_results.
198 + LP_User_Items_Result_DB::instance()->insert( $this->get_user_item_id() );
199 + // Remove user_item_meta.
200 + learn_press_delete_user_item_meta( $this->get_user_item_id(), '_lp_question_checked' );
201 +
202 + $this->status = self::STATUS_STARTED;
203 + $this->start_time = gmdate( LP_Datetime::$format, time() );
204 + $this->end_time = null;
205 + $this->graduation = LP_COURSE_GRADUATION_IN_PROGRESS;
206 + $this->save();
207 +
208 + // Hook old - random quiz using.
209 + do_action( 'learn-press/user/quiz-retried', $this->item_id, $this->ref_id, $this->user_id );
210 + // Hook new
211 + do_action( 'learn-press/user/quiz/retake', $this );
212 + }
213 +
214 + /**
215 + * Check user can start quiz.
216 + * If user can start quiz, return true, else return WP_Error.
217 + * Set user, quiz, course, user_course and parent_id for this object.
218 + *
219 + * @throws Exception
220 + * return bool|WP_Error
221 + *
222 + * @since 4.2.5
223 + * @version 1.0.2
224 + */
225 + public function check_can_start() {
226 + $can_start = true;
227 +
228 + $userModel = $this->get_user_model();
229 + if ( ! $userModel instanceof UserModel ) {
230 + $can_start = new WP_Error( 'user_invalid', __( 'User is invalid.', 'learnpress' ) );
231 + }
232 +
233 + $quizPostModel = $this->get_quiz_post_model();
234 + if ( ! $quizPostModel instanceof QuizPostModel ) {
235 + $can_start = new WP_Error( 'quiz_invalid', __( 'Quiz is invalid.', 'learnpress' ) );
236 + }
237 +
238 + $courseModel = $this->get_course_model();
239 + if ( ! $courseModel instanceof CourseModel ) {
240 + $can_start = new WP_Error( 'course_invalid', __( 'Course is invalid.', 'learnpress' ) );
241 + }
242 +
243 + $userQuizModel = UserQuizModel::find_user_item(
244 + $this->user_id,
245 + $this->item_id,
246 + $this->item_type,
247 + $this->ref_id,
248 + $this->ref_type,
249 + true
250 + );
251 + if ( $userQuizModel instanceof UserQuizModel ) {
252 + $can_start = new WP_Error( 'started_quiz', __( 'You have already started the quiz.', 'learnpress' ) );
253 + }
254 +
255 + // Check user, course of quiz is enrolled.
256 + $userCourseModel = $this->get_user_course_model();
257 + if ( ! $userCourseModel instanceof UserCourseModel
258 + || $userCourseModel->status !== LP_COURSE_ENROLLED ) {
259 + $can_start = new WP_Error( 'not_errol_course', __( 'Please enroll in the course before starting the quiz.', 'learnpress' ) );
260 + } elseif ( $userCourseModel->status === LP_COURSE_FINISHED ) {
261 + $can_start = new WP_Error( 'finished_course', __( 'You have already finished the course of this quiz.', 'learnpress' ) );
262 + } else {
263 + // Set Parent id for user quiz to save DB.
264 + $this->parent_id = $userCourseModel->get_user_item_id();
265 + }
266 +
267 + // Hook old.
268 + if ( has_filter( 'learn-press/can-start-quiz' ) ) {
269 + try {
270 + $can_start = apply_filters(
271 + 'learn-press/can-start-quiz',
272 + $can_start,
273 + $userQuizModel->item_id,
274 + $userQuizModel->ref_id,
275 + $userQuizModel->user_id
276 + );
277 + } catch ( Throwable $e ) {
278 + $can_start = new WP_Error( 'can_not_start_quiz', $e->getMessage() );
279 + }
280 + }
281 +
282 + // Hook can start quiz
283 + return apply_filters(
284 + 'learn-press/user/can-start-quiz',
285 + $can_start,
286 + $this
287 + );
288 + }
289 +
290 + /**
291 + * Check can retake quiz.
292 + *
293 + * @throws Exception
294 + * @since 4.2.5
295 + * @version 1.0.1
296 + */
297 + public function check_can_retake() {
298 + $can_retake = true;
299 +
300 + $userModel = $this->get_user_model();
301 + if ( ! $userModel instanceof UserModel ) {
302 + $can_retake = new WP_Error( 'user_invalid', __( 'User is invalid.', 'learnpress' ) );
303 + }
304 +
305 + $quizPostModel = $this->get_quiz_post_model();
306 + if ( ! $quizPostModel instanceof QuizPostModel ) {
307 + $can_retake = new WP_Error( 'quiz_invalid', __( 'Quiz is invalid.', 'learnpress' ) );
308 + }
309 +
310 + $course = CourseModel::find( $this->ref_id, true );
311 + if ( ! $course instanceof CourseModel ) {
312 + $can_retake = new WP_Error( 'course_invalid', __( 'Course is invalid.', 'learnpress' ) );
313 + }
314 +
315 + // Check user, course of quiz is enrolled.
316 + $userCourseModel = $this->get_user_course_model();
317 + if ( ! $userCourseModel instanceof UserCourseModel
318 + || $userCourseModel->get_graduation() !== LP_COURSE_GRADUATION_IN_PROGRESS ) {
319 + $can_retake = new WP_Error(
320 + 'not_errol_course',
321 + __( 'Please enroll in the course before starting the quiz.', 'learnpress' )
322 + );
323 + } elseif ( $userCourseModel->get_status() === LP_COURSE_FINISHED ) {
324 + $can_retake = new WP_Error(
325 + 'finished_course',
326 + __( 'You have already finished the course of this quiz.', 'learnpress' )
327 + );
328 + }
329 +
330 + // Check user quiz start and completed?.
331 + if ( $this->get_status() !== LP_ITEM_COMPLETED ) {
332 + $can_retake = new WP_Error( 'not_completed_quiz', __( 'You have not completed the quiz.', 'learnpress' ) );
333 + }
334 +
335 + // Check retaken count.
336 + $retake_max = $quizPostModel->get_retake_count();
337 + if ( $retake_max != -1 && $this->get_remaining_retake() === 0 ) {
338 + $can_retake = new WP_Error( 'exceed_retaken_count', __( 'You have exceeded the number of retakes.', 'learnpress' ) );
339 + }
340 +
341 + // Hook can retake quiz
342 + return apply_filters(
343 + 'learn-press/user/can-retake-quiz',
344 + $can_retake,
345 + $this
346 + );
347 + }
348 +
349 + /**
350 + * Get number retake remaining.
351 + *
352 + * @return int
353 + * @since 4.2.7.6
354 + * @version 1.0.0
355 + */
356 + public function get_remaining_retake(): int {
357 + $quizPostModel = $this->get_quiz_post_model();
358 + $retake_max = $quizPostModel->get_retake_count();
359 + $retaken_count = $this->get_retaken_count();
360 + $remaining_retake = $retake_max - $retaken_count;
361 + if ( $remaining_retake <= 0 ) {
362 + $remaining_retake = 0;
363 + }
364 +
365 + return $remaining_retake;
366 + }
367 +
368 + /**
369 + * Get all attempts of a quiz.
370 + *
371 + * @move from LP_Quiz
372 + *
373 + * @return array
374 + */
375 + public function get_attempts( $limit = 3 ) {
376 + $limit = $limit ?? 3;
377 +
378 + $limit = absint( apply_filters( 'lp/quiz/get-attempts/limit', $limit ) );
379 +
380 + $results = LP_User_Items_Result_DB::instance()->get_results( $this->get_user_item_id(), $limit, true );
381 + $output = array();
382 +
383 + if ( ! empty( $results ) ) {
384 + foreach ( $results as $result ) {
385 + if ( $result && is_string( $result ) ) {
386 + $result = json_decode( $result );
387 +
388 + unset( $result->questions );
389 +
390 + $output[] = $result;
391 + }
392 + }
393 + }
394 +
395 + return $output;
396 + }
397 +
398 + /**
399 + * Get number retaken count.
400 + * @move from LP_Quiz
401 + *
402 + * @return integer
403 + */
404 + public function get_retaken_count(): int {
405 + return absint( $this->get_meta_value_from_key( UserQuizMetaModel::KEY_RETAKEN_COUNT, 0 ) );
406 + }
407 +
408 + /**
409 + * Get all questions user has already used "Check"
410 + * @move from LP_Quiz
411 + *
412 + * @return array
413 + */
414 + public function get_checked_questions(): array {
415 + $value_str = $this->get_meta_value_from_key( UserQuizMetaModel::KEY_QUESTION_CHECKED, [] );
416 + $value = maybe_unserialize( $value_str );
417 +
418 + if ( $value ) {
419 + $value = (array) $value;
420 + } else {
421 + $value = array();
422 + }
423 +
424 + return $value;
425 + }
426 +
427 + /**
428 + * Get result when user completed quiz.
429 + *
430 + * @move from LP_Quiz
431 + * @return array
432 + */
433 + public function get_result(): array {
434 + $result = array(
435 + 'questions' => array(),
436 + 'mark' => 0,
437 + 'user_mark' => 0,
438 + 'minus_point' => 0,
439 + 'question_count' => 0,
440 + 'question_empty' => 0,
441 + 'question_answered' => 0,
442 + 'question_wrong' => 0,
443 + 'question_correct' => 0,
444 + 'status' => '',
445 + 'result' => 0,
446 + 'time_spend' => '',
447 + 'passing_grade' => '',
448 + 'pass' => 0,
449 + );
450 +
451 + try {
452 + $result_tmp = LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
453 + if ( $result_tmp ) {
454 + if ( isset( $result_tmp['user_mark'] ) && $result_tmp['user_mark'] < 0 ) {
455 + $result_tmp['user_mark'] = 0;
456 + }
457 +
458 + $result = $result_tmp;
459 + }
460 +
461 + return $result;
462 + } catch ( Throwable $e ) {
463 + error_log( __METHOD__ . ': ' . $e->getMessage() );
464 + }
465 +
466 + return $result;
467 + }
468 +
469 + /**
470 + * Calculate result of quiz.
471 + * @move from LP_Quiz
472 + * @param array $answered [question_id => answered, 'instant_check' => 0]
473 + *
474 + * @return array
475 + * @since 4.2.5
476 + * @version 1.0.0
477 + */
478 + public function calculate_quiz_result( array $answered ): array {
479 + $result = array(
480 + 'questions' => array(),
481 + 'mark' => 0,
482 + 'user_mark' => 0,
483 + 'minus_point' => 0,
484 + 'question_count' => 0,
485 + 'question_empty' => 0,
486 + 'question_answered' => 0,
487 + 'question_wrong' => 0,
488 + 'question_correct' => 0,
489 + 'status' => '',
490 + 'result' => 0,
491 + 'time_spend' => '',
492 + 'passing_grade' => '',
493 + 'pass' => 0,
494 + );
495 +
496 + $quizPostModel = $this->get_quiz_post_model();
497 + if ( ! $quizPostModel instanceof QuizPostModel ) {
498 + return $result;
499 + }
500 +
501 + $question_ids = $quizPostModel->get_question_ids();
502 + $result['mark'] = $quizPostModel->get_mark();
503 + $result['question_count'] = $quizPostModel->count_questions();
504 + $result['time_spend'] = $this->get_time_spend();
505 + $result['passing_grade'] = $quizPostModel->get_passing_grade();
506 + $checked_questions = $this->get_checked_questions();
507 +
508 + foreach ( $question_ids as $question_id ) {
509 + $question = LP_Question::get_question( $question_id );
510 + $point = floatval( $question->get_mark() );
511 +
512 + $result['questions'][ $question_id ] = array();
513 + $result['questions'][ $question_id ]['answered'] = $answered[ $question_id ] ?? '';
514 +
515 + if ( isset( $answered[ $question_id ] ) ) { // User's answer
516 + ++$result['question_answered'];
517 +
518 + $check = $question->check( $answered[ $question_id ] );
519 + $point = apply_filters( 'learn-press/user/calculate-quiz-result/point', $point, $question, $check );
520 + if ( $check['correct'] ) {
521 + ++$result['question_correct'];
522 + $result['user_mark'] += $point;
523 +
524 + $result['questions'][ $question_id ]['correct'] = true;
525 + $result['questions'][ $question_id ]['mark'] = $point;
526 + } else {
527 + if ( $quizPostModel->has_negative_marking() ) {
528 + $result['user_mark'] -= $point;
529 + $result['minus_point'] += $point;
530 + }
531 + ++$result['question_wrong'];
532 +
533 + $result['questions'][ $question_id ]['correct'] = false;
534 + $result['questions'][ $question_id ]['mark'] = 0;
535 + }
536 + } elseif ( ! array_key_exists( 'instant_check', $answered ) ) { // User skip question
537 + if ( $quizPostModel->has_minus_skip_questions() ) {
538 + $result['user_mark'] -= $point;
539 + $result['minus_point'] += $point;
540 + }
541 + ++$result['question_empty'];
542 +
543 + $result['questions'][ $question_id ]['correct'] = false;
544 + $result['questions'][ $question_id ]['mark'] = 0;
545 + }
546 +
547 + if ( $quizPostModel->has_instant_check() && ! array_key_exists( 'instant_check', $answered ) ) {
548 + $result['questions'][ $question_id ]['explanation'] = $question->get_explanation();
549 + $result['questions'][ $question_id ]['options'] = learn_press_get_question_options_for_js(
550 + $question,
551 + array(
552 + 'include_is_true' => in_array( $question_id, $checked_questions ) || $quizPostModel->has_show_correct_review(),
553 + 'answer' => $answered[ $question_id ] ?? '',
554 + )
555 + );
556 + }
557 + }
558 +
559 + if ( $result['user_mark'] < 0 ) {
560 + $result['user_mark'] = 0;
561 + }
562 +
563 + if ( $result['user_mark'] > 0 && $result['mark'] > 0 ) {
564 + $result['result'] = round( $result['user_mark'] * 100 / $result['mark'], 2, PHP_ROUND_HALF_DOWN );
565 + }
566 +
567 + $passing_grade = $quizPostModel->get_passing_grade();
568 + if ( $result['result'] >= $passing_grade ) {
569 + $result['pass'] = 1;
570 + } else {
571 + $result['pass'] = 0;
572 + }
573 +
574 + return $result;
575 + }
576 +
577 + /**
578 + * Get string time spend.
579 + *
580 + * @return string
581 + */
582 + public function get_time_spend(): string {
583 + $interval = $this->get_total_timestamp_completed();
584 + if ( empty( $interval ) ) {
585 + return '--:--';
586 + }
587 +
588 + $duration = new LP_Datetime( $interval );
589 + return $duration->format( 'H:i:s' );
590 + }
591 +
592 + /**
593 + * Get history user did quiz.
594 + *
595 + * @param int $limit
596 + *
597 + * @return array
598 + * @version 1.0.0
599 + * @since 4.2.7.6
600 + */
601 + public function get_history( int $limit = 3 ): array {
602 + $history = array();
603 +
604 + try {
605 + $results = LP_User_Items_Result_DB::instance()->get_results( $this->get_user_item_id(), $limit, true );
606 +
607 + if ( ! empty( $results ) ) {
608 + foreach ( $results as $result ) {
609 + if ( $result && is_string( $result ) ) {
610 + $result = LP_Helper::json_decode( $result );
611 +
612 + unset( $result->questions );
613 +
614 + $history[] = $result;
615 + }
616 + }
617 + }
618 + } catch ( Throwable $e ) {
619 + error_log( __METHOD__ . ': ' . $e->getMessage() );
620 + }
621 +
622 + return $history;
623 + }
624 +}