gocodebox/lifterlms

View on GitHub
includes/models/model.llms.quiz.php

Summary

Maintainability
A
1 hr
Test Coverage
F
59%
<?php
/**
 * LifterLMS Quiz Model
 *
 * @package LifterLMS/Models/Classes
 *
 * @since 3.3.0
 * @version 7.6.2
 */

defined( 'ABSPATH' ) || exit;

/**
 * LLMS_Quiz model class.
 *
 * @property $allowed_attempts (int) Number of times a student is allowed to take the quiz before being locked out of it.
 * @property $passing_percent (float) Grade required for a student to "pass" the quiz.
 * @property $random_questions (yesno) Whether or not to randomize the order of questions for each attempt.
 * @property $show_correct_answer (yesno) Whether or not to show the correct answer(s) to students on the quiz results screen.
 * @property $show_options_description_right_answer (yesno) If yes, displays the question description when the student chooses the correct answer.
 * @property $show_options_description_wrong_answer (yesno) If yes, displays the question description when the student chooses the wrong answer.
 * @property $show_results (yesno) If yes, results will be shown to the student at the conclusion of the quiz.
 * @property $time_limit (int) Quiz time limit (in minutes), empty denotes unlimited (untimed) quiz.
 *
 * @since 3.3.0
 * @since 3.19.2 Unkwnown.
 * @since 3.37.2 Added `llms_quiz_is_open` filter hook.
 * @since 3.38.0 Only add theme metadata to the quiz array when the `llms_get_quiz_theme_settings` filter is being used.
 * @since 4.0.0 Remove deprecated methods.
 * @since 4.2.0 Added a parameter to the `is_orphan()` method to deeply check the quiz is not really attached to any lesson.
 * @since 5.0.0 Remove previously deprecated method `LLMS_Quiz::get_lessons()`.
 */
class LLMS_Quiz extends LLMS_Post_Model {

    /**
     * Post Type Database name (as registered via `register_post_type()`).
     *
     * @var string
     */
    protected $db_post_type = 'llms_quiz';

    /**
     * Post type name (without prefix).
     *
     * @var string
     */
    protected $model_post_type = 'quiz';

    /**
     * Post type meta properties.
     *
     * Array key is the meta_key and array values is property's type.
     *
     * @since Unknown.
     * @since 7.6.2 Added the `disable_retake` property.
     * @var string[]
     */
    protected $properties = array(
        'lesson_id'           => 'absint',
        'allowed_attempts'    => 'int',
        'limit_attempts'      => 'yesno',
        'limit_time'          => 'yesno',
        'passing_percent'     => 'float',
        'random_questions'    => 'yesno',
        'show_correct_answer' => 'yesno',
        'time_limit'          => 'int',
        'disable_retake'      => 'yesno',
    );

    /**
     * Retrieve the LLMS_Course for the quiz.
     *
     * @since 3.16.0
     *
     * @return LLMS_Course|false
     */
    public function get_course() {
        $lesson = $this->get_lesson();
        if ( $lesson ) {
            return $lesson->get_course();
        }
        return false;
    }

    /**
     * Retrieve LLMS_Lesson for the quiz's parent lesson.
     *
     * @since 3.16.0
     * @since 3.16.12 Unknown.
     *
     * @return LLMS_Lesson|false|null The lesson object on success, `false` if no id stored, and `null` if the stored ID doesn't exist.
     */
    public function get_lesson() {
        $id = $this->get( 'lesson_id' );
        if ( ! $id ) {
            return false;
        }
        return llms_get_post( $id );
    }

    /**
     * Retrieve the quizzes child questions.
     *
     * @since 3.16.0
     *
     * @param string $return Optional. Type of return [ids|posts|questions]. Default `'questions'`.
     * @return array
     */
    public function get_questions( $return = 'questions' ) {
        return $this->questions()->get_questions( $return );
    }

    /**
     * Get questions count.
     *
     * @since 7.4.0
     *
     * @return int Question Count.
     */
    public function get_questions_count() {

        /**
         * Filter the count of questions in a quiz.
         *
         * @since 7.4.0
         *
         * @param int       $questions_count Number of questions in a quiz.
         * @param LLMS_Quiz $quiz            Current quiz object.
         */
        return apply_filters( 'llms_quiz_questions_count', count( $this->get_questions( 'ids' ) ), $this );
    }

    /**
     * Retrieve the time limit formatted as a human readable string.
     *
     * @since 3.16.0
     *
     * @return string
     */
    public function get_time_limit_string() {
        return LLMS_Date::convert_to_hours_minutes_string( $this->get( 'time_limit' ) );
    }

    /**
     * Determine if the quiz defines limited attempts.
     *
     * @since 3.16.0
     *
     * @return bool
     */
    public function has_attempt_limit() {
        return ( 'yes' === $this->get( 'limit_attempts' ) );
    }

    /**
     * Determine if a time limit is enabled for the quiz.
     *
     * @since 3.16.0
     *
     * @return bool
     */
    public function has_time_limit() {
        return ( 'yes' === $this->get( 'limit_time' ) );
    }

    /**
     * Determine if the quiz is an orphan.
     *
     * @since 3.16.12
     * @since 4.2.0 Added the $deep parameter.
     *
     * @param bool $deep Optional. Whether or not deeply check this quiz is orphan. Default `false`.
     *                   When set to true will ensure not only that this quiz as a `lesson_id` property set
     *                   But also that the lesson with id `lesson_id` has a `quiz` property as equal as this quiz id.
     * @return bool
     */
    public function is_orphan( $deep = false ) {

        $parent_id = $this->get( 'lesson_id' );

        if ( ! $parent_id ) {
            return true;
        }

        /**
         * This is to take into account possible data inconsistency.
         *
         * @link https://github.com/gocodebox/lifterlms/issues/1039
         */
        if ( $deep ) {
            $lesson = llms_get_post( $parent_id );
            // Both the ids are already absint, see LLMS_Post_Model::___get().
            if ( ! $lesson || $this->get( 'id' ) !== $lesson->get( 'quiz' ) ) {
                return true;
            }
        }

        return false;

    }

    /**
     * Determine if a student can take the quiz.
     *
     * @since 3.0.0
     * @since 3.16.0 Unkwnown.
     * @since 3.37.2 Added `llms_quiz_is_open` filter hook.
     *
     * @param int $user_id Optional. WP User ID, none supplied uses current user. Default `null`.
     * @return boolean
     */
    public function is_open( $user_id = null ) {

        $student = llms_get_student( $user_id );
        if ( ! $student ) {
            $quiz_open = false;
        } else {

            $remaining = $student->quizzes()->get_attempts_remaining_for_quiz( $this->get( 'id' ) );

            // string for "unlimited" or number of attempts.
            $quiz_open = ! is_numeric( $remaining ) || $remaining > 0;

            // Check for a passed attempt and disable the quiz.
            if ( $quiz_open && llms_parse_bool( $this->get( 'disable_retake' ) ) ) {
                $passed_attempts = $student->quizzes()->get_attempts_by_quiz(
                    $this->get( 'id' ),
                    array(
                        'status' => array( 'pass' ),
                    )
                );

                if ( count( $passed_attempts ) ) {
                    $quiz_open = false;
                }
            }
        }

        /**
         * Filters whether the quiz is open to a student or not.
         *
         * @param boolean            $quiz_open Whether the quiz is open.
         * @param int|null           $user_id   WP User ID, can be `null`.
         * @param int                $quiz_id   The Quiz id.
         * @param LLMS_Quiz          $quiz      The LLMS_Quiz instance.
         * @param LLMS_Student|false $student   LLMS_Student instance or false if user not found.
         */
        return apply_filters( 'llms_quiz_is_open', $quiz_open, $user_id, $this->get( 'id' ), $this, $student );

    }

    /**
     * Retrieve an instance of the question manager for the quiz.
     *
     * @since 3.16.0
     *
     * @return LLMS_Question_Manager
     */
    public function questions() {
        return new LLMS_Question_Manager( $this );
    }

    /**
     * Called before data is sorted and returned by $this->toArray().
     * Extending classes should override this data if custom data should
     * be added when object is converted to an array or json.
     *
     * @since 3.3.0
     * @since 3.19.2 Unknown.
     * @since 3.38.0 Only add theme metadata to the quiz array when the `llms_get_quiz_theme_settings` filter is being used.
     *
     * @param array $arr Array of data to be serialized.
     * @return array
     */
    protected function toArrayAfter( $arr ) {

        $arr['questions'] = array();

        // Builder lazy loads questions via ajax.
        global $llms_builder_lazy_load;
        if ( ! $llms_builder_lazy_load ) {
            foreach ( $this->get_questions() as $question ) {
                $arr['questions'][] = $question->toArray();
            }
        }

        // If theme has legacy support quiz layouts, add theme metadata to the array.
        if ( get_theme_support( 'lifterlms-quizzes' ) && has_filter( 'llms_get_quiz_theme_settings' ) ) {
            $layout = llms_get_quiz_theme_setting( 'layout' );
            if ( $layout ) {
                $arr[ $layout['id'] ] = get_post_meta( $this->get( 'id' ), $layout['id'], true );
            }
        }

        return $arr;

    }

    /**
     * Get the (points) value of a question.
     *
     * @since 3.3.0
     * @since 3.37.2 Use strict comparison '===' in place of '=='.
     *
     * @param int $question_id  WP Post ID of the LLMS_Question.
     * @return int
     */
    public function get_question_value( $question_id ) {

        foreach ( $this->get_questions_raw() as $q ) {
            if ( $question_id === $q['id'] ) {
                return absint( $q['points'] );
            }
        }

        return 0;

    }

    /**
     * Retrieve the array of raw question data from the postmeta table.
     *
     * @since 3.3.0
     *
     * @return array
     */
    private function get_questions_raw() {

        $q = get_post_meta( $this->get( 'id' ), $this->meta_prefix . 'questions', true );
        return $q ? $q : array();

    }

}