PluginProbe
Tutor LMS – eLearning and online course solution / 3.9.14
Tutor LMS – eLearning and online course solution v3.9.14
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 3.9.15 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 All 191 releases
← All changes | models/QuizModel.php +23 -567 trunk3.9.14 View file →
@@ -10,10 +10,8 @@
10 10
11 11 namespace Tutor\Models;
12 12
13 13 use Tutor\Cache\TutorCache;
14 -use TUTOR\Course_List;
15 -use Tutor\Helpers\DateTimeHelper;
16 14 use Tutor\Helpers\QueryHelper;
17 15 use TUTOR\Quiz;
18 16
19 17 /**
@@ -31,41 +29,9 @@
31 29 const RESULT_PASS = 'pass';
32 30 const RESULT_FAIL = 'fail';
33 31 const RESULT_PENDING = 'pending';
34 32
35 - const ATTEMPTS_TABLE = 'tutor_quiz_attempts';
36 -
37 33 /**
38 - * Question type constants
39 - *
40 - * @since 4.0.0
41 - *
42 - * @var string
43 - */
44 - const QUESTION_TYPE_TRUE_FALSE = 'true_false';
45 - const QUESTION_TYPE_SINGLE_CHOICE = 'single_choice';
46 - const QUESTION_TYPE_MULTIPLE_CHOICE = 'multiple_choice';
47 - const QUESTION_TYPE_OPEN_ENDED = 'open_ended';
48 - const QUESTION_TYPE_FILL_IN_THE_BLANK = 'fill_in_the_blank';
49 - const QUESTION_TYPE_SHORT_ANSWER = 'short_answer';
50 - const QUESTION_TYPE_MATCHING = 'matching';
51 - const QUESTION_TYPE_IMAGE_MATCHING = 'image_matching';
52 - const QUESTION_TYPE_IMAGE_ANSWERING = 'image_answering';
53 - const QUESTION_TYPE_ORDERING = 'ordering';
54 -
55 - /**
56 - * Newly added question types.
57 - * These questions types are not supported in legacy learning mode.
58 - *
59 - * @since 4.0.0
60 - */
61 - const QUESTION_TYPE_DRAW_IMAGE = 'draw_image';
62 - const QUESTION_TYPE_SCALE = 'scale';
63 - const QUESTION_TYPE_PIN_IMAGE = 'pin_image';
64 - const QUESTION_TYPE_COORDINATES = 'coordinates';
65 - const QUESTION_TYPE_PUZZLE = 'puzzle';
66 -
67 - /**
68 34 * Get quiz table name
69 35 *
70 36 * @since 2.1.0
71 37 *
@@ -71,123 +37,13 @@
71 37 *
72 38 * @return string
73 39 */
74 40 public function get_table(): string {
75 - return QueryHelper::prepare_table_name( self::ATTEMPTS_TABLE );
41 + global $wpdb;
42 + return $wpdb->prefix . 'tutor_quiz_attempts';
76 43 }
77 44
78 -
79 45 /**
80 - * Get all quiz types which are only available in modern learning mode.
81 - *
82 - * @since 4.0.0
83 - *
84 - * @return array
85 - */
86 - public static function get_modern_mode_quiz_types() {
87 - return array(
88 - self::QUESTION_TYPE_DRAW_IMAGE => array(
89 - 'name' => __( 'Image Marking', 'tutor' ),
90 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-draw-image tutor-icon-image"></i></span>',
91 - 'is_pro' => true,
92 - ),
93 - self::QUESTION_TYPE_SCALE => array(
94 - 'name' => __( 'Range', 'tutor' ),
95 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-scale tutor-icon-slider-horizontal"></i></span>',
96 - 'is_pro' => true,
97 - ),
98 - self::QUESTION_TYPE_PIN_IMAGE => array(
99 - 'name' => __( 'Pin', 'tutor' ),
100 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-pin-image tutor-icon-image"></i></span>',
101 - 'is_pro' => true,
102 - ),
103 - self::QUESTION_TYPE_COORDINATES => array(
104 - 'name' => __( 'Graph', 'tutor' ),
105 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-coordinates tutor-icon-grid"></i></span>',
106 - 'is_pro' => true,
107 - ),
108 - self::QUESTION_TYPE_PUZZLE => array(
109 - 'name' => __( 'Puzzle', 'tutor' ),
110 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-puzzle tutor-icon-images"></i></span>',
111 - 'is_pro' => true,
112 - ),
113 - );
114 - }
115 -
116 - /**
117 - * Get all question types
118 - *
119 - * @since 1.0.0
120 - *
121 - * @param mixed $type type.
122 - *
123 - * @return array|mixed
124 - */
125 - public static function get_question_types( $type = '' ) {
126 - $types = array(
127 - self::QUESTION_TYPE_TRUE_FALSE => array(
128 - 'name' => __( 'True/False', 'tutor' ),
129 - 'icon' => '<span class="tooltip-btn" ><i class="tutor-quiz-type-icon tutor-quiz-type-boolean tutor-icon-circle-half"></i></span>',
130 - 'is_pro' => false,
131 - ),
132 - self::QUESTION_TYPE_SINGLE_CHOICE => array(
133 - 'name' => __( 'Single Choice', 'tutor' ),
134 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-single-choice tutor-icon-mark"></i></span>',
135 - 'is_pro' => false,
136 - ),
137 - self::QUESTION_TYPE_MULTIPLE_CHOICE => array(
138 - 'name' => __( 'Multiple Choice', 'tutor' ),
139 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-multiple-choices tutor-icon-double-mark"></i></span>',
140 - 'is_pro' => false,
141 - ),
142 - self::QUESTION_TYPE_OPEN_ENDED => array(
143 - 'name' => __( 'Open Ended', 'tutor' ),
144 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-open-ended tutor-icon-text-width"></i></span>',
145 - 'is_pro' => false,
146 - ),
147 - self::QUESTION_TYPE_FILL_IN_THE_BLANK => array(
148 - 'name' => __( 'Fill In The Blanks', 'tutor' ),
149 - 'icon' => '<span class="tooltip-btn" ><i class="tutor-quiz-type-icon tutor-quiz-type-fill-blanks tutor-icon-hourglass"></i></span>',
150 - 'is_pro' => false,
151 - ),
152 - self::QUESTION_TYPE_SHORT_ANSWER => array(
153 - 'name' => __( 'Short Answer', 'tutor' ),
154 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-short-answer tutor-icon-minimize"></i></span>',
155 - 'is_pro' => true,
156 - ),
157 - self::QUESTION_TYPE_MATCHING => array(
158 - 'name' => __( 'Matching', 'tutor' ),
159 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-matching tutor-icon-arrow-right-left"></i></span>',
160 - 'is_pro' => true,
161 - ),
162 - self::QUESTION_TYPE_IMAGE_MATCHING => array(
163 - 'name' => __( 'Image Matching', 'tutor' ),
164 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-image-matching tutor-icon-images"></i></span>',
165 - 'is_pro' => true,
166 - ),
167 - self::QUESTION_TYPE_IMAGE_ANSWERING => array(
168 - 'name' => __( 'Image Answering', 'tutor' ),
169 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-image-answering tutor-icon-camera"></i></span>',
170 - 'is_pro' => true,
171 - ),
172 - self::QUESTION_TYPE_ORDERING => array(
173 - 'name' => __( 'Ordering', 'tutor' ),
174 - 'icon' => '<span class="tooltip-btn"><i class="tutor-quiz-type-icon tutor-quiz-type-ordering tutor-icon-ordering-z-a"></i></span>',
175 - 'is_pro' => true,
176 - ),
177 - );
178 -
179 - $types = $types + self::get_modern_mode_quiz_types();
180 - $types = apply_filters( 'tutor_get_question_types', $types );
181 -
182 - if ( isset( $types[ $type ] ) ) {
183 - return $types[ $type ];
184 - }
185 -
186 - return $types;
187 - }
188 -
189 - /**
190 46 * Get manual review types.
191 47 *
192 48 * @since 3.8.0
193 49 *
@@ -193,135 +49,12 @@
193 49 *
194 50 * @return array
195 51 */
196 52 public static function get_manual_review_types() {
197 - return array(
198 - self::QUESTION_TYPE_OPEN_ENDED,
199 - self::QUESTION_TYPE_SHORT_ANSWER,
200 - );
53 + return array( 'open_ended', 'short_answer' );
201 54 }
202 55
203 -
204 56 /**
205 - * Format the quiz attempts result obtained from query.
206 - *
207 - * @since 4.0.0
208 - *
209 - * @param array $quiz_attempts the quiz_attempts result obtained from query.
210 - * @param string $result attempt result type.
211 - *
212 - * @return array
213 - */
214 - public static function format_quiz_attempts( array $quiz_attempts, string $result = '' ): array {
215 - $formatted_attempts = array();
216 -
217 - if ( ! count( $quiz_attempts ) ) {
218 - return $quiz_attempts;
219 - }
220 -
221 - foreach ( $quiz_attempts as $quiz_attempt ) {
222 - $course_title = $quiz_attempt->course_title ?? '';
223 - $quiz_title = $quiz_attempt->post_title ?? '';
224 -
225 - $quiz_attempt_result = $quiz_attempt->result ?? 'fail';
226 - $result_types = array( self::RESULT_FAIL, self::RESULT_PASS, self::RESULT_PENDING );
227 -
228 - if ( ! empty( $result ) && in_array( $result, $result_types, true ) && $quiz_attempt_result !== $result ) {
229 - continue;
230 - }
231 -
232 - // Common formatting logic.
233 - $start_time = DateTimeHelper::create( $quiz_attempt->attempt_started_at ?? '' )
234 - ->format( get_option( 'date_format' ) . ', ' . get_option( 'time_format' ) );
235 -
236 - $attempt_time = strtotime( $quiz_attempt->attempt_ended_at ?? '' ) - strtotime( $quiz_attempt->attempt_started_at ?? '' );
237 - $attempt_time = tutor_utils()->playtime_string( $attempt_time );
238 -
239 - $earned_percent = self::calculate_attempt_earned_percentage( $quiz_attempt );
240 -
241 - $correct_answers = 0;
242 - $incorrect_answers = 0;
243 -
244 - $answers = self::get_quiz_answers_by_attempt_id( $quiz_attempt->attempt_id );
245 -
246 - if ( tutor_utils()->count( $answers ) ) {
247 - foreach ( $answers as $answer ) {
248 - $is_correct = (int) $answer->is_correct ?? 0;
249 - if ( $is_correct ) {
250 - ++$correct_answers;
251 - } else {
252 - ++$incorrect_answers;
253 - }
254 - }
255 - }
256 -
257 - $formatted_attempt = array(
258 - 'attempt_id' => $quiz_attempt->attempt_id ?? 0,
259 - 'result' => $quiz_attempt_result,
260 - 'marks_percent' => $earned_percent ?? 0,
261 - 'correct_answers' => $correct_answers,
262 - 'incorrect_answers' => $incorrect_answers,
263 - 'time_taken' => $attempt_time ?? '',
264 - 'date' => $start_time ?? '',
265 - 'quiz_title' => $quiz_title,
266 - 'course_title' => $course_title,
267 - 'quiz_id' => $quiz_attempt->quiz_id ?? 0,
268 - 'course_id' => $quiz_attempt->course_id ?? 0,
269 - 'student' => $quiz_attempt->display_name ?? '',
270 - 'attempt_info' => maybe_unserialize( $quiz_attempt->attempt_info ) ?? array(),
271 - );
272 -
273 - $formatted_attempts[] = $formatted_attempt;
274 - }
275 -
276 - return $formatted_attempts;
277 - }
278 -
279 - /**
280 - * Get formatted quiz attempt list by quiz data.
281 - *
282 - * @since 4.0.0
283 - *
284 - * @param array $quizzes list of quiz data.
285 - * @param string $result attempt result type.
286 - *
287 - * @return array
288 - */
289 - public function get_formatted_quiz_attempt_list_by_quiz_id( $quizzes, $result = '' ) {
290 - $quiz_attempts_list = array();
291 -
292 - if ( ! tutor_utils()->count( $quizzes ) ) {
293 - return $quiz_attempts_list;
294 - }
295 -
296 - foreach ( $quizzes as $quiz_info ) {
297 - $quiz_id = $quiz_info['quiz_id'] ?? 0;
298 - $course_id = $quiz_info['course_id'] ?? 0;
299 -
300 - $quiz_attempts = $this->quiz_attempts( $quiz_id, get_current_user_id() );
301 - $formatted_quiz_attempts = self::format_quiz_attempts( $quiz_attempts, $result );
302 -
303 - if ( ! tutor_utils()->count( $formatted_quiz_attempts ) ) {
304 - continue;
305 - }
306 -
307 - $quiz_attempts_list[ $quiz_id ] = array(
308 - 'quiz_id' => $quiz_id,
309 - 'quiz_title' => get_the_title( $quiz_id ),
310 - 'course_title' => get_the_title( $course_id ),
311 - 'course_id' => $course_id,
312 - 'attempts' => array(),
313 - );
314 -
315 - foreach ( $formatted_quiz_attempts as $attempt ) {
316 - $quiz_attempts_list[ $quiz_id ]['attempts'][] = $attempt;
317 - }
318 - }
319 -
320 - return $quiz_attempts_list;
321 - }
322 -
323 - /**
324 57 * Get total number of quiz
325 58 *
326 59 * @since 2.0.2
327 60 *
@@ -620,24 +353,22 @@
620 353 *
621 354 * @since 1.0.0
622 355 * @since 1.9.5 sorting params added.
623 356 * @since 3.8.0 refactor and query optimize.
624 - * @since 4.0.0 date-range filtering added via $start_date / $end_date.
625 357 *
626 - * @param integer $start Query offset.
627 - * @param integer $limit Number of rows to return (0 = no limit).
628 - * @param string $search_filter Search keyword matched against user email, display name, quiz title, and course title.
629 - * @param string $course_filter Course ID (or array of IDs) to restrict results to.
630 - * @param string $start_date Range start date (Y-m-d).
631 - * @param string $end_date Range end date (Y-m-d).
632 - * @param string $order_filter SQL ORDER BY direction – 'ASC' or 'DESC'.
633 - * @param mixed $result_state Attempt result to filter by (pass|fail|pending). Null returns all results.
634 - * @param boolean $count_only When true, returns an integer count instead of rows.
635 - * @param boolean $instructor_id_check When true, restricts results to courses the current user instructs.
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.
636 367 *
637 - * @return mixed Integer count when $count_only is true, array of row objects otherwise.
368 + * @return mixed
638 369 */
639 - public static function get_quiz_attempts( $start = 0, $limit = 10, $search_filter = '', $course_filter = array(), $start_date = '', $end_date = '', $order_filter = 'DESC', $result_state = null, $count_only = false, $instructor_id_check = false ) {
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 ) {
640 371 global $wpdb;
641 372
642 373 $start = (int) $start;
643 374 $limit = (int) $limit;
@@ -642,8 +373,9 @@
642 373 $start = (int) $start;
643 374 $limit = (int) $limit;
644 375 $search_filter = sanitize_text_field( $search_filter );
645 376 $course_filter = sanitize_text_field( $course_filter );
377 + $date_filter = sanitize_text_field( $date_filter );
646 378 $order_filter = sanitize_sql_orderby( $order_filter );
647 379
648 380 $search_term_raw = $search_filter;
649 381 $search_filter = '%' . $wpdb->esc_like( $search_filter ) . '%';
@@ -657,19 +389,15 @@
657 389 $course_ids = QueryHelper::prepare_in_clause( array_map( 'intval', $course_filter ) );
658 390 $course_filter = " AND quiz_attempts.course_id IN ($course_ids) ";
659 391 }
660 392
661 - // Filter by date (single) or date range.
662 - $date_filter = '';
663 - if ( '' !== $start_date && '' !== $end_date ) {
664 - $start_date = tutor_get_formated_date( 'Y-m-d', $start_date );
665 - $end_date = tutor_get_formated_date( 'Y-m-d', $end_date );
666 - $date_filter = $wpdb->prepare( ' AND DATE(quiz_attempts.attempt_started_at) BETWEEN %s AND %s ', $start_date, $end_date );
667 - }
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 ) : '';
668 396
669 397 $result_clause = '';
670 - $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, course.post_title as course_title';
671 - $limit_offset = $count_only || ( 0 === $limit && 0 === $start ) ? '' : $wpdb->prepare( ' LIMIT %d OFFSET %d', $limit, $start );
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 );
672 400
673 401 // Get attempts by instructor ID.
674 402 $instructor_clause = '';
675 403 if ( $instructor_id_check ) {
@@ -726,20 +454,14 @@
726 454 *
727 455 * @return void
728 456 */
729 457 public static function delete_quiz_attempt( $attempt_ids ) {
730 - // Singlular to array.
731 458 global $wpdb;
732 459
733 460 // Singular to array.
734 461 ! is_array( $attempt_ids ) ? $attempt_ids = array( $attempt_ids ) : 0;
735 - $attempt_ids = array_map( 'absint', array_filter( $attempt_ids ) );
736 462
737 463 if ( count( $attempt_ids ) ) {
738 - // Collect file paths from all question types that store files (e.g. draw_image). Files deleted after DB for safety.
739 - $attempt_file_paths = apply_filters( 'tutor_quiz/attempt_file_paths_for_deletion', array(), $attempt_ids );
740 - $attempt_file_paths = is_array( $attempt_file_paths ) ? array_values( array_filter( array_unique( $attempt_file_paths ) ) ) : array();
741 -
742 464 $attempt_ids = QueryHelper::prepare_in_clause( $attempt_ids );
743 465
744 466 // Deleting attempt (comment), child attempt and attempt meta (comment meta).
745 467 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempts WHERE attempt_id IN({$attempt_ids})" ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
@@ -744,32 +466,13 @@
744 466 // Deleting attempt (comment), child attempt and attempt meta (comment meta).
745 467 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempts WHERE attempt_id IN({$attempt_ids})" ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
746 468 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempt_answers WHERE quiz_attempt_id IN({$attempt_ids})" ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
747 469
748 - self::delete_files_by_paths( $attempt_file_paths );
749 -
750 470 do_action( 'tutor_quiz/attempt_deleted', $attempt_ids );
751 471 }
752 472 }
753 473
754 474 /**
755 - * Delete files by absolute path (e.g. after DB rows have been removed).
756 - *
757 - * @since 4.0.0
758 - *
759 - * @param string[] $paths Array of absolute file paths.
760 - *
761 - * @return void
762 - */
763 - public static function delete_files_by_paths( array $paths ) {
764 - foreach ( $paths as $path ) {
765 - if ( is_string( $path ) && '' !== $path && is_file( $path ) && is_readable( $path ) ) {
766 - wp_delete_file( $path );
767 - }
768 - }
769 - }
770 -
771 - /**
772 475 * Sorting params added on quiz attempt
773 476 *
774 477 * @since 1.9.5
775 478 *
@@ -810,10 +513,10 @@
810 513 $date_filter = '' != $date_filter ? tutor_get_formated_date( 'Y-m-d', $date_filter ) : '';
811 514 $date_filter = '' != $date_filter ? " AND DATE(quiz_attempts.attempt_started_at) = '$date_filter' " : '';
812 515 $user_filter = $user_id ? ' AND user_id=\'' . esc_sql( $user_id ) . '\' ' : '';
813 516
814 - $limit_offset = $count_only || ( 0 === $limit && 0 === $start ) ? '' : " LIMIT {$start}, {$limit} ";
815 - $select_col = $count_only ? ' COUNT(DISTINCT quiz_attempts.attempt_id) ' : ' quiz_attempts.*, quiz.* ';
517 + $limit_offset = $count_only ? '' : " LIMIT {$start}, {$limit} ";
518 + $select_col = $count_only ? ' COUNT(DISTINCT quiz_attempts.attempt_id) ' : ' quiz_attempts.*, users.*, quiz.* ';
816 519
817 520 $attempt_type = $all_attempt ? '' : " AND quiz_attempts.attempt_status != 'attempt_started' ";
818 521
819 522 $query = "SELECT {$select_col}
@@ -837,74 +540,8 @@
837 540 return $count_only ? $wpdb->get_var( $query ) : $wpdb->get_results( $query );
838 541 }
839 542
840 543 /**
841 - * Obtain quiz data based on student quiz attempts.
842 - *
843 - * @since 4.0.0
844 - *
845 - * @param integer $user_id the user id.
846 - * @param integer $course_id the course id.
847 - * @param integer $start the query offset.
848 - * @param integer $limit the query limit.
849 - * @param string $order_filter filter for ASC or DESC order.
850 - * @param array $args list of filters to apply.
851 - *
852 - * @return array
853 - */
854 - public static function get_attempted_quizzes( $user_id = 0, $course_id = 0, $start = 0, $limit = 0, $order_filter = 'DESC', $args = array() ) {
855 - $quiz_table = 'posts as quiz';
856 - $user_table = 'users as users';
857 - $courses_table = 'posts as course';
858 -
859 - $join_conditions = array(
860 - array(
861 - 'type' => 'INNER',
862 - 'table' => $quiz_table,
863 - 'on' => 'quiz_attempts.quiz_id = quiz.ID',
864 - ),
865 - array(
866 - 'type' => 'INNER',
867 - 'table' => $user_table,
868 - 'on' => 'quiz_attempts.user_id = users.ID',
869 - ),
870 - array(
871 - 'type' => 'INNER',
872 - 'table' => $courses_table,
873 - 'on' => 'course.ID = quiz_attempts.course_id',
874 - ),
875 - );
876 -
877 - $where_conditions = array(
878 - 'quiz_attempts.attempt_status' => array( '!=', 'attempt_started' ),
879 - 'user_id' => $user_id,
880 - );
881 -
882 - if ( $course_id ) {
883 - $where_conditions['quiz_attempts.course_id'] = $course_id;
884 - }
885 -
886 - if ( isset( $args['status'] ) && ! empty( $args['status'] ) ) {
887 - $where_conditions['quiz_attempts.result'] = $args['status'];
888 - }
889 -
890 - $quiz_ids = QueryHelper::get_joined_data(
891 - self::ATTEMPTS_TABLE . ' as quiz_attempts',
892 - $join_conditions,
893 - array( 'DISTINCT quiz_attempts.quiz_id,quiz_attempts.course_id' ),
894 - $where_conditions,
895 - array(),
896 - 'quiz_attempts.quiz_id',
897 - $limit,
898 - $start,
899 - $order_filter,
900 - 'ARRAY_A',
901 - );
902 -
903 - return $quiz_ids;
904 - }
905 -
906 - /**
907 544 * Get answers list by quiz question
908 545 *
909 546 * @since 1.0.0
910 547 *
@@ -1001,115 +638,8 @@
1001 638 return $results;
1002 639 }
1003 640
1004 641 /**
1005 - * Check whether an attempt answer should be treated as skipped.
1006 - *
1007 - * @since 4.0.0
1008 - *
1009 - * @param object|null $attempt_answer Attempt answer object.
1010 - *
1011 - * @return bool
1012 - */
1013 - public static function is_attempt_answer_skipped( $attempt_answer ): bool {
1014 - if ( ! is_object( $attempt_answer ) ) {
1015 - return true;
1016 - }
1017 -
1018 - $given_answer = maybe_unserialize( $attempt_answer->given_answer ?? '' );
1019 -
1020 - if ( is_array( $given_answer ) ) {
1021 - $given_answer = array_filter(
1022 - array_map(
1023 - static function ( $item ) {
1024 - return trim( wp_strip_all_tags( (string) $item ) );
1025 - },
1026 - $given_answer
1027 - ),
1028 - static function ( string $item ) {
1029 - return '' !== $item;
1030 - }
1031 - );
1032 -
1033 - return empty( $given_answer );
1034 - }
1035 -
1036 - return '' === trim( wp_strip_all_tags( (string) $given_answer ) );
1037 - }
1038 -
1039 - /**
1040 - * Filter attempt answers for attempt-details views.
1041 - *
1042 - * @since 4.0.0
1043 - *
1044 - * @param array $attempt_answers Attempt answer rows.
1045 - * @param bool $is_instructor_review Whether the current view is instructor review.
1046 - *
1047 - * @return array
1048 - */
1049 - public static function filter_attempt_answers_for_details( $attempt_answers, bool $is_instructor_review = false ): array {
1050 - if ( ! is_array( $attempt_answers ) ) {
1051 - return array();
1052 - }
1053 -
1054 - return array_values(
1055 - array_filter(
1056 - $attempt_answers,
1057 - static function ( $attempt_answer ) use ( $is_instructor_review ) {
1058 - if ( ! is_object( $attempt_answer ) ) {
1059 - return false;
1060 - }
1061 -
1062 - if ( $is_instructor_review ) {
1063 - return true;
1064 - }
1065 -
1066 - return ! self::is_attempt_answer_skipped( $attempt_answer );
1067 - }
1068 - )
1069 - );
1070 - }
1071 -
1072 - /**
1073 - * Get normalized attempt-answer status.
1074 - *
1075 - * Status rules follow legacy attempt-details logic:
1076 - * - correct: is_correct is truthy.
1077 - * - pending: is_correct is null for manually reviewed question types.
1078 - * - incorrect: all other cases.
1079 - *
1080 - * @since 4.0.0
1081 - *
1082 - * @param object $attempt_answer Attempt answer object.
1083 - *
1084 - * @return string One of: correct, pending, wrong.
1085 - */
1086 - public static function get_attempt_answer_status( $attempt_answer ): string {
1087 - $question_type = (string) ( $attempt_answer->question_type ?? '' );
1088 -
1089 - if ( 'image_matching' === $question_type ) {
1090 - $question_type = 'matching';
1091 - }
1092 -
1093 - if ( 'single_choice' === $question_type ) {
1094 - $question_type = 'multiple_choice';
1095 - }
1096 -
1097 - if ( (bool) ( $attempt_answer->is_correct ?? false ) ) {
1098 - return 'correct';
1099 - }
1100 -
1101 - if (
1102 - null === ( $attempt_answer->is_correct ?? null ) &&
1103 - in_array( $question_type, array( 'open_ended', 'short_answer', 'image_answering' ), true )
1104 - ) {
1105 - return 'pending';
1106 - }
1107 -
1108 - return 'incorrect';
1109 - }
1110 -
1111 - /**
1112 642 * Get single answer by answer_id
1113 643 *
1114 644 * @since 1.0.0
1115 645 *
@@ -1509,50 +1039,12 @@
1509 1039 $answer->image_url = wp_get_attachment_url( $answer->image_id );
1510 1040 }
1511 1041 }
1512 1042
1513 - /**
1514 - * Filter question answers after loading (e.g. expand stored mask paths for REST/API).
1515 - *
1516 - * @param array $answers Answer rows.
1517 - * @param int $question_id Question id.
1518 - * @param string|null $question_type Question type when known.
1519 - */
1520 - return apply_filters( 'tutor_quiz_question_answers', $answers, $question_id, $question_type );
1043 + return $answers;
1521 1044 }
1522 1045
1523 1046 /**
1524 - * Get full image URL for a quiz answer.
1525 - *
1526 - * Uses attachment ID if present; falls back to stored image URL.
1527 - *
1528 - * @since 4.0.0
1529 - *
1530 - * @param object $answer Quiz answer object.
1531 - * @param string $size Image size to retrieve. Default full.
1532 - *
1533 - * @return string
1534 - */
1535 - public static function get_answer_image_url( $answer, $size = 'full' ) {
1536 - if ( empty( $answer ) ) {
1537 - return '';
1538 - }
1539 -
1540 - if ( ! empty( $answer->image_id ) ) {
1541 - $url = wp_get_attachment_image_url( $answer->image_id, $size );
1542 - if ( $url ) {
1543 - return $url;
1544 - }
1545 - }
1546 -
1547 - if ( ! empty( $answer->image_url ) ) {
1548 - return $answer->image_url;
1549 - }
1550 -
1551 - return '';
1552 - }
1553 -
1554 - /**
1555 1047 * Get next answer order SL no
1556 1048 *
1557 1049 * @since 3.0.0
1558 1050 *
@@ -1724,42 +1216,6 @@
1724 1216 return true;
1725 1217 }
1726 1218
1727 1219 return false;
1728 - }
1729 -
1730 - /**
1731 - * Kill the current execution if user don't have access to this quiz
1732 - *
1733 - * @since 4.0.2
1734 - *
1735 - * @param int $quiz_id Quiz id to check the access.
1736 - * @param int $course_id Quiz's course id.
1737 - * @param bool $wp_die Whether to die the execution or not.
1738 - *
1739 - * @return mixed If wp_die is true then this method will kill the execution if
1740 - * user don't have access to the provided quiz. Otherwise returns bool.
1741 - */
1742 - public static function has_quiz_access( $quiz_id, $course_id = 0, $wp_die = true ) {
1743 - $message = __( 'Something went wrong', 'tutor' );
1744 - $has_access = true;
1745 -
1746 - $course_id = $course_id ? $course_id : tutor_utils()->get_course_id_by( 'quiz', $quiz_id );
1747 - if ( Course_List::is_public( $course_id ) ) {
1748 - return true;
1749 - }
1750 -
1751 - if ( ! $quiz_id ) {
1752 - $message = __( 'Invalid quiz ID', 'tutor' );
1753 - $has_access = false;
1754 - } elseif ( ! tutor_utils()->has_enrolled_content_access( 'quiz', $quiz_id ) ) {
1755 - $message = __( 'You don\'t have access to this course', 'tutor' );
1756 - $has_access = false;
1757 - }
1758 -
1759 - if ( ! $has_access && $wp_die ) {
1760 - wp_die( esc_html( $message ) );
1761 - }
1762 -
1763 - return $has_access;
1764 1220 }
1765 1221 }