Skip to content
2 changes: 1 addition & 1 deletion assets/core/ts/components/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ export const form = (config: FormControlConfig & { id?: string } = {}) => {
const isCheckbox = type === 'checkbox';
const isFile = type === 'file';

const defaultValue = this.values[name] ?? (isCheckbox ? element?.checked ?? false : '');
const defaultValue = this.values[name] ?? (isCheckbox ? (element?.checked ?? false) : '');

this.fields[name] = {
name,
Expand Down
2 changes: 1 addition & 1 deletion assets/src/js/frontend/dashboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const getCurrentPage = (): string => {
if (path.includes('/assignments')) {
return 'assignments';
}
if (path.includes('/quiz-attempts')) {
if (path.includes('/quiz-attempts') || path.includes('/my-quiz-attempts')) {
return 'quiz-attempts';
}
if (path.includes('/settings')) {
Expand Down
28 changes: 27 additions & 1 deletion assets/src/js/frontend/dashboard/pages/quiz-attempts.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
// Quiz Attempts Page
import { type MutationState } from '@Core/ts/services/Query';
import { tutorConfig } from '@TutorShared/config/config';
import { wpAjaxInstance } from '@TutorShared/utils/api';
import { convertToErrorMessage } from '@TutorShared/utils/util';
import axios from 'axios';

interface RetryAttempt {
quizID: string;
redirectURL: string;
}

const quizAttemptsPage = () => {
const query = window.TutorCore.query;

return {
query,
deleteMutation: null as MutationState<unknown, number> | null,
retryMutation: null as MutationState<unknown, RetryAttempt> | null,

init() {
this.deleteMutation = this.query.useMutation(this.deleteAttempt, {
Expand All @@ -16,7 +25,16 @@ const quizAttemptsPage = () => {
window.location.reload();
},
onError: (error: Error) => {
window.TutorCore.toast.error(error.message || 'Failed to delete quiz attempt');
window.TutorCore.toast.error(convertToErrorMessage(error));
},
});

this.retryMutation = this.query.useMutation(this.retryAttempt, {
onSuccess: (_, payload) => {
window.location.href = payload.redirectURL;
},
onError: (error: Error) => {
window.TutorCore.toast.error(convertToErrorMessage(error));
},
});
},
Expand All @@ -30,6 +48,14 @@ const quizAttemptsPage = () => {
async handleDeleteAttempt(attemptID: number) {
await this.deleteMutation?.mutate(attemptID);
},

retryAttempt(payload: RetryAttempt) {
return axios.postForm(payload.redirectURL, {
quiz_id: payload.quizID,
tutor_action: 'tutor_start_quiz',
_tutor_nonce: tutorConfig._tutor_nonce,
});
},
};
};

Expand Down
3 changes: 2 additions & 1 deletion assets/src/js/v3/shared/utils/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const endpoints = {
QUIZ_IMPORT_DATA: 'quiz_import_data',
QUIZ_EXPORT_DATA: 'quiz_export_data',
DELETE_QUIZ: 'tutor_quiz_delete',
START_QUIZ: 'tutor_start_quiz',

// ZOOM
GET_ZOOM_MEETING_DETAILS: 'tutor_zoom_meeting_details',
Expand Down Expand Up @@ -181,7 +182,7 @@ const endpoints = {
SAVE_BILLING_INFO: 'tutor_save_billing_info',
SAVE_WITHDRAW_METHOD: 'tutor_save_withdraw_account',
RESET_PASSWORD: 'tutor_profile_password_reset',
UPDATE_PROFILE_NOTIFICATION: 'tutor_save_notification_preference'
UPDATE_PROFILE_NOTIFICATION: 'tutor_save_notification_preference',
} as const;

export default endpoints;
9 changes: 8 additions & 1 deletion assets/src/scss/frontend/dashboard/_quiz-attempts.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
@use '@Core/scss/tokens' as *;
@use '@Core/scss/mixins' as *;

.tutor-h4 {
.tutor-quiz-attempts-mobile-heading {
@include tutor-breakpoint-up(sm) {
display: none;
}
Expand Down Expand Up @@ -197,6 +197,13 @@
justify-content: space-between;
}
}

.tutor-student-attempt-detail {
text-decoration: none;
@include tutor-breakpoint-up(sm) {
display: none;
}
}
}

.tutor-quiz-item-marks {
Expand Down
273 changes: 273 additions & 0 deletions classes/Quiz_Attempts_List.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
}

use Tutor\Cache\QuizAttempts;
use Tutor\Components\Badge;
use Tutor\Components\Button;
use Tutor\Components\Constants\Size;
use Tutor\Components\Popover;
use Tutor\Helpers\UrlHelper;
use Tutor\Models\QuizModel;

/**
Expand Down Expand Up @@ -350,4 +355,272 @@ public function get_bulk_actions() {
);
return $actions;
}

/**
* Check whether to show instructor or student quiz attempt.
*
* @since 4.0.0
*
* @param integer $course_id the course id.
*
* @return bool
*/
private function check_is_student( $course_id = 0 ): bool {
$is_student_view = User::VIEW_AS_STUDENT === User::get_current_view_mode();
$is_student = tutor_utils()->is_enrolled( $course_id, get_current_user_id(), false ) && $is_student_view;

return $is_student;
}

/**
* Get attempt row template for quiz attempts.
*
* @since 4.0.0
*
* @param integer $course_id the course id.
*
* @return string
*/
public function get_quiz_attempt_row_template( $course_id = 0 ): string {
$template = $this->check_is_student( $course_id ) ? 'dashboard.components.student-quiz-attempt-row'
: 'dashboard.components.quiz-attempt-row';
return $template;
}

/**
* 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 the quiz attempt.
*
* @return string
*/
public function get_review_url( $attempt = array() ): string {
return UrlHelper::add_query_params( get_pagenum_link(), array( 'view_quiz_attempt_id' => $attempt['attempt_id'] ?? 0 ) );
}

/**
* 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 ) {
if ( $this->check_is_student( $course_id ) && $this->should_retry( $attempt, $attempts_count ) ) {
Button::make()
->label( __( 'Retry', 'tutor' ) )
->icon( Icon::RELOAD )
->size( Size::MEDIUM )
->variant( 'primary' )
->attr( '@click', $this->get_retry_attribute( $quiz_id ) )
->render();
}
}

/**
* Whether student can retry attempt or not.
*
* @since 4.0.0
*
* @param array $attempt the quiz attempt.
* @param integer $attempts_count the quiz attempt count.
*
* @return boolean
*/
private function should_retry( $attempt = array(), $attempts_count = 0 ): bool {
$attempt_info = $attempt['attempt_info'] ?? array();

$should_retry = false;

if ( tutor_utils()->count( $attempt_info ) ) {
$allowed_attempts = (int) $attempt_info['attempts_allowed'] ?? 0;
$feedback_mode = $attempt_info['feedback_mode'] ?? '';
$should_retry = 'retry' === $feedback_mode && $attempts_count < $allowed_attempts;
}

return $should_retry;
}

/**
* Get kebab button for quiz attempt popover.
*
* @since 4.0.0
*
* @return string
*/
private function get_kebab_button() {
$kebab_button = Button::make()
->icon( Icon::THREE_DOTS_VERTICAL )
->attr( 'x-ref', 'trigger' )
->attr( '@click', 'toggle()' )
->attr( 'class', 'tutor-quiz-item-result-more' )
->variant( 'secondary' )
->size( Size::X_SMALL )
->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() ) {
$details_item = array(
'tag' => 'a',
'content' => __( 'Details', 'tutor' ),
'icon' => tutor_utils()->get_svg_icon( Icon::RESOURCES ),
'attr' => array( 'href' => $this->get_review_url( $attempt ) ),
);
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.
*
* @return void
*/
public function render_student_attempt_popover( $attempt = array(), $attempts_count = 0, $quiz_id = 0 ) {
// Only add retry option to the first attempt.
if ( ! $this->should_retry( $attempt, $attempts_count ) || ! $attempts_count ) {
Popover::make()
->trigger( $this->get_kebab_button() )
->placement( 'bottom' )
->menu_item( $this->get_details_item( $attempt ) )
->render();
} else {
Popover::make()
->trigger( $this->get_kebab_button() )
->placement( 'bottom' )
->menu_item(
array(
'tag' => 'button',
'content' => __( 'Retry', 'tutor' ),
'icon' => tutor_utils()->get_svg_icon( Icon::RELOAD ),
'attr' => array(
'@click' => $this->get_retry_attribute( $quiz_id ),
),
)
)
->menu_item( $this->get_details_item( $attempt ) )
->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, 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, 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( 'bottom' )
->menu_item( $this->get_details_item( $attempt ) )
->menu_item(
array(
'tag' => 'button',
'content' => __( 'Delete', 'tutor' ),
'icon' => tutor_utils()->get_svg_icon( Icon::DELETE_2 ),
'attr' => array(
'@click' => sprintf(
'hide(); TutorCore.modal.showModal("tutor-quiz-attempt-delete-modal", { attemptID: %d });',
$attempt['attempt_id'] ?? 0
),
),
)
)
->render();
}
}
Loading
Loading