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

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

625 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Class 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 }
625