PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.0.1
Tutor LMS – eLearning and online course solution v3.0.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 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
2159 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 // Check if h5p addon is enabled.
597 if ( tutor()->has_pro && \TutorPro\H5P\H5P::is_enabled() ) {
598 // Update the total marks to include the marks from h5p questions.
599 foreach ( $question_ids as $question_id ) {
600 $question = QuizModel::get_quiz_question_by_id( $question_id );
601 $question_type = $question->question_type;
602 $attempt_result = \TutorPro\H5P\Utils::get_h5p_quiz_result( $question_id, $user_id, $attempt_id );
603
604 if ( 'h5p' === $question_type ) {
605 if ( is_array( $attempt_result ) && count( $attempt_result ) ) {
606 $h5p_attempt_answer = $attempt_result[0];
607 $total_question_marks += $h5p_attempt_answer->max_score;
608 }
609 }
610 }
611 }
612
613 // Set the the total mark in the attempt table for the question.
614 $wpdb->update(
615 $wpdb->prefix . 'tutor_quiz_attempts',
616 array( 'total_marks' => $total_question_marks ),
617 array( 'attempt_id' => $attempt_id )
618 );
619 }
620
621 $total_marks = 0;
622 $review_required = false;
623 $quiz_answers = tutor_utils()->avalue_dot( 'quiz_question', $attempt_answer );
624
625 if ( tutor_utils()->count( $quiz_answers ) ) {
626
627 foreach ( $quiz_answers as $question_id => $answers ) {
628 $question = QuizModel::get_quiz_question_by_id( $question_id );
629 $question_type = $question->question_type;
630
631 $is_answer_was_correct = false;
632 $given_answer = '';
633
634 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
635
636 if ( ! is_numeric( $answers ) || ! $answers ) {
637 wp_send_json_error();
638 exit;
639 }
640
641 $given_answer = $answers;
642 $is_answer_was_correct = (bool) $wpdb->get_var(
643 $wpdb->prepare(
644 "SELECT is_correct
645 FROM {$wpdb->prefix}tutor_quiz_question_answers
646 WHERE answer_id = %d
647 ",
648 $answers
649 )
650 );
651
652 } elseif ( 'multiple_choice' === $question_type ) {
653
654 $given_answer = (array) ( $answers );
655
656 $given_answer = array_filter(
657 $given_answer,
658 function( $id ) {
659 return is_numeric( $id ) && $id > 0;
660 }
661 );
662 $get_original_answers = (array) $wpdb->get_col(
663 $wpdb->prepare(
664 "SELECT
665 answer_id
666 FROM
667 {$wpdb->prefix}tutor_quiz_question_answers
668 WHERE belongs_question_id = %d
669 AND belongs_question_type = %s
670 AND is_correct = 1 ;
671 ",
672 $question->question_id,
673 $question_type
674 )
675 );
676
677 if ( count( array_diff( $get_original_answers, $given_answer ) ) === 0 && count( $get_original_answers ) === count( $given_answer ) ) {
678 $is_answer_was_correct = true;
679 }
680 $given_answer = maybe_serialize( $answers );
681
682 } elseif ( 'fill_in_the_blank' === $question_type ) {
683
684 $get_original_answer = $wpdb->get_row(
685 $wpdb->prepare(
686 "SELECT *
687 FROM {$wpdb->prefix}tutor_quiz_question_answers
688 WHERE belongs_question_id = %d
689 AND belongs_question_type = %s ;
690 ",
691 $question->question_id,
692 $question_type
693 )
694 );
695
696 /**
697 * Answers stored in DB
698 */
699 $gap_answer = (array) explode( '|', $get_original_answer->answer_two_gap_match );
700 $gap_answer = maybe_serialize(
701 array_map(
702 function ( $ans ) {
703 return wp_slash( trim( $ans ) );
704 },
705 $gap_answer
706 )
707 );
708
709 /**
710 * Answers from user input
711 */
712 $given_answer = (array) array_map( 'sanitize_text_field', $answers );
713 $given_answer = maybe_serialize( $given_answer );
714
715 /**
716 * Compare answer's by making both case-insensitive.
717 */
718 if ( strtolower( $given_answer ) == strtolower( $gap_answer ) ) {
719 $is_answer_was_correct = true;
720 }
721 } elseif ( 'open_ended' === $question_type || 'short_answer' === $question_type ) {
722 $review_required = true;
723 $given_answer = wp_kses_post( $answers );
724
725 } elseif ( 'ordering' === $question_type || 'matching' === $question_type || 'image_matching' === $question_type ) {
726
727 $given_answer = (array) array_map( 'sanitize_text_field', tutor_utils()->avalue_dot( 'answers', $answers ) );
728 $given_answer = maybe_serialize( $given_answer );
729
730 $get_original_answers = (array) $wpdb->get_col(
731 $wpdb->prepare(
732 "SELECT answer_id
733 FROM {$wpdb->prefix}tutor_quiz_question_answers
734 WHERE belongs_question_id = %d
735 AND belongs_question_type = %s
736 ORDER BY answer_order ASC ;
737 ",
738 $question->question_id,
739 $question_type
740 )
741 );
742
743 $get_original_answers = array_map( 'sanitize_text_field', $get_original_answers );
744
745 if ( maybe_serialize( $get_original_answers ) == $given_answer ) {
746 $is_answer_was_correct = true;
747 }
748 } elseif ( 'image_answering' === $question_type ) {
749 $image_inputs = tutor_utils()->avalue_dot( 'answer_id', $answers );
750 $image_inputs = (array) array_map( 'sanitize_text_field', $image_inputs );
751 $given_answer = maybe_serialize( $image_inputs );
752 $is_answer_was_correct = false;
753 /**
754 * For the image_answering question type result
755 * remain pending in spite of correct answer & required
756 * review of admin/instructor. Since it's
757 * pending we need to mark it as incorrect. Otherwise if
758 * mark it correct then earned mark will be updated. then
759 * again when instructor/admin review & mark it as correct
760 * extra mark is adding. In this case, student
761 * getting double mark for the same question.
762 *
763 * For now code is commenting will be removed later on
764 *
765 * @since 2.1.5
766 */
767
768 //phpcs:disable
769
770 // $db_answer = $wpdb->get_col(
771 // $wpdb->prepare(
772 // "SELECT answer_title
773 // FROM {$wpdb->prefix}tutor_quiz_question_answers
774 // WHERE belongs_question_id = %d
775 // AND belongs_question_type = 'image_answering'
776 // ORDER BY answer_order asc ;",
777 // $question_id
778 // )
779 // );
780
781 // if ( is_array( $db_answer ) && count( $db_answer ) ) {
782 // $is_answer_was_correct = ( strtolower( maybe_serialize( array_values( $image_inputs ) ) ) == strtolower( maybe_serialize( $db_answer ) ) );
783 // }
784 //phpcs:enable
785 }
786
787 $question_mark = $is_answer_was_correct ? $question->question_mark : 0;
788 $total_marks += $question_mark;
789
790 $answers_data = array(
791 'user_id' => $user_id,
792 'quiz_id' => $attempt->quiz_id,
793 'question_id' => $question_id,
794 'quiz_attempt_id' => $attempt_id,
795 'given_answer' => $given_answer,
796 'question_mark' => $question->question_mark,
797 'achieved_mark' => $question_mark,
798 'minus_mark' => 0,
799 'is_correct' => $is_answer_was_correct ? 1 : 0,
800 );
801
802 /**
803 * Check if question_type open ended or short ans the set
804 * is_correct default value null before saving
805 */
806 if ( in_array( $question_type, array( 'open_ended', 'short_answer', 'image_answering' ) ) ) {
807 $answers_data['is_correct'] = null;
808 $review_required = true;
809 }
810 // Check if h5p addon is enabled.
811 if ( tutor()->has_pro && \TutorPro\H5P\H5P::is_enabled() ) {
812 // Check if it is a h5p question.
813 if ( 'h5p' === $question_type ) {
814 $attempt_result = \TutorPro\H5P\Utils::get_h5p_quiz_result( $question_id, $user_id, $attempt_id );
815 // Set the h5p question answer to tutor quiz attempt result.
816 if ( is_array( $attempt_result ) && count( $attempt_result ) ) {
817 $h5p_question_answer = $attempt_result[0];
818 $answers_data['question_mark'] = $h5p_question_answer->max_score;
819 $answers_data['achieved_mark'] = $h5p_question_answer->raw_score;
820 $answers_data['is_correct'] = $h5p_question_answer->max_score === $h5p_question_answer->raw_score;
821 $total_marks += $h5p_question_answer->raw_score;
822 }
823 }
824 }
825
826 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answers_data );
827 }
828 }
829
830 $attempt_info = array(
831 'total_answered_questions' => tutor_utils()->count( $quiz_answers ),
832 'earned_marks' => $total_marks,
833 'attempt_status' => 'attempt_ended',
834 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
835 );
836
837 if ( $review_required ) {
838 $attempt_info['attempt_status'] = 'review_required';
839 }
840
841 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
842 }
843
844 // After hook.
845 do_action( 'tutor_quiz/attempt_ended', $attempt_id, $course_id, $user_id );
846 }
847
848
849 /**
850 * Quiz attempt will be finish here
851 *
852 * @since 1.0.0
853 *
854 * @return void
855 */
856 public function finishing_quiz_attempt() {
857
858 if ( Input::post( 'tutor_action' ) !== 'tutor_finish_quiz_attempt' ) {
859 return;
860 }
861 // Checking nonce.
862 tutor_utils()->checking_nonce();
863
864 if ( ! is_user_logged_in() ) {
865 die( 'Please sign in to do this operation' );
866 }
867
868 global $wpdb;
869
870 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
871 $attempt = tutor_utils()->is_started_quiz( $quiz_id );
872 $attempt_id = $attempt->attempt_id;
873
874 $attempt_info = array(
875 'total_answered_questions' => 0,
876 'earned_marks' => 0,
877 'attempt_status' => 'attempt_ended',
878 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
879 );
880
881 do_action( 'tutor_quiz_before_finish', $attempt_id, $quiz_id, $attempt->user_id );
882 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
883 do_action( 'tutor_quiz_finished', $attempt_id, $quiz_id, $attempt->user_id );
884
885 wp_redirect( tutor_utils()->input_old( '_wp_http_referer' ) );
886 }
887
888 /**
889 * Get quiz total marks.
890 *
891 * @since 3.0.0
892 *
893 * @param int $quiz_id quiz id.
894 *
895 * @return int|float
896 */
897 public static function get_quiz_total_marks( $quiz_id ) {
898 global $wpdb;
899
900 $total_marks = $wpdb->get_var(
901 $wpdb->prepare(
902 "SELECT SUM(question_mark) total_marks
903 FROM {$wpdb->prefix}tutor_quiz_questions
904 WHERE quiz_id=%d",
905 $quiz_id
906 )
907 );
908
909 return floatval( $total_marks );
910 }
911
912 /**
913 * Quiz timeout by ajax
914 *
915 * @since 1.0.0
916 *
917 * @return void
918 */
919 public function tutor_quiz_timeout() {
920 tutils()->checking_nonce();
921
922 global $wpdb;
923
924 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
925 $attempt = tutor_utils()->is_started_quiz( $quiz_id );
926
927 if ( $attempt ) {
928 $attempt_id = $attempt->attempt_id;
929
930 $data = array(
931 'attempt_status' => 'attempt_timeout',
932 'total_marks' => self::get_quiz_total_marks( $quiz_id ),
933 'earned_marks' => 0,
934 'attempt_ended_at' => gmdate( 'Y-m-d H:i:s', tutor_time() ),
935 );
936
937 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $data, array( 'attempt_id' => $attempt->attempt_id ) );
938
939 do_action( 'tutor_quiz_timeout', $attempt_id, $quiz_id, $attempt->user_id );
940
941 wp_send_json_success();
942 }
943
944 wp_send_json_error( __( 'Quiz has been timeout already', 'tutor' ) );
945 }
946
947 /**
948 * Review quiz answer
949 *
950 * @since 1.0.0
951 *
952 * @return void
953 */
954 public function review_quiz_answer() {
955
956 tutor_utils()->checking_nonce();
957
958 global $wpdb;
959
960 $attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
961 $context = Input::post( 'context' );
962 $attempt_answer_id = Input::post( 'attempt_answer_id', 0, Input::TYPE_INT );
963 $mark_as = Input::post( 'mark_as' );
964
965 if ( ! tutor_utils()->can_user_manage( 'attempt', $attempt_id ) || ! tutor_utils()->can_user_manage( 'attempt_answer', $attempt_answer_id ) ) {
966 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
967 }
968
969 $attempt_answer = $wpdb->get_row(
970 $wpdb->prepare(
971 "SELECT *
972 FROM {$wpdb->prefix}tutor_quiz_attempt_answers
973 WHERE attempt_answer_id = %d
974 ",
975 $attempt_answer_id
976 )
977 );
978
979 $attempt = tutor_utils()->get_attempt( $attempt_id );
980 $question = QuizModel::get_quiz_question_by_id( $attempt_answer->question_id );
981 $course_id = $attempt->course_id;
982 $student_id = $attempt->user_id;
983 $previous_ans = $attempt_answer->is_correct;
984
985 do_action( 'tutor_quiz_review_answer_before', $attempt_answer_id, $attempt_id, $mark_as );
986
987 if ( 'correct' === $mark_as ) {
988
989 $answer_update_data = array(
990 'achieved_mark' => $attempt_answer->question_mark,
991 'is_correct' => 1,
992 );
993 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
994 if ( 0 == $previous_ans || null == $previous_ans ) {
995 // if previous answer was wrong or in review then add point as correct.
996 $attempt_update_data = array(
997 'earned_marks' => $attempt->earned_marks + $attempt_answer->question_mark,
998 'is_manually_reviewed' => 1,
999 'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
1000 );
1001 }
1002
1003 if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
1004 $attempt_update_data['attempt_status'] = 'attempt_ended';
1005 }
1006 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
1007
1008 } elseif ( 'incorrect' === $mark_as ) {
1009
1010 $answer_update_data = array(
1011 'achieved_mark' => '0.00',
1012 'is_correct' => 0,
1013 );
1014 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
1015
1016 if ( 1 == $previous_ans ) {
1017 // If previous ans was right then mynus.
1018 $attempt_update_data = array(
1019 'earned_marks' => $attempt->earned_marks - $attempt_answer->question_mark,
1020 'is_manually_reviewed' => 1,
1021 'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ),//phpcs:ignore
1022 );
1023 }
1024 if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
1025 $attempt_update_data['attempt_status'] = 'attempt_ended';
1026 }
1027
1028 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
1029 }
1030 do_action( 'tutor_quiz_review_answer_after', $attempt_answer_id, $attempt_id, $mark_as );
1031 do_action( 'tutor_quiz/answer/review/after', $attempt_answer_id, $course_id, $student_id );
1032
1033 ob_start();
1034 tutor_load_template_from_custom_path(
1035 tutor()->path . '/views/quiz/attempt-details.php',
1036 array(
1037 'attempt_id' => $attempt_id,
1038 'user_id' => $student_id,
1039 'context' => $context,
1040 'back_url' => Input::post( 'back_url' ),
1041 )
1042 );
1043 wp_send_json_success( array( 'html' => ob_get_clean() ) );
1044 }
1045
1046 /**
1047 * Do auto course complete after review a quiz attempt.
1048 *
1049 * @since 2.4.0
1050 *
1051 * @param int $attempt_answer_id attempt answer id.
1052 * @param int $course_id course id.
1053 * @param int $user_id student id.
1054 *
1055 * @return void
1056 */
1057 public function do_auto_course_complete( $attempt_answer_id, $course_id, $user_id ) {
1058 if ( CourseModel::can_autocomplete_course( $course_id, $user_id ) ) {
1059 CourseModel::mark_course_as_completed( $course_id, $user_id );
1060 Course::set_review_popup_data( $user_id, $course_id );
1061 }
1062 }
1063
1064 /**
1065 * Quiz create and update.
1066 *
1067 * @since 1.0.0
1068 * @since 3.0.0 refactor and response change.
1069 *
1070 * @return void
1071 */
1072 public function ajax_quiz_save() {
1073 if ( ! tutor_utils()->is_nonce_verified() ) {
1074 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1075 }
1076
1077 $is_update = false;
1078 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
1079 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1080 $quiz_title = Input::post( 'quiz_title' );
1081 $quiz_description = isset( $_POST['quiz_description'] ) ? wp_kses( wp_unslash( $_POST['quiz_description'] ), $this->allowed_html ) : ''; //phpcs:ignore
1082
1083 $next_order_id = tutor_utils()->get_next_course_content_order_id( $topic_id, $quiz_id );
1084
1085 // Check edit privilege.
1086 if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
1087 $this->json_response(
1088 tutor_utils()->error_message(),
1089 null,
1090 HttpHelper::STATUS_FORBIDDEN
1091 );
1092 }
1093
1094 if ( 0 !== $topic_id && 0 !== $quiz_id ) {
1095 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1096 $this->json_response(
1097 tutor_utils()->error_message(),
1098 null,
1099 HttpHelper::STATUS_FORBIDDEN
1100 );
1101 }
1102 }
1103
1104 // Prepare quiz data to save in database.
1105 $post_arr = array(
1106 'post_type' => 'tutor_quiz',
1107 'post_title' => $quiz_title,
1108 'post_content' => $quiz_description,
1109 'post_status' => 'publish',
1110 'post_author' => get_current_user_id(),
1111 'post_parent' => $topic_id,
1112 'menu_order' => $next_order_id,
1113 );
1114
1115 if ( $quiz_id ) {
1116 $is_update = true;
1117 $post_arr['ID'] = $quiz_id;
1118 }
1119
1120 // Insert quiz and run hook.
1121 $quiz_id = wp_insert_post( $post_arr );
1122 do_action( ( $is_update ? 'tutor_quiz_updated' : 'tutor_initial_quiz_created' ), $quiz_id );
1123
1124 // Sanitize by helper method & save quiz settings.
1125 $quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] ); //phpcs:ignore
1126 update_post_meta( $quiz_id, 'tutor_quiz_option', $quiz_option );
1127 do_action( 'tutor_quiz_settings_updated', $quiz_id );
1128
1129 if ( $is_update ) {
1130 $this->json_response(
1131 __( 'Quiz updated successfully', 'tutor' ),
1132 $quiz_id
1133 );
1134 } else {
1135 $this->json_response(
1136 __( 'Quiz created successfully', 'tutor' ),
1137 $quiz_id,
1138 HttpHelper::STATUS_CREATED
1139 );
1140 }
1141 }
1142
1143 /**
1144 * Get a quiz details by id
1145 *
1146 * @return void
1147 */
1148 public function ajax_quiz_details() {
1149 tutor_utils()->check_nonce();
1150
1151 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1152 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1153 $this->json_response(
1154 tutor_utils()->error_message(),
1155 null,
1156 HttpHelper::STATUS_FORBIDDEN
1157 );
1158 }
1159
1160 $data = QuizModel::get_quiz_details( $quiz_id );
1161
1162 $this->json_response(
1163 __( 'Quiz data fetched successfully', 'tutor' ),
1164 $data
1165 );
1166 }
1167
1168 /**
1169 * Delete quiz by id
1170 *
1171 * @since 1.0.0
1172 * @since 3.0.0 refactor and response change.
1173 *
1174 * @return void
1175 */
1176 public function ajax_quiz_delete() {
1177 if ( ! tutor_utils()->is_nonce_verified() ) {
1178 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1179 }
1180
1181 global $wpdb;
1182
1183 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1184 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1185 $this->json_response(
1186 tutor_utils()->error_message(),
1187 null,
1188 HttpHelper::STATUS_FORBIDDEN
1189 );
1190 }
1191
1192 $post = get_post( $quiz_id );
1193 if ( 'tutor_quiz' !== $post->post_type ) {
1194 $this->json_response(
1195 __( 'Invalid quiz', 'tutor' ),
1196 null,
1197 HttpHelper::STATUS_BAD_REQUEST
1198 );
1199 }
1200
1201 do_action( 'tutor_delete_quiz_before', $quiz_id );
1202
1203 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $quiz_id ) );
1204 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $quiz_id ) );
1205
1206 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $quiz_id ) );
1207
1208 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
1209 $in_question_ids = QueryHelper::prepare_in_clause( $questions_ids );
1210 //phpcs:disable
1211 $wpdb->query(
1212 "DELETE
1213 FROM {$wpdb->prefix}tutor_quiz_question_answers
1214 WHERE belongs_question_id IN({$in_question_ids})
1215 "
1216 );
1217 //phpcs:enable
1218 }
1219
1220 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ) );
1221
1222 wp_delete_post( $quiz_id, true );
1223
1224 do_action( 'tutor_delete_quiz_after', $quiz_id );
1225
1226 $this->json_response(
1227 __( 'Quiz deleted successfully', 'tutor' ),
1228 $quiz_id
1229 );
1230 }
1231
1232 /**
1233 * Load quiz Modal on add/edit click
1234 *
1235 * @since 1.0.0
1236 *
1237 * @param array $params params.
1238 * @param boolean $return should return or not.
1239 *
1240 * @return mixed
1241 */
1242 public function tutor_load_quiz_builder_modal( $params = array(), $return = false ) {
1243 tutor_utils()->checking_nonce();
1244
1245 //phpcs:ignore WordPress.Security.NonceVerification.Missing
1246 $data = array_merge( $_POST, $params );
1247 $quiz_id = isset( $data['quiz_id'] ) ? sanitize_text_field( $data['quiz_id'] ) : 0;
1248 $topic_id = isset( $data['topic_id'] ) ? sanitize_text_field( $data['topic_id'] ) : 0;
1249 $quiz = $quiz_id ? get_post( $quiz_id ) : null;
1250 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1251
1252 if ( $quiz_id && ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1253 wp_send_json_error( array( 'message' => __( 'Quiz Permission Denied', 'tutor' ) ) );
1254 }
1255
1256 ob_start();
1257 include tutor()->path . 'views/modal/edit_quiz.php';
1258 $output = ob_get_clean();
1259
1260 if ( $return ) {
1261 return $output;
1262 }
1263
1264 wp_send_json_success( array( 'output' => $output ) );
1265 }
1266
1267 /**
1268 * Delete quiz question
1269 *
1270 * @since 1.0.0
1271 * @since 3.0.0 refactor and response updated.
1272 *
1273 * @return void
1274 */
1275 public function ajax_quiz_question_delete() {
1276 if ( ! tutor_utils()->is_nonce_verified() ) {
1277 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1278 }
1279
1280 global $wpdb;
1281
1282 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1283
1284 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1285 $this->json_response(
1286 tutor_utils()->error_message(),
1287 null,
1288 HttpHelper::STATUS_FORBIDDEN
1289 );
1290 }
1291
1292 if ( $question_id ) {
1293 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_id' => $question_id ) );
1294 }
1295
1296 $this->json_response(
1297 __( 'Question successfully deleted', 'tutor' ),
1298 $question_id
1299 );
1300
1301 }
1302
1303 /**
1304 * Get answers options form for quiz question
1305 *
1306 * @since 1.0.0
1307 *
1308 * @return void send wp_json response
1309 */
1310 public function tutor_quiz_question_answer_editor() {
1311 tutor_utils()->checking_nonce();
1312
1313 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1314 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1315 $quiz_option = isset( $_POST['tutor_quiz_question'] ) ? tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ) : array(); //phpcs:ignore
1316 $question = tutor_utils()->avalue_dot( $question_id, $quiz_option );
1317 $question_type = $question['question_type'];
1318
1319 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1320 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1321 }
1322
1323 if ( $answer_id ) {
1324 $old_answer = tutor_utils()->get_answer_by_id( $answer_id );
1325 //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedForeach
1326 foreach ( $old_answer as $old_answer ) {
1327 }
1328 }
1329
1330 ob_start();
1331 include tutor()->path . 'views/modal/question_answer_form.php';
1332 $output = ob_get_clean();
1333
1334 wp_send_json_success( array( 'output' => $output ) );
1335 }
1336
1337 /**
1338 * Undocumented function
1339 *
1340 * @since 1.0.0
1341 *
1342 * @param mixed $questions questions.
1343 * @param mixed $answers answers.
1344 * @param boolean $response should send json response.
1345 *
1346 * @return void
1347 */
1348 public function tutor_save_quiz_answer_options( $questions = null, $answers = null, $response = true ) {
1349 tutor_utils()->checking_nonce();
1350
1351 global $wpdb;
1352 $questions = $questions ? $questions : tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1353 $answers = $answers ? $answers : tutor_utils()->sanitize_array( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1354
1355 foreach ( $answers as $question_id => $answer ) {
1356 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1357 continue;
1358 }
1359
1360 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1361 $question_type = $question['question_type'];
1362
1363 // Getting next sorting order.
1364 $next_order_id = (int) $wpdb->get_var(
1365 $wpdb->prepare(
1366 "SELECT MAX(answer_order)
1367 FROM {$wpdb->prefix}tutor_quiz_question_answers
1368 WHERE belongs_question_id = %d
1369 AND belongs_question_type = %s
1370 ",
1371 $question_id,
1372 esc_sql( $question_type )
1373 )
1374 );
1375
1376 //phpcs:ignore Squiz.Operators.IncrementDecrementUsage.Found
1377 $next_order_id = $next_order_id + 1;
1378
1379 if ( $question ) {
1380 if ( 'true_false' === $question_type ) {
1381 $wpdb->delete(
1382 $wpdb->prefix . 'tutor_quiz_question_answers',
1383 array(
1384 'belongs_question_id' => $question_id,
1385 'belongs_question_type' => $question_type,
1386 )
1387 );
1388 $data_true_false = array(
1389 array(
1390 'belongs_question_id' => esc_sql( $question_id ),
1391 'belongs_question_type' => $question_type,
1392 'answer_title' => __( 'True', 'tutor' ),
1393 'is_correct' => 'true' == $answer['true_false'] ? 1 : 0,
1394 'answer_two_gap_match' => 'true',
1395 ),
1396 array(
1397 'belongs_question_id' => esc_sql( $question_id ),
1398 'belongs_question_type' => $question_type,
1399 'answer_title' => __( 'False', 'tutor' ),
1400 'is_correct' => 'false' === $answer['true_false'] ? 1 : 0,
1401 'answer_two_gap_match' => 'false',
1402 ),
1403 );
1404
1405 foreach ( $data_true_false as $true_false_data ) {
1406 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $true_false_data );
1407 }
1408 } elseif ( 'multiple_choice' === $question_type ||
1409 'single_choice' === $question_type ||
1410 'ordering' === $question_type ||
1411 'matching' === $question_type ||
1412 'image_matching' === $question_type ||
1413 'image_answering' === $question_type ) {
1414
1415 $answer_data = array(
1416 'belongs_question_id' => sanitize_text_field( $question_id ),
1417 'belongs_question_type' => $question_type,
1418 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1419 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1420 'answer_view_format' => isset( $answer['answer_view_format'] ) ? $answer['answer_view_format'] : 0,
1421 'answer_order' => $next_order_id,
1422 );
1423 if ( isset( $answer['matched_answer_title'] ) ) {
1424 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1425 }
1426
1427 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1428
1429 } elseif ( 'fill_in_the_blank' === $question_type ) {
1430 $wpdb->delete(
1431 $wpdb->prefix . 'tutor_quiz_question_answers',
1432 array(
1433 'belongs_question_id' => $question_id,
1434 'belongs_question_type' => $question_type,
1435 )
1436 );
1437 $answer_data = array(
1438 'belongs_question_id' => sanitize_text_field( $question_id ),
1439 'belongs_question_type' => $question_type,
1440 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1441 'answer_two_gap_match' => isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null,
1442 );
1443 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1444 }
1445 }
1446 }
1447
1448 // Send response to browser if not internal call.
1449 if ( $response ) {
1450 wp_send_json_success();
1451 exit;
1452 }
1453 }
1454
1455 /**
1456 * Tutor Update Answer
1457 *
1458 * @since 1.0.0
1459 *
1460 * @return void send wp_json response
1461 */
1462 public function tutor_update_quiz_answer_options() {
1463 tutor_utils()->checking_nonce();
1464
1465 global $wpdb;
1466
1467 $answer_id = Input::post( 'tutor_quiz_answer_id', 0, Input::TYPE_INT );
1468
1469 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1470 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1471 }
1472
1473 // Data sanitizing by helper method.
1474 $questions = tutor_sanitize_data( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1475 $answers = tutor_sanitize_data( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1476
1477 foreach ( $answers as $question_id => $answer ) {
1478 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1479 $question_type = $question['question_type'];
1480
1481 if ( $question ) {
1482 if ( 'multiple_choice' === $question_type ||
1483 'single_choice' === $question_type ||
1484 'ordering' === $question_type ||
1485 'matching' === $question_type ||
1486 'image_matching' === $question_type ||
1487 'fill_in_the_blank' === $question_type ||
1488 'image_answering' === $question_type ) {
1489
1490 $answer_data = array(
1491 'belongs_question_id' => $question_id,
1492 'belongs_question_type' => $question_type,
1493 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1494 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1495 'answer_view_format' => isset( $answer['answer_view_format'] ) ? sanitize_text_field( $answer['answer_view_format'] ) : '',
1496 );
1497 if ( isset( $answer['matched_answer_title'] ) ) {
1498 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1499 }
1500
1501 if ( 'fill_in_the_blank' === $question_type ) {
1502 $answer_data['answer_two_gap_match'] = isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null;
1503 }
1504
1505 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data, array( 'answer_id' => $answer_id ) );
1506 }
1507 }
1508 }
1509 wp_send_json_success();
1510 }
1511
1512 /**
1513 * Get answers by quiz id
1514 *
1515 * @since 1.0.0
1516 *
1517 * @param int $question_id question id.
1518 * @param mixed $question_type type of question.
1519 * @param boolean $is_correct only correct answers or not.
1520 *
1521 * @return wpdb:get_results
1522 */
1523 private function get_answers_by_q_id( $question_id, $question_type, $is_correct = false ) {
1524 global $wpdb;
1525
1526 $correct_clause = $is_correct ? ' AND is_correct=1 ' : '';
1527 //phpcs:disable
1528 return $wpdb->get_results(
1529 $wpdb->prepare(
1530 "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers
1531 WHERE belongs_question_id = %d
1532 AND belongs_question_type = %s
1533 {$correct_clause}
1534 ORDER BY answer_order ASC;
1535 ",
1536 $question_id,
1537 esc_sql( $question_type )
1538 )
1539 );
1540 //phpcs:enable
1541 }
1542
1543 /**
1544 * Quiz builder changed type
1545 *
1546 * @since 1.0.0
1547 *
1548 * @return void send wp_json response
1549 */
1550 public function tutor_quiz_builder_change_type() {
1551 tutor_utils()->checking_nonce();
1552
1553 global $wpdb;
1554 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1555 $question_type = Input::post( 'question_type' );
1556
1557 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1558 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1559 }
1560
1561 // Get question data by question ID.
1562 $question = $wpdb->get_row(
1563 $wpdb->prepare(
1564 "SELECT *
1565 FROM {$wpdb->prefix}tutor_quiz_questions
1566 WHERE question_id = %d
1567 ",
1568 $question_id
1569 )
1570 );
1571
1572 // Get answers by question ID.
1573 $answers = $this->get_answers_by_q_id( $question_id, $question_type );
1574
1575 ob_start();
1576 require tutor()->path . '/views/modal/question_answer_list.php';
1577 $output = ob_get_clean();
1578
1579 wp_send_json_success( array( 'output' => $output ) );
1580 }
1581
1582
1583 /**
1584 * Create quiz question
1585 *
1586 * @since 3.0.0
1587 *
1588 * @return void
1589 */
1590 public function ajax_quiz_question_create() {
1591 if ( ! tutor_utils()->is_nonce_verified() ) {
1592 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1593 }
1594
1595 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1596
1597 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1598 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1599 }
1600
1601 global $wpdb;
1602 $next_question_sl = QueryHelper::get_count( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ), array(), '*' ) + 1;
1603 $next_question_order = QuizModel::quiz_next_question_order_id( $quiz_id );
1604 $question_title = __( 'Question', 'tutor' ) . ' ' . $next_question_sl;
1605
1606 $new_question_data = array(
1607 'quiz_id' => $quiz_id,
1608 'question_title' => $question_title,
1609 'question_description' => '',
1610 'question_type' => 'true_false',
1611 'question_mark' => 1,
1612 'question_settings' => maybe_serialize( array() ),
1613 'question_order' => esc_sql( $next_question_order ),
1614 );
1615
1616 $new_question_data = apply_filters( 'tutor_quiz_question_data', $new_question_data );
1617
1618 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_questions', $new_question_data );
1619 $question_id = $wpdb->insert_id;
1620
1621 // Add question with default true_false type and options.
1622 $this->add_true_false_options( $question_id );
1623
1624 // Add created question object to response.
1625 $question = QuizModel::get_question( $question_id );
1626 $question->question_answers = QuizModel::get_question_answers( $question->question_id );
1627 if ( isset( $question->question_settings ) ) {
1628 $question->question_settings = maybe_unserialize( $question->question_settings );
1629 }
1630
1631 $this->json_response(
1632 __( 'Question created successfully', 'tutor' ),
1633 $question,
1634 HttpHelper::STATUS_CREATED
1635 );
1636 }
1637
1638 /**
1639 * Update question
1640 *
1641 * @since 3.0.0
1642 *
1643 * @return void
1644 */
1645 public function ajax_quiz_question_update() {
1646 if ( ! tutor_utils()->is_nonce_verified() ) {
1647 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1648 }
1649
1650 global $wpdb;
1651
1652 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1653 if ( ! $question_id ) {
1654 $this->json_response( __( 'Invalid quiz question ID', 'tutor' ), null, HttpHelper::STATUS_BAD_REQUEST );
1655 }
1656
1657 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1658 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1659 }
1660
1661 $requires_answers = array(
1662 'multiple_choice',
1663 'single_choice',
1664 'true_false',
1665 'fill_in_the_blank',
1666 'matching',
1667 'image_matching',
1668 'image_answering',
1669 'ordering',
1670 );
1671
1672 $need_correct = array(
1673 'multiple_choice',
1674 'single_choice',
1675 'true_false',
1676 );
1677
1678 $question_title = Input::post( 'question_title', '' );
1679 $question_type = Input::post( 'question_type', 'true_false' );
1680 $question_mark = Input::post( 'question_mark', 1, Input::TYPE_INT );
1681 $question_settings = Input::sanitize_array( $_POST['question_settings'] ?? array() ); //phpcs:ignore
1682
1683 add_filter( 'wp_kses_allowed_html', Input::class . '::allow_iframe', 10, 2 );
1684 $question_description = Input::post( 'question_description', '', Input::TYPE_KSES_POST );
1685 remove_filter( 'wp_kses_allowed_html', Input::class . '::allow_iframe', 10, 2 );
1686
1687 if ( in_array( $question_type, $requires_answers, true ) ) {
1688 $require_correct = in_array( $question_type, $need_correct, true );
1689 $all_answers = $this->get_answers_by_q_id( $question_id, $question_type );
1690 $correct_answers = $this->get_answers_by_q_id( $question_id, $question_type, $require_correct );
1691
1692 if ( ! empty( $all_answers ) && empty( $correct_answers ) ) {
1693 $this->json_response(
1694 __( 'Please make sure the question has answer', 'tutor' ),
1695 null,
1696 HttpHelper::STATUS_BAD_REQUEST
1697 );
1698 }
1699 }
1700
1701 if ( isset( $question_settings['question_title'] ) ) {
1702 unset( $question_settings['question_title'] );
1703 }
1704
1705 if ( isset( $question_settings['question_description'] ) ) {
1706 unset( $question_settings['question_description'] );
1707 }
1708
1709 $data = array(
1710 'question_title' => $question_title,
1711 'question_description' => $question_description,
1712 'question_type' => $question_type,
1713 'question_mark' => $question_mark,
1714 'question_settings' => maybe_serialize( $question_settings ),
1715 );
1716
1717 $data = apply_filters( 'tutor_quiz_question_data', $data );
1718
1719 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', $data, array( 'question_id' => $question_id ) );
1720
1721 $this->json_response(
1722 __( 'Question updated successfully', 'tutor' ),
1723 $question_id
1724 );
1725 }
1726
1727 /**
1728 * Save quiz questions sorting
1729 *
1730 * @since 1.0.0
1731 * @since 3.0.0 refactor and update response.
1732 *
1733 * @return void
1734 */
1735 public function ajax_quiz_question_sorting() {
1736 if ( ! tutor_utils()->is_nonce_verified() ) {
1737 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1738 }
1739
1740 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1741 $question_ids = Input::post( 'sorted_question_ids', array(), Input::TYPE_ARRAY );
1742
1743 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1744 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1745 }
1746
1747 global $wpdb;
1748
1749 $i = 0;
1750 foreach ( $question_ids as $question_id ) {
1751 $i++;
1752 $wpdb->update(
1753 $wpdb->prefix . 'tutor_quiz_questions',
1754 array( 'question_order' => $i ),
1755 array(
1756 'quiz_id' => $quiz_id,
1757 'question_id' => $question_id,
1758 )
1759 );
1760 }
1761
1762 $this->json_response( __( 'Question order successfully updated', 'tutor' ) );
1763 }
1764
1765 /**
1766 * Add true false type question answer options.
1767 *
1768 * @param int $question_id question id.
1769 *
1770 * @return void
1771 */
1772 private function add_true_false_options( $question_id ) {
1773 global $wpdb;
1774 $question_type = 'true_false';
1775
1776 $wpdb->delete(
1777 $wpdb->prefix . 'tutor_quiz_question_answers',
1778 array(
1779 'belongs_question_id' => $question_id,
1780 'belongs_question_type' => $question_type,
1781 )
1782 );
1783
1784 $data = array(
1785 array(
1786 'belongs_question_id' => $question_id,
1787 'belongs_question_type' => $question_type,
1788 'answer_title' => __( 'True', 'tutor' ),
1789 'is_correct' => 1,
1790 'answer_two_gap_match' => 'true',
1791 ),
1792 array(
1793 'belongs_question_id' => $question_id,
1794 'belongs_question_type' => $question_type,
1795 'answer_title' => __( 'False', 'tutor' ),
1796 'is_correct' => 0,
1797 'answer_two_gap_match' => 'false',
1798 ),
1799 );
1800
1801 foreach ( $data as $row ) {
1802 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $row );
1803 }
1804 }
1805
1806 /**
1807 * Save question answer
1808 *
1809 * @since 3.0.0
1810 *
1811 * @return void
1812 */
1813 public function ajax_quiz_question_answer_save() {
1814 if ( ! tutor_utils()->is_nonce_verified() ) {
1815 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1816 }
1817
1818 $is_update = false;
1819 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1820 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1821
1822 if ( $answer_id ) {
1823 $is_update = true;
1824 }
1825
1826 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1827 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1828 }
1829
1830 global $wpdb;
1831
1832 $table_question = "{$wpdb->prefix}tutor_quiz_questions";
1833 $table_answer = "{$wpdb->prefix}tutor_quiz_question_answers";
1834
1835 $question = QueryHelper::get_row( $table_question, array( 'question_id' => $question_id ), 'question_id' );
1836
1837 if ( ! $question ) {
1838 $this->json_response(
1839 __( 'Invalid question', 'tutor' ),
1840 null,
1841 HttpHelper::STATUS_BAD_REQUEST
1842 );
1843 }
1844
1845 $question_type = Input::post( 'question_type' );
1846 $answer_title = Input::post( 'answer_title', '' );
1847 $image_id = Input::post( 'image_id', 0, Input::TYPE_INT );
1848 $answer_view_format = Input::post( 'answer_view_format', '' );
1849
1850 $answer_data = array(
1851 'belongs_question_id' => $question_id,
1852 'belongs_question_type' => $question_type,
1853 'answer_title' => $answer_title,
1854 );
1855
1856 if ( ! $is_update ) {
1857 $answer_data['answer_order'] = QuizModel::get_next_answer_order( $question_id, $question_type );
1858 }
1859
1860 $question_types = array(
1861 'single_choice',
1862 'multiple_choice',
1863 'ordering',
1864 'matching',
1865 'image_matching',
1866 'image_answering',
1867 );
1868
1869 if ( in_array( $question_type, $question_types, true ) ) {
1870 $answer_data['image_id'] = $image_id;
1871 $answer_data['answer_view_format'] = $answer_view_format;
1872
1873 if ( Input::has( 'matched_answer_title' ) ) {
1874 $answer_data['answer_two_gap_match'] = Input::post( 'matched_answer_title' );
1875 }
1876 } elseif ( 'fill_in_the_blank' === $question_type ) {
1877 $answer_data['answer_two_gap_match'] = Input::post( 'answer_two_gap_match' );
1878 }
1879
1880 if ( $is_update ) {
1881 $wpdb->update( $table_answer, $answer_data, array( 'answer_id' => $answer_id ) );
1882 } else {
1883 $question_types[] = 'fill_in_the_blank';
1884 if ( ! in_array( $question_type, $question_types, true ) ) {
1885 $this->json_response( __( 'Invalid question type', 'tutor' ), null, HttpHelper::STATUS_BAD_REQUEST );
1886 }
1887
1888 $answer_data['belongs_question_type'] = Input::post( 'question_type' );
1889 $wpdb->insert( $table_answer, $answer_data );
1890 $answer_id = $wpdb->insert_id;
1891 }
1892
1893 if ( $is_update ) {
1894 $this->json_response(
1895 __( 'Question answer updated successfully', 'tutor' ),
1896 $answer_id
1897 );
1898 } else {
1899 $this->json_response(
1900 __( 'Question answer saved successfully', 'tutor' ),
1901 $answer_id,
1902 HttpHelper::STATUS_CREATED
1903 );
1904 }
1905 }
1906
1907 /**
1908 * Delete quiz question's answer
1909 *
1910 * @since 3.0.0
1911 *
1912 * @return void
1913 */
1914 public function ajax_quiz_question_answer_delete() {
1915 if ( ! tutor_utils()->is_nonce_verified() ) {
1916 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1917 }
1918
1919 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1920
1921 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1922 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1923 }
1924
1925 global $wpdb;
1926 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_id' => $answer_id ) );
1927
1928 $this->json_response( __( 'Answer deleted successfully', 'tutor' ) );
1929 }
1930
1931 /**
1932 * Quiz question's answer shorting
1933 *
1934 * @since 1.0.0
1935 * @since 3.0.0 refactor and response update.
1936 *
1937 * @return void
1938 */
1939 public function ajax_quiz_question_answer_sorting() {
1940 if ( ! tutor_utils()->is_nonce_verified() ) {
1941 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1942 }
1943
1944 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1945 $answer_ids = Input::post( 'sorted_answer_ids', array(), Input::TYPE_ARRAY );
1946
1947 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1948 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1949 }
1950
1951 global $wpdb;
1952 $i = 0;
1953 foreach ( $answer_ids as $answer_id ) {
1954 $i++;
1955 $wpdb->update(
1956 $wpdb->prefix . 'tutor_quiz_question_answers',
1957 array( 'answer_order' => $i ),
1958 array(
1959 'belongs_question_id' => $question_id,
1960 'answer_id' => $answer_id,
1961 )
1962 );
1963 }
1964
1965 $this->json_response( __( 'Question answer order successfully updated', 'tutor' ) );
1966 }
1967
1968 /**
1969 * Mark answer as correct
1970 *
1971 * @since 1.0.0
1972 * @since 3.0.0 refactor and response updated.
1973 *
1974 * @return void
1975 */
1976 public function ajax_mark_answer_as_correct() {
1977 if ( ! tutor_utils()->is_nonce_verified() ) {
1978 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1979 }
1980
1981 global $wpdb;
1982
1983 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1984
1985 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1986 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1987 }
1988
1989 // get question info.
1990 $belong_question = $wpdb->get_row(
1991 $wpdb->prepare(
1992 " SELECT belongs_question_id, belongs_question_type
1993 FROM {$wpdb->tutor_quiz_question_answers}
1994 WHERE answer_id = %d
1995 LIMIT 1
1996 ",
1997 $answer_id
1998 )
1999 );
2000
2001 if ( $belong_question ) {
2002 // if question found update all answer is_correct to 0 except post answer.
2003 $question_type = $belong_question->belongs_question_type;
2004 $question_id = $belong_question->belongs_question_id;
2005 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
2006 $update = $wpdb->query(
2007 $wpdb->prepare(
2008 "UPDATE {$wpdb->tutor_quiz_question_answers}
2009 SET is_correct = 0
2010 WHERE belongs_question_id = %d
2011 AND answer_id != %d
2012 ",
2013 $question_id,
2014 $answer_id
2015 )
2016 );
2017 }
2018 }
2019
2020 $is_correct = Input::post( 'is_correct', 0, Input::TYPE_INT );
2021
2022 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
2023 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2024 }
2025
2026 $answer = $wpdb->get_row(
2027 $wpdb->prepare(
2028 "SELECT *
2029 FROM {$wpdb->prefix}tutor_quiz_question_answers
2030 WHERE answer_id = %d
2031 LIMIT 0,1 ;
2032 ",
2033 $answer_id
2034 )
2035 );
2036
2037 if ( 'single_choice' === $answer->belongs_question_type ) {
2038 $wpdb->update(
2039 $wpdb->prefix . 'tutor_quiz_question_answers',
2040 array( 'is_correct' => 0 ),
2041 array( 'belongs_question_id' => esc_sql( $answer->belongs_question_id ) )
2042 );
2043 }
2044
2045 $wpdb->update(
2046 $wpdb->prefix . 'tutor_quiz_question_answers',
2047 array( 'is_correct' => $is_correct ),
2048 array( 'answer_id' => $answer_id )
2049 );
2050
2051 $this->json_response(
2052 __( 'Answer mark as correct updated', 'tutor' ),
2053 $answer_id
2054 );
2055 }
2056
2057 /**
2058 * Rendering quiz for frontend
2059 *
2060 * @since 1.0.0
2061 *
2062 * @return void send wp_json response
2063 */
2064 public function tutor_render_quiz_content() {
2065
2066 tutor_utils()->checking_nonce();
2067
2068 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
2069
2070 if ( ! tutor_utils()->has_enrolled_content_access( 'quiz', $quiz_id ) ) {
2071 wp_send_json_error( array( 'message' => __( 'Access Denied.', 'tutor' ) ) );
2072 }
2073
2074 ob_start();
2075 global $post;
2076
2077 $post = get_post( $quiz_id ); //phpcs:ignore
2078 setup_postdata( $post );
2079
2080 single_quiz_contents();
2081 wp_reset_postdata();
2082
2083 $html = ob_get_clean();
2084 wp_send_json_success( array( 'html' => $html ) );
2085 }
2086
2087 /**
2088 * Get attempt details
2089 *
2090 * @since 1.0.0
2091 *
2092 * @param int $attempt_id required attempt id to get details.
2093 *
2094 * @return mixed object on success, null on failure
2095 */
2096 public static function attempt_details( int $attempt_id ) {
2097 global $wpdb;
2098 $attempt_details = $wpdb->get_row(
2099 $wpdb->prepare(
2100 "SELECT *
2101 FROM {$wpdb->prefix}tutor_quiz_attempts
2102 WHERE attempt_id = %d
2103 ",
2104 $attempt_id
2105 )
2106 );
2107 return $attempt_details;
2108 }
2109
2110 /**
2111 * Update quiz attempt info
2112 *
2113 * @since 1.0.0
2114 *
2115 * @param int $attempt_id attempt id.
2116 * @param mixed $attempt_info serialize data.
2117 *
2118 * @return bool, true on success, false on failure
2119 */
2120 public static function update_attempt_info( int $attempt_id, $attempt_info ) {
2121 global $wpdb;
2122 $table = $wpdb->prefix . 'tutor_quiz_attempts';
2123 $update_info = $wpdb->update(
2124 $table,
2125 array( 'attempt_info' => $attempt_info ),
2126 array( 'attempt_id' => $attempt_id )
2127 );
2128 return $update_info ? true : false;
2129 }
2130
2131 /**
2132 * Attempt delete ajax request handler
2133 *
2134 * @since 2.1.0
2135 *
2136 * @return void wp_json response
2137 */
2138 public function attempt_delete() {
2139 tutor_utils()->checking_nonce();
2140
2141 $attempt_id = Input::post( 'id', 0, Input::TYPE_INT );
2142 $attempt = tutor_utils()->get_attempt( $attempt_id );
2143 if ( ! $attempt ) {
2144 wp_send_json_error( __( 'Invalid attempt ID', 'tutor' ) );
2145 }
2146
2147 $user_id = get_current_user_id();
2148 $course_id = $attempt->course_id;
2149
2150 if ( tutor_utils()->can_user_edit_course( $user_id, $course_id ) ) {
2151 QuizModel::delete_quiz_attempt( $attempt_id );
2152 wp_send_json_success( __( 'Attempt deleted successfully!', 'tutor' ) );
2153 } else {
2154 wp_send_json_error( tutor_utils()->error_message() );
2155 }
2156 }
2157
2158 }
2159