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