Addons.php
17 hours ago
Admin.php
17 hours ago
Ajax.php
17 hours ago
Announcements.php
17 hours ago
Assets.php
17 hours ago
Backend_Page_Trait.php
1 year ago
BaseController.php
1 year ago
Config.php
17 hours ago
Container.php
11 months ago
Course.php
17 hours ago
Course_Embed.php
3 years ago
Course_Filter.php
17 hours ago
Course_List.php
17 hours ago
Course_Settings_Tabs.php
17 hours ago
Course_Widget.php
1 year ago
Custom_Validation.php
17 hours ago
Dashboard.php
17 hours ago
Earnings.php
9 months ago
FormHandler.php
17 hours ago
Frontend.php
17 hours ago
Gutenberg.php
1 year ago
Icon.php
17 hours ago
Input.php
17 hours ago
Instructor.php
17 hours ago
Instructors_List.php
17 hours ago
Lesson.php
17 hours ago
Options_V2.php
17 hours ago
Permalink.php
17 hours ago
Post_types.php
1 day ago
Private_Course_Access.php
17 hours ago
Q_And_A.php
17 hours ago
Question_Answers_List.php
11 months ago
Quiz.php
17 hours ago
QuizBuilder.php
17 hours ago
Quiz_Attempts_List.php
17 hours ago
RestAPI.php
2 years ago
Reviews.php
17 hours ago
Rewrite_Rules.php
2 years ago
SampleCourse.php
17 hours ago
Shortcode.php
17 hours ago
Singleton.php
1 year ago
Student.php
17 hours ago
Students_List.php
1 year ago
Taxonomies.php
1 year ago
Template.php
17 hours ago
Theme_Compatibility.php
3 years ago
Tools.php
1 year ago
Tools_V2.php
4 weeks ago
Tutor.php
17 hours ago
TutorEDD.php
17 hours ago
Tutor_Base.php
2 years ago
Tutor_Setup.php
17 hours ago
Upgrader.php
17 hours ago
User.php
17 hours ago
UserPreference.php
17 hours ago
Utils.php
17 hours ago
Video_Stream.php
3 years ago
WhatsNew.php
10 months ago
Withdraw.php
17 hours ago
Withdraw_Requests_List.php
11 months ago
WooCommerce.php
17 hours ago
Quiz_Attempts_List.php
716 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Quiz attempt list management |
| 4 | * |
| 5 | * @package Tutor\QuestionAnswer |
| 6 | * @author Themeum <support@themeum.com> |
| 7 | * @link https://themeum.com |
| 8 | * @since 1.0.0 |
| 9 | */ |
| 10 | |
| 11 | namespace TUTOR; |
| 12 | |
| 13 | if ( ! defined( 'ABSPATH' ) ) { |
| 14 | exit; |
| 15 | } |
| 16 | |
| 17 | use TUTOR\User; |
| 18 | use Tutor\Cache\QuizAttempts; |
| 19 | use Tutor\Components\Badge; |
| 20 | use Tutor\Components\Button; |
| 21 | use Tutor\Components\Constants\Size; |
| 22 | use Tutor\Components\Constants\Positions; |
| 23 | use Tutor\Components\Constants\Variant; |
| 24 | use Tutor\Components\Popover; |
| 25 | use Tutor\Helpers\UrlHelper; |
| 26 | use Tutor\Models\QuizModel; |
| 27 | use Tutor\Components\SvgIcon; |
| 28 | use Tutor\Components\Progress; |
| 29 | |
| 30 | /** |
| 31 | * Quiz attempt class |
| 32 | * |
| 33 | * @since 1.0.0 |
| 34 | */ |
| 35 | class Quiz_Attempts_List { |
| 36 | |
| 37 | const QUIZ_ATTEMPT_PAGE = 'tutor_quiz_attempts'; |
| 38 | |
| 39 | /** |
| 40 | * Trait for utilities |
| 41 | * |
| 42 | * @var $page_title |
| 43 | */ |
| 44 | |
| 45 | use Backend_Page_Trait; |
| 46 | |
| 47 | /** |
| 48 | * Bulk Action |
| 49 | * |
| 50 | * @var $bulk_action |
| 51 | */ |
| 52 | public $bulk_action = true; |
| 53 | |
| 54 | /** |
| 55 | * Handle dependencies |
| 56 | * |
| 57 | * @since 1.0.0 |
| 58 | * |
| 59 | * @param boolean $register_hook should register hook or not. |
| 60 | */ |
| 61 | public function __construct( $register_hook = true ) { |
| 62 | if ( ! $register_hook ) { |
| 63 | return; |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * Handle bulk action |
| 68 | * |
| 69 | * @since 2.0.0 |
| 70 | */ |
| 71 | add_action( 'wp_ajax_tutor_quiz_attempts_bulk_action', array( $this, 'quiz_attempts_bulk_action' ) ); |
| 72 | add_action( 'wp_ajax_tutor_quiz_attempts_count', array( $this, 'get_quiz_attempts_stat' ) ); |
| 73 | |
| 74 | /** |
| 75 | * Delete quiz attempt cache |
| 76 | * |
| 77 | * @since 2.1.0 |
| 78 | */ |
| 79 | add_action( 'tutor_quiz/attempt_ended', array( new QuizAttempts(), 'delete_cache' ) ); |
| 80 | add_action( 'tutor_quiz/attempt_deleted', array( new QuizAttempts(), 'delete_cache' ) ); |
| 81 | add_action( 'tutor_quiz/answer/review/after', array( new QuizAttempts(), 'delete_cache' ) ); |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * Page title fallback |
| 86 | * |
| 87 | * @since 3.5.0 |
| 88 | * |
| 89 | * @param string $name Property name. |
| 90 | * |
| 91 | * @return string |
| 92 | */ |
| 93 | public function __get( $name ) { |
| 94 | if ( 'page_title' === $name ) { |
| 95 | return esc_html__( 'Quiz Attempts', 'tutor' ); |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | /** |
| 100 | * Get the attempts stat from specific instructor context |
| 101 | * |
| 102 | * @since 2.0.0 |
| 103 | * @since 3.8.0 refactor and query optimize. |
| 104 | * |
| 105 | * @return array |
| 106 | */ |
| 107 | public function get_quiz_attempts_stat() { |
| 108 | global $wpdb; |
| 109 | |
| 110 | if ( wp_doing_ajax() ) { |
| 111 | tutor_utils()->checking_nonce(); |
| 112 | } |
| 113 | |
| 114 | $count_obj = (object) array( |
| 115 | 'pass' => 0, |
| 116 | 'fail' => 0, |
| 117 | 'pending' => 0, |
| 118 | ); |
| 119 | |
| 120 | $is_ajax_action = 'tutor_quiz_attempts_count' === Input::post( 'action' ); |
| 121 | $user_id = get_current_user_id(); |
| 122 | $course_id = Input::post( 'course_id', 0, Input::TYPE_INT ); |
| 123 | $date = Input::post( 'date', '' ); |
| 124 | $search = Input::post( 'search', '' ); |
| 125 | |
| 126 | if ( $is_ajax_action ) { |
| 127 | $current_params = compact( 'course_id', 'date', 'search' ); |
| 128 | $attempt_cache = new QuizAttempts( $current_params ); |
| 129 | |
| 130 | $cached_attempts = $attempt_cache->get_cache(); |
| 131 | if ( $attempt_cache->has_cache() && $attempt_cache->is_same_query() && isset( $cached_attempts['result'] ) ) { |
| 132 | $count_obj = $cached_attempts['result']; |
| 133 | } else { |
| 134 | |
| 135 | $course_filter = $course_id ? $wpdb->prepare( ' AND quiz_attempts.course_id = %d', $course_id ) : ''; |
| 136 | $date_filter = empty( $date ) ? '' : $wpdb->prepare( ' AND DATE(quiz_attempts.attempt_started_at) = %s ', $date ); |
| 137 | $user_clause = User::is_admin() ? '' : $wpdb->prepare( ' AND quiz.post_author = %d', $user_id ); |
| 138 | |
| 139 | $search_term_raw = $search; |
| 140 | $search_filter = '%' . $wpdb->esc_like( $search ) . '%'; |
| 141 | |
| 142 | //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 143 | $results = $wpdb->get_results( |
| 144 | $wpdb->prepare( |
| 145 | "SELECT result, COUNT( DISTINCT attempt_id) AS total |
| 146 | FROM {$wpdb->prefix}tutor_quiz_attempts quiz_attempts |
| 147 | INNER JOIN {$wpdb->posts} quiz ON quiz_attempts.quiz_id = quiz.ID |
| 148 | INNER JOIN {$wpdb->users} AS users ON quiz_attempts.user_id = users.ID |
| 149 | INNER JOIN {$wpdb->posts} AS course ON course.ID = quiz_attempts.course_id |
| 150 | WHERE result IS NOT NULL |
| 151 | AND ( |
| 152 | users.user_email = %s |
| 153 | OR users.display_name LIKE %s |
| 154 | OR quiz.post_title LIKE %s |
| 155 | OR course.post_title LIKE %s |
| 156 | ) |
| 157 | {$user_clause} |
| 158 | {$course_filter} |
| 159 | {$date_filter} |
| 160 | GROUP BY result", |
| 161 | $search_term_raw, |
| 162 | $search_filter, |
| 163 | $search_filter, |
| 164 | $search_filter |
| 165 | ) |
| 166 | ); |
| 167 | //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 168 | |
| 169 | foreach ( $results as $row ) { |
| 170 | if ( isset( $count_obj->{$row->result} ) ) { |
| 171 | $count_obj->{$row->result} = (int) $row->total; |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | $attempt_cache->data = $count_obj; |
| 176 | $attempt_cache->set_cache(); |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | $all = $count_obj->pass + $count_obj->fail + $count_obj->pending; |
| 181 | $pass = $count_obj->pass; |
| 182 | $fail = $count_obj->fail; |
| 183 | $pending = $count_obj->pending; |
| 184 | $response = compact( 'all', 'pass', 'fail', 'pending' ); |
| 185 | |
| 186 | return $is_ajax_action ? wp_send_json_success( $response ) : $response; |
| 187 | } |
| 188 | |
| 189 | /** |
| 190 | * Available tabs that will visible on the right side of page navbar |
| 191 | * |
| 192 | * @since 2.0.0 |
| 193 | * |
| 194 | * @param string $user_id selected quiz_attempts id | optional. |
| 195 | * @param int $course_id selected quiz_attempts id | optional. |
| 196 | * @param string $date selected date | optional. |
| 197 | * @param string $search search by user name or email | optional. |
| 198 | * |
| 199 | * @return array |
| 200 | */ |
| 201 | public function tabs_key_value( $user_id, $course_id, $date, $search ): array { |
| 202 | $url = apply_filters( 'tutor_data_tab_base_url', get_pagenum_link() ); |
| 203 | $stats = $this->get_quiz_attempts_stat(); |
| 204 | |
| 205 | $tabs = array( |
| 206 | array( |
| 207 | 'key' => '', |
| 208 | 'title' => __( 'All', 'tutor' ), |
| 209 | 'value' => $stats['all'], |
| 210 | 'url' => $url . '&data=all', |
| 211 | ), |
| 212 | array( |
| 213 | 'key' => 'pass', |
| 214 | 'title' => __( 'Pass', 'tutor' ), |
| 215 | 'value' => $stats['pass'], |
| 216 | 'url' => $url . '&data=pass', |
| 217 | ), |
| 218 | array( |
| 219 | 'key' => 'fail', |
| 220 | 'title' => __( 'Fail', 'tutor' ), |
| 221 | 'value' => $stats['fail'], |
| 222 | 'url' => $url . '&data=fail', |
| 223 | ), |
| 224 | array( |
| 225 | 'key' => 'pending', |
| 226 | 'title' => __( 'Pending', 'tutor' ), |
| 227 | 'value' => $stats['pending'], |
| 228 | 'url' => $url . '&data=pending', |
| 229 | ), |
| 230 | ); |
| 231 | |
| 232 | return $tabs; |
| 233 | } |
| 234 | |
| 235 | /** |
| 236 | * Obtain nav data for quiz attempts. |
| 237 | * |
| 238 | * @since 4.0.0 |
| 239 | * |
| 240 | * @param int $quiz_attempts_count the quiz attempt count. |
| 241 | * @param string $url the url. |
| 242 | * @param string $result_filter the current result state. |
| 243 | * @param string $search_filter the search filter. |
| 244 | * @param string $course_filter the course id filter. |
| 245 | * @param string $start_date range start date (Y-m-d). |
| 246 | * @param string $end_date range end date (Y-m-d). |
| 247 | * @param string $order_filter the order filter. |
| 248 | * @param array $all_quizzes all quiz data for student. |
| 249 | * |
| 250 | * @return array |
| 251 | */ |
| 252 | public function get_quiz_attempts_nav_data( $quiz_attempts_count = 0, $url = '', $result_filter = '', $search_filter = '', $course_filter = 0, $start_date = '', $end_date = '', $order_filter = 'DESC', $all_quizzes = array() ): array { |
| 253 | $quiz_model = new QuizModel(); |
| 254 | |
| 255 | if ( tutor_utils()->count( $all_quizzes ) ) { |
| 256 | $results = isset( $all_quizzes['results'] ) ? $all_quizzes['results'] : array(); |
| 257 | $all_attempts = isset( $all_quizzes['total_count'] ) ? $all_quizzes['total_count'] : 0; |
| 258 | $quiz_attempts_count = $all_attempts; |
| 259 | $passed_attempts = count( $quiz_model->get_formatted_quiz_attempt_list_by_quiz_id( $results, QuizModel::RESULT_PASS ) ); |
| 260 | $failed_attempts = count( $quiz_model->get_formatted_quiz_attempt_list_by_quiz_id( $results, QuizModel::RESULT_FAIL ) ); |
| 261 | $pending_attempts = count( $quiz_model->get_formatted_quiz_attempt_list_by_quiz_id( $results, QuizModel::RESULT_PENDING ) ); |
| 262 | } else { |
| 263 | $all_attempts = QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, '', true, true ); |
| 264 | $pending_attempts = QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, QuizModel::RESULT_PENDING, true, true ); |
| 265 | $passed_attempts = QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, QuizModel::RESULT_PASS, true, true ); |
| 266 | $failed_attempts = QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, QuizModel::RESULT_FAIL, true, true ); |
| 267 | } |
| 268 | |
| 269 | $filter_url = remove_query_arg( 'current_page', $url ); |
| 270 | |
| 271 | $nav_links = array( |
| 272 | 'type' => 'dropdown', |
| 273 | 'active' => true, |
| 274 | 'count' => $quiz_attempts_count, |
| 275 | 'options' => array( |
| 276 | array( |
| 277 | 'label' => __( 'All', 'tutor' ), |
| 278 | 'count' => $all_attempts, |
| 279 | 'url' => remove_query_arg( 'result', $filter_url ), |
| 280 | 'active' => '' === $result_filter, |
| 281 | ), |
| 282 | array( |
| 283 | 'label' => __( 'Pending', 'tutor' ), |
| 284 | 'url' => UrlHelper::add_query_params( $filter_url, array( 'result' => QuizModel::RESULT_PENDING ) ), |
| 285 | 'count' => $pending_attempts, |
| 286 | 'active' => QuizModel::RESULT_PENDING === $result_filter, |
| 287 | ), |
| 288 | array( |
| 289 | 'label' => __( 'Failed', 'tutor' ), |
| 290 | 'url' => UrlHelper::add_query_params( $filter_url, array( 'result' => QuizModel::RESULT_FAIL ) ), |
| 291 | 'count' => $failed_attempts, |
| 292 | 'active' => QuizModel::RESULT_FAIL === $result_filter, |
| 293 | ), |
| 294 | array( |
| 295 | 'label' => __( 'Passed', 'tutor' ), |
| 296 | 'url' => UrlHelper::add_query_params( $filter_url, array( 'result' => QuizModel::RESULT_PASS ) ), |
| 297 | 'count' => $passed_attempts, |
| 298 | 'active' => QuizModel::RESULT_PASS === $result_filter, |
| 299 | ), |
| 300 | ), |
| 301 | ); |
| 302 | |
| 303 | return $nav_links; |
| 304 | } |
| 305 | |
| 306 | /** |
| 307 | * Prepare bulk actions that will show on dropdown options |
| 308 | * |
| 309 | * @since 2.0.0 |
| 310 | * |
| 311 | * @return array |
| 312 | */ |
| 313 | public function prepare_bulk_actions(): array { |
| 314 | $actions = array( |
| 315 | $this->bulk_action_default(), |
| 316 | $this->bulk_action_delete(), |
| 317 | ); |
| 318 | return $actions; |
| 319 | } |
| 320 | |
| 321 | |
| 322 | /** |
| 323 | * Handle bulk action for instructor delete |
| 324 | * |
| 325 | * @since 2.0.0 |
| 326 | * |
| 327 | * @return void send wp_json response |
| 328 | */ |
| 329 | public function quiz_attempts_bulk_action() { |
| 330 | // check nonce. |
| 331 | tutor_utils()->checking_nonce(); |
| 332 | |
| 333 | // Check if user is privileged. |
| 334 | if ( ! User::has_any_role( array( User::ADMIN, User::INSTRUCTOR ) ) ) { |
| 335 | wp_send_json_error( tutor_utils()->error_message() ); |
| 336 | } |
| 337 | |
| 338 | $bulk_action = Input::post( 'bulk-action', '' ); |
| 339 | $bulk_ids = Input::post( 'bulk-ids', '' ); |
| 340 | $bulk_ids = explode( ',', $bulk_ids ); |
| 341 | $bulk_ids = array_map( |
| 342 | function ( $id ) { |
| 343 | return (int) trim( $id ); |
| 344 | }, |
| 345 | $bulk_ids |
| 346 | ); |
| 347 | |
| 348 | // prevent instructor to remove quiz attempt from admin. |
| 349 | $bulk_ids = array_filter( |
| 350 | $bulk_ids, |
| 351 | function ( $attempt_id ) { |
| 352 | $attempt = tutor_utils()->get_attempt( $attempt_id ); |
| 353 | $user_id = get_current_user_id(); |
| 354 | $course_id = $attempt && is_object( $attempt ) ? $attempt->course_id : 0; |
| 355 | return $course_id && tutor_utils()->can_user_edit_course( $user_id, $course_id ); |
| 356 | } |
| 357 | ); |
| 358 | |
| 359 | switch ( $bulk_action ) { |
| 360 | case 'delete': |
| 361 | QuizModel::delete_quiz_attempt( $bulk_ids ); |
| 362 | break; |
| 363 | } |
| 364 | |
| 365 | wp_send_json_success(); |
| 366 | } |
| 367 | |
| 368 | /** |
| 369 | * Get bulk action as an array |
| 370 | * |
| 371 | * @since 2.0.0 |
| 372 | * |
| 373 | * @return array |
| 374 | */ |
| 375 | public function get_bulk_actions() { |
| 376 | $actions = array( |
| 377 | 'delete' => 'Delete', |
| 378 | ); |
| 379 | return $actions; |
| 380 | } |
| 381 | |
| 382 | /** |
| 383 | * Check if attempt details are hidden. |
| 384 | * |
| 385 | * @since 4.0.0 |
| 386 | * |
| 387 | * @return bool |
| 388 | */ |
| 389 | public static function is_attempt_details_hidden(): bool { |
| 390 | $is_student_view = User::is_student_view(); |
| 391 | $is_quiz_details_hidden = $is_student_view && tutor_utils()->get_option( 'hide_quiz_details' ); |
| 392 | return $is_quiz_details_hidden; |
| 393 | } |
| 394 | |
| 395 | /** |
| 396 | * Get retry button attributes. |
| 397 | * |
| 398 | * @since 4.0.0 |
| 399 | * |
| 400 | * @param integer $quiz_id the quiz id. |
| 401 | * |
| 402 | * @return string |
| 403 | */ |
| 404 | private function get_retry_attribute( $quiz_id = 0 ): string { |
| 405 | $retry_attr = sprintf( |
| 406 | 'TutorCore.modal.showModal("tutor-retry-modal", { data: %s });', |
| 407 | wp_json_encode( |
| 408 | array( |
| 409 | 'quizID' => $quiz_id, |
| 410 | 'redirectURL' => get_post_permalink( $quiz_id ), |
| 411 | ) |
| 412 | ) |
| 413 | ); |
| 414 | |
| 415 | return $retry_attr; |
| 416 | } |
| 417 | |
| 418 | /** |
| 419 | * Get the quiz attempt review url. |
| 420 | * |
| 421 | * @since 4.0.0 |
| 422 | * |
| 423 | * @param array $attempt Quiz attempt. |
| 424 | * @param array $query_param Query param to add with the URL. |
| 425 | * |
| 426 | * @return string |
| 427 | */ |
| 428 | public function get_review_url( $attempt = array(), $query_param = array() ): string { |
| 429 | $default = array( 'attempt_id' => $attempt['attempt_id'] ?? 0 ); |
| 430 | $params = wp_parse_args( $query_param, $default ); |
| 431 | |
| 432 | return UrlHelper::add_query_params( get_pagenum_link(), $params ); |
| 433 | } |
| 434 | |
| 435 | /** |
| 436 | * Render student quiz attempt retry button. |
| 437 | * |
| 438 | * @since 4.0.0 |
| 439 | * |
| 440 | * @param integer $course_id the course id. |
| 441 | * @param integer $quiz_id the quiz id. |
| 442 | * @param array $attempt the quiz attempt. |
| 443 | * @param integer $attempts_count the quiz attempt count. |
| 444 | * |
| 445 | * @return void |
| 446 | */ |
| 447 | public function render_retry_button( $course_id = 0, $quiz_id = 0, $attempt = array(), $attempts_count = 0 ) { |
| 448 | $quiz_settings = tutor_utils()->get_quiz_option( $quiz_id, '', array() ); |
| 449 | $limit_attempts_allowed = '1' === (string) ( $quiz_settings['limit_attempts_allowed'] ?? '0' ); |
| 450 | $attempts_allowed = (int) ( $quiz_settings['attempts_allowed'] ?? 0 ); |
| 451 | $can_retry = Quiz::can_retry_quiz( $limit_attempts_allowed, $attempts_allowed, $attempts_count ); |
| 452 | |
| 453 | if ( User::is_student_view() && $can_retry ) { |
| 454 | Button::make() |
| 455 | ->label( __( 'Retry', 'tutor' ) ) |
| 456 | ->icon( Icon::RELOAD ) |
| 457 | ->size( Size::MEDIUM ) |
| 458 | ->variant( 'primary' ) |
| 459 | ->attr( '@click', $this->get_retry_attribute( $quiz_id ) ) |
| 460 | ->render(); |
| 461 | } |
| 462 | } |
| 463 | |
| 464 | /** |
| 465 | * Render quiz attempt details button. |
| 466 | * |
| 467 | * @since 4.0.0 |
| 468 | * |
| 469 | * @param array $attempt the quiz attempt. |
| 470 | * |
| 471 | * @return void |
| 472 | */ |
| 473 | public function render_details_button( $attempt ) { |
| 474 | if ( User::is_student_view() ) { |
| 475 | Button::make() |
| 476 | ->label( __( 'Details', 'tutor' ) ) |
| 477 | ->icon( Icon::RESOURCES, 'left', 20 ) |
| 478 | ->size( Size::MEDIUM ) |
| 479 | ->tag( 'a' ) |
| 480 | ->attr( 'href', $this->get_review_url( $attempt ) ) |
| 481 | ->variant( 'primary' ) |
| 482 | ->render(); |
| 483 | } |
| 484 | } |
| 485 | |
| 486 | /** |
| 487 | * Get kebab button for quiz attempt popover. |
| 488 | * |
| 489 | * @since 4.0.0 |
| 490 | * |
| 491 | * @param string $size the size of the button. |
| 492 | * |
| 493 | * @return string |
| 494 | */ |
| 495 | private function get_kebab_button( $size = Size::X_SMALL ) { |
| 496 | $kebab_button = Button::make() |
| 497 | ->label( __( 'More options', 'tutor' ) ) |
| 498 | ->icon( Icon::ELLIPSES ) |
| 499 | ->icon_only() |
| 500 | ->attr( 'x-ref', 'trigger' ) |
| 501 | ->attr( '@click', 'toggle()' ) |
| 502 | ->attr( 'class', 'tutor-quiz-item-result-more' ) |
| 503 | ->variant( Variant::SECONDARY ) |
| 504 | ->size( $size ) |
| 505 | ->get(); |
| 506 | return $kebab_button; |
| 507 | } |
| 508 | |
| 509 | /** |
| 510 | * Get quiz detail item for quiz attempt popover. |
| 511 | * |
| 512 | * @since 4.0.0 |
| 513 | * |
| 514 | * @param array $attempt the quiz attempt. |
| 515 | * |
| 516 | * @return array |
| 517 | */ |
| 518 | private function get_details_item( $attempt = array() ) { |
| 519 | $query_param = array( 'action' => 'view_details' ); |
| 520 | |
| 521 | $url = $this->get_review_url( $attempt, $query_param ); |
| 522 | |
| 523 | $details_item = array( |
| 524 | 'tag' => 'a', |
| 525 | 'content' => __( 'Details', 'tutor' ), |
| 526 | 'icon' => SvgIcon::make()->name( Icon::RESOURCES )->size( 20 )->get(), |
| 527 | 'attr' => array( 'href' => $url ), |
| 528 | ); |
| 529 | return $details_item; |
| 530 | } |
| 531 | |
| 532 | /** |
| 533 | * Render student quiz attempt popover. |
| 534 | * |
| 535 | * @since 4.0.0 |
| 536 | * |
| 537 | * @param array $attempt the quiz attempt. |
| 538 | * @param integer $attempts_count the quiz attempt count. |
| 539 | * @param integer $quiz_id the quiz id. |
| 540 | * @param bool $is_learning_area is learning area list item. |
| 541 | * @param bool $show_details whether to show details. |
| 542 | * |
| 543 | * @return void |
| 544 | */ |
| 545 | public function render_student_attempt_popover( $attempt = array(), $attempts_count = 0, $quiz_id = 0, $is_learning_area = false, $show_details = true ) { |
| 546 | $is_quiz_details_hidden = $this->is_attempt_details_hidden(); |
| 547 | $quiz_settings = tutor_utils()->get_quiz_option( $quiz_id, '', array() ); |
| 548 | $limit_attempts_allowed = '1' === (string) ( $quiz_settings['limit_attempts_allowed'] ?? '0' ); |
| 549 | $attempts_allowed = (int) ( $quiz_settings['attempts_allowed'] ?? 0 ); |
| 550 | |
| 551 | $can_retry = ! $is_learning_area && Quiz::can_retry_quiz( $limit_attempts_allowed, $attempts_allowed, $attempts_count ); |
| 552 | $show_retry = $can_retry && $attempts_count > 0; |
| 553 | |
| 554 | if ( ! $show_retry && $is_quiz_details_hidden ) { |
| 555 | return; |
| 556 | } |
| 557 | |
| 558 | if ( $is_learning_area && ! $is_quiz_details_hidden ) { |
| 559 | |
| 560 | $query_param = array( 'action' => 'view_details' ); |
| 561 | |
| 562 | $url = $this->get_review_url( $attempt, $query_param ); |
| 563 | |
| 564 | $button_html = Button::make() |
| 565 | ->tag( 'a' ) |
| 566 | ->label( __( 'Details', 'tutor' ) ) |
| 567 | ->size( Size::X_SMALL ) |
| 568 | ->variant( Variant::PRIMARY ) |
| 569 | ->attr( 'href', $url ) |
| 570 | ->attr( 'class', 'tutor-quiz-item-result-more tutor-quiz-details-btn' ) |
| 571 | ->get(); |
| 572 | |
| 573 | echo '<div class="tutor-flex">' . wp_kses_post( $button_html ) . '</div>'; |
| 574 | return; |
| 575 | } |
| 576 | |
| 577 | $popover = Popover::make() |
| 578 | ->trigger( $this->get_kebab_button( $show_details ? Size::X_SMALL : Size::MEDIUM ) ) |
| 579 | ->placement( 'bottom' ) |
| 580 | ->menu_min_width( '110px' ); |
| 581 | |
| 582 | if ( $show_retry ) { |
| 583 | $popover->menu_item( |
| 584 | array( |
| 585 | 'tag' => 'button', |
| 586 | 'content' => __( 'Retry', 'tutor' ), |
| 587 | 'icon' => SvgIcon::make()->name( Icon::RELOAD_3 )->size( 20 )->get(), |
| 588 | 'attr' => array( |
| 589 | '@click' => $this->get_retry_attribute( $quiz_id ), |
| 590 | ), |
| 591 | ) |
| 592 | ); |
| 593 | } |
| 594 | |
| 595 | if ( ! $is_quiz_details_hidden && $show_details ) { |
| 596 | $popover->menu_item( $this->get_details_item( $attempt ) ); |
| 597 | } |
| 598 | |
| 599 | $popover->render(); |
| 600 | } |
| 601 | |
| 602 | /** |
| 603 | * Render quiz attempt marks percentage. |
| 604 | * |
| 605 | * @since 4.0.0 |
| 606 | * |
| 607 | * @param string $attempt_result the quiz attempt `QuizModel::RESULT_PASS | QuizModel::RESULT_PENDING | QuizModel::RESULT_FAIL`. |
| 608 | * @param int $earned_percentage the earned percentage. |
| 609 | * @param string $size the size of the component. |
| 610 | * @param string $wrapper_class the wrapper class of the component. |
| 611 | * |
| 612 | * @return void |
| 613 | */ |
| 614 | public static function render_quiz_attempt_marks_percentage( $attempt_result = '', $earned_percentage = 0, $size = 'small', $wrapper_class = '' ) { |
| 615 | $statics_stroke_color = 'var(--tutor-icon-critical)'; |
| 616 | |
| 617 | if ( QuizModel::RESULT_PASS === $attempt_result ) { |
| 618 | $statics_stroke_color = 'var(--tutor-icon-success-secondary)'; |
| 619 | |
| 620 | if ( 100 === (int) $earned_percentage ) { |
| 621 | $statics_stroke_color = 'var(--tutor-icon-success-primary)'; |
| 622 | } |
| 623 | } elseif ( QuizModel::RESULT_PENDING === $attempt_result ) { |
| 624 | $statics_stroke_color = 'var(--tutor-icon-warning-secondary)'; |
| 625 | } |
| 626 | |
| 627 | Progress::make() |
| 628 | ->type( 'circle' ) |
| 629 | ->value( $earned_percentage ) |
| 630 | ->size( $size ) |
| 631 | ->stroke_color( 'var(--tutor-border-idle2)' ) |
| 632 | ->progress_stroke_color( $statics_stroke_color ) |
| 633 | ->animated() |
| 634 | ->attr( 'class', $wrapper_class ) |
| 635 | ->render(); |
| 636 | } |
| 637 | |
| 638 | /** |
| 639 | * Render List Badge for quiz attempts. |
| 640 | * |
| 641 | * @since 4.0.0 |
| 642 | * |
| 643 | * @param array $attempt the quiz attempt. |
| 644 | * |
| 645 | * @return void |
| 646 | */ |
| 647 | public function render_quiz_attempt_list_badge( $attempt = array() ) { |
| 648 | if ( QuizModel::RESULT_PASS === $attempt['result'] ) { |
| 649 | Badge::make()->label( __( 'Passed', 'tutor' ) )->variant( Badge::SUCCESS )->rounded()->render(); |
| 650 | } elseif ( QuizModel::RESULT_PENDING === $attempt['result'] ) { |
| 651 | Badge::make()->label( __( 'Pending', 'tutor' ) )->variant( Badge::WARNING )->rounded()->render(); |
| 652 | } else { |
| 653 | Badge::make()->label( 'Failed' )->variant( Badge::ERROR )->rounded()->render(); |
| 654 | } |
| 655 | } |
| 656 | |
| 657 | /** |
| 658 | * Render quiz attempt mobile view buttons. |
| 659 | * |
| 660 | * @since 4.0.0 |
| 661 | * |
| 662 | * @param array $attempt the quiz attempt. |
| 663 | * |
| 664 | * @return void |
| 665 | */ |
| 666 | public function render_quiz_attempt_buttons( $attempt = array() ) { |
| 667 | Button::make() |
| 668 | ->label( __( 'Details', 'tutor' ) ) |
| 669 | ->icon( Icon::RESOURCES, 'left', 20 ) |
| 670 | ->size( Size::MEDIUM ) |
| 671 | ->tag( 'a' ) |
| 672 | ->attr( 'href', $this->get_review_url( $attempt ) ) |
| 673 | ->variant( 'primary' ) |
| 674 | ->render(); |
| 675 | |
| 676 | Button::make() |
| 677 | ->label( __( 'Delete', 'tutor' ) ) |
| 678 | ->icon( Icon::DELETE_2, 'left', 20 ) |
| 679 | ->size( Size::MEDIUM ) |
| 680 | ->attr( '@click', sprintf( 'TutorCore.modal.showModal("tutor-quiz-attempt-delete-modal", { attemptID: %d });', $attempt['attempt_id'] ?? 0 ) ) |
| 681 | ->variant( 'secondary' ) |
| 682 | ->render(); |
| 683 | } |
| 684 | |
| 685 | /** |
| 686 | * Render quiz attempt popover for instructor quiz attempt list. |
| 687 | * |
| 688 | * @since 4.0.0 |
| 689 | * |
| 690 | * @param array $attempt the quiz attempt. |
| 691 | * |
| 692 | * @return void |
| 693 | */ |
| 694 | public function render_quiz_attempt_popover( $attempt = array() ) { |
| 695 | Popover::make() |
| 696 | ->trigger( $this->get_kebab_button() ) |
| 697 | ->placement( Positions::BOTTOM ) |
| 698 | ->menu_min_width( '110px' ) |
| 699 | ->menu_item( $this->get_details_item( $attempt ) ) |
| 700 | ->menu_item( |
| 701 | array( |
| 702 | 'tag' => 'button', |
| 703 | 'content' => __( 'Delete', 'tutor' ), |
| 704 | 'icon' => SvgIcon::make()->name( Icon::DELETE_2 )->size( 20 )->get(), |
| 705 | 'attr' => array( |
| 706 | '@click' => sprintf( |
| 707 | 'hide(); TutorCore.modal.showModal("tutor-quiz-attempt-delete-modal", { attemptID: %d });', |
| 708 | $attempt['attempt_id'] ?? 0 |
| 709 | ), |
| 710 | ), |
| 711 | ) |
| 712 | ) |
| 713 | ->render(); |
| 714 | } |
| 715 | } |
| 716 |