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