PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.10
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.10
4.4.10 4.4.9.1 4.4.9 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 All 142 releases
← All changes | inc/user-item/class-lp-user-item-quiz.php +930 -906 4.3.8 → 4.4.10 View file →
@@ -1,906 +1,930 @@
1 -<?php
2 -
3 -/**
4 - * Class LP_User_Item_Quiz
5 - */
6 -class LP_User_Item_Quiz extends LP_User_Item {
7 - public $_item_type = LP_QUIZ_CPT;
8 - public $_ref_type = LP_COURSE_CPT;
9 - /**
10 - * @var array
11 - */
12 - protected $_answers = array();
13 -
14 - /**
15 - * LP_User_Item_Quiz constructor.
16 - *
17 - * @param array $data
18 - */
19 - public function __construct( $data ) {
20 - $this->_curd = new LP_Quiz_CURD();
21 -
22 - parent::__construct( $data );
23 - $this->_parse_answers();
24 - }
25 -
26 - /**
27 - *
28 - */
29 - protected function _parse_answers() {
30 - foreach ( array( '_question_answers', 'question_answers' ) as $k ) {
31 - $answers = learn_press_get_user_item_meta( $this->get_user_item_id(), $k, true );
32 -
33 - if ( $answers ) {
34 - $this->_answers = $answers;
35 - break;
36 - }
37 - }
38 - }
39 -
40 - /**
41 - * Add user answer to DB.
42 - *
43 - * @param int|array $id
44 - * @param mixed $values
45 - *
46 - * @return array|bool|LP_Quiz_Results|mixed
47 - * @throws Exception
48 - * @editor tungnx
49 - * @modify 4.1.4.1 - comment - not use
50 - */
51 - /*
52 - public function add_question_answer( $id, $values = null ) {
53 - $results = $this->get_results( '' );
54 -
55 - if ( ! $results ) {
56 - return false;
57 - }
58 -
59 - $questions = $results->get( 'questions', array() );
60 -
61 - if ( is_numeric( $id ) ) {
62 - $values = array( $id => $values );
63 - } else {
64 - $values = (array) $id;
65 - }
66 -
67 - foreach ( $values as $id => $answer ) {
68 - if ( ! $this->has_checked_question( $id ) ) {
69 - if ( ! empty( $questions[ $id ] ) ) {
70 - $questions[ $id ]['answered'] = $answer;
71 - } else {
72 - $questions[ $id ] = array( 'answered' => $answer );
73 - }
74 - }
75 - }
76 -
77 - $results['questions'] = $questions;
78 -
79 - //LP_User_Items_Result_DB::instance()->update( $this->get_user_item_id(), wp_json_encode( $results->get() ) );
80 -
81 - $this->calculate_results();
82 -
83 - $cache_key = sprintf( 'quiz-%d-%d-%d', $this->get_user_id(), $this->get_course_id(), $this->get_item_id() );
84 -
85 - LP_Object_Cache::set( $cache_key, false, 'learn-press/quiz-result' );
86 -
87 - return $this->get_results( '' );
88 - }*/
89 -
90 - public function get_question_answer( $id ) {
91 - $results = $this->get_results( '' );
92 -
93 - if ( ! $results ) {
94 - return false;
95 - }
96 -
97 - $questions = $results->get( 'questions', false );
98 -
99 - if ( $questions && is_array( $questions[ $id ] ) ) {
100 - return $questions[ $id ]['answered'];
101 - }
102 -
103 - return false;
104 - }
105 -
106 - /**
107 - * Update data to database
108 - *
109 - * @param bool $force
110 - * @param bool $wp_error
111 - *
112 - * @return bool|mixed
113 - */
114 - /*
115 - public function update( $force = false, $wp_error = false ) {
116 - $return = parent::update( $force, $wp_error );
117 - $this->calculate_results();
118 -
119 - return $return;
120 - }*/
121 -
122 - /**
123 - * Get list of data to update to database
124 - *
125 - * @since 3.1.0
126 - *
127 - * @return array
128 - */
129 - public function get_mysql_data() {
130 - $columns = parent::get_mysql_data();
131 -
132 - return apply_filters( 'learn-press/update-user-item-quiz-data', $columns, $this->get_item_id(), $this->get_course_id(), $this->get_user_id() );
133 - }
134 -
135 - /**
136 - * @return bool|LP_Quiz
137 - */
138 - public function get_quiz() {
139 - return learn_press_get_quiz( $this->get_item_id() );
140 - }
141 -
142 - public function get_status_label( $status = '' ) {
143 - $statuses = array(
144 - 'started' => __( 'In Progress', 'learnpress' ),
145 - 'in-progress' => __( 'In Progress', 'learnpress' ),
146 - 'completed' => __( 'Completed', 'learnpress' ),
147 - 'passed' => __( 'Passed', 'learnpress' ),
148 - 'failed' => __( 'Failed', 'learnpress' ),
149 - );
150 -
151 - if ( ! $status ) {
152 - $status = $this->get_status();
153 - }
154 -
155 - return ! empty( $statuses[ $status ] ) ? $statuses[ $status ] : __( 'Not Started', 'learnpress' );
156 - }
157 -
158 - /**
159 - * Get ID of the course that this item assigned to.
160 - *
161 - * @return array|mixed
162 - */
163 - public function get_course_id() {
164 - return $this->get_data( 'ref_id' );
165 - }
166 -
167 - /**
168 - * Get result
169 - *
170 - * @param string $prop
171 - *
172 - * @return array|bool|mixed
173 - */
174 - public function get_result( $prop = '' ) {
175 - $result = $this->calculate_quiz_result();
176 -
177 - // Fix temporary for case call 'grade' - addons called
178 - if ( 'grade' === $prop ) {
179 - if ( $result['pass'] ) {
180 - $result['grade'] = 'passed';
181 - } else {
182 - $result['grade'] = 'failed';
183 - }
184 - }
185 -
186 - return $prop && $result && array_key_exists( $prop, $result ) ? $result[ $prop ] : $result;
187 - }
188 -
189 - /**
190 - * Calculate result of quiz.
191 - *
192 - * @param string $prop
193 - * @param bool $force - Optional. Force to refresh cache.
194 - *
195 - * Clear cache on
196 - * @see LP_REST_Users_Controller::start_quiz() | retake quiz
197 - *
198 - * @return LP_Quiz_Results|bool|mixed
199 - * @throws Exception
200 - * @editor tungnx
201 - * @modify 4.1.3
202 - * @version 4.0.1
203 - */
204 - public function get_results( string $prop = 'result', bool $force = false ) {
205 - if ( ! $this->get_status() ) {
206 - return false;
207 - }
208 -
209 - $lp_quiz_cache = LP_Quiz_Cache::instance();
210 -
211 - $key_cache = sprintf( '%d/user/%d/course/%d', $this->get_item_id(), $this->get_user_id(), $this->get_course_id() );
212 - $result = $lp_quiz_cache->get_cache( $key_cache );
213 -
214 - if ( false === $result || $force ) {
215 - // $result = $this->_get_results();
216 -
217 - // if ( false === $result ) {
218 - $result = $this->calculate_results();
219 - // }
220 -
221 - $lp_quiz_cache->set_cache( $key_cache, $result );
222 - }
223 -
224 - $result['user_item_id'] = $this->get_user_item_id();
225 - $result['interval'] = array( $this->get_start_time(), $this->get_end_time() );
226 - $result['graduation'] = $this->get_graduation();
227 - $result['graduationText'] = $this->get_graduation_text();
228 -
229 - return $prop ? $result[ $prop ] : new LP_Quiz_Results( $result );
230 - }
231 -
232 - /**
233 - * Get user quiz graduation text for displaying purpose. [Passed, Failed, null]
234 - *
235 - * @since 4.0.0
236 - *
237 - * @return mixed
238 - */
239 - public function get_graduation_text() {
240 - $graduation = $this->get_graduation();
241 -
242 - return apply_filters( 'learn-press/user-quiz-graduation-text', learn_press_get_graduation_text( $graduation ) );
243 - }
244 -
245 - /**
246 - * Get Timestamp remaining when user doing quiz
247 - *
248 - * @author tungnx
249 - * @version 1.0.1
250 - * @sicne 4.1.4.1
251 - * @return int
252 - */
253 - public function get_timestamp_remaining(): int {
254 - $timestamp_remaining = - 1;
255 - $quiz = false;
256 - $user_quiz = false;
257 -
258 - try {
259 - $quiz = learn_press_get_quiz( $this->get_item_id() );
260 - if ( ! $quiz || LP_ITEM_STARTED != $this->get_status() ) {
261 - return $timestamp_remaining;
262 - }
263 -
264 - $parent_id = $this->get_parent_id();
265 - $duration = $quiz->get_duration()->get() . ' second';
266 -
267 - $filter = new LP_User_Items_Filter();
268 - $filter->parent_id = $parent_id;
269 - $filter->item_id = $this->get_item_id();
270 - $filter->user_id = get_current_user_id();
271 - $user_quiz = LP_User_Items_DB::getInstance()->get_user_course_item( $filter, true );
272 - $course_start_time = $user_quiz->start_time;
273 - $timestamp_expire = strtotime( $course_start_time . ' +' . $duration );
274 - $timestamp_current = time();
275 - $timestamp_remaining = $timestamp_expire - $timestamp_current;
276 -
277 - if ( $timestamp_remaining < 0 ) {
278 - $timestamp_remaining = 0;
279 - }
280 - } catch ( Throwable $e ) {
281 -
282 - }
283 -
284 - return apply_filters( 'learn-press/user-course-quiz/timestamp_remaining', $timestamp_remaining, $user_quiz, $quiz );
285 - }
286 -
287 - /**
288 - * Get all attempts of a quiz.
289 - *
290 - * @param string $args
291 - *
292 - * @return array
293 - */
294 - public function get_attempts( $limit = 3 ) {
295 - $limit = $limit ?? 3;
296 -
297 - $limit = absint( apply_filters( 'lp/quiz/get-attempts/limit', $limit ) );
298 -
299 - $results = LP_User_Items_Result_DB::instance()->get_results( $this->get_user_item_id(), $limit, true );
300 - $output = array();
301 -
302 - if ( ! empty( $results ) ) {
303 - foreach ( $results as $result ) {
304 - if ( $result && is_string( $result ) ) {
305 - $result = json_decode( $result );
306 -
307 - unset( $result->questions );
308 -
309 - $output[] = $result;
310 - }
311 - }
312 - }
313 -
314 - return $output;
315 - }
316 -
317 - /**
318 - * Get question ids user has started inside quiz.
319 - *
320 - * @since 4.0.0
321 - *
322 - * @return bool|array
323 - */
324 - public function get_questions() {
325 - $quiz = learn_press_get_quiz( $this->get_item_id() );
326 - $ids = $quiz->get_question_ids();
327 -
328 - return apply_filters( 'learn-press/user-item-quiz-questions', $ids, $this->get_user_id(), $this );
329 - }
330 -
331 - /**
332 - * Calculate results of quiz.
333 - *
334 - * @return array
335 - * @throws Exception
336 - * @version 4.0.0
337 - */
338 - public function calculate_results(): array {
339 - $quiz = learn_press_get_quiz( $this->get_item_id() );
340 - $last_results = LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
341 -
342 - if ( ! $last_results ) {
343 - $last_results = array();
344 - }
345 -
346 - $questions = $last_results['questions'] ?? array_fill_keys( $quiz->get_question_ids(), array() );
347 -
348 - $result = array(
349 - 'questions' => array(),
350 - 'mark' => $quiz->get_mark(),
351 - 'user_mark' => 0,
352 - 'question_count' => 0,
353 - 'question_empty' => 0,
354 - 'question_answered' => 0,
355 - 'question_wrong' => 0,
356 - 'question_correct' => 0,
357 - 'status' => $this->get_status(),
358 - 'result' => 0,
359 - 'time_spend' => $this->get_time_interval( 'display' ),
360 - 'passing_grade' => $quiz->get_passing_grade(),
361 - );
362 -
363 - if ( $questions ) {
364 - foreach ( $questions as $question_id => $last_checked ) {
365 - $question = LP_Question::get_question( $question_id );
366 - if ( ! $question ) {
367 - continue;
368 - }
369 -
370 - $answered = array_key_exists( 'answered', $last_checked ) ? $last_checked['answered'] : '';
371 - $check = apply_filters( 'learn-press/quiz/check-question-result', $question->check( $answered ), $question_id, $this );
372 - $check['answered'] = $answered;
373 -
374 - if ( $check['answered'] && $check['correct'] ) {
375 - $result['question_correct'] ++;
376 - $result['user_mark'] += array_key_exists( 'mark', $check ) ? floatval( $check['mark'] ) : $question->get_mark();
377 - } else {
378 - $negative_marking = apply_filters( 'learn-press/get-negative-marking-value', floatval( $question->get_mark() ), $question_id, $quiz->get_id() );
379 -
380 - // If answered is empty consider user has skipped question
381 - if ( ! $check['answered'] ) {
382 - if ( $quiz->get_negative_marking() && $quiz->get_minus_skip_questions() ) {
383 - $result['user_mark'] -= $negative_marking;
384 - }
385 - $result['question_empty'] ++;
386 - } else {
387 - if ( $quiz->get_negative_marking() ) {
388 - $result['user_mark'] -= $negative_marking;
389 - }
390 - $result['question_wrong'] ++;
391 - }
392 - }
393 -
394 - $result['questions'][ $question_id ] = apply_filters( 'learn-press/question-results-data', $last_checked ? array_merge( $last_checked, $check ) : $check, $question_id, $quiz->get_id() );
395 -
396 - if ( $check['answered'] ) {
397 - $result['question_answered'] ++;
398 - }
399 - }
400 -
401 - $result['user_mark'] = ( $result['user_mark'] >= 0 ) ? $result['user_mark'] : 0;
402 -
403 - $percent = $result['mark'] ? ( $result['user_mark'] / $result['mark'] ) * 100 : 0;
404 - $result['result'] = $percent;
405 - $grade = '';
406 -
407 - if ( $this->get_status() === 'completed' ) {
408 - $grade = $percent >= $this->get_quiz()->get_data( 'passing_grade' ) ? 'passed' : 'failed';
409 - $this->_set_data( 'graduation', $grade );
410 - }
411 -
412 - $result['question_count'] = count( $questions );
413 -
414 - if ( $grade ) {
415 - learn_press_update_user_item_field(
416 - array(
417 - 'graduation' => $grade,
418 - ),
419 - array(
420 - 'user_item_id' => $this->get_user_item_id(),
421 - )
422 - );
423 - }
424 - }
425 -
426 - return $result;
427 - }
428 -
429 - /**
430 - * Calculate result of quiz.
431 - *
432 - * @param array $answered [question_id => answered, 'instant_check' => 0]
433 - *
434 - * @return array
435 - * @version 1.0.1
436 - * @author tungnx
437 - * @since 4.1.4.1
438 - */
439 - public function calculate_quiz_result( array $answered = array() ): array {
440 - $result = array(
441 - 'questions' => array(),
442 - 'mark' => 0,
443 - 'user_mark' => 0,
444 - 'minus_point' => 0,
445 - 'question_count' => 0,
446 - 'question_empty' => 0,
447 - 'question_answered' => 0,
448 - 'question_wrong' => 0,
449 - 'question_correct' => 0,
450 - 'status' => '',
451 - 'result' => 0,
452 - 'time_spend' => '',
453 - 'passing_grade' => '',
454 - 'pass' => 0,
455 - );
456 -
457 - try {
458 - if ( LP_ITEM_COMPLETED === $this->get_status() ) {
459 - $result_tmp = LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
460 -
461 - if ( $result_tmp ) {
462 - if ( isset( $result_tmp['user_mark'] ) && $result_tmp['user_mark'] < 0 ) {
463 - $result_tmp['user_mark'] = 0;
464 - }
465 -
466 - $result = $result_tmp;
467 - }
468 -
469 - return $result;
470 - }
471 -
472 - $quiz = learn_press_get_quiz( $this->get_item_id() );
473 -
474 - if ( ! $quiz ) {
475 - throw new Exception();
476 - }
477 -
478 - $question_ids = $quiz->get_question_ids();
479 - $result['mark'] = $quiz->get_mark();
480 - $result['question_count'] = $quiz->count_questions();
481 - $result['time_spend'] = $this->get_time_interval( 'display' );
482 - $result['passing_grade'] = $quiz->get_passing_grade();
483 - $checked_questions = $this->get_checked_questions();
484 -
485 - foreach ( $question_ids as $question_id ) {
486 - $question = LP_Question::get_question( $question_id );
487 - $point = floatval( $question->get_mark() );
488 -
489 - $result['questions'][ $question_id ] = array();
490 - $result['questions'][ $question_id ]['answered'] = $answered[ $question_id ] ?? '';
491 -
492 - if ( isset( $answered[ $question_id ] ) ) { // User's answer
493 - $result['question_answered']++;
494 -
495 - $check = $question->check( $answered[ $question_id ] );
496 - $point = apply_filters( 'learn-press/user/calculate-quiz-result/point', $point, $question, $check );
497 - if ( $check['correct'] ) {
498 - $result['question_correct']++;
499 - $result['user_mark'] += $point;
500 -
501 - $result['questions'][ $question_id ]['correct'] = true;
502 - $result['questions'][ $question_id ]['mark'] = $point;
503 - } else {
504 - if ( $quiz->get_negative_marking() ) {
505 - $result['user_mark'] -= $point;
506 - $result['minus_point'] += $point;
507 - }
508 - $result['question_wrong']++;
509 -
510 - $result['questions'][ $question_id ]['correct'] = false;
511 - $result['questions'][ $question_id ]['mark'] = 0;
512 - }
513 - } elseif ( ! array_key_exists( 'instant_check', $answered ) ) { // User skip question
514 - if ( $quiz->get_minus_skip_questions() ) {
515 - $result['user_mark'] -= $point;
516 - $result['minus_point'] += $point;
517 - }
518 - $result['question_empty']++;
519 -
520 - $result['questions'][ $question_id ]['correct'] = false;
521 - $result['questions'][ $question_id ]['mark'] = 0;
522 - }
523 -
524 - $can_review_quiz = get_post_meta( $quiz->get_id(), '_lp_review', true ) === 'yes';
525 - if ( $can_review_quiz && ! array_key_exists( 'instant_check', $answered ) ) {
526 -
527 - $result['questions'][ $question_id ]['explanation'] = $question->get_explanation();
528 - $result['questions'][ $question_id ]['options'] = learn_press_get_question_options_for_js(
529 - $question,
530 - array(
531 - 'include_is_true' => in_array( $question_id, $checked_questions ) || get_post_meta( $quiz->get_id(), '_lp_show_correct_review', true ) === 'yes',
532 - 'answer' => $answered[ $question_id ] ?? '',
533 - )
534 - );
535 - }
536 - }
537 -
538 - if ( $result['user_mark'] < 0 ) {
539 - $result['user_mark'] = 0;
540 - }
541 -
542 - if ( $result['user_mark'] > 0 && $result['mark'] > 0 ) {
543 - $result['result'] = round( $result['user_mark'] * 100 / $result['mark'], 2, PHP_ROUND_HALF_DOWN );
544 - }
545 -
546 - $passing_grade = $quiz->get_data( 'passing_grade', 0 );
547 - if ( $result['result'] >= $passing_grade ) {
548 - $result['pass'] = 1;
549 - } else {
550 - $result['pass'] = 0;
551 - }
552 - } catch ( Throwable $e ) {
553 -
554 - }
555 -
556 - return $result;
557 - }
558 -
559 - public function get_option_answer() {
560 -
561 - }
562 -
563 - protected function _get_results() {
564 - return LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
565 - }
566 -
567 - public function get_percent_result( $decimal = 2 ) {
568 - return apply_filters( 'learn-press/user/quiz-percent-result', sprintf( '%s%%', round( $this->get_result( 'result' ), $decimal ), $this->get_user_id(), $this->get_item_id() ) );
569 - }
570 -
571 - public function get_time_interval( $context = '' ) {
572 - $interval = parent::get_time_interval();
573 -
574 - if ( $context == 'display' ) {
575 - $quiz = $this->get_quiz();
576 -
577 - if ( $quiz && $interval && $quiz->get_duration() ) {
578 - $interval = new LP_Duration( $interval );
579 - $interval = $interval->to_timer();
580 - } else {
581 - $interval = '--:--';
582 - }
583 - }
584 -
585 - return $interval;
586 - }
587 -
588 - /**
589 - * Return TRUE if user has pressed SKIP on this question
590 - *
591 - * @param int $question_id
592 - *
593 - * @return bool
594 - */
595 - public function is_skipped( $question_id ) {
596 - return $this->get_question_answer( $question_id ) === '__SKIPPED__';
597 - }
598 -
599 - public function is_answered( $question_id ) {
600 - $result = $this->get_results();
601 -
602 - if ( ! empty( $result['questions'][ $question_id ] ) ) {
603 - return $result['questions'][ $question_id ]['answered'];
604 - }
605 -
606 - return false;
607 - }
608 -
609 - public function is_answered_true( $question_id ) {
610 - $result = $this->get_results( false );
611 -
612 - if ( ! empty( $result['questions'][ $question_id ] ) ) {
613 - return $result['questions'][ $question_id ]['correct'];
614 - }
615 -
616 - return false;
617 - }
618 -
619 - /**
620 - * Get questions user has answered.
621 - *
622 - * @param bool $percent - Optional. TRUE will return by percentage with total questions.
623 - *
624 - * @return float|int|mixed
625 - */
626 - public function get_questions_answered( $percent = false ) {
627 - $result = $this->get_results( '' );
628 -
629 - if ( $percent ) {
630 - if ( $result['question_count'] ) {
631 - $return = 0;
632 - } else {
633 - $return = $result['question_answered'] ? ( $result['question_answered'] / $result['question_count'] ) * 100 : 0;
634 - }
635 - } else {
636 - $return = $result['question_answered'];
637 - }
638 -
639 - return $return;
640 - }
641 -
642 - /**
643 - * Get total mark user achieved.
644 - *
645 - * @param bool $percent - Optional. TRUE will return by percentage with total mark.
646 - *
647 - * @return float|int|mixed
648 - */
649 - public function get_mark( $percent = false ) {
650 - $result = $this->get_results();
651 -
652 - if ( $percent ) {
653 - $return = $result['mark'] ? ( $result['user_mark'] / $result['mark'] ) * 100 : 0;
654 - } else {
655 - $return = $result['user_mark'];
656 - }
657 -
658 - return $return;
659 - }
660 -
661 - /**
662 - * @deprecated 4.2.0
663 - */
664 - public function get_total_questions() {
665 - _deprecated_function( __METHOD__, '4.2.0' );
666 - $quiz = learn_press_get_quiz( $this->get_item_id() );
667 - $questions = $quiz->get_question_ids();
668 -
669 - return sizeof( $questions );
670 - }
671 -
672 - public function get_quiz_mark() {
673 - $result = $this->get_results();
674 -
675 - return $result['mark'];
676 - }
677 -
678 - /**
679 - * Return time remaining.
680 - *
681 - * @param string $return - Optional.
682 - *
683 - * @return LP_Duration
684 - * @deprecated 4.1.7.3
685 - */
686 - public function get_time_remaining( $return = 'object' ) {
687 - _deprecated_function( __METHOD__, '4.1.7.3' );
688 - /*$time = parent::get_time_remaining( $return );
689 -
690 - return apply_filters( 'learn-press/quiz/time-remaining', $time, $this->get_item_id(), $this->get_course_id(), $this->get_user_id() );*/
691 - }
692 -
693 - /**
694 - * Get all questions user has already used "Check"
695 - *
696 - * @return array
697 - */
698 - public function get_checked_questions(): array {
699 - $value = $this->get_meta( '_lp_question_checked', true );
700 -
701 - if ( $value ) {
702 - $value = (array) $value;
703 - } else {
704 - $value = array();
705 - }
706 -
707 - return $value;
708 - }
709 -
710 - public function add_checked_question( $id ) {
711 - settype( $id, 'array' );
712 - $checked = $this->get_checked_questions();
713 - $checked = array_merge( $checked, $id );
714 -
715 - $this->update_meta( '_lp_question_checked', $checked );
716 -
717 - return $checked;
718 - }
719 -
720 - /**
721 - * Return true if user has already checked a question.
722 - *
723 - * @param int $question_id
724 - *
725 - * @return bool
726 - */
727 - public function has_checked_question( $question_id ) {
728 - return in_array( $question_id, $this->get_checked_questions() );
729 - }
730 -
731 - /**
732 - * Instant check question
733 - *
734 - * @param int $question_id
735 - * @param mixed $answered
736 - *
737 - * @return array
738 - * @throws Exception
739 - */
740 - public function instant_check_question( int $question_id, $answered = null ): array {
741 - $question = learn_press_get_question( $question_id );
742 - if ( ! $question ) {
743 - throw new Exception( __( 'The question is invalid!', 'learnpress' ) );
744 - }
745 -
746 - $can_check = $this->can_check_answer( $question_id );
747 - if ( ! $can_check ) {
748 - throw new Exception( __( 'Cannot check the answer to the question.', 'learnpress' ) );
749 - }
750 -
751 - $answered_check = array(
752 - 'instant_check' => 1,
753 - $question_id => $answered,
754 - );
755 -
756 - // For case save result when check instant answer
757 - $result_instant_check = LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
758 - if ( $result_instant_check ) {
759 - foreach ( $result_instant_check['questions'] as $question_answer_id => $question_answer ) {
760 - if ( ! empty( $question_answer['answered'] ) ) {
761 - $answered_check[ $question_answer_id ] = $question_answer['answered'];
762 - }
763 - }
764 - }
765 -
766 - $result_answer = $this->calculate_quiz_result( $answered_check );
767 -
768 - LP_User_Items_Result_DB::instance()->update( $this->get_user_item_id(), json_encode( $result_answer ) );
769 - $this->add_checked_question( $question_id );
770 -
771 - $checked['answered'] = $answered;
772 - $checked['mark'] = $result_answer['questions'][ $question_id ]['mark'];
773 - $checked['correct'] = $result_answer['questions'][ $question_id ]['correct'];
774 -
775 - return $checked;
776 - }
777 -
778 - /**
779 - * @param int $question_id
780 - *
781 - * @return int
782 - * @deprecated 4.2.5
783 - */
784 - public function hint( $question_id ) {
785 - _deprecated_function( __METHOD__, '4.2.5' );
786 - return false;
787 - $remain = $this->can_hint_answer();
788 -
789 - if ( $remain ) {
790 - if ( ! $this->has_hinted_question( $question_id ) ) {
791 - $checked = $this->get_hint_questions();
792 - $checked[] = $question_id;
793 - $this->set_meta( '_lp_question_hint', $checked );
794 - }
795 -
796 - $count = $this->get_count_hint();
797 - $this->update_meta();
798 - $remain --;
799 - } else {
800 - return false;
801 - }
802 -
803 - return $remain;
804 - }
805 -
806 - /**
807 - * Return true if user has already checked a question.
808 - *
809 - * @param int $question_id
810 - *
811 - * @return bool
812 - */
813 - public function has_hinted_question( $question_id ) {
814 - return in_array( $question_id, $this->get_hint_questions() );
815 - }
816 -
817 - public function get_check_answer_count() {
818 - return count( $this->get_checked_questions() );
819 - }
820 -
821 - /**
822 - * @param int $question_id - Added since 3.3.0
823 - *
824 - * @return bool
825 - */
826 - public function can_check_answer( int $question_id = 0 ) : bool {
827 - $can = false;
828 - $quiz = learn_press_get_quiz( $this->get_item_id() );
829 -
830 - if ( ! $quiz ) {
831 - return $can;
832 - }
833 -
834 - if ( $quiz->get_instant_check() && $this->get_status() === 'started' ) {
835 - $can = ! $this->has_checked_question( $question_id );
836 - }
837 -
838 - return apply_filters( 'learn-press/can-instant-check-question', $can, $question_id, $this->get_item_id(), $this->get_course_id() );
839 - }
840 -
841 - /**
842 - * Get number retaken count.
843 - *
844 - * @return integer
845 - */
846 - public function get_retaken_count(): int {
847 - return absint( learn_press_get_user_item_meta( $this->get_user_item_id(), '_lp_retaken_count' ) );
848 - }
849 -
850 - /**
851 - * Update learnpress_user_itemmeta retaken
852 - *
853 - * @return void
854 - */
855 - public function update_retake_count() {
856 - $count = $this->get_retaken_count();
857 - $count ++;
858 -
859 - return $this->update_meta( '_lp_retaken_count', $count );
860 - }
861 -
862 - /**
863 - * Get all questions user has already used "Check"
864 - *
865 - * @return array
866 - * @deprecated 4.2.5
867 - */
868 - public function get_hint_questions() {
869 - return [];
870 - _deprecated_function( __METHOD__, '4.2.5' );
871 - $value = $this->get_meta( '_lp_question_hint', true );
872 -
873 - if ( $value ) {
874 - $value = (array) $value;
875 - } else {
876 - $value = array();
877 - }
878 -
879 - return $value;
880 - }
881 -
882 - /**
883 - * @deprecated 4.2.5
884 - */
885 - public function get_count_hint() {
886 - _deprecated_function( __METHOD__, '4.2.5' );
887 - return 0;
888 - return count( $this->get_hint_questions() );
889 - }
890 -
891 - /**
892 - * Return true if check answer is enabled.
893 - *
894 - * @return bool
895 - */
896 - public function can_hint_answer() {
897 - return apply_filters( 'learn-press/user-quiz/can-hint-answer', true, $this->get_id(), $this->get_course_id() );
898 - }
899 -
900 - /**
901 - * @return bool
902 - */
903 - public function is_review_questions(): bool {
904 - return LP_Global::quiz_question() && ( $this->get_status() === 'completed' );
905 - }
906 -}
1 +<?php
2 +
3 +/**
4 + * Class LP_User_Item_Quiz
5 + */
6 +class LP_User_Item_Quiz extends LP_User_Item {
7 + public $_item_type = LP_QUIZ_CPT;
8 + public $_ref_type = LP_COURSE_CPT;
9 + /**
10 + * @var array
11 + */
12 + protected $_answers = array();
13 +
14 + /**
15 + * LP_User_Item_Quiz constructor.
16 + *
17 + * @param array $data
18 + */
19 + public function __construct( $data ) {
20 + $this->_curd = new LP_Quiz_CURD();
21 +
22 + parent::__construct( $data );
23 + $this->_parse_answers();
24 + }
25 +
26 + /**
27 + *
28 + */
29 + protected function _parse_answers() {
30 + foreach ( array( '_question_answers', 'question_answers' ) as $k ) {
31 + $answers = learn_press_get_user_item_meta( $this->get_user_item_id(), $k, true );
32 +
33 + if ( $answers ) {
34 + $this->_answers = $answers;
35 + break;
36 + }
37 + }
38 + }
39 +
40 + /**
41 + * Add user answer to DB.
42 + *
43 + * @param int|array $id
44 + * @param mixed $values
45 + *
46 + * @return array|bool|LP_Quiz_Results|mixed
47 + * @throws Exception
48 + * @editor tungnx
49 + * @modify 4.1.4.1 - comment - not use
50 + */
51 + /*
52 + public function add_question_answer( $id, $values = null ) {
53 + $results = $this->get_results( '' );
54 +
55 + if ( ! $results ) {
56 + return false;
57 + }
58 +
59 + $questions = $results->get( 'questions', array() );
60 +
61 + if ( is_numeric( $id ) ) {
62 + $values = array( $id => $values );
63 + } else {
64 + $values = (array) $id;
65 + }
66 +
67 + foreach ( $values as $id => $answer ) {
68 + if ( ! $this->has_checked_question( $id ) ) {
69 + if ( ! empty( $questions[ $id ] ) ) {
70 + $questions[ $id ]['answered'] = $answer;
71 + } else {
72 + $questions[ $id ] = array( 'answered' => $answer );
73 + }
74 + }
75 + }
76 +
77 + $results['questions'] = $questions;
78 +
79 + //LP_User_Items_Result_DB::instance()->update( $this->get_user_item_id(), wp_json_encode( $results->get() ) );
80 +
81 + $this->calculate_results();
82 +
83 + $cache_key = sprintf( 'quiz-%d-%d-%d', $this->get_user_id(), $this->get_course_id(), $this->get_item_id() );
84 +
85 + LP_Object_Cache::set( $cache_key, false, 'learn-press/quiz-result' );
86 +
87 + return $this->get_results( '' );
88 + }*/
89 +
90 + public function get_question_answer( $id ) {
91 + $results = $this->get_results( '' );
92 +
93 + if ( ! $results ) {
94 + return false;
95 + }
96 +
97 + $questions = $results->get( 'questions', false );
98 +
99 + if ( $questions && is_array( $questions[ $id ] ) ) {
100 + return $questions[ $id ]['answered'];
101 + }
102 +
103 + return false;
104 + }
105 +
106 + /**
107 + * Update data to database
108 + *
109 + * @param bool $force
110 + * @param bool $wp_error
111 + *
112 + * @return bool|mixed
113 + */
114 + /*
115 + public function update( $force = false, $wp_error = false ) {
116 + $return = parent::update( $force, $wp_error );
117 + $this->calculate_results();
118 +
119 + return $return;
120 + }*/
121 +
122 + /**
123 + * Get list of data to update to database
124 + *
125 + * @since 3.1.0
126 + *
127 + * @return array
128 + */
129 + public function get_mysql_data() {
130 + $columns = parent::get_mysql_data();
131 +
132 + return apply_filters( 'learn-press/update-user-item-quiz-data', $columns, $this->get_item_id(), $this->get_course_id(), $this->get_user_id() );
133 + }
134 +
135 + /**
136 + * @return bool|LP_Quiz
137 + */
138 + public function get_quiz() {
139 + return learn_press_get_quiz( $this->get_item_id() );
140 + }
141 +
142 + public function get_status_label( $status = '' ) {
143 + $statuses = array(
144 + 'started' => __( 'In Progress', 'learnpress' ),
145 + 'in-progress' => __( 'In Progress', 'learnpress' ),
146 + 'completed' => __( 'Completed', 'learnpress' ),
147 + 'passed' => __( 'Passed', 'learnpress' ),
148 + 'failed' => __( 'Failed', 'learnpress' ),
149 + );
150 +
151 + if ( ! $status ) {
152 + $status = $this->get_status();
153 + }
154 +
155 + return ! empty( $statuses[ $status ] ) ? $statuses[ $status ] : __( 'Not Started', 'learnpress' );
156 + }
157 +
158 + /**
159 + * Get ID of the course that this item assigned to.
160 + *
161 + * @return array|mixed
162 + */
163 + public function get_course_id() {
164 + return $this->get_data( 'ref_id' );
165 + }
166 +
167 + /**
168 + * Get result
169 + *
170 + * @param string $prop
171 + *
172 + * @return array|bool|mixed
173 + */
174 + public function get_result( $prop = '' ) {
175 + $result = $this->calculate_quiz_result();
176 +
177 + // Fix temporary for case call 'grade' - addons called
178 + if ( 'grade' === $prop ) {
179 + if ( $result['pass'] ) {
180 + $result['grade'] = 'passed';
181 + } else {
182 + $result['grade'] = 'failed';
183 + }
184 + }
185 +
186 + return $prop && $result && array_key_exists( $prop, $result ) ? $result[ $prop ] : $result;
187 + }
188 +
189 + /**
190 + * Calculate result of quiz.
191 + *
192 + * @param string $prop
193 + * @param bool $force - Optional. Force to refresh cache.
194 + *
195 + * Clear cache on
196 + * @see LP_REST_Users_Controller::start_quiz() | retake quiz
197 + *
198 + * @return LP_Quiz_Results|bool|mixed
199 + * @throws Exception
200 + * @editor tungnx
201 + * @modify 4.1.3
202 + * @version 4.0.1
203 + */
204 + public function get_results( string $prop = 'result', bool $force = false ) {
205 + if ( ! $this->get_status() ) {
206 + return false;
207 + }
208 +
209 + $lp_quiz_cache = LP_Quiz_Cache::instance();
210 +
211 + $key_cache = $this->get_results_cache_key();
212 + $result = $lp_quiz_cache->get_cache( $key_cache );
213 +
214 + if ( false === $result || $force ) {
215 + // $result = $this->_get_results();
216 +
217 + // if ( false === $result ) {
218 + $result = $this->calculate_results();
219 + // }
220 +
221 + $lp_quiz_cache->set_cache( $key_cache, $result );
222 + }
223 +
224 + $result['user_item_id'] = $this->get_user_item_id();
225 + $result['interval'] = array( $this->get_start_time(), $this->get_end_time() );
226 + $result['graduation'] = $this->get_graduation();
227 + $result['graduationText'] = $this->get_graduation_text();
228 +
229 + return $prop ? $result[ $prop ] : new LP_Quiz_Results( $result );
230 + }
231 +
232 + /**
233 + * Key of the cache filled by @see LP_User_Item_Quiz::get_results.
234 + *
235 + * @return string
236 + */
237 + public function get_results_cache_key(): string {
238 + return sprintf( '%d/user/%d/course/%d', $this->get_item_id(), $this->get_user_id(), $this->get_course_id() );
239 + }
240 +
241 + /**
242 + * Drop the cached result of this quiz for this user.
243 + *
244 + * get_results() caches what it calculates, and that cache outlives the request wherever a
245 + * persistent object cache is installed. Anything that rewrites the stored result must clear
246 + * it, or the next read serves the result from before the write - a quiz submitted after the
247 + * in-progress result was cached comes back with every question still unanswered.
248 + *
249 + * @return void
250 + */
251 + public function clear_results_cache() {
252 + LP_Quiz_Cache::instance()->clear( $this->get_results_cache_key() );
253 + }
254 +
255 + /**
256 + * Get user quiz graduation text for displaying purpose. [Passed, Failed, null]
257 + *
258 + * @since 4.0.0
259 + *
260 + * @return mixed
261 + */
262 + public function get_graduation_text() {
263 + $graduation = $this->get_graduation();
264 +
265 + return apply_filters( 'learn-press/user-quiz-graduation-text', learn_press_get_graduation_text( $graduation ) );
266 + }
267 +
268 + /**
269 + * Get Timestamp remaining when user doing quiz
270 + *
271 + * @author tungnx
272 + * @version 1.0.1
273 + * @sicne 4.1.4.1
274 + * @return int
275 + */
276 + public function get_timestamp_remaining(): int {
277 + $timestamp_remaining = - 1;
278 + $quiz = false;
279 + $user_quiz = false;
280 +
281 + try {
282 + $quiz = learn_press_get_quiz( $this->get_item_id() );
283 + if ( ! $quiz || LP_ITEM_STARTED != $this->get_status() ) {
284 + return $timestamp_remaining;
285 + }
286 +
287 + $parent_id = $this->get_parent_id();
288 + $duration = $quiz->get_duration()->get() . ' second';
289 +
290 + $filter = new LP_User_Items_Filter();
291 + $filter->parent_id = $parent_id;
292 + $filter->item_id = $this->get_item_id();
293 + $filter->user_id = get_current_user_id();
294 + $user_quiz = LP_User_Items_DB::getInstance()->get_user_course_item( $filter, true );
295 + $course_start_time = $user_quiz->start_time;
296 + $timestamp_expire = strtotime( $course_start_time . ' +' . $duration );
297 + $timestamp_current = time();
298 + $timestamp_remaining = $timestamp_expire - $timestamp_current;
299 +
300 + if ( $timestamp_remaining < 0 ) {
301 + $timestamp_remaining = 0;
302 + }
303 + } catch ( Throwable $e ) {
304 +
305 + }
306 +
307 + return apply_filters( 'learn-press/user-course-quiz/timestamp_remaining', $timestamp_remaining, $user_quiz, $quiz );
308 + }
309 +
310 + /**
311 + * Get all attempts of a quiz.
312 + *
313 + * @param string $args
314 + *
315 + * @return array
316 + */
317 + public function get_attempts( $limit = 3 ) {
318 + $limit = $limit ?? 3;
319 +
320 + $limit = absint( apply_filters( 'lp/quiz/get-attempts/limit', $limit ) );
321 +
322 + $results = LP_User_Items_Result_DB::instance()->get_results( $this->get_user_item_id(), $limit, true );
323 + $output = array();
324 +
325 + if ( ! empty( $results ) ) {
326 + foreach ( $results as $result ) {
327 + if ( $result && is_string( $result ) ) {
328 + $result = json_decode( $result );
329 +
330 + unset( $result->questions );
331 +
332 + $output[] = $result;
333 + }
334 + }
335 + }
336 +
337 + return $output;
338 + }
339 +
340 + /**
341 + * Get question ids user has started inside quiz.
342 + *
343 + * @since 4.0.0
344 + *
345 + * @return bool|array
346 + */
347 + public function get_questions() {
348 + $quiz = learn_press_get_quiz( $this->get_item_id() );
349 + $ids = $quiz->get_question_ids();
350 +
351 + return apply_filters( 'learn-press/user-item-quiz-questions', $ids, $this->get_user_id(), $this );
352 + }
353 +
354 + /**
355 + * Calculate results of quiz.
356 + *
357 + * @return array
358 + * @throws Exception
359 + * @version 4.0.0
360 + */
361 + public function calculate_results(): array {
362 + $quiz = learn_press_get_quiz( $this->get_item_id() );
363 + $last_results = LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
364 +
365 + if ( ! $last_results ) {
366 + $last_results = array();
367 + }
368 +
369 + $questions = $last_results['questions'] ?? array_fill_keys( $quiz->get_question_ids(), array() );
370 +
371 + $result = array(
372 + 'questions' => array(),
373 + 'mark' => $quiz->get_mark(),
374 + 'user_mark' => 0,
375 + 'question_count' => 0,
376 + 'question_empty' => 0,
377 + 'question_answered' => 0,
378 + 'question_wrong' => 0,
379 + 'question_correct' => 0,
380 + 'status' => $this->get_status(),
381 + 'result' => 0,
382 + 'time_spend' => $this->get_time_interval( 'display' ),
383 + 'passing_grade' => $quiz->get_passing_grade(),
384 + );
385 +
386 + if ( $questions ) {
387 + foreach ( $questions as $question_id => $last_checked ) {
388 + $question = LP_Question::get_question( $question_id );
389 + if ( ! $question ) {
390 + continue;
391 + }
392 +
393 + $answered = array_key_exists( 'answered', $last_checked ) ? $last_checked['answered'] : '';
394 + $check = apply_filters( 'learn-press/quiz/check-question-result', $question->check( $answered ), $question_id, $this );
395 + $check['answered'] = $answered;
396 +
397 + if ( $check['answered'] && $check['correct'] ) {
398 + $result['question_correct'] ++;
399 + $result['user_mark'] += array_key_exists( 'mark', $check ) ? floatval( $check['mark'] ) : $question->get_mark();
400 + } else {
401 + $negative_marking = apply_filters( 'learn-press/get-negative-marking-value', floatval( $question->get_mark() ), $question_id, $quiz->get_id() );
402 +
403 + // If answered is empty consider user has skipped question
404 + if ( ! $check['answered'] ) {
405 + if ( $quiz->get_negative_marking() && $quiz->get_minus_skip_questions() ) {
406 + $result['user_mark'] -= $negative_marking;
407 + }
408 + $result['question_empty'] ++;
409 + } else {
410 + if ( $quiz->get_negative_marking() ) {
411 + $result['user_mark'] -= $negative_marking;
412 + }
413 + $result['question_wrong'] ++;
414 + }
415 + }
416 +
417 + $result['questions'][ $question_id ] = apply_filters( 'learn-press/question-results-data', $last_checked ? array_merge( $last_checked, $check ) : $check, $question_id, $quiz->get_id() );
418 +
419 + if ( $check['answered'] ) {
420 + $result['question_answered'] ++;
421 + }
422 + }
423 +
424 + $result['user_mark'] = ( $result['user_mark'] >= 0 ) ? $result['user_mark'] : 0;
425 +
426 + $percent = $result['mark'] ? ( $result['user_mark'] / $result['mark'] ) * 100 : 0;
427 + $result['result'] = $percent;
428 + $grade = '';
429 +
430 + if ( $this->get_status() === 'completed' ) {
431 + $grade = $percent >= $this->get_quiz()->get_data( 'passing_grade' ) ? 'passed' : 'failed';
432 + $this->_set_data( 'graduation', $grade );
433 + }
434 +
435 + $result['question_count'] = count( $questions );
436 +
437 + if ( $grade ) {
438 + learn_press_update_user_item_field(
439 + array(
440 + 'graduation' => $grade,
441 + ),
442 + array(
443 + 'user_item_id' => $this->get_user_item_id(),
444 + )
445 + );
446 + }
447 + }
448 +
449 + return $result;
450 + }
451 +
452 + /**
453 + * Calculate result of quiz.
454 + *
455 + * @param array $answered [question_id => answered, 'instant_check' => 0]
456 + *
457 + * @return array
458 + * @version 1.0.1
459 + * @author tungnx
460 + * @since 4.1.4.1
461 + */
462 + public function calculate_quiz_result( array $answered = array() ): array {
463 + $result = array(
464 + 'questions' => array(),
465 + 'mark' => 0,
466 + 'user_mark' => 0,
467 + 'minus_point' => 0,
468 + 'question_count' => 0,
469 + 'question_empty' => 0,
470 + 'question_answered' => 0,
471 + 'question_wrong' => 0,
472 + 'question_correct' => 0,
473 + 'status' => '',
474 + 'result' => 0,
475 + 'time_spend' => '',
476 + 'passing_grade' => '',
477 + 'pass' => 0,
478 + );
479 +
480 + try {
481 + if ( LP_ITEM_COMPLETED === $this->get_status() ) {
482 + $result_tmp = LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
483 +
484 + if ( $result_tmp ) {
485 + if ( isset( $result_tmp['user_mark'] ) && $result_tmp['user_mark'] < 0 ) {
486 + $result_tmp['user_mark'] = 0;
487 + }
488 +
489 + $result = $result_tmp;
490 + }
491 +
492 + return $result;
493 + }
494 +
495 + $quiz = learn_press_get_quiz( $this->get_item_id() );
496 +
497 + if ( ! $quiz ) {
498 + throw new Exception();
499 + }
500 +
501 + $question_ids = $quiz->get_question_ids();
502 + $result['mark'] = $quiz->get_mark();
503 + $result['question_count'] = $quiz->count_questions();
504 + $result['time_spend'] = $this->get_time_interval( 'display' );
505 + $result['passing_grade'] = $quiz->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 ( $quiz->get_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 ( $quiz->get_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 + $can_review_quiz = get_post_meta( $quiz->get_id(), '_lp_review', true ) === 'yes';
548 + if ( $can_review_quiz && ! array_key_exists( 'instant_check', $answered ) ) {
549 +
550 + $result['questions'][ $question_id ]['explanation'] = $question->get_explanation();
551 + $result['questions'][ $question_id ]['options'] = learn_press_get_question_options_for_js(
552 + $question,
553 + array(
554 + 'include_is_true' => in_array( $question_id, $checked_questions ) || get_post_meta( $quiz->get_id(), '_lp_show_correct_review', true ) === 'yes',
555 + 'answer' => $answered[ $question_id ] ?? '',
556 + )
557 + );
558 + }
559 + }
560 +
561 + if ( $result['user_mark'] < 0 ) {
562 + $result['user_mark'] = 0;
563 + }
564 +
565 + if ( $result['user_mark'] > 0 && $result['mark'] > 0 ) {
566 + $result['result'] = round( $result['user_mark'] * 100 / $result['mark'], 2, PHP_ROUND_HALF_DOWN );
567 + }
568 +
569 + $passing_grade = $quiz->get_data( 'passing_grade', 0 );
570 + if ( $result['result'] >= $passing_grade ) {
571 + $result['pass'] = 1;
572 + } else {
573 + $result['pass'] = 0;
574 + }
575 + } catch ( Throwable $e ) {
576 +
577 + }
578 +
579 + return $result;
580 + }
581 +
582 + public function get_option_answer() {
583 +
584 + }
585 +
586 + protected function _get_results() {
587 + return LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
588 + }
589 +
590 + public function get_percent_result( $decimal = 2 ) {
591 + return apply_filters( 'learn-press/user/quiz-percent-result', sprintf( '%s%%', round( $this->get_result( 'result' ), $decimal ) ), $this->get_user_id(), $this->get_item_id() );
592 + }
593 +
594 + public function get_time_interval( $context = '' ) {
595 + $interval = parent::get_time_interval();
596 +
597 + if ( $context == 'display' ) {
598 + $quiz = $this->get_quiz();
599 +
600 + if ( $quiz && $interval && $quiz->get_duration() ) {
601 + $interval = new LP_Duration( $interval );
602 + $interval = $interval->to_timer();
603 + } else {
604 + $interval = '--:--';
605 + }
606 + }
607 +
608 + return $interval;
609 + }
610 +
611 + /**
612 + * Return TRUE if user has pressed SKIP on this question
613 + *
614 + * @param int $question_id
615 + *
616 + * @return bool
617 + */
618 + public function is_skipped( $question_id ) {
619 + return $this->get_question_answer( $question_id ) === '__SKIPPED__';
620 + }
621 +
622 + public function is_answered( $question_id ) {
623 + $result = $this->get_results();
624 +
625 + if ( ! empty( $result['questions'][ $question_id ] ) ) {
626 + return $result['questions'][ $question_id ]['answered'];
627 + }
628 +
629 + return false;
630 + }
631 +
632 + public function is_answered_true( $question_id ) {
633 + $result = $this->get_results( false );
634 +
635 + if ( ! empty( $result['questions'][ $question_id ] ) ) {
636 + return $result['questions'][ $question_id ]['correct'];
637 + }
638 +
639 + return false;
640 + }
641 +
642 + /**
643 + * Get questions user has answered.
644 + *
645 + * @param bool $percent - Optional. TRUE will return by percentage with total questions.
646 + *
647 + * @return float|int|mixed
648 + */
649 + public function get_questions_answered( $percent = false ) {
650 + $result = $this->get_results( '' );
651 +
652 + if ( $percent ) {
653 + if ( $result['question_count'] ) {
654 + $return = 0;
655 + } else {
656 + $return = $result['question_answered'] ? ( $result['question_answered'] / $result['question_count'] ) * 100 : 0;
657 + }
658 + } else {
659 + $return = $result['question_answered'];
660 + }
661 +
662 + return $return;
663 + }
664 +
665 + /**
666 + * Get total mark user achieved.
667 + *
668 + * @param bool $percent - Optional. TRUE will return by percentage with total mark.
669 + *
670 + * @return float|int|mixed
671 + */
672 + public function get_mark( $percent = false ) {
673 + $result = $this->get_results();
674 +
675 + if ( $percent ) {
676 + $return = $result['mark'] ? ( $result['user_mark'] / $result['mark'] ) * 100 : 0;
677 + } else {
678 + $return = $result['user_mark'];
679 + }
680 +
681 + return $return;
682 + }
683 +
684 + /**
685 + * @deprecated 4.2.0
686 + */
687 + public function get_total_questions() {
688 + _deprecated_function( __METHOD__, '4.2.0' );
689 + $quiz = learn_press_get_quiz( $this->get_item_id() );
690 + $questions = $quiz->get_question_ids();
691 +
692 + return sizeof( $questions );
693 + }
694 +
695 + public function get_quiz_mark() {
696 + $result = $this->get_results();
697 +
698 + return $result['mark'];
699 + }
700 +
701 + /**
702 + * Return time remaining.
703 + *
704 + * @param string $return - Optional.
705 + *
706 + * @return LP_Duration
707 + * @deprecated 4.1.7.3
708 + */
709 + public function get_time_remaining( $return = 'object' ) {
710 + _deprecated_function( __METHOD__, '4.1.7.3' );
711 + /*$time = parent::get_time_remaining( $return );
712 +
713 + return apply_filters( 'learn-press/quiz/time-remaining', $time, $this->get_item_id(), $this->get_course_id(), $this->get_user_id() );*/
714 + }
715 +
716 + /**
717 + * Get all questions user has already used "Check"
718 + *
719 + * @return array
720 + */
721 + public function get_checked_questions(): array {
722 + $value = $this->get_meta( '_lp_question_checked', true );
723 +
724 + if ( $value ) {
725 + $value = (array) $value;
726 + } else {
727 + $value = array();
728 + }
729 +
730 + return $value;
731 + }
732 +
733 + public function add_checked_question( $id ) {
734 + settype( $id, 'array' );
735 + $checked = $this->get_checked_questions();
736 + $checked = array_merge( $checked, $id );
737 +
738 + $this->update_meta( '_lp_question_checked', $checked );
739 +
740 + return $checked;
741 + }
742 +
743 + /**
744 + * Return true if user has already checked a question.
745 + *
746 + * @param int $question_id
747 + *
748 + * @return bool
749 + */
750 + public function has_checked_question( $question_id ) {
751 + return in_array( $question_id, $this->get_checked_questions() );
752 + }
753 +
754 + /**
755 + * Instant check question
756 + *
757 + * @param int $question_id
758 + * @param mixed $answered
759 + *
760 + * @return array
761 + * @throws Exception
762 + */
763 + public function instant_check_question( int $question_id, $answered = null ): array {
764 + $question = learn_press_get_question( $question_id );
765 + if ( ! $question ) {
766 + throw new Exception( __( 'The question is invalid!', 'learnpress' ) );
767 + }
768 +
769 + $can_check = $this->can_check_answer( $question_id );
770 + if ( ! $can_check ) {
771 + throw new Exception( __( 'Cannot check the answer to the question.', 'learnpress' ) );
772 + }
773 +
774 + $answered_check = array(
775 + 'instant_check' => 1,
776 + $question_id => $answered,
777 + );
778 +
779 + // For case save result when check instant answer
780 + $result_instant_check = LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
781 + if ( $result_instant_check ) {
782 + foreach ( $result_instant_check['questions'] as $question_answer_id => $question_answer ) {
783 + if ( ! empty( $question_answer['answered'] ) ) {
784 + $answered_check[ $question_answer_id ] = $question_answer['answered'];
785 + }
786 + }
787 + }
788 +
789 + $result_answer = $this->calculate_quiz_result( $answered_check );
790 +
791 + LP_User_Items_Result_DB::instance()->update( $this->get_user_item_id(), json_encode( $result_answer ) );
792 + $this->clear_results_cache();
793 + $this->add_checked_question( $question_id );
794 +
795 + $checked['answered'] = $answered;
796 + $checked['mark'] = $result_answer['questions'][ $question_id ]['mark'];
797 + $checked['correct'] = $result_answer['questions'][ $question_id ]['correct'];
798 +
799 + return $checked;
800 + }
801 +
802 + /**
803 + * @param int $question_id
804 + *
805 + * @return int
806 + * @deprecated 4.2.5
807 + */
808 + public function hint( $question_id ) {
809 + _deprecated_function( __METHOD__, '4.2.5' );
810 + return false;
811 + $remain = $this->can_hint_answer();
812 +
813 + if ( $remain ) {
814 + if ( ! $this->has_hinted_question( $question_id ) ) {
815 + $checked = $this->get_hint_questions();
816 + $checked[] = $question_id;
817 + $this->set_meta( '_lp_question_hint', $checked );
818 + }
819 +
820 + $count = $this->get_count_hint();
821 + $this->update_meta();
822 + $remain --;
823 + } else {
824 + return false;
825 + }
826 +
827 + return $remain;
828 + }
829 +
830 + /**
831 + * Return true if user has already checked a question.
832 + *
833 + * @param int $question_id
834 + *
835 + * @return bool
836 + */
837 + public function has_hinted_question( $question_id ) {
838 + return in_array( $question_id, $this->get_hint_questions() );
839 + }
840 +
841 + public function get_check_answer_count() {
842 + return count( $this->get_checked_questions() );
843 + }
844 +
845 + /**
846 + * @param int $question_id - Added since 3.3.0
847 + *
848 + * @return bool
849 + */
850 + public function can_check_answer( int $question_id = 0 ) : bool {
851 + $can = false;
852 + $quiz = learn_press_get_quiz( $this->get_item_id() );
853 +
854 + if ( ! $quiz ) {
855 + return $can;
856 + }
857 +
858 + if ( $quiz->get_instant_check() && $this->get_status() === 'started' ) {
859 + $can = ! $this->has_checked_question( $question_id );
860 + }
861 +
862 + return apply_filters( 'learn-press/can-instant-check-question', $can, $question_id, $this->get_item_id(), $this->get_course_id() );
863 + }
864 +
865 + /**
866 + * Get number retaken count.
867 + *
868 + * @return integer
869 + */
870 + public function get_retaken_count(): int {
871 + return absint( learn_press_get_user_item_meta( $this->get_user_item_id(), '_lp_retaken_count' ) );
872 + }
873 +
874 + /**
875 + * Update learnpress_user_itemmeta retaken
876 + *
877 + * @return void
878 + */
879 + public function update_retake_count() {
880 + $count = $this->get_retaken_count();
881 + $count ++;
882 +
883 + return $this->update_meta( '_lp_retaken_count', $count );
884 + }
885 +
886 + /**
887 + * Get all questions user has already used "Check"
888 + *
889 + * @return array
890 + * @deprecated 4.2.5
891 + */
892 + public function get_hint_questions() {
893 + return [];
894 + _deprecated_function( __METHOD__, '4.2.5' );
895 + $value = $this->get_meta( '_lp_question_hint', true );
896 +
897 + if ( $value ) {
898 + $value = (array) $value;
899 + } else {
900 + $value = array();
901 + }
902 +
903 + return $value;
904 + }
905 +
906 + /**
907 + * @deprecated 4.2.5
908 + */
909 + public function get_count_hint() {
910 + _deprecated_function( __METHOD__, '4.2.5' );
911 + return 0;
912 + return count( $this->get_hint_questions() );
913 + }
914 +
915 + /**
916 + * Return true if check answer is enabled.
917 + *
918 + * @return bool
919 + */
920 + public function can_hint_answer() {
921 + return apply_filters( 'learn-press/user-quiz/can-hint-answer', true, $this->get_id(), $this->get_course_id() );
922 + }
923 +
924 + /**
925 + * @return bool
926 + */
927 + public function is_review_questions(): bool {
928 + return LP_Global::quiz_question() && ( $this->get_status() === 'completed' );
929 + }
930 +}