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