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