PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 2.1.4
Tutor LMS – eLearning and online course solution v2.1.4
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 / models / QuizModel.php
tutor / models Last commit date
CourseModel.php 3 years ago LessonModel.php 3 years ago QuizModel.php 3 years ago WithdrawModel.php 3 years ago
QuizModel.php
745 lines
1 <?php
2 /**
3 * Quiz Model
4 *
5 * @package Tutor\Models
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com
8 * @since 2.0.10
9 */
10
11 namespace Tutor\Models;
12
13 use Tutor\Helpers\QueryHelper;
14
15 /**
16 * Class QuizModel
17 *
18 * @since 2.0.10
19 */
20 class QuizModel {
21
22 /**
23 * Get quiz table name
24 *
25 * @since 2.1.0
26 *
27 * @return string
28 */
29 public function get_table(): string {
30 global $wpdb;
31 return $wpdb->prefix . 'tutor_quiz_attempts';
32 }
33
34 /**
35 * Get all of the attempts by an user of a quiz
36 *
37 * @since 1.0.0
38 *
39 * @param int $quiz_id quiz ID.
40 * @param int $user_id user ID.
41 *
42 * @return array|bool|null|object
43 */
44 public function quiz_attempts( $quiz_id = 0, $user_id = 0 ) {
45 global $wpdb;
46
47 $quiz_id = tutor_utils()->get_post_id( $quiz_id );
48 $user_id = tutor_utils()->get_user_id( $user_id );
49
50 $attempts = $wpdb->get_results(
51 $wpdb->prepare(
52 "SELECT *
53 FROM {$wpdb->prefix}tutor_quiz_attempts
54 WHERE quiz_id = %d
55 AND user_id = %d
56 ORDER BY attempt_id DESC
57 ",
58 $quiz_id,
59 $user_id
60 )
61 );
62
63 if ( is_array( $attempts ) && count( $attempts ) ) {
64 return $attempts;
65 }
66
67 return false;
68 }
69
70 /**
71 * Get Quiz question by question id
72 *
73 * @since 1.0.0
74 *
75 * @param int $question_id question ID.
76 *
77 * @return array|bool|object|void|null
78 */
79 public static function get_quiz_question_by_id( $question_id = 0 ) {
80 global $wpdb;
81
82 if ( $question_id ) {
83 $question = $wpdb->get_row(
84 $wpdb->prepare(
85 "SELECT *
86 FROM {$wpdb->prefix}tutor_quiz_questions
87 WHERE question_id = %d
88 LIMIT 0, 1;
89 ",
90 $question_id
91 )
92 );
93
94 return $question;
95 }
96
97 return false;
98 }
99
100 /**
101 * Get all ended attempts by an user of a quiz
102 *
103 * @since 1.4.1
104 *
105 * @param int $quiz_id quiz ID.
106 * @param int $user_id user ID.
107 *
108 * @return array|bool|null|object
109 */
110 public function quiz_ended_attempts( $quiz_id = 0, $user_id = 0 ) {
111 global $wpdb;
112
113 $quiz_id = tutor_utils()->get_post_id( $quiz_id );
114 $user_id = tutor_utils()->get_user_id( $user_id );
115
116 $attempts = $wpdb->get_results(
117 $wpdb->prepare(
118 "SELECT *
119 FROM {$wpdb->prefix}tutor_quiz_attempts
120 WHERE quiz_id = %d
121 AND user_id = %d
122 AND attempt_status != %s
123 ",
124 $quiz_id,
125 $user_id,
126 'attempt_started'
127 )
128 );
129
130 if ( is_array( $attempts ) && count( $attempts ) ) {
131 return $attempts;
132 }
133
134 return false;
135 }
136
137 /**
138 * Get the next question order ID
139 *
140 * @since 1.0.0
141 *
142 * @param integer $quiz_id quiz ID.
143 *
144 * @return int
145 */
146 public static function quiz_next_question_order_id( $quiz_id ) {
147 global $wpdb;
148
149 $last_order = (int) $wpdb->get_var(
150 $wpdb->prepare(
151 "SELECT MAX(question_order)
152 FROM {$wpdb->prefix}tutor_quiz_questions
153 WHERE quiz_id = %d ;
154 ",
155 $quiz_id
156 )
157 );
158
159 return $last_order + 1;
160 }
161
162 /**
163 * Get next quiz question ID
164 *
165 * @since 1.0.0
166 *
167 * @return int
168 */
169 public static function quiz_next_question_id() {
170 global $wpdb;
171
172 $last_order = (int) $wpdb->get_var( "SELECT MAX(question_id) FROM {$wpdb->prefix}tutor_quiz_questions;" );
173 return $last_order + 1;
174 }
175
176 /**
177 * Total number of quiz attempts
178 *
179 * @since 1.0.0
180 *
181 * @param string $search_term search term.
182 * @param integer $course_id course ID.
183 * @param string $tab tab.
184 * @param string $date_filter date filter.
185 *
186 * @return int
187 */
188 public static function get_total_quiz_attempts( $search_term = '', int $course_id = 0, string $tab = '', $date_filter = '' ) {
189 global $wpdb;
190
191 if ( '' !== $search_term ) {
192 $search_term = '%' . $wpdb->esc_like( $search_term ) . '%';
193 }
194
195 // Set query based on action tab.
196 $pass_mark = "(((SUBSTRING_INDEX(SUBSTRING_INDEX(quiz_attempts.attempt_info, '\"passing_grade\";s:2:\"', -1), '\"', 1))/100)*quiz_attempts.total_marks)";
197 $pending_count = "(SELECT COUNT(DISTINCT attempt_answer_id) FROM {$wpdb->prefix}tutor_quiz_attempt_answers WHERE quiz_attempt_id=quiz_attempts.attempt_id AND is_correct IS NULL)";
198
199 $tab_join = '';
200 $tab_clause = '';
201 if ( '' !== $tab ) {
202 $tab_join = "INNER JOIN {$wpdb->prefix}tutor_quiz_attempt_answers AS ans ON quiz_attempts.attempt_id = ans.quiz_attempt_id";
203 }
204 switch ( $tab ) {
205 case 'pass':
206 // Just check if the earned mark is greater than pass mark.
207 // It doesn't matter if there is any pending or failed question.
208 $tab_clause = " AND quiz_attempts.earned_marks >= {$pass_mark} ";
209 break;
210
211 case 'fail':
212 // Check if earned marks is less than pass mark and there is no pending question.
213 $tab_clause = " AND quiz_attempts.earned_marks < {$pass_mark}
214 AND {$pending_count} = 0 ";
215 break;
216 case 'pending':
217 $tab_clause = " AND {$pending_count} > 0 ";
218 break;
219 }
220
221 $course_join = '';
222 $course_clause = '';
223 if ( $course_id || '' !== $search_term ) {
224 $course_join = "INNER JOIN {$wpdb->posts} AS course ON course.ID = quiz_attempts.course_id";
225 }
226 if ( $course_id ) {
227 $course_clause = " AND quiz_attempts.course_id = $course_id";
228 }
229
230 $user_join = '';
231 $user_clause = '';
232 $search_term1 = sanitize_text_field( $search_term );
233 $search_term2 = sanitize_text_field( $search_term );
234 $search_term3 = sanitize_text_field( $search_term );
235 if ( '' !== $search_term ) {
236 $user_join = "INNER JOIN {$wpdb->users}
237 ON quiz_attempts.user_id = {$wpdb->users}.ID";
238
239 $user_clause = "AND ( user_email LIKE '%$search_term1%' OR display_name LIKE '%$search_term2%' OR course.post_title LIKE '%$search_term3%' )";
240 }
241
242 if ( '' !== $date_filter ) {
243 $date_filter = '' != $date_filter ? tutor_get_formated_date( 'Y-m-d', $date_filter ) : '';
244 $date_filter = '' != $date_filter ? " AND DATE(quiz_attempts.attempt_started_at) = '$date_filter' " : '';
245 }
246
247 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
248 $count = $wpdb->get_var(
249 $wpdb->prepare(
250 "SELECT COUNT( DISTINCT attempt_id)
251 FROM {$wpdb->prefix}tutor_quiz_attempts quiz_attempts
252 INNER JOIN {$wpdb->posts} quiz
253 ON quiz_attempts.quiz_id = quiz.ID
254 {$user_join}
255 {$course_join}
256 {$tab_join}
257 WHERE attempt_status != %s
258 {$user_clause}
259 {$course_clause}
260 {$tab_clause}
261 {$date_filter}
262 ",
263 'attempt_started'
264 )
265 );
266
267 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
268
269 return (int) $count;
270 }
271
272 /**
273 * Get the all quiz attempts
274 *
275 * @since 1.0.0
276 * @since 1.9.5 sorting paramas added
277 *
278 * @param integer $start start.
279 * @param integer $limit limit.
280 * @param string $search_filter search filter.
281 * @param string $course_filter course filter.
282 * @param string $date_filter date filter.
283 * @param string $order_filter order filter.
284 * @param mixed $result_state result state.
285 * @param boolean $count_only count only or not.
286 * @param boolean $instructor_id_check need instructor id check or not.
287 *
288 * @return mixed
289 */
290 public static function get_quiz_attempts( $start = 0, $limit = 10, $search_filter = '', $course_filter = '', $date_filter = '', $order_filter = 'DESC', $result_state = null, $count_only = false, $instructor_id_check = false ) {
291 global $wpdb;
292
293 $search_term_raw = $search_filter;
294 $search_filter = '%' . $wpdb->esc_like( $search_filter ) . '%';
295
296 // Filter by course.
297 if ( '' != $course_filter ) {
298 ! is_array( $course_filter ) ? $course_filter = array( $course_filter ) : 0;
299 $course_ids = implode( ',', $course_filter );
300 $course_filter = " AND quiz_attempts.course_id IN ($course_ids) ";
301 }
302
303 // Filter by date.
304 $date_filter = '' != $date_filter ? tutor_get_formated_date( 'Y-m-d', $date_filter ) : '';
305 $date_filter = '' != $date_filter ? " AND DATE(quiz_attempts.attempt_started_at) = '$date_filter' " : '';
306
307 $result_clause = '';
308 $select_columns = $count_only ? 'COUNT(DISTINCT quiz_attempts.attempt_id)' : 'DISTINCT quiz_attempts.*, quiz.post_title, users.user_email, users.user_login, users.display_name';
309 $limit_offset = $count_only ? '' : ' LIMIT ' . $limit . ' OFFSET ' . $start;
310
311 $pass_mark = "(((SUBSTRING_INDEX(SUBSTRING_INDEX(quiz_attempts.attempt_info, '\"passing_grade\";s:2:\"', -1), '\"', 1))/100)*quiz_attempts.total_marks)";
312 $pending_count = "(SELECT COUNT(DISTINCT attempt_answer_id) FROM {$wpdb->prefix}tutor_quiz_attempt_answers WHERE quiz_attempt_id=quiz_attempts.attempt_id AND is_correct IS NULL)";
313
314 // Get attempts by instructor ID.
315 $instructor_clause = '';
316 $instructor_join = '';
317 if ( $instructor_id_check ) {
318 $current_user_id = get_current_user_id();
319 $instructor_id = tutor_utils()->has_user_role( 'administrator', $current_user_id ) ? null : $current_user_id;
320
321 if ( $instructor_id ) {
322 // $instructor_clause = " AND (instructor_meta.meta_key='_tutor_instructor_course_id' AND instructor_meta.user_id=$instructor_id)";
323 $instructor_clause = " INNER JOIN {$wpdb->prefix}usermeta AS instructor_meta ON course.ID = instructor_meta.meta_value AND (instructor_meta.meta_key='_tutor_instructor_course_id' AND instructor_meta.user_id=$instructor_id) ";
324 }
325 }
326
327 // Switc hthrough result state and assign meta clause.
328 switch ( $result_state ) {
329 case 'pass':
330 // Just check if the earned mark is greater than pass mark.
331 // It doesn't matter if there is any pending or failed question.
332 $result_clause = " AND quiz_attempts.earned_marks>={$pass_mark} ";
333 break;
334
335 case 'fail':
336 // Check if earned marks is less than pass mark and there is no pending question.
337 $result_clause = " AND quiz_attempts.earned_marks<{$pass_mark}
338 AND {$pending_count}=0 ";
339 break;
340
341 case 'pending':
342 $result_clause = " AND {$pending_count}>0 ";
343 break;
344 }
345
346 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
347 $query = $wpdb->prepare(
348 "SELECT {$select_columns}
349 FROM {$wpdb->prefix}tutor_quiz_attempts quiz_attempts
350 INNER JOIN {$wpdb->posts} quiz ON quiz_attempts.quiz_id = quiz.ID
351 INNER JOIN {$wpdb->users} AS users ON quiz_attempts.user_id = users.ID
352 INNER JOIN {$wpdb->posts} AS course ON course.ID = quiz_attempts.course_id
353 INNER JOIN {$wpdb->prefix}tutor_quiz_attempt_answers AS ans ON quiz_attempts.attempt_id = ans.quiz_attempt_id
354 {$instructor_clause}
355 WHERE quiz_attempts.attempt_ended_at IS NOT NULL
356 AND (
357 users.user_email = %s
358 OR users.display_name LIKE %s
359 OR quiz.post_title LIKE %s
360 OR course.post_title LIKE %s
361 )
362 AND quiz_attempts.attempt_ended_at IS NOT NULL
363 {$result_clause}
364 {$course_filter}
365 {$date_filter}
366 ORDER BY quiz_attempts.attempt_ended_at {$order_filter} {$limit_offset}",
367 $search_term_raw,
368 $search_filter,
369 $search_filter,
370 $search_filter
371 );
372
373 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
374
375 //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
376 return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query );
377 }
378
379 /**
380 * Delete quizattempt for user
381 *
382 * @since 1.9.5
383 *
384 * @param mixed $attempt_ids attempt ids.
385 *
386 * @return void
387 */
388 public static function delete_quiz_attempt( $attempt_ids ) {
389 global $wpdb;
390
391 // Singlular to array.
392 ! is_array( $attempt_ids ) ? $attempt_ids = array( $attempt_ids ) : 0;
393
394 if ( count( $attempt_ids ) ) {
395 $attempt_ids = implode( ',', $attempt_ids );
396
397 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
398 // Deleting attempt (comment), child attempt and attempt meta (comment meta).
399 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempts WHERE attempt_id IN($attempt_ids)" );
400 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempt_answers WHERE quiz_attempt_id IN($attempt_ids)" );
401 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
402
403 do_action( 'tutor_quiz/attempt_deleted', $attempt_ids );
404 }
405 }
406
407 /**
408 * Sorting params added on quiz attempt
409 *
410 * @since 1.9.5
411 *
412 * @param integer $start start.
413 * @param integer $limit limit.
414 * @param array $course_ids course ids.
415 * @param string $search_filter search filter.
416 * @param string $course_filter course filter.
417 * @param string $date_filter date filter.
418 * @param string $order_filter order filter.
419 * @param mixed $user_id user id.
420 * @param boolean $count_only is only count or not.
421 * @param boolean $all_attempt need all atempt or not.
422 *
423 * @return mixed
424 */
425 public static function get_quiz_attempts_by_course_ids( $start = 0, $limit = 10, $course_ids = array(), $search_filter = '', $course_filter = '', $date_filter = '', $order_filter = '', $user_id = null, $count_only = false, $all_attempt = false ) {
426 global $wpdb;
427 $search_filter = sanitize_text_field( $search_filter );
428 $course_filter = sanitize_text_field( $course_filter );
429 $date_filter = sanitize_text_field( $date_filter );
430
431 $course_ids = array_map(
432 function ( $id ) {
433 return "'" . esc_sql( $id ) . "'";
434 },
435 $course_ids
436 );
437
438 $course_ids_in = count( $course_ids ) ? ' AND quiz_attempts.course_id IN (' . implode( ', ', $course_ids ) . ') ' : '';
439
440 $search_filter = $search_filter ? '%' . $wpdb->esc_like( $search_filter ) . '%' : '';
441 $search_term_raw = $search_filter;
442 $search_filter = $search_filter ? "AND ( users.user_email = '{$search_term_raw}' OR users.display_name LIKE {$search_filter} OR quiz.post_title LIKE {$search_filter} OR course.post_title LIKE {$search_filter} )" : '';
443
444 $course_filter = '' != $course_filter ? " AND quiz_attempts.course_id = $course_filter " : '';
445 $date_filter = '' != $date_filter ? tutor_get_formated_date( 'Y-m-d', $date_filter ) : '';
446 $date_filter = '' != $date_filter ? " AND DATE(quiz_attempts.attempt_started_at) = '$date_filter' " : '';
447 $user_filter = $user_id ? ' AND user_id=\'' . esc_sql( $user_id ) . '\' ' : '';
448
449 $limit_offset = $count_only ? '' : " LIMIT {$start}, {$limit} ";
450 $select_col = $count_only ? ' COUNT(DISTINCT quiz_attempts.attempt_id) ' : ' quiz_attempts.*, users.*, quiz.* ';
451
452 $attempt_type = $all_attempt ? '' : " AND quiz_attempts.attempt_status != 'attempt_started' ";
453
454 $query = "SELECT {$select_col}
455 FROM {$wpdb->prefix}tutor_quiz_attempts AS quiz_attempts
456 INNER JOIN {$wpdb->posts} AS quiz
457 ON quiz_attempts.quiz_id = quiz.ID
458 INNER JOIN {$wpdb->users} AS users
459 ON quiz_attempts.user_id = users.ID
460 INNER JOIN {$wpdb->posts} AS course
461 ON course.ID = quiz_attempts.course_id
462 WHERE 1=1
463 {$attempt_type}
464 {$course_ids_in}
465 {$search_filter}
466 {$course_filter}
467 {$date_filter}
468 {$user_filter}
469 ORDER BY quiz_attempts.attempt_id {$order_filter} {$limit_offset};";
470
471 //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
472 return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query );
473 }
474
475 /**
476 * Get answers list by quiz question
477 *
478 * @since 1.0.0
479 *
480 * @param int $question_id question ID.
481 * @param bool $rand rand.
482 *
483 * @return array|bool|null|object
484 */
485 public static function get_answers_by_quiz_question( $question_id, $rand = false ) {
486 global $wpdb;
487
488 $question = $wpdb->get_row(
489 $wpdb->prepare(
490 "SELECT *
491 FROM {$wpdb->prefix}tutor_quiz_questions
492 WHERE question_id = %d;
493 ",
494 $question_id
495 )
496 );
497
498 if ( ! $question ) {
499 return false;
500 }
501
502 $order = ' answer_order ASC ';
503 if ( 'ordering' === $question->question_type ) {
504 $order = ' RAND() ';
505 }
506
507 if ( $rand ) {
508 $order = ' RAND() ';
509 }
510
511 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
512 $answers = $wpdb->get_results(
513 $wpdb->prepare(
514 "SELECT *
515 FROM {$wpdb->prefix}tutor_quiz_question_answers
516 WHERE belongs_question_id = %d
517 AND belongs_question_type = %s
518 ORDER BY {$order}
519 ",
520 $question_id,
521 $question->question_type
522 )
523 );
524 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
525
526 return $answers;
527 }
528
529 /**
530 * Get quiz answers by attempt id
531 *
532 * @since 1.0.0
533 *
534 * @param mixed $attempt_id attempt ID.
535 * @param bool $add_index need index or not.
536 *
537 * @return array|null|object
538 */
539 public static function get_quiz_answers_by_attempt_id( $attempt_id, $add_index = false ) {
540 global $wpdb;
541
542 $ids = is_array( $attempt_id ) ? $attempt_id : array( $attempt_id );
543 $ids_in = implode( ',', $ids );
544
545 if ( empty( $ids_in ) ) {
546 // Prevent empty.
547 return array();
548 }
549
550 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
551 $results = $wpdb->get_results(
552 "SELECT answers.*,
553 question.question_title,
554 question.question_type
555 FROM {$wpdb->prefix}tutor_quiz_attempt_answers answers
556 LEFT JOIN {$wpdb->prefix}tutor_quiz_questions question
557 ON answers.question_id = question.question_id
558 WHERE answers.quiz_attempt_id IN ({$ids_in})
559 ORDER BY attempt_answer_id ASC;"
560 );
561 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
562
563 if ( $add_index ) {
564 $new_array = array();
565
566 foreach ( $results as $result ) {
567 ! isset( $new_array[ $result->quiz_attempt_id ] ) ? $new_array[ $result->quiz_attempt_id ] = array() : 0;
568 $new_array[ $result->quiz_attempt_id ][] = $result;
569 }
570
571 return $new_array;
572 }
573
574 return $results;
575 }
576
577 /**
578 * Get single answer by answer_id
579 *
580 * @since 1.0.0
581 *
582 * @param array|init $answer_id answer id.
583 *
584 * @return array|null|object
585 */
586 public static function get_answer_by_id( $answer_id ) {
587 global $wpdb;
588
589 ! is_array( $answer_id ) ? $answer_id = array( $answer_id ) : 0;
590
591 $answer_id = array_map(
592 function ( $id ) {
593 return "'" . esc_sql( $id ) . "'";
594 },
595 $answer_id
596 );
597
598 $in_ids_string = implode( ', ', $answer_id );
599
600 //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
601 $answer = $wpdb->get_results(
602 $wpdb->prepare(
603 "SELECT answer.*,
604 question.question_title,
605 question.question_type
606 FROM {$wpdb->prefix}tutor_quiz_question_answers answer
607 LEFT JOIN {$wpdb->prefix}tutor_quiz_questions question
608 ON answer.belongs_question_id = question.question_id
609 WHERE answer.answer_id IN (" . $in_ids_string . ')
610 AND 1 = %d;
611 ',
612 1
613 )
614 );
615 //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
616
617 return $answer;
618 }
619
620 /**
621 * Get quiz attempt timing
622 *
623 * @since 1.0.0
624 *
625 * @param mixed $attempt_data attempt data.
626 * @return array
627 */
628 public static function get_quiz_attempt_timing( $attempt_data ) {
629 $attempt_duration = '';
630 $attempt_duration_taken = '';
631 $attempt_info = @unserialize( $attempt_data->attempt_info );
632 if ( is_array( $attempt_info ) ) {
633 // Allowed duration.
634 if ( isset( $attempt_info['time_limit'] ) ) {
635 //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
636 $time_type = __( ucwords( tutor_utils()->array_get( 'time_limit.time_type', $attempt_info, 'minutes' ) ), 'tutor' );
637 $time_value = tutor_utils()->array_get( 'time_limit.time_value', $attempt_info, 0 );
638 $attempt_duration = $time_value . ' ' . $time_type;
639 }
640
641 // Taken duration.
642 $seconds = strtotime( $attempt_data->attempt_ended_at ) - strtotime( $attempt_data->attempt_started_at );
643 $attempt_duration_taken = tutor_utils()->seconds_to_time( $seconds );
644 }
645
646 return compact( 'attempt_duration', 'attempt_duration_taken' );
647 }
648
649 /**
650 * Check student is passed in a quiz or not.
651 * Quiz retry mode: student required at least one quiz passed in attempts
652 *
653 * @since 2.1.0
654 *
655 * @param int $quiz_id quiz ID.
656 * @param int $user_id user ID.
657 *
658 * @return boolean
659 */
660 public static function is_quiz_passed( $quiz_id, $user_id = 0 ) {
661 global $wpdb;
662
663 $user_id = tutor_utils()->get_user_id( $user_id );
664 $attempts = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}tutor_quiz_attempts WHERE user_id=%d AND quiz_id=%d", $user_id, $quiz_id ) );
665 $required_percentage = tutor_utils()->get_quiz_option( $quiz_id, 'passing_grade', 0 );
666
667 foreach ( $attempts as $attempt ) {
668 $earned_percentage = $attempt->earned_marks > 0 ? ( ( $attempt->earned_marks * 100 ) / $attempt->total_marks ) : 0;
669 if ( $earned_percentage >= $required_percentage ) {
670 return true;
671 }
672 }
673
674 return false;
675 }
676
677 /**
678 * Get all question type for a quiz
679 *
680 * @since 2.1.0
681 *
682 * @param integer $quiz_id quiz ID.
683 *
684 * @return array
685 */
686 public static function get_quiz_question_types( int $quiz_id ) {
687 global $wpdb;
688 $types = $wpdb->get_col(
689 $wpdb->prepare( "SELECT DISTINCT question_type FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id=%d", $quiz_id )
690 );
691
692 return $types;
693 }
694
695 /**
696 * Check a quiz attempt need manual review or not
697 *
698 * @since 2.1.0
699 *
700 * @param int $quiz_id quiz ID.
701 *
702 * @return boolean
703 */
704 public static function is_manual_review_required( $quiz_id ) {
705 $required = false;
706 $review_question_types = array( 'open_ended', 'short_answer' );
707 $question_types = self::get_quiz_question_types( $quiz_id );
708
709 foreach ( $review_question_types as $type ) {
710 if ( in_array( $type, $question_types, true ) ) {
711 $required = true;
712 break;
713 }
714 }
715
716 return $required;
717 }
718
719 /**
720 * Get last or first quiz attempt
721 *
722 * @since 2.1.0
723 * @since 2.1.3 user_id param added.
724 *
725 * @param integer $quiz_id quiz id to get attempt of.
726 * @param integer $user_id user ID who attempt the quiz.
727 * @param string $order ASC or DESC, default is DESC
728 * pass ASC to get first attempt.
729 *
730 * @return mixed object on success, null on failure
731 */
732 public function get_first_or_last_attempt( int $quiz_id, int $user_id = 0, string $order = 'DESC' ) {
733 $attempt = QueryHelper::get_row(
734 $this->get_table(),
735 array(
736 'quiz_id' => $quiz_id,
737 'user_id' => tutor_utils()->get_user_id( $user_id ),
738 ),
739 'attempt_id',
740 $order
741 );
742 return $attempt;
743 }
744 }
745