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