File: //proc/thread-self/cwd/wp-content/plugins/tutor/classes/Quiz_Attempts_List.php
<?php
/**
* Quiz attempt list management
*
* @package Tutor\QuestionAnswer
* @author Themeum <support@themeum.com>
* @link https://themeum.com
* @since 1.0.0
*/
namespace TUTOR;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use TUTOR\User;
use Tutor\Cache\QuizAttempts;
use Tutor\Components\Badge;
use Tutor\Components\Button;
use Tutor\Components\Constants\Size;
use Tutor\Components\Constants\Positions;
use Tutor\Components\Constants\Variant;
use Tutor\Components\Popover;
use Tutor\Helpers\UrlHelper;
use Tutor\Models\QuizModel;
use Tutor\Components\SvgIcon;
use Tutor\Components\Progress;
/**
* Quiz attempt class
*
* @since 1.0.0
*/
class Quiz_Attempts_List {
const QUIZ_ATTEMPT_PAGE = 'tutor_quiz_attempts';
/**
* Trait for utilities
*
* @var $page_title
*/
use Backend_Page_Trait;
/**
* Bulk Action
*
* @var $bulk_action
*/
public $bulk_action = true;
/**
* Handle dependencies
*
* @since 1.0.0
*
* @param boolean $register_hook should register hook or not.
*/
public function __construct( $register_hook = true ) {
if ( ! $register_hook ) {
return;
}
/**
* Handle bulk action
*
* @since 2.0.0
*/
add_action( 'wp_ajax_tutor_quiz_attempts_bulk_action', array( $this, 'quiz_attempts_bulk_action' ) );
add_action( 'wp_ajax_tutor_quiz_attempts_count', array( $this, 'get_quiz_attempts_stat' ) );
/**
* Delete quiz attempt cache
*
* @since 2.1.0
*/
add_action( 'tutor_quiz/attempt_ended', array( new QuizAttempts(), 'delete_cache' ) );
add_action( 'tutor_quiz/attempt_deleted', array( new QuizAttempts(), 'delete_cache' ) );
add_action( 'tutor_quiz/answer/review/after', array( new QuizAttempts(), 'delete_cache' ) );
}
/**
* Page title fallback
*
* @since 3.5.0
*
* @param string $name Property name.
*
* @return string
*/
public function __get( $name ) {
if ( 'page_title' === $name ) {
return esc_html__( 'Quiz Attempts', 'tutor' );
}
}
/**
* Get the attempts stat from specific instructor context
*
* @since 2.0.0
* @since 3.8.0 refactor and query optimize.
*
* @return array
*/
public function get_quiz_attempts_stat() {
global $wpdb;
if ( wp_doing_ajax() ) {
tutor_utils()->checking_nonce();
}
$count_obj = (object) array(
'pass' => 0,
'fail' => 0,
'pending' => 0,
);
$is_ajax_action = 'tutor_quiz_attempts_count' === Input::post( 'action' );
$user_id = get_current_user_id();
$course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
$date = Input::post( 'date', '' );
$search = Input::post( 'search', '' );
if ( $is_ajax_action ) {
$current_params = compact( 'course_id', 'date', 'search' );
$attempt_cache = new QuizAttempts( $current_params );
$cached_attempts = $attempt_cache->get_cache();
if ( $attempt_cache->has_cache() && $attempt_cache->is_same_query() && isset( $cached_attempts['result'] ) ) {
$count_obj = $cached_attempts['result'];
} else {
$course_filter = $course_id ? $wpdb->prepare( ' AND quiz_attempts.course_id = %d', $course_id ) : '';
$date_filter = empty( $date ) ? '' : $wpdb->prepare( ' AND DATE(quiz_attempts.attempt_started_at) = %s ', $date );
$user_clause = User::is_admin() ? '' : $wpdb->prepare( ' AND quiz.post_author = %d', $user_id );
$search_term_raw = $search;
$search_filter = '%' . $wpdb->esc_like( $search ) . '%';
//phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT result, COUNT( DISTINCT attempt_id) AS total
FROM {$wpdb->prefix}tutor_quiz_attempts quiz_attempts
INNER JOIN {$wpdb->posts} quiz ON quiz_attempts.quiz_id = quiz.ID
INNER JOIN {$wpdb->users} AS users ON quiz_attempts.user_id = users.ID
INNER JOIN {$wpdb->posts} AS course ON course.ID = quiz_attempts.course_id
WHERE result IS NOT NULL
AND (
users.user_email = %s
OR users.display_name LIKE %s
OR quiz.post_title LIKE %s
OR course.post_title LIKE %s
)
{$user_clause}
{$course_filter}
{$date_filter}
GROUP BY result",
$search_term_raw,
$search_filter,
$search_filter,
$search_filter
)
);
//phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
foreach ( $results as $row ) {
if ( isset( $count_obj->{$row->result} ) ) {
$count_obj->{$row->result} = (int) $row->total;
}
}
$attempt_cache->data = $count_obj;
$attempt_cache->set_cache();
}
}
$all = $count_obj->pass + $count_obj->fail + $count_obj->pending;
$pass = $count_obj->pass;
$fail = $count_obj->fail;
$pending = $count_obj->pending;
$response = compact( 'all', 'pass', 'fail', 'pending' );
return $is_ajax_action ? wp_send_json_success( $response ) : $response;
}
/**
* Available tabs that will visible on the right side of page navbar
*
* @since 2.0.0
*
* @param string $user_id selected quiz_attempts id | optional.
* @param int $course_id selected quiz_attempts id | optional.
* @param string $date selected date | optional.
* @param string $search search by user name or email | optional.
*
* @return array
*/
public function tabs_key_value( $user_id, $course_id, $date, $search ): array {
$url = apply_filters( 'tutor_data_tab_base_url', get_pagenum_link() );
$stats = $this->get_quiz_attempts_stat();
$tabs = array(
array(
'key' => '',
'title' => __( 'All', 'tutor' ),
'value' => $stats['all'],
'url' => $url . '&data=all',
),
array(
'key' => 'pass',
'title' => __( 'Pass', 'tutor' ),
'value' => $stats['pass'],
'url' => $url . '&data=pass',
),
array(
'key' => 'fail',
'title' => __( 'Fail', 'tutor' ),
'value' => $stats['fail'],
'url' => $url . '&data=fail',
),
array(
'key' => 'pending',
'title' => __( 'Pending', 'tutor' ),
'value' => $stats['pending'],
'url' => $url . '&data=pending',
),
);
return $tabs;
}
/**
* Obtain nav data for quiz attempts.
*
* @since 4.0.0
*
* @param int $quiz_attempts_count the quiz attempt count.
* @param string $url the url.
* @param string $result_filter the current result state.
* @param string $search_filter the search filter.
* @param string $course_filter the course id filter.
* @param string $start_date range start date (Y-m-d).
* @param string $end_date range end date (Y-m-d).
* @param string $order_filter the order filter.
* @param array $all_quizzes all quiz data for student.
*
* @return array
*/
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 {
$quiz_model = new QuizModel();
if ( tutor_utils()->count( $all_quizzes ) ) {
$results = isset( $all_quizzes['results'] ) ? $all_quizzes['results'] : array();
$all_attempts = isset( $all_quizzes['total_count'] ) ? $all_quizzes['total_count'] : 0;
$quiz_attempts_count = $all_attempts;
$passed_attempts = count( $quiz_model->get_formatted_quiz_attempt_list_by_quiz_id( $results, QuizModel::RESULT_PASS ) );
$failed_attempts = count( $quiz_model->get_formatted_quiz_attempt_list_by_quiz_id( $results, QuizModel::RESULT_FAIL ) );
$pending_attempts = count( $quiz_model->get_formatted_quiz_attempt_list_by_quiz_id( $results, QuizModel::RESULT_PENDING ) );
} else {
$all_attempts = QuizModel::get_quiz_attempts( 0, 0, $search_filter, $course_filter > 0 ? $course_filter : '', $start_date, $end_date, $order_filter, '', true, true );
$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 );
$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 );
$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 );
}
$filter_url = remove_query_arg( 'current_page', $url );
$nav_links = array(
'type' => 'dropdown',
'active' => true,
'count' => $quiz_attempts_count,
'options' => array(
array(
'label' => __( 'All', 'tutor' ),
'count' => $all_attempts,
'url' => remove_query_arg( 'result', $filter_url ),
'active' => '' === $result_filter,
),
array(
'label' => __( 'Pending', 'tutor' ),
'url' => UrlHelper::add_query_params( $filter_url, array( 'result' => QuizModel::RESULT_PENDING ) ),
'count' => $pending_attempts,
'active' => QuizModel::RESULT_PENDING === $result_filter,
),
array(
'label' => __( 'Failed', 'tutor' ),
'url' => UrlHelper::add_query_params( $filter_url, array( 'result' => QuizModel::RESULT_FAIL ) ),
'count' => $failed_attempts,
'active' => QuizModel::RESULT_FAIL === $result_filter,
),
array(
'label' => __( 'Passed', 'tutor' ),
'url' => UrlHelper::add_query_params( $filter_url, array( 'result' => QuizModel::RESULT_PASS ) ),
'count' => $passed_attempts,
'active' => QuizModel::RESULT_PASS === $result_filter,
),
),
);
return $nav_links;
}
/**
* Prepare bulk actions that will show on dropdown options
*
* @since 2.0.0
*
* @return array
*/
public function prepare_bulk_actions(): array {
$actions = array(
$this->bulk_action_default(),
$this->bulk_action_delete(),
);
return $actions;
}
/**
* Handle bulk action for instructor delete
*
* @since 2.0.0
*
* @return void send wp_json response
*/
public function quiz_attempts_bulk_action() {
// check nonce.
tutor_utils()->checking_nonce();
// Check if user is privileged.
if ( ! User::has_any_role( array( User::ADMIN, User::INSTRUCTOR ) ) ) {
wp_send_json_error( tutor_utils()->error_message() );
}
$bulk_action = Input::post( 'bulk-action', '' );
$bulk_ids = Input::post( 'bulk-ids', '' );
$bulk_ids = explode( ',', $bulk_ids );
$bulk_ids = array_map(
function ( $id ) {
return (int) trim( $id );
},
$bulk_ids
);
// prevent instructor to remove quiz attempt from admin.
$bulk_ids = array_filter(
$bulk_ids,
function ( $attempt_id ) {
$attempt = tutor_utils()->get_attempt( $attempt_id );
$user_id = get_current_user_id();
$course_id = $attempt && is_object( $attempt ) ? $attempt->course_id : 0;
return $course_id && tutor_utils()->can_user_edit_course( $user_id, $course_id );
}
);
switch ( $bulk_action ) {
case 'delete':
QuizModel::delete_quiz_attempt( $bulk_ids );
break;
}
wp_send_json_success();
}
/**
* Get bulk action as an array
*
* @since 2.0.0
*
* @return array
*/
public function get_bulk_actions() {
$actions = array(
'delete' => 'Delete',
);
return $actions;
}
/**
* Check if attempt details are hidden.
*
* @since 4.0.0
*
* @return bool
*/
public static function is_attempt_details_hidden(): bool {
$is_student_view = User::is_student_view();
$is_quiz_details_hidden = $is_student_view && tutor_utils()->get_option( 'hide_quiz_details' );
return $is_quiz_details_hidden;
}
/**
* Get retry button attributes.
*
* @since 4.0.0
*
* @param integer $quiz_id the quiz id.
*
* @return string
*/
private function get_retry_attribute( $quiz_id = 0 ): string {
$retry_attr = sprintf(
'TutorCore.modal.showModal("tutor-retry-modal", { data: %s });',
wp_json_encode(
array(
'quizID' => $quiz_id,
'redirectURL' => get_post_permalink( $quiz_id ),
)
)
);
return $retry_attr;
}
/**
* Get the quiz attempt review url.
*
* @since 4.0.0
*
* @param array $attempt Quiz attempt.
* @param array $query_param Query param to add with the URL.
*
* @return string
*/
public function get_review_url( $attempt = array(), $query_param = array() ): string {
$default = array( 'attempt_id' => $attempt['attempt_id'] ?? 0 );
$params = wp_parse_args( $query_param, $default );
return UrlHelper::add_query_params( get_pagenum_link(), $params );
}
/**
* Render student quiz attempt retry button.
*
* @since 4.0.0
*
* @param integer $course_id the course id.
* @param integer $quiz_id the quiz id.
* @param array $attempt the quiz attempt.
* @param integer $attempts_count the quiz attempt count.
*
* @return void
*/
public function render_retry_button( $course_id = 0, $quiz_id = 0, $attempt = array(), $attempts_count = 0 ) {
$quiz_settings = tutor_utils()->get_quiz_option( $quiz_id, '', array() );
$limit_attempts_allowed = '1' === (string) ( $quiz_settings['limit_attempts_allowed'] ?? '0' );
$attempts_allowed = (int) ( $quiz_settings['attempts_allowed'] ?? 0 );
$can_retry = Quiz::can_retry_quiz( $limit_attempts_allowed, $attempts_allowed, $attempts_count );
if ( User::is_student_view() && $can_retry ) {
Button::make()
->label( __( 'Retry', 'tutor' ) )
->icon( Icon::RELOAD )
->size( Size::MEDIUM )
->variant( 'primary' )
->attr( '@click', $this->get_retry_attribute( $quiz_id ) )
->render();
}
}
/**
* Render quiz attempt details button.
*
* @since 4.0.0
*
* @param array $attempt the quiz attempt.
*
* @return void
*/
public function render_details_button( $attempt ) {
if ( User::is_student_view() ) {
Button::make()
->label( __( 'Details', 'tutor' ) )
->icon( Icon::RESOURCES, 'left', 20 )
->size( Size::MEDIUM )
->tag( 'a' )
->attr( 'href', $this->get_review_url( $attempt ) )
->variant( 'primary' )
->render();
}
}
/**
* Get kebab button for quiz attempt popover.
*
* @since 4.0.0
*
* @param string $size the size of the button.
*
* @return string
*/
private function get_kebab_button( $size = Size::X_SMALL ) {
$kebab_button = Button::make()
->label( __( 'More options', 'tutor' ) )
->icon( Icon::ELLIPSES )
->icon_only()
->attr( 'x-ref', 'trigger' )
->attr( '@click', 'toggle()' )
->attr( 'class', 'tutor-quiz-item-result-more' )
->variant( Variant::SECONDARY )
->size( $size )
->get();
return $kebab_button;
}
/**
* Get quiz detail item for quiz attempt popover.
*
* @since 4.0.0
*
* @param array $attempt the quiz attempt.
*
* @return array
*/
private function get_details_item( $attempt = array() ) {
$query_param = array( 'action' => 'view_details' );
$url = $this->get_review_url( $attempt, $query_param );
$details_item = array(
'tag' => 'a',
'content' => __( 'Details', 'tutor' ),
'icon' => SvgIcon::make()->name( Icon::RESOURCES )->size( 20 )->get(),
'attr' => array( 'href' => $url ),
);
return $details_item;
}
/**
* Render student quiz attempt popover.
*
* @since 4.0.0
*
* @param array $attempt the quiz attempt.
* @param integer $attempts_count the quiz attempt count.
* @param integer $quiz_id the quiz id.
* @param bool $is_learning_area is learning area list item.
* @param bool $show_details whether to show details.
*
* @return void
*/
public function render_student_attempt_popover( $attempt = array(), $attempts_count = 0, $quiz_id = 0, $is_learning_area = false, $show_details = true ) {
$is_quiz_details_hidden = $this->is_attempt_details_hidden();
$quiz_settings = tutor_utils()->get_quiz_option( $quiz_id, '', array() );
$limit_attempts_allowed = '1' === (string) ( $quiz_settings['limit_attempts_allowed'] ?? '0' );
$attempts_allowed = (int) ( $quiz_settings['attempts_allowed'] ?? 0 );
$can_retry = ! $is_learning_area && Quiz::can_retry_quiz( $limit_attempts_allowed, $attempts_allowed, $attempts_count );
$show_retry = $can_retry && $attempts_count > 0;
if ( ! $show_retry && $is_quiz_details_hidden ) {
return;
}
if ( $is_learning_area && ! $is_quiz_details_hidden ) {
$query_param = array( 'action' => 'view_details' );
$url = $this->get_review_url( $attempt, $query_param );
$button_html = Button::make()
->tag( 'a' )
->label( __( 'Details', 'tutor' ) )
->size( Size::X_SMALL )
->variant( Variant::PRIMARY )
->attr( 'href', $url )
->attr( 'class', 'tutor-quiz-item-result-more tutor-quiz-details-btn' )
->get();
echo '<div class="tutor-flex">' . wp_kses_post( $button_html ) . '</div>';
return;
}
$popover = Popover::make()
->trigger( $this->get_kebab_button( $show_details ? Size::X_SMALL : Size::MEDIUM ) )
->placement( 'bottom' )
->menu_min_width( '110px' );
if ( $show_retry ) {
$popover->menu_item(
array(
'tag' => 'button',
'content' => __( 'Retry', 'tutor' ),
'icon' => SvgIcon::make()->name( Icon::RELOAD_3 )->size( 20 )->get(),
'attr' => array(
'@click' => $this->get_retry_attribute( $quiz_id ),
),
)
);
}
if ( ! $is_quiz_details_hidden && $show_details ) {
$popover->menu_item( $this->get_details_item( $attempt ) );
}
$popover->render();
}
/**
* Render quiz attempt marks percentage.
*
* @since 4.0.0
*
* @param string $attempt_result the quiz attempt `QuizModel::RESULT_PASS | QuizModel::RESULT_PENDING | QuizModel::RESULT_FAIL`.
* @param int $earned_percentage the earned percentage.
* @param string $size the size of the component.
* @param string $wrapper_class the wrapper class of the component.
*
* @return void
*/
public static function render_quiz_attempt_marks_percentage( $attempt_result = '', $earned_percentage = 0, $size = 'small', $wrapper_class = '' ) {
$statics_stroke_color = 'var(--tutor-icon-critical)';
if ( QuizModel::RESULT_PASS === $attempt_result ) {
$statics_stroke_color = 'var(--tutor-icon-success-secondary)';
if ( 100 === (int) $earned_percentage ) {
$statics_stroke_color = 'var(--tutor-icon-success-primary)';
}
} elseif ( QuizModel::RESULT_PENDING === $attempt_result ) {
$statics_stroke_color = 'var(--tutor-icon-warning-secondary)';
}
Progress::make()
->type( 'circle' )
->value( $earned_percentage )
->size( $size )
->stroke_color( 'var(--tutor-border-idle2)' )
->progress_stroke_color( $statics_stroke_color )
->animated()
->attr( 'class', $wrapper_class )
->render();
}
/**
* Render List Badge for quiz attempts.
*
* @since 4.0.0
*
* @param array $attempt the quiz attempt.
*
* @return void
*/
public function render_quiz_attempt_list_badge( $attempt = array() ) {
if ( QuizModel::RESULT_PASS === $attempt['result'] ) {
Badge::make()->label( __( 'Passed', 'tutor' ) )->variant( Badge::SUCCESS )->rounded()->render();
} elseif ( QuizModel::RESULT_PENDING === $attempt['result'] ) {
Badge::make()->label( __( 'Pending', 'tutor' ) )->variant( Badge::WARNING )->rounded()->render();
} else {
Badge::make()->label( 'Failed' )->variant( Badge::ERROR )->rounded()->render();
}
}
/**
* Render quiz attempt mobile view buttons.
*
* @since 4.0.0
*
* @param array $attempt the quiz attempt.
*
* @return void
*/
public function render_quiz_attempt_buttons( $attempt = array() ) {
Button::make()
->label( __( 'Details', 'tutor' ) )
->icon( Icon::RESOURCES, 'left', 20 )
->size( Size::MEDIUM )
->tag( 'a' )
->attr( 'href', $this->get_review_url( $attempt ) )
->variant( 'primary' )
->render();
Button::make()
->label( __( 'Delete', 'tutor' ) )
->icon( Icon::DELETE_2, 'left', 20 )
->size( Size::MEDIUM )
->attr( '@click', sprintf( 'TutorCore.modal.showModal("tutor-quiz-attempt-delete-modal", { attemptID: %d });', $attempt['attempt_id'] ?? 0 ) )
->variant( 'secondary' )
->render();
}
/**
* Render quiz attempt popover for instructor quiz attempt list.
*
* @since 4.0.0
*
* @param array $attempt the quiz attempt.
*
* @return void
*/
public function render_quiz_attempt_popover( $attempt = array() ) {
Popover::make()
->trigger( $this->get_kebab_button() )
->placement( Positions::BOTTOM )
->menu_min_width( '110px' )
->menu_item( $this->get_details_item( $attempt ) )
->menu_item(
array(
'tag' => 'button',
'content' => __( 'Delete', 'tutor' ),
'icon' => SvgIcon::make()->name( Icon::DELETE_2 )->size( 20 )->get(),
'attr' => array(
'@click' => sprintf(
'hide(); TutorCore.modal.showModal("tutor-quiz-attempt-delete-modal", { attemptID: %d });',
$attempt['attempt_id'] ?? 0
),
),
)
)
->render();
}
}