HEX
Server: LiteSpeed
System: Linux server.zepintelhosting.com 4.18.0 #1 SMP Mon Sep 30 15:36:27 MSK 2024 x86_64
User: enamadmin (1026)
PHP: 8.2.30
Disabled: exec,system,passthru,shell_exec,proc_open,popen,apache_child_terminate
Upload Files
File: /home/enamadmin/public_html/wp-content/plugins/learnpress/inc/Models/UserItems/UserQuizModel.php
<?php

/**
 * Class UserItemModel
 *
 * @package LearnPress/Classes
 * @version 1.0.0
 * @since 4.2.5
 */

namespace LearnPress\Models\UserItems;

use Exception;
use LearnPress\Models\UserItemMeta\UserItemMetaModel;
use LearnPress\Models\UserItemMeta\UserQuizMetaModel;
use LP_Course;
use LP_Datetime;
use LP_Question;
use LP_Quiz;
use LP_Quiz_CURD;
use LP_User;
use LP_User_Items_Result_DB;
use Throwable;
use WP_Error;

class UserQuizModel extends UserItemModel {
	/**
	 * Item type Course
	 *
	 * @var string Item type
	 */
	public $item_type = LP_QUIZ_CPT;
	/**
	 * Ref type Order
	 *
	 * @var string
	 */
	public $ref_type = LP_COURSE_CPT;
	/**
	 * @var LP_User $user not column in DB
	 */
	public $user;
	/**
	 * @var LP_Course $course not column in DB
	 */
	public $course;
	/**
	 * @var LP_Quiz $quiz not column in DB
	 */
	public $quiz;
	/**
	 * @var UserCourseModel $user_course not column in DB
	 */
	public $user_course;

	public function __construct( $data = null ) {
		parent::__construct( $data );

		if ( $data ) {
			$this->get_quiz_model();
		}
	}

	/**
	 * Get quiz model
	 *
	 * @return bool|LP_Quiz
	 */
	public function get_quiz_model() {
		if ( empty( $this->quiz ) ) {
			$this->quiz = learn_press_get_quiz( $this->item_id );
		}

		return $this->quiz;
	}

	/**
	 * Get status
	 *
	 * @return string
	 */
	public function get_status(): string {
		return $this->status;
	}

	/**
	 * Get all question's ids of the quiz.
	 *
	 * @param string $context
	 *
	 * @move from LP_Quiz
	 * @return int[]
	 * @editor tungnx
	 * @version 1.0.1
	 * @since 3.2.0
	 */
	public function get_question_ids( string $context = 'display' ): array {
		$curd         = new LP_Quiz_CURD();
		$question_ids = $curd->read_question_ids( $this->item_id, $context );
		$question_ids = apply_filters( 'learn-press/quiz/get-question-ids', $question_ids, $this->item_id, $this->ref_id, $context );
		if ( ! is_array( $question_ids ) ) {
			$question_ids = array();
		}

		return $question_ids;
	}

	/**
	 * Get Timestamp remaining when user doing quiz
	 *
	 * @return int
	 * @throws Exception
	 * @version 1.0.1
	 * @sicne 4.1.4.1
	 */
	public function get_timestamp_remaining(): int {
		$timestamp_remaining = 0;
		if ( empty( $this->get_user_item_id() ) || $this->status !== LP_ITEM_STARTED ) {
			return $timestamp_remaining;
		}

		$quiz = $this->get_quiz_model();
		if ( empty( $quiz ) ) {
			return $timestamp_remaining;
		}

		$duration            = $quiz->get_duration()->get() . ' second';
		$course_start_time   = $this->start_time;
		$timestamp_expire    = strtotime( $course_start_time . ' +' . $duration );
		$timestamp_current   = time();
		$timestamp_remaining = $timestamp_expire - $timestamp_current;

		if ( $timestamp_remaining < 0 ) {
			$timestamp_remaining = 0;
		}

		return apply_filters( 'learn-press/user-course-quiz/timestamp_remaining', $timestamp_remaining, $this, $quiz );
	}

	/**
	 * Start quiz.
	 *
	 * @throws Exception
	 * @since 4.2.5
	 * @version 1.0.0
	 */
	public function start_quiz() {
		$can_start = $this->check_can_start();
		if ( is_wp_error( $can_start ) ) {
			/**
			 * @var WP_Error $can_start
			 */
			throw new Exception( $can_start->get_error_message() );
		}

		$this->status     = LP_ITEM_STARTED;
		$this->graduation = LP_COURSE_GRADUATION_IN_PROGRESS;
		$this->save();

		// Hook old - random quiz using.
		do_action( 'learn-press/user/quiz-started', $this->item_id, $this->ref_id, $this->user_id );
		// Hook new
		do_action( 'learn-press/user/quiz/started', $this );
	}

	/**
	 * Retake quiz.
	 *
	 * @throws Exception
	 */
	public function retake() {
		$can_retake = $this->check_can_retake();
		if ( is_wp_error( $can_retake ) ) {
			/**
			 * @var WP_Error $can_retake
			 */
			throw new Exception( $can_retake->get_error_message() );
		}

		// Update retaken count.
		$user_quiz_retaken = $this->get_meta_model_from_key( UserQuizMetaModel::KEY_RETAKEN_COUNT );
		if ( $user_quiz_retaken instanceof UserItemMetaModel ) {
			$number_retaken = absint( $user_quiz_retaken->meta_value );
			++$number_retaken;
			$user_quiz_retaken->meta_value = $number_retaken;
			$user_quiz_retaken->save();
		} else {
			$user_quiz_retaken_new                          = new UserQuizMetaModel();
			$user_quiz_retaken_new->learnpress_user_item_id = $this->get_user_item_id();
			$user_quiz_retaken_new->meta_key                = UserQuizMetaModel::KEY_RETAKEN_COUNT;
			$user_quiz_retaken_new->meta_value              = 1;
			$user_quiz_retaken_new->save();
		}

		//Todo: rewrite by object.
		//Create new result in table learnpress_user_item_results.
		LP_User_Items_Result_DB::instance()->insert( $this->get_user_item_id() );
		// Remove user_item_meta.
		learn_press_delete_user_item_meta( $this->get_user_item_id(), '_lp_question_checked' );

		$this->status     = LP_ITEM_STARTED;
		$this->start_time = gmdate( LP_Datetime::$format, time() );
		$this->end_time   = null;
		$this->graduation = LP_COURSE_GRADUATION_IN_PROGRESS;
		$this->save();

		// Hook old - random quiz using.
		do_action( 'learn-press/user/quiz-retried', $this->item_id, $this->ref_id, $this->user_id );
		// Hook new
		do_action( 'learn-press/user/quiz/retried', $this );
	}

	/**
	 * Check user can start quiz.
	 * If user can start quiz, return true, else return WP_Error.
	 * Set user, quiz, course, user_course and parent_id for this object.
	 *
	 * @throws Exception
	 * return bool|WP_Error
	 */
	public function check_can_start() {
		$can_start = true;

		$this->user = $this->get_user_model();
		if ( ! $this->user instanceof LP_User ) {
			$can_start = new WP_Error( 'user_invalid', __( 'User is invalid.', 'learnpress' ) );
		}

		$this->quiz = $this->get_quiz_model();
		if ( empty( $this->quiz ) ) {
			$can_start = new WP_Error( 'quiz_invalid', __( 'Quiz is invalid.', 'learnpress' ) );
		}

		$this->course = learn_press_get_course( $this->ref_id );
		if ( empty( $this->course ) ) {
			$can_start = new WP_Error( 'course_invalid', __( 'Course is invalid.', 'learnpress' ) );
		}

		// Check user, course of quiz is enrolled.
		$user_course       = $this->user->get_course_attend( $this->ref_id );
		$this->user_course = $user_course;
		if ( ! $user_course instanceof UserCourseModel
			|| $user_course->graduation !== LP_COURSE_GRADUATION_IN_PROGRESS ) {
			$can_start = new WP_Error( 'not_errol_course', __( 'Please enroll in the course before starting the quiz.', 'learnpress' ) );
		} elseif ( $user_course->status === LP_COURSE_FINISHED ) {
			$can_start = new WP_Error( 'finished_course', __( 'You have already finished the course of this quiz.', 'learnpress' ) );
		} else {
			// Set Parent id for user quiz to save DB.
			$this->parent_id = $this->user_course->get_user_item_id();

			// Check if user has already started or completed quiz
			$user_quiz = $this->user_course->get_item_attend( $this->item_id, $this->item_type );
			if ( $user_quiz instanceof UserQuizModel ) {
				$can_start = new WP_Error( 'started_quiz', __( 'You have already started or completed the quiz.', 'learnpress' ) );
			}
		}

		// Hook can start quiz
		return apply_filters(
			'learn-press/can-start-quiz',
			$can_start,
			$user_course,
			$this
		);
	}

	/**
	 * Check can retake quiz.
	 *
	 * @throws Exception
	 */
	public function check_can_retake() {
		$can_retake = true;

		$this->user = $this->get_user_model();
		if ( ! $this->user instanceof LP_User ) {
			$can_retake = new WP_Error( 'user_invalid', __( 'User is invalid.', 'learnpress' ) );
		}

		$this->quiz = $this->get_quiz_model();
		if ( empty( $this->quiz ) ) {
			$can_retake = new WP_Error( 'quiz_invalid', __( 'Quiz is invalid.', 'learnpress' ) );
		}

		$this->course = learn_press_get_course( $this->ref_id );
		if ( empty( $this->course ) ) {
			$can_retake = new WP_Error( 'course_invalid', __( 'Course is invalid.', 'learnpress' ) );
		}

		// Check user, course of quiz is enrolled.
		$user_course       = $this->user->get_course_attend( $this->ref_id );
		$this->user_course = $user_course;
		if ( ! $user_course instanceof UserCourseModel
			|| $user_course->graduation !== LP_COURSE_GRADUATION_IN_PROGRESS ) {
			$can_retake = new WP_Error( 'not_errol_course', __( 'Please enroll in the course before starting the quiz.', 'learnpress' ) );
		} elseif ( $user_course->status === LP_COURSE_FINISHED ) {
			$can_retake = new WP_Error( 'finished_course', __( 'You have already finished the course of this quiz.', 'learnpress' ) );
		}

		// Check user quiz start and completed?.
		$user_quiz_exists = $this->user_course->get_item_attend( $this->item_id, $this->item_type );
		if ( ! $user_quiz_exists instanceof UserQuizModel ) {
			$can_retake = new WP_Error( 'not_started_quiz', __( 'You have not start the quiz.', 'learnpress' ) );
		} elseif ( $user_quiz_exists->status !== LP_ITEM_COMPLETED ) {
			$can_retake = new WP_Error( 'not_completed_quiz', __( 'You have not completed the quiz.', 'learnpress' ) );
		}

		// Check retaken count.
		$retake_config = get_post_meta( $this->item_id, '_lp_retake_count', true );
		if ( $retake_config !== '-1' ) {
			$number_retaken = absint( $this->get_meta_value_from_key( UserQuizMetaModel::KEY_RETAKEN_COUNT ) );
			if ( $number_retaken >= $retake_config ) {
				$can_retake = new WP_Error( 'exceed_retaken_count', __( 'You have exceeded the number of retakes.', 'learnpress' ) );
			}
		}

		// Hook can retake quiz
		return apply_filters(
			'learn-press/can-retake-quiz',
			$can_retake,
			$user_course,
			$this
		);
	}

	/**
	 * Get all attempts of a quiz.
	 *
	 * @move from LP_Quiz
	 *
	 * @return array
	 */
	public function get_attempts( $limit = 3 ) {
		$limit = $limit ?? 3;

		$limit = absint( apply_filters( 'lp/quiz/get-attempts/limit', $limit ) );

		$results = LP_User_Items_Result_DB::instance()->get_results( $this->get_user_item_id(), $limit, true );
		$output  = array();

		if ( ! empty( $results ) ) {
			foreach ( $results as $result ) {
				if ( $result && is_string( $result ) ) {
					$result = json_decode( $result );

					unset( $result->questions );

					$output[] = $result;
				}
			}
		}

		return $output;
	}

	/**
	 * Get number retaken count.
	 * @move from LP_Quiz
	 *
	 * @return integer
	 */
	public function get_retaken_count(): int {
		return absint( $this->get_meta_value_from_key( UserQuizMetaModel::KEY_RETAKEN_COUNT ) );
	}

	/**
	 * Get all questions user has already used "Check"
	 * @move from LP_Quiz
	 *
	 * @return array
	 */
	public function get_checked_questions(): array {
		$value_str = $this->get_meta_value_from_key( UserQuizMetaModel::KEY_QUESTION_CHECKED );
		$value     = maybe_unserialize( $value_str );

		if ( $value ) {
			$value = (array) $value;
		} else {
			$value = array();
		}

		return $value;
	}

	/**
	 * Get result when user completed quiz.
	 *
	 * @move from LP_Quiz
	 * @return array
	 */
	public function get_result(): array {
		$result = array(
			'questions'         => array(),
			'mark'              => 0,
			'user_mark'         => 0,
			'minus_point'       => 0,
			'question_count'    => 0,
			'question_empty'    => 0,
			'question_answered' => 0,
			'question_wrong'    => 0,
			'question_correct'  => 0,
			'status'            => '',
			'result'            => 0,
			'time_spend'        => '',
			'passing_grade'     => '',
			'pass'              => 0,
		);

		try {
			$result_tmp = LP_User_Items_Result_DB::instance()->get_result( $this->get_user_item_id() );
			if ( $result_tmp ) {
				if ( isset( $result_tmp['user_mark'] ) && $result_tmp['user_mark'] < 0 ) {
					$result_tmp['user_mark'] = 0;
				}

				$result = $result_tmp;
			}

			return $result;
		} catch ( Throwable $e ) {
			error_log( __METHOD__ . ': ' . $e->getMessage() );
		}

		return $result;
	}

	/**
	 * Calculate result of quiz.
	 * @move from LP_Quiz
	 * @param array $answered [question_id => answered, 'instant_check' => 0]
	 *
	 * @return array
	 * @author tungnx
	 * @since 4.1.4.1
	 * @version 1.0.1
	 */
	private function calculate_quiz_result( array $answered ): array {
		$result = array(
			'questions'         => array(),
			'mark'              => 0,
			'user_mark'         => 0,
			'minus_point'       => 0,
			'question_count'    => 0,
			'question_empty'    => 0,
			'question_answered' => 0,
			'question_wrong'    => 0,
			'question_correct'  => 0,
			'status'            => '',
			'result'            => 0,
			'time_spend'        => '',
			'passing_grade'     => '',
			'pass'              => 0,
		);

		$quiz = $this->quiz;
		if ( empty( $quiz ) ) {
			return $result;
		}

		$question_ids             = $quiz->get_question_ids();
		$result['mark']           = $quiz->get_mark();
		$result['question_count'] = $quiz->count_questions();
		$result['time_spend']     = $this->get_time_spend();
		$result['passing_grade']  = $quiz->get_passing_grade();
		$checked_questions        = $this->get_checked_questions();

		foreach ( $question_ids as $question_id ) {
			$question = LP_Question::get_question( $question_id );
			$point    = floatval( $question->get_mark() );

			$result['questions'][ $question_id ]             = array();
			$result['questions'][ $question_id ]['answered'] = $answered[ $question_id ] ?? '';

			if ( isset( $answered[ $question_id ] ) ) { // User's answer
				++$result['question_answered'];

				$check = $question->check( $answered[ $question_id ] );
				$point = apply_filters( 'learn-press/user/calculate-quiz-result/point', $point, $question, $check );
				if ( $check['correct'] ) {
					++$result['question_correct'];
					$result['user_mark'] += $point;

					$result['questions'][ $question_id ]['correct'] = true;
					$result['questions'][ $question_id ]['mark']    = $point;
				} else {
					if ( $quiz->get_negative_marking() ) {
						$result['user_mark']   -= $point;
						$result['minus_point'] += $point;
					}
					++$result['question_wrong'];

					$result['questions'][ $question_id ]['correct'] = false;
					$result['questions'][ $question_id ]['mark']    = 0;
				}
			} elseif ( ! array_key_exists( 'instant_check', $answered ) ) { // User skip question
				if ( $quiz->get_minus_skip_questions() ) {
					$result['user_mark']   -= $point;
					$result['minus_point'] += $point;
				}
				++$result['question_empty'];

				$result['questions'][ $question_id ]['correct'] = false;
				$result['questions'][ $question_id ]['mark']    = 0;
			}

			$can_review_quiz = get_post_meta( $quiz->get_id(), '_lp_review', true ) === 'yes';
			if ( $can_review_quiz && ! array_key_exists( 'instant_check', $answered ) ) {

				$result['questions'][ $question_id ]['explanation'] = $question->get_explanation();
				$result['questions'][ $question_id ]['options']     = learn_press_get_question_options_for_js(
					$question,
					array(
						'include_is_true' => in_array( $question_id, $checked_questions ) || get_post_meta( $quiz->get_id(), '_lp_show_correct_review', true ) === 'yes',
						'answer'          => $answered[ $question_id ] ?? '',
					)
				);
			}
		}

		if ( $result['user_mark'] < 0 ) {
			$result['user_mark'] = 0;
		}

		if ( $result['user_mark'] > 0 && $result['mark'] > 0 ) {
			$result['result'] = round( $result['user_mark'] * 100 / $result['mark'], 2, PHP_ROUND_HALF_DOWN );
		}

		$passing_grade = $quiz->get_data( 'passing_grade', 0 );
		if ( $result['result'] >= $passing_grade ) {
			$result['pass'] = 1;
		} else {
			$result['pass'] = 0;
		}

		return $result;
	}

	/**
	 * Get string time spend.
	 *
	 * @return string
	 */
	public function get_time_spend(): string {
		$interval = $this->get_total_timestamp_completed();
		if ( empty( $interval ) ) {
			return '--:--';
		}

		$duration = new LP_Datetime( $interval );
		return $duration->format( 'H:i:s' );
	}
}