LLMS_Question
LLMS Quiz Question Model.
Description Description
Source Source
File: includes/models/model.llms.question.php
class LLMS_Question extends LLMS_Post_Model { protected $db_post_type = 'llms_question'; protected $model_post_type = 'question'; protected $properties = array( 'content' => 'html', 'clarifications' => 'html', 'clarifications_enabled' => 'yesno', 'description_enabled' => 'yesno', 'image' => 'array', 'multi_choices' => 'yesno', 'parent_id' => 'absint', 'points' => 'absint', 'question_type' => 'string', 'question' => 'html', 'title' => 'html', 'video_enabled' => 'yesno', 'video_src' => 'string', ); /** * Create a new question choice * @param array $data array of question choice data * @return string|boolean * @since 3.16.0 * @version 3.16.0 */ public function create_choice( $data ) { $data = wp_parse_args( $data, array( 'choice' => '', 'choice_type' => 'text', 'correct' => false, 'marker' => $this->get_next_choice_marker(), 'question_id' => $this->get( 'id' ), ) ); $choice = new LLMS_Question_Choice( $this->get( 'id' ) ); if ( $choice->create( $data ) ) { return $choice->get( 'id' ); } return false; } /** * Delete a choice by ID * @param string $id choice ID * @return boolean * @since 3.16.0 * @version 3.16.0 */ public function delete_choice( $id ) { $choice = $this->get_choice( $id ); if ( ! $choice ) { return false; } return $choice->delete(); } /** * Retrieve the type of automatic grading that can be performed on the question * @return string|false * @since 3.16.0 * @version 3.16.0 */ public function get_auto_grade_type() { if ( $this->supports( 'choices' ) && $this->supports( 'grading', 'auto' ) ) { return 'choices'; } elseif ( $this->supports( 'grading', 'conditional' ) && llms_parse_bool( $this->get( 'auto_grade' ) ) ) { return 'conditional'; } return false; } /** * An array of default arguments to pass to $this->create() when creating a new post * @param array $args args of data to be passed to wp_insert_post * @return array * @since 3.16.0 * @version 3.16.12 */ protected function get_creation_args( $args = null ) { // allow nothing to be passed in if ( empty( $args ) ) { $args = array(); } // backwards compat to original 3.0.0 format when just a title was passed in if ( is_string( $args ) ) { $args = array( 'post_title' => $args, ); } if ( isset( $args['title'] ) ) { $args['post_title'] = $args['title']; unset( $args['title'] ); } if ( isset( $args['content'] ) ) { $args['post_content'] = $args['content']; unset( $args['content'] ); } $meta = isset( $args['meta_input'] ) ? $args['meta_input'] : array(); $props = array_diff( array_keys( $this->get_properties() ), array_keys( $this->get_post_properties() ) ); foreach ( $props as $prop ) { if ( isset( $args[ $prop ] ) ) { $meta[ $this->meta_prefix . $prop ] = $args[ $prop ]; unset( $args[ $prop ] ); } } $args['meta_input'] = wp_parse_args( $meta, $meta ); $args = wp_parse_args( $args, array( 'comment_status' => 'closed', 'meta_input' => array(), 'menu_order' => 1, 'ping_status' => 'closed', 'post_author' => get_current_user_id(), 'post_content' => '', 'post_excerpt' => '', 'post_status' => 'publish', 'post_title' => '', 'post_type' => $this->get( 'db_post_type' ), ) ); return apply_filters( 'llms_' . $this->model_post_type . '_get_creation_args', $args, $this ); } /** * Retrieve a choice by id * @param string $id Choice ID * @return obj|false * @since 3.16.0 * @version 3.16.0 */ public function get_choice( $id ) { $choice = new LLMS_Question_Choice( $this->get( 'id' ), $id ); if ( $choice->exists() && $this->get( 'id' ) == $choice->get_question_id() ) { return $choice; } return false; } /** * Retrieve the question's choices * * @since 3.16.0 * @since 3.30.1 Improve choice sorting to accommodate numeric markers. * * @param string $return Optional. Determine how to return the choice data. * 'choices' (default) returns an array of LLMS_Question_Choice objects. * 'ids' returns an array of LLMS_Question_Choice ids. * @return array */ public function get_choices( $return = 'choices' ) { global $wpdb; $results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key AS id , meta_value AS data FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key LIKE '_llms_choice_%' ;", $this->get( 'id' ) ) ); usort( $results, array( $this, 'sort_choices' ) ); if ( 'ids' === $return ) { return wp_list_pluck( $results, 'id' ); } $ret = array(); foreach ( $results as $result ) { $ret[] = new LLMS_Question_Choice( $this->get( 'id' ), unserialize( $result->data ) ); } return $ret; } /** * Retrieve the question description (post_content) * Add's extra allowed tags to wp_kses_post allowed tags so that async audio shortcodes will work properly * @return string * @since 3.16.6 * @version 3.16.6 */ public function get_description() { global $allowedposttags; $allowedposttags['source'] = array( 'src' => true, 'type' => true, ); $desc = $this->get( 'content' ); unset( $allowedposttags['source'] ); return apply_filters( 'llms_' . $this->get( 'question_type' ) . '_question_get_description', $desc, $this ); } /** * Retrieve the correct values for a conditionally graded question * @return array * @since 3.16.15 * @version 3.16.15 */ public function get_conditional_correct_value() { $correct = explode( '|', $this->get( 'correct_value' ) ); $correct = array_map( 'trim', $correct ); return $correct; } /** * Retrieve correct choices for a given question * @return array * @since 3.16.0 * @version 3.16.0 */ public function get_correct_choice() { $correct = false; if ( $this->supports( 'choices' ) && $this->supports( 'grading', 'auto' ) ) { $multi = ( 'yes' === $this->get( 'multi_choices' ) ); $correct = array(); foreach ( $this->get_choices() as $choice ) { if ( $choice->is_correct() ) { $correct[] = $choice->get( 'id' ); if ( ! $multi ) { break; } } } // always sort multi choices for easy auto comparison if ( $multi && $this->supports( 'selectable' ) ) { sort( $correct ); } } return $correct; } /** * Get the question text (title) * @return string * @since 3.16.0 * @version 3.16.0 */ public function get_question( $format = 'html' ) { return apply_filters( 'llms_' . $this->get( 'question_type' ) . '_question_get_question', $this->get( 'title' ), $format, $this ); } /** * Retrieve child questions (for question group) * @todo need to prevent access for non-group questions... * @return array * @since 3.16.0 * @version 3.16.0 */ public function get_questions() { return $this->questions()->get_questions(); } /** * Retrieves an object cache key for the question's choices * @return string * @since 3.16.0 * @version 3.16.0 */ private function get_choice_cache_key() { return sprintf( 'question_%d_choices', $this->get( 'id' ) ); } /** * Retrieve URL for an image associated with the question if it's enabled * @param string|array $size registered image size or a numeric array with width/height * @return string empty string if no image or not supported * @since 3.16.0 * @version 3.16.0 */ public function get_image( $size = 'full', $unused = null ) { $url = ''; if ( $this->has_image() ) { $img = $this->get( 'image' ); if ( isset( $img['id'] ) && is_numeric( $img['id'] ) ) { $src = wp_get_attachment_image_src( $img['id'], $size ); if ( $src ) { $url = $src[0]; } elseif ( isset( $img['src'] ) ) { $url = $img['src']; } } } return apply_filters( 'llms_' . $this->get( 'question_type' ) . '_question_get_image', $url, $this ); } /** * Retrieve the next marker for question choices * * @since 3.16.0 * @since 3.30.1 Fixed bug which caused the next marker to be 1 index too high. * * @return string */ protected function get_next_choice_marker() { $next_index = count( $this->get_choices( 'ids', false ) ); $type = $this->get_question_type(); $markers = $type['choices']['markers']; return $next_index > count( $markers ) ? false : $markers[ $next_index ]; } /** * Retrieve question type data for the given question * @return array * @since 3.16.0 * @version 3.16.0 */ public function get_question_type() { return llms_get_question_type( $this->get( 'question_type' ) ); } /** * Retrieve an instance of the questions parent LLMS_Quiz * @return obj * @since 3.16.0 * @version 3.16.0 */ public function get_quiz() { return new LLMS_Quiz( $this->get( 'parent_id' ) ); } /** * Retrieve video embed for question featured video * @return string * @since 3.16.0 * @version 3.17.0 */ public function get_video() { $html = ''; $embed = $this->get( 'video_src' ); if ( $embed ) { // get oembed $html = wp_oembed_get( $embed ); // fallback to video shortcode if ( ! $html ) { $html = do_shortcode( '[video src="' . $embed . '"]' ); } } return apply_filters( 'llms_' . $this->get( 'question_type' ) . '_question_get_video', $html, $embed, $this ); } /** * Attempt to grade a question * @param array $answer selected answer(s) * @return mixed yes = correct * no = incorrect * null = not auto gradable * @since 3.16.0 * @version 3.16.15 */ public function grade( $answer ) { /** * Allow 3rd parties to do custom grading * If filter returns non-null will bypass core grading */ $grade = apply_filters( 'llms_' . $this->get( 'question_type' ) . '_question_pre_grade', null, $answer, $this ); if ( is_null( $grade ) ) { if ( $this->get( 'points' ) >= 1 ) { $grading_type = $this->get_auto_grade_type(); if ( 'choices' === $grading_type ) { sort( $answer ); $grade = ( $answer === $this->get_correct_choice() ) ? 'yes' : 'no'; } elseif ( 'conditional' === $grading_type ) { $correct = $this->get_conditional_correct_value(); // allow case sensitivity to be enabled if required if ( false === apply_filters( 'llms_quiz_grading_case_sensitive', false, $answer, $correct, $this ) ) { $answer = array_map( 'strtolower', $answer ); $correct = array_map( 'strtolower', $correct ); } $grade = ( $answer === $correct ) ? 'yes' : 'no'; } } } return apply_filters( 'llms_' . $this->get( 'question_type' ) . '_question_grade', $grade, $answer, $this ); } /** * Determine if a description is enabled and not empty * @return bool * @since 3.16.0 * @version 3.16.12 */ public function has_description() { $enabled = $this->get( 'description_enabled' ); $content = $this->get( 'content' ); return ( 'yes' === $enabled && $content ); } /** * Determine if a featured image is enabled and not empty * @return bool * @since 3.16.0 * @version 3.16.0 */ public function has_image() { $img = $this->get( 'image' ); if ( is_array( $img ) ) { if ( ! empty( $img['enabled'] ) && ( ! empty( $img['id'] ) || ! empty( $img['src'] ) ) ) { return ( 'yes' === $img['enabled'] ); } } return false; } /** * Determine if a featured video is enabled & not empty * @return bool * @since 3.16.0 * @version 3.16.12 */ public function has_video() { $enabled = $this->get( 'video_enabled' ); $src = $this->get( 'video_src' ); return ( 'yes' === $enabled && $src ); } /** * Determine if the question is an orphan * @return bool * @since 3.27.0 * @version 3.27.0 */ public function is_orphan() { $statuses = array( 'publish', 'draft' ); $parent_id = $this->get( 'parent_id' ); if ( ! $parent_id ) { return true; } elseif ( ! in_array( get_post_status( $parent_id ), $statuses, true ) ) { return true; } return false; } /** * Access question manager (used for question groups) * @todo need to prevent access for non-group questions... * @return obj * @since 3.16.0 * @version 3.16.0 */ public function questions() { return new LLMS_Question_Manager( $this ); } /** * Sort choices by marker. * * @since 3.30.1 * @version 3.30.1 * * @param string $choice_a Serialized choice data. * @param string $choice_b Serialized choice data. * @return int */ private function sort_choices( $choice_a, $choice_b ) { $a_data = unserialize( $choice_a->data ); $b_data = unserialize( $choice_b->data ); return strnatcmp( $a_data['marker'], $b_data['marker'] ); } /** * Determine if the question supports a question feature * @param string $feature name of the feature (eg "choices") * @param mixed $option allow matching feature options * @return boolean * @since 3.16.0 * @version 3.16.15 */ public function supports( $feature, $option = null ) { $ret = false; $type = $this->get_question_type(); if ( $type ) { if ( 'choices' === $feature ) { $ret = ( ! empty( $type['choices'] ) ); } elseif ( 'grading' === $feature ) { $ret = ( $type['grading'] && $option === $type['grading'] ); } elseif ( 'points' === $feature ) { $ret = $type['points']; } elseif ( 'random_lock' === $feature ) { $ret = $type['random_lock']; } elseif ( 'selectable' === $feature ) { $ret = empty( $type['choices'] ) ? false : $type['choices']['selectable']; } } /** * @filter llms_{$question_type}_question_supports * @param boolean $ret return value * @param string $string name of the feature being checked (eg "choices") * @param obj $this instance of the LLMS_Question * @usage apply_filters( 'llms_choice_question_supports', function( $ret, $feature, $option, $question ) { * return $ret; * }, 10, 4 ); */ return apply_filters( 'llms_' . $this->get( 'question_type' ) . '_question_supports', $ret, $feature, $option, $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 * @param array $arr array of data to be serialized * @return array * @since 3.3.0 * @version 3.16.0 */ protected function toArrayAfter( $arr ) { unset( $arr['author'] ); unset( $arr['date'] ); unset( $arr['excerpt'] ); unset( $arr['modified'] ); unset( $arr['status'] ); $choices = array(); foreach ( $this->get_choices() as $choice ) { $choices[] = $choice->get_data(); } $arr['choices'] = $choices; if ( 'group' === $this->get( 'question_type' ) ) { $arr['questions'] = array(); foreach ( $this->get_questions() as $question ) { $arr['questions'][] = $question->toArray(); } } return $arr; } /** * Update a question choice * if no id is supplied will create a new choice * @param array $data array of choice data (see $this->create_choice()) * @return string|boolean * @since 3.16.0 * @version 3.16.0 */ public function update_choice( $data ) { // if there's no ID, we'll add a new choice if ( ! isset( $data['id'] ) ) { return $this->create_choice( $data ); } // get the question $choice = $this->get_choice( $data['id'] ); if ( ! $choice ) { return false; } $choice->update( $data )->save(); // return choice ID return $choice->get( 'id' ); } /** * Get the correct option for the question * @return array|null * @since 1.0.0 * @version 3.9.0 */ public function get_correct_option() { $options = $this->get_options(); $key = $this->get_correct_option_key(); if ( ! is_null( $key ) && isset( $options[ $key ] ) ) { return $options[ $key ]; } return null; } /** * Get the key of the correct option * @return int|null * @since 3.9.0 * @version 3.9.0 */ public function get_correct_option_key() { $options = $this->get_options(); foreach ( $options as $key => $option ) { if ( $option['correct_option'] ) { return $key; } } return null; } /** * Retrieve quizzes this quiz is assigned to * @return array array of WP_Post IDs (quiz post types) * @since 3.12.0 * @version 3.12.0 */ public function get_quizzes() { $id = absint( $this->get( 'id' ) ); $len = strlen( strval( $id ) ); $str_like = '%' . sprintf( 's:2:"id";s:%1$d:"%2$s";', $len, $id ) . '%'; $int_like = '%' . sprintf( 's:2:"id";i:%1$s;', $id ) . '%'; global $wpdb; $query = $wpdb->get_col( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_llms_questions' AND ( meta_value LIKE '{$str_like}' OR meta_value LIKE '{$int_like}' );" ); return $query; } /** * Don't add custom fields during toArray() * @param array $arr post model array * @return array * @since 3.16.11 * @version 3.16.11 */ protected function toArrayCustom( $arr ) { return $arr; } /* /$$ /$$ /$$ | $$ | $$ | $$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$__ $$ /$$__ $$ /$$__ $$ /$$__ $$ /$$__ $$ /$$_____/ |____ $$|_ $$_/ /$$__ $$ /$$__ $$ | $$ | $$| $$$$$$$$| $$ \ $$| $$ \__/| $$$$$$$$| $$ /$$$$$$$ | $$ | $$$$$$$$| $$ | $$ | $$ | $$| $$_____/| $$ | $$| $$ | $$_____/| $$ /$$__ $$ | $$ /$$| $$_____/| $$ | $$ | $$$$$$$| $$$$$$$| $$$$$$$/| $$ | $$$$$$$| $$$$$$$| $$$$$$$ | $$$$/| $$$$$$$| $$$$$$$ \_______/ \_______/| $$____/ |__/ \_______/ \_______/ \_______/ \___/ \_______/ \_______/ | $$ | $$ |__/ */ /** * Get the options for the question * @return array * @since 1.0.0 * @version 3.16.0 * @deprecated 3.16.0 */ public function get_options() { llms_deprecated_function( 'LLMS_Question::get_options()', '3.16.0', 'LLMS_Question::get_choices()' ); return $this->get_choices(); } }
Expand full source code Collapse full source code View on GitHub
Changelog Changelog
Version | Description |
---|---|
3.30.1 | Fixed choice sorting issues. |
1.0.0 | Introduced. |
Methods Methods
- create_choice — Create a new question choice
- delete_choice — Delete a choice by ID
- get_auto_grade_type — Retrieve the type of automatic grading that can be performed on the question
- get_choice — Retrieve a choice by id
- get_choice_cache_key — Retrieves an object cache key for the question's choices
- get_choices — Retrieve the question's choices
- get_conditional_correct_value — Retrieve the correct values for a conditionally graded question
- get_correct_choice — Retrieve correct choices for a given question
- get_correct_option — Get the correct option for the question
- get_correct_option_key — Get the key of the correct option
- get_creation_args — An array of default arguments to pass to $this->create() when creating a new post
- get_description — Retrieve the question description (post_content) Add's extra allowed tags to wp_kses_post allowed tags so that async audio shortcodes will work properly
- get_image — Retrieve URL for an image associated with the question if it's enabled
- get_next_choice_marker — Retrieve the next marker for question choices
- get_options — Get the options for the question — deprecated
- get_question — Get the question text (title)
- get_question_type — Retrieve question type data for the given question
- get_questions — Retrieve child questions (for question group)
- get_quiz — Retrieve an instance of the questions parent LLMS_Quiz
- get_quizzes — Retrieve quizzes this quiz is assigned to
- get_video — Retrieve video embed for question featured video
- grade — Attempt to grade a question
- has_description — Determine if a description is enabled and not empty
- has_image — Determine if a featured image is enabled and not empty
- has_video — Determine if a featured video is enabled & not empty
- is_orphan — Determine if the question is an orphan
- questions — Access question manager (used for question groups)
- sort_choices — Sort choices by marker.
- supports — Determine if the question supports a question feature
- toArrayAfter — 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
- toArrayCustom — Don't add custom fields during toArray()
- update_choice — Update a question choice if no id is supplied will create a new choice
User Contributed Notes User Contributed Notes
Permalink: