PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.0.2
Tutor LMS – eLearning and online course solution v3.0.2
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 1 year ago Admin.php 1 year ago Ajax.php 1 year ago Announcements.php 1 year ago Assets.php 1 year ago Backend_Page_Trait.php 1 year ago BaseController.php 1 year ago Course.php 1 year ago Course_Embed.php 3 years ago Course_Filter.php 1 year ago Course_List.php 1 year ago Course_Settings_Tabs.php 1 year ago Course_Widget.php 3 years ago Custom_Validation.php 3 years ago Dashboard.php 1 year ago Earnings.php 1 year ago FormHandler.php 2 years ago Frontend.php 1 year ago Gutenberg.php 1 year ago Input.php 1 year ago Instructor.php 1 year ago Instructors_List.php 1 year ago Lesson.php 1 year ago Options_V2.php 1 year ago Permalink.php 2 years ago Post_types.php 2 years ago Private_Course_Access.php 1 year ago Q_And_A.php 1 year ago Question_Answers_List.php 3 years ago Quiz.php 1 year ago QuizBuilder.php 1 year ago Quiz_Attempts_List.php 1 year ago RestAPI.php 2 years ago Reviews.php 3 years ago Rewrite_Rules.php 2 years ago Shortcode.php 1 year ago Singleton.php 1 year ago Student.php 1 year ago Students_List.php 3 years ago Taxonomies.php 3 years ago Template.php 1 year ago Theme_Compatibility.php 3 years ago Tools.php 3 years ago Tools_V2.php 1 year ago Tutor.php 1 year ago TutorEDD.php 1 year ago Tutor_Base.php 2 years ago Tutor_Setup.php 1 year ago Upgrader.php 1 year ago User.php 1 year ago Utils.php 1 year ago Video_Stream.php 3 years ago WhatsNew.php 2 years ago Withdraw.php 1 year ago Withdraw_Requests_List.php 3 years ago WooCommerce.php 1 year ago
Quiz.php
2133 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\HttpHelper;
18 use Tutor\Helpers\QueryHelper;
19 use Tutor\Models\CourseModel;
20 use Tutor\Models\QuizModel;
21 use Tutor\Traits\JsonResponse;
22
23 /**
24 * Manage quiz operations.
25 *
26 * @since 1.0.0
27 */
28 class Quiz {
29 use JsonResponse;
30
31 const META_QUIZ_OPTION = 'tutor_quiz_option';
32
33 /**
34 * Allowed attrs
35 *
36 * @var array
37 */
38 private $allowed_attributes = array(
39 'src' => array(),
40 'style' => array(),
41 'class' => array(),
42 'id' => array(),
43 'href' => array(),
44 'alt' => array(),
45 'title' => array(),
46 'type' => array(),
47 'controls' => array(),
48 'muted' => array(),
49 'loop' => array(),
50 'poster' => array(),
51 'preload' => array(),
52 'autoplay' => array(),
53 'width' => array(),
54 'height' => array(),
55 );
56
57 /**
58 * Allowed HTML tags
59 *
60 * @var array
61 */
62 private $allowed_html = array( 'img', 'b', 'i', 'br', 'a', 'audio', 'video', 'source' );
63
64 /**
65 * Register hooks
66 *
67 * @since 1.0.0
68 *
69 * @return void
70 */
71 public function __construct() {
72 add_action( 'save_post_tutor_quiz', array( $this, 'save_quiz_meta' ) );
73 add_action( 'wp_ajax_remove_quiz_from_post', array( $this, 'remove_quiz_from_post' ) );
74
75 add_action( 'wp_ajax_tutor_quiz_timeout', array( $this, 'tutor_quiz_timeout' ) );
76
77 // User take the quiz.
78 add_action( 'template_redirect', array( $this, 'start_the_quiz' ) );
79 add_action( 'template_redirect', array( $this, 'answering_quiz' ) );
80 add_action( 'template_redirect', array( $this, 'finishing_quiz_attempt' ) );
81
82 add_action( 'wp_ajax_review_quiz_answer', array( $this, 'review_quiz_answer' ) );
83 // Instructor Feedback Action.
84 add_action( 'wp_ajax_tutor_instructor_feedback', array( $this, 'tutor_instructor_feedback' ) );
85
86 /**
87 * New Design Quiz
88 */
89
90 add_action( 'wp_ajax_tutor_quiz_save', array( $this, 'ajax_quiz_save' ) );
91 add_action( 'wp_ajax_tutor_quiz_delete', array( $this, 'ajax_quiz_delete' ) );
92 add_action( 'wp_ajax_tutor_quiz_details', array( $this, 'ajax_quiz_details' ) );
93
94 add_action( 'wp_ajax_tutor_quiz_question_create', array( $this, 'ajax_quiz_question_create' ) );
95 add_action( 'wp_ajax_tutor_quiz_question_update', array( $this, 'ajax_quiz_question_update' ) );
96 add_action( 'wp_ajax_tutor_quiz_question_delete', array( $this, 'ajax_quiz_question_delete' ) );
97 add_action( 'wp_ajax_tutor_quiz_question_sorting', array( $this, 'ajax_quiz_question_sorting' ) );
98
99 add_action( 'wp_ajax_tutor_quiz_question_answer_save', array( $this, 'ajax_quiz_question_answer_save' ) );
100 add_action( 'wp_ajax_tutor_quiz_question_answer_delete', array( $this, 'ajax_quiz_question_answer_delete' ) );
101 add_action( 'wp_ajax_tutor_quiz_question_answer_sorting', array( $this, 'ajax_quiz_question_answer_sorting' ) );
102 add_action( 'wp_ajax_tutor_mark_answer_as_correct', array( $this, 'ajax_mark_answer_as_correct' ) );
103
104 add_action( 'wp_ajax_tutor_load_quiz_builder_modal', array( $this, 'tutor_load_quiz_builder_modal' ), 10, 0 );
105 add_action( 'wp_ajax_tutor_quiz_builder_get_question_form', array( $this, 'tutor_quiz_builder_get_question_form' ) );
106 add_action( 'wp_ajax_tutor_quiz_modal_update_question', array( $this, 'tutor_quiz_modal_update_question' ) );
107 add_action( 'wp_ajax_tutor_quiz_question_answer_editor', array( $this, 'tutor_quiz_question_answer_editor' ) );
108 add_action( 'wp_ajax_tutor_save_quiz_answer_options', array( $this, 'tutor_save_quiz_answer_options' ), 10, 0 );
109 add_action( 'wp_ajax_tutor_update_quiz_answer_options', array( $this, 'tutor_update_quiz_answer_options' ) );
110 add_action( 'wp_ajax_tutor_quiz_builder_change_type', array( $this, 'tutor_quiz_builder_change_type' ) );
111
112 /**
113 * Frontend Stuff
114 */
115 add_action( 'wp_ajax_tutor_render_quiz_content', array( $this, 'tutor_render_quiz_content' ) );
116
117 /**
118 * Quiz abandon action
119 *
120 * @since 1.9.6
121 */
122 add_action( 'wp_ajax_tutor_quiz_abandon', array( $this, 'tutor_quiz_abandon' ) );
123
124 $this->prepare_allowed_html();
125
126 /**
127 * Delete quiz attempt
128 *
129 * @since 2.1.0
130 */
131 add_action( 'wp_ajax_tutor_attempt_delete', array( $this, 'attempt_delete' ) );
132
133 add_action( 'tutor_quiz/answer/review/after', array( $this, 'do_auto_course_complete' ), 10, 3 );
134 }
135
136 /**
137 * Get quiz time units options.
138 *
139 * @since 2.6.0
140 *
141 * @return array
142 */
143 public static function quiz_time_units() {
144 $time_units = array(
145 'seconds' => __( 'Seconds', 'tutor' ),
146 'minutes' => __( 'Minutes', 'tutor' ),
147 'hours' => __( 'Hours', 'tutor' ),
148 'days' => __( 'Days', 'tutor' ),
149 'weeks' => __( 'Weeks', 'tutor' ),
150 );
151
152 return apply_filters( 'tutor_quiz_time_units', $time_units );
153 }
154
155 /**
156 * Get quiz default settings.
157 *
158 * @since 3.0.0
159 *
160 * @return array
161 */
162 public static function get_default_quiz_settings() {
163 $settings = array(
164 'time_limit' => array(
165 'time_type' => 'minutes',
166 'time_value' => 0,
167 ),
168 'attempts_allowed' => 10,
169 'feedback_mode' => 'retry',
170 'hide_question_number_overview' => 0,
171 'hide_quiz_time_display' => 0,
172 'max_questions_for_answer' => 10,
173 'open_ended_answer_characters_limit' => 500,
174 'pass_is_required' => 0,
175 'passing_grade' => 80,
176 'question_layout_view' => '',
177 'questions_order' => 'rand',
178 'quiz_auto_start' => 0,
179 'short_answer_characters_limit' => 200,
180 );
181
182 return apply_filters( 'tutor_quiz_default_settings', $settings );
183 }
184
185 /**
186 * Get question default settings.
187 *
188 * @since 3.0.0
189 *
190 * @param string $type type of question.
191 *
192 * @return array
193 */
194 public static function get_default_question_settings( $type ) {
195 $settings = array(
196 'question_type' => $type,
197 'question_mark' => 1,
198 'answer_required' => 0,
199 'randomize_options' => 0,
200 'show_question_mark' => 0,
201 );
202
203 return apply_filters( 'tutor_question_default_settings', $settings );
204 }
205
206 /**
207 * Get quiz modes
208 *
209 * @since 2.6.0
210 *
211 * @return array
212 */
213 public static function quiz_modes() {
214 $modes = array(
215 array(
216 'key' => 'default',
217 'value' => __( 'Default', 'tutor' ),
218 'description' => __( 'Answers shown after quiz is finished', 'tutor' ),
219 ),
220 array(
221 'key' => 'reveal',
222 'value' => __( 'Reveal Mode', 'tutor' ),
223 'description' => __( 'Show result after the attempt.', 'tutor' ),
224 ),
225 array(
226 'key' => 'retry',
227 'value' => __( 'Retry Mode', 'tutor' ),
228 'description' => __( 'Reattempt quiz any number of times. Define Attempts Allowed below.', 'tutor' ),
229 ),
230 );
231
232 return apply_filters( 'tutor_quiz_modes', $modes );
233 }
234
235 /**
236 * Get quiz modes
237 *
238 * @since 2.6.0
239 *
240 * @return array
241 */
242 public static function quiz_question_layouts() {
243 $layouts = array(
244 '' => __( 'Set question layout view', 'tutor' ),
245 'single_question' => __( 'Single Question', 'tutor' ),
246 'question_pagination' => __( 'Question Pagination', 'tutor' ),
247 'question_below_each_other' => __( 'Question below each other', 'tutor' ),
248 );
249
250 return apply_filters( 'tutor_quiz_layouts', $layouts );
251 }
252
253 /**
254 * Get quiz modes
255 *
256 * @since 2.6.0
257 *
258 * @return array
259 */
260 public static function quiz_question_orders() {
261 $orders = array(
262 'rand' => __( 'Random', 'tutor' ),
263 'sorting' => __( 'Sorting', 'tutor' ),
264 'asc' => __( 'Ascending', 'tutor' ),
265 'desc' => __( 'Descending', 'tutor' ),
266 );
267
268 return apply_filters( 'tutor_quiz_layouts', $orders );
269 }
270
271 /**
272 * Prepare allowed HTML
273 *
274 * @since 1.0.0
275 *
276 * @return void
277 */
278 private function prepare_allowed_html() {
279
280 $allowed = array();
281
282 foreach ( $this->allowed_html as $tag ) {
283 $allowed[ $tag ] = $this->allowed_attributes;
284 }
285
286 $this->allowed_html = $allowed;
287 }
288
289 /**
290 * Instructor feedback ajax request handler
291 *
292 * @since 1.0.0
293 *
294 * @return void | send json response
295 */
296 public function tutor_instructor_feedback() {
297 tutor_utils()->checking_nonce();
298
299 // Check if user is privileged.
300 if ( ! User::has_any_role( array( User::ADMIN, User::INSTRUCTOR ) ) ) {
301 wp_send_json_error( tutor_utils()->error_message() );
302 }
303
304 $attempt_details = self::attempt_details( Input::post( 'attempt_id', 0, Input::TYPE_INT ) );
305 $feedback = Input::post( 'feedback', '', Input::TYPE_KSES_POST );
306 $attempt_info = isset( $attempt_details->attempt_info ) ? $attempt_details->attempt_info : false;
307 if ( $attempt_info ) {
308 //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
309 $unserialized = unserialize( $attempt_details->attempt_info );
310 if ( is_array( $unserialized ) ) {
311 $unserialized['instructor_feedback'] = $feedback;
312
313 do_action( 'tutor_quiz/attempt/submitted/feedback', $attempt_details->attempt_id );
314 //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
315 $update = self::update_attempt_info( $attempt_details->attempt_id, serialize( $unserialized ) );
316 if ( $update ) {
317 wp_send_json_success();
318 } else {
319 wp_send_json_error();
320 }
321 } else {
322 wp_send_json_error( __( 'Invalid quiz info' ) );
323 }
324 }
325 wp_send_json_error();
326 }
327
328 /**
329 * Update quiz meta
330 *
331 * @since 1.0.0
332 *
333 * @param int $post_ID post id.
334 * @return void
335 */
336 public function save_quiz_meta( $post_ID ) {
337 //phpcs:ignore WordPress.Security.NonceVerification.Missing
338 if ( isset( $_POST['quiz_option'] ) ) {
339 $quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] ); //phpcs:ignore
340 update_post_meta( $post_ID, 'tutor_quiz_option', $quiz_option );
341 }
342 }
343
344 /**
345 * Remove quiz from post
346 *
347 * @since 1.0.0
348 *
349 * @return void
350 */
351 public function remove_quiz_from_post() {
352 tutor_utils()->checking_nonce();
353
354 global $wpdb;
355 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
356
357 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
358 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
359 }
360
361 $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'ID' => $quiz_id ) );
362 wp_send_json_success();
363 }
364
365 /**
366 * Start Quiz from here...
367 *
368 * @since 1.0.0
369 *
370 * @return void
371 */
372 public function start_the_quiz() {
373 if ( Input::post( 'tutor_action' ) !== 'tutor_start_quiz' ) {
374 return;
375 }
376 // Checking nonce.
377 tutor_utils()->checking_nonce();
378
379 if ( ! is_user_logged_in() ) {
380 // TODO: need to set a view in the next version.
381 die( 'Please sign in to do this operation' );
382 }
383
384 $user_id = get_current_user_id();
385 $user = get_userdata( $user_id );
386
387 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
388
389 $quiz = get_post( $quiz_id );
390 $course = CourseModel::get_course_by_quiz( $quiz_id );
391
392 self::quiz_attempt( $course->ID, $quiz_id, $user_id );
393 wp_safe_redirect( get_permalink( $quiz_id ) );
394 die();
395 }
396
397 /**
398 * Manage quiz attempt
399 *
400 * @since 2.6.1
401 *
402 * @param integer $course_id course id.
403 * @param integer $quiz_id quiz id.
404 * @param integer $user_id user id.
405 * @param string $attempt_status attempt status.
406 *
407 * @return int inserted id|0
408 */
409 public static function quiz_attempt( int $course_id, int $quiz_id, int $user_id, $attempt_status = 'attempt_started' ) {
410 global $wpdb;
411
412 if ( ! $course_id ) {
413 die( 'There is something went wrong with course, please check if quiz attached with a course' );
414 }
415
416 do_action( 'tutor_quiz/start/before', $quiz_id, $user_id );
417
418 $date = date( 'Y-m-d H:i:s', tutor_time() ); //phpcs:ignore
419
420 $tutor_quiz_option = (array) maybe_unserialize( get_post_meta( $quiz_id, 'tutor_quiz_option', true ) );
421 $attempts_allowed = tutor_utils()->get_quiz_option( $quiz_id, 'attempts_allowed', 0 );
422
423 $time_limit = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_value' );
424 $time_limit_seconds = 0;
425 $time_type = 'seconds';
426 if ( $time_limit ) {
427 $time_type = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_type' );
428
429 switch ( $time_type ) {
430 case 'seconds':
431 $time_limit_seconds = $time_limit;
432 break;
433 case 'minutes':
434 $time_limit_seconds = $time_limit * 60;
435 break;
436 case 'hours':
437 $time_limit_seconds = $time_limit * 60 * 60;
438 break;
439 case 'days':
440 $time_limit_seconds = $time_limit * 60 * 60 * 24;
441 break;
442 case 'weeks':
443 $time_limit_seconds = $time_limit * 60 * 60 * 24 * 7;
444 break;
445 }
446 }
447
448 $max_question_allowed = tutor_utils()->max_questions_for_take_quiz( $quiz_id );
449 $tutor_quiz_option['time_limit']['time_limit_seconds'] = $time_limit_seconds;
450
451 $attempt_data = array(
452 'course_id' => $course_id,
453 'quiz_id' => $quiz_id,
454 'user_id' => $user_id,
455 'total_questions' => $max_question_allowed,
456 'total_answered_questions' => 0,
457 'attempt_info' => maybe_serialize( $tutor_quiz_option ),
458 'attempt_status' => $attempt_status,
459 'attempt_ip' => tutor_utils()->get_ip(),
460 'attempt_started_at' => $date,
461 );
462
463 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_data );
464 $attempt_id = (int) $wpdb->insert_id;
465
466 if ( $attempt_id ) {
467 do_action( 'tutor_quiz/start/after', $quiz_id, $user_id, $attempt_id );
468 return $attempt_id;
469 } else {
470 return 0;
471 }
472 }
473
474 /**
475 * Answering quiz
476 *
477 * @since 1.0.0
478 *
479 * @return void
480 */
481 public function answering_quiz() {
482
483 if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) {
484 return;
485 }
486 // submit quiz attempts.
487 self::tutor_quiz_attemp_submit();
488
489 wp_safe_redirect( get_the_permalink() );
490 die();
491 }
492
493 /**
494 * Quiz abandon submission handler
495 *
496 * @since 1.9.6
497 *
498 * @return JSON response
499 */
500 public function tutor_quiz_abandon() {
501 if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) {
502 return;
503 }
504 tutor_utils()->checking_nonce();
505 // submit quiz attempts.
506 if ( self::tutor_quiz_attemp_submit() ) {
507 wp_send_json_success();
508 } else {
509 wp_send_json_error();
510 }
511 }
512
513 /**
514 * This is a unified method for handling normal quiz submit or abandon submit
515 * It will handle ajax or normal form submit and can be used with different hooks
516 *
517 * @since 1.9.6
518 *
519 * @return true | false
520 */
521 public static function tutor_quiz_attemp_submit() {
522 // Check logged in.
523 if ( ! is_user_logged_in() ) {
524 die( 'Please sign in to do this operation' );
525 }
526
527 // Check nonce.
528 tutor_utils()->checking_nonce();
529
530 // Prepare attempt info.
531 $user_id = get_current_user_id();
532 $attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
533 $attempt = tutor_utils()->get_attempt( $attempt_id );
534 $course_id = CourseModel::get_course_by_quiz( $attempt->quiz_id )->ID;
535
536 // Sanitize data by helper method.
537 $attempt_answers = isset( $_POST['attempt'] ) ? tutor_sanitize_data( $_POST['attempt'] ) : false; //phpcs:ignore
538 $attempt_answers = is_array( $attempt_answers ) ? $attempt_answers : array();
539
540 // Check if has access to the attempt.
541 if ( ! $attempt || $user_id != $attempt->user_id ) {
542 die( 'Operation not allowed, attempt not found or permission denied' );
543 }
544 self::manage_attempt_answers( $attempt_answers, $attempt, $attempt_id, $course_id, $user_id );
545 return true;
546 }
547
548 /**
549 * Manage attempt answers
550 *
551 * Evaluate each attempt answer and update the attempts table & insert in the attempt_answers table.
552 *
553 * @since 2.6.1
554 *
555 * @param array $attempt_answers attempt answers.
556 * @param object $attempt single attempt.
557 * @param int $attempt_id attempt id.
558 * @param int $course_id course id.
559 * @param int $user_id user id.
560 *
561 * @return void
562 */
563 public static function manage_attempt_answers( $attempt_answers, $attempt, $attempt_id, $course_id, $user_id ) {
564 global $wpdb;
565 // Before hook.
566 do_action( 'tutor_quiz/attempt_analysing/before', $attempt_id );
567
568 // Single quiz can have multiple question. So multiple answer should be saved.
569 foreach ( $attempt_answers as $attempt_id => $attempt_answer ) {
570 // Get total marks of all question comes.
571 $question_ids = tutor_utils()->avalue_dot( 'quiz_question_ids', $attempt_answer );
572 $question_ids = array_filter(
573 $question_ids,
574 function( $id ) {
575 return (int) $id;
576 }
577 );
578
579 // Calculate and set the total marks in attempt table for this question.
580 if ( is_array( $question_ids ) && count( $question_ids ) ) {
581 $question_ids_string = QueryHelper::prepare_in_clause( $question_ids );
582
583 // Get total marks of the questions from question table.
584 //phpcs:disable
585 $query = $wpdb->prepare(
586 "SELECT SUM(question_mark)
587 FROM {$wpdb->prefix}tutor_quiz_questions
588 WHERE 1 = %d
589 AND question_id IN({$question_ids_string});
590 ",
591 1
592 );
593 $total_question_marks = $wpdb->get_var( $query );
594 //phpcs:enable
595
596 $total_question_marks = apply_filters( 'tutor_filter_update_before_question_mark', $total_question_marks, $question_ids, $user_id, $attempt_id );
597
598 // Set the the total mark in the attempt table for the question.
599 $wpdb->update(
600 $wpdb->prefix . 'tutor_quiz_attempts',
601 array( 'total_marks' => $total_question_marks ),
602 array( 'attempt_id' => $attempt_id )
603 );
604 }
605
606 $total_marks = 0;
607 $review_required = false;
608 $quiz_answers = tutor_utils()->avalue_dot( 'quiz_question', $attempt_answer );
609
610 if ( tutor_utils()->count( $quiz_answers ) ) {
611
612 foreach ( $quiz_answers as $question_id => $answers ) {
613 $question = QuizModel::get_quiz_question_by_id( $question_id );
614 $question_type = $question->question_type;
615
616 $is_answer_was_correct = false;
617 $given_answer = '';
618
619 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
620
621 if ( ! is_numeric( $answers ) || ! $answers ) {
622 wp_send_json_error();
623 exit;
624 }
625
626 $given_answer = $answers;
627 $is_answer_was_correct = (bool) $wpdb->get_var(
628 $wpdb->prepare(
629 "SELECT is_correct
630 FROM {$wpdb->prefix}tutor_quiz_question_answers
631 WHERE answer_id = %d
632 ",
633 $answers
634 )
635 );
636
637 } elseif ( 'multiple_choice' === $question_type ) {
638
639 $given_answer = (array) ( $answers );
640
641 $given_answer = array_filter(
642 $given_answer,
643 function( $id ) {
644 return is_numeric( $id ) && $id > 0;
645 }
646 );
647 $get_original_answers = (array) $wpdb->get_col(
648 $wpdb->prepare(
649 "SELECT
650 answer_id
651 FROM
652 {$wpdb->prefix}tutor_quiz_question_answers
653 WHERE belongs_question_id = %d
654 AND belongs_question_type = %s
655 AND is_correct = 1 ;
656 ",
657 $question->question_id,
658 $question_type
659 )
660 );
661
662 if ( count( array_diff( $get_original_answers, $given_answer ) ) === 0 && count( $get_original_answers ) === count( $given_answer ) ) {
663 $is_answer_was_correct = true;
664 }
665 $given_answer = maybe_serialize( $answers );
666
667 } elseif ( 'fill_in_the_blank' === $question_type ) {
668
669 $get_original_answer = $wpdb->get_row(
670 $wpdb->prepare(
671 "SELECT *
672 FROM {$wpdb->prefix}tutor_quiz_question_answers
673 WHERE belongs_question_id = %d
674 AND belongs_question_type = %s ;
675 ",
676 $question->question_id,
677 $question_type
678 )
679 );
680
681 /**
682 * Answers stored in DB
683 */
684 $gap_answer = (array) explode( '|', $get_original_answer->answer_two_gap_match );
685 $gap_answer = maybe_serialize(
686 array_map(
687 function ( $ans ) {
688 return wp_slash( trim( $ans ) );
689 },
690 $gap_answer
691 )
692 );
693
694 /**
695 * Answers from user input
696 */
697 $given_answer = (array) array_map( 'sanitize_text_field', $answers );
698 $given_answer = maybe_serialize( $given_answer );
699
700 /**
701 * Compare answer's by making both case-insensitive.
702 */
703 if ( strtolower( $given_answer ) == strtolower( $gap_answer ) ) {
704 $is_answer_was_correct = true;
705 }
706 } elseif ( 'open_ended' === $question_type || 'short_answer' === $question_type ) {
707 $review_required = true;
708 $given_answer = wp_kses_post( $answers );
709
710 } elseif ( 'ordering' === $question_type || 'matching' === $question_type || 'image_matching' === $question_type ) {
711
712 $given_answer = (array) array_map( 'sanitize_text_field', tutor_utils()->avalue_dot( 'answers', $answers ) );
713 $given_answer = maybe_serialize( $given_answer );
714
715 $get_original_answers = (array) $wpdb->get_col(
716 $wpdb->prepare(
717 "SELECT answer_id
718 FROM {$wpdb->prefix}tutor_quiz_question_answers
719 WHERE belongs_question_id = %d
720 AND belongs_question_type = %s
721 ORDER BY answer_order ASC ;
722 ",
723 $question->question_id,
724 $question_type
725 )
726 );
727
728 $get_original_answers = array_map( 'sanitize_text_field', $get_original_answers );
729
730 if ( maybe_serialize( $get_original_answers ) == $given_answer ) {
731 $is_answer_was_correct = true;
732 }
733 } elseif ( 'image_answering' === $question_type ) {
734 $image_inputs = tutor_utils()->avalue_dot( 'answer_id', $answers );
735 $image_inputs = (array) array_map( 'sanitize_text_field', $image_inputs );
736 $given_answer = maybe_serialize( $image_inputs );
737 $is_answer_was_correct = false;
738 /**
739 * For the image_answering question type result
740 * remain pending in spite of correct answer & required
741 * review of admin/instructor. Since it's
742 * pending we need to mark it as incorrect. Otherwise if
743 * mark it correct then earned mark will be updated. then
744 * again when instructor/admin review & mark it as correct
745 * extra mark is adding. In this case, student
746 * getting double mark for the same question.
747 *
748 * For now code is commenting will be removed later on
749 *
750 * @since 2.1.5
751 */
752
753 //phpcs:disable
754
755 // $db_answer = $wpdb->get_col(
756 // $wpdb->prepare(
757 // "SELECT answer_title
758 // FROM {$wpdb->prefix}tutor_quiz_question_answers
759 // WHERE belongs_question_id = %d
760 // AND belongs_question_type = 'image_answering'
761 // ORDER BY answer_order asc ;",
762 // $question_id
763 // )
764 // );
765
766 // if ( is_array( $db_answer ) && count( $db_answer ) ) {
767 // $is_answer_was_correct = ( strtolower( maybe_serialize( array_values( $image_inputs ) ) ) == strtolower( maybe_serialize( $db_answer ) ) );
768 // }
769 //phpcs:enable
770 }
771
772 $question_mark = $is_answer_was_correct ? $question->question_mark : 0;
773 $total_marks += $question_mark;
774
775 $total_marks = apply_filters( 'tutor_filter_quiz_total_marks', $total_marks, $question_id, $question_type, $user_id, $attempt_id );
776
777 $answers_data = array(
778 'user_id' => $user_id,
779 'quiz_id' => $attempt->quiz_id,
780 'question_id' => $question_id,
781 'quiz_attempt_id' => $attempt_id,
782 'given_answer' => $given_answer,
783 'question_mark' => $question->question_mark,
784 'achieved_mark' => $question_mark,
785 'minus_mark' => 0,
786 'is_correct' => $is_answer_was_correct ? 1 : 0,
787 );
788
789 /**
790 * Check if question_type open ended or short ans the set
791 * is_correct default value null before saving
792 */
793 if ( in_array( $question_type, array( 'open_ended', 'short_answer', 'image_answering' ) ) ) {
794 $answers_data['is_correct'] = null;
795 $review_required = true;
796 }
797
798 $answers_data = apply_filters( 'tutor_filter_quiz_answer_data', $answers_data, $question_id, $question_type, $user_id, $attempt_id );
799
800 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answers_data );
801 }
802 }
803
804 $attempt_info = array(
805 'total_answered_questions' => tutor_utils()->count( $quiz_answers ),
806 'earned_marks' => $total_marks,
807 'attempt_status' => 'attempt_ended',
808 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
809 );
810
811 if ( $review_required ) {
812 $attempt_info['attempt_status'] = 'review_required';
813 }
814
815 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
816 }
817
818 // After hook.
819 do_action( 'tutor_quiz/attempt_ended', $attempt_id, $course_id, $user_id );
820 }
821
822
823 /**
824 * Quiz attempt will be finish here
825 *
826 * @since 1.0.0
827 *
828 * @return void
829 */
830 public function finishing_quiz_attempt() {
831
832 if ( Input::post( 'tutor_action' ) !== 'tutor_finish_quiz_attempt' ) {
833 return;
834 }
835 // Checking nonce.
836 tutor_utils()->checking_nonce();
837
838 if ( ! is_user_logged_in() ) {
839 die( 'Please sign in to do this operation' );
840 }
841
842 global $wpdb;
843
844 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
845 $attempt = tutor_utils()->is_started_quiz( $quiz_id );
846 $attempt_id = $attempt->attempt_id;
847
848 $attempt_info = array(
849 'total_answered_questions' => 0,
850 'earned_marks' => 0,
851 'attempt_status' => 'attempt_ended',
852 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
853 );
854
855 do_action( 'tutor_quiz_before_finish', $attempt_id, $quiz_id, $attempt->user_id );
856 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
857 do_action( 'tutor_quiz_finished', $attempt_id, $quiz_id, $attempt->user_id );
858
859 wp_redirect( tutor_utils()->input_old( '_wp_http_referer' ) );
860 }
861
862 /**
863 * Get quiz total marks.
864 *
865 * @since 3.0.0
866 *
867 * @param int $quiz_id quiz id.
868 *
869 * @return int|float
870 */
871 public static function get_quiz_total_marks( $quiz_id ) {
872 global $wpdb;
873
874 $total_marks = $wpdb->get_var(
875 $wpdb->prepare(
876 "SELECT SUM(question_mark) total_marks
877 FROM {$wpdb->prefix}tutor_quiz_questions
878 WHERE quiz_id=%d",
879 $quiz_id
880 )
881 );
882
883 return floatval( $total_marks );
884 }
885
886 /**
887 * Quiz timeout by ajax
888 *
889 * @since 1.0.0
890 *
891 * @return void
892 */
893 public function tutor_quiz_timeout() {
894 tutils()->checking_nonce();
895
896 global $wpdb;
897
898 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
899 $attempt = tutor_utils()->is_started_quiz( $quiz_id );
900
901 if ( $attempt ) {
902 $attempt_id = $attempt->attempt_id;
903
904 $data = array(
905 'attempt_status' => 'attempt_timeout',
906 'total_marks' => self::get_quiz_total_marks( $quiz_id ),
907 'earned_marks' => 0,
908 'attempt_ended_at' => gmdate( 'Y-m-d H:i:s', tutor_time() ),
909 );
910
911 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $data, array( 'attempt_id' => $attempt->attempt_id ) );
912
913 do_action( 'tutor_quiz_timeout', $attempt_id, $quiz_id, $attempt->user_id );
914
915 wp_send_json_success();
916 }
917
918 wp_send_json_error( __( 'Quiz has been timeout already', 'tutor' ) );
919 }
920
921 /**
922 * Review quiz answer
923 *
924 * @since 1.0.0
925 *
926 * @return void
927 */
928 public function review_quiz_answer() {
929
930 tutor_utils()->checking_nonce();
931
932 global $wpdb;
933
934 $attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
935 $context = Input::post( 'context' );
936 $attempt_answer_id = Input::post( 'attempt_answer_id', 0, Input::TYPE_INT );
937 $mark_as = Input::post( 'mark_as' );
938
939 if ( ! tutor_utils()->can_user_manage( 'attempt', $attempt_id ) || ! tutor_utils()->can_user_manage( 'attempt_answer', $attempt_answer_id ) ) {
940 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
941 }
942
943 $attempt_answer = $wpdb->get_row(
944 $wpdb->prepare(
945 "SELECT *
946 FROM {$wpdb->prefix}tutor_quiz_attempt_answers
947 WHERE attempt_answer_id = %d
948 ",
949 $attempt_answer_id
950 )
951 );
952
953 $attempt = tutor_utils()->get_attempt( $attempt_id );
954 $question = QuizModel::get_quiz_question_by_id( $attempt_answer->question_id );
955 $course_id = $attempt->course_id;
956 $student_id = $attempt->user_id;
957 $previous_ans = $attempt_answer->is_correct;
958
959 do_action( 'tutor_quiz_review_answer_before', $attempt_answer_id, $attempt_id, $mark_as );
960
961 if ( 'correct' === $mark_as ) {
962
963 $answer_update_data = array(
964 'achieved_mark' => $attempt_answer->question_mark,
965 'is_correct' => 1,
966 );
967 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
968 if ( 0 == $previous_ans || null == $previous_ans ) {
969 // if previous answer was wrong or in review then add point as correct.
970 $attempt_update_data = array(
971 'earned_marks' => $attempt->earned_marks + $attempt_answer->question_mark,
972 'is_manually_reviewed' => 1,
973 'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
974 );
975 }
976
977 if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
978 $attempt_update_data['attempt_status'] = 'attempt_ended';
979 }
980 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
981
982 } elseif ( 'incorrect' === $mark_as ) {
983
984 $answer_update_data = array(
985 'achieved_mark' => '0.00',
986 'is_correct' => 0,
987 );
988 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
989
990 if ( 1 == $previous_ans ) {
991 // If previous ans was right then mynus.
992 $attempt_update_data = array(
993 'earned_marks' => $attempt->earned_marks - $attempt_answer->question_mark,
994 'is_manually_reviewed' => 1,
995 'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ),//phpcs:ignore
996 );
997 }
998 if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
999 $attempt_update_data['attempt_status'] = 'attempt_ended';
1000 }
1001
1002 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
1003 }
1004 do_action( 'tutor_quiz_review_answer_after', $attempt_answer_id, $attempt_id, $mark_as );
1005 do_action( 'tutor_quiz/answer/review/after', $attempt_answer_id, $course_id, $student_id );
1006
1007 ob_start();
1008 tutor_load_template_from_custom_path(
1009 tutor()->path . '/views/quiz/attempt-details.php',
1010 array(
1011 'attempt_id' => $attempt_id,
1012 'user_id' => $student_id,
1013 'context' => $context,
1014 'back_url' => Input::post( 'back_url' ),
1015 )
1016 );
1017 wp_send_json_success( array( 'html' => ob_get_clean() ) );
1018 }
1019
1020 /**
1021 * Do auto course complete after review a quiz attempt.
1022 *
1023 * @since 2.4.0
1024 *
1025 * @param int $attempt_answer_id attempt answer id.
1026 * @param int $course_id course id.
1027 * @param int $user_id student id.
1028 *
1029 * @return void
1030 */
1031 public function do_auto_course_complete( $attempt_answer_id, $course_id, $user_id ) {
1032 if ( CourseModel::can_autocomplete_course( $course_id, $user_id ) ) {
1033 CourseModel::mark_course_as_completed( $course_id, $user_id );
1034 Course::set_review_popup_data( $user_id, $course_id );
1035 }
1036 }
1037
1038 /**
1039 * Quiz create and update.
1040 *
1041 * @since 1.0.0
1042 * @since 3.0.0 refactor and response change.
1043 *
1044 * @return void
1045 */
1046 public function ajax_quiz_save() {
1047 if ( ! tutor_utils()->is_nonce_verified() ) {
1048 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1049 }
1050
1051 $is_update = false;
1052 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
1053 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1054 $quiz_title = Input::post( 'quiz_title' );
1055 $quiz_description = isset( $_POST['quiz_description'] ) ? wp_kses( wp_unslash( $_POST['quiz_description'] ), $this->allowed_html ) : ''; //phpcs:ignore
1056
1057 $next_order_id = tutor_utils()->get_next_course_content_order_id( $topic_id, $quiz_id );
1058
1059 // Check edit privilege.
1060 if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
1061 $this->json_response(
1062 tutor_utils()->error_message(),
1063 null,
1064 HttpHelper::STATUS_FORBIDDEN
1065 );
1066 }
1067
1068 if ( 0 !== $topic_id && 0 !== $quiz_id ) {
1069 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1070 $this->json_response(
1071 tutor_utils()->error_message(),
1072 null,
1073 HttpHelper::STATUS_FORBIDDEN
1074 );
1075 }
1076 }
1077
1078 // Prepare quiz data to save in database.
1079 $post_arr = array(
1080 'post_type' => 'tutor_quiz',
1081 'post_title' => $quiz_title,
1082 'post_content' => $quiz_description,
1083 'post_status' => 'publish',
1084 'post_author' => get_current_user_id(),
1085 'post_parent' => $topic_id,
1086 'menu_order' => $next_order_id,
1087 );
1088
1089 if ( $quiz_id ) {
1090 $is_update = true;
1091 $post_arr['ID'] = $quiz_id;
1092 }
1093
1094 // Insert quiz and run hook.
1095 $quiz_id = wp_insert_post( $post_arr );
1096 do_action( ( $is_update ? 'tutor_quiz_updated' : 'tutor_initial_quiz_created' ), $quiz_id );
1097
1098 // Sanitize by helper method & save quiz settings.
1099 $quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] ); //phpcs:ignore
1100 update_post_meta( $quiz_id, 'tutor_quiz_option', $quiz_option );
1101 do_action( 'tutor_quiz_settings_updated', $quiz_id );
1102
1103 if ( $is_update ) {
1104 $this->json_response(
1105 __( 'Quiz updated successfully', 'tutor' ),
1106 $quiz_id
1107 );
1108 } else {
1109 $this->json_response(
1110 __( 'Quiz created successfully', 'tutor' ),
1111 $quiz_id,
1112 HttpHelper::STATUS_CREATED
1113 );
1114 }
1115 }
1116
1117 /**
1118 * Get a quiz details by id
1119 *
1120 * @return void
1121 */
1122 public function ajax_quiz_details() {
1123 tutor_utils()->check_nonce();
1124
1125 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1126 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1127 $this->json_response(
1128 tutor_utils()->error_message(),
1129 null,
1130 HttpHelper::STATUS_FORBIDDEN
1131 );
1132 }
1133
1134 $data = QuizModel::get_quiz_details( $quiz_id );
1135
1136 $this->json_response(
1137 __( 'Quiz data fetched successfully', 'tutor' ),
1138 $data
1139 );
1140 }
1141
1142 /**
1143 * Delete quiz by id
1144 *
1145 * @since 1.0.0
1146 * @since 3.0.0 refactor and response change.
1147 *
1148 * @return void
1149 */
1150 public function ajax_quiz_delete() {
1151 if ( ! tutor_utils()->is_nonce_verified() ) {
1152 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1153 }
1154
1155 global $wpdb;
1156
1157 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1158 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1159 $this->json_response(
1160 tutor_utils()->error_message(),
1161 null,
1162 HttpHelper::STATUS_FORBIDDEN
1163 );
1164 }
1165
1166 $post = get_post( $quiz_id );
1167 if ( 'tutor_quiz' !== $post->post_type ) {
1168 $this->json_response(
1169 __( 'Invalid quiz', 'tutor' ),
1170 null,
1171 HttpHelper::STATUS_BAD_REQUEST
1172 );
1173 }
1174
1175 do_action( 'tutor_delete_quiz_before', $quiz_id );
1176
1177 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $quiz_id ) );
1178 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $quiz_id ) );
1179
1180 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $quiz_id ) );
1181
1182 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
1183 $in_question_ids = QueryHelper::prepare_in_clause( $questions_ids );
1184 //phpcs:disable
1185 $wpdb->query(
1186 "DELETE
1187 FROM {$wpdb->prefix}tutor_quiz_question_answers
1188 WHERE belongs_question_id IN({$in_question_ids})
1189 "
1190 );
1191 //phpcs:enable
1192 }
1193
1194 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ) );
1195
1196 wp_delete_post( $quiz_id, true );
1197
1198 do_action( 'tutor_delete_quiz_after', $quiz_id );
1199
1200 $this->json_response(
1201 __( 'Quiz deleted successfully', 'tutor' ),
1202 $quiz_id
1203 );
1204 }
1205
1206 /**
1207 * Load quiz Modal on add/edit click
1208 *
1209 * @since 1.0.0
1210 *
1211 * @param array $params params.
1212 * @param boolean $return should return or not.
1213 *
1214 * @return mixed
1215 */
1216 public function tutor_load_quiz_builder_modal( $params = array(), $return = false ) {
1217 tutor_utils()->checking_nonce();
1218
1219 //phpcs:ignore WordPress.Security.NonceVerification.Missing
1220 $data = array_merge( $_POST, $params );
1221 $quiz_id = isset( $data['quiz_id'] ) ? sanitize_text_field( $data['quiz_id'] ) : 0;
1222 $topic_id = isset( $data['topic_id'] ) ? sanitize_text_field( $data['topic_id'] ) : 0;
1223 $quiz = $quiz_id ? get_post( $quiz_id ) : null;
1224 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1225
1226 if ( $quiz_id && ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1227 wp_send_json_error( array( 'message' => __( 'Quiz Permission Denied', 'tutor' ) ) );
1228 }
1229
1230 ob_start();
1231 include tutor()->path . 'views/modal/edit_quiz.php';
1232 $output = ob_get_clean();
1233
1234 if ( $return ) {
1235 return $output;
1236 }
1237
1238 wp_send_json_success( array( 'output' => $output ) );
1239 }
1240
1241 /**
1242 * Delete quiz question
1243 *
1244 * @since 1.0.0
1245 * @since 3.0.0 refactor and response updated.
1246 *
1247 * @return void
1248 */
1249 public function ajax_quiz_question_delete() {
1250 if ( ! tutor_utils()->is_nonce_verified() ) {
1251 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1252 }
1253
1254 global $wpdb;
1255
1256 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1257
1258 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1259 $this->json_response(
1260 tutor_utils()->error_message(),
1261 null,
1262 HttpHelper::STATUS_FORBIDDEN
1263 );
1264 }
1265
1266 if ( $question_id ) {
1267 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_id' => $question_id ) );
1268 }
1269
1270 $this->json_response(
1271 __( 'Question successfully deleted', 'tutor' ),
1272 $question_id
1273 );
1274
1275 }
1276
1277 /**
1278 * Get answers options form for quiz question
1279 *
1280 * @since 1.0.0
1281 *
1282 * @return void send wp_json response
1283 */
1284 public function tutor_quiz_question_answer_editor() {
1285 tutor_utils()->checking_nonce();
1286
1287 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1288 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1289 $quiz_option = isset( $_POST['tutor_quiz_question'] ) ? tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ) : array(); //phpcs:ignore
1290 $question = tutor_utils()->avalue_dot( $question_id, $quiz_option );
1291 $question_type = $question['question_type'];
1292
1293 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1294 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1295 }
1296
1297 if ( $answer_id ) {
1298 $old_answer = tutor_utils()->get_answer_by_id( $answer_id );
1299 //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedForeach
1300 foreach ( $old_answer as $old_answer ) {
1301 }
1302 }
1303
1304 ob_start();
1305 include tutor()->path . 'views/modal/question_answer_form.php';
1306 $output = ob_get_clean();
1307
1308 wp_send_json_success( array( 'output' => $output ) );
1309 }
1310
1311 /**
1312 * Undocumented function
1313 *
1314 * @since 1.0.0
1315 *
1316 * @param mixed $questions questions.
1317 * @param mixed $answers answers.
1318 * @param boolean $response should send json response.
1319 *
1320 * @return void
1321 */
1322 public function tutor_save_quiz_answer_options( $questions = null, $answers = null, $response = true ) {
1323 tutor_utils()->checking_nonce();
1324
1325 global $wpdb;
1326 $questions = $questions ? $questions : tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1327 $answers = $answers ? $answers : tutor_utils()->sanitize_array( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1328
1329 foreach ( $answers as $question_id => $answer ) {
1330 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1331 continue;
1332 }
1333
1334 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1335 $question_type = $question['question_type'];
1336
1337 // Getting next sorting order.
1338 $next_order_id = (int) $wpdb->get_var(
1339 $wpdb->prepare(
1340 "SELECT MAX(answer_order)
1341 FROM {$wpdb->prefix}tutor_quiz_question_answers
1342 WHERE belongs_question_id = %d
1343 AND belongs_question_type = %s
1344 ",
1345 $question_id,
1346 esc_sql( $question_type )
1347 )
1348 );
1349
1350 //phpcs:ignore Squiz.Operators.IncrementDecrementUsage.Found
1351 $next_order_id = $next_order_id + 1;
1352
1353 if ( $question ) {
1354 if ( 'true_false' === $question_type ) {
1355 $wpdb->delete(
1356 $wpdb->prefix . 'tutor_quiz_question_answers',
1357 array(
1358 'belongs_question_id' => $question_id,
1359 'belongs_question_type' => $question_type,
1360 )
1361 );
1362 $data_true_false = array(
1363 array(
1364 'belongs_question_id' => esc_sql( $question_id ),
1365 'belongs_question_type' => $question_type,
1366 'answer_title' => __( 'True', 'tutor' ),
1367 'is_correct' => 'true' == $answer['true_false'] ? 1 : 0,
1368 'answer_two_gap_match' => 'true',
1369 ),
1370 array(
1371 'belongs_question_id' => esc_sql( $question_id ),
1372 'belongs_question_type' => $question_type,
1373 'answer_title' => __( 'False', 'tutor' ),
1374 'is_correct' => 'false' === $answer['true_false'] ? 1 : 0,
1375 'answer_two_gap_match' => 'false',
1376 ),
1377 );
1378
1379 foreach ( $data_true_false as $true_false_data ) {
1380 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $true_false_data );
1381 }
1382 } elseif ( 'multiple_choice' === $question_type ||
1383 'single_choice' === $question_type ||
1384 'ordering' === $question_type ||
1385 'matching' === $question_type ||
1386 'image_matching' === $question_type ||
1387 'image_answering' === $question_type ) {
1388
1389 $answer_data = array(
1390 'belongs_question_id' => sanitize_text_field( $question_id ),
1391 'belongs_question_type' => $question_type,
1392 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1393 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1394 'answer_view_format' => isset( $answer['answer_view_format'] ) ? $answer['answer_view_format'] : 0,
1395 'answer_order' => $next_order_id,
1396 );
1397 if ( isset( $answer['matched_answer_title'] ) ) {
1398 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1399 }
1400
1401 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1402
1403 } elseif ( 'fill_in_the_blank' === $question_type ) {
1404 $wpdb->delete(
1405 $wpdb->prefix . 'tutor_quiz_question_answers',
1406 array(
1407 'belongs_question_id' => $question_id,
1408 'belongs_question_type' => $question_type,
1409 )
1410 );
1411 $answer_data = array(
1412 'belongs_question_id' => sanitize_text_field( $question_id ),
1413 'belongs_question_type' => $question_type,
1414 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1415 'answer_two_gap_match' => isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null,
1416 );
1417 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1418 }
1419 }
1420 }
1421
1422 // Send response to browser if not internal call.
1423 if ( $response ) {
1424 wp_send_json_success();
1425 exit;
1426 }
1427 }
1428
1429 /**
1430 * Tutor Update Answer
1431 *
1432 * @since 1.0.0
1433 *
1434 * @return void send wp_json response
1435 */
1436 public function tutor_update_quiz_answer_options() {
1437 tutor_utils()->checking_nonce();
1438
1439 global $wpdb;
1440
1441 $answer_id = Input::post( 'tutor_quiz_answer_id', 0, Input::TYPE_INT );
1442
1443 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1444 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1445 }
1446
1447 // Data sanitizing by helper method.
1448 $questions = tutor_sanitize_data( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1449 $answers = tutor_sanitize_data( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1450
1451 foreach ( $answers as $question_id => $answer ) {
1452 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1453 $question_type = $question['question_type'];
1454
1455 if ( $question ) {
1456 if ( 'multiple_choice' === $question_type ||
1457 'single_choice' === $question_type ||
1458 'ordering' === $question_type ||
1459 'matching' === $question_type ||
1460 'image_matching' === $question_type ||
1461 'fill_in_the_blank' === $question_type ||
1462 'image_answering' === $question_type ) {
1463
1464 $answer_data = array(
1465 'belongs_question_id' => $question_id,
1466 'belongs_question_type' => $question_type,
1467 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1468 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1469 'answer_view_format' => isset( $answer['answer_view_format'] ) ? sanitize_text_field( $answer['answer_view_format'] ) : '',
1470 );
1471 if ( isset( $answer['matched_answer_title'] ) ) {
1472 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1473 }
1474
1475 if ( 'fill_in_the_blank' === $question_type ) {
1476 $answer_data['answer_two_gap_match'] = isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null;
1477 }
1478
1479 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data, array( 'answer_id' => $answer_id ) );
1480 }
1481 }
1482 }
1483 wp_send_json_success();
1484 }
1485
1486 /**
1487 * Get answers by quiz id
1488 *
1489 * @since 1.0.0
1490 *
1491 * @param int $question_id question id.
1492 * @param mixed $question_type type of question.
1493 * @param boolean $is_correct only correct answers or not.
1494 *
1495 * @return wpdb:get_results
1496 */
1497 private function get_answers_by_q_id( $question_id, $question_type, $is_correct = false ) {
1498 global $wpdb;
1499
1500 $correct_clause = $is_correct ? ' AND is_correct=1 ' : '';
1501 //phpcs:disable
1502 return $wpdb->get_results(
1503 $wpdb->prepare(
1504 "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers
1505 WHERE belongs_question_id = %d
1506 AND belongs_question_type = %s
1507 {$correct_clause}
1508 ORDER BY answer_order ASC;
1509 ",
1510 $question_id,
1511 esc_sql( $question_type )
1512 )
1513 );
1514 //phpcs:enable
1515 }
1516
1517 /**
1518 * Quiz builder changed type
1519 *
1520 * @since 1.0.0
1521 *
1522 * @return void send wp_json response
1523 */
1524 public function tutor_quiz_builder_change_type() {
1525 tutor_utils()->checking_nonce();
1526
1527 global $wpdb;
1528 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1529 $question_type = Input::post( 'question_type' );
1530
1531 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1532 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1533 }
1534
1535 // Get question data by question ID.
1536 $question = $wpdb->get_row(
1537 $wpdb->prepare(
1538 "SELECT *
1539 FROM {$wpdb->prefix}tutor_quiz_questions
1540 WHERE question_id = %d
1541 ",
1542 $question_id
1543 )
1544 );
1545
1546 // Get answers by question ID.
1547 $answers = $this->get_answers_by_q_id( $question_id, $question_type );
1548
1549 ob_start();
1550 require tutor()->path . '/views/modal/question_answer_list.php';
1551 $output = ob_get_clean();
1552
1553 wp_send_json_success( array( 'output' => $output ) );
1554 }
1555
1556
1557 /**
1558 * Create quiz question
1559 *
1560 * @since 3.0.0
1561 *
1562 * @return void
1563 */
1564 public function ajax_quiz_question_create() {
1565 if ( ! tutor_utils()->is_nonce_verified() ) {
1566 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1567 }
1568
1569 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1570
1571 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1572 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1573 }
1574
1575 global $wpdb;
1576 $next_question_sl = QueryHelper::get_count( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ), array(), '*' ) + 1;
1577 $next_question_order = QuizModel::quiz_next_question_order_id( $quiz_id );
1578 $question_title = __( 'Question', 'tutor' ) . ' ' . $next_question_sl;
1579
1580 $new_question_data = array(
1581 'quiz_id' => $quiz_id,
1582 'question_title' => $question_title,
1583 'question_description' => '',
1584 'question_type' => 'true_false',
1585 'question_mark' => 1,
1586 'question_settings' => maybe_serialize( array() ),
1587 'question_order' => esc_sql( $next_question_order ),
1588 );
1589
1590 $new_question_data = apply_filters( 'tutor_quiz_question_data', $new_question_data );
1591
1592 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_questions', $new_question_data );
1593 $question_id = $wpdb->insert_id;
1594
1595 // Add question with default true_false type and options.
1596 $this->add_true_false_options( $question_id );
1597
1598 // Add created question object to response.
1599 $question = QuizModel::get_question( $question_id );
1600 $question->question_answers = QuizModel::get_question_answers( $question->question_id );
1601 if ( isset( $question->question_settings ) ) {
1602 $question->question_settings = maybe_unserialize( $question->question_settings );
1603 }
1604
1605 $this->json_response(
1606 __( 'Question created successfully', 'tutor' ),
1607 $question,
1608 HttpHelper::STATUS_CREATED
1609 );
1610 }
1611
1612 /**
1613 * Update question
1614 *
1615 * @since 3.0.0
1616 *
1617 * @return void
1618 */
1619 public function ajax_quiz_question_update() {
1620 if ( ! tutor_utils()->is_nonce_verified() ) {
1621 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1622 }
1623
1624 global $wpdb;
1625
1626 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1627 if ( ! $question_id ) {
1628 $this->json_response( __( 'Invalid quiz question ID', 'tutor' ), null, HttpHelper::STATUS_BAD_REQUEST );
1629 }
1630
1631 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1632 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1633 }
1634
1635 $requires_answers = array(
1636 'multiple_choice',
1637 'single_choice',
1638 'true_false',
1639 'fill_in_the_blank',
1640 'matching',
1641 'image_matching',
1642 'image_answering',
1643 'ordering',
1644 );
1645
1646 $need_correct = array(
1647 'multiple_choice',
1648 'single_choice',
1649 'true_false',
1650 );
1651
1652 $question_title = Input::post( 'question_title', '' );
1653 $question_type = Input::post( 'question_type', 'true_false' );
1654 $question_mark = Input::post( 'question_mark', 1, Input::TYPE_INT );
1655 $question_settings = Input::sanitize_array( $_POST['question_settings'] ?? array() ); //phpcs:ignore
1656
1657 add_filter( 'wp_kses_allowed_html', Input::class . '::allow_iframe', 10, 2 );
1658 $question_description = Input::post( 'question_description', '', Input::TYPE_KSES_POST );
1659 remove_filter( 'wp_kses_allowed_html', Input::class . '::allow_iframe', 10, 2 );
1660
1661 if ( in_array( $question_type, $requires_answers, true ) ) {
1662 $require_correct = in_array( $question_type, $need_correct, true );
1663 $all_answers = $this->get_answers_by_q_id( $question_id, $question_type );
1664 $correct_answers = $this->get_answers_by_q_id( $question_id, $question_type, $require_correct );
1665
1666 if ( ! empty( $all_answers ) && empty( $correct_answers ) ) {
1667 $this->json_response(
1668 __( 'Please make sure the question has answer', 'tutor' ),
1669 null,
1670 HttpHelper::STATUS_BAD_REQUEST
1671 );
1672 }
1673 }
1674
1675 if ( isset( $question_settings['question_title'] ) ) {
1676 unset( $question_settings['question_title'] );
1677 }
1678
1679 if ( isset( $question_settings['question_description'] ) ) {
1680 unset( $question_settings['question_description'] );
1681 }
1682
1683 $data = array(
1684 'question_title' => $question_title,
1685 'question_description' => $question_description,
1686 'question_type' => $question_type,
1687 'question_mark' => $question_mark,
1688 'question_settings' => maybe_serialize( $question_settings ),
1689 );
1690
1691 $data = apply_filters( 'tutor_quiz_question_data', $data );
1692
1693 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', $data, array( 'question_id' => $question_id ) );
1694
1695 $this->json_response(
1696 __( 'Question updated successfully', 'tutor' ),
1697 $question_id
1698 );
1699 }
1700
1701 /**
1702 * Save quiz questions sorting
1703 *
1704 * @since 1.0.0
1705 * @since 3.0.0 refactor and update response.
1706 *
1707 * @return void
1708 */
1709 public function ajax_quiz_question_sorting() {
1710 if ( ! tutor_utils()->is_nonce_verified() ) {
1711 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1712 }
1713
1714 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1715 $question_ids = Input::post( 'sorted_question_ids', array(), Input::TYPE_ARRAY );
1716
1717 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1718 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1719 }
1720
1721 global $wpdb;
1722
1723 $i = 0;
1724 foreach ( $question_ids as $question_id ) {
1725 $i++;
1726 $wpdb->update(
1727 $wpdb->prefix . 'tutor_quiz_questions',
1728 array( 'question_order' => $i ),
1729 array(
1730 'quiz_id' => $quiz_id,
1731 'question_id' => $question_id,
1732 )
1733 );
1734 }
1735
1736 $this->json_response( __( 'Question order successfully updated', 'tutor' ) );
1737 }
1738
1739 /**
1740 * Add true false type question answer options.
1741 *
1742 * @param int $question_id question id.
1743 *
1744 * @return void
1745 */
1746 private function add_true_false_options( $question_id ) {
1747 global $wpdb;
1748 $question_type = 'true_false';
1749
1750 $wpdb->delete(
1751 $wpdb->prefix . 'tutor_quiz_question_answers',
1752 array(
1753 'belongs_question_id' => $question_id,
1754 'belongs_question_type' => $question_type,
1755 )
1756 );
1757
1758 $data = array(
1759 array(
1760 'belongs_question_id' => $question_id,
1761 'belongs_question_type' => $question_type,
1762 'answer_title' => __( 'True', 'tutor' ),
1763 'is_correct' => 1,
1764 'answer_two_gap_match' => 'true',
1765 ),
1766 array(
1767 'belongs_question_id' => $question_id,
1768 'belongs_question_type' => $question_type,
1769 'answer_title' => __( 'False', 'tutor' ),
1770 'is_correct' => 0,
1771 'answer_two_gap_match' => 'false',
1772 ),
1773 );
1774
1775 foreach ( $data as $row ) {
1776 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $row );
1777 }
1778 }
1779
1780 /**
1781 * Save question answer
1782 *
1783 * @since 3.0.0
1784 *
1785 * @return void
1786 */
1787 public function ajax_quiz_question_answer_save() {
1788 if ( ! tutor_utils()->is_nonce_verified() ) {
1789 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1790 }
1791
1792 $is_update = false;
1793 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1794 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1795
1796 if ( $answer_id ) {
1797 $is_update = true;
1798 }
1799
1800 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1801 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1802 }
1803
1804 global $wpdb;
1805
1806 $table_question = "{$wpdb->prefix}tutor_quiz_questions";
1807 $table_answer = "{$wpdb->prefix}tutor_quiz_question_answers";
1808
1809 $question = QueryHelper::get_row( $table_question, array( 'question_id' => $question_id ), 'question_id' );
1810
1811 if ( ! $question ) {
1812 $this->json_response(
1813 __( 'Invalid question', 'tutor' ),
1814 null,
1815 HttpHelper::STATUS_BAD_REQUEST
1816 );
1817 }
1818
1819 $question_type = Input::post( 'question_type' );
1820 $answer_title = Input::post( 'answer_title', '' );
1821 $image_id = Input::post( 'image_id', 0, Input::TYPE_INT );
1822 $answer_view_format = Input::post( 'answer_view_format', '' );
1823
1824 $answer_data = array(
1825 'belongs_question_id' => $question_id,
1826 'belongs_question_type' => $question_type,
1827 'answer_title' => $answer_title,
1828 );
1829
1830 if ( ! $is_update ) {
1831 $answer_data['answer_order'] = QuizModel::get_next_answer_order( $question_id, $question_type );
1832 }
1833
1834 $question_types = array(
1835 'single_choice',
1836 'multiple_choice',
1837 'ordering',
1838 'matching',
1839 'image_matching',
1840 'image_answering',
1841 );
1842
1843 if ( in_array( $question_type, $question_types, true ) ) {
1844 $answer_data['image_id'] = $image_id;
1845 $answer_data['answer_view_format'] = $answer_view_format;
1846
1847 if ( Input::has( 'matched_answer_title' ) ) {
1848 $answer_data['answer_two_gap_match'] = Input::post( 'matched_answer_title' );
1849 }
1850 } elseif ( 'fill_in_the_blank' === $question_type ) {
1851 $answer_data['answer_two_gap_match'] = Input::post( 'answer_two_gap_match' );
1852 }
1853
1854 if ( $is_update ) {
1855 $wpdb->update( $table_answer, $answer_data, array( 'answer_id' => $answer_id ) );
1856 } else {
1857 $question_types[] = 'fill_in_the_blank';
1858 if ( ! in_array( $question_type, $question_types, true ) ) {
1859 $this->json_response( __( 'Invalid question type', 'tutor' ), null, HttpHelper::STATUS_BAD_REQUEST );
1860 }
1861
1862 $answer_data['belongs_question_type'] = Input::post( 'question_type' );
1863 $wpdb->insert( $table_answer, $answer_data );
1864 $answer_id = $wpdb->insert_id;
1865 }
1866
1867 if ( $is_update ) {
1868 $this->json_response(
1869 __( 'Question answer updated successfully', 'tutor' ),
1870 $answer_id
1871 );
1872 } else {
1873 $this->json_response(
1874 __( 'Question answer saved successfully', 'tutor' ),
1875 $answer_id,
1876 HttpHelper::STATUS_CREATED
1877 );
1878 }
1879 }
1880
1881 /**
1882 * Delete quiz question's answer
1883 *
1884 * @since 3.0.0
1885 *
1886 * @return void
1887 */
1888 public function ajax_quiz_question_answer_delete() {
1889 if ( ! tutor_utils()->is_nonce_verified() ) {
1890 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1891 }
1892
1893 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1894
1895 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1896 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1897 }
1898
1899 global $wpdb;
1900 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_id' => $answer_id ) );
1901
1902 $this->json_response( __( 'Answer deleted successfully', 'tutor' ) );
1903 }
1904
1905 /**
1906 * Quiz question's answer shorting
1907 *
1908 * @since 1.0.0
1909 * @since 3.0.0 refactor and response update.
1910 *
1911 * @return void
1912 */
1913 public function ajax_quiz_question_answer_sorting() {
1914 if ( ! tutor_utils()->is_nonce_verified() ) {
1915 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1916 }
1917
1918 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1919 $answer_ids = Input::post( 'sorted_answer_ids', array(), Input::TYPE_ARRAY );
1920
1921 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1922 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1923 }
1924
1925 global $wpdb;
1926 $i = 0;
1927 foreach ( $answer_ids as $answer_id ) {
1928 $i++;
1929 $wpdb->update(
1930 $wpdb->prefix . 'tutor_quiz_question_answers',
1931 array( 'answer_order' => $i ),
1932 array(
1933 'belongs_question_id' => $question_id,
1934 'answer_id' => $answer_id,
1935 )
1936 );
1937 }
1938
1939 $this->json_response( __( 'Question answer order successfully updated', 'tutor' ) );
1940 }
1941
1942 /**
1943 * Mark answer as correct
1944 *
1945 * @since 1.0.0
1946 * @since 3.0.0 refactor and response updated.
1947 *
1948 * @return void
1949 */
1950 public function ajax_mark_answer_as_correct() {
1951 if ( ! tutor_utils()->is_nonce_verified() ) {
1952 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1953 }
1954
1955 global $wpdb;
1956
1957 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1958
1959 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1960 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1961 }
1962
1963 // get question info.
1964 $belong_question = $wpdb->get_row(
1965 $wpdb->prepare(
1966 " SELECT belongs_question_id, belongs_question_type
1967 FROM {$wpdb->tutor_quiz_question_answers}
1968 WHERE answer_id = %d
1969 LIMIT 1
1970 ",
1971 $answer_id
1972 )
1973 );
1974
1975 if ( $belong_question ) {
1976 // if question found update all answer is_correct to 0 except post answer.
1977 $question_type = $belong_question->belongs_question_type;
1978 $question_id = $belong_question->belongs_question_id;
1979 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
1980 $update = $wpdb->query(
1981 $wpdb->prepare(
1982 "UPDATE {$wpdb->tutor_quiz_question_answers}
1983 SET is_correct = 0
1984 WHERE belongs_question_id = %d
1985 AND answer_id != %d
1986 ",
1987 $question_id,
1988 $answer_id
1989 )
1990 );
1991 }
1992 }
1993
1994 $is_correct = Input::post( 'is_correct', 0, Input::TYPE_INT );
1995
1996 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1997 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1998 }
1999
2000 $answer = $wpdb->get_row(
2001 $wpdb->prepare(
2002 "SELECT *
2003 FROM {$wpdb->prefix}tutor_quiz_question_answers
2004 WHERE answer_id = %d
2005 LIMIT 0,1 ;
2006 ",
2007 $answer_id
2008 )
2009 );
2010
2011 if ( 'single_choice' === $answer->belongs_question_type ) {
2012 $wpdb->update(
2013 $wpdb->prefix . 'tutor_quiz_question_answers',
2014 array( 'is_correct' => 0 ),
2015 array( 'belongs_question_id' => esc_sql( $answer->belongs_question_id ) )
2016 );
2017 }
2018
2019 $wpdb->update(
2020 $wpdb->prefix . 'tutor_quiz_question_answers',
2021 array( 'is_correct' => $is_correct ),
2022 array( 'answer_id' => $answer_id )
2023 );
2024
2025 $this->json_response(
2026 __( 'Answer mark as correct updated', 'tutor' ),
2027 $answer_id
2028 );
2029 }
2030
2031 /**
2032 * Rendering quiz for frontend
2033 *
2034 * @since 1.0.0
2035 *
2036 * @return void send wp_json response
2037 */
2038 public function tutor_render_quiz_content() {
2039
2040 tutor_utils()->checking_nonce();
2041
2042 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
2043
2044 if ( ! tutor_utils()->has_enrolled_content_access( 'quiz', $quiz_id ) ) {
2045 wp_send_json_error( array( 'message' => __( 'Access Denied.', 'tutor' ) ) );
2046 }
2047
2048 ob_start();
2049 global $post;
2050
2051 $post = get_post( $quiz_id ); //phpcs:ignore
2052 setup_postdata( $post );
2053
2054 single_quiz_contents();
2055 wp_reset_postdata();
2056
2057 $html = ob_get_clean();
2058 wp_send_json_success( array( 'html' => $html ) );
2059 }
2060
2061 /**
2062 * Get attempt details
2063 *
2064 * @since 1.0.0
2065 *
2066 * @param int $attempt_id required attempt id to get details.
2067 *
2068 * @return mixed object on success, null on failure
2069 */
2070 public static function attempt_details( int $attempt_id ) {
2071 global $wpdb;
2072 $attempt_details = $wpdb->get_row(
2073 $wpdb->prepare(
2074 "SELECT *
2075 FROM {$wpdb->prefix}tutor_quiz_attempts
2076 WHERE attempt_id = %d
2077 ",
2078 $attempt_id
2079 )
2080 );
2081 return $attempt_details;
2082 }
2083
2084 /**
2085 * Update quiz attempt info
2086 *
2087 * @since 1.0.0
2088 *
2089 * @param int $attempt_id attempt id.
2090 * @param mixed $attempt_info serialize data.
2091 *
2092 * @return bool, true on success, false on failure
2093 */
2094 public static function update_attempt_info( int $attempt_id, $attempt_info ) {
2095 global $wpdb;
2096 $table = $wpdb->prefix . 'tutor_quiz_attempts';
2097 $update_info = $wpdb->update(
2098 $table,
2099 array( 'attempt_info' => $attempt_info ),
2100 array( 'attempt_id' => $attempt_id )
2101 );
2102 return $update_info ? true : false;
2103 }
2104
2105 /**
2106 * Attempt delete ajax request handler
2107 *
2108 * @since 2.1.0
2109 *
2110 * @return void wp_json response
2111 */
2112 public function attempt_delete() {
2113 tutor_utils()->checking_nonce();
2114
2115 $attempt_id = Input::post( 'id', 0, Input::TYPE_INT );
2116 $attempt = tutor_utils()->get_attempt( $attempt_id );
2117 if ( ! $attempt ) {
2118 wp_send_json_error( __( 'Invalid attempt ID', 'tutor' ) );
2119 }
2120
2121 $user_id = get_current_user_id();
2122 $course_id = $attempt->course_id;
2123
2124 if ( tutor_utils()->can_user_edit_course( $user_id, $course_id ) ) {
2125 QuizModel::delete_quiz_attempt( $attempt_id );
2126 wp_send_json_success( __( 'Attempt deleted successfully!', 'tutor' ) );
2127 } else {
2128 wp_send_json_error( tutor_utils()->error_message() );
2129 }
2130 }
2131
2132 }
2133