PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.7.0
Tutor LMS – eLearning and online course solution v3.7.0
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
BaseModel.php 11 months ago BillingModel.php 1 year ago CartModel.php 1 year ago CouponModel.php 11 months ago CourseModel.php 11 months ago LessonModel.php 11 months ago OrderActivitiesModel.php 1 year ago OrderMetaModel.php 1 year ago OrderModel.php 11 months ago QuizModel.php 11 months ago UserModel.php 1 year ago WithdrawModel.php 1 year ago
QuizModel.php
1144 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 {$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
616 if ( $rand ) {
617 $order = ' RAND() ';
618 }
619
620 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
621 $answers = $wpdb->get_results(
622 $wpdb->prepare(
623 "SELECT *
624 FROM {$wpdb->prefix}tutor_quiz_question_answers
625 WHERE belongs_question_id = %d
626 AND belongs_question_type = %s
627 ORDER BY {$order}
628 ",
629 $question_id,
630 $question->question_type
631 )
632 );
633 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
634
635 return $answers;
636 }
637
638 /**
639 * Get quiz answers by attempt id
640 *
641 * @since 1.0.0
642 *
643 * @param mixed $attempt_id attempt ID.
644 * @param bool $add_index need index or not.
645 *
646 * @return array|null|object
647 */
648 public static function get_quiz_answers_by_attempt_id( $attempt_id, $add_index = false ) {
649 global $wpdb;
650
651 $ids = is_array( $attempt_id ) ? $attempt_id : array( $attempt_id );
652 $ids_in = implode( ',', $ids );
653
654 if ( empty( $ids_in ) ) {
655 // Prevent empty.
656 return array();
657 }
658
659 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
660 $results = $wpdb->get_results(
661 "SELECT answers.*,
662 question.*
663 FROM {$wpdb->prefix}tutor_quiz_attempt_answers answers
664 LEFT JOIN {$wpdb->prefix}tutor_quiz_questions question
665 ON answers.question_id = question.question_id
666 WHERE answers.quiz_attempt_id IN ({$ids_in})
667 ORDER BY attempt_answer_id ASC;"
668 );
669 //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
670
671 if ( $add_index ) {
672 $new_array = array();
673
674 foreach ( $results as $result ) {
675 ! isset( $new_array[ $result->quiz_attempt_id ] ) ? $new_array[ $result->quiz_attempt_id ] = array() : 0;
676 $new_array[ $result->quiz_attempt_id ][] = $result;
677 }
678
679 return $new_array;
680 }
681
682 return $results;
683 }
684
685 /**
686 * Get single answer by answer_id
687 *
688 * @since 1.0.0
689 *
690 * @param array|init $answer_id answer id.
691 *
692 * @return array|null|object
693 */
694 public static function get_answer_by_id( $answer_id ) {
695 global $wpdb;
696
697 ! is_array( $answer_id ) ? $answer_id = array( $answer_id ) : 0;
698
699 $answer_id = array_map(
700 function ( $id ) {
701 return "'" . esc_sql( $id ) . "'";
702 },
703 $answer_id
704 );
705
706 $in_ids_string = implode( ', ', $answer_id );
707
708 //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
709 $answer = $wpdb->get_results(
710 $wpdb->prepare(
711 "SELECT answer.*,
712 question.question_title,
713 question.question_type
714 FROM {$wpdb->prefix}tutor_quiz_question_answers answer
715 LEFT JOIN {$wpdb->prefix}tutor_quiz_questions question
716 ON answer.belongs_question_id = question.question_id
717 WHERE answer.answer_id IN (" . $in_ids_string . ')
718 AND 1 = %d;
719 ',
720 1
721 )
722 );
723 //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
724
725 return $answer;
726 }
727
728 /**
729 * Get quiz attempt timing
730 *
731 * @since 1.0.0
732 *
733 * @param mixed $attempt_data attempt data.
734 * @return array
735 */
736 public static function get_quiz_attempt_timing( $attempt_data ) {
737 $attempt_duration = '';
738 $attempt_duration_taken = '';
739 $attempt_info = @unserialize( $attempt_data->attempt_info );
740 if ( is_array( $attempt_info ) ) {
741 // Allowed duration.
742 if ( isset( $attempt_info['time_limit'] ) ) {
743 //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
744 $time_type = __( ucwords( tutor_utils()->array_get( 'time_limit.time_type', $attempt_info, 'minutes' ) ), 'tutor' );
745 $time_value = tutor_utils()->array_get( 'time_limit.time_value', $attempt_info, 0 );
746 $attempt_duration = $time_value . ' ' . $time_type;
747 }
748
749 // Taken duration.
750 $seconds = strtotime( $attempt_data->attempt_ended_at ) - strtotime( $attempt_data->attempt_started_at );
751 $attempt_duration_taken = tutor_utils()->seconds_to_time( $seconds );
752 }
753
754 return compact( 'attempt_duration', 'attempt_duration_taken' );
755 }
756
757 /**
758 * Check student is passed in a quiz or not.
759 * Quiz retry mode: student required at least one quiz passed in attempts
760 *
761 * @since 2.1.0
762 *
763 * @param int $quiz_id quiz ID.
764 * @param int $user_id user ID.
765 *
766 * @return boolean
767 */
768 public static function is_quiz_passed( $quiz_id, $user_id = 0 ) {
769 global $wpdb;
770
771 $user_id = tutor_utils()->get_user_id( $user_id );
772 $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 ) );
773 $required_percentage = tutor_utils()->get_quiz_option( $quiz_id, 'passing_grade', 0 );
774
775 foreach ( $attempts as $attempt ) {
776 $earned_percentage = $attempt->earned_marks > 0 ? ( ( $attempt->earned_marks * 100 ) / $attempt->total_marks ) : 0;
777 if ( $earned_percentage >= $required_percentage ) {
778 return true;
779 }
780 }
781
782 return false;
783 }
784
785 /**
786 * Get all question type for a quiz
787 *
788 * @since 2.1.0
789 *
790 * @param integer $quiz_id quiz ID.
791 *
792 * @return array
793 */
794 public static function get_quiz_question_types( int $quiz_id ) {
795 global $wpdb;
796 $types = $wpdb->get_col(
797 $wpdb->prepare( "SELECT DISTINCT question_type FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id=%d", $quiz_id )
798 );
799
800 return $types;
801 }
802
803 /**
804 * Check a quiz attempt need manual review or not
805 *
806 * @since 2.1.0
807 *
808 * @param int $quiz_id quiz ID.
809 *
810 * @return boolean
811 */
812 public static function is_manual_review_required( $quiz_id ) {
813 $required = false;
814 $review_question_types = array( 'open_ended', 'short_answer' );
815 $question_types = self::get_quiz_question_types( $quiz_id );
816
817 foreach ( $review_question_types as $type ) {
818 if ( in_array( $type, $question_types, true ) ) {
819 $required = true;
820 break;
821 }
822 }
823
824 return $required;
825 }
826
827 /**
828 * Get last or first quiz attempt
829 *
830 * @since 2.1.0
831 * @since 2.1.3 user_id param added.
832 *
833 * @param integer $quiz_id quiz id to get attempt of.
834 * @param integer $user_id user ID who attempt the quiz.
835 * @param string $order ASC or DESC, default is DESC
836 * pass ASC to get first attempt.
837 *
838 * @return mixed object on success, null on failure
839 */
840 public function get_first_or_last_attempt( int $quiz_id, int $user_id = 0, string $order = 'DESC' ) {
841 $attempt = QueryHelper::get_row(
842 $this->get_table(),
843 array(
844 'quiz_id' => $quiz_id,
845 'user_id' => tutor_utils()->get_user_id( $user_id ),
846 ),
847 'attempt_id',
848 $order
849 );
850 return $attempt;
851 }
852
853 /**
854 * Get total number of quizzes by course id
855 *
856 * @since 2.2.0
857 *
858 * @param int|array $course_id Course id or array of course ids.
859 *
860 * @return int
861 */
862 public static function get_quiz_count_by_course( $course_id ) {
863 global $wpdb;
864
865 $and_clause = is_array( $course_id ) && count( $course_id ) ? ' AND post_parent IN (' . QueryHelper::prepare_in_clause( $course_id ) . ')' : "AND post_parent = $course_id";
866
867 //phpcs:disable
868 $count = $wpdb->get_var(
869 $wpdb->prepare(
870 "SELECT
871 COUNT(ID)
872 FROM {$wpdb->posts}
873 WHERE post_parent IN
874 (SELECT
875 ID
876 FROM {$wpdb->posts}
877 WHERE post_type = %s
878 {$and_clause}
879 AND post_status = %s
880 )
881 AND post_type = %s
882 AND post_status = %s",
883 'topics',
884 'publish',
885 'tutor_quiz',
886 'publish'
887 )
888 );
889 //phpcs:enable
890 return $count ? $count : 0;
891 }
892
893 /**
894 * Get final quiz result depending on all attempts.
895 *
896 * @since 2.4.0
897 *
898 * @param int $quiz_id quiz id.
899 * @param int $user_id user id.
900 *
901 * @return string pass, fail, pending
902 */
903 public static function get_quiz_result( $quiz_id, $user_id = 0 ) {
904 global $wpdb;
905
906 $all_pending = true;
907 $result = 'pending';
908
909 $user_id = tutor_utils()->get_user_id( $user_id );
910 $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 ) );
911
912 $total_pending_attempt = (int) $wpdb->get_var(
913 $wpdb->prepare(
914 "SELECT COUNT(quiz_attempt_id) total_pending_attempt
915 FROM (
916 SELECT qa.quiz_attempt_id, COUNT(*) AS total_pending
917 FROM {$wpdb->prefix}tutor_quiz_attempt_answers qa
918 WHERE qa.quiz_id = %d AND qa.user_id=%d AND qa.is_correct IS NULL
919 GROUP BY qa.quiz_attempt_id
920 ) a
921 ",
922 $quiz_id,
923 $user_id
924 )
925 );
926
927 if ( count( $attempt_list ) !== $total_pending_attempt ) {
928 $all_pending = false;
929 }
930
931 if ( false === $all_pending ) {
932 $required_percentage = tutor_utils()->get_quiz_option( $quiz_id, 'passing_grade', 0 );
933 foreach ( $attempt_list as $attempt ) {
934 $earned_percentage = $attempt->earned_marks > 0 ? ( ( $attempt->earned_marks * 100 ) / $attempt->total_marks ) : 0;
935 if ( $earned_percentage >= $required_percentage ) {
936 // If at least one attempt passed then quiz passed.
937 $result = 'pass';
938 break;
939 } else {
940 $result = 'fail';
941 }
942 }
943 }
944
945 return $result;
946 }
947
948 /**
949 * Get quiz attempt details
950 *
951 * @since 2.6.1
952 *
953 * @param integer $attempt_id attempt id.
954 *
955 * @return mixed
956 */
957 public static function quiz_attempt_details( int $attempt_id ) {
958 global $wpdb;
959
960 $table_quiz_attempt_answers = $wpdb->prefix . 'tutor_quiz_attempt_answers';
961 $table_quiz_questions = $wpdb->prefix . 'tutor_quiz_questions';
962 $table_quiz_attempts = $wpdb->prefix . 'tutor_quiz_attempts';
963 $table_quiz_question_answers = $wpdb->prefix . 'tutor_quiz_question_answers';
964
965 $query = "SELECT
966 ques.question_id,
967 ques.question_title,
968 ques.question_type,
969 (
970 SELECT
971 GROUP_CONCAT(answer_title)
972 FROM
973 {$table_quiz_question_answers}
974 WHERE
975 belongs_question_id = ques.question_id
976 AND is_correct = 1
977 ) AS correct_answers,
978
979 (
980
981 SELECT
982
983 CASE
984 WHEN CHAR_LENGTH(att_ans.given_answer) = 1 AND att_ans.given_answer REGEXP '^[0-9]$' THEN
985 -- If given_answer is a single digit integer
986 (
987 SELECT
988 answer_title
989 FROM
990 {$table_quiz_question_answers}
991 WHERE
992 answer_id = CAST(att_ans.given_answer AS UNSIGNED)
993 )
994 WHEN CHAR_LENGTH(att_ans.given_answer) > 1 AND SUBSTRING(att_ans.given_answer, 1, 2) = 'a:' THEN
995 -- If given_answer is serialized array
996 (
997 att_ans.given_answer
998 )
999 ELSE
1000 -- If given_answer is a serialized string
1001 att_ans.given_answer
1002 END
1003 ) AS given_answer,
1004 att_ans.question_mark,
1005 att_ans.achieved_mark,
1006 att_ans.is_correct,
1007 (
1008 SELECT
1009 attempt_info
1010 FROM {$table_quiz_attempts}
1011 WHERE attempt_id = {$attempt_id}
1012 LIMIT 1
1013 ) AS attempt_info
1014 FROM
1015 {$table_quiz_attempt_answers} AS att_ans
1016 JOIN {$table_quiz_questions} AS ques ON ques.question_id = att_ans.question_id
1017 JOIN {$table_quiz_question_answers} AS ans ON ans.answer_id = att_ans.attempt_answer_id
1018 WHERE
1019 quiz_attempt_id = %d
1020 LIMIT
1021 50
1022 ";
1023
1024 //phpcs:ignore
1025 $result = $wpdb->get_results( $wpdb->prepare( $query, $attempt_id ) );
1026
1027 // If array and count result then loop with each result and prepare given answer.
1028 if ( is_array( $result ) && count( $result ) ) {
1029 foreach ( $result as $key => $value ) {
1030 // Check if given answer is a serialized string.
1031 if ( is_serialized( $value->given_answer ) ) {
1032 $given_answers = tutor_utils()->get_answer_by_id( maybe_unserialize( $value->given_answer ) );
1033 $result[ $key ]->given_answer = array_column( $given_answers, 'answer_title' );
1034 } elseif ( is_numeric( $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 }
1038 }
1039 }
1040
1041 return $result;
1042 }
1043
1044 /**
1045 * Get a question record.
1046 *
1047 * @since 3.0.0
1048 *
1049 * @param int $question_id quiz question id.
1050 *
1051 * @return array|object|null|void
1052 */
1053 public static function get_question( $question_id ) {
1054 global $wpdb;
1055 return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}tutor_quiz_questions WHERE question_id = %d", $question_id ) );
1056 }
1057
1058 /**
1059 * Get all answer's of a quiz question.
1060 *
1061 * @since 3.0.0
1062 *
1063 * @param int $question_id question id.
1064 * @param string $question_type question type.
1065 *
1066 * @return array
1067 */
1068 public static function get_question_answers( $question_id, $question_type = null ) {
1069 global $wpdb;
1070
1071 $query = "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id = %d";
1072
1073 if ( $question_type ) {
1074 $query .= ' AND belongs_question_type = %s ORDER BY answer_order ASC';
1075 //phpcs:ignore
1076 $answers = $wpdb->get_results( $wpdb->prepare( $query, $question_id, $question_type ) );
1077 } else {
1078 $query .= ' ORDER BY answer_order ASC';
1079 //phpcs:ignore
1080 $answers = $wpdb->get_results( $wpdb->prepare( $query, $question_id ) );
1081 }
1082
1083 foreach ( $answers as $answer ) {
1084 $answer->answer_title = stripslashes( $answer->answer_title );
1085 if ( $answer->image_id ) {
1086 $answer->image_url = wp_get_attachment_url( $answer->image_id );
1087 }
1088 }
1089
1090 return $answers;
1091 }
1092
1093 /**
1094 * Get next answer order SL no
1095 *
1096 * @since 3.0.0
1097 *
1098 * @param int $question_id question id.
1099 * @param int $question_type question type.
1100 *
1101 * @return int
1102 */
1103 public static function get_next_answer_order( $question_id, $question_type ) {
1104 global $wpdb;
1105 $max_id = (int) $wpdb->get_var(
1106 $wpdb->prepare(
1107 "SELECT MAX(answer_order) FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id = %d AND belongs_question_type = %s",//phpcs:ignore
1108 $question_id,
1109 $question_type
1110 )
1111 );
1112
1113 return $max_id + 1;
1114 }
1115
1116 /**
1117 * Get quiz details by quiz id.
1118 *
1119 * @since 3.0.0
1120 *
1121 * @param int $quiz_id quiz id.
1122 *
1123 * @return object
1124 */
1125 public static function get_quiz_details( $quiz_id ) {
1126 $quiz = get_post( $quiz_id );
1127 $quiz->quiz_option = get_post_meta( $quiz_id, Quiz::META_QUIZ_OPTION, true );
1128 $quiz->questions = tutor_utils()->get_questions_by_quiz( $quiz_id );
1129
1130 if ( ! is_array( $quiz->questions ) ) {
1131 $quiz->questions = array();
1132 }
1133
1134 foreach ( $quiz->questions as $question ) {
1135 $question->question_answers = self::get_question_answers( $question->question_id, $question->question_type );
1136 if ( isset( $question->question_settings ) ) {
1137 $question->question_settings = maybe_unserialize( $question->question_settings );
1138 }
1139 }
1140
1141 return $quiz;
1142 }
1143 }
1144