PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 2.2.1
Tutor LMS – eLearning and online course solution v2.2.1
3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / 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
791 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\Cache\TutorCache;
14 use Tutor\Helpers\QueryHelper;
15
16 /**
17 * Class QuizModel
18 *
19 * @since 2.0.10
20 */
21 class QuizModel {
22
23 /**
24 * Get quiz table name
25 *
26 * @since 2.1.0
27 *
28 * @return string
29 */
30 public function get_table(): string {
31 global $wpdb;
32 return $wpdb->prefix . 'tutor_quiz_attempts';
33 }
34
35 /**
36 * Get all of the attempts by an user of a quiz
37 *
38 * @since 1.0.0
39 *
40 * @param int $quiz_id quiz ID.
41 * @param int $user_id user ID.
42 *
43 * @return array|bool|null|object
44 */
45 public function quiz_attempts( $quiz_id = 0, $user_id = 0 ) {
46 global $wpdb;
47
48 $quiz_id = tutor_utils()->get_post_id( $quiz_id );
49 $user_id = tutor_utils()->get_user_id( $user_id );
50
51 $cache_key = "tutor_quiz_attempts_for_{$user_id}_{$quiz_id}";
52 $attempts = TutorCache::get( $cache_key );
53
54 if ( false === $attempts ) {
55 $attempts = $wpdb->get_results(
56 $wpdb->prepare(
57 "SELECT *
58 FROM {$wpdb->prefix}tutor_quiz_attempts
59 WHERE quiz_id = %d
60 AND user_id = %d
61 ORDER BY attempt_id DESC
62 ",
63 $quiz_id,
64 $user_id
65 )
66 );
67 TutorCache::set( $cache_key, $attempts );
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 = array(), $date_filter = '', $order_filter = 'DESC', $result_state = null, $count_only = false, $instructor_id_check = false ) {
298 global $wpdb;
299
300 $start = sanitize_text_field( $start );
301 $limit = sanitize_text_field( $limit );
302 $search_filter = sanitize_text_field( $search_filter );
303 $course_filter = sanitize_text_field( $course_filter );
304 $date_filter = sanitize_text_field( $date_filter );
305 $order_filter = sanitize_sql_orderby( $order_filter );
306
307 $search_term_raw = $search_filter;
308 $search_filter = '%' . $wpdb->esc_like( $search_filter ) . '%';
309
310 // Filter by course.
311 if ( '' != $course_filter ) {
312 ! is_array( $course_filter ) ? $course_filter = array( $course_filter ) : 0;
313 $course_ids = implode( ',', array_map( 'intval', $course_filter ) );
314 $course_filter = " AND quiz_attempts.course_id IN ($course_ids) ";
315 }
316
317 // Filter by date.
318 $date_filter = '' != $date_filter ? tutor_get_formated_date( 'Y-m-d', $date_filter ) : '';
319 $date_filter = '' != $date_filter ? " AND DATE(quiz_attempts.attempt_started_at) = '$date_filter' " : '';
320
321 $result_clause = '';
322 $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';
323 $limit_offset = $count_only ? '' : ' LIMIT ' . $limit . ' OFFSET ' . $start;
324
325 $pass_mark = "(((SUBSTRING_INDEX(SUBSTRING_INDEX(quiz_attempts.attempt_info, '\"passing_grade\";s:2:\"', -1), '\"', 1))/100)*quiz_attempts.total_marks)";
326 $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)";
327
328 // Get attempts by instructor ID.
329 $instructor_clause = '';
330 $instructor_join = '';
331 if ( $instructor_id_check ) {
332 $current_user_id = get_current_user_id();
333 $instructor_id = tutor_utils()->has_user_role( 'administrator', $current_user_id ) ? null : $current_user_id;
334
335 if ( $instructor_id ) {
336 // $instructor_clause = " AND (instructor_meta.meta_key='_tutor_instructor_course_id' AND instructor_meta.user_id=$instructor_id)";
337 $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) ";
338 }
339 }
340
341 // Switc hthrough result state and assign meta clause.
342 switch ( $result_state ) {
343 case 'pass':
344 // Just check if the earned mark is greater than pass mark.
345 // It doesn't matter if there is any pending or failed question.
346 $result_clause = " AND quiz_attempts.earned_marks>={$pass_mark} ";
347 break;
348
349 case 'fail':
350 // Check if earned marks is less than pass mark and there is no pending question.
351 $result_clause = " AND quiz_attempts.earned_marks<{$pass_mark}
352 AND {$pending_count}=0 ";
353 break;
354
355 case 'pending':
356 $result_clause = " AND {$pending_count}>0 ";
357 break;
358 }
359
360 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
361 $query = $wpdb->prepare(
362 "SELECT {$select_columns}
363 FROM {$wpdb->prefix}tutor_quiz_attempts quiz_attempts
364 INNER JOIN {$wpdb->posts} quiz ON quiz_attempts.quiz_id = quiz.ID
365 INNER JOIN {$wpdb->users} AS users ON quiz_attempts.user_id = users.ID
366 INNER JOIN {$wpdb->posts} AS course ON course.ID = quiz_attempts.course_id
367 INNER JOIN {$wpdb->prefix}tutor_quiz_attempt_answers AS ans ON quiz_attempts.attempt_id = ans.quiz_attempt_id
368 {$instructor_clause}
369 WHERE quiz_attempts.attempt_ended_at IS NOT NULL
370 AND (
371 users.user_email = %s
372 OR users.display_name LIKE %s
373 OR quiz.post_title LIKE %s
374 OR course.post_title LIKE %s
375 )
376 AND quiz_attempts.attempt_ended_at IS NOT NULL
377 {$result_clause}
378 {$course_filter}
379 {$date_filter}
380 ORDER BY quiz_attempts.attempt_ended_at {$order_filter} {$limit_offset}",
381 $search_term_raw,
382 $search_filter,
383 $search_filter,
384 $search_filter
385 );
386
387 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
388
389 //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
390 return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query );
391 }
392
393 /**
394 * Delete quizattempt for user
395 *
396 * @since 1.9.5
397 *
398 * @param mixed $attempt_ids attempt ids.
399 *
400 * @return void
401 */
402 public static function delete_quiz_attempt( $attempt_ids ) {
403 global $wpdb;
404
405 // Singlular to array.
406 ! is_array( $attempt_ids ) ? $attempt_ids = array( $attempt_ids ) : 0;
407
408 if ( count( $attempt_ids ) ) {
409 $attempt_ids = implode( ',', $attempt_ids );
410
411 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
412 // Deleting attempt (comment), child attempt and attempt meta (comment meta).
413 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempts WHERE attempt_id IN($attempt_ids)" );
414 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempt_answers WHERE quiz_attempt_id IN($attempt_ids)" );
415 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
416
417 do_action( 'tutor_quiz/attempt_deleted', $attempt_ids );
418 }
419 }
420
421 /**
422 * Sorting params added on quiz attempt
423 *
424 * @since 1.9.5
425 *
426 * @param integer $start start.
427 * @param integer $limit limit.
428 * @param array $course_ids course ids.
429 * @param string $search_filter search filter.
430 * @param string $course_filter course filter.
431 * @param string $date_filter date filter.
432 * @param string $order_filter order filter.
433 * @param mixed $user_id user id.
434 * @param boolean $count_only is only count or not.
435 * @param boolean $all_attempt need all atempt or not.
436 *
437 * @return mixed
438 */
439 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 ) {
440 global $wpdb;
441 $search_filter = sanitize_text_field( $search_filter );
442 $course_filter = (int) sanitize_text_field( $course_filter );
443 $date_filter = sanitize_text_field( $date_filter );
444 $order_filter = sanitize_sql_orderby( $order_filter );
445
446 $course_ids = array_map(
447 function ( $id ) {
448 return "'" . esc_sql( $id ) . "'";
449 },
450 $course_ids
451 );
452
453 $course_ids_in = count( $course_ids ) ? ' AND quiz_attempts.course_id IN (' . implode( ', ', $course_ids ) . ') ' : '';
454
455 $search_filter = $search_filter ? '%' . $wpdb->esc_like( $search_filter ) . '%' : '';
456 $search_term_raw = $search_filter;
457 $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} )" : '';
458
459 $course_filter = 0 !== $course_filter ? " AND quiz_attempts.course_id = $course_filter " : '';
460 $date_filter = '' != $date_filter ? tutor_get_formated_date( 'Y-m-d', $date_filter ) : '';
461 $date_filter = '' != $date_filter ? " AND DATE(quiz_attempts.attempt_started_at) = '$date_filter' " : '';
462 $user_filter = $user_id ? ' AND user_id=\'' . esc_sql( $user_id ) . '\' ' : '';
463
464 $limit_offset = $count_only ? '' : " LIMIT {$start}, {$limit} ";
465 $select_col = $count_only ? ' COUNT(DISTINCT quiz_attempts.attempt_id) ' : ' quiz_attempts.*, users.*, quiz.* ';
466
467 $attempt_type = $all_attempt ? '' : " AND quiz_attempts.attempt_status != 'attempt_started' ";
468
469 $query = "SELECT {$select_col}
470 FROM {$wpdb->prefix}tutor_quiz_attempts AS quiz_attempts
471 INNER JOIN {$wpdb->posts} AS quiz
472 ON quiz_attempts.quiz_id = quiz.ID
473 INNER JOIN {$wpdb->users} AS users
474 ON quiz_attempts.user_id = users.ID
475 INNER JOIN {$wpdb->posts} AS course
476 ON course.ID = quiz_attempts.course_id
477 WHERE 1=1
478 {$attempt_type}
479 {$course_ids_in}
480 {$search_filter}
481 {$course_filter}
482 {$date_filter}
483 {$user_filter}
484 ORDER BY quiz_attempts.attempt_id {$order_filter} {$limit_offset};";
485
486 //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
487 return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query );
488 }
489
490 /**
491 * Get answers list by quiz question
492 *
493 * @since 1.0.0
494 *
495 * @param int $question_id question ID.
496 * @param bool $rand rand.
497 *
498 * @return array|bool|null|object
499 */
500 public static function get_answers_by_quiz_question( $question_id, $rand = false ) {
501 global $wpdb;
502
503 $question = $wpdb->get_row(
504 $wpdb->prepare(
505 "SELECT *
506 FROM {$wpdb->prefix}tutor_quiz_questions
507 WHERE question_id = %d;
508 ",
509 $question_id
510 )
511 );
512
513 if ( ! $question ) {
514 return false;
515 }
516
517 $order = ' answer_order ASC ';
518 if ( 'ordering' === $question->question_type ) {
519 $order = ' RAND() ';
520 }
521
522 if ( $rand ) {
523 $order = ' RAND() ';
524 }
525
526 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
527 $answers = $wpdb->get_results(
528 $wpdb->prepare(
529 "SELECT *
530 FROM {$wpdb->prefix}tutor_quiz_question_answers
531 WHERE belongs_question_id = %d
532 AND belongs_question_type = %s
533 ORDER BY {$order}
534 ",
535 $question_id,
536 $question->question_type
537 )
538 );
539 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
540
541 return $answers;
542 }
543
544 /**
545 * Get quiz answers by attempt id
546 *
547 * @since 1.0.0
548 *
549 * @param mixed $attempt_id attempt ID.
550 * @param bool $add_index need index or not.
551 *
552 * @return array|null|object
553 */
554 public static function get_quiz_answers_by_attempt_id( $attempt_id, $add_index = false ) {
555 global $wpdb;
556
557 $ids = is_array( $attempt_id ) ? $attempt_id : array( $attempt_id );
558 $ids_in = implode( ',', $ids );
559
560 if ( empty( $ids_in ) ) {
561 // Prevent empty.
562 return array();
563 }
564
565 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
566 $results = $wpdb->get_results(
567 "SELECT answers.*,
568 question.*
569 FROM {$wpdb->prefix}tutor_quiz_attempt_answers answers
570 LEFT JOIN {$wpdb->prefix}tutor_quiz_questions question
571 ON answers.question_id = question.question_id
572 WHERE answers.quiz_attempt_id IN ({$ids_in})
573 ORDER BY attempt_answer_id ASC;"
574 );
575 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
576
577 if ( $add_index ) {
578 $new_array = array();
579
580 foreach ( $results as $result ) {
581 ! isset( $new_array[ $result->quiz_attempt_id ] ) ? $new_array[ $result->quiz_attempt_id ] = array() : 0;
582 $new_array[ $result->quiz_attempt_id ][] = $result;
583 }
584
585 return $new_array;
586 }
587
588 return $results;
589 }
590
591 /**
592 * Get single answer by answer_id
593 *
594 * @since 1.0.0
595 *
596 * @param array|init $answer_id answer id.
597 *
598 * @return array|null|object
599 */
600 public static function get_answer_by_id( $answer_id ) {
601 global $wpdb;
602
603 ! is_array( $answer_id ) ? $answer_id = array( $answer_id ) : 0;
604
605 $answer_id = array_map(
606 function ( $id ) {
607 return "'" . esc_sql( $id ) . "'";
608 },
609 $answer_id
610 );
611
612 $in_ids_string = implode( ', ', $answer_id );
613
614 //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
615 $answer = $wpdb->get_results(
616 $wpdb->prepare(
617 "SELECT answer.*,
618 question.question_title,
619 question.question_type
620 FROM {$wpdb->prefix}tutor_quiz_question_answers answer
621 LEFT JOIN {$wpdb->prefix}tutor_quiz_questions question
622 ON answer.belongs_question_id = question.question_id
623 WHERE answer.answer_id IN (" . $in_ids_string . ')
624 AND 1 = %d;
625 ',
626 1
627 )
628 );
629 //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
630
631 return $answer;
632 }
633
634 /**
635 * Get quiz attempt timing
636 *
637 * @since 1.0.0
638 *
639 * @param mixed $attempt_data attempt data.
640 * @return array
641 */
642 public static function get_quiz_attempt_timing( $attempt_data ) {
643 $attempt_duration = '';
644 $attempt_duration_taken = '';
645 $attempt_info = @unserialize( $attempt_data->attempt_info );
646 if ( is_array( $attempt_info ) ) {
647 // Allowed duration.
648 if ( isset( $attempt_info['time_limit'] ) ) {
649 //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
650 $time_type = __( ucwords( tutor_utils()->array_get( 'time_limit.time_type', $attempt_info, 'minutes' ) ), 'tutor' );
651 $time_value = tutor_utils()->array_get( 'time_limit.time_value', $attempt_info, 0 );
652 $attempt_duration = $time_value . ' ' . $time_type;
653 }
654
655 // Taken duration.
656 $seconds = strtotime( $attempt_data->attempt_ended_at ) - strtotime( $attempt_data->attempt_started_at );
657 $attempt_duration_taken = tutor_utils()->seconds_to_time( $seconds );
658 }
659
660 return compact( 'attempt_duration', 'attempt_duration_taken' );
661 }
662
663 /**
664 * Check student is passed in a quiz or not.
665 * Quiz retry mode: student required at least one quiz passed in attempts
666 *
667 * @since 2.1.0
668 *
669 * @param int $quiz_id quiz ID.
670 * @param int $user_id user ID.
671 *
672 * @return boolean
673 */
674 public static function is_quiz_passed( $quiz_id, $user_id = 0 ) {
675 global $wpdb;
676
677 $user_id = tutor_utils()->get_user_id( $user_id );
678 $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 ) );
679 $required_percentage = tutor_utils()->get_quiz_option( $quiz_id, 'passing_grade', 0 );
680
681 foreach ( $attempts as $attempt ) {
682 $earned_percentage = $attempt->earned_marks > 0 ? ( ( $attempt->earned_marks * 100 ) / $attempt->total_marks ) : 0;
683 if ( $earned_percentage >= $required_percentage ) {
684 return true;
685 }
686 }
687
688 return false;
689 }
690
691 /**
692 * Get all question type for a quiz
693 *
694 * @since 2.1.0
695 *
696 * @param integer $quiz_id quiz ID.
697 *
698 * @return array
699 */
700 public static function get_quiz_question_types( int $quiz_id ) {
701 global $wpdb;
702 $types = $wpdb->get_col(
703 $wpdb->prepare( "SELECT DISTINCT question_type FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id=%d", $quiz_id )
704 );
705
706 return $types;
707 }
708
709 /**
710 * Check a quiz attempt need manual review or not
711 *
712 * @since 2.1.0
713 *
714 * @param int $quiz_id quiz ID.
715 *
716 * @return boolean
717 */
718 public static function is_manual_review_required( $quiz_id ) {
719 $required = false;
720 $review_question_types = array( 'open_ended', 'short_answer' );
721 $question_types = self::get_quiz_question_types( $quiz_id );
722
723 foreach ( $review_question_types as $type ) {
724 if ( in_array( $type, $question_types, true ) ) {
725 $required = true;
726 break;
727 }
728 }
729
730 return $required;
731 }
732
733 /**
734 * Get last or first quiz attempt
735 *
736 * @since 2.1.0
737 * @since 2.1.3 user_id param added.
738 *
739 * @param integer $quiz_id quiz id to get attempt of.
740 * @param integer $user_id user ID who attempt the quiz.
741 * @param string $order ASC or DESC, default is DESC
742 * pass ASC to get first attempt.
743 *
744 * @return mixed object on success, null on failure
745 */
746 public function get_first_or_last_attempt( int $quiz_id, int $user_id = 0, string $order = 'DESC' ) {
747 $attempt = QueryHelper::get_row(
748 $this->get_table(),
749 array(
750 'quiz_id' => $quiz_id,
751 'user_id' => tutor_utils()->get_user_id( $user_id ),
752 ),
753 'attempt_id',
754 $order
755 );
756 return $attempt;
757 }
758
759 /**
760 * Get total number of quizzes by course id
761 *
762 * @since 2.2.0
763 *
764 * @param int|array $course_id Course id or array of course ids.
765 *
766 * @return int
767 */
768 public static function get_quiz_count_by_course( $course_id ) {
769 global $wpdb;
770
771 $and_clause = is_array( $course_id ) && count( $course_id ) ? ' AND post_parent IN (' . QueryHelper::prepare_in_clause( $course_id ) . ')' : "AND post_parent = $course_id";
772
773 $count = $wpdb->get_var(
774 "SELECT
775 COUNT(ID)
776 FROM {$wpdb->posts}
777 WHERE post_parent IN
778 (SELECT
779 ID
780 FROM {$wpdb->posts}
781 WHERE post_type='topics'
782 {$and_clause}
783 AND post_status = 'publish'
784 )
785 AND post_type ='tutor_quiz'
786 AND post_status = 'publish'"
787 );
788 return $count ? $count : 0;
789 }
790 }
791