PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 2.1.6
Tutor LMS – eLearning and online course solution v2.1.6
3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / classes / Quiz.php
tutor / classes Last commit date
Addons.php 3 years ago Admin.php 3 years ago Ajax.php 3 years ago Announcements.php 3 years ago Assets.php 3 years ago Backend_Page_Trait.php 3 years ago Course.php 3 years ago Course_Embed.php 3 years ago Course_Filter.php 3 years ago Course_List.php 3 years ago Course_Settings_Tabs.php 3 years ago Course_Widget.php 3 years ago Custom_Validation.php 3 years ago Dashboard.php 3 years ago FormHandler.php 3 years ago Frontend.php 3 years ago Gutenberg.php 3 years ago Input.php 3 years ago Instructor.php 3 years ago Instructors_List.php 3 years ago Lesson.php 3 years ago Options_V2.php 3 years ago Post_types.php 3 years ago Private_Course_Access.php 3 years ago Q_and_A.php 3 years ago Question_Answers_List.php 3 years ago Quiz.php 3 years ago Quiz_Attempts_List.php 3 years ago RestAPI.php 3 years ago Reviews.php 3 years ago Rewrite_Rules.php 3 years ago Shortcode.php 3 years ago Student.php 3 years ago Students_List.php 3 years ago Taxonomies.php 3 years ago Template.php 3 years ago Theme_Compatibility.php 3 years ago Tools.php 3 years ago Tools_V2.php 3 years ago Tutor.php 3 years ago TutorEDD.php 3 years ago Tutor_Base.php 3 years ago Tutor_Setup.php 3 years ago Upgrader.php 3 years ago User.php 3 years ago Utils.php 3 years ago Video_Stream.php 3 years ago Withdraw.php 3 years ago Withdraw_Requests_List.php 3 years ago WooCommerce.php 3 years ago
Quiz.php
1645 lines
1 <?php
2 /**
3 * Quiz class
4 *
5 * @package Tutor\QuestionAnswer
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com
8 * @since 1.0.0
9 */
10
11 namespace TUTOR;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 use Tutor\Helpers\QueryHelper;
18 use Tutor\Models\QuizModel;
19 /**
20 * Manage quiz operations.
21 *
22 * @since 1.0.0
23 */
24 class Quiz {
25
26 /**
27 * Allowed attrs
28 *
29 * @var array
30 */
31 private $allowed_attributes = array(
32 'src' => array(),
33 'style' => array(),
34 'class' => array(),
35 'id' => array(),
36 'href' => array(),
37 'alt' => array(),
38 'title' => array(),
39 'type' => array(),
40 'controls' => array(),
41 'muted' => array(),
42 'loop' => array(),
43 'poster' => array(),
44 'preload' => array(),
45 'autoplay' => array(),
46 'width' => array(),
47 'height' => array(),
48 );
49
50 /**
51 * Allowed HTML tags
52 *
53 * @var array
54 */
55 private $allowed_html = array( 'img', 'b', 'i', 'br', 'a', 'audio', 'video', 'source' );
56
57 /**
58 * Register hooks
59 *
60 * @since 1.0.0
61 *
62 * @return void
63 */
64 public function __construct() {
65 add_action( 'save_post_tutor_quiz', array( $this, 'save_quiz_meta' ) );
66 add_action( 'wp_ajax_remove_quiz_from_post', array( $this, 'remove_quiz_from_post' ) );
67
68 add_action( 'wp_ajax_tutor_quiz_timeout', array( $this, 'tutor_quiz_timeout' ) );
69
70 // User take the quiz.
71 add_action( 'template_redirect', array( $this, 'start_the_quiz' ) );
72 add_action( 'template_redirect', array( $this, 'answering_quiz' ) );
73 add_action( 'template_redirect', array( $this, 'finishing_quiz_attempt' ) );
74
75 add_action( 'wp_ajax_review_quiz_answer', array( $this, 'review_quiz_answer' ) );
76 // Instructor Feedback Action.
77 add_action( 'wp_ajax_tutor_instructor_feedback', array( $this, 'tutor_instructor_feedback' ) );
78
79 /**
80 * New Design Quiz
81 */
82
83 add_action( 'wp_ajax_tutor_quiz_save', array( $this, 'tutor_quiz_save' ) );
84 add_action( 'wp_ajax_tutor_delete_quiz_by_id', array( $this, 'tutor_delete_quiz_by_id' ) );
85 add_action( 'wp_ajax_tutor_load_quiz_builder_modal', array( $this, 'tutor_load_quiz_builder_modal' ), 10, 0 );
86 add_action( 'wp_ajax_tutor_quiz_builder_get_question_form', array( $this, 'tutor_quiz_builder_get_question_form' ) );
87 add_action( 'wp_ajax_tutor_quiz_modal_update_question', array( $this, 'tutor_quiz_modal_update_question' ) );
88 add_action( 'wp_ajax_tutor_quiz_builder_question_delete', array( $this, 'tutor_quiz_builder_question_delete' ) );
89 add_action( 'wp_ajax_tutor_quiz_question_answer_editor', array( $this, 'tutor_quiz_question_answer_editor' ) );
90 add_action( 'wp_ajax_tutor_save_quiz_answer_options', array( $this, 'tutor_save_quiz_answer_options' ), 10, 0 );
91 add_action( 'wp_ajax_tutor_update_quiz_answer_options', array( $this, 'tutor_update_quiz_answer_options' ) );
92 add_action( 'wp_ajax_tutor_quiz_builder_change_type', array( $this, 'tutor_quiz_builder_change_type' ) );
93 add_action( 'wp_ajax_tutor_quiz_builder_delete_answer', array( $this, 'tutor_quiz_builder_delete_answer' ) );
94 add_action( 'wp_ajax_tutor_quiz_question_sorting', array( $this, 'tutor_quiz_question_sorting' ) );
95 add_action( 'wp_ajax_tutor_quiz_answer_sorting', array( $this, 'tutor_quiz_answer_sorting' ) );
96 add_action( 'wp_ajax_tutor_mark_answer_as_correct', array( $this, 'tutor_mark_answer_as_correct' ) );
97
98 /**
99 * Frontend Stuff
100 */
101 add_action( 'wp_ajax_tutor_render_quiz_content', array( $this, 'tutor_render_quiz_content' ) );
102
103 /**
104 * Quiz abandon action
105 *
106 * @since 1.9.6
107 */
108 add_action( 'wp_ajax_tutor_quiz_abandon', array( $this, 'tutor_quiz_abandon' ) );
109
110 $this->prepare_allowed_html();
111
112 /**
113 * Delete quiz attempt
114 *
115 * @since 2.1.0
116 */
117 add_action( 'wp_ajax_tutor_attempt_delete', array( $this, 'attempt_delete' ) );
118 }
119
120 /**
121 * Prepare allowed HTML
122 *
123 * @since 1.0.0
124 *
125 * @return void
126 */
127 private function prepare_allowed_html() {
128
129 $allowed = array();
130
131 foreach ( $this->allowed_html as $tag ) {
132 $allowed[ $tag ] = $this->allowed_attributes;
133 }
134
135 $this->allowed_html = $allowed;
136 }
137
138 /**
139 * Instructor feedback ajax request handler
140 *
141 * @since 1.0.0
142 *
143 * @return void | send json response
144 */
145 public function tutor_instructor_feedback() {
146 tutor_utils()->checking_nonce();
147 $attempt_details = self::attempt_details( Input::post( 'attempt_id', 0, Input::TYPE_INT ) );
148 $feedback = Input::post( 'feedback', '', Input::TYPE_KSES_POST );
149 $attempt_info = isset( $attempt_details->attempt_info ) ? $attempt_details->attempt_info : false;
150 if ( $attempt_info ) {
151 //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
152 $unserialized = unserialize( $attempt_details->attempt_info );
153 if ( is_array( $unserialized ) ) {
154 $unserialized['instructor_feedback'] = $feedback;
155
156 do_action( 'tutor_quiz/attempt/submitted/feedback', $attempt_details->attempt_id );
157 //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
158 $update = self::update_attempt_info( $attempt_details->attempt_id, serialize( $unserialized ) );
159 if ( $update ) {
160 wp_send_json_success();
161 } else {
162 wp_send_json_error();
163 }
164 } else {
165 wp_send_json_error( __( 'Invalid quiz info' ) );
166 }
167 }
168 wp_send_json_error();
169 }
170
171 /**
172 * Update quiz meta
173 *
174 * @since 1.0.0
175 *
176 * @param int $post_ID post id.
177 * @return void
178 */
179 public function save_quiz_meta( $post_ID ) {
180 //phpcs:ignore WordPress.Security.NonceVerification.Missing
181 if ( isset( $_POST['quiz_option'] ) ) {
182 $quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] ); //phpcs:ignore
183 update_post_meta( $post_ID, 'tutor_quiz_option', $quiz_option );
184 }
185 }
186
187 /**
188 * Remove quiz from post
189 *
190 * @since 1.0.0
191 *
192 * @return void
193 */
194 public function remove_quiz_from_post() {
195 tutor_utils()->checking_nonce();
196
197 global $wpdb;
198 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
199
200 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
201 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
202 }
203
204 $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'ID' => $quiz_id ) );
205 wp_send_json_success();
206 }
207
208 /**
209 * Start Quiz from here...
210 *
211 * @since 1.0.0
212 *
213 * @return void
214 */
215 public function start_the_quiz() {
216 if ( Input::post( 'tutor_action' ) !== 'tutor_start_quiz' ) {
217 return;
218 }
219 // Checking nonce.
220 tutor_utils()->checking_nonce();
221
222 if ( ! is_user_logged_in() ) {
223 // TODO: need to set a view in the next version.
224 die( 'Please sign in to do this operation' );
225 }
226
227 global $wpdb;
228
229 $user_id = get_current_user_id();
230 $user = get_userdata( $user_id );
231
232 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
233
234 $quiz = get_post( $quiz_id );
235 $course = tutor_utils()->get_course_by_quiz( $quiz_id );
236 if ( empty( $course->ID ) ) {
237 die( 'There is something went wrong with course, please check if quiz attached with a course' );
238 }
239
240 do_action( 'tutor_quiz/start/before', $quiz_id, $user_id );
241
242 $date = date( 'Y-m-d H:i:s', tutor_time() );
243
244 $tutor_quiz_option = (array) maybe_unserialize( get_post_meta( $quiz_id, 'tutor_quiz_option', true ) );
245 $attempts_allowed = tutor_utils()->get_quiz_option( $quiz_id, 'attempts_allowed', 0 );
246
247 $time_limit = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_value' );
248 $time_limit_seconds = 0;
249 $time_type = 'seconds';
250 if ( $time_limit ) {
251 $time_type = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_type' );
252
253 switch ( $time_type ) {
254 case 'seconds':
255 $time_limit_seconds = $time_limit;
256 break;
257 case 'minutes':
258 $time_limit_seconds = $time_limit * 60;
259 break;
260 case 'hours':
261 $time_limit_seconds = $time_limit * 60 * 60;
262 break;
263 case 'days':
264 $time_limit_seconds = $time_limit * 60 * 60 * 24;
265 break;
266 case 'weeks':
267 $time_limit_seconds = $time_limit * 60 * 60 * 24 * 7;
268 break;
269 }
270 }
271
272 $max_question_allowed = tutor_utils()->max_questions_for_take_quiz( $quiz_id );
273 $tutor_quiz_option['time_limit']['time_limit_seconds'] = $time_limit_seconds;
274
275 $attempt_data = array(
276 'course_id' => $course->ID,
277 'quiz_id' => $quiz_id,
278 'user_id' => $user_id,
279 'total_questions' => $max_question_allowed,
280 'total_answered_questions' => 0,
281 'attempt_info' => maybe_serialize( $tutor_quiz_option ),
282 'attempt_status' => 'attempt_started',
283 'attempt_ip' => tutor_utils()->get_ip(),
284 'attempt_started_at' => $date,
285 );
286
287 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_data );
288 $attempt_id = (int) $wpdb->insert_id;
289
290 do_action( 'tutor_quiz/start/after', $quiz_id, $user_id, $attempt_id );
291
292 wp_safe_redirect( get_permalink( $quiz_id ) );
293 die();
294 }
295
296 /**
297 * Answering quiz
298 *
299 * @since 1.0.0
300 *
301 * @return void
302 */
303 public function answering_quiz() {
304
305 if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) {
306 return;
307 }
308 // submit quiz attempts.
309 self::tutor_quiz_attemp_submit();
310
311 wp_safe_redirect( get_the_permalink() );
312 die();
313 }
314
315 /**
316 * Quiz abandon submission handler
317 *
318 * @since 1.9.6
319 *
320 * @return JSON response
321 */
322 public function tutor_quiz_abandon() {
323 if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) {
324 return;
325 }
326 // submit quiz attempts.
327 if ( self::tutor_quiz_attemp_submit() ) {
328 wp_send_json_success();
329 } else {
330 wp_send_json_error();
331 }
332 }
333
334 /**
335 * This is a unified method for handling normal quiz submit or abandon submit
336 * It will handle ajax or normal form submit and can be used with different hooks
337 *
338 * @since 1.9.6
339 *
340 * @return true | false
341 */
342 public static function tutor_quiz_attemp_submit() {
343 // Check logged in.
344 if ( ! is_user_logged_in() ) {
345 die( 'Please sign in to do this operation' );
346 }
347
348 // Check nonce.
349 tutor_utils()->checking_nonce();
350
351 // Prepare attempt info.
352 global $wpdb;
353 $user_id = get_current_user_id();
354 $attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
355 $attempt = tutor_utils()->get_attempt( $attempt_id );
356 $course_id = tutor_utils()->get_course_by_quiz( $attempt->quiz_id )->ID;
357
358 // Sanitize data by helper method.
359 $attempt_answers = isset( $_POST['attempt'] ) ? tutor_sanitize_data( $_POST['attempt'] ) : false; //phpcs:ignore
360 $attempt_answers = is_array( $attempt_answers ) ? $attempt_answers : array();
361
362 // Check if has access to the attempt.
363 if ( ! $attempt || $user_id != $attempt->user_id ) {
364 die( 'Operation not allowed, attempt not found or permission denied' );
365 }
366
367 // Before ook.
368 do_action( 'tutor_quiz/attempt_analysing/before', $attempt_id );
369
370 // Loop through every single attempt answer
371 // Single quiz can have multiple question. So multiple answer should be saved.
372 foreach ( $attempt_answers as $attempt_id => $attempt_answer ) {
373
374 /**
375 * Get total marks of all question comes
376 */
377 $question_ids = tutor_utils()->avalue_dot( 'quiz_question_ids', $attempt_answer );
378 $question_ids = array_filter(
379 $question_ids,
380 function( $id ) {
381 return (int) $id;
382 }
383 );
384
385 // Calculate and set the total marks in attempt table for this question.
386 if ( is_array( $question_ids ) && count( $question_ids ) ) {
387 $question_ids_string = QueryHelper::prepare_in_clause( $question_ids );
388
389 // Get total marks of the questions from question table.
390 $total_question_marks = $wpdb->get_var(
391 "SELECT SUM(question_mark)
392 FROM {$wpdb->prefix}tutor_quiz_questions
393 WHERE question_id IN({$question_ids_string});
394 "
395 );
396
397 // Set the the total mark in the attempt table for the question.
398 $wpdb->update(
399 $wpdb->prefix . 'tutor_quiz_attempts',
400 array( 'total_marks' => $total_question_marks ),
401 array( 'attempt_id' => $attempt_id )
402 );
403 }
404
405 $total_marks = 0;
406 $review_required = false;
407 $quiz_answers = tutor_utils()->avalue_dot( 'quiz_question', $attempt_answer );
408
409 if ( tutor_utils()->count( $quiz_answers ) ) {
410
411 foreach ( $quiz_answers as $question_id => $answers ) {
412 $question = QuizModel::get_quiz_question_by_id( $question_id );
413 $question_type = $question->question_type;
414
415 $is_answer_was_correct = false;
416 $given_answer = '';
417
418 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
419
420 if ( ! is_numeric( $answers ) || ! $answers ) {
421 wp_send_json_error();
422 exit;
423 }
424
425 $given_answer = $answers;
426 $is_answer_was_correct = (bool) $wpdb->get_var(
427 $wpdb->prepare(
428 "SELECT is_correct
429 FROM {$wpdb->prefix}tutor_quiz_question_answers
430 WHERE answer_id = %d
431 ",
432 $answers
433 )
434 );
435
436 } elseif ( 'multiple_choice' === $question_type ) {
437
438 $given_answer = (array) ( $answers );
439
440 $given_answer = array_filter(
441 $given_answer,
442 function( $id ) {
443 return is_numeric( $id ) && $id > 0;
444 }
445 );
446 $get_original_answers = (array) $wpdb->get_col(
447 $wpdb->prepare(
448 "SELECT
449 answer_id
450 FROM
451 {$wpdb->prefix}tutor_quiz_question_answers
452 WHERE belongs_question_id = %d
453 AND belongs_question_type = %s
454 AND is_correct = 1 ;
455 ",
456 $question->question_id,
457 $question_type
458 )
459 );
460
461 if ( count( array_diff( $get_original_answers, $given_answer ) ) === 0 && count( $get_original_answers ) === count( $given_answer ) ) {
462 $is_answer_was_correct = true;
463 }
464 $given_answer = maybe_serialize( $answers );
465
466 } elseif ( 'fill_in_the_blank' === $question_type ) {
467
468 $given_answer = (array) array_map( 'sanitize_text_field', $answers );
469 $given_answer = maybe_serialize( $given_answer );
470
471 $get_original_answer = $wpdb->get_row(
472 $wpdb->prepare(
473 "SELECT *
474 FROM {$wpdb->prefix}tutor_quiz_question_answers
475 WHERE belongs_question_id = %d
476 AND belongs_question_type = %s ;
477 ",
478 $question->question_id,
479 $question_type
480 )
481 );
482
483 $gap_answer = (array) explode( '|', $get_original_answer->answer_two_gap_match );
484
485 $gap_answer = array_map( 'sanitize_text_field', $gap_answer );
486 if ( strtolower( $given_answer ) == strtolower( maybe_serialize( $gap_answer ) ) ) {
487 $is_answer_was_correct = true;
488 }
489 } elseif ( 'open_ended' === $question_type || 'short_answer' === $question_type ) {
490 $review_required = true;
491 $given_answer = wp_kses_post( $answers );
492
493 } elseif ( 'ordering' === $question_type || 'matching' === $question_type || 'image_matching' === $question_type ) {
494
495 $given_answer = (array) array_map( 'sanitize_text_field', tutor_utils()->avalue_dot( 'answers', $answers ) );
496 $given_answer = maybe_serialize( $given_answer );
497
498 $get_original_answers = (array) $wpdb->get_col(
499 $wpdb->prepare(
500 "SELECT answer_id
501 FROM {$wpdb->prefix}tutor_quiz_question_answers
502 WHERE belongs_question_id = %d
503 AND belongs_question_type = %s
504 ORDER BY answer_order ASC ;
505 ",
506 $question->question_id,
507 $question_type
508 )
509 );
510
511 $get_original_answers = array_map( 'sanitize_text_field', $get_original_answers );
512
513 if ( maybe_serialize( $get_original_answers ) == $given_answer ) {
514 $is_answer_was_correct = true;
515 }
516 } elseif ( 'image_answering' === $question_type ) {
517 $image_inputs = tutor_utils()->avalue_dot( 'answer_id', $answers );
518 $image_inputs = (array) array_map( 'sanitize_text_field', $image_inputs );
519 $given_answer = maybe_serialize( $image_inputs );
520 $is_answer_was_correct = false;
521 /**
522 * For the image_answering question type result
523 * remain pending in spite of correct answer & required
524 * review of admin/instructor. Since it's
525 * pending we need to mark it as incorrect. Otherwise if
526 * mark it correct then earned mark will be updated. then
527 * again when instructor/admin review & mark it as correct
528 * extra mark is adding. In this case, student
529 * getting double mark for the same question.
530 *
531 * For now code is commenting will be removed later on
532 *
533 * @since 2.1.5
534 */
535
536 // $db_answer = $wpdb->get_col(
537 // $wpdb->prepare(
538 // "SELECT answer_title
539 // FROM {$wpdb->prefix}tutor_quiz_question_answers
540 // WHERE belongs_question_id = %d
541 // AND belongs_question_type = 'image_answering'
542 // ORDER BY answer_order asc ;",
543 // $question_id
544 // )
545 // );
546
547 // if ( is_array( $db_answer ) && count( $db_answer ) ) {
548 // $is_answer_was_correct = ( strtolower( maybe_serialize( array_values( $image_inputs ) ) ) == strtolower( maybe_serialize( $db_answer ) ) );
549 // }
550 }
551
552 $question_mark = $is_answer_was_correct ? $question->question_mark : 0;
553 $total_marks += $question_mark;
554
555 $answers_data = array(
556 'user_id' => $user_id,
557 'quiz_id' => $attempt->quiz_id,
558 'question_id' => $question_id,
559 'quiz_attempt_id' => $attempt_id,
560 'given_answer' => $given_answer,
561 'question_mark' => $question->question_mark,
562 'achieved_mark' => $question_mark,
563 'minus_mark' => 0,
564 'is_correct' => $is_answer_was_correct ? 1 : 0,
565 );
566
567 /**
568 * Check if question_type open ended or short ans the set
569 * is_correct default value null before saving
570 */
571 if ( in_array( $question_type, array( 'open_ended', 'short_answer', 'image_answering' ) ) ) {
572 $answers_data['is_correct'] = null;
573 $review_required = true;
574 }
575
576 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answers_data );
577 }
578 }
579
580 $attempt_info = array(
581 'total_answered_questions' => tutor_utils()->count( $quiz_answers ),
582 'earned_marks' => $total_marks,
583 'attempt_status' => 'attempt_ended',
584 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ),
585 );
586
587 if ( $review_required ) {
588 $attempt_info['attempt_status'] = 'review_required';
589 }
590
591 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
592 }
593
594 // After hook.
595 do_action( 'tutor_quiz/attempt_ended', $attempt_id, $course_id, $user_id );
596
597 return true;
598 }
599
600
601 /**
602 * Quiz attempt will be finish here
603 *
604 * @since 1.0.0
605 *
606 * @return void
607 */
608 public function finishing_quiz_attempt() {
609
610 if ( Input::post( 'tutor_action' ) !== 'tutor_finish_quiz_attempt' ) {
611 return;
612 }
613 // Checking nonce.
614 tutor_utils()->checking_nonce();
615
616 if ( ! is_user_logged_in() ) {
617 die( 'Please sign in to do this operation' );
618 }
619
620 global $wpdb;
621
622 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
623 $attempt = tutor_utils()->is_started_quiz( $quiz_id );
624 $attempt_id = $attempt->attempt_id;
625
626 $attempt_info = array(
627 'total_answered_questions' => 0,
628 'earned_marks' => 0,
629 'attempt_status' => 'attempt_ended',
630 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ),
631 );
632
633 do_action( 'tutor_quiz_before_finish', $attempt_id, $quiz_id, $attempt->user_id );
634 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
635 do_action( 'tutor_quiz_finished', $attempt_id, $quiz_id, $attempt->user_id );
636
637 wp_redirect( tutor_utils()->input_old( '_wp_http_referer' ) );
638 }
639
640 /**
641 * Quiz timeout by ajax
642 *
643 * @since 1.0.0
644 *
645 * @return void
646 */
647 public function tutor_quiz_timeout() {
648 tutils()->checking_nonce();
649
650 global $wpdb;
651
652 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
653 $attempt = tutor_utils()->is_started_quiz( $quiz_id );
654
655 if ( $attempt ) {
656 $attempt_id = $attempt->attempt_id;
657
658 $data = array(
659 'attempt_status' => 'attempt_timeout',
660 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ),
661 );
662 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $data, array( 'attempt_id' => $attempt->attempt_id ) );
663
664 do_action( 'tutor_quiz_timeout', $attempt_id, $quiz_id, $attempt->user_id );
665
666 wp_send_json_success();
667 }
668
669 wp_send_json_error( __( 'Quiz has been timeout already', 'tutor' ) );
670 }
671
672 /**
673 * Review quiz answer
674 *
675 * @since 1.0.0
676 *
677 * @return void
678 */
679 public function review_quiz_answer() {
680
681 tutor_utils()->checking_nonce();
682
683 global $wpdb;
684
685 $attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
686 $context = Input::post( 'context' );
687 $attempt_answer_id = Input::post( 'attempt_answer_id', 0, Input::TYPE_INT );
688 $mark_as = Input::post( 'mark_as' );
689
690 if ( ! tutor_utils()->can_user_manage( 'attempt', $attempt_id ) || ! tutor_utils()->can_user_manage( 'attempt_answer', $attempt_answer_id ) ) {
691 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
692 }
693
694 $attempt_answer = $wpdb->get_row(
695 $wpdb->prepare(
696 "SELECT *
697 FROM {$wpdb->prefix}tutor_quiz_attempt_answers
698 WHERE attempt_answer_id = %d
699 ",
700 $attempt_answer_id
701 )
702 );
703
704 $attempt = tutor_utils()->get_attempt( $attempt_id );
705 $question = QuizModel::get_quiz_question_by_id( $attempt_answer->question_id );
706 $course_id = $attempt->course_id;
707 $student_id = $attempt->user_id;
708 $previous_ans = $attempt_answer->is_correct;
709
710 do_action( 'tutor_quiz_review_answer_before', $attempt_answer_id, $attempt_id, $mark_as );
711
712 if ( 'correct' === $mark_as ) {
713
714 $answer_update_data = array(
715 'achieved_mark' => $attempt_answer->question_mark,
716 'is_correct' => 1,
717 );
718 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
719 if ( 0 == $previous_ans || null == $previous_ans ) {
720 // if previous answer was wrong or in review then add point as correct.
721 $attempt_update_data = array(
722 'earned_marks' => $attempt->earned_marks + $attempt_answer->question_mark,
723 'is_manually_reviewed' => 1,
724 'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ),
725 );
726 }
727
728 if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
729 $attempt_update_data['attempt_status'] = 'attempt_ended';
730 }
731 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
732
733 } elseif ( 'incorrect' === $mark_as ) {
734
735 $answer_update_data = array(
736 'achieved_mark' => '0.00',
737 'is_correct' => 0,
738 );
739 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
740
741 if ( 1 == $previous_ans ) {
742 // If previous ans was right then mynus.
743 $attempt_update_data = array(
744 'earned_marks' => $attempt->earned_marks - $attempt_answer->question_mark,
745 'is_manually_reviewed' => 1,
746 'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ),
747 );
748 }
749 if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
750 $attempt_update_data['attempt_status'] = 'attempt_ended';
751 }
752
753 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
754 }
755 do_action( 'tutor_quiz_review_answer_after', $attempt_answer_id, $attempt_id, $mark_as );
756 do_action( 'tutor_quiz/answer/review/after', $attempt_answer_id, $course_id, $student_id );
757
758 ob_start();
759 tutor_load_template_from_custom_path(
760 tutor()->path . '/views/quiz/attempt-details.php',
761 array(
762 'attempt_id' => $attempt_id,
763 'user_id' => $student_id,
764 'context' => $context,
765 'back_url' => Input::post( 'back_url' ),
766 )
767 );
768 wp_send_json_success( array( 'html' => ob_get_clean() ) );
769 }
770
771 /**
772 * Save single quiz into database and send html response
773 *
774 * @since 1.0.0
775 *
776 * @return void
777 */
778 public function tutor_quiz_save() {
779 tutor_utils()->checking_nonce();
780 // Prepare args.
781 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
782 $ex_quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
783 $quiz_title = Input::post( 'quiz_title' );
784 $quiz_description = isset( $_POST['quiz_description'] ) ? wp_kses( wp_unslash( $_POST['quiz_description'] ), $this->allowed_html ) : ''; //phpcs:ignore
785
786 $next_order_id = tutor_utils()->get_next_course_content_order_id( $topic_id, $ex_quiz_id );
787
788 // Check edit privilege.
789 if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
790 wp_send_json_error(
791 array(
792 'message' => __( 'Access Denied', 'tutor' ),
793 'data' => array(),
794 )
795 );
796 }
797
798 // Prepare quiz data to save in database.
799 $post_arr = array(
800 'ID' => $ex_quiz_id,
801 'post_type' => 'tutor_quiz',
802 'post_title' => $quiz_title,
803 'post_content' => $quiz_description,
804 'post_status' => 'publish',
805 'post_author' => get_current_user_id(),
806 'post_parent' => $topic_id,
807 'menu_order' => $next_order_id,
808 );
809
810 // Insert quiz and run hook.
811 $quiz_id = wp_insert_post( $post_arr );
812 do_action( ( $ex_quiz_id ? 'tutor_quiz_updated' : 'tutor_initial_quiz_created' ), $quiz_id );
813
814 // Sanitize by helper method & save quiz settings.
815 $quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] ); //phpcs:ignore
816 update_post_meta( $quiz_id, 'tutor_quiz_option', $quiz_option );
817 do_action( 'tutor_quiz_settings_updated', $quiz_id );
818
819 // Generate quiz modal to show in modal.
820 $output = $this->tutor_load_quiz_builder_modal(
821 array(
822 'topic_id' => $topic_id,
823 'quiz_id' => $quiz_id,
824 ),
825 true
826 );
827
828 // Generate quiz list to show under topic as sub list.
829 ob_start();
830 tutor_load_template_from_custom_path(
831 tutor()->path . '/views/fragments/quiz-list-single.php',
832 array(
833 'quiz_id' => $quiz_id,
834 'topic_id' => $topic_id,
835 'quiz_title' => $quiz_title,
836 ),
837 false
838 );
839 $output_quiz_row = ob_get_clean();
840
841 wp_send_json_success(
842 array(
843 'output' => $output,
844 'output_quiz_row' => $output_quiz_row,
845 )
846 );
847 }
848
849 /**
850 * Delete quiz by id
851 *
852 * @since 1.0.0
853 *
854 * @return void
855 */
856 public function tutor_delete_quiz_by_id() {
857 tutor_utils()->checking_nonce();
858
859 global $wpdb;
860
861 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
862 $post = get_post( $quiz_id );
863
864 if ( ! tutils()->can_user_manage( 'quiz', $quiz_id ) ) {
865 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
866 }
867
868 if ( 'tutor_quiz' === $post->post_type ) {
869 do_action( 'tutor_delete_quiz_before', $quiz_id );
870
871 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $quiz_id ) );
872 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $quiz_id ) );
873
874 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $quiz_id ) );
875
876 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
877 $in_question_ids = QueryHelper::prepare_in_clause( $questions_ids );
878 $wpdb->query(
879 "DELETE
880 FROM {$wpdb->prefix}tutor_quiz_question_answers
881 WHERE belongs_question_id IN({$in_question_ids})
882 "
883 );
884 }
885
886 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ) );
887
888 wp_delete_post( $quiz_id, true );
889
890 do_action( 'tutor_delete_quiz_after', $quiz_id );
891
892 wp_send_json_success();
893 }
894
895 wp_send_json_error();
896 }
897
898 /**
899 * Load quiz Modal on add/edit click
900 *
901 * @since 1.0.0
902 *
903 * @param array $params params.
904 * @param boolean $return should return or not.
905 *
906 * @return mixed
907 */
908 public function tutor_load_quiz_builder_modal( $params = array(), $return = false ) {
909 tutor_utils()->checking_nonce();
910
911 //phpcs:ignore WordPress.Security.NonceVerification.Missing
912 $data = array_merge( $_POST, $params );
913 $quiz_id = isset( $data['quiz_id'] ) ? sanitize_text_field( $data['quiz_id'] ) : 0;
914 $topic_id = isset( $data['topic_id'] ) ? sanitize_text_field( $data['topic_id'] ) : 0;
915 $quiz = $quiz_id ? get_post( $quiz_id ) : null;
916 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
917
918 if ( $quiz_id && ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
919 wp_send_json_error( array( 'message' => __( 'Quiz Permission Denied', 'tutor' ) ) );
920 }
921
922 ob_start();
923 include tutor()->path . 'views/modal/edit_quiz.php';
924 $output = ob_get_clean();
925
926 if ( $return ) {
927 return $output;
928 }
929
930 wp_send_json_success( array( 'output' => $output ) );
931 }
932
933 /**
934 * Load quiz question form for quiz
935 *
936 * @since 1.0.0
937 *
938 * @return void
939 */
940 public function tutor_quiz_builder_get_question_form() {
941 tutor_utils()->checking_nonce();
942
943 global $wpdb;
944 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
945 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
946 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
947
948 // check if the user can manage the quiz.
949 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
950 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
951 }
952
953 // If question ID not provided, then create new before rendering the form.
954 if ( ! $question_id ) {
955 $next_question_id = QuizModel::quiz_next_question_id();
956 $next_question_order = QuizModel::quiz_next_question_order_id( $quiz_id );
957 $question_title = __( 'Question', 'tutor' ) . ' ' . $next_question_id;
958
959 $new_question_data = array(
960 'quiz_id' => $quiz_id,
961 'question_title' => $question_title,
962 'question_description' => '',
963 'question_type' => 'true_false',
964 'question_mark' => 1,
965 'question_settings' => maybe_serialize( array() ),
966 'question_order' => esc_sql( $next_question_order ),
967 );
968
969 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_questions', $new_question_data );
970 $question_id = $wpdb->insert_id;
971
972 // Add default true/false options for this question since it is by default true/false type.
973 $question_array = array(
974 $question_id => array(
975 'Question' => $question_title,
976 'question_type' => 'true_false',
977 'question_mark' => '1.00',
978 'question_description' => '',
979 ),
980 );
981
982 $answer_array = array(
983 $question_id => array(
984 'true_false' => true,
985 ),
986 );
987
988 $this->tutor_save_quiz_answer_options( $question_array, $answer_array, false );
989 }
990
991 // Now get all data by this question id.
992 $question = $wpdb->get_row(
993 $wpdb->prepare(
994 "SELECT * FROM {$wpdb->prefix}tutor_quiz_questions
995 WHERE question_id = %d ",
996 $question_id
997 )
998 );
999
1000 // Render the question form finally.
1001 ob_start();
1002 require tutor()->path . 'views/modal/question_form.php';
1003 $output = ob_get_clean();
1004
1005 wp_send_json_success( array( 'output' => $output ) );
1006 }
1007
1008 /**
1009 * Update quiz modal
1010 *
1011 * @since 1.0.0
1012 *
1013 * @return void
1014 */
1015 public function tutor_quiz_modal_update_question() {
1016 tutor_utils()->checking_nonce();
1017
1018 global $wpdb;
1019 // Sanitize $_POST below before using.
1020 $quiz_question_id = Input::post( 'tutor_quiz_question_id', 0, Input::TYPE_INT );
1021 if ( ! $quiz_question_id ) {
1022 wp_send_json_error( __( 'Invalid quiz question ID', 'tutor' ) );
1023 }
1024
1025 /**
1026 * Sanitize $_POST[tutor_quiz_question] data through array_walk
1027 * it will override & sanitize all the question data.
1028 *
1029 * @since 2.1.3
1030 */
1031 if ( isset( $_POST['tutor_quiz_question'][ $quiz_question_id ] ) ) {
1032 array_walk(
1033 $_POST['tutor_quiz_question'][ $quiz_question_id ], // phpcs:ignore
1034 function( $v, $k ) use ( $quiz_question_id ) {
1035 if ( 'question_description' === $k ) {
1036 $_POST['tutor_quiz_question'][ $quiz_question_id ][ $k ] = wp_kses_post( wp_unslash( $v ) );
1037 } else {
1038 $_POST['tutor_quiz_question'][ $quiz_question_id ][ $k ] = sanitize_text_field( wp_unslash( $v ) );
1039 }
1040 }
1041 );
1042 } else {
1043 wp_send_json_error( __( 'Invalid quiz question ID', 'tutor' ) );
1044 }
1045
1046 $question_data = wp_unslash( $_POST['tutor_quiz_question'] ); //phpcs:ignore
1047 $requires_answeres = array(
1048 'multiple_choice',
1049 'single_choice',
1050 'true_false',
1051 'fill_in_the_blank',
1052 'matching',
1053 'image_matching',
1054 'image_answering',
1055 'ordering',
1056 );
1057
1058 $need_correct = array(
1059 'multiple_choice',
1060 'single_choice',
1061 'true_false',
1062 );
1063
1064 foreach ( $question_data as $question_id => $question ) {
1065 // Make sure the quiz has answers.
1066 if ( isset( $question['question_type'] ) && in_array( $question['question_type'], $requires_answeres ) ) {
1067 $require_correct = in_array( $question['question_type'], $need_correct );
1068 $all_answers = $this->get_answers_by_q_id( $question_id, $question['question_type'] );
1069 $correct_answers = $this->get_answers_by_q_id( $question_id, $question['question_type'], $require_correct );
1070
1071 if ( ! empty( $all_answers ) && empty( $correct_answers ) ) {
1072 wp_send_json_error( array( 'message' => __( 'Please make sure the question has answer' ) ) );
1073 exit;
1074 }
1075 }
1076
1077 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1078 continue;
1079 }
1080 // Data already sanitize above.
1081 $question_title = $question['question_title'] ?? '';
1082 $question_description = $question['question_description'];
1083 $question_type = $question['question_type'] ?? '';
1084 $question_mark = $question['question_mark'] ?? '';
1085
1086 unset( $question['question_title'] );
1087 unset( $question['question_description'] );
1088
1089 $data = array(
1090 'question_title' => $question_title,
1091 'question_description' => $question_description,
1092 'question_type' => $question_type,
1093 'question_mark' => $question_mark,
1094 'question_settings' => maybe_serialize( $question ),
1095 );
1096
1097 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', $data, array( 'question_id' => $question_id ) );
1098 }
1099
1100 wp_send_json_success();
1101 }
1102
1103 /**
1104 * Delete quiz questions
1105 *
1106 * @since 1.0.0
1107 *
1108 * @return void
1109 */
1110 public function tutor_quiz_builder_question_delete() {
1111 tutor_utils()->checking_nonce();
1112
1113 global $wpdb;
1114
1115 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1116
1117 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1118 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1119 }
1120
1121 if ( $question_id ) {
1122 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_id' => esc_sql( $question_id ) ) );
1123 }
1124
1125 wp_send_json_success();
1126 }
1127
1128 /**
1129 * Get answers options form for quiz question
1130 *
1131 * @since 1.0.0
1132 *
1133 * @return void send wp_json response
1134 */
1135 public function tutor_quiz_question_answer_editor() {
1136 tutor_utils()->checking_nonce();
1137
1138 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1139 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1140 $quiz_option = isset( $_POST['tutor_quiz_question'] ) ? tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ) : array(); //phpcs:ignore
1141 $question = tutor_utils()->avalue_dot( $question_id, $quiz_option );
1142 $question_type = $question['question_type'];
1143
1144 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1145 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1146 }
1147
1148 if ( $answer_id ) {
1149 $old_answer = tutor_utils()->get_answer_by_id( $answer_id );
1150 //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedForeach
1151 foreach ( $old_answer as $old_answer ) {
1152 }
1153 }
1154
1155 ob_start();
1156 include tutor()->path . 'views/modal/question_answer_form.php';
1157 $output = ob_get_clean();
1158
1159 wp_send_json_success( array( 'output' => $output ) );
1160 }
1161
1162 /**
1163 * Undocumented function
1164 *
1165 * @since 1.0.0
1166 *
1167 * @param mixed $questions questions.
1168 * @param mixed $answers answers.
1169 * @param boolean $response should send json response.
1170 *
1171 * @return void
1172 */
1173 public function tutor_save_quiz_answer_options( $questions = null, $answers = null, $response = true ) {
1174 tutor_utils()->checking_nonce();
1175
1176 global $wpdb;
1177 $questions = $questions ? $questions : tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1178 $answers = $answers ? $answers : tutor_utils()->sanitize_array( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1179
1180 foreach ( $answers as $question_id => $answer ) {
1181 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1182 continue;
1183 }
1184
1185 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1186 $question_type = $question['question_type'];
1187
1188 // Getting next sorting order.
1189 $next_order_id = (int) $wpdb->get_var(
1190 $wpdb->prepare(
1191 "SELECT MAX(answer_order)
1192 FROM {$wpdb->prefix}tutor_quiz_question_answers
1193 WHERE belongs_question_id = %d
1194 AND belongs_question_type = %s
1195 ",
1196 $question_id,
1197 esc_sql( $question_type )
1198 )
1199 );
1200
1201 //phpcs:ignore Squiz.Operators.IncrementDecrementUsage.Found
1202 $next_order_id = $next_order_id + 1;
1203
1204 if ( $question ) {
1205 if ( 'true_false' === $question_type ) {
1206 $wpdb->delete(
1207 $wpdb->prefix . 'tutor_quiz_question_answers',
1208 array(
1209 'belongs_question_id' => $question_id,
1210 'belongs_question_type' => $question_type,
1211 )
1212 );
1213 $data_true_false = array(
1214 array(
1215 'belongs_question_id' => esc_sql( $question_id ),
1216 'belongs_question_type' => $question_type,
1217 'answer_title' => __( 'True', 'tutor' ),
1218 'is_correct' => 'true' == $answer['true_false'] ? 1 : 0,
1219 'answer_two_gap_match' => 'true',
1220 ),
1221 array(
1222 'belongs_question_id' => esc_sql( $question_id ),
1223 'belongs_question_type' => $question_type,
1224 'answer_title' => __( 'False', 'tutor' ),
1225 'is_correct' => 'false' === $answer['true_false'] ? 1 : 0,
1226 'answer_two_gap_match' => 'false',
1227 ),
1228 );
1229
1230 foreach ( $data_true_false as $true_false_data ) {
1231 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $true_false_data );
1232 }
1233 } elseif ( 'multiple_choice' === $question_type ||
1234 'single_choice' === $question_type ||
1235 'ordering' === $question_type ||
1236 'matching' === $question_type ||
1237 'image_matching' === $question_type ||
1238 'image_answering' === $question_type ) {
1239
1240 $answer_data = array(
1241 'belongs_question_id' => sanitize_text_field( $question_id ),
1242 'belongs_question_type' => $question_type,
1243 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1244 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1245 'answer_view_format' => isset( $answer['answer_view_format'] ) ? $answer['answer_view_format'] : 0,
1246 'answer_order' => $next_order_id,
1247 );
1248 if ( isset( $answer['matched_answer_title'] ) ) {
1249 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1250 }
1251
1252 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1253
1254 } elseif ( 'fill_in_the_blank' === $question_type ) {
1255 $wpdb->delete(
1256 $wpdb->prefix . 'tutor_quiz_question_answers',
1257 array(
1258 'belongs_question_id' => $question_id,
1259 'belongs_question_type' => $question_type,
1260 )
1261 );
1262 $answer_data = array(
1263 'belongs_question_id' => sanitize_text_field( $question_id ),
1264 'belongs_question_type' => $question_type,
1265 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1266 'answer_two_gap_match' => isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null,
1267 );
1268 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1269 }
1270 }
1271 }
1272
1273 // Send response to browser if not internal call.
1274 if ( $response ) {
1275 wp_send_json_success();
1276 exit;
1277 }
1278 }
1279
1280 /**
1281 * Tutor Update Answer
1282 *
1283 * @since 1.0.0
1284 *
1285 * @return void send wp_json response
1286 */
1287 public function tutor_update_quiz_answer_options() {
1288 tutor_utils()->checking_nonce();
1289
1290 global $wpdb;
1291
1292 $answer_id = Input::post( 'tutor_quiz_answer_id', 0, Input::TYPE_INT );
1293
1294 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1295 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1296 }
1297
1298 // Data sanitizing by helper method.
1299 $questions = tutor_sanitize_data( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1300 $answers = tutor_sanitize_data( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1301
1302 foreach ( $answers as $question_id => $answer ) {
1303 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1304 $question_type = $question['question_type'];
1305
1306 if ( $question ) {
1307 if ( 'multiple_choice' === $question_type ||
1308 'single_choice' === $question_type ||
1309 'ordering' === $question_type ||
1310 'matching' === $question_type ||
1311 'image_matching' === $question_type ||
1312 'fill_in_the_blank' === $question_type ||
1313 'image_answering' === $question_type ) {
1314
1315 $answer_data = array(
1316 'belongs_question_id' => $question_id,
1317 'belongs_question_type' => $question_type,
1318 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1319 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1320 'answer_view_format' => isset( $answer['answer_view_format'] ) ? sanitize_text_field( $answer['answer_view_format'] ) : '',
1321 );
1322 if ( isset( $answer['matched_answer_title'] ) ) {
1323 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1324 }
1325
1326 if ( 'fill_in_the_blank' === $question_type ) {
1327 $answer_data['answer_two_gap_match'] = isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null;
1328 }
1329
1330 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data, array( 'answer_id' => $answer_id ) );
1331 }
1332 }
1333 }
1334 wp_send_json_success();
1335 }
1336
1337 /**
1338 * Get answers by quiz id
1339 *
1340 * @since 1.0.0
1341 *
1342 * @param int $question_id question id.
1343 * @param mixed $question_type type of question.
1344 * @param boolean $is_correct only correct answers or not.
1345 *
1346 * @return wpdb:get_results
1347 */
1348 private function get_answers_by_q_id( $question_id, $question_type, $is_correct = false ) {
1349 global $wpdb;
1350
1351 $correct_clause = $is_correct ? ' AND is_correct=1 ' : '';
1352
1353 return $wpdb->get_results(
1354 $wpdb->prepare(
1355 "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers
1356 WHERE belongs_question_id = %d
1357 AND belongs_question_type = %s
1358 {$correct_clause}
1359 ORDER BY answer_order ASC;
1360 ",
1361 $question_id,
1362 esc_sql( $question_type )
1363 )
1364 );
1365 }
1366
1367 /**
1368 * Quiz builder changed type
1369 *
1370 * @since 1.0.0
1371 *
1372 * @return void send wp_json response
1373 */
1374 public function tutor_quiz_builder_change_type() {
1375 tutor_utils()->checking_nonce();
1376
1377 global $wpdb;
1378 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1379 $question_type = Input::post( 'question_type' );
1380
1381 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1382 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1383 }
1384
1385 // Get question data by question ID.
1386 $question = $wpdb->get_row(
1387 $wpdb->prepare(
1388 "SELECT *
1389 FROM {$wpdb->prefix}tutor_quiz_questions
1390 WHERE question_id = %d
1391 ",
1392 $question_id
1393 )
1394 );
1395
1396 // Get answers by question ID.
1397 $answers = $this->get_answers_by_q_id( $question_id, $question_type );
1398
1399 ob_start();
1400 require tutor()->path . '/views/modal/question_answer_list.php';
1401 $output = ob_get_clean();
1402
1403 wp_send_json_success( array( 'output' => $output ) );
1404 }
1405
1406 /**
1407 * Delete quiz question's answer
1408 *
1409 * @since 1.0.0
1410 *
1411 * @return void send wp_json response
1412 */
1413 public function tutor_quiz_builder_delete_answer() {
1414 tutor_utils()->checking_nonce();
1415
1416 global $wpdb;
1417 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1418
1419 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1420 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1421 }
1422
1423 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_id' => esc_sql( $answer_id ) ) );
1424 wp_send_json_success();
1425 }
1426
1427 /**
1428 * Save quiz questions sorting
1429 *
1430 * @since 1.0.0
1431 *
1432 * @return void
1433 */
1434 public function tutor_quiz_question_sorting() {
1435 tutor_utils()->checking_nonce();
1436
1437 global $wpdb;
1438
1439 // Data sanitizing by helper method.
1440 $question_ids = tutor_utils()->avalue_dot( 'sorted_question_ids', tutor_sanitize_data( $_POST ) ); //phpcs:ignore
1441 if ( is_array( $question_ids ) && count( $question_ids ) ) {
1442 $i = 0;
1443 foreach ( $question_ids as $key => $question_id ) {
1444 if ( tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1445 $i++;
1446 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_order' => $i ), array( 'question_id' => $question_id ) );
1447 }
1448 }
1449 }
1450 }
1451
1452 /**
1453 * Save sorting data for quiz answers
1454 *
1455 * @since 1.0.0
1456 *
1457 * @return void
1458 */
1459 public function tutor_quiz_answer_sorting() {
1460 tutor_utils()->checking_nonce();
1461
1462 global $wpdb;
1463 $answer_ids = Input::post( 'sorted_answer_ids', array(), Input::TYPE_ARRAY );
1464 if ( count( $answer_ids ) ) {
1465 $i = 0;
1466 foreach ( $answer_ids as $key => $answer_id ) {
1467 if ( tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1468 $i++;
1469 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_order' => $i ), array( 'answer_id' => $answer_id ) );
1470 }
1471 }
1472 }
1473 }
1474
1475 /**
1476 * Mark answer as correct
1477 *
1478 * @since 1.0.0
1479 *
1480 * @return void
1481 */
1482 public function tutor_mark_answer_as_correct() {
1483 tutor_utils()->checking_nonce();
1484
1485 global $wpdb;
1486
1487 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1488 // get question info.
1489 $belong_question = $wpdb->get_row(
1490 $wpdb->prepare(
1491 " SELECT belongs_question_id, belongs_question_type
1492 FROM {$wpdb->tutor_quiz_question_answers}
1493 WHERE answer_id = %d
1494 LIMIT 1
1495 ",
1496 $answer_id
1497 )
1498 );
1499 if ( $belong_question ) {
1500 // if question found update all answer is_correct to 0 except post answer.
1501 $question_type = $belong_question->belongs_question_type;
1502 $question_id = $belong_question->belongs_question_id;
1503 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
1504 $update = $wpdb->query(
1505 $wpdb->prepare(
1506 "UPDATE {$wpdb->tutor_quiz_question_answers}
1507 SET is_correct = 0
1508 WHERE belongs_question_id = %d
1509 AND answer_id != %d
1510 ",
1511 $question_id,
1512 $answer_id
1513 )
1514 );
1515 }
1516 }
1517
1518 $input_value = Input::post( 'inputValue', '' );
1519
1520 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1521 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1522 }
1523
1524 $answer = $wpdb->get_row(
1525 $wpdb->prepare(
1526 "SELECT *
1527 FROM {$wpdb->prefix}tutor_quiz_question_answers
1528 WHERE answer_id = %d
1529 LIMIT 0,1 ;
1530 ",
1531 $answer_id
1532 )
1533 );
1534 if ( 'single_choice' === $answer->belongs_question_type ) {
1535 $wpdb->update(
1536 $wpdb->prefix . 'tutor_quiz_question_answers',
1537 array( 'is_correct' => 0 ),
1538 array( 'belongs_question_id' => esc_sql( $answer->belongs_question_id ) )
1539 );
1540 }
1541 $wpdb->update(
1542 $wpdb->prefix . 'tutor_quiz_question_answers',
1543 array( 'is_correct' => esc_sql( $input_value ) ),
1544 array( 'answer_id' => esc_sql( $answer_id ) )
1545 );
1546 }
1547
1548 /**
1549 * Rendering quiz for frontend
1550 *
1551 * @since 1.0.0
1552 *
1553 * @return void send wp_json response
1554 */
1555 public function tutor_render_quiz_content() {
1556
1557 tutor_utils()->checking_nonce();
1558
1559 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1560
1561 if ( ! tutor_utils()->has_enrolled_content_access( 'quiz', $quiz_id ) ) {
1562 wp_send_json_error( array( 'message' => __( 'Access Denied.', 'tutor' ) ) );
1563 }
1564
1565 ob_start();
1566 global $post;
1567
1568 $post = get_post( $quiz_id );
1569 setup_postdata( $post );
1570
1571 single_quiz_contents();
1572 wp_reset_postdata();
1573
1574 $html = ob_get_clean();
1575 wp_send_json_success( array( 'html' => $html ) );
1576 }
1577
1578 /**
1579 * Get attempt details
1580 *
1581 * @since 1.0.0
1582 *
1583 * @param int $attempt_id required attempt id to get details.
1584 *
1585 * @return mixed object on success, null on failure
1586 */
1587 public static function attempt_details( int $attempt_id ) {
1588 global $wpdb;
1589 $attempt_details = $wpdb->get_row(
1590 $wpdb->prepare(
1591 "SELECT *
1592 FROM {$wpdb->prefix}tutor_quiz_attempts
1593 WHERE attempt_id = %d
1594 ",
1595 $attempt_id
1596 )
1597 );
1598 return $attempt_details;
1599 }
1600
1601 /**
1602 * Update quiz attempt info
1603 *
1604 * @since 1.0.0
1605 *
1606 * @param int $attempt_id attempt id.
1607 * @param mixed $attempt_info serialize data.
1608 *
1609 * @return bool, true on success, false on failure
1610 */
1611 public static function update_attempt_info( int $attempt_id, $attempt_info ) {
1612 global $wpdb;
1613 $table = $wpdb->prefix . 'tutor_quiz_attempts';
1614 $update_info = $wpdb->update(
1615 $table,
1616 array( 'attempt_info' => $attempt_info ),
1617 array( 'attempt_id' => $attempt_id )
1618 );
1619 return $update_info ? true : false;
1620 }
1621
1622 /**
1623 * Attempt delete ajax request handler
1624 *
1625 * @since 2.1.0
1626 *
1627 * @return void wp_json response
1628 */
1629 public function attempt_delete() {
1630 tutor_utils()->checking_nonce();
1631 if ( current_user_can( 'administrator' ) || current_user_can( tutor()->instructor_role ) ) {
1632 $attempt_id = Input::post( 'id', 0, Input::TYPE_INT );
1633 if ( $attempt_id ) {
1634 QuizModel::delete_quiz_attempt( $attempt_id );
1635 wp_send_json_success( __( 'Attempt deleted successfully!', 'tutor' ) );
1636 } else {
1637 wp_send_json_error( __( 'Invalid attempt ID', 'tutor' ) );
1638 }
1639 } else {
1640 wp_send_json_error( __( 'You are not authorized to perform this action!', 'tutor' ) );
1641 }
1642 }
1643
1644 }
1645