| @@ -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 | } |