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