Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assets/core/scss/themes/_dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@
--tutor-icon-exception1: #{$tutor-exception-1};
--tutor-icon-exception2: #{$tutor-exception-2};
--tutor-icon-success-primary: #{$tutor-success-600};
--tutor-icon-success-secondary: #{$tutor-success-500};
--tutor-icon-exception4: #{$tutor-warning-400};
--tutor-icon-exception5: #{$tutor-exception-5};
--tutor-icon-caution: #{$tutor-yellow-400};
--tutor-icon-critical: #{$tutor-error-600};
--tutor-icon-critical-hover: #{$tutor-error-700};
--tutor-icon-warning: #{$tutor-warning-600};
--tutor-icon-warning-secondary: #{$tutor-warning-400};

// =============================================================================
// BUTTON COLORS
Expand Down
2 changes: 2 additions & 0 deletions assets/core/scss/themes/_light.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@
--tutor-icon-exception1: #{$tutor-exception-1};
--tutor-icon-exception2: #{$tutor-exception-2};
--tutor-icon-success-primary: #{$tutor-success-700};
--tutor-icon-success-secondary: #{$tutor-success-600};
--tutor-icon-exception4: #{$tutor-warning-400};
--tutor-icon-exception5: #{$tutor-exception-5};
--tutor-icon-caution: #{$tutor-yellow-400};
--tutor-icon-critical: #{$tutor-error-600};
--tutor-icon-critical-hover: #{$tutor-error-700};
--tutor-icon-warning: #{$tutor-warning-700};
--tutor-icon-warning-secondary: #{$tutor-warning-400};

// =============================================================================
// BUTTON COLORS
Expand Down
1 change: 1 addition & 0 deletions assets/core/scss/tokens/_icons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ $tutor-icon-success-primary: var(--tutor-icon-success-primary);
$tutor-icon-critical: var(--tutor-icon-critical);
$tutor-icon-critical-hover: var(--tutor-icon-critical-hover);
$tutor-icon-warning: var(--tutor-icon-warning);
$tutor-icon-warning-secondary: var(--tutor-icon-warning-secondary);
$tutor-icon-caution: var(--tutor-icon-caution);
$tutor-icon-exception1: var(--tutor-icon-exception1);
$tutor-icon-exception2: var(--tutor-icon-exception2);
Expand Down
3 changes: 2 additions & 1 deletion assets/core/ts/components/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { tutorConfig } from '@TutorShared/config/config';
import { DateFormats } from '@TutorShared/config/constants';
import { type Calendar, type Options, Calendar as VanillaCalendar } from 'vanilla-calendar-pro';

// @ts-ignore
import 'vanilla-calendar-pro/styles/index.css';

const PRESETS = {
Expand Down Expand Up @@ -71,7 +72,7 @@ const TUTOR_CALENDAR_VALUES = {
apply: 'apply',
clear: 'clear',
calendarZIndex: '100001',
themeAttrDetect: 'body[data-tutor-theme]',
themeAttrDetect: '[data-tutor-theme]',
calendarClasses: 'vc tutor-vc-calendar',
} as const;

Expand Down
5 changes: 4 additions & 1 deletion assets/core/ts/components/statics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface StaticsProps {
size?: StaticsSize;
background?: string;
strokeColor?: string;
progressStrokeColor?: string;
showLabel?: boolean;
label?: string;
animated?: boolean;
Expand All @@ -28,6 +29,7 @@ const DEFAULT_CONFIG = {
size: 'small' as StaticsSize,
background: 'none',
strokeColor: 'var(--tutor-actions-brand-secondary)',
progressStrokeColor: 'var(--tutor-actions-brand-primary)',
showLabel: true,
label: '',
animated: false,
Expand All @@ -44,6 +46,7 @@ export const statics = (config: StaticsProps) => ({
type: config.type ?? DEFAULT_CONFIG.type,
background: config.background ?? DEFAULT_CONFIG.background,
strokeColor: config.strokeColor ?? DEFAULT_CONFIG.strokeColor,
progressStrokeColor: config.progressStrokeColor ?? DEFAULT_CONFIG.progressStrokeColor,
showLabel: config.showLabel ?? DEFAULT_CONFIG.showLabel,
label: config.label ?? DEFAULT_CONFIG.label,
animated: config.animated ?? DEFAULT_CONFIG.animated,
Expand Down Expand Up @@ -170,7 +173,7 @@ export const statics = (config: StaticsProps) => ({
cy="${this.center}"
r="${this.radius}"
fill="none"
stroke="var(--tutor-actions-brand-primary)"
stroke="${this.progressStrokeColor}"
stroke-width="${this.strokeWidth}"
stroke-linecap="round"
stroke-dasharray="${this.circumference}"
Expand Down
2 changes: 1 addition & 1 deletion assets/icons/github.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion assets/icons/wallet.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion assets/icons/x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions assets/src/js/lib/modules/quiz.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ window.jQuery(document).ready(($) => {
var $that = $(this);
var attempt_id = $that.attr('data-attempt-id');
var attempt_answer_id = $that.attr('data-attempt-answer-id');
var question_id = $that.attr('data-question-id');
var mark_as = $that.attr('data-mark-as');
var context = $that.attr('data-context');
var back_url = $that.attr('data-back-url');
Expand All @@ -23,6 +24,7 @@ window.jQuery(document).ready(($) => {
data: {
attempt_id,
attempt_answer_id,
question_id,
mark_as,
context,
back_url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,10 @@ const getPreviewFrameStyles = () => `
[data-question=fill_in_the_blank] .tutor-quiz-question-input {
box-shadow: none;
}
.tutor-quiz-question-option {
cursor: default;
}
`;

export default QuestionPreviewModal;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,12 +516,12 @@ const styles = {
}
`,
options: css`
${styleUtils.ulReset};
z-index: ${zIndex.dropdown};
background-color: ${colorTokens.background.white};
list-style-type: none;
box-shadow: ${shadow.popover};
margin: ${spacing[4]} 0;
margin: 0;
max-height: 400px;
border: 1px solid ${colorTokens.stroke.border};
border-radius: ${borderRadius[6]};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@
}
}

&-status {
@include tutor-flex(row, center, flex-start, wrap);
gap: $tutor-spacing-3;
}

&-divider {
width: 1px;
align-self: stretch;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

.tutor-preferences-setting-icon {
@include tutor-flex-center;
color: $tutor-icon-idle;
}

.tutor-preferences-setting-title {
Expand Down
4 changes: 4 additions & 0 deletions assets/src/scss/frontend/dashboard/settings/_profile.scss
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ body:has(#wpadminbar) {
border: 1px solid $tutor-border-idle;
overflow: hidden;

svg {
color: $tutor-icon-idle;
}

&:hover {
.tutor-profile-notification-toggle {
visibility: visible;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

&-icon {
position: relative;
color: $tutor-icon-idle;
top: 38px;
}
}
124 changes: 119 additions & 5 deletions classes/Quiz.php
Original file line number Diff line number Diff line change
Expand Up @@ -1088,13 +1088,18 @@ public function review_quiz_answer() {
$attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
$context = Input::post( 'context' );
$attempt_answer_id = Input::post( 'attempt_answer_id', 0, Input::TYPE_INT );
$question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
$mark_as = Input::post( 'mark_as' );

if ( ! tutor_utils()->can_user_manage( 'attempt', $attempt_id ) || ! tutor_utils()->can_user_manage( 'attempt_answer', $attempt_answer_id ) ) {
if ( ! tutor_utils()->can_user_manage( 'attempt', $attempt_id ) ) {
wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
}

$attempt_answer = $this->get_attempt_answer( $attempt_answer_id );
if ( $attempt_answer_id && ! tutor_utils()->can_user_manage( 'attempt_answer', $attempt_answer_id ) ) {
wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
}

$attempt_answer = $this->resolve_attempt_answer_for_review( $attempt_id, $attempt_answer_id, $question_id );
$review_data = $attempt_answer ? $this->apply_quiz_answer_review( $attempt_id, $attempt_answer, $mark_as ) : null;

if ( ! $review_data ) {
Expand Down Expand Up @@ -1171,11 +1176,11 @@ private function review_quiz_answers_bulk( int $attempt_id, array $review_status
$attempt_answer = $answers_by_question_id[ $question_id ] ?? null;

if ( ! $attempt_answer ) {
continue;
$attempt_answer = $this->resolve_attempt_answer_for_review( $attempt_id, 0, $question_id );
}

if ( ! tutor_utils()->can_user_manage( 'attempt_answer', $attempt_answer->attempt_answer_id ) ) {
$this->response_fail( __( 'Access Denied', 'tutor' ), 403 );
if ( ! $attempt_answer ) {
continue;
}

$this->apply_quiz_answer_review( $attempt_id, $attempt_answer, $mark_as );
Expand Down Expand Up @@ -1208,6 +1213,115 @@ private function get_attempt_answer( int $attempt_answer_id ) {
);
}

/**
* Get attempt answer by attempt and question IDs.
*
* @since 4.0.0
*
* @param int $attempt_id Attempt ID.
* @param int $question_id Question ID.
*
* @return object|null
*/
private function get_attempt_answer_by_attempt_and_question( int $attempt_id, int $question_id ) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a method for fetching attempt: tutor_utils()->get_attempt( $attempt_id )

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need get_attempt_answer_by_attempt_and_question() because creating a placeholder row is only correct when no answer row already exists for the same attempt_id + question_id. In the skipped legacy case there is no attempt_answer_id, so the review flow first has to look up by attempt_id and question_id: if a row exists we should update it, and if not we create the missing row and then apply the review. Without that lookup the fallback becomes a blind insert, which can create duplicate attempt-answer rows for questions that already have a saved record.

if ( $attempt_id <= 0 || $question_id <= 0 ) {
return null;
}

return QueryHelper::get_row(
'tutor_quiz_attempt_answers',
array(
'quiz_attempt_id' => $attempt_id,
'question_id' => $question_id,
),
'attempt_answer_id',
'ASC'
);
}

/**
* Create a placeholder attempt answer for a skipped question.
*
* @since 4.0.0
*
* @param int $attempt_id Attempt ID.
* @param int $question_id Question ID.
*
* @return object|null
*/
private function create_skipped_attempt_answer( int $attempt_id, int $question_id ) {
$attempt = tutor_utils()->get_attempt( $attempt_id );
if ( ! $attempt ) {
return null;
}

$question = QuizModel::get_quiz_question_by_id( $question_id );
if ( ! $question || (int) $question->quiz_id !== (int) $attempt->quiz_id ) {
return null;
}

$attempt_answer = $this->get_attempt_answer_by_attempt_and_question( $attempt_id, $question_id );
if ( $attempt_answer ) {
return $attempt_answer;
}

try {
$inserted_id = QueryHelper::insert(
'tutor_quiz_attempt_answers',
array(
'user_id' => (int) $attempt->user_id,
'quiz_id' => (int) $attempt->quiz_id,
'question_id' => $question_id,
'quiz_attempt_id' => $attempt_id,
'given_answer' => '',
'question_mark' => $question->question_mark,
'achieved_mark' => 0,
'minus_mark' => 0,
'is_correct' => 0,
)
);
} catch ( \Exception $exception ) {
return null;
}

if ( $inserted_id <= 0 ) {
return null;
}

return $this->get_attempt_answer( $inserted_id );
}

/**
* Resolve the attempt answer used for instructor review.
*
* @since 4.0.0
*
* @param int $attempt_id Attempt ID.
* @param int $attempt_answer_id Attempt answer ID.
* @param int $question_id Question ID.
*
* @return object|null
*/
private function resolve_attempt_answer_for_review( int $attempt_id, int $attempt_answer_id = 0, int $question_id = 0 ) {
$attempt_answer = $attempt_answer_id ? $this->get_attempt_answer( $attempt_answer_id ) : null;

if ( $attempt_answer ) {
return $attempt_answer;
}

if ( $question_id <= 0 ) {
return null;
}

$attempt_answer = $this->get_attempt_answer_by_attempt_and_question( $attempt_id, $question_id );

if ( $attempt_answer ) {
return $attempt_answer;
}

return $this->create_skipped_attempt_answer( $attempt_id, $question_id );
}

/**
* Apply quiz answer review update.
*
Expand Down
40 changes: 40 additions & 0 deletions classes/Quiz_Attempts_List.php
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,46 @@ public function render_student_attempt_popover( $attempt = array(), $attempts_co
}
}

/**
* 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)';
}

$config = array(
'value' => $earned_percentage,
'strokeColor' => 'var(--tutor-border-idle)',
'progressStrokeColor' => $statics_stroke_color,
'type' => 'progress',
'size' => $size,
'animated' => true,
);
?>
<div x-data="tutorStatics(<?php echo esc_attr( wp_json_encode( $config ) ); ?>)" class="<?php echo esc_attr( $wrapper_class ); ?>">
<div x-html="render()"></div>
</div>
<?php
}

/**
* Render List Badge for quiz attempts.
*
Expand Down
2 changes: 1 addition & 1 deletion components/Nav.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ class="tutor-nav-item %s">
%s
%s
</button>
<div x-ref="content" x-show="open" x-cloak @click.outside="handleClickOutside()" class="tutor-popover tutor-nav-dropdown">
<div x-ref="content" x-show="open" x-cloak @click.outside="handleClickOutside()" class="tutor-popover tutor-nav-dropdown" x-transition.origin.top.left>
%s
</div>
</div>',
Expand Down
Loading
Loading