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