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