PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.3.0
Tutor LMS – eLearning and online course solution v3.3.0
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 1 year 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 1 year 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 1 year ago Template.php 1 year ago Theme_Compatibility.php 3 years ago Tools.php 1 year 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 1 year ago WooCommerce.php 1 year ago
Quiz.php
2135 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_question' => 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', 'tutor' ) );
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 $data = apply_filters( 'tutor_quiz_details_response', $data, $quiz_id );
1137
1138 $this->json_response(
1139 __( 'Quiz data fetched successfully', 'tutor' ),
1140 $data
1141 );
1142 }
1143
1144 /**
1145 * Delete quiz by id
1146 *
1147 * @since 1.0.0
1148 * @since 3.0.0 refactor and response change.
1149 *
1150 * @return void
1151 */
1152 public function ajax_quiz_delete() {
1153 if ( ! tutor_utils()->is_nonce_verified() ) {
1154 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1155 }
1156
1157 global $wpdb;
1158
1159 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1160 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1161 $this->json_response(
1162 tutor_utils()->error_message(),
1163 null,
1164 HttpHelper::STATUS_FORBIDDEN
1165 );
1166 }
1167
1168 $post = get_post( $quiz_id );
1169 if ( 'tutor_quiz' !== $post->post_type ) {
1170 $this->json_response(
1171 __( 'Invalid quiz', 'tutor' ),
1172 null,
1173 HttpHelper::STATUS_BAD_REQUEST
1174 );
1175 }
1176
1177 do_action( 'tutor_delete_quiz_before', $quiz_id );
1178
1179 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $quiz_id ) );
1180 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $quiz_id ) );
1181
1182 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $quiz_id ) );
1183
1184 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
1185 $in_question_ids = QueryHelper::prepare_in_clause( $questions_ids );
1186 //phpcs:disable
1187 $wpdb->query(
1188 "DELETE
1189 FROM {$wpdb->prefix}tutor_quiz_question_answers
1190 WHERE belongs_question_id IN({$in_question_ids})
1191 "
1192 );
1193 //phpcs:enable
1194 }
1195
1196 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ) );
1197
1198 wp_delete_post( $quiz_id, true );
1199
1200 do_action( 'tutor_delete_quiz_after', $quiz_id );
1201
1202 $this->json_response(
1203 __( 'Quiz deleted successfully', 'tutor' ),
1204 $quiz_id
1205 );
1206 }
1207
1208 /**
1209 * Load quiz Modal on add/edit click
1210 *
1211 * @since 1.0.0
1212 *
1213 * @param array $params params.
1214 * @param boolean $return should return or not.
1215 *
1216 * @return mixed
1217 */
1218 public function tutor_load_quiz_builder_modal( $params = array(), $return = false ) {
1219 tutor_utils()->checking_nonce();
1220
1221 //phpcs:ignore WordPress.Security.NonceVerification.Missing
1222 $data = array_merge( $_POST, $params );
1223 $quiz_id = isset( $data['quiz_id'] ) ? sanitize_text_field( $data['quiz_id'] ) : 0;
1224 $topic_id = isset( $data['topic_id'] ) ? sanitize_text_field( $data['topic_id'] ) : 0;
1225 $quiz = $quiz_id ? get_post( $quiz_id ) : null;
1226 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1227
1228 if ( $quiz_id && ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1229 wp_send_json_error( array( 'message' => __( 'Quiz Permission Denied', 'tutor' ) ) );
1230 }
1231
1232 ob_start();
1233 include tutor()->path . 'views/modal/edit_quiz.php';
1234 $output = ob_get_clean();
1235
1236 if ( $return ) {
1237 return $output;
1238 }
1239
1240 wp_send_json_success( array( 'output' => $output ) );
1241 }
1242
1243 /**
1244 * Delete quiz question
1245 *
1246 * @since 1.0.0
1247 * @since 3.0.0 refactor and response updated.
1248 *
1249 * @return void
1250 */
1251 public function ajax_quiz_question_delete() {
1252 if ( ! tutor_utils()->is_nonce_verified() ) {
1253 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1254 }
1255
1256 global $wpdb;
1257
1258 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1259
1260 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1261 $this->json_response(
1262 tutor_utils()->error_message(),
1263 null,
1264 HttpHelper::STATUS_FORBIDDEN
1265 );
1266 }
1267
1268 if ( $question_id ) {
1269 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_id' => $question_id ) );
1270 }
1271
1272 $this->json_response(
1273 __( 'Question successfully deleted', 'tutor' ),
1274 $question_id
1275 );
1276
1277 }
1278
1279 /**
1280 * Get answers options form for quiz question
1281 *
1282 * @since 1.0.0
1283 *
1284 * @return void send wp_json response
1285 */
1286 public function tutor_quiz_question_answer_editor() {
1287 tutor_utils()->checking_nonce();
1288
1289 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1290 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1291 $quiz_option = isset( $_POST['tutor_quiz_question'] ) ? tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ) : array(); //phpcs:ignore
1292 $question = tutor_utils()->avalue_dot( $question_id, $quiz_option );
1293 $question_type = $question['question_type'];
1294
1295 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1296 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1297 }
1298
1299 if ( $answer_id ) {
1300 $old_answer = tutor_utils()->get_answer_by_id( $answer_id );
1301 //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedForeach
1302 foreach ( $old_answer as $old_answer ) {
1303 }
1304 }
1305
1306 ob_start();
1307 include tutor()->path . 'views/modal/question_answer_form.php';
1308 $output = ob_get_clean();
1309
1310 wp_send_json_success( array( 'output' => $output ) );
1311 }
1312
1313 /**
1314 * Undocumented function
1315 *
1316 * @since 1.0.0
1317 *
1318 * @param mixed $questions questions.
1319 * @param mixed $answers answers.
1320 * @param boolean $response should send json response.
1321 *
1322 * @return void
1323 */
1324 public function tutor_save_quiz_answer_options( $questions = null, $answers = null, $response = true ) {
1325 tutor_utils()->checking_nonce();
1326
1327 global $wpdb;
1328 $questions = $questions ? $questions : tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1329 $answers = $answers ? $answers : tutor_utils()->sanitize_array( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1330
1331 foreach ( $answers as $question_id => $answer ) {
1332 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1333 continue;
1334 }
1335
1336 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1337 $question_type = $question['question_type'];
1338
1339 // Getting next sorting order.
1340 $next_order_id = (int) $wpdb->get_var(
1341 $wpdb->prepare(
1342 "SELECT MAX(answer_order)
1343 FROM {$wpdb->prefix}tutor_quiz_question_answers
1344 WHERE belongs_question_id = %d
1345 AND belongs_question_type = %s
1346 ",
1347 $question_id,
1348 esc_sql( $question_type )
1349 )
1350 );
1351
1352 //phpcs:ignore Squiz.Operators.IncrementDecrementUsage.Found
1353 $next_order_id = $next_order_id + 1;
1354
1355 if ( $question ) {
1356 if ( 'true_false' === $question_type ) {
1357 $wpdb->delete(
1358 $wpdb->prefix . 'tutor_quiz_question_answers',
1359 array(
1360 'belongs_question_id' => $question_id,
1361 'belongs_question_type' => $question_type,
1362 )
1363 );
1364 $data_true_false = array(
1365 array(
1366 'belongs_question_id' => esc_sql( $question_id ),
1367 'belongs_question_type' => $question_type,
1368 'answer_title' => __( 'True', 'tutor' ),
1369 'is_correct' => 'true' == $answer['true_false'] ? 1 : 0,
1370 'answer_two_gap_match' => 'true',
1371 ),
1372 array(
1373 'belongs_question_id' => esc_sql( $question_id ),
1374 'belongs_question_type' => $question_type,
1375 'answer_title' => __( 'False', 'tutor' ),
1376 'is_correct' => 'false' === $answer['true_false'] ? 1 : 0,
1377 'answer_two_gap_match' => 'false',
1378 ),
1379 );
1380
1381 foreach ( $data_true_false as $true_false_data ) {
1382 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $true_false_data );
1383 }
1384 } elseif ( 'multiple_choice' === $question_type ||
1385 'single_choice' === $question_type ||
1386 'ordering' === $question_type ||
1387 'matching' === $question_type ||
1388 'image_matching' === $question_type ||
1389 'image_answering' === $question_type ) {
1390
1391 $answer_data = array(
1392 'belongs_question_id' => sanitize_text_field( $question_id ),
1393 'belongs_question_type' => $question_type,
1394 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1395 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1396 'answer_view_format' => isset( $answer['answer_view_format'] ) ? $answer['answer_view_format'] : 0,
1397 'answer_order' => $next_order_id,
1398 );
1399 if ( isset( $answer['matched_answer_title'] ) ) {
1400 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1401 }
1402
1403 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1404
1405 } elseif ( 'fill_in_the_blank' === $question_type ) {
1406 $wpdb->delete(
1407 $wpdb->prefix . 'tutor_quiz_question_answers',
1408 array(
1409 'belongs_question_id' => $question_id,
1410 'belongs_question_type' => $question_type,
1411 )
1412 );
1413 $answer_data = array(
1414 'belongs_question_id' => sanitize_text_field( $question_id ),
1415 'belongs_question_type' => $question_type,
1416 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1417 'answer_two_gap_match' => isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null,
1418 );
1419 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1420 }
1421 }
1422 }
1423
1424 // Send response to browser if not internal call.
1425 if ( $response ) {
1426 wp_send_json_success();
1427 exit;
1428 }
1429 }
1430
1431 /**
1432 * Tutor Update Answer
1433 *
1434 * @since 1.0.0
1435 *
1436 * @return void send wp_json response
1437 */
1438 public function tutor_update_quiz_answer_options() {
1439 tutor_utils()->checking_nonce();
1440
1441 global $wpdb;
1442
1443 $answer_id = Input::post( 'tutor_quiz_answer_id', 0, Input::TYPE_INT );
1444
1445 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1446 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1447 }
1448
1449 // Data sanitizing by helper method.
1450 $questions = tutor_sanitize_data( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1451 $answers = tutor_sanitize_data( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1452
1453 foreach ( $answers as $question_id => $answer ) {
1454 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1455 $question_type = $question['question_type'];
1456
1457 if ( $question ) {
1458 if ( 'multiple_choice' === $question_type ||
1459 'single_choice' === $question_type ||
1460 'ordering' === $question_type ||
1461 'matching' === $question_type ||
1462 'image_matching' === $question_type ||
1463 'fill_in_the_blank' === $question_type ||
1464 'image_answering' === $question_type ) {
1465
1466 $answer_data = array(
1467 'belongs_question_id' => $question_id,
1468 'belongs_question_type' => $question_type,
1469 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1470 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1471 'answer_view_format' => isset( $answer['answer_view_format'] ) ? sanitize_text_field( $answer['answer_view_format'] ) : '',
1472 );
1473 if ( isset( $answer['matched_answer_title'] ) ) {
1474 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1475 }
1476
1477 if ( 'fill_in_the_blank' === $question_type ) {
1478 $answer_data['answer_two_gap_match'] = isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null;
1479 }
1480
1481 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data, array( 'answer_id' => $answer_id ) );
1482 }
1483 }
1484 }
1485 wp_send_json_success();
1486 }
1487
1488 /**
1489 * Get answers by quiz id
1490 *
1491 * @since 1.0.0
1492 *
1493 * @param int $question_id question id.
1494 * @param mixed $question_type type of question.
1495 * @param boolean $is_correct only correct answers or not.
1496 *
1497 * @return wpdb:get_results
1498 */
1499 private function get_answers_by_q_id( $question_id, $question_type, $is_correct = false ) {
1500 global $wpdb;
1501
1502 $correct_clause = $is_correct ? ' AND is_correct=1 ' : '';
1503 //phpcs:disable
1504 return $wpdb->get_results(
1505 $wpdb->prepare(
1506 "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers
1507 WHERE belongs_question_id = %d
1508 AND belongs_question_type = %s
1509 {$correct_clause}
1510 ORDER BY answer_order ASC;
1511 ",
1512 $question_id,
1513 esc_sql( $question_type )
1514 )
1515 );
1516 //phpcs:enable
1517 }
1518
1519 /**
1520 * Quiz builder changed type
1521 *
1522 * @since 1.0.0
1523 *
1524 * @return void send wp_json response
1525 */
1526 public function tutor_quiz_builder_change_type() {
1527 tutor_utils()->checking_nonce();
1528
1529 global $wpdb;
1530 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1531 $question_type = Input::post( 'question_type' );
1532
1533 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1534 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1535 }
1536
1537 // Get question data by question ID.
1538 $question = $wpdb->get_row(
1539 $wpdb->prepare(
1540 "SELECT *
1541 FROM {$wpdb->prefix}tutor_quiz_questions
1542 WHERE question_id = %d
1543 ",
1544 $question_id
1545 )
1546 );
1547
1548 // Get answers by question ID.
1549 $answers = $this->get_answers_by_q_id( $question_id, $question_type );
1550
1551 ob_start();
1552 require tutor()->path . '/views/modal/question_answer_list.php';
1553 $output = ob_get_clean();
1554
1555 wp_send_json_success( array( 'output' => $output ) );
1556 }
1557
1558
1559 /**
1560 * Create quiz question
1561 *
1562 * @since 3.0.0
1563 *
1564 * @return void
1565 */
1566 public function ajax_quiz_question_create() {
1567 if ( ! tutor_utils()->is_nonce_verified() ) {
1568 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1569 }
1570
1571 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1572
1573 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1574 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1575 }
1576
1577 global $wpdb;
1578 $next_question_sl = QueryHelper::get_count( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ), array(), '*' ) + 1;
1579 $next_question_order = QuizModel::quiz_next_question_order_id( $quiz_id );
1580 $question_title = __( 'Question', 'tutor' ) . ' ' . $next_question_sl;
1581
1582 $new_question_data = array(
1583 'quiz_id' => $quiz_id,
1584 'question_title' => $question_title,
1585 'question_description' => '',
1586 'question_type' => 'true_false',
1587 'question_mark' => 1,
1588 'question_settings' => maybe_serialize( array() ),
1589 'question_order' => esc_sql( $next_question_order ),
1590 );
1591
1592 $new_question_data = apply_filters( 'tutor_quiz_question_data', $new_question_data );
1593
1594 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_questions', $new_question_data );
1595 $question_id = $wpdb->insert_id;
1596
1597 // Add question with default true_false type and options.
1598 $this->add_true_false_options( $question_id );
1599
1600 // Add created question object to response.
1601 $question = QuizModel::get_question( $question_id );
1602 $question->question_answers = QuizModel::get_question_answers( $question->question_id );
1603 if ( isset( $question->question_settings ) ) {
1604 $question->question_settings = maybe_unserialize( $question->question_settings );
1605 }
1606
1607 $this->json_response(
1608 __( 'Question created successfully', 'tutor' ),
1609 $question,
1610 HttpHelper::STATUS_CREATED
1611 );
1612 }
1613
1614 /**
1615 * Update question
1616 *
1617 * @since 3.0.0
1618 *
1619 * @return void
1620 */
1621 public function ajax_quiz_question_update() {
1622 if ( ! tutor_utils()->is_nonce_verified() ) {
1623 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1624 }
1625
1626 global $wpdb;
1627
1628 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1629 if ( ! $question_id ) {
1630 $this->json_response( __( 'Invalid quiz question ID', 'tutor' ), null, HttpHelper::STATUS_BAD_REQUEST );
1631 }
1632
1633 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1634 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1635 }
1636
1637 $requires_answers = array(
1638 'multiple_choice',
1639 'single_choice',
1640 'true_false',
1641 'fill_in_the_blank',
1642 'matching',
1643 'image_matching',
1644 'image_answering',
1645 'ordering',
1646 );
1647
1648 $need_correct = array(
1649 'multiple_choice',
1650 'single_choice',
1651 'true_false',
1652 );
1653
1654 $question_title = Input::post( 'question_title', '' );
1655 $question_type = Input::post( 'question_type', 'true_false' );
1656 $question_mark = Input::post( 'question_mark', 1, Input::TYPE_INT );
1657 $question_settings = Input::sanitize_array( $_POST['question_settings'] ?? array() ); //phpcs:ignore
1658
1659 add_filter( 'wp_kses_allowed_html', Input::class . '::allow_iframe', 10, 2 );
1660 $question_description = Input::post( 'question_description', '', Input::TYPE_KSES_POST );
1661 remove_filter( 'wp_kses_allowed_html', Input::class . '::allow_iframe', 10, 2 );
1662
1663 if ( in_array( $question_type, $requires_answers, true ) ) {
1664 $require_correct = in_array( $question_type, $need_correct, true );
1665 $all_answers = $this->get_answers_by_q_id( $question_id, $question_type );
1666 $correct_answers = $this->get_answers_by_q_id( $question_id, $question_type, $require_correct );
1667
1668 if ( ! empty( $all_answers ) && empty( $correct_answers ) ) {
1669 $this->json_response(
1670 __( 'Please make sure the question has answer', 'tutor' ),
1671 null,
1672 HttpHelper::STATUS_BAD_REQUEST
1673 );
1674 }
1675 }
1676
1677 if ( isset( $question_settings['question_title'] ) ) {
1678 unset( $question_settings['question_title'] );
1679 }
1680
1681 if ( isset( $question_settings['question_description'] ) ) {
1682 unset( $question_settings['question_description'] );
1683 }
1684
1685 $data = array(
1686 'question_title' => $question_title,
1687 'question_description' => $question_description,
1688 'question_type' => $question_type,
1689 'question_mark' => $question_mark,
1690 'question_settings' => maybe_serialize( $question_settings ),
1691 );
1692
1693 $data = apply_filters( 'tutor_quiz_question_data', $data );
1694
1695 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', $data, array( 'question_id' => $question_id ) );
1696
1697 $this->json_response(
1698 __( 'Question updated successfully', 'tutor' ),
1699 $question_id
1700 );
1701 }
1702
1703 /**
1704 * Save quiz questions sorting
1705 *
1706 * @since 1.0.0
1707 * @since 3.0.0 refactor and update response.
1708 *
1709 * @return void
1710 */
1711 public function ajax_quiz_question_sorting() {
1712 if ( ! tutor_utils()->is_nonce_verified() ) {
1713 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1714 }
1715
1716 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1717 $question_ids = Input::post( 'sorted_question_ids', array(), Input::TYPE_ARRAY );
1718
1719 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1720 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1721 }
1722
1723 global $wpdb;
1724
1725 $i = 0;
1726 foreach ( $question_ids as $question_id ) {
1727 $i++;
1728 $wpdb->update(
1729 $wpdb->prefix . 'tutor_quiz_questions',
1730 array( 'question_order' => $i ),
1731 array(
1732 'quiz_id' => $quiz_id,
1733 'question_id' => $question_id,
1734 )
1735 );
1736 }
1737
1738 $this->json_response( __( 'Question order successfully updated', 'tutor' ) );
1739 }
1740
1741 /**
1742 * Add true false type question answer options.
1743 *
1744 * @param int $question_id question id.
1745 *
1746 * @return void
1747 */
1748 private function add_true_false_options( $question_id ) {
1749 global $wpdb;
1750 $question_type = 'true_false';
1751
1752 $wpdb->delete(
1753 $wpdb->prefix . 'tutor_quiz_question_answers',
1754 array(
1755 'belongs_question_id' => $question_id,
1756 'belongs_question_type' => $question_type,
1757 )
1758 );
1759
1760 $data = array(
1761 array(
1762 'belongs_question_id' => $question_id,
1763 'belongs_question_type' => $question_type,
1764 'answer_title' => __( 'True', 'tutor' ),
1765 'is_correct' => 1,
1766 'answer_two_gap_match' => 'true',
1767 ),
1768 array(
1769 'belongs_question_id' => $question_id,
1770 'belongs_question_type' => $question_type,
1771 'answer_title' => __( 'False', 'tutor' ),
1772 'is_correct' => 0,
1773 'answer_two_gap_match' => 'false',
1774 ),
1775 );
1776
1777 foreach ( $data as $row ) {
1778 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $row );
1779 }
1780 }
1781
1782 /**
1783 * Save question answer
1784 *
1785 * @since 3.0.0
1786 *
1787 * @return void
1788 */
1789 public function ajax_quiz_question_answer_save() {
1790 if ( ! tutor_utils()->is_nonce_verified() ) {
1791 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1792 }
1793
1794 $is_update = false;
1795 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1796 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1797
1798 if ( $answer_id ) {
1799 $is_update = true;
1800 }
1801
1802 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1803 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1804 }
1805
1806 global $wpdb;
1807
1808 $table_question = "{$wpdb->prefix}tutor_quiz_questions";
1809 $table_answer = "{$wpdb->prefix}tutor_quiz_question_answers";
1810
1811 $question = QueryHelper::get_row( $table_question, array( 'question_id' => $question_id ), 'question_id' );
1812
1813 if ( ! $question ) {
1814 $this->json_response(
1815 __( 'Invalid question', 'tutor' ),
1816 null,
1817 HttpHelper::STATUS_BAD_REQUEST
1818 );
1819 }
1820
1821 $question_type = Input::post( 'question_type' );
1822 $answer_title = Input::post( 'answer_title', '' );
1823 $image_id = Input::post( 'image_id', 0, Input::TYPE_INT );
1824 $answer_view_format = Input::post( 'answer_view_format', '' );
1825
1826 $answer_data = array(
1827 'belongs_question_id' => $question_id,
1828 'belongs_question_type' => $question_type,
1829 'answer_title' => $answer_title,
1830 );
1831
1832 if ( ! $is_update ) {
1833 $answer_data['answer_order'] = QuizModel::get_next_answer_order( $question_id, $question_type );
1834 }
1835
1836 $question_types = array(
1837 'single_choice',
1838 'multiple_choice',
1839 'ordering',
1840 'matching',
1841 'image_matching',
1842 'image_answering',
1843 );
1844
1845 if ( in_array( $question_type, $question_types, true ) ) {
1846 $answer_data['image_id'] = $image_id;
1847 $answer_data['answer_view_format'] = $answer_view_format;
1848
1849 if ( Input::has( 'matched_answer_title' ) ) {
1850 $answer_data['answer_two_gap_match'] = Input::post( 'matched_answer_title' );
1851 }
1852 } elseif ( 'fill_in_the_blank' === $question_type ) {
1853 $answer_data['answer_two_gap_match'] = Input::post( 'answer_two_gap_match' );
1854 }
1855
1856 if ( $is_update ) {
1857 $wpdb->update( $table_answer, $answer_data, array( 'answer_id' => $answer_id ) );
1858 } else {
1859 $question_types[] = 'fill_in_the_blank';
1860 if ( ! in_array( $question_type, $question_types, true ) ) {
1861 $this->json_response( __( 'Invalid question type', 'tutor' ), null, HttpHelper::STATUS_BAD_REQUEST );
1862 }
1863
1864 $answer_data['belongs_question_type'] = Input::post( 'question_type' );
1865 $wpdb->insert( $table_answer, $answer_data );
1866 $answer_id = $wpdb->insert_id;
1867 }
1868
1869 if ( $is_update ) {
1870 $this->json_response(
1871 __( 'Question answer updated successfully', 'tutor' ),
1872 $answer_id
1873 );
1874 } else {
1875 $this->json_response(
1876 __( 'Question answer saved successfully', 'tutor' ),
1877 $answer_id,
1878 HttpHelper::STATUS_CREATED
1879 );
1880 }
1881 }
1882
1883 /**
1884 * Delete quiz question's answer
1885 *
1886 * @since 3.0.0
1887 *
1888 * @return void
1889 */
1890 public function ajax_quiz_question_answer_delete() {
1891 if ( ! tutor_utils()->is_nonce_verified() ) {
1892 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1893 }
1894
1895 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1896
1897 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1898 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1899 }
1900
1901 global $wpdb;
1902 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_id' => $answer_id ) );
1903
1904 $this->json_response( __( 'Answer deleted successfully', 'tutor' ) );
1905 }
1906
1907 /**
1908 * Quiz question's answer shorting
1909 *
1910 * @since 1.0.0
1911 * @since 3.0.0 refactor and response update.
1912 *
1913 * @return void
1914 */
1915 public function ajax_quiz_question_answer_sorting() {
1916 if ( ! tutor_utils()->is_nonce_verified() ) {
1917 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1918 }
1919
1920 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1921 $answer_ids = Input::post( 'sorted_answer_ids', array(), Input::TYPE_ARRAY );
1922
1923 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1924 $this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
1925 }
1926
1927 global $wpdb;
1928 $i = 0;
1929 foreach ( $answer_ids as $answer_id ) {
1930 $i++;
1931 $wpdb->update(
1932 $wpdb->prefix . 'tutor_quiz_question_answers',
1933 array( 'answer_order' => $i ),
1934 array(
1935 'belongs_question_id' => $question_id,
1936 'answer_id' => $answer_id,
1937 )
1938 );
1939 }
1940
1941 $this->json_response( __( 'Question answer order successfully updated', 'tutor' ) );
1942 }
1943
1944 /**
1945 * Mark answer as correct
1946 *
1947 * @since 1.0.0
1948 * @since 3.0.0 refactor and response updated.
1949 *
1950 * @return void
1951 */
1952 public function ajax_mark_answer_as_correct() {
1953 if ( ! tutor_utils()->is_nonce_verified() ) {
1954 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1955 }
1956
1957 global $wpdb;
1958
1959 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1960
1961 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1962 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1963 }
1964
1965 // get question info.
1966 $belong_question = $wpdb->get_row(
1967 $wpdb->prepare(
1968 " SELECT belongs_question_id, belongs_question_type
1969 FROM {$wpdb->tutor_quiz_question_answers}
1970 WHERE answer_id = %d
1971 LIMIT 1
1972 ",
1973 $answer_id
1974 )
1975 );
1976
1977 if ( $belong_question ) {
1978 // if question found update all answer is_correct to 0 except post answer.
1979 $question_type = $belong_question->belongs_question_type;
1980 $question_id = $belong_question->belongs_question_id;
1981 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
1982 $update = $wpdb->query(
1983 $wpdb->prepare(
1984 "UPDATE {$wpdb->tutor_quiz_question_answers}
1985 SET is_correct = 0
1986 WHERE belongs_question_id = %d
1987 AND answer_id != %d
1988 ",
1989 $question_id,
1990 $answer_id
1991 )
1992 );
1993 }
1994 }
1995
1996 $is_correct = Input::post( 'is_correct', 0, Input::TYPE_INT );
1997
1998 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1999 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
2000 }
2001
2002 $answer = $wpdb->get_row(
2003 $wpdb->prepare(
2004 "SELECT *
2005 FROM {$wpdb->prefix}tutor_quiz_question_answers
2006 WHERE answer_id = %d
2007 LIMIT 0,1 ;
2008 ",
2009 $answer_id
2010 )
2011 );
2012
2013 if ( 'single_choice' === $answer->belongs_question_type ) {
2014 $wpdb->update(
2015 $wpdb->prefix . 'tutor_quiz_question_answers',
2016 array( 'is_correct' => 0 ),
2017 array( 'belongs_question_id' => esc_sql( $answer->belongs_question_id ) )
2018 );
2019 }
2020
2021 $wpdb->update(
2022 $wpdb->prefix . 'tutor_quiz_question_answers',
2023 array( 'is_correct' => $is_correct ),
2024 array( 'answer_id' => $answer_id )
2025 );
2026
2027 $this->json_response(
2028 __( 'Answer mark as correct updated', 'tutor' ),
2029 $answer_id
2030 );
2031 }
2032
2033 /**
2034 * Rendering quiz for frontend
2035 *
2036 * @since 1.0.0
2037 *
2038 * @return void send wp_json response
2039 */
2040 public function tutor_render_quiz_content() {
2041
2042 tutor_utils()->checking_nonce();
2043
2044 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
2045
2046 if ( ! tutor_utils()->has_enrolled_content_access( 'quiz', $quiz_id ) ) {
2047 wp_send_json_error( array( 'message' => __( 'Access Denied.', 'tutor' ) ) );
2048 }
2049
2050 ob_start();
2051 global $post;
2052
2053 $post = get_post( $quiz_id ); //phpcs:ignore
2054 setup_postdata( $post );
2055
2056 single_quiz_contents();
2057 wp_reset_postdata();
2058
2059 $html = ob_get_clean();
2060 wp_send_json_success( array( 'html' => $html ) );
2061 }
2062
2063 /**
2064 * Get attempt details
2065 *
2066 * @since 1.0.0
2067 *
2068 * @param int $attempt_id required attempt id to get details.
2069 *
2070 * @return mixed object on success, null on failure
2071 */
2072 public static function attempt_details( int $attempt_id ) {
2073 global $wpdb;
2074 $attempt_details = $wpdb->get_row(
2075 $wpdb->prepare(
2076 "SELECT *
2077 FROM {$wpdb->prefix}tutor_quiz_attempts
2078 WHERE attempt_id = %d
2079 ",
2080 $attempt_id
2081 )
2082 );
2083 return $attempt_details;
2084 }
2085
2086 /**
2087 * Update quiz attempt info
2088 *
2089 * @since 1.0.0
2090 *
2091 * @param int $attempt_id attempt id.
2092 * @param mixed $attempt_info serialize data.
2093 *
2094 * @return bool, true on success, false on failure
2095 */
2096 public static function update_attempt_info( int $attempt_id, $attempt_info ) {
2097 global $wpdb;
2098 $table = $wpdb->prefix . 'tutor_quiz_attempts';
2099 $update_info = $wpdb->update(
2100 $table,
2101 array( 'attempt_info' => $attempt_info ),
2102 array( 'attempt_id' => $attempt_id )
2103 );
2104 return $update_info ? true : false;
2105 }
2106
2107 /**
2108 * Attempt delete ajax request handler
2109 *
2110 * @since 2.1.0
2111 *
2112 * @return void wp_json response
2113 */
2114 public function attempt_delete() {
2115 tutor_utils()->checking_nonce();
2116
2117 $attempt_id = Input::post( 'id', 0, Input::TYPE_INT );
2118 $attempt = tutor_utils()->get_attempt( $attempt_id );
2119 if ( ! $attempt ) {
2120 wp_send_json_error( __( 'Invalid attempt ID', 'tutor' ) );
2121 }
2122
2123 $user_id = get_current_user_id();
2124 $course_id = $attempt->course_id;
2125
2126 if ( tutor_utils()->can_user_edit_course( $user_id, $course_id ) ) {
2127 QuizModel::delete_quiz_attempt( $attempt_id );
2128 wp_send_json_success( __( 'Attempt deleted successfully!', 'tutor' ) );
2129 } else {
2130 wp_send_json_error( tutor_utils()->error_message() );
2131 }
2132 }
2133
2134 }
2135