Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/tutor/classes/Quiz.php
Keine Baseline-Datei – Diff nur gegen leer.
1
-
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( 'wp_ajax_tutor_quiz_timeout', array( $this, 'tutor_quiz_timeout' ) );
73
+
74
+
// User take the quiz.
75
+
add_action( 'template_redirect', array( $this, 'start_the_quiz' ) );
76
+
add_action( 'template_redirect', array( $this, 'answering_quiz' ) );
77
+
add_action( 'template_redirect', array( $this, 'finishing_quiz_attempt' ) );
78
+
79
+
/**
80
+
* Instructor quiz review and feedback Ajax API.
81
+
*/
82
+
add_action( 'wp_ajax_review_quiz_answer', array( $this, 'review_quiz_answer' ) );
83
+
add_action( 'wp_ajax_tutor_instructor_feedback', array( $this, 'tutor_instructor_feedback' ) );
84
+
85
+
/**
86
+
* New quiz builder Ajax API.
87
+
*/
88
+
add_action( 'wp_ajax_tutor_quiz_details', array( $this, 'ajax_quiz_details' ) );
89
+
add_action( 'wp_ajax_tutor_quiz_delete', array( $this, 'ajax_quiz_delete' ) );
90
+
91
+
/**
92
+
* Frontend Stuff
93
+
*/
94
+
add_action( 'wp_ajax_tutor_render_quiz_content', array( $this, 'tutor_render_quiz_content' ) );
95
+
96
+
/**
97
+
* Quiz abandon action
98
+
*
99
+
* @since 1.9.6
100
+
*/
101
+
add_action( 'wp_ajax_tutor_quiz_abandon', array( $this, 'tutor_quiz_abandon' ) );
102
+
103
+
$this->prepare_allowed_html();
104
+
105
+
/**
106
+
* Delete quiz attempt
107
+
*
108
+
* @since 2.1.0
109
+
*/
110
+
add_action( 'wp_ajax_tutor_attempt_delete', array( $this, 'attempt_delete' ) );
111
+
112
+
add_action( 'tutor_quiz/answer/review/after', array( $this, 'do_auto_course_complete' ), 10, 3 );
113
+
}
114
+
115
+
/**
116
+
* Get quiz time units options.
117
+
*
118
+
* @since 2.6.0
119
+
*
120
+
* @return array
121
+
*/
122
+
public static function quiz_time_units() {
123
+
$time_units = array(
124
+
'seconds' => __( 'Seconds', 'tutor' ),
125
+
'minutes' => __( 'Minutes', 'tutor' ),
126
+
'hours' => __( 'Hours', 'tutor' ),
127
+
'days' => __( 'Days', 'tutor' ),
128
+
'weeks' => __( 'Weeks', 'tutor' ),
129
+
);
130
+
131
+
return apply_filters( 'tutor_quiz_time_units', $time_units );
132
+
}
133
+
134
+
/**
135
+
* Get quiz default settings.
136
+
*
137
+
* @since 3.0.0
138
+
*
139
+
* @return array
140
+
*/
141
+
public static function get_default_quiz_settings() {
142
+
$settings = array(
143
+
'time_limit' => array(
144
+
'time_type' => 'minutes',
145
+
'time_value' => 0,
146
+
),
147
+
'attempts_allowed' => 10,
148
+
'feedback_mode' => 'retry',
149
+
'hide_question_number_overview' => 0,
150
+
'hide_quiz_time_display' => 0,
151
+
'max_questions_for_answer' => 10,
152
+
'open_ended_answer_characters_limit' => 500,
153
+
'pass_is_required' => 0,
154
+
'passing_grade' => 80,
155
+
'question_layout_view' => '',
156
+
'questions_order' => 'rand',
157
+
'quiz_auto_start' => 0,
158
+
'short_answer_characters_limit' => 200,
159
+
);
160
+
161
+
return apply_filters( 'tutor_quiz_default_settings', $settings );
162
+
}
163
+
164
+
/**
165
+
* Get question default settings.
166
+
*
167
+
* @since 3.0.0
168
+
*
169
+
* @param string $type type of question.
170
+
*
171
+
* @return array
172
+
*/
173
+
public static function get_default_question_settings( $type ) {
174
+
$settings = array(
175
+
'question_type' => $type,
176
+
'question_mark' => 1,
177
+
'answer_required' => 0,
178
+
'randomize_question' => 0,
179
+
'show_question_mark' => 0,
180
+
);
181
+
182
+
return apply_filters( 'tutor_question_default_settings', $settings );
183
+
}
184
+
185
+
/**
186
+
* Get quiz modes
187
+
*
188
+
* @since 2.6.0
189
+
*
190
+
* @return array
191
+
*/
192
+
public static function quiz_modes() {
193
+
$modes = array(
194
+
array(
195
+
'key' => 'default',
196
+
'value' => __( 'Default', 'tutor' ),
197
+
'description' => __( 'Answers shown after quiz is finished', 'tutor' ),
198
+
),
199
+
array(
200
+
'key' => 'reveal',
201
+
'value' => __( 'Reveal Mode', 'tutor' ),
202
+
'description' => __( 'Show result after the attempt.', 'tutor' ),
203
+
),
204
+
array(
205
+
'key' => 'retry',
206
+
'value' => __( 'Retry Mode', 'tutor' ),
207
+
'description' => __( 'Reattempt quiz any number of times. Define Attempts Allowed below.', 'tutor' ),
208
+
),
209
+
);
210
+
211
+
return apply_filters( 'tutor_quiz_modes', $modes );
212
+
}
213
+
214
+
/**
215
+
* Get quiz modes
216
+
*
217
+
* @since 2.6.0
218
+
*
219
+
* @return array
220
+
*/
221
+
public static function quiz_question_layouts() {
222
+
$layouts = array(
223
+
'' => __( 'Set question layout view', 'tutor' ),
224
+
'single_question' => __( 'Single Question', 'tutor' ),
225
+
'question_pagination' => __( 'Question Pagination', 'tutor' ),
226
+
'question_below_each_other' => __( 'Question below each other', 'tutor' ),
227
+
);
228
+
229
+
return apply_filters( 'tutor_quiz_layouts', $layouts );
230
+
}
231
+
232
+
/**
233
+
* Get quiz modes
234
+
*
235
+
* @since 2.6.0
236
+
*
237
+
* @return array
238
+
*/
239
+
public static function quiz_question_orders() {
240
+
$orders = array(
241
+
'rand' => __( 'Random', 'tutor' ),
242
+
'sorting' => __( 'Sorting', 'tutor' ),
243
+
'asc' => __( 'Ascending', 'tutor' ),
244
+
'desc' => __( 'Descending', 'tutor' ),
245
+
);
246
+
247
+
return apply_filters( 'tutor_quiz_layouts', $orders );
248
+
}
249
+
250
+
/**
251
+
* Prepare allowed HTML
252
+
*
253
+
* @since 1.0.0
254
+
*
255
+
* @return void
256
+
*/
257
+
private function prepare_allowed_html() {
258
+
259
+
$allowed = array();
260
+
261
+
foreach ( $this->allowed_html as $tag ) {
262
+
$allowed[ $tag ] = $this->allowed_attributes;
263
+
}
264
+
265
+
$this->allowed_html = $allowed;
266
+
}
267
+
268
+
/**
269
+
* Instructor feedback ajax request handler
270
+
*
271
+
* @since 1.0.0
272
+
*
273
+
* @return void | send json response
274
+
*/
275
+
public function tutor_instructor_feedback() {
276
+
tutor_utils()->checking_nonce();
277
+
278
+
// Check if user is privileged.
279
+
if ( ! User::has_any_role( array( User::ADMIN, User::INSTRUCTOR ) ) ) {
280
+
wp_send_json_error( tutor_utils()->error_message() );
281
+
}
282
+
283
+
$attempt_details = self::attempt_details( Input::post( 'attempt_id', 0, Input::TYPE_INT ) );
284
+
$feedback = Input::post( 'feedback', '', Input::TYPE_KSES_POST );
285
+
$attempt_info = isset( $attempt_details->attempt_info ) ? $attempt_details->attempt_info : false;
286
+
$course_id = tutor_utils()->avalue_dot( 'course_id', $attempt_details, 0 );
287
+
$is_instructor = tutor_utils()->is_instructor_of_this_course( get_current_user_id(), $course_id );
288
+
if ( ! current_user_can( 'manage_options' ) && ! $is_instructor ) {
289
+
wp_send_json_error( tutor_utils()->error_message() );
290
+
}
291
+
292
+
if ( $attempt_info ) {
293
+
//phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
294
+
$unserialized = unserialize( $attempt_details->attempt_info );
295
+
if ( is_array( $unserialized ) ) {
296
+
$unserialized['instructor_feedback'] = $feedback;
297
+
298
+
//phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
299
+
$update = self::update_attempt_info( $attempt_details->attempt_id, serialize( $unserialized ) );
300
+
if ( $update ) {
301
+
do_action( 'tutor_quiz/attempt/submitted/feedback', $attempt_details->attempt_id );
302
+
wp_send_json_success();
303
+
} else {
304
+
wp_send_json_error();
305
+
}
306
+
} else {
307
+
wp_send_json_error( __( 'Invalid quiz info', 'tutor' ) );
308
+
}
309
+
}
310
+
wp_send_json_error();
311
+
}
312
+
313
+
/**
314
+
* Start Quiz from here...
315
+
*
316
+
* @since 1.0.0
317
+
*
318
+
* @return void
319
+
*/
320
+
public function start_the_quiz() {
321
+
if ( 'tutor_start_quiz' !== Input::post( 'tutor_action' ) ) {
322
+
return;
323
+
}
324
+
325
+
tutor_utils()->checking_nonce();
326
+
327
+
if ( ! is_user_logged_in() ) {
328
+
die( esc_html__( 'Please sign in to do this operation', 'tutor' ) );
329
+
}
330
+
331
+
$user_id = get_current_user_id();
332
+
$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
333
+
$course = CourseModel::get_course_by_quiz( $quiz_id );
334
+
335
+
self::quiz_attempt( $course->ID, $quiz_id, $user_id );
336
+
wp_safe_redirect( get_permalink( $quiz_id ) );
337
+
die();
338
+
}
339
+
340
+
/**
341
+
* Manage quiz attempt
342
+
*
343
+
* @since 2.6.1
344
+
*
345
+
* @param integer $course_id course id.
346
+
* @param integer $quiz_id quiz id.
347
+
* @param integer $user_id user id.
348
+
* @param string $attempt_status attempt status.
349
+
*
350
+
* @return int inserted id|0
351
+
*/
352
+
public static function quiz_attempt( int $course_id, int $quiz_id, int $user_id, $attempt_status = 'attempt_started' ) {
353
+
global $wpdb;
354
+
355
+
if ( ! $course_id ) {
356
+
die( 'There is something went wrong with course, please check if quiz attached with a course' );
357
+
}
358
+
359
+
do_action( 'tutor_quiz/start/before', $quiz_id, $user_id );
360
+
361
+
$date = date( 'Y-m-d H:i:s', tutor_time() ); //phpcs:ignore
362
+
363
+
$tutor_quiz_option = (array) maybe_unserialize( get_post_meta( $quiz_id, 'tutor_quiz_option', true ) );
364
+
$attempts_allowed = tutor_utils()->get_quiz_option( $quiz_id, 'attempts_allowed', 0 );
365
+
366
+
$time_limit = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_value' );
367
+
$time_limit_seconds = 0;
368
+
$time_type = 'seconds';
369
+
if ( $time_limit ) {
370
+
$time_type = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_type' );
371
+
372
+
switch ( $time_type ) {
373
+
case 'seconds':
374
+
$time_limit_seconds = $time_limit;
375
+
break;
376
+
case 'minutes':
377
+
$time_limit_seconds = $time_limit * 60;
378
+
break;
379
+
case 'hours':
380
+
$time_limit_seconds = $time_limit * 60 * 60;
381
+
break;
382
+
case 'days':
383
+
$time_limit_seconds = $time_limit * 60 * 60 * 24;
384
+
break;
385
+
case 'weeks':
386
+
$time_limit_seconds = $time_limit * 60 * 60 * 24 * 7;
387
+
break;
388
+
}
389
+
}
390
+
391
+
$max_question_allowed = tutor_utils()->max_questions_for_take_quiz( $quiz_id );
392
+
$tutor_quiz_option['time_limit']['time_limit_seconds'] = $time_limit_seconds;
393
+
394
+
$attempt_data = array(
395
+
'course_id' => $course_id,
396
+
'quiz_id' => $quiz_id,
397
+
'user_id' => $user_id,
398
+
'total_questions' => $max_question_allowed,
399
+
'total_answered_questions' => 0,
400
+
'attempt_info' => maybe_serialize( $tutor_quiz_option ),
401
+
'attempt_status' => $attempt_status,
402
+
'attempt_ip' => tutor_utils()->get_ip(),
403
+
'attempt_started_at' => $date,
404
+
);
405
+
406
+
$wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_data );
407
+
$attempt_id = (int) $wpdb->insert_id;
408
+
409
+
if ( $attempt_id ) {
410
+
do_action( 'tutor_quiz/start/after', $quiz_id, $user_id, $attempt_id );
411
+
return $attempt_id;
412
+
} else {
413
+
return 0;
414
+
}
415
+
}
416
+
417
+
/**
418
+
* Answering quiz
419
+
*
420
+
* @since 1.0.0
421
+
*
422
+
* @return void
423
+
*/
424
+
public function answering_quiz() {
425
+
426
+
if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) {
427
+
return;
428
+
}
429
+
// submit quiz attempts.
430
+
self::tutor_quiz_attempt_submit();
431
+
432
+
wp_safe_redirect( get_the_permalink() );
433
+
die();
434
+
}
435
+
436
+
/**
437
+
* Quiz abandon submission handler
438
+
*
439
+
* @since 1.9.6
440
+
*
441
+
* @return JSON response
442
+
*/
443
+
public function tutor_quiz_abandon() {
444
+
if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) {
445
+
return;
446
+
}
447
+
tutor_utils()->checking_nonce();
448
+
// submit quiz attempts.
449
+
if ( self::tutor_quiz_attempt_submit() ) {
450
+
wp_send_json_success();
451
+
} else {
452
+
wp_send_json_error();
453
+
}
454
+
}
455
+
456
+
/**
457
+
* This is a unified method for handling normal quiz submit or abandon submit
458
+
* It will handle ajax or normal form submit and can be used with different hooks
459
+
*
460
+
* @since 1.9.6
461
+
*
462
+
* @return true | false
463
+
*/
464
+
public static function tutor_quiz_attempt_submit() {
465
+
// Check logged in.
466
+
if ( ! is_user_logged_in() ) {
467
+
die( 'Please sign in to do this operation' );
468
+
}
469
+
470
+
// Check nonce.
471
+
tutor_utils()->checking_nonce();
472
+
473
+
// Prepare attempt info.
474
+
$user_id = get_current_user_id();
475
+
$attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
476
+
$attempt = tutor_utils()->get_attempt( $attempt_id );
477
+
$course_id = CourseModel::get_course_by_quiz( $attempt->quiz_id )->ID;
478
+
479
+
if ( QuizModel::ATTEMPT_TIMEOUT === $attempt->attempt_status ) {
480
+
return false;
481
+
}
482
+
483
+
// Sanitize data by helper method.
484
+
$attempt_answers = isset( $_POST['attempt'] ) ? tutor_sanitize_data( $_POST['attempt'] ) : false; //phpcs:ignore
485
+
$attempt_answers = is_array( $attempt_answers ) ? $attempt_answers : array();
486
+
487
+
// Check if has access to the attempt.
488
+
if ( ! $attempt || $user_id != $attempt->user_id ) {
489
+
die( 'Operation not allowed, attempt not found or permission denied' );
490
+
}
491
+
self::manage_attempt_answers( $attempt_answers, $attempt, $attempt_id, $course_id, $user_id );
492
+
return true;
493
+
}
494
+
495
+
/**
496
+
* Manage attempt answers
497
+
*
498
+
* Evaluate each attempt answer and update the attempts table & insert in the attempt_answers table.
499
+
*
500
+
* @since 2.6.1
501
+
*
502
+
* @param array $attempt_answers attempt answers.
503
+
* @param object $attempt single attempt.
504
+
* @param int $attempt_id attempt id.
505
+
* @param int $course_id course id.
506
+
* @param int $user_id user id.
507
+
*
508
+
* @return void
509
+
*/
510
+
public static function manage_attempt_answers( $attempt_answers, $attempt, $attempt_id, $course_id, $user_id ) {
511
+
global $wpdb;
512
+
// Before hook.
513
+
do_action( 'tutor_quiz/attempt_analysing/before', $attempt_id );
514
+
515
+
// Single quiz can have multiple question. So multiple answer should be saved.
516
+
foreach ( $attempt_answers as $attempt_id => $attempt_answer ) {
517
+
// Get total marks of all question comes.
518
+
$question_ids = tutor_utils()->avalue_dot( 'quiz_question_ids', $attempt_answer );
519
+
$question_ids = array_filter(
520
+
$question_ids,
521
+
function ( $id ) {
522
+
return (int) $id;
523
+
}
524
+
);
525
+
526
+
// Calculate and set the total marks in attempt table for this question.
527
+
if ( is_array( $question_ids ) && count( $question_ids ) ) {
528
+
$question_ids_string = QueryHelper::prepare_in_clause( $question_ids );
529
+
530
+
// Get total marks of the questions from question table.
531
+
//phpcs:disable
532
+
$query = $wpdb->prepare(
533
+
"SELECT SUM(question_mark)
534
+
FROM {$wpdb->prefix}tutor_quiz_questions
535
+
WHERE 1 = %d
536
+
AND question_id IN({$question_ids_string});
537
+
",
538
+
1
539
+
);
540
+
$total_question_marks = $wpdb->get_var( $query );
541
+
//phpcs:enable
542
+
543
+
$total_question_marks = apply_filters( 'tutor_filter_update_before_question_mark', $total_question_marks, $question_ids, $user_id, $attempt_id );
544
+
545
+
// Set the the total mark in the attempt table for the question.
546
+
$wpdb->update(
547
+
$wpdb->prefix . 'tutor_quiz_attempts',
548
+
array( 'total_marks' => $total_question_marks ),
549
+
array( 'attempt_id' => $attempt_id )
550
+
);
551
+
}
552
+
553
+
$total_marks = 0;
554
+
$review_required = false;
555
+
$quiz_answers = tutor_utils()->avalue_dot( 'quiz_question', $attempt_answer );
556
+
557
+
if ( tutor_utils()->count( $quiz_answers ) ) {
558
+
559
+
foreach ( $quiz_answers as $question_id => $answers ) {
560
+
$question = QuizModel::get_quiz_question_by_id( $question_id );
561
+
$question_type = $question->question_type;
562
+
563
+
$is_answer_was_correct = false;
564
+
$given_answer = '';
565
+
566
+
if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
567
+
568
+
if ( ! is_numeric( $answers ) || ! $answers ) {
569
+
wp_send_json_error();
570
+
exit;
571
+
}
572
+
573
+
$given_answer = $answers;
574
+
$is_answer_was_correct = (bool) $wpdb->get_var(
575
+
$wpdb->prepare(
576
+
"SELECT is_correct
577
+
FROM {$wpdb->prefix}tutor_quiz_question_answers
578
+
WHERE answer_id = %d
579
+
",
580
+
$answers
581
+
)
582
+
);
583
+
584
+
} elseif ( 'multiple_choice' === $question_type ) {
585
+
586
+
$given_answer = (array) ( $answers );
587
+
588
+
$given_answer = array_filter(
589
+
$given_answer,
590
+
function ( $id ) {
591
+
return is_numeric( $id ) && $id > 0;
592
+
}
593
+
);
594
+
$get_original_answers = (array) $wpdb->get_col(
595
+
$wpdb->prepare(
596
+
"SELECT
597
+
answer_id
598
+
FROM
599
+
{$wpdb->prefix}tutor_quiz_question_answers
600
+
WHERE belongs_question_id = %d
601
+
AND belongs_question_type = %s
602
+
AND is_correct = 1 ;
603
+
",
604
+
$question->question_id,
605
+
$question_type
606
+
)
607
+
);
608
+
609
+
if ( count( array_diff( $get_original_answers, $given_answer ) ) === 0 && count( $get_original_answers ) === count( $given_answer ) ) {
610
+
$is_answer_was_correct = true;
611
+
}
612
+
$given_answer = maybe_serialize( $answers );
613
+
614
+
} elseif ( 'fill_in_the_blank' === $question_type ) {
615
+
616
+
$get_original_answer = $wpdb->get_row(
617
+
$wpdb->prepare(
618
+
"SELECT *
619
+
FROM {$wpdb->prefix}tutor_quiz_question_answers
620
+
WHERE belongs_question_id = %d
621
+
AND belongs_question_type = %s ;
622
+
",
623
+
$question->question_id,
624
+
$question_type
625
+
)
626
+
);
627
+
628
+
/**
629
+
* Answers stored in DB
630
+
*/
631
+
$gap_answer = (array) explode( '|', $get_original_answer->answer_two_gap_match );
632
+
$gap_answer = maybe_serialize(
633
+
array_map(
634
+
function ( $ans ) {
635
+
return wp_slash( trim( $ans ) );
636
+
},
637
+
$gap_answer
638
+
)
639
+
);
640
+
641
+
/**
642
+
* Answers from user input
643
+
*/
644
+
$given_answer = (array) array_map( 'sanitize_text_field', $answers );
645
+
$given_answer = maybe_serialize( $given_answer );
646
+
647
+
/**
648
+
* Compare answer's by making both case-insensitive.
649
+
*/
650
+
if ( strtolower( $given_answer ) == strtolower( $gap_answer ) ) {
651
+
$is_answer_was_correct = true;
652
+
}
653
+
} elseif ( 'open_ended' === $question_type || 'short_answer' === $question_type ) {
654
+
$review_required = true;
655
+
$given_answer = wp_kses_post( $answers );
656
+
657
+
} elseif ( 'ordering' === $question_type || 'matching' === $question_type || 'image_matching' === $question_type ) {
658
+
659
+
$given_answer = (array) array_map( 'sanitize_text_field', tutor_utils()->avalue_dot( 'answers', $answers ) );
660
+
$given_answer = maybe_serialize( $given_answer );
661
+
662
+
$get_original_answers = (array) $wpdb->get_col(
663
+
$wpdb->prepare(
664
+
"SELECT answer_id
665
+
FROM {$wpdb->prefix}tutor_quiz_question_answers
666
+
WHERE belongs_question_id = %d
667
+
AND belongs_question_type = %s
668
+
ORDER BY answer_order ASC ;
669
+
",
670
+
$question->question_id,
671
+
$question_type
672
+
)
673
+
);
674
+
675
+
$get_original_answers = array_map( 'sanitize_text_field', $get_original_answers );
676
+
677
+
if ( maybe_serialize( $get_original_answers ) == $given_answer ) {
678
+
$is_answer_was_correct = true;
679
+
}
680
+
} elseif ( 'image_answering' === $question_type ) {
681
+
$image_inputs = tutor_utils()->avalue_dot( 'answer_id', $answers );
682
+
$image_inputs = (array) array_map( 'sanitize_text_field', $image_inputs );
683
+
$given_answer = maybe_serialize( $image_inputs );
684
+
$is_answer_was_correct = false;
685
+
/**
686
+
* For the image_answering question type result
687
+
* remain pending in spite of correct answer & required
688
+
* review of admin/instructor. Since it's
689
+
* pending we need to mark it as incorrect. Otherwise if
690
+
* mark it correct then earned mark will be updated. then
691
+
* again when instructor/admin review & mark it as correct
692
+
* extra mark is adding. In this case, student
693
+
* getting double mark for the same question.
694
+
*
695
+
* For now code is commenting will be removed later on
696
+
*
697
+
* @since 2.1.5
698
+
*/
699
+
700
+
//phpcs:disable
701
+
702
+
// $db_answer = $wpdb->get_col(
703
+
// $wpdb->prepare(
704
+
// "SELECT answer_title
705
+
// FROM {$wpdb->prefix}tutor_quiz_question_answers
706
+
// WHERE belongs_question_id = %d
707
+
// AND belongs_question_type = 'image_answering'
708
+
// ORDER BY answer_order asc ;",
709
+
// $question_id
710
+
// )
711
+
// );
712
+
713
+
// if ( is_array( $db_answer ) && count( $db_answer ) ) {
714
+
// $is_answer_was_correct = ( strtolower( maybe_serialize( array_values( $image_inputs ) ) ) == strtolower( maybe_serialize( $db_answer ) ) );
715
+
// }
716
+
//phpcs:enable
717
+
}
718
+
719
+
$question_mark = $is_answer_was_correct ? $question->question_mark : 0;
720
+
$total_marks += $question_mark;
721
+
722
+
$total_marks = apply_filters( 'tutor_filter_quiz_total_marks', $total_marks, $question_id, $question_type, $user_id, $attempt_id );
723
+
724
+
$answers_data = array(
725
+
'user_id' => $user_id,
726
+
'quiz_id' => $attempt->quiz_id,
727
+
'question_id' => $question_id,
728
+
'quiz_attempt_id' => $attempt_id,
729
+
'given_answer' => $given_answer,
730
+
'question_mark' => $question->question_mark,
731
+
'achieved_mark' => $question_mark,
732
+
'minus_mark' => 0,
733
+
'is_correct' => $is_answer_was_correct ? 1 : 0,
734
+
);
735
+
736
+
/**
737
+
* Check if question_type open ended or short ans the set
738
+
* is_correct default value null before saving
739
+
*/
740
+
if ( in_array( $question_type, array( 'open_ended', 'short_answer', 'image_answering' ) ) ) {
741
+
$answers_data['is_correct'] = null;
742
+
$review_required = true;
743
+
}
744
+
745
+
$answers_data = apply_filters( 'tutor_filter_quiz_answer_data', $answers_data, $question_id, $question_type, $user_id, $attempt_id );
746
+
747
+
$wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answers_data );
748
+
}
749
+
}
750
+
751
+
$attempt_info = array(
752
+
'total_answered_questions' => tutor_utils()->count( $quiz_answers ),
753
+
'earned_marks' => $total_marks,
754
+
'attempt_status' => 'attempt_ended',
755
+
'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
756
+
);
757
+
758
+
if ( $review_required ) {
759
+
$attempt_info['attempt_status'] = 'review_required';
760
+
}
761
+
762
+
$wpdb->update( $wpdb->tutor_quiz_attempts, $attempt_info, array( 'attempt_id' => $attempt_id ) );
763
+
764
+
QuizModel::update_attempt_result( $attempt_id );
765
+
}
766
+
767
+
// After hook.
768
+
do_action( 'tutor_quiz/attempt_ended', $attempt_id, $course_id, $user_id );
769
+
}
770
+
771
+
772
+
/**
773
+
* Quiz attempt will be finish here
774
+
*
775
+
* @since 1.0.0
776
+
*
777
+
* @return void
778
+
*/
779
+
public function finishing_quiz_attempt() {
780
+
781
+
if ( Input::post( 'tutor_action' ) !== 'tutor_finish_quiz_attempt' ) {
782
+
return;
783
+
}
784
+
// Checking nonce.
785
+
tutor_utils()->checking_nonce();
786
+
787
+
if ( ! is_user_logged_in() ) {
788
+
die( 'Please sign in to do this operation' );
789
+
}
790
+
791
+
global $wpdb;
792
+
793
+
$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
794
+
$attempt = tutor_utils()->is_started_quiz( $quiz_id );
795
+
$attempt_id = $attempt->attempt_id;
796
+
797
+
$attempt_info = array(
798
+
'total_answered_questions' => 0,
799
+
'earned_marks' => 0,
800
+
'attempt_status' => 'attempt_ended',
801
+
'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
802
+
);
803
+
804
+
do_action( 'tutor_quiz_before_finish', $attempt_id, $quiz_id, $attempt->user_id );
805
+
$wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
806
+
do_action( 'tutor_quiz_finished', $attempt_id, $quiz_id, $attempt->user_id );
807
+
808
+
wp_redirect( tutor_utils()->input_old( '_wp_http_referer' ) );
809
+
}
810
+
811
+
/**
812
+
* Get quiz total marks.
813
+
*
814
+
* @since 3.0.0
815
+
*
816
+
* @param int $quiz_id quiz id.
817
+
*
818
+
* @return int|float
819
+
*/
820
+
public static function get_quiz_total_marks( $quiz_id ) {
821
+
global $wpdb;
822
+
823
+
$total_marks = $wpdb->get_var(
824
+
$wpdb->prepare(
825
+
"SELECT SUM(question_mark) total_marks
826
+
FROM {$wpdb->prefix}tutor_quiz_questions
827
+
WHERE quiz_id=%d",
828
+
$quiz_id
829
+
)
830
+
);
831
+
832
+
return floatval( $total_marks );
833
+
}
834
+
835
+
/**
836
+
* Quiz timeout by ajax
837
+
*
838
+
* @since 1.0.0
839
+
*
840
+
* @return void
841
+
*/
842
+
public function tutor_quiz_timeout() {
843
+
tutils()->checking_nonce();
844
+
845
+
global $wpdb;
846
+
847
+
$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
848
+
$attempt = tutor_utils()->is_started_quiz( $quiz_id );
849
+
850
+
if ( $attempt ) {
851
+
$attempt_id = $attempt->attempt_id;
852
+
853
+
$data = array(
854
+
'attempt_status' => 'attempt_timeout',
855
+
'total_marks' => self::get_quiz_total_marks( $quiz_id ),
856
+
'earned_marks' => 0,
857
+
'attempt_ended_at' => gmdate( 'Y-m-d H:i:s', tutor_time() ),
858
+
);
859
+
860
+
$wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $data, array( 'attempt_id' => $attempt->attempt_id ) );
861
+
862
+
do_action( 'tutor_quiz_timeout', $attempt_id, $quiz_id, $attempt->user_id );
863
+
864
+
wp_send_json_success();
865
+
}
866
+
867
+
wp_send_json_error( __( 'Quiz has been timeout already', 'tutor' ) );
868
+
}
869
+
870
+
/**
871
+
* Review quiz answer
872
+
*
873
+
* @since 1.0.0
874
+
*
875
+
* @return void
876
+
*/
877
+
public function review_quiz_answer() {
878
+
879
+
tutor_utils()->checking_nonce();
880
+
881
+
global $wpdb;
882
+
883
+
$attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
884
+
$context = Input::post( 'context' );
885
+
$attempt_answer_id = Input::post( 'attempt_answer_id', 0, Input::TYPE_INT );
886
+
$mark_as = Input::post( 'mark_as' );
887
+
888
+
if ( ! tutor_utils()->can_user_manage( 'attempt', $attempt_id ) || ! tutor_utils()->can_user_manage( 'attempt_answer', $attempt_answer_id ) ) {
889
+
wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
890
+
}
891
+
892
+
$attempt_answer = $wpdb->get_row(
893
+
$wpdb->prepare(
894
+
"SELECT *
895
+
FROM {$wpdb->prefix}tutor_quiz_attempt_answers
896
+
WHERE attempt_answer_id = %d
897
+
",
898
+
$attempt_answer_id
899
+
)
900
+
);
901
+
902
+
$attempt = tutor_utils()->get_attempt( $attempt_id );
903
+
$question = QuizModel::get_quiz_question_by_id( $attempt_answer->question_id );
904
+
$course_id = $attempt->course_id;
905
+
$student_id = $attempt->user_id;
906
+
$previous_ans = $attempt_answer->is_correct;
907
+
908
+
do_action( 'tutor_quiz_review_answer_before', $attempt_answer_id, $attempt_id, $mark_as );
909
+
910
+
if ( 'correct' === $mark_as ) {
911
+
$attempt_update_data = array();
912
+
$answer_update_data = array(
913
+
'achieved_mark' => $attempt_answer->question_mark,
914
+
'is_correct' => 1,
915
+
);
916
+
917
+
$wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
918
+
919
+
if ( 0 == $previous_ans || null == $previous_ans ) {
920
+
// if previous answer was wrong or in review then add point as correct.
921
+
$attempt_update_data = array(
922
+
'earned_marks' => $attempt->earned_marks + $attempt_answer->question_mark,
923
+
'is_manually_reviewed' => 1,
924
+
'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
925
+
);
926
+
}
927
+
928
+
if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
929
+
$attempt_update_data['attempt_status'] = 'attempt_ended';
930
+
}
931
+
932
+
if ( ! empty( $attempt_update_data ) ) {
933
+
$wpdb->update( $wpdb->tutor_quiz_attempts, $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
934
+
}
935
+
} elseif ( 'incorrect' === $mark_as ) {
936
+
$attempt_update_data = array();
937
+
$answer_update_data = array(
938
+
'achieved_mark' => '0.00',
939
+
'is_correct' => 0,
940
+
);
941
+
942
+
$wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
943
+
944
+
if ( 1 == $previous_ans ) {
945
+
// If previous ans was right then mynus.
946
+
$attempt_update_data = array(
947
+
'earned_marks' => $attempt->earned_marks - $attempt_answer->question_mark,
948
+
'is_manually_reviewed' => 1,
949
+
'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ),//phpcs:ignore
950
+
);
951
+
}
952
+
953
+
if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
954
+
$attempt_update_data['attempt_status'] = 'attempt_ended';
955
+
}
956
+
957
+
if ( ! empty( $attempt_update_data ) ) {
958
+
$wpdb->update( $wpdb->tutor_quiz_attempts, $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
959
+
}
960
+
}
961
+
962
+
QuizModel::update_attempt_result( $attempt_id );
963
+
964
+
do_action( 'tutor_quiz_review_answer_after', $attempt_answer_id, $attempt_id, $mark_as );
965
+
do_action( 'tutor_quiz/answer/review/after', $attempt_answer_id, $course_id, $student_id );
966
+
967
+
ob_start();
968
+
tutor_load_template_from_custom_path(
969
+
tutor()->path . '/views/quiz/attempt-details.php',
970
+
array(
971
+
'attempt_id' => $attempt_id,
972
+
'user_id' => $student_id,
973
+
'context' => $context,
974
+
'back_url' => Input::post( 'back_url' ),
975
+
)
976
+
);
977
+
wp_send_json_success( array( 'html' => ob_get_clean() ) );
978
+
}
979
+
980
+
/**
981
+
* Do auto course complete after review a quiz attempt.
982
+
*
983
+
* @since 2.4.0
984
+
*
985
+
* @param int $attempt_answer_id attempt answer id.
986
+
* @param int $course_id course id.
987
+
* @param int $user_id student id.
988
+
*
989
+
* @return void
990
+
*/
991
+
public function do_auto_course_complete( $attempt_answer_id, $course_id, $user_id ) {
992
+
if ( CourseModel::can_autocomplete_course( $course_id, $user_id ) ) {
993
+
CourseModel::mark_course_as_completed( $course_id, $user_id );
994
+
Course::set_review_popup_data( $user_id, $course_id );
995
+
}
996
+
}
997
+
998
+
/**
999
+
* Get a quiz details by id
1000
+
*
1001
+
* @return void
1002
+
*/
1003
+
public function ajax_quiz_details() {
1004
+
tutor_utils()->check_nonce();
1005
+
1006
+
$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1007
+
if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1008
+
$this->json_response(
1009
+
tutor_utils()->error_message(),
1010
+
null,
1011
+
HttpHelper::STATUS_FORBIDDEN
1012
+
);
1013
+
}
1014
+
1015
+
$data = QuizModel::get_quiz_details( $quiz_id );
1016
+
1017
+
$data = apply_filters( 'tutor_quiz_details_response', $data, $quiz_id );
1018
+
1019
+
$this->json_response(
1020
+
__( 'Quiz data fetched successfully', 'tutor' ),
1021
+
$data
1022
+
);
1023
+
}
1024
+
1025
+
/**
1026
+
* Delete quiz by id
1027
+
*
1028
+
* @since 1.0.0
1029
+
* @since 3.0.0 refactor and response change.
1030
+
*
1031
+
* @return void
1032
+
*/
1033
+
public function ajax_quiz_delete() {
1034
+
if ( ! tutor_utils()->is_nonce_verified() ) {
1035
+
$this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
1036
+
}
1037
+
1038
+
global $wpdb;
1039
+
1040
+
$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1041
+
if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1042
+
$this->json_response(
1043
+
tutor_utils()->error_message(),
1044
+
null,
1045
+
HttpHelper::STATUS_FORBIDDEN
1046
+
);
1047
+
}
1048
+
1049
+
$post = get_post( $quiz_id );
1050
+
if ( 'tutor_quiz' !== $post->post_type ) {
1051
+
$this->json_response(
1052
+
__( 'Invalid quiz', 'tutor' ),
1053
+
null,
1054
+
HttpHelper::STATUS_BAD_REQUEST
1055
+
);
1056
+
}
1057
+
1058
+
do_action( 'tutor_delete_quiz_before', $quiz_id );
1059
+
1060
+
$wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $quiz_id ) );
1061
+
$wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $quiz_id ) );
1062
+
1063
+
$questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $quiz_id ) );
1064
+
1065
+
if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
1066
+
$in_question_ids = QueryHelper::prepare_in_clause( $questions_ids );
1067
+
//phpcs:disable
1068
+
$wpdb->query(
1069
+
"DELETE
1070
+
FROM {$wpdb->prefix}tutor_quiz_question_answers
1071
+
WHERE belongs_question_id IN({$in_question_ids})
1072
+
"
1073
+
);
1074
+
//phpcs:enable
1075
+
}
1076
+
1077
+
$wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ) );
1078
+
1079
+
wp_delete_post( $quiz_id, true );
1080
+
1081
+
do_action( 'tutor_delete_quiz_after', $quiz_id );
1082
+
1083
+
$this->json_response(
1084
+
__( 'Quiz deleted successfully', 'tutor' ),
1085
+
$quiz_id
1086
+
);
1087
+
}
1088
+
1089
+
/**
1090
+
* Get answers by quiz id
1091
+
*
1092
+
* @since 1.0.0
1093
+
*
1094
+
* @param int $question_id question id.
1095
+
* @param mixed $question_type type of question.
1096
+
* @param boolean $is_correct only correct answers or not.
1097
+
*
1098
+
* @return wpdb:get_results
1099
+
*/
1100
+
private function get_answers_by_q_id( $question_id, $question_type, $is_correct = false ) {
1101
+
global $wpdb;
1102
+
1103
+
$correct_clause = $is_correct ? ' AND is_correct=1 ' : '';
1104
+
//phpcs:disable
1105
+
return $wpdb->get_results(
1106
+
$wpdb->prepare(
1107
+
"SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers
1108
+
WHERE belongs_question_id = %d
1109
+
AND belongs_question_type = %s
1110
+
{$correct_clause}
1111
+
ORDER BY answer_order ASC;
1112
+
",
1113
+
$question_id,
1114
+
esc_sql( $question_type )
1115
+
)
1116
+
);
1117
+
//phpcs:enable
1118
+
}
1119
+
1120
+
/**
1121
+
* Rendering quiz for frontend
1122
+
*
1123
+
* @since 1.0.0
1124
+
*
1125
+
* @return void send wp_json response
1126
+
*/
1127
+
public function tutor_render_quiz_content() {
1128
+
1129
+
tutor_utils()->checking_nonce();
1130
+
1131
+
$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1132
+
1133
+
if ( ! tutor_utils()->has_enrolled_content_access( 'quiz', $quiz_id ) ) {
1134
+
wp_send_json_error( array( 'message' => __( 'Access Denied.', 'tutor' ) ) );
1135
+
}
1136
+
1137
+
ob_start();
1138
+
global $post;
1139
+
1140
+
$post = get_post( $quiz_id ); //phpcs:ignore
1141
+
setup_postdata( $post );
1142
+
1143
+
single_quiz_contents();
1144
+
wp_reset_postdata();
1145
+
1146
+
$html = ob_get_clean();
1147
+
wp_send_json_success( array( 'html' => $html ) );
1148
+
}
1149
+
1150
+
/**
1151
+
* Get attempt details
1152
+
*
1153
+
* @since 1.0.0
1154
+
*
1155
+
* @param int $attempt_id required attempt id to get details.
1156
+
*
1157
+
* @return mixed object on success, null on failure
1158
+
*/
1159
+
public static function attempt_details( int $attempt_id ) {
1160
+
global $wpdb;
1161
+
$attempt_details = $wpdb->get_row(
1162
+
$wpdb->prepare(
1163
+
"SELECT *
1164
+
FROM {$wpdb->prefix}tutor_quiz_attempts
1165
+
WHERE attempt_id = %d
1166
+
",
1167
+
$attempt_id
1168
+
)
1169
+
);
1170
+
return $attempt_details;
1171
+
}
1172
+
1173
+
/**
1174
+
* Update quiz attempt info
1175
+
*
1176
+
* @since 1.0.0
1177
+
*
1178
+
* @param int $attempt_id attempt id.
1179
+
* @param mixed $attempt_info serialize data.
1180
+
*
1181
+
* @return bool, true on success, false on failure
1182
+
*/
1183
+
public static function update_attempt_info( int $attempt_id, $attempt_info ) {
1184
+
global $wpdb;
1185
+
$table = $wpdb->prefix . 'tutor_quiz_attempts';
1186
+
$update_info = $wpdb->update(
1187
+
$table,
1188
+
array( 'attempt_info' => $attempt_info ),
1189
+
array( 'attempt_id' => $attempt_id )
1190
+
);
1191
+
return $update_info ? true : false;
1192
+
}
1193
+
1194
+
/**
1195
+
* Attempt delete ajax request handler
1196
+
*
1197
+
* @since 2.1.0
1198
+
*
1199
+
* @return void wp_json response
1200
+
*/
1201
+
public function attempt_delete() {
1202
+
tutor_utils()->checking_nonce();
1203
+
1204
+
$attempt_id = Input::post( 'id', 0, Input::TYPE_INT );
1205
+
$attempt = tutor_utils()->get_attempt( $attempt_id );
1206
+
if ( ! $attempt ) {
1207
+
wp_send_json_error( __( 'Invalid attempt ID', 'tutor' ) );
1208
+
}
1209
+
1210
+
$user_id = get_current_user_id();
1211
+
$course_id = $attempt->course_id;
1212
+
1213
+
if ( tutor_utils()->can_user_edit_course( $user_id, $course_id ) ) {
1214
+
QuizModel::delete_quiz_attempt( $attempt_id );
1215
+
wp_send_json_success( __( 'Attempt deleted successfully!', 'tutor' ) );
1216
+
} else {
1217
+
wp_send_json_error( tutor_utils()->error_message() );
1218
+
}
1219
+
}
1220
+
1221
+
/**
1222
+
* Get all quiz attempts for a user in a specific course.
1223
+
*
1224
+
* @since 3.8.1
1225
+
*
1226
+
* @param int $course_id The ID of the course.
1227
+
* @param int $user_id The ID of the user.
1228
+
*
1229
+
* @return array Returns an array of quiz attempt objects with their answers, or an empty array on error.
1230
+
*/
1231
+
public function get_quiz_attempts_and_answers_by_course_id( int $course_id ): array {
1232
+
global $wpdb;
1233
+
1234
+
$results = QueryHelper::get_all( $wpdb->tutor_quiz_attempts, array( 'course_id' => $course_id ), 'course_id', -1 );
1235
+
1236
+
if ( empty( $results ) ) {
1237
+
return array();
1238
+
}
1239
+
1240
+
return array_map(
1241
+
function ( $item ) {
1242
+
$item->quiz_attempt_answers = $this->get_quiz_attempt_answers_by_attempt_id( $item->attempt_id );
1243
+
return $item;
1244
+
},
1245
+
$results
1246
+
);
1247
+
}
1248
+
1249
+
/**
1250
+
* Get all quiz attempt answers for a specific quiz attempt.
1251
+
*
1252
+
* @since 3.8.1
1253
+
*
1254
+
* @param int $attempt_id The ID of the quiz attempt.
1255
+
*
1256
+
* @return array Returns an array of quiz attempt answers objects, or an empty array on error.
1257
+
*/
1258
+
private function get_quiz_attempt_answers_by_attempt_id( int $attempt_id ): array {
1259
+
global $wpdb;
1260
+
1261
+
$results = QueryHelper::get_all( $wpdb->tutor_quiz_attempt_answers, array( 'quiz_attempt_id' => $attempt_id ), 'quiz_attempt_id', -1 );
1262
+
1263
+
if ( empty( $results ) ) {
1264
+
return array();
1265
+
}
1266
+
1267
+
return $results;
1268
+
}
1269
+
}
1270
+