PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.6.3
Tutor LMS – eLearning and online course solution v3.6.3
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
BillingModel.php 1 year ago CartModel.php 1 year ago CouponModel.php 1 year ago CourseModel.php 1 year ago LessonModel.php 1 year ago OrderActivitiesModel.php 1 year ago OrderMetaModel.php 1 year ago OrderModel.php 1 year ago QuizModel.php 1 year ago UserModel.php 1 year ago WithdrawModel.php 1 year ago
QuizModel.php
1145 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 use TUTOR\Quiz;
16
17 /**
18 * Class QuizModel
19 *
20 * @since 2.0.10
21 */
22 class QuizModel {
23
24 const ATTEMPT_STARTED = 'attempt_started';
25 const ATTEMPT_ENDED = 'attempt_ended';
26 const REVIEW_REQUIRED = 'review_required';
27
28 /**
29 * Get quiz table name
30 *
31 * @since 2.1.0
32 *
33 * @return string
34 */
35 public function get_table(): string {
36 global $wpdb;
37 return $wpdb->prefix . 'tutor_quiz_attempts';
38 }
39
40 /**
41 * Get total number of quiz
42 *
43 * @since 2.0.2
44 *
45 * @return int
46 */
47 public static function get_total_quiz() {
48 global $wpdb;
49
50 $sql = "SELECT COUNT(DISTINCT quiz.ID)
51 FROM {$wpdb->posts} quiz
52 INNER JOIN {$wpdb->posts} topic ON quiz.post_parent=topic.ID
53 INNER JOIN {$wpdb->posts} course ON topic.post_parent=course.ID
54 WHERE course.post_type=%s
55 AND course.post_status = 'publish'
56 AND quiz.post_type='tutor_quiz'";
57
58 //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
59 return $wpdb->get_var( $wpdb->prepare( $sql, tutor()->course_post_type ) );
60 }
61
62 /**
63 * Get Attempt row by grade method settings
64 *
65 * @since 1.4.2
66 *
67 * @param int $quiz_id quiz id.
68 * @param int $user_id user id.
69 *
70 * @return array|bool|null|object
71 */
72 public function get_quiz_attempt( $quiz_id = 0, $user_id = 0 ) {
73 global $wpdb;
74
75 $quiz_id = tutils()->get_post_id( $quiz_id );
76 $user_id = tutils()->get_user_id( $user_id );
77
78 $attempt = false;
79
80 $quiz_grade_method = get_tutor_option( 'quiz_grade_method', 'highest_grade' );
81 $from_string = "FROM {$wpdb->tutor_quiz_attempts} WHERE quiz_id = %d AND user_id = %d AND attempt_status != 'attempt_started' ";
82
83 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
84 if ( 'highest_grade' === $quiz_grade_method ) {
85 $attempt = $wpdb->get_row( $wpdb->prepare( "SELECT * {$from_string} ORDER BY earned_marks DESC LIMIT 1; ", $quiz_id, $user_id ) );
86 } elseif ( 'average_grade' === $quiz_grade_method ) {
87
88 $attempt = $wpdb->get_row(
89 $wpdb->prepare(
90 "SELECT {$wpdb->tutor_quiz_attempts}.*,
91 COUNT(attempt_id) AS attempt_count,
92 AVG(total_marks) AS total_marks,
93 AVG(earned_marks) AS earned_marks {$from_string}
94 ",
95 $quiz_id,
96 $user_id
97 )
98 );
99 } elseif ( 'first_attempt' === $quiz_grade_method ) {
100
101 $attempt = $wpdb->get_row( $wpdb->prepare( "SELECT * {$from_string} ORDER BY attempt_id ASC LIMIT 1; ", $quiz_id, $user_id ) );
102 } elseif ( 'last_attempt' === $quiz_grade_method ) {
103
104 $attempt = $wpdb->get_row( $wpdb->prepare( "SELECT * {$from_string} ORDER BY attempt_id DESC LIMIT 1; ", $quiz_id, $user_id ) );
105 }
106 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
107
108 return $attempt;
109 }
110
111 /**
112 * Get all of the attempts by an user of a quiz
113 *
114 * @since 1.0.0
115 *
116 * @param int $quiz_id quiz ID.
117 * @param int $user_id user ID.
118 *
119 * @return array|bool|null|object
120 */
121 public function quiz_attempts( $quiz_id = 0, $user_id = 0 ) {
122 global $wpdb;
123
124 $quiz_id = tutor_utils()->get_post_id( $quiz_id );
125 $user_id = tutor_utils()->get_user_id( $user_id );
126
127 $cache_key = "tutor_quiz_attempts_for_{$user_id}_{$quiz_id}";
128 $attempts = TutorCache::get( $cache_key );
129
130 if ( false === $attempts ) {
131 $attempts = $wpdb->get_results(
132 $wpdb->prepare(
133 "SELECT *
134 FROM {$wpdb->prefix}tutor_quiz_attempts
135 WHERE quiz_id = %d
136 AND user_id = %d
137 ORDER BY attempt_id DESC
138 ",
139 $quiz_id,
140 $user_id
141 )
142 );
143 TutorCache::set( $cache_key, $attempts );
144 }
145
146 if ( is_array( $attempts ) && count( $attempts ) ) {
147 return $attempts;
148 }
149
150 return false;
151 }
152
153 /**
154 * Get Quiz question by question id
155 *
156 * @since 1.0.0
157 *
158 * @param int $question_id question ID.
159 *
160 * @return array|bool|object|void|null
161 */
162 public static function get_quiz_question_by_id( $question_id = 0 ) {
163 global $wpdb;
164
165 if ( $question_id ) {
166 $question = $wpdb->get_row(
167 $wpdb->prepare(
168 "SELECT *
169 FROM {$wpdb->prefix}tutor_quiz_questions
170 WHERE question_id = %d
171 LIMIT 0, 1;
172 ",
173 $question_id
174 )
175 );
176
177 return $question;
178 }
179
180 return false;
181 }
182
183 /**
184 * Get all ended attempts by an user of a quiz
185 *
186 * @since 1.4.1
187 *
188 * @param int $quiz_id quiz ID.
189 * @param int $user_id user ID.
190 *
191 * @return array|bool|null|object
192 */
193 public function quiz_ended_attempts( $quiz_id = 0, $user_id = 0 ) {
194 global $wpdb;
195
196 $quiz_id = tutor_utils()->get_post_id( $quiz_id );
197 $user_id = tutor_utils()->get_user_id( $user_id );
198
199 $attempts = $wpdb->get_results(
200 $wpdb->prepare(
201 "SELECT *
202 FROM {$wpdb->prefix}tutor_quiz_attempts
203 WHERE quiz_id = %d
204 AND user_id = %d
205 AND attempt_status != %s
206 ",
207 $quiz_id,
208 $user_id,
209 'attempt_started'
210 )
211 );
212
213 if ( is_array( $attempts ) && count( $attempts ) ) {
214 return $attempts;
215 }
216
217 return false;
218 }
219
220 /**
221 * Get the next question order ID
222 *
223 * @since 1.0.0
224 *
225 * @param integer $quiz_id quiz ID.
226 *
227 * @return int
228 */
229 public static function quiz_next_question_order_id( $quiz_id ) {
230 global $wpdb;
231
232 $last_order = (int) $wpdb->get_var(
233 $wpdb->prepare(
234 "SELECT MAX(question_order)
235 FROM {$wpdb->prefix}tutor_quiz_questions
236 WHERE quiz_id = %d ;
237 ",
238 $quiz_id
239 )
240 );
241
242 return $last_order + 1;
243 }
244
245 /**
246 * Get next quiz question ID
247 *
248 * @since 1.0.0
249 *
250 * @return int
251 */
252 public static function quiz_next_question_id() {
253 global $wpdb;
254
255 $last_order = (int) $wpdb->get_var( "SELECT MAX(question_id) FROM {$wpdb->prefix}tutor_quiz_questions;" );
256 return $last_order + 1;
257 }
258
259 /**
260 * Total number of quiz attempts
261 *
262 * @since 1.0.0
263 *
264 * @param string $search_term search term.
265 * @param integer $course_id course ID.
266 * @param string $tab tab.
267 * @param string $date_filter date filter.
268 *
269 * @return int
270 */
271 public static function get_total_quiz_attempts( $search_term = '', int $course_id = 0, string $tab = '', $date_filter = '' ) {
272 global $wpdb;
273
274 if ( '' !== $search_term ) {
275 $search_term = '%' . $wpdb->esc_like( $search_term ) . '%';
276 }
277
278 // Set query based on action tab.
279 $pass_mark = "((( SUBSTRING_INDEX(
280 SUBSTRING_INDEX(
281 attempt_info,
282 CONCAT(
283 '\"passing_grade\";s:',
284 SUBSTRING_INDEX(SUBSTRING_INDEX(attempt_info, '\"passing_grade\";s:', -1), ':\"', 1),
285 ':\"'
286 ),
287 -1
288 ),
289 '\"',
290 1
291 ))/100) * quiz_attempts.total_marks)";
292 $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)";
293
294 $tab_join = '';
295 $tab_clause = '';
296 if ( '' !== $tab ) {
297 $tab_join = "INNER JOIN {$wpdb->prefix}tutor_quiz_attempt_answers AS ans ON quiz_attempts.attempt_id = ans.quiz_attempt_id";
298 }
299 switch ( $tab ) {
300 case 'pass':
301 // Just check if the earned mark is greater than pass mark.
302 // It doesn't matter if there is any pending or failed question.
303 $tab_clause = " AND quiz_attempts.earned_marks >= {$pass_mark} ";
304 break;
305
306 case 'fail':
307 // Check if earned marks is less than pass mark and there is no pending question.
308 $tab_clause = " AND quiz_attempts.earned_marks < {$pass_mark} AND {$pending_count} < 1 ";
309 break;
310 case 'pending':
311 $tab_clause = " AND {$pending_count} > 0 ";
312 break;
313 }
314
315 $course_join = '';
316 $course_clause = '';
317 if ( $course_id || '' !== $search_term ) {
318 $course_join = "INNER JOIN {$wpdb->posts} AS course ON course.ID = quiz_attempts.course_id";
319 }
320 if ( $course_id ) {
321 $course_clause = " AND quiz_attempts.course_id = $course_id";
322 }
323
324 $user_join = '';
325 $user_clause = '';
326 $search_term1 = sanitize_text_field( $search_term );
327 $search_term2 = sanitize_text_field( $search_term );
328 $search_term3 = sanitize_text_field( $search_term );
329 if ( '' !== $search_term ) {
330 $user_join = "INNER JOIN {$wpdb->users}
331 ON quiz_attempts.user_id = {$wpdb->users}.ID";
332
333 $user_clause = "AND ( user_email LIKE '%$search_term1%' OR display_name LIKE '%$search_term2%' OR course.post_title LIKE '%$search_term3%' )";
334 }
335
336 if ( '' !== $date_filter ) {
337 $date_filter = '' != $date_filter ? tutor_get_formated_date( 'Y-m-d', $date_filter ) : '';
338 $date_filter = '' != $date_filter ? " AND DATE(quiz_attempts.attempt_started_at) = '$date_filter' " : '';
339 }
340
341 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
342 $count = $wpdb->get_var(
343 $wpdb->prepare(
344 "SELECT COUNT( DISTINCT attempt_id)
345 FROM {$wpdb->prefix}tutor_quiz_attempts quiz_attempts
346 INNER JOIN {$wpdb->posts} quiz
347 ON quiz_attempts.quiz_id = quiz.ID
348 {$user_join}
349 {$course_join}
350 {$tab_join}
351 WHERE attempt_status != %s
352 {$user_clause}
353 {$course_clause}
354 {$tab_clause}
355 {$date_filter}
356 ",
357 'attempt_started'
358 )
359 );
360
361 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
362
363 return (int) $count;
364 }
365
366 /**
367 * Get the all quiz attempts
368 *
369 * @since 1.0.0
370 * @since 1.9.5 sorting paramas added
371 *
372 * @param integer $start start.
373 * @param integer $limit limit.
374 * @param string $search_filter search filter.
375 * @param string $course_filter course filter.
376 * @param string $date_filter date filter.
377 * @param string $order_filter order filter.
378 * @param mixed $result_state result state.
379 * @param boolean $count_only count only or not.
380 * @param boolean $instructor_id_check need instructor id check or not.
381 *
382 * @return mixed
383 */
384 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 ) {
385 global $wpdb;
386
387 $start = sanitize_text_field( $start );
388 $limit = sanitize_text_field( $limit );
389 $search_filter = sanitize_text_field( $search_filter );
390 $course_filter = sanitize_text_field( $course_filter );
391 $date_filter = sanitize_text_field( $date_filter );
392 $order_filter = sanitize_sql_orderby( $order_filter );
393
394 $search_term_raw = $search_filter;
395 $search_filter = '%' . $wpdb->esc_like( $search_filter ) . '%';
396
397 // Filter by course.
398 if ( '' != $course_filter ) {
399 ! is_array( $course_filter ) ? $course_filter = array( $course_filter ) : 0;
400 $course_ids = implode( ',', array_map( 'intval', $course_filter ) );
401 $course_filter = " AND quiz_attempts.course_id IN ($course_ids) ";
402 }
403
404 // Filter by date.
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
408 $result_clause = '';
409 $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';
410 $limit_offset = $count_only ? '' : ' LIMIT ' . $limit . ' OFFSET ' . $start;
411
412 $pass_mark = "((( SUBSTRING_INDEX(
413 SUBSTRING_INDEX(
414 attempt_info,
415 CONCAT(
416 '\"passing_grade\";s:',
417 SUBSTRING_INDEX(SUBSTRING_INDEX(attempt_info, '\"passing_grade\";s:', -1), ':\"', 1),
418 ':\"'
419 ),
420 -1
421 ),
422 '\"',
423 1
424 ))/100) * quiz_attempts.total_marks)";
425 $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)";
426
427 // Get attempts by instructor ID.
428 $instructor_clause = '';
429 $instructor_join = '';
430 if ( $instructor_id_check ) {
431 $current_user_id = get_current_user_id();
432 $instructor_id = tutor_utils()->has_user_role( 'administrator', $current_user_id ) ? null : $current_user_id;
433
434 if ( $instructor_id ) {
435 // $instructor_clause = " AND (instructor_meta.meta_key='_tutor_instructor_course_id' AND instructor_meta.user_id=$instructor_id)";
436 $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) ";
437 }
438 }
439
440 // Switc hthrough result state and assign meta clause.
441 switch ( $result_state ) {
442 case 'pass':
443 // Just check if the earned mark is greater than pass mark.
444 // It doesn't matter if there is any pending or failed question.
445 $result_clause = " AND quiz_attempts.earned_marks>={$pass_mark} ";
446 break;
447
448 case 'fail':
449 // Check if earned marks is less than pass mark and there is no pending question.
450 $result_clause = " AND quiz_attempts.earned_marks<{$pass_mark} AND {$pending_count} < 1 ";
451 break;
452
453 case 'pending':
454 $result_clause = " AND {$pending_count}>0 ";
455 break;
456 }
457
458 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
459 $query = $wpdb->prepare(
460 "SELECT {$select_columns}
461 FROM {$wpdb->prefix}tutor_quiz_attempts quiz_attempts
462 INNER JOIN {$wpdb->posts} quiz ON quiz_attempts.quiz_id = quiz.ID
463 INNER JOIN {$wpdb->users} AS users ON quiz_attempts.user_id = users.ID
464 INNER JOIN {$wpdb->posts} AS course ON course.ID = quiz_attempts.course_id
465 -- INNER JOIN {$wpdb->prefix}tutor_quiz_attempt_answers AS ans ON quiz_attempts.attempt_id = ans.quiz_attempt_id
466 {$instructor_clause}
467 WHERE quiz_attempts.attempt_ended_at IS NOT NULL
468 AND (
469 users.user_email = %s
470 OR users.display_name LIKE %s
471 OR quiz.post_title LIKE %s
472 OR course.post_title LIKE %s
473 )
474 AND quiz_attempts.attempt_ended_at IS NOT NULL
475 {$result_clause}
476 {$course_filter}
477 {$date_filter}
478 ORDER BY quiz_attempts.attempt_ended_at {$order_filter} {$limit_offset}",
479 $search_term_raw,
480 $search_filter,
481 $search_filter,
482 $search_filter
483 );
484
485 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
486
487 //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
488 return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query );
489 }
490
491 /**
492 * Delete quizattempt for user
493 *
494 * @since 1.9.5
495 *
496 * @param mixed $attempt_ids attempt ids.
497 *
498 * @return void
499 */
500 public static function delete_quiz_attempt( $attempt_ids ) {
501 global $wpdb;
502
503 // Singlular to array.
504 ! is_array( $attempt_ids ) ? $attempt_ids = array( $attempt_ids ) : 0;
505
506 if ( count( $attempt_ids ) ) {
507 $attempt_ids = implode( ',', $attempt_ids );
508
509 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
510 // Deleting attempt (comment), child attempt and attempt meta (comment meta).
511 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempts WHERE attempt_id IN($attempt_ids)" );
512 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempt_answers WHERE quiz_attempt_id IN($attempt_ids)" );
513 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
514
515 do_action( 'tutor_quiz/attempt_deleted', $attempt_ids );
516 }
517 }
518
519 /**
520 * Sorting params added on quiz attempt
521 *
522 * @since 1.9.5
523 *
524 * @param integer $start start.
525 * @param integer $limit limit.
526 * @param array $course_ids course ids.
527 * @param string $search_filter search filter.
528 * @param string $course_filter course filter.
529 * @param string $date_filter date filter.
530 * @param string $order_filter order filter.
531 * @param mixed $user_id user id.
532 * @param boolean $count_only is only count or not.
533 * @param boolean $all_attempt need all atempt or not.
534 *
535 * @return mixed
536 */
537 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 ) {
538 global $wpdb;
539 $search_filter = sanitize_text_field( $search_filter );
540 $course_filter = (int) sanitize_text_field( $course_filter );
541 $date_filter = sanitize_text_field( $date_filter );
542 $order_filter = sanitize_sql_orderby( $order_filter );
543
544 $course_ids = array_map(
545 function ( $id ) {
546 return "'" . esc_sql( $id ) . "'";
547 },
548 $course_ids
549 );
550
551 $course_ids_in = count( $course_ids ) ? ' AND quiz_attempts.course_id IN (' . implode( ', ', $course_ids ) . ') ' : '';
552
553 $search_filter = $search_filter ? '%' . $wpdb->esc_like( $search_filter ) . '%' : '';
554 $search_term_raw = $search_filter;
555 $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} )" : '';
556
557 $course_filter = 0 !== $course_filter ? " AND quiz_attempts.course_id = $course_filter " : '';
558 $date_filter = '' != $date_filter ? tutor_get_formated_date( 'Y-m-d', $date_filter ) : '';
559 $date_filter = '' != $date_filter ? " AND DATE(quiz_attempts.attempt_started_at) = '$date_filter' " : '';
560 $user_filter = $user_id ? ' AND user_id=\'' . esc_sql( $user_id ) . '\' ' : '';
561
562 $limit_offset = $count_only ? '' : " LIMIT {$start}, {$limit} ";
563 $select_col = $count_only ? ' COUNT(DISTINCT quiz_attempts.attempt_id) ' : ' quiz_attempts.*, users.*, quiz.* ';
564
565 $attempt_type = $all_attempt ? '' : " AND quiz_attempts.attempt_status != 'attempt_started' ";
566
567 $query = "SELECT {$select_col}
568 FROM {$wpdb->prefix}tutor_quiz_attempts AS quiz_attempts
569 INNER JOIN {$wpdb->posts} AS quiz
570 ON quiz_attempts.quiz_id = quiz.ID
571 INNER JOIN {$wpdb->users} AS users
572 ON quiz_attempts.user_id = users.ID
573 INNER JOIN {$wpdb->posts} AS course
574 ON course.ID = quiz_attempts.course_id
575 WHERE 1=1
576 {$attempt_type}
577 {$course_ids_in}
578 {$search_filter}
579 {$course_filter}
580 {$date_filter}
581 {$user_filter}
582 ORDER BY quiz_attempts.attempt_id {$order_filter} {$limit_offset};";
583
584 //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
585 return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query );
586 }
587
588 /**
589 * Get answers list by quiz question
590 *
591 * @since 1.0.0
592 *
593 * @param int $question_id question ID.
594 * @param bool $rand rand.
595 *
596 * @return array|bool|null|object
597 */
598 public static function get_answers_by_quiz_question( $question_id, $rand = false ) {
599 global $wpdb;
600
601 $question = $wpdb->get_row(
602 $wpdb->prepare(
603 "SELECT *
604 FROM {$wpdb->prefix}tutor_quiz_questions
605 WHERE question_id = %d;
606 ",
607 $question_id
608 )
609 );
610
611 if ( ! $question ) {
612 return false;
613 }
614
615 $order = ' answer_order ASC ';
616
617 if ( $rand ) {
618 $order = ' RAND() ';
619 }
620
621 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
622 $answers = $wpdb->get_results(
623 $wpdb->prepare(
624 "SELECT *
625 FROM {$wpdb->prefix}tutor_quiz_question_answers
626 WHERE belongs_question_id = %d
627 AND belongs_question_type = %s
628 ORDER BY {$order}
629 ",
630 $question_id,
631 $question->question_type
632 )
633 );
634 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
635
636 return $answers;
637 }
638
639 /**
640 * Get quiz answers by attempt id
641 *
642 * @since 1.0.0
643 *
644 * @param mixed $attempt_id attempt ID.
645 * @param bool $add_index need index or not.
646 *
647 * @return array|null|object
648 */
649 public static function get_quiz_answers_by_attempt_id( $attempt_id, $add_index = false ) {
650 global $wpdb;
651
652 $ids = is_array( $attempt_id ) ? $attempt_id : array( $attempt_id );
653 $ids_in = implode( ',', $ids );
654
655 if ( empty( $ids_in ) ) {
656 // Prevent empty.
657 return array();
658 }
659
660 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
661 $results = $wpdb->get_results(
662 "SELECT answers.*,
663 question.*
664 FROM {$wpdb->prefix}tutor_quiz_attempt_answers answers
665 LEFT JOIN {$wpdb->prefix}tutor_quiz_questions question
666 ON answers.question_id = question.question_id
667 WHERE answers.quiz_attempt_id IN ({$ids_in})
668 ORDER BY attempt_answer_id ASC;"
669 );
670 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
671
672 if ( $add_index ) {
673 $new_array = array();
674
675 foreach ( $results as $result ) {
676 ! isset( $new_array[ $result->quiz_attempt_id ] ) ? $new_array[ $result->quiz_attempt_id ] = array() : 0;
677 $new_array[ $result->quiz_attempt_id ][] = $result;
678 }
679
680 return $new_array;
681 }
682
683 return $results;
684 }
685
686 /**
687 * Get single answer by answer_id
688 *
689 * @since 1.0.0
690 *
691 * @param array|init $answer_id answer id.
692 *
693 * @return array|null|object
694 */
695 public static function get_answer_by_id( $answer_id ) {
696 global $wpdb;
697
698 ! is_array( $answer_id ) ? $answer_id = array( $answer_id ) : 0;
699
700 $answer_id = array_map(
701 function ( $id ) {
702 return "'" . esc_sql( $id ) . "'";
703 },
704 $answer_id
705 );
706
707 $in_ids_string = implode( ', ', $answer_id );
708
709 //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
710 $answer = $wpdb->get_results(
711 $wpdb->prepare(
712 "SELECT answer.*,
713 question.question_title,
714 question.question_type
715 FROM {$wpdb->prefix}tutor_quiz_question_answers answer
716 LEFT JOIN {$wpdb->prefix}tutor_quiz_questions question
717 ON answer.belongs_question_id = question.question_id
718 WHERE answer.answer_id IN (" . $in_ids_string . ')
719 AND 1 = %d;
720 ',
721 1
722 )
723 );
724 //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
725
726 return $answer;
727 }
728
729 /**
730 * Get quiz attempt timing
731 *
732 * @since 1.0.0
733 *
734 * @param mixed $attempt_data attempt data.
735 * @return array
736 */
737 public static function get_quiz_attempt_timing( $attempt_data ) {
738 $attempt_duration = '';
739 $attempt_duration_taken = '';
740 $attempt_info = @unserialize( $attempt_data->attempt_info );
741 if ( is_array( $attempt_info ) ) {
742 // Allowed duration.
743 if ( isset( $attempt_info['time_limit'] ) ) {
744 //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
745 $time_type = __( ucwords( tutor_utils()->array_get( 'time_limit.time_type', $attempt_info, 'minutes' ) ), 'tutor' );
746 $time_value = tutor_utils()->array_get( 'time_limit.time_value', $attempt_info, 0 );
747 $attempt_duration = $time_value . ' ' . $time_type;
748 }
749
750 // Taken duration.
751 $seconds = strtotime( $attempt_data->attempt_ended_at ) - strtotime( $attempt_data->attempt_started_at );
752 $attempt_duration_taken = tutor_utils()->seconds_to_time( $seconds );
753 }
754
755 return compact( 'attempt_duration', 'attempt_duration_taken' );
756 }
757
758 /**
759 * Check student is passed in a quiz or not.
760 * Quiz retry mode: student required at least one quiz passed in attempts
761 *
762 * @since 2.1.0
763 *
764 * @param int $quiz_id quiz ID.
765 * @param int $user_id user ID.
766 *
767 * @return boolean
768 */
769 public static function is_quiz_passed( $quiz_id, $user_id = 0 ) {
770 global $wpdb;
771
772 $user_id = tutor_utils()->get_user_id( $user_id );
773 $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 ) );
774 $required_percentage = tutor_utils()->get_quiz_option( $quiz_id, 'passing_grade', 0 );
775
776 foreach ( $attempts as $attempt ) {
777 $earned_percentage = $attempt->earned_marks > 0 ? ( ( $attempt->earned_marks * 100 ) / $attempt->total_marks ) : 0;
778 if ( $earned_percentage >= $required_percentage ) {
779 return true;
780 }
781 }
782
783 return false;
784 }
785
786 /**
787 * Get all question type for a quiz
788 *
789 * @since 2.1.0
790 *
791 * @param integer $quiz_id quiz ID.
792 *
793 * @return array
794 */
795 public static function get_quiz_question_types( int $quiz_id ) {
796 global $wpdb;
797 $types = $wpdb->get_col(
798 $wpdb->prepare( "SELECT DISTINCT question_type FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id=%d", $quiz_id )
799 );
800
801 return $types;
802 }
803
804 /**
805 * Check a quiz attempt need manual review or not
806 *
807 * @since 2.1.0
808 *
809 * @param int $quiz_id quiz ID.
810 *
811 * @return boolean
812 */
813 public static function is_manual_review_required( $quiz_id ) {
814 $required = false;
815 $review_question_types = array( 'open_ended', 'short_answer' );
816 $question_types = self::get_quiz_question_types( $quiz_id );
817
818 foreach ( $review_question_types as $type ) {
819 if ( in_array( $type, $question_types, true ) ) {
820 $required = true;
821 break;
822 }
823 }
824
825 return $required;
826 }
827
828 /**
829 * Get last or first quiz attempt
830 *
831 * @since 2.1.0
832 * @since 2.1.3 user_id param added.
833 *
834 * @param integer $quiz_id quiz id to get attempt of.
835 * @param integer $user_id user ID who attempt the quiz.
836 * @param string $order ASC or DESC, default is DESC
837 * pass ASC to get first attempt.
838 *
839 * @return mixed object on success, null on failure
840 */
841 public function get_first_or_last_attempt( int $quiz_id, int $user_id = 0, string $order = 'DESC' ) {
842 $attempt = QueryHelper::get_row(
843 $this->get_table(),
844 array(
845 'quiz_id' => $quiz_id,
846 'user_id' => tutor_utils()->get_user_id( $user_id ),
847 ),
848 'attempt_id',
849 $order
850 );
851 return $attempt;
852 }
853
854 /**
855 * Get total number of quizzes by course id
856 *
857 * @since 2.2.0
858 *
859 * @param int|array $course_id Course id or array of course ids.
860 *
861 * @return int
862 */
863 public static function get_quiz_count_by_course( $course_id ) {
864 global $wpdb;
865
866 $and_clause = is_array( $course_id ) && count( $course_id ) ? ' AND post_parent IN (' . QueryHelper::prepare_in_clause( $course_id ) . ')' : "AND post_parent = $course_id";
867
868 //phpcs:disable
869 $count = $wpdb->get_var(
870 $wpdb->prepare(
871 "SELECT
872 COUNT(ID)
873 FROM {$wpdb->posts}
874 WHERE post_parent IN
875 (SELECT
876 ID
877 FROM {$wpdb->posts}
878 WHERE post_type = %s
879 {$and_clause}
880 AND post_status = %s
881 )
882 AND post_type = %s
883 AND post_status = %s",
884 'topics',
885 'publish',
886 'tutor_quiz',
887 'publish'
888 )
889 );
890 //phpcs:enable
891 return $count ? $count : 0;
892 }
893
894 /**
895 * Get final quiz result depending on all attempts.
896 *
897 * @since 2.4.0
898 *
899 * @param int $quiz_id quiz id.
900 * @param int $user_id user id.
901 *
902 * @return string pass, fail, pending
903 */
904 public static function get_quiz_result( $quiz_id, $user_id = 0 ) {
905 global $wpdb;
906
907 $all_pending = true;
908 $result = 'pending';
909
910 $user_id = tutor_utils()->get_user_id( $user_id );
911 $attempt_list = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}tutor_quiz_attempts WHERE user_id=%d AND quiz_id=%d", $user_id, $quiz_id ) );
912
913 $total_pending_attempt = (int) $wpdb->get_var(
914 $wpdb->prepare(
915 "SELECT COUNT(quiz_attempt_id) total_pending_attempt
916 FROM (
917 SELECT qa.quiz_attempt_id, COUNT(*) AS total_pending
918 FROM {$wpdb->prefix}tutor_quiz_attempt_answers qa
919 WHERE qa.quiz_id = %d AND qa.user_id=%d AND qa.is_correct IS NULL
920 GROUP BY qa.quiz_attempt_id
921 ) a
922 ",
923 $quiz_id,
924 $user_id
925 )
926 );
927
928 if ( count( $attempt_list ) !== $total_pending_attempt ) {
929 $all_pending = false;
930 }
931
932 if ( false === $all_pending ) {
933 $required_percentage = tutor_utils()->get_quiz_option( $quiz_id, 'passing_grade', 0 );
934 foreach ( $attempt_list as $attempt ) {
935 $earned_percentage = $attempt->earned_marks > 0 ? ( ( $attempt->earned_marks * 100 ) / $attempt->total_marks ) : 0;
936 if ( $earned_percentage >= $required_percentage ) {
937 // If at least one attempt passed then quiz passed.
938 $result = 'pass';
939 break;
940 } else {
941 $result = 'fail';
942 }
943 }
944 }
945
946 return $result;
947 }
948
949 /**
950 * Get quiz attempt details
951 *
952 * @since 2.6.1
953 *
954 * @param integer $attempt_id attempt id.
955 *
956 * @return mixed
957 */
958 public static function quiz_attempt_details( int $attempt_id ) {
959 global $wpdb;
960
961 $table_quiz_attempt_answers = $wpdb->prefix . 'tutor_quiz_attempt_answers';
962 $table_quiz_questions = $wpdb->prefix . 'tutor_quiz_questions';
963 $table_quiz_attempts = $wpdb->prefix . 'tutor_quiz_attempts';
964 $table_quiz_question_answers = $wpdb->prefix . 'tutor_quiz_question_answers';
965
966 $query = "SELECT
967 ques.question_id,
968 ques.question_title,
969 ques.question_type,
970 (
971 SELECT
972 GROUP_CONCAT(answer_title)
973 FROM
974 {$table_quiz_question_answers}
975 WHERE
976 belongs_question_id = ques.question_id
977 AND is_correct = 1
978 ) AS correct_answers,
979
980 (
981
982 SELECT
983
984 CASE
985 WHEN CHAR_LENGTH(att_ans.given_answer) = 1 AND att_ans.given_answer REGEXP '^[0-9]$' THEN
986 -- If given_answer is a single digit integer
987 (
988 SELECT
989 answer_title
990 FROM
991 {$table_quiz_question_answers}
992 WHERE
993 answer_id = CAST(att_ans.given_answer AS UNSIGNED)
994 )
995 WHEN CHAR_LENGTH(att_ans.given_answer) > 1 AND SUBSTRING(att_ans.given_answer, 1, 2) = 'a:' THEN
996 -- If given_answer is serialized array
997 (
998 att_ans.given_answer
999 )
1000 ELSE
1001 -- If given_answer is a serialized string
1002 att_ans.given_answer
1003 END
1004 ) AS given_answer,
1005 att_ans.question_mark,
1006 att_ans.achieved_mark,
1007 att_ans.is_correct,
1008 (
1009 SELECT
1010 attempt_info
1011 FROM {$table_quiz_attempts}
1012 WHERE attempt_id = {$attempt_id}
1013 LIMIT 1
1014 ) AS attempt_info
1015 FROM
1016 {$table_quiz_attempt_answers} AS att_ans
1017 JOIN {$table_quiz_questions} AS ques ON ques.question_id = att_ans.question_id
1018 JOIN {$table_quiz_question_answers} AS ans ON ans.answer_id = att_ans.attempt_answer_id
1019 WHERE
1020 quiz_attempt_id = %d
1021 LIMIT
1022 50
1023 ";
1024
1025 //phpcs:ignore
1026 $result = $wpdb->get_results( $wpdb->prepare( $query, $attempt_id ) );
1027
1028 // If array and count result then loop with each result and prepare given answer.
1029 if ( is_array( $result ) && count( $result ) ) {
1030 foreach ( $result as $key => $value ) {
1031 // Check if given answer is a serialized string.
1032 if ( is_serialized( $value->given_answer ) ) {
1033 $given_answers = tutor_utils()->get_answer_by_id( maybe_unserialize( $value->given_answer ) );
1034 $result[ $key ]->given_answer = array_column( $given_answers, 'answer_title' );
1035 } elseif ( is_numeric( $value->given_answer ) ) {
1036 $given_answers = tutor_utils()->get_answer_by_id( maybe_unserialize( $value->given_answer ) );
1037 $result[ $key ]->given_answer = array_column( $given_answers, 'answer_title' );
1038 }
1039 }
1040 }
1041
1042 return $result;
1043 }
1044
1045 /**
1046 * Get a question record.
1047 *
1048 * @since 3.0.0
1049 *
1050 * @param int $question_id quiz question id.
1051 *
1052 * @return array|object|null|void
1053 */
1054 public static function get_question( $question_id ) {
1055 global $wpdb;
1056 return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}tutor_quiz_questions WHERE question_id = %d", $question_id ) );
1057 }
1058
1059 /**
1060 * Get all answer's of a quiz question.
1061 *
1062 * @since 3.0.0
1063 *
1064 * @param int $question_id question id.
1065 * @param string $question_type question type.
1066 *
1067 * @return array
1068 */
1069 public static function get_question_answers( $question_id, $question_type = null ) {
1070 global $wpdb;
1071
1072 $query = "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id = %d";
1073
1074 if ( $question_type ) {
1075 $query .= ' AND belongs_question_type = %s ORDER BY answer_order ASC';
1076 //phpcs:ignore
1077 $answers = $wpdb->get_results( $wpdb->prepare( $query, $question_id, $question_type ) );
1078 } else {
1079 $query .= ' ORDER BY answer_order ASC';
1080 //phpcs:ignore
1081 $answers = $wpdb->get_results( $wpdb->prepare( $query, $question_id ) );
1082 }
1083
1084 foreach ( $answers as $answer ) {
1085 $answer->answer_title = stripslashes( $answer->answer_title );
1086 if ( $answer->image_id ) {
1087 $answer->image_url = wp_get_attachment_url( $answer->image_id );
1088 }
1089 }
1090
1091 return $answers;
1092 }
1093
1094 /**
1095 * Get next answer order SL no
1096 *
1097 * @since 3.0.0
1098 *
1099 * @param int $question_id question id.
1100 * @param int $question_type question type.
1101 *
1102 * @return int
1103 */
1104 public static function get_next_answer_order( $question_id, $question_type ) {
1105 global $wpdb;
1106 $max_id = (int) $wpdb->get_var(
1107 $wpdb->prepare(
1108 "SELECT MAX(answer_order) FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id = %d AND belongs_question_type = %s",//phpcs:ignore
1109 $question_id,
1110 $question_type
1111 )
1112 );
1113
1114 return $max_id + 1;
1115 }
1116
1117 /**
1118 * Get quiz details by quiz id.
1119 *
1120 * @since 3.0.0
1121 *
1122 * @param int $quiz_id quiz id.
1123 *
1124 * @return object
1125 */
1126 public static function get_quiz_details( $quiz_id ) {
1127 $quiz = get_post( $quiz_id );
1128 $quiz->quiz_option = get_post_meta( $quiz_id, Quiz::META_QUIZ_OPTION, true );
1129 $quiz->questions = tutor_utils()->get_questions_by_quiz( $quiz_id );
1130
1131 if ( ! is_array( $quiz->questions ) ) {
1132 $quiz->questions = array();
1133 }
1134
1135 foreach ( $quiz->questions as $question ) {
1136 $question->question_answers = self::get_question_answers( $question->question_id, $question->question_type );
1137 if ( isset( $question->question_settings ) ) {
1138 $question->question_settings = maybe_unserialize( $question->question_settings );
1139 }
1140 }
1141
1142 return $quiz;
1143 }
1144 }
1145