PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 2.1.1
Tutor LMS – eLearning and online course solution v2.1.1
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 4 years ago Custom_Validation.php 4 years ago Dashboard.php 3 years ago FormHandler.php 4 years ago Frontend.php 3 years ago Gutenberg.php 3 years ago Input.php 3 years ago Instructor.php 4 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 4 years ago Q_and_A.php 3 years ago Question_Answers_List.php 4 years ago Quiz.php 3 years ago Quiz_Attempts_List.php 3 years ago RestAPI.php 4 years ago Reviews.php 3 years ago Rewrite_Rules.php 4 years ago Shortcode.php 4 years ago Student.php 4 years ago Students_List.php 4 years ago Taxonomies.php 4 years ago Template.php 3 years ago Theme_Compatibility.php 5 years ago Tools.php 3 years ago Tools_V2.php 4 years ago Tutor.php 3 years ago TutorEDD.php 4 years ago Tutor_Base.php 5 years ago Tutor_List_Table.php 3 years ago Tutor_Setup.php 3 years ago Upgrader.php 4 years ago User.php 4 years ago Utils.php 3 years ago Video_Stream.php 4 years ago Withdraw.php 3 years ago Withdraw_Requests_List.php 3 years ago WooCommerce.php 3 years ago
Quiz.php
1382 lines
1 <?php
2
3 /**
4 * Quiz class
5 *
6 * @author: themeum
7 * @author_uri: https://themeum.com
8 * @package Tutor
9 * @since v.1.0.0
10 */
11
12 namespace TUTOR;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 use Tutor\Models\QuizModel;
19 class Quiz {
20
21 private $allowed_attributes = array(
22 'src' => array(),
23 'style' => array(),
24 'class' => array(),
25 'id' => array(),
26 'href' => array(),
27 'alt' => array(),
28 'title' => array(),
29 'type' => array(),
30 'controls' => array(),
31 'muted' => array(),
32 'loop' => array(),
33 'poster' => array(),
34 'preload' => array(),
35 'autoplay' => array(),
36 'width' => array(),
37 'height' => array(),
38 );
39
40 private $allowed_html = array( 'img', 'b', 'i', 'br', 'a', 'audio', 'video', 'source' );
41
42 public function __construct() {
43
44 add_action('save_post_tutor_quiz', array($this, 'save_quiz_meta'));
45 add_action('wp_ajax_remove_quiz_from_post', array($this, 'remove_quiz_from_post'));
46
47 add_action( 'wp_ajax_tutor_quiz_timeout', array( $this, 'tutor_quiz_timeout' ) );
48
49 // User take the quiz
50 add_action( 'template_redirect', array( $this, 'start_the_quiz' ) );
51 add_action( 'template_redirect', array( $this, 'answering_quiz' ) );
52 add_action( 'template_redirect', array( $this, 'finishing_quiz_attempt' ) );
53
54 add_action('wp_ajax_review_quiz_answer', array($this, 'review_quiz_answer'));
55 add_action('wp_ajax_tutor_instructor_feedback', array($this, 'tutor_instructor_feedback')); // Instructor Feedback Action
56
57 /**
58 * New Design Quiz
59 */
60
61 add_action('wp_ajax_tutor_quiz_save', array($this, 'tutor_quiz_save'));
62 add_action('wp_ajax_tutor_delete_quiz_by_id', array($this, 'tutor_delete_quiz_by_id'));
63 add_action('wp_ajax_tutor_load_quiz_builder_modal', array($this, 'tutor_load_quiz_builder_modal'), 10, 0);
64 add_action('wp_ajax_tutor_quiz_builder_get_question_form', array($this, 'tutor_quiz_builder_get_question_form'));
65 add_action('wp_ajax_tutor_quiz_modal_update_question', array($this, 'tutor_quiz_modal_update_question'));
66 add_action('wp_ajax_tutor_quiz_builder_question_delete', array($this, 'tutor_quiz_builder_question_delete'));
67 add_action('wp_ajax_tutor_quiz_question_answer_editor', array($this, 'tutor_quiz_question_answer_editor'));
68 add_action('wp_ajax_tutor_save_quiz_answer_options', array($this, 'tutor_save_quiz_answer_options'), 10, 0);
69 add_action('wp_ajax_tutor_update_quiz_answer_options', array($this, 'tutor_update_quiz_answer_options'));
70 add_action('wp_ajax_tutor_quiz_builder_change_type', array($this, 'tutor_quiz_builder_change_type'));
71 add_action('wp_ajax_tutor_quiz_builder_delete_answer', array($this, 'tutor_quiz_builder_delete_answer'));
72 add_action('wp_ajax_tutor_quiz_question_sorting', array($this, 'tutor_quiz_question_sorting'));
73 add_action('wp_ajax_tutor_quiz_answer_sorting', array($this, 'tutor_quiz_answer_sorting'));
74 add_action('wp_ajax_tutor_mark_answer_as_correct', array($this, 'tutor_mark_answer_as_correct'));
75
76 /**
77 * Frontend Stuff
78 */
79 add_action( 'wp_ajax_tutor_render_quiz_content', array( $this, 'tutor_render_quiz_content' ) );
80
81 /**
82 * Quiz abandon action
83 *
84 * @since 1.9.6
85 */
86 add_action( 'wp_ajax_tutor_quiz_abandon', array( $this, 'tutor_quiz_abandon' ) );
87
88 $this->prepare_allowed_html();
89
90 /**
91 * Delete quiz attempt
92 *
93 * @since v2.1.0
94 */
95 add_action( 'wp_ajax_tutor_attempt_delete', array( $this, 'attempt_delete' ) );
96 }
97
98 private function prepare_allowed_html() {
99
100 $allowed = array();
101
102 foreach ( $this->allowed_html as $tag ) {
103 $allowed[ $tag ] = $this->allowed_attributes;
104 }
105
106 $this->allowed_html = $allowed;
107 }
108
109 /**
110 * Instructor feedback ajax request handler
111 *
112 * @return void | send json response
113 */
114 public function tutor_instructor_feedback() {
115 tutor_utils()->checking_nonce();
116 $attempt_details = self::attempt_details( Input::post( 'attempt_id', 0, Input::TYPE_INT ) );
117 $feedback = wp_kses_post( $_POST['feedback'] );
118 $attempt_info = isset( $attempt_details->attempt_info ) ? $attempt_details->attempt_info : false;
119 if ( $attempt_info ) {
120 $unserialized = unserialize( $attempt_details->attempt_info );
121 if ( is_array( $unserialized ) ) {
122 $unserialized['instructor_feedback'] = $feedback;
123
124 do_action( 'tutor_quiz/attempt/submitted/feedback', $attempt_details->attempt_id );
125
126 $update = self::update_attempt_info( $attempt_details->attempt_id, serialize( $unserialized ) );
127 if ( $update ) {
128 wp_send_json_success();
129 } else {
130 wp_send_json_error();
131 }
132 } else {
133 wp_send_json_error( __( 'Invalid quiz info' ) );
134 }
135 }
136 wp_send_json_error();
137 }
138
139 public function save_quiz_meta( $post_ID ) {
140 if ( isset( $_POST['quiz_option'] ) ) {
141 $quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] );
142 update_post_meta( $post_ID, 'tutor_quiz_option', $quiz_option );
143 }
144 }
145
146 public function remove_quiz_from_post(){
147 tutor_utils()->checking_nonce();
148
149 global $wpdb;
150 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
151
152 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
153 wp_send_json_error( array( 'message'=> __( 'Access Denied', 'tutor' ) ) );
154 }
155
156 $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'ID' => $quiz_id ) );
157 wp_send_json_success();
158 }
159
160 /**
161 *
162 * Start Quiz from here...
163 *
164 * @since v.1.0.0
165 */
166
167 public function start_the_quiz() {
168 if ( Input::post( 'tutor_action' ) !== 'tutor_start_quiz' ) {
169 return;
170 }
171 // Checking nonce
172 tutor_utils()->checking_nonce();
173
174 if ( ! is_user_logged_in() ) {
175 // TODO: need to set a view in the next version
176 die( 'Please sign in to do this operation' );
177 }
178
179 global $wpdb;
180
181 $user_id = get_current_user_id();
182 $user = get_userdata( $user_id );
183
184 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
185
186 $quiz = get_post( $quiz_id );
187 $course = tutor_utils()->get_course_by_quiz( $quiz_id );
188 if ( empty( $course->ID ) ) {
189 die( 'There is something went wrong with course, please check if quiz attached with a course' );
190 }
191
192 do_action( 'tutor_quiz/start/before', $quiz_id, $user_id );
193
194 $date = date( 'Y-m-d H:i:s', tutor_time() );
195
196 $tutor_quiz_option = (array) maybe_unserialize( get_post_meta( $quiz_id, 'tutor_quiz_option', true ) );
197 $attempts_allowed = tutor_utils()->get_quiz_option( $quiz_id, 'attempts_allowed', 0 );
198
199 $time_limit = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_value' );
200 $time_limit_seconds = 0;
201 $time_type = 'seconds';
202 if ( $time_limit ) {
203 $time_type = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_type' );
204
205 switch ( $time_type ) {
206 case 'seconds':
207 $time_limit_seconds = $time_limit;
208 break;
209 case 'minutes':
210 $time_limit_seconds = $time_limit * 60;
211 break;
212 case 'hours':
213 $time_limit_seconds = $time_limit * 60 * 60;
214 break;
215 case 'days':
216 $time_limit_seconds = $time_limit * 60 * 60 * 24;
217 break;
218 case 'weeks':
219 $time_limit_seconds = $time_limit * 60 * 60 * 24 * 7;
220 break;
221 }
222 }
223
224 $max_question_allowed = tutor_utils()->max_questions_for_take_quiz( $quiz_id );
225 $tutor_quiz_option['time_limit']['time_limit_seconds'] = $time_limit_seconds;
226
227 $attempt_data = array(
228 'course_id' => $course->ID,
229 'quiz_id' => $quiz_id,
230 'user_id' => $user_id,
231 'total_questions' => $max_question_allowed,
232 'total_answered_questions' => 0,
233 'attempt_info' => maybe_serialize( $tutor_quiz_option ),
234 'attempt_status' => 'attempt_started',
235 'attempt_ip' => tutor_utils()->get_ip(),
236 'attempt_started_at' => $date,
237 );
238
239 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_data );
240 $attempt_id = (int) $wpdb->insert_id;
241
242 do_action( 'tutor_quiz/start/after', $quiz_id, $user_id, $attempt_id );
243
244 wp_redirect( get_permalink( $quiz_id ) );
245 die();
246 }
247
248 public function answering_quiz() {
249
250 if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ){
251 return;
252 }
253 // submit quiz attempts
254 self::tutor_quiz_attemp_submit();
255
256 wp_redirect( get_the_permalink() );
257 die();
258 }
259
260 /**
261 * Quiz abandon submission handler
262 *
263 * @return JSON response
264 *
265 * @since 1.9.6
266 */
267 public function tutor_quiz_abandon(){
268 if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ){
269 return;
270 }
271 // submit quiz attempts
272 if ( self::tutor_quiz_attemp_submit() ) {
273 wp_send_json_success();
274 } else {
275 wp_send_json_error();
276 }
277 }
278
279 /**
280 * This is a unified method for handling normal quiz submit or abandon submit
281 *
282 * It will handle ajax or normal form submit and can be used with different hooks
283 *
284 * @return true | false
285 *
286 * @since 1.9.6
287 */
288 public static function tutor_quiz_attemp_submit() {
289
290 // Check logged in
291 if ( ! is_user_logged_in() ) {
292 die( 'Please sign in to do this operation' );
293 }
294
295 // Check nonce
296 tutor_utils()->checking_nonce();
297
298 // Prepare attempt info
299 global $wpdb;
300 $user_id = get_current_user_id();
301 $attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
302 $attempt = tutor_utils()->get_attempt( $attempt_id );
303 $course_id = tutor_utils()->get_course_by_quiz( $attempt->quiz_id )->ID;
304 $attempt_answers = isset( $_POST['attempt'] ) ? tutor_sanitize_data( $_POST['attempt'] ) : false;
305 $attempt_answers = is_array($attempt_answers) ? $attempt_answers : array();
306
307 // Check if has access to the attempt
308 if ( ! $attempt || $user_id != $attempt->user_id ) {
309 die( 'Operation not allowed, attempt not found or permission denied' );
310 }
311
312 // Before ook
313 do_action( 'tutor_quiz/attempt_analysing/before', $attempt_id );
314
315 // Loop through every single attempt answer
316 // Single quiz can have multiple question. So multiple answer should be saved
317 foreach ( $attempt_answers as $attempt_id => $attempt_answer ) {
318
319 /**
320 * Get total marks of all question comes
321 */
322 $question_ids = tutor_utils()->avalue_dot( 'quiz_question_ids', $attempt_answer );
323 $question_ids = array_filter($question_ids, function($id){
324 return (int)$id;
325 });
326
327 // Calculate and set the total marks in attempt table for this question
328 if ( is_array( $question_ids ) && count( $question_ids ) ) {
329 $question_ids_string = implode(',', $question_ids );
330
331 // Get total marks of the questions from question table
332 $total_question_marks = $wpdb->get_var(
333 "SELECT SUM(question_mark)
334 FROM {$wpdb->prefix}tutor_quiz_questions
335 WHERE question_id IN({$question_ids_string});"
336 );
337
338 // Set the the total mark in the attempt table for the question
339 $wpdb->update(
340 $wpdb->prefix . 'tutor_quiz_attempts',
341 array( 'total_marks' => $total_question_marks ),
342 array( 'attempt_id' => $attempt_id )
343 );
344 }
345
346 $total_marks = 0;
347 $review_required = false;
348 $quiz_answers = tutor_utils()->avalue_dot( 'quiz_question', $attempt_answer );
349
350 if ( tutor_utils()->count($quiz_answers)) {
351
352 foreach ( $quiz_answers as $question_id => $answers ) {
353 $question = QuizModel::get_quiz_question_by_id( $question_id );
354 $question_type = $question->question_type;
355
356 $is_answer_was_correct = false;
357 $given_answer = '';
358
359 if ( $question_type === 'true_false' || $question_type === 'single_choice' ) {
360
361 if ( ! is_numeric( $answers ) || ! $answers ) {
362 wp_send_json_error();
363 exit;
364 }
365
366 $given_answer = $answers;
367 $is_answer_was_correct = (bool) $wpdb->get_var( $wpdb->prepare(
368 "SELECT is_correct
369 FROM {$wpdb->prefix}tutor_quiz_question_answers
370 WHERE answer_id = %d ",
371 $answers
372 ) );
373
374 } elseif ( $question_type === 'multiple_choice' ) {
375
376 $given_answer = (array) ( $answers );
377
378 $given_answer = array_filter( $given_answer, function($id) {
379 return is_numeric($id) && $id>0;
380 } );
381 $get_original_answers = (array) $wpdb->get_col($wpdb->prepare(
382 "SELECT
383 answer_id
384 FROM
385 {$wpdb->prefix}tutor_quiz_question_answers
386 WHERE belongs_question_id = %d
387 AND belongs_question_type = %s
388 AND is_correct = 1 ;
389 ",
390 $question->question_id,
391 $question_type
392 )
393 );
394
395 if ( count( array_diff( $get_original_answers, $given_answer ) ) === 0 && count( $get_original_answers ) === count( $given_answer ) ) {
396 $is_answer_was_correct = true;
397 }
398 $given_answer = maybe_serialize( $answers );
399
400 } elseif ( $question_type === 'fill_in_the_blank' ) {
401
402 $given_answer = (array) array_map( 'sanitize_text_field', $answers );
403 $given_answer = maybe_serialize( $given_answer );
404
405 $get_original_answer = $wpdb->get_row( $wpdb->prepare(
406 "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers
407 WHERE belongs_question_id = %d
408 AND belongs_question_type = %s ;",
409 $question->question_id,
410 $question_type
411 ) );
412
413 $gap_answer = (array) explode( '|', $get_original_answer->answer_two_gap_match );
414
415 $gap_answer = array_map( 'sanitize_text_field', $gap_answer );
416 if ( strtolower( $given_answer ) == strtolower( maybe_serialize( $gap_answer ) ) ) {
417 $is_answer_was_correct = true;
418 }
419 } elseif ( $question_type === 'open_ended' || $question_type === 'short_answer' ) {
420 $review_required = true;
421 $given_answer = wp_kses_post( $answers );
422
423 } elseif ( $question_type === 'ordering' || $question_type === 'matching' || $question_type === 'image_matching' ) {
424
425 $given_answer = (array) array_map( 'sanitize_text_field', tutor_utils()->avalue_dot( 'answers', $answers ) );
426 $given_answer = maybe_serialize( $given_answer );
427
428 $get_original_answers = (array) $wpdb->get_col(
429 $wpdb->prepare(
430 "SELECT answer_id
431 FROM {$wpdb->prefix}tutor_quiz_question_answers
432 WHERE belongs_question_id = %d AND belongs_question_type = %s ORDER BY answer_order ASC ;",
433 $question->question_id,
434 $question_type
435 )
436 );
437
438 $get_original_answers = array_map( 'sanitize_text_field', $get_original_answers );
439
440 if ( $given_answer == maybe_serialize( $get_original_answers ) ) {
441 $is_answer_was_correct = true;
442 }
443 } elseif ( $question_type === 'image_answering' ) {
444 $image_inputs = tutor_utils()->avalue_dot( 'answer_id', $answers );
445 $image_inputs = (array) array_map( 'sanitize_text_field', $image_inputs );
446 $given_answer = maybe_serialize( $image_inputs );
447 $is_answer_was_correct = false;
448
449 $db_answer = $wpdb->get_col(
450 $wpdb->prepare(
451 "SELECT answer_title
452 FROM {$wpdb->prefix}tutor_quiz_question_answers
453 WHERE belongs_question_id = %d
454 AND belongs_question_type = 'image_answering'
455 ORDER BY answer_order asc ;",
456 $question_id
457 )
458 );
459
460 if ( is_array( $db_answer ) && count( $db_answer ) ) {
461 $is_answer_was_correct = ( strtolower( maybe_serialize( array_values( $image_inputs ) ) ) == strtolower( maybe_serialize( $db_answer ) ) );
462 }
463 }
464
465 $question_mark = $is_answer_was_correct ? $question->question_mark : 0;
466 $total_marks += $question_mark;
467
468 $answers_data = array(
469 'user_id' => $user_id,
470 'quiz_id' => $attempt->quiz_id,
471 'question_id' => $question_id,
472 'quiz_attempt_id' => $attempt_id,
473 'given_answer' => $given_answer,
474 'question_mark' => $question->question_mark,
475 'achieved_mark' => $question_mark,
476 'minus_mark' => 0,
477 'is_correct' => $is_answer_was_correct ? 1 : 0,
478 );
479
480 /*
481 check if question_type open ended or short ans the set is_correct default value null before saving
482 */
483 if ( in_array($question_type, array('open_ended', 'short_answer', 'image_answering'))) {
484 $answers_data['is_correct'] = null;
485 $review_required = true;
486 }
487
488 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answers_data );
489 }
490 }
491
492 $attempt_info = array(
493 'total_answered_questions' => tutor_utils()->count($quiz_answers),
494 'earned_marks' => $total_marks,
495 'attempt_status' => 'attempt_ended',
496 'attempt_ended_at' => date("Y-m-d H:i:s", tutor_time()),
497 );
498
499 if ($review_required){
500 $attempt_info['attempt_status'] = 'review_required';
501 }
502
503 $wpdb->update($wpdb->prefix.'tutor_quiz_attempts', $attempt_info, array('attempt_id' => $attempt_id));
504 }
505
506 // After hook
507 do_action('tutor_quiz/attempt_ended', $attempt_id, $course_id, $user_id);
508
509 return true;
510 }
511
512
513 /**
514 * Quiz attempt will be finish here
515 */
516
517 public function finishing_quiz_attempt() {
518
519 if ( Input::post( 'tutor_action' ) !== 'tutor_finish_quiz_attempt' ) {
520 return;
521 }
522 // Checking nonce
523 tutor_utils()->checking_nonce();
524
525 if ( ! is_user_logged_in() ) {
526 die( 'Please sign in to do this operation' );
527 }
528
529 global $wpdb;
530
531 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
532 $attempt = tutor_utils()->is_started_quiz( $quiz_id );
533 $attempt_id = $attempt->attempt_id;
534
535 $attempt_info = array(
536 'total_answered_questions' => 0,
537 'earned_marks' => 0,
538 'attempt_status' => 'attempt_ended',
539 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ),
540 );
541
542 do_action( 'tutor_quiz_before_finish', $attempt_id, $quiz_id, $attempt->user_id );
543 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
544 do_action( 'tutor_quiz_finished', $attempt_id, $quiz_id, $attempt->user_id );
545
546 wp_redirect( tutor_utils()->input_old( '_wp_http_referer' ) );
547 }
548
549 /**
550 * Quiz timeout by ajax
551 */
552 public function tutor_quiz_timeout() {
553 tutils()->checking_nonce();
554
555 global $wpdb;
556
557 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
558 $attempt = tutor_utils()->is_started_quiz( $quiz_id );
559
560 if ( $attempt ) {
561 $attempt_id = $attempt->attempt_id;
562
563 $data = array(
564 'attempt_status' => 'attempt_timeout',
565 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ),
566 );
567 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $data, array( 'attempt_id' => $attempt->attempt_id ) );
568
569 do_action( 'tutor_quiz_timeout', $attempt_id, $quiz_id, $attempt->user_id );
570
571 wp_send_json_success();
572 }
573
574 wp_send_json_error( __( 'Quiz has been timeout already', 'tutor' ) );
575 }
576
577 /**
578 * Review the answer and change individual answer result
579 */
580
581 public function review_quiz_answer() {
582
583 tutor_utils()->checking_nonce();
584
585 global $wpdb;
586
587 $attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
588 $context = Input::post( 'context' );
589 $attempt_answer_id = Input::post( 'attempt_answer_id', 0, Input::TYPE_INT );
590 $mark_as = Input::post( 'mark_as' );
591
592 if ( ! tutor_utils()->can_user_manage( 'attempt', $attempt_id ) || !tutor_utils()->can_user_manage( 'attempt_answer', $attempt_answer_id ) ) {
593 wp_send_json_error( array( 'message'=>__( 'Access Denied', 'tutor' ) ) );
594 }
595
596 $attempt_answer = $wpdb->get_row(
597 $wpdb->prepare(
598 "SELECT * FROM {$wpdb->prefix}tutor_quiz_attempt_answers
599 WHERE attempt_answer_id = %d ",
600 $attempt_answer_id
601 )
602 );
603
604 $attempt = tutor_utils()->get_attempt( $attempt_id );
605 $question = QuizModel::get_quiz_question_by_id( $attempt_answer->question_id );
606 $course_id = $attempt->course_id;
607 $student_id = $attempt->user_id;
608 $previous_ans = $attempt_answer->is_correct;
609
610 do_action( 'tutor_quiz_review_answer_before', $attempt_answer_id, $attempt_id, $mark_as );
611
612 if ( $mark_as === 'correct' ) {
613
614 $answer_update_data = array(
615 'achieved_mark' => $attempt_answer->question_mark,
616 'is_correct' => 1,
617 );
618 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
619 if ( $previous_ans == 0 or $previous_ans == null ) {
620
621 // if previous answer was wrong or in review then add point as correct
622 $attempt_update_data = array(
623 'earned_marks' => $attempt->earned_marks + $attempt_answer->question_mark,
624 'is_manually_reviewed' => 1,
625 'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ),
626 );
627 }
628
629 if ($question->question_type === 'open_ended' || $question->question_type === 'short_answer' ){
630 $attempt_update_data['attempt_status'] = 'attempt_ended';
631 }
632 $wpdb->update($wpdb->prefix.'tutor_quiz_attempts', $attempt_update_data, array('attempt_id' => $attempt_id ));
633
634 } elseif($mark_as === 'incorrect') {
635
636 $answer_update_data = array(
637 'achieved_mark' => '0.00',
638 'is_correct' => 0,
639 );
640 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
641
642 if ( $previous_ans == 1 ) {
643
644 // if previous ans was right then mynus
645 $attempt_update_data = array(
646 'earned_marks' => $attempt->earned_marks - $attempt_answer->question_mark,
647 'is_manually_reviewed' => 1,
648 'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ),
649 );
650
651 }
652 if ( $question->question_type === 'open_ended' || $question->question_type === 'short_answer' ) {
653 $attempt_update_data['attempt_status'] = 'attempt_ended';
654 }
655
656 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
657 }
658 do_action('tutor_quiz_review_answer_after', $attempt_answer_id, $attempt_id, $mark_as);
659 do_action('tutor_quiz/answer/review/after', $attempt_answer_id, $course_id, $student_id);
660
661 ob_start();
662 tutor_load_template_from_custom_path(tutor()->path . '/views/quiz/attempt-details.php', array(
663 'attempt_id' => $attempt_id,
664 'user_id' => $student_id,
665 'context' => $context,
666 'back_url' => Input::post( 'back_url' )
667 ));
668 wp_send_json_success( array( 'html' => ob_get_clean() ) );
669 }
670
671 /**
672 * Save single quiz into database and send html response
673 */
674 public function tutor_quiz_save(){
675
676 tutor_utils()->checking_nonce();
677
678 // Prepare args
679 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
680 $ex_quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
681 $quiz_title = Input::post( 'quiz_title' );
682 $quiz_description = wp_kses( $_POST['quiz_description'], $this->allowed_html );
683 $next_order_id = tutor_utils()->get_next_course_content_order_id( $topic_id, $ex_quiz_id );
684
685 // Check edit privilege
686 if( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
687 wp_send_json_error( array(
688 'message' => __( 'Access Denied', 'tutor' ),
689 'data' => $_POST
690 ));
691 }
692
693 // Prepare quiz data to save in database
694 $post_arr = array(
695 'ID' => $ex_quiz_id,
696 'post_type' => 'tutor_quiz',
697 'post_title' => $quiz_title,
698 'post_content' => $quiz_description,
699 'post_status' => 'publish',
700 'post_author' => get_current_user_id(),
701 'post_parent' => $topic_id,
702 'menu_order' => $next_order_id,
703 );
704
705 // Insert quiz and run hook
706 $quiz_id = wp_insert_post( $post_arr );
707 do_action(( $ex_quiz_id ? 'tutor_quiz_updated' : 'tutor_initial_quiz_created' ), $quiz_id );
708
709 // Now save quiz settings
710 $quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] );
711 update_post_meta($quiz_id, 'tutor_quiz_option', $quiz_option);
712 do_action('tutor_quiz_settings_updated', $quiz_id);
713
714 // Generate quiz modal to show in modal
715 $output = $this->tutor_load_quiz_builder_modal(array(
716 'topic_id'=> $topic_id,
717 'quiz_id'=>$quiz_id
718 ), true);
719
720 // Generate quiz list to show under topic as sub list
721 ob_start();
722 tutor_load_template_from_custom_path(tutor()->path.'/views/fragments/quiz-list-single.php', array(
723 'quiz_id' => $quiz_id,
724 'topic_id' => $topic_id,
725 'quiz_title' => $quiz_title,
726 ), false);
727 $output_quiz_row = ob_get_clean();
728
729 wp_send_json_success(array(
730 'output' => $output,
731 'output_quiz_row' => $output_quiz_row
732 ));
733 }
734
735 public function tutor_delete_quiz_by_id(){
736 tutor_utils()->checking_nonce();
737
738 global $wpdb;
739
740 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
741 $post = get_post( $quiz_id );
742
743 if ( ! tutils()->can_user_manage( 'quiz', $quiz_id ) ) {
744 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
745 }
746
747 if ( $post->post_type === 'tutor_quiz' ) {
748 do_action( 'tutor_delete_quiz_before', $quiz_id );
749
750 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $quiz_id ) );
751 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $quiz_id ) );
752
753 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $quiz_id ) );
754
755 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
756 $in_question_ids = "'" . implode( "','", $questions_ids ) . "'";
757 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id IN({$in_question_ids}) " );
758 }
759
760 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ) );
761
762 wp_delete_post( $quiz_id, true );
763
764 do_action( 'tutor_delete_quiz_after', $quiz_id );
765
766 wp_send_json_success();
767 }
768
769 wp_send_json_error();
770 }
771
772 /**
773 * Load quiz Modal on add/edit click
774 *
775 * @param array $params
776 * @param boolean $ret
777 * @return void
778 *
779 * @since 1.0.0
780 */
781 public function tutor_load_quiz_builder_modal( $params = array(), $ret = false ) {
782 tutor_utils()->checking_nonce();
783
784 $data = array_merge( $_POST, $params );
785 $quiz_id = isset( $data['quiz_id'] ) ? sanitize_text_field( $data['quiz_id'] ) : 0;
786 $topic_id = isset( $data['topic_id'] ) ? sanitize_text_field( $data['topic_id'] ) : 0;
787 $quiz = $quiz_id ? get_post( $quiz_id ) : null;
788 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
789
790 if ( $quiz_id && ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
791 wp_send_json_error( array( 'message' => __( 'Quiz Permission Denied', 'tutor' ) ) );
792 }
793
794 ob_start();
795 include tutor()->path . 'views/modal/edit_quiz.php';
796 $output = ob_get_clean();
797
798 if( $ret ) {
799 return $output;
800 }
801
802 wp_send_json_success( array( 'output' => $output ) );
803 }
804
805 /**
806 * Load quiz question form for quiz
807 *
808 * @since v.1.0.0
809 */
810 public function tutor_quiz_builder_get_question_form(){
811 tutor_utils()->checking_nonce();
812
813 global $wpdb;
814 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
815 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
816 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
817
818 // check if the user can manage the quiz
819 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
820 wp_send_json_error( array('message'=>__('Access Denied', 'tutor')) );
821 }
822
823 // If question ID not provided, then create new before rendering the form
824 if ( ! $question_id ) {
825 $next_question_id = QuizModel::quiz_next_question_id();
826 $next_question_order = QuizModel::quiz_next_question_order_id( $quiz_id );
827 $question_title = __( 'Question', 'tutor' ) . ' ' . $next_question_id;
828
829 $new_question_data = array(
830 'quiz_id' => $quiz_id,
831 'question_title' => $question_title,
832 'question_description' => '',
833 'question_type' => 'true_false',
834 'question_mark' => 1,
835 'question_settings' => maybe_serialize(array()),
836 'question_order' => esc_sql( $next_question_order ) ,
837 );
838
839 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_questions', $new_question_data );
840 $question_id = $wpdb->insert_id;
841
842
843
844 // Add default true/false options for this question since it is by default true/false type.
845 $question_array = array(
846 $question_id => array(
847 'Question' => $question_title,
848 'question_type' => 'true_false',
849 'question_mark' => '1.00',
850 'question_description' => '',
851 )
852 );
853
854 $answer_array = array(
855 $question_id=>array(
856 'true_false' => true
857 )
858 );
859
860 $this->tutor_save_quiz_answer_options($question_array, $answer_array, false);
861 }
862
863 // Now get all data by this question id
864 $question = $wpdb->get_row($wpdb->prepare(
865 "SELECT * FROM {$wpdb->prefix}tutor_quiz_questions
866 WHERE question_id = %d ",
867 $question_id
868 ));
869
870 // Render the question form finally
871 ob_start();
872 require tutor()->path.'views/modal/question_form.php';
873 $output = ob_get_clean();
874
875 wp_send_json_success( array( 'output' => $output ) );
876 }
877
878 public function tutor_quiz_modal_update_question(){
879 tutor_utils()->checking_nonce();
880
881 global $wpdb;
882 $question_data = $_POST['tutor_quiz_question'];
883 $requires_answeres = array(
884 'multiple_choice',
885 'single_choice',
886 'true_false',
887 'fill_in_the_blank',
888 'matching',
889 'image_matching',
890 'image_answering',
891 'ordering'
892 );
893
894 $need_correct = array(
895 'multiple_choice',
896 'single_choice',
897 'true_false'
898 );
899
900 foreach ( $question_data as $question_id => $question ) {
901
902 // Make sure the quiz has answers
903 if( isset( $question['question_type'] ) && in_array($question['question_type'], $requires_answeres ) ) {
904 $require_correct = in_array($question['question_type'], $need_correct);
905 $all_answers = $this->get_answers_by_q_id($question_id, $question['question_type']);
906 $correct_answers = $this->get_answers_by_q_id($question_id, $question['question_type'], $require_correct);
907
908 if(!empty($all_answers) && empty($correct_answers)) {
909 wp_send_json_error( array('message' => __('Please make sure the question has answer')) );
910 exit;
911 }
912 }
913
914 if(!tutor_utils()->can_user_manage('question', $question_id)) {
915 continue;
916 }
917
918 $question_title = sanitize_text_field( $question['question_title'] ?? '' );
919 //$question_description = wp_kses( $question['question_description'] ?? '', $this->allowed_html ); // sanitize_text_field($question['question_description']);
920 $question_description = sanitize_post( $question['question_description'] );
921 $question_type = sanitize_text_field( $question['question_type'] ?? '' );
922 $question_mark = sanitize_text_field( $question['question_mark'] ?? '' );
923
924 unset( $question['question_title'] );
925 unset( $question['question_description'] );
926
927 $data = array(
928 'question_title' => $question_title,
929 'question_description' => $question_description,
930 'question_type' => $question_type,
931 'question_mark' => $question_mark,
932 'question_settings' => maybe_serialize( $question ),
933 );
934
935 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', $data, array( 'question_id' => $question_id ) );
936 }
937
938 wp_send_json_success();
939 }
940
941 public function tutor_quiz_builder_question_delete(){
942 tutor_utils()->checking_nonce();
943
944 global $wpdb;
945
946 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
947
948 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
949 wp_send_json_error( array( 'message'=>__('Access Denied', 'tutor' ) ) );
950 }
951
952 if ( $question_id ) {
953 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_id' => esc_sql( $question_id ) ) );
954 }
955
956 wp_send_json_success();
957 }
958
959 /**
960 * Get answers options form for quiz question
961 *
962 * @since v.1.0.0
963 */
964 public function tutor_quiz_question_answer_editor(){
965 tutor_utils()->checking_nonce();
966
967 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
968 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
969 $question = tutor_utils()->avalue_dot($question_id, $_POST['tutor_quiz_question']);
970 $question_type = $question['question_type'];
971
972 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
973 wp_send_json_error( array( 'message'=> __( 'Access Denied', 'tutor' ) ) );
974 }
975
976 if ( $answer_id ) {
977 $old_answer = tutor_utils()->get_answer_by_id( $answer_id );
978 foreach ( $old_answer as $old_answer );
979 }
980
981 ob_start();
982 include tutor()->path.'views/modal/question_answer_form.php';
983 $output = ob_get_clean();
984
985 wp_send_json_success( array( 'output' => $output ) );
986 }
987
988 public function tutor_save_quiz_answer_options($questions=null, $answers=null, $response=true){
989 tutor_utils()->checking_nonce();
990
991 global $wpdb;
992
993 $questions = $questions ? $questions : $_POST['tutor_quiz_question'];
994 $answers = $answers ? $answers : $_POST['quiz_answer'];
995
996 foreach ( $answers as $question_id => $answer ) {
997
998 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
999 continue;
1000 }
1001
1002 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1003 $question_type = $question['question_type'];
1004
1005 // Getting next sorting order
1006 $next_order_id = (int) $wpdb->get_var(
1007 $wpdb->prepare(
1008 "SELECT MAX(answer_order)
1009 FROM {$wpdb->prefix}tutor_quiz_question_answers
1010 where belongs_question_id = %d
1011 AND belongs_question_type = %s ",
1012 $question_id,
1013 esc_sql( $question_type )
1014 )
1015 );
1016
1017 $next_order_id = $next_order_id + 1;
1018
1019 if ( $question ) {
1020 if ( $question_type === 'true_false' ) {
1021 $wpdb->delete(
1022 $wpdb->prefix . 'tutor_quiz_question_answers',
1023 array(
1024 'belongs_question_id' => $question_id,
1025 'belongs_question_type' => $question_type,
1026 )
1027 );
1028 $data_true_false = array(
1029 array(
1030 'belongs_question_id' => esc_sql( $question_id ),
1031 'belongs_question_type' => $question_type,
1032 'answer_title' => __( 'True', 'tutor' ),
1033 'is_correct' => $answer['true_false'] == 'true' ? 1 : 0,
1034 'answer_two_gap_match' => 'true',
1035 ),
1036 array(
1037 'belongs_question_id' => esc_sql( $question_id ),
1038 'belongs_question_type' => $question_type,
1039 'answer_title' => __('False', 'tutor'),
1040 'is_correct' => $answer['true_false'] == 'false' ? 0 : 1,
1041 'answer_two_gap_match' => 'false',
1042 ),
1043 );
1044
1045 foreach ( $data_true_false as $true_false_data ) {
1046 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $true_false_data );
1047 }
1048 } elseif ( $question_type === 'multiple_choice' || $question_type === 'single_choice' || $question_type === 'ordering' ||
1049 $question_type === 'matching' || $question_type === 'image_matching' || $question_type === 'image_answering' ) {
1050
1051 $answer_data = array(
1052 'belongs_question_id' => sanitize_text_field( $question_id ),
1053 'belongs_question_type' => $question_type,
1054 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1055 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1056 'answer_view_format' => isset( $answer['answer_view_format'] ) ? $answer['answer_view_format'] : 0,
1057 'answer_order' => $next_order_id,
1058 );
1059 if ( isset( $answer['matched_answer_title'] ) ) {
1060 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1061 }
1062
1063 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1064
1065 } elseif ( $question_type === 'fill_in_the_blank' ) {
1066 $wpdb->delete(
1067 $wpdb->prefix . 'tutor_quiz_question_answers',
1068 array(
1069 'belongs_question_id' => $question_id,
1070 'belongs_question_type' => $question_type,
1071 )
1072 );
1073 $answer_data = array(
1074 'belongs_question_id' => sanitize_text_field( $question_id ),
1075 'belongs_question_type' => $question_type,
1076 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1077 'answer_two_gap_match' => isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null,
1078 );
1079 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1080 }
1081 }
1082 }
1083
1084 // Send response to browser if not internal call
1085 if($response) {
1086 wp_send_json_success();
1087 exit;
1088 }
1089 }
1090
1091 /**
1092 * Tutor Update Answer
1093 *
1094 * @since v.1.0.0
1095 */
1096 public function tutor_update_quiz_answer_options(){
1097 tutor_utils()->checking_nonce();
1098
1099 global $wpdb;
1100
1101 $answer_id = Input::post( 'tutor_quiz_answer_id', 0, Input::TYPE_INT );
1102
1103 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1104 wp_send_json_error( array( 'message'=>__( 'Access Denied', 'tutor' ) ) );
1105 }
1106
1107 $questions = tutor_sanitize_data( $_POST['tutor_quiz_question'] );
1108 $answers = tutor_sanitize_data( $_POST['quiz_answer'] );
1109
1110 foreach ( $answers as $question_id => $answer ) {
1111 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1112 $question_type = $question['question_type'];
1113
1114 if ( $question ) {
1115 if ( $question_type === 'multiple_choice' || $question_type === 'single_choice' || $question_type === 'ordering' || $question_type === 'matching' || $question_type === 'image_matching' || $question_type === 'fill_in_the_blank' || $question_type === 'image_answering' ) {
1116
1117 $answer_data = array(
1118 'belongs_question_id' => $question_id,
1119 'belongs_question_type' => $question_type,
1120 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1121 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1122 'answer_view_format' => isset( $answer['answer_view_format'] ) ? sanitize_text_field( $answer['answer_view_format'] ) : '',
1123 );
1124 if ( isset( $answer['matched_answer_title'] ) ) {
1125 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1126 }
1127
1128 if ( $question_type === 'fill_in_the_blank' ) {
1129 $answer_data['answer_two_gap_match'] = isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null;
1130 }
1131
1132 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data, array( 'answer_id' => $answer_id ) );
1133 }
1134 }
1135 }
1136
1137 // die(print_r($_POST));
1138 wp_send_json_success();
1139 }
1140
1141 private function get_answers_by_q_id($question_id, $question_type, $is_correct=false) {
1142 global $wpdb;
1143
1144 $correct_clause = $is_correct ? ' AND is_correct=1 ' : '';
1145
1146 return $wpdb->get_results($wpdb->prepare(
1147 "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers
1148 WHERE belongs_question_id = %d AND
1149 belongs_question_type = %s
1150 {$correct_clause}
1151 ORDER BY answer_order ASC;",
1152 $question_id,
1153 esc_sql( $question_type )
1154 ));
1155 }
1156
1157 public function tutor_quiz_builder_change_type(){
1158 tutor_utils()->checking_nonce();
1159
1160 global $wpdb;
1161 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1162 $question_type = Input::post( 'question_type' );
1163
1164 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1165 wp_send_json_error( array( 'message'=>__('Access Denied', 'tutor' ) ) );
1166 }
1167
1168 // Get question data by question ID
1169 $question = $wpdb->get_row($wpdb->prepare(
1170 "SELECT * FROM {$wpdb->prefix}tutor_quiz_questions
1171 WHERE question_id = %d ",
1172 $question_id
1173 ));
1174
1175 // Get answers by question ID
1176 $answers = $this->get_answers_by_q_id( $question_id, $question_type );
1177
1178 ob_start();
1179 require tutor()->path . '/views/modal/question_answer_list.php';
1180 $output = ob_get_clean();
1181
1182 wp_send_json_success( array( 'output' => $output ) );
1183 }
1184
1185 public function tutor_quiz_builder_delete_answer(){
1186 tutor_utils()->checking_nonce();
1187
1188 global $wpdb;
1189 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1190
1191 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1192 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1193 }
1194
1195 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_id' => esc_sql( $answer_id ) ) );
1196 wp_send_json_success();
1197 }
1198
1199 /**
1200 * Save quiz questions sorting
1201 */
1202 public function tutor_quiz_question_sorting(){
1203 tutor_utils()->checking_nonce();
1204
1205 global $wpdb;
1206
1207 $question_ids = tutor_utils()->avalue_dot( 'sorted_question_ids', tutor_sanitize_data($_POST) );
1208 if ( is_array( $question_ids ) && count( $question_ids ) ) {
1209 $i = 0;
1210 foreach ($question_ids as $key => $question_id){
1211 if(tutor_utils()->can_user_manage('question', $question_id)) {
1212 $i++;
1213 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_order' => $i ), array( 'question_id' => $question_id ) );
1214 }
1215 }
1216 }
1217 }
1218
1219 /**
1220 * Save sorting data for quiz answers
1221 */
1222 public function tutor_quiz_answer_sorting(){
1223 tutor_utils()->checking_nonce();
1224
1225 global $wpdb;
1226
1227 if ( ! empty($_POST['sorted_answer_ids']) && is_array($_POST['sorted_answer_ids']) && count($_POST['sorted_answer_ids']) ){
1228 $answer_ids = $_POST['sorted_answer_ids'];
1229 $i = 0;
1230 foreach ($answer_ids as $key => $answer_id){
1231 if(tutor_utils()->can_user_manage('quiz_answer', $answer_id)) {
1232 $i++;
1233 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_order' => $i ), array( 'answer_id' => $answer_id ) );
1234 }
1235 }
1236 }
1237 }
1238
1239 /**
1240 * Mark answer as correct
1241 */
1242
1243 public function tutor_mark_answer_as_correct(){
1244 tutor_utils()->checking_nonce();
1245
1246 global $wpdb;
1247
1248 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1249 // get question info.
1250 $belong_question = $wpdb->get_row( $wpdb->prepare(
1251 " SELECT belongs_question_id, belongs_question_type
1252 FROM {$wpdb->tutor_quiz_question_answers}
1253 WHERE answer_id = %d
1254 LIMIT 1
1255 ",
1256 $answer_id
1257 ) );
1258 if ( $belong_question ) {
1259 // if question found update all answer is_correct to 0 except post answer.
1260 $question_type = $belong_question->belongs_question_type;
1261 $question_id = $belong_question->belongs_question_id;
1262 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
1263 $update = $wpdb->query( $wpdb->prepare(
1264 "UPDATE {$wpdb->tutor_quiz_question_answers}
1265 SET is_correct = 0
1266 WHERE belongs_question_id = %d
1267 AND answer_id != %d
1268 ",
1269 $question_id,
1270 $answer_id
1271 ) );
1272 }
1273 }
1274
1275 $inputValue = sanitize_text_field($_POST['inputValue']);
1276
1277 if(!tutor_utils()->can_user_manage('quiz_answer', $answer_id)) {
1278 wp_send_json_error( array('message'=>__('Access Denied', 'tutor')) );
1279 }
1280
1281 $answer = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE answer_id = %d LIMIT 0,1 ;", $answer_id ) );
1282 if ( $answer->belongs_question_type === 'single_choice' ) {
1283 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'is_correct' => 0 ), array( 'belongs_question_id' => esc_sql( $answer->belongs_question_id ) ) );
1284 }
1285 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'is_correct' => esc_sql( $inputValue ) ), array( 'answer_id' => esc_sql( $answer_id ) ) );
1286 }
1287
1288 //=========================//
1289 // Front end stuffs
1290 //=========================//
1291
1292 /**
1293 * Rendering quiz for frontend
1294 *
1295 * @since v.1.0.0
1296 */
1297
1298 public function tutor_render_quiz_content() {
1299
1300 tutor_utils()->checking_nonce();
1301
1302 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1303
1304 if ( ! tutor_utils()->has_enrolled_content_access( 'quiz', $quiz_id ) ) {
1305 wp_send_json_error( array( 'message' => __( 'Access Denied.', 'tutor' ) ) );
1306 }
1307
1308 ob_start();
1309 global $post;
1310
1311 $post = get_post( $quiz_id );
1312 setup_postdata( $post );
1313 // tutor_lesson_content();
1314
1315 single_quiz_contents();
1316 wp_reset_postdata();
1317
1318 $html = ob_get_clean();
1319 wp_send_json_success( array( 'html' => $html ) );
1320 }
1321
1322 /**
1323 * Get attempt details
1324 *
1325 * @param int $attempt_id, required attempt id to get details
1326 *
1327 * @return mixed, object on success, null on failure
1328 */
1329 public static function attempt_details( int $attempt_id ) {
1330 global $wpdb;
1331 $attempt_details = $wpdb->get_row(
1332 $wpdb->prepare(
1333 "SELECT *FROM {$wpdb->prefix}tutor_quiz_attempts
1334 WHERE attempt_id = %d",
1335 $attempt_id
1336 )
1337 );
1338 return $attempt_details;
1339 }
1340
1341 /**
1342 * Update attempt info
1343 *
1344 * @param $attempt_info, serialize data
1345 *
1346 * @return bool, true on success, false on failure
1347 */
1348 public static function update_attempt_info( int $attempt_id, $attempt_info ) {
1349 global $wpdb;
1350 $table = $wpdb->prefix . 'tutor_quiz_attempts';
1351 $update_info = $wpdb->update(
1352 $table,
1353 array( 'attempt_info' => $attempt_info ),
1354 array( 'attempt_id' => $attempt_id )
1355 );
1356 return $update_info ? true : false;
1357 }
1358
1359 /**
1360 * Attempt delete ajax request handler
1361 *
1362 * @since v2.1.0
1363 *
1364 * @return void wp_json response
1365 */
1366 public function attempt_delete() {
1367 tutor_utils()->checking_nonce();
1368 if ( current_user_can( 'administrator') || current_user_can( tutor()->instructor_role ) ) {
1369 $attempt_id = Input::post( 'id', 0, Input::TYPE_INT );
1370 if ( $attempt_id ) {
1371 QuizModel::delete_quiz_attempt( $attempt_id );
1372 wp_send_json_success( __( 'Attempt deleted successfully!', 'tutor' ) );
1373 } else {
1374 wp_send_json_error( __( 'Invalid attempt ID', 'tutor' ) );
1375 }
1376 } else {
1377 wp_send_json_error( __( 'You are not authorized to perform this action!', 'tutor' ) );
1378 }
1379 }
1380
1381 }
1382