Merge pull request #4226 from crazyserver/MOBILE-4616

MOBILE-4616 quiz: Improve summary and navigation info to match LMS
main
Albert Gasset 2024-10-23 16:27:02 +02:00 committed by GitHub
commit c120c290f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 71 additions and 40 deletions

View File

@ -19,24 +19,24 @@
[detail]="false"> [detail]="false">
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
<span *ngIf="question.type !== 'description' && question.questionnumber"> <p class="item-heading" *ngIf="question.type !== 'description' && question.questionnumber">
{{ 'core.question.questionno' | translate:{$a: question.questionnumber} }} {{ 'core.question.questionno' | translate:{$a: question.questionnumber} }}
</span> </p>
<span *ngIf="question.type === 'description' || !question.questionnumber"> <p class="item-heading" *ngIf="question.type === 'description' || !question.questionnumber">
{{ 'core.question.information' | translate }} {{ 'core.question.information' | translate }}
</span> </p>
<p>{{ question.status }}</p>
</ion-label> </ion-label>
<ion-icon *ngIf="question.type === 'description' || !question.questionnumber" name="fas-circle-info" slot="end" <ion-icon *ngIf="question.type === 'description' || !question.questionnumber" name="fas-circle-info" slot="end"
aria-hidden="true" /> aria-hidden="true" />
<ion-icon *ngIf="question.stateClass === 'core-question-requiresgrading'" name="fas-circle-question" <ion-icon *ngIf="question.stateclass === 'requiresgrading'" name="fas-circle-question" aria-hidden="true" slot="end" />
[title]="question.status" slot="end" /> <ion-icon *ngIf="question.stateclass === 'correct'" [name]="correctIcon" color="success" aria-hidden="true" slot="end" />
<ion-icon *ngIf="question.stateClass === 'core-question-correct'" [name]="correctIcon" color="success" <ion-icon *ngIf="question.stateclass === 'partiallycorrect'" [name]="partialCorrectIcon" color="warning" aria-hidden="true"
[title]="question.status" slot="end" /> slot="end" />
<ion-icon *ngIf="question.stateClass === 'core-question-partiallycorrect'" [name]="partialCorrectIcon" color="warning" <ion-icon *ngIf="question.stateclass === 'incorrect' || question.stateclass === 'notanswered'" [name]="incorrectIcon"
[title]="question.status" slot="end" /> color="danger" aria-hidden="true" slot="end" />
<ion-icon *ngIf="question.stateClass === 'core-question-incorrect' || <ion-icon *ngIf="question.stateclass === 'invalidanswer'" name="fas-triangle-exclamation" color="danger" aria-hidden="true"
question.stateClass === 'core-question-notanswered'" [name]="incorrectIcon" color="danger" [title]="question.status"
slot="end" /> slot="end" />
</ion-item> </ion-item>
</ion-list> </ion-list>

View File

@ -0,0 +1,6 @@
ion-item.core-question-blocked,
ion-item.core-question-complete,
ion-item.core-question-answersaved,
ion-item.core-question-requiresgrading {
--background: var(--gray-300);
}

View File

@ -26,6 +26,7 @@ import { ModalController } from '@singletons';
@Component({ @Component({
selector: 'addon-mod-quiz-navigation-modal', selector: 'addon-mod-quiz-navigation-modal',
templateUrl: 'navigation-modal.html', templateUrl: 'navigation-modal.html',
styleUrl: 'navigation-modal.scss',
standalone: true, standalone: true,
imports: [ imports: [
CoreSharedModule, CoreSharedModule,

View File

@ -15,7 +15,8 @@
(click)="showConnectionError($event)" [ariaLabel]="'addon.mod_quiz.connectionerror' | translate" aria-haspopup="dialog"> (click)="showConnectionError($event)" [ariaLabel]="'addon.mod_quiz.connectionerror' | translate" aria-haspopup="dialog">
<ion-icon name="fas-circle-exclamation" slot="icon-only" aria-hidden="true" /> <ion-icon name="fas-circle-exclamation" slot="icon-only" aria-hidden="true" />
</ion-button> </ion-button>
<ion-button *ngIf="navigation.length" [ariaLabel]="'addon.mod_quiz.opentoc' | translate" (click)="openNavigation()"> <ion-button *ngIf="navigation.length && !showSummary" [ariaLabel]="'addon.mod_quiz.opentoc' | translate"
(click)="openNavigation()">
<ion-icon name="fas-bookmark" slot="icon-only" aria-hidden="true" /> <ion-icon name="fas-bookmark" slot="icon-only" aria-hidden="true" />
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
@ -72,11 +73,24 @@
<ng-container *ngFor="let question of summaryQuestions"> <ng-container *ngFor="let question of summaryQuestions">
<ion-item *ngIf="question.type !== 'description' && question.questionnumber" <ion-item *ngIf="question.type !== 'description' && question.questionnumber"
(click)="!isSequential && canReturn && changePage(question.page, false, question.slot)" (click)="!isSequential && canReturn && changePage(question.page, false, question.slot)"
[detail]="!isSequential && canReturn" [button]="!isSequential && canReturn" class="ion-text-wrap"> [detail]="!isSequential && canReturn" [button]="!isSequential && canReturn"
<ion-label> [class]="'ion-text-wrap ' + question.stateClass">
<span [attr.aria-label]="'core.question.questionno' | translate:{$a: question.questionnumber}"> <ion-label class="ion-text-wrap">
{{ question.questionnumber }}.</span> {{ question.status }} <p class="item-heading">
{{ 'core.question.questionno' | translate:{$a: question.questionnumber} }}
</p>
<p>{{ question.status }}</p>
</ion-label> </ion-label>
<ion-icon *ngIf="question.stateclass === 'requiresgrading'" name="fas-circle-question" aria-hidden="true" slot="end" />
<ion-icon *ngIf="question.stateclass === 'correct'" [name]="correctIcon" color="success" aria-hidden="true"
slot="end" />
<ion-icon *ngIf="question.stateclass === 'partiallycorrect'" [name]="partialCorrectIcon" color="warning"
aria-hidden="true" slot="end" />
<ion-icon *ngIf="question.stateclass === 'incorrect' || question.stateclass === 'notanswered'" [name]="incorrectIcon"
color="danger" aria-hidden="true" slot="end" />
<ion-icon *ngIf="question.stateclass === 'invalidanswer'" name="fas-triangle-exclamation" color="danger"
aria-hidden="true" slot="end" />
</ion-item> </ion-item>
</ng-container> </ng-container>

View File

@ -17,4 +17,11 @@ $quiz-timer-iterations: 15 !default;
} }
} }
} }
ion-item.core-question-blocked,
ion-item.core-question-complete,
ion-item.core-question-answersaved,
ion-item.core-question-requiresgrading {
--background: var(--gray-300);
}
} }

View File

@ -63,7 +63,7 @@ import { CoreLoadings } from '@services/loadings';
@Component({ @Component({
selector: 'page-addon-mod-quiz-player', selector: 'page-addon-mod-quiz-player',
templateUrl: 'player.html', templateUrl: 'player.html',
styleUrls: ['player.scss'], styleUrl: 'player.scss',
}) })
export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
@ -93,6 +93,9 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
dueDateWarning?: string; // Warning about due date. dueDateWarning?: string; // Warning about due date.
courseId!: number; // The course ID the quiz belongs to. courseId!: number; // The course ID the quiz belongs to.
cmId!: number; // Course module ID. cmId!: number; // Course module ID.
correctIcon = '';
incorrectIcon = '';
partialCorrectIcon = '';
protected preflightData: Record<string, string> = {}; // Preflight data to attempt the quiz. protected preflightData: Record<string, string> = {}; // Preflight data to attempt the quiz.
protected quizAccessInfo?: AddonModQuizGetQuizAccessInformationWSResponse; // Quiz access information. protected quizAccessInfo?: AddonModQuizGetQuizAccessInformationWSResponse; // Quiz access information.
@ -672,6 +675,12 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
return; return;
} }
if (!this.correctIcon) {
this.correctIcon = CoreQuestionHelper.getCorrectIcon().fullName;
this.incorrectIcon = CoreQuestionHelper.getIncorrectIcon().fullName;
this.partialCorrectIcon = CoreQuestionHelper.getPartiallyCorrectIcon().fullName;
}
this.summaryQuestions = []; this.summaryQuestions = [];
this.summaryQuestions = await AddonModQuiz.getAttemptSummary(this.attempt.id, this.preflightData, { this.summaryQuestions = await AddonModQuiz.getAttemptSummary(this.attempt.id, this.preflightData, {
@ -680,6 +689,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK, readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
}); });
this.summaryQuestions.forEach((question) => {
CoreQuestionHelper.populateQuestionStateClass(question);
});
this.showSummary = true; this.showSummary = true;
this.canReturn = this.attempt.state === AddonModQuizAttemptStates.IN_PROGRESS && !this.attempt.finishedOffline; this.canReturn = this.attempt.state === AddonModQuizAttemptStates.IN_PROGRESS && !this.attempt.finishedOffline;
this.preventSubmitMessages = AddonModQuiz.getPreventSubmitMessages(this.summaryQuestions); this.preventSubmitMessages = AddonModQuiz.getPreventSubmitMessages(this.summaryQuestions);
@ -707,7 +720,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
}); });
this.navigation.forEach((question) => { this.navigation.forEach((question) => {
question.stateClass = CoreQuestionHelper.getQuestionStateClass(question.state || ''); CoreQuestionHelper.populateQuestionStateClass(question);
}); });
} }

View File

@ -210,7 +210,7 @@ export class AddonModQuizReviewPage implements OnInit {
this.navigation = data.questions; this.navigation = data.questions;
this.navigation.forEach((question) => { this.navigation.forEach((question) => {
question.stateClass = CoreQuestionHelper.getQuestionStateClass(question.state || ''); CoreQuestionHelper.populateQuestionStateClass(question);
}); });
const lastQuestion = data.questions[data.questions.length - 1]; const lastQuestion = data.questions[data.questions.length - 1];

View File

@ -32,6 +32,7 @@ import { ContextLevel } from '@/core/constants';
import { CoreIonicColorNames } from '@singletons/colors'; import { CoreIonicColorNames } from '@singletons/colors';
import { CoreViewer } from '@features/viewer/services/viewer'; import { CoreViewer } from '@features/viewer/services/viewer';
import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
import { AddonModQuizNavigationQuestion } from '@addons/mod/quiz/components/navigation-modal/navigation-modal';
/** /**
* Service with some common functions to handle questions. * Service with some common functions to handle questions.
@ -476,15 +477,18 @@ export class CoreQuestionHelperProvider {
} }
/** /**
* Get the CSS class for a question based on its state. * Populates the CSS class for a question based on its state.
* *
* @param name Question's state name. * @param question Question.
* @returns State class.
*/ */
getQuestionStateClass(name: string): string { populateQuestionStateClass(question: AddonModQuizNavigationQuestion): void {
const state = CoreQuestion.getState(name); if (!question.stateclass) {
const state = CoreQuestion.getState(question.state);
return state ? state.class : ''; question.stateclass = state.stateclass;
}
question.stateClass = 'core-question-' + (question.stateclass ?? 'unknown');
} }
/** /**

View File

@ -42,7 +42,6 @@ const QUESTION_PREFIX_REGEX = /q\d+:(\d+)_/;
const STATES: Record<string, CoreQuestionState> = { const STATES: Record<string, CoreQuestionState> = {
todo: { todo: {
name: 'todo', name: 'todo',
class: 'core-question-notyetanswered',
status: 'notyetanswered', status: 'notyetanswered',
stateclass: 'notyetanswered', stateclass: 'notyetanswered',
active: true, active: true,
@ -50,7 +49,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
invalid: { invalid: {
name: 'invalid', name: 'invalid',
class: 'core-question-invalidanswer',
status: 'invalidanswer', status: 'invalidanswer',
stateclass: 'invalidanswer', stateclass: 'invalidanswer',
active: true, active: true,
@ -58,7 +56,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
complete: { complete: {
name: 'complete', name: 'complete',
class: 'core-question-answersaved',
status: 'answersaved', status: 'answersaved',
stateclass: 'answersaved', stateclass: 'answersaved',
active: true, active: true,
@ -66,7 +63,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
needsgrading: { needsgrading: {
name: 'needsgrading', name: 'needsgrading',
class: 'core-question-requiresgrading',
status: 'requiresgrading', status: 'requiresgrading',
stateclass: 'requiresgrading', stateclass: 'requiresgrading',
active: false, active: false,
@ -74,7 +70,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
finished: { finished: {
name: 'finished', name: 'finished',
class: 'core-question-complete',
status: 'complete', status: 'complete',
stateclass: 'complete', stateclass: 'complete',
active: false, active: false,
@ -82,7 +77,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
gaveup: { gaveup: {
name: 'gaveup', name: 'gaveup',
class: 'core-question-notanswered',
status: 'notanswered', status: 'notanswered',
stateclass: 'notanswered', stateclass: 'notanswered',
active: false, active: false,
@ -90,7 +84,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
gradedwrong: { gradedwrong: {
name: 'gradedwrong', name: 'gradedwrong',
class: 'core-question-incorrect',
status: 'incorrect', status: 'incorrect',
stateclass: 'incorrect', stateclass: 'incorrect',
active: false, active: false,
@ -98,7 +91,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
gradedpartial: { gradedpartial: {
name: 'gradedpartial', name: 'gradedpartial',
class: 'core-question-partiallycorrect',
status: 'partiallycorrect', status: 'partiallycorrect',
stateclass: 'partiallycorrect', stateclass: 'partiallycorrect',
active: false, active: false,
@ -106,7 +98,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
gradedright: { gradedright: {
name: 'gradedright', name: 'gradedright',
class: 'core-question-correct',
status: 'correct', status: 'correct',
stateclass: 'correct', stateclass: 'correct',
active: false, active: false,
@ -114,7 +105,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
mangrwrong: { mangrwrong: {
name: 'mangrwrong', name: 'mangrwrong',
class: 'core-question-incorrect',
status: 'incorrect', status: 'incorrect',
stateclass: 'incorrect', stateclass: 'incorrect',
active: false, active: false,
@ -122,7 +112,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
mangrpartial: { mangrpartial: {
name: 'mangrpartial', name: 'mangrpartial',
class: 'core-question-partiallycorrect',
status: 'partiallycorrect', status: 'partiallycorrect',
stateclass: 'partiallycorrect', stateclass: 'partiallycorrect',
active: false, active: false,
@ -130,7 +119,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
mangrright: { mangrright: {
name: 'mangrright', name: 'mangrright',
class: 'core-question-correct',
status: 'correct', status: 'correct',
stateclass: 'correct', stateclass: 'correct',
active: false, active: false,
@ -138,7 +126,6 @@ const STATES: Record<string, CoreQuestionState> = {
}, },
cannotdeterminestatus: { // Special state for Mobile, sometimes we won't have enough data to detemrine the state. cannotdeterminestatus: { // Special state for Mobile, sometimes we won't have enough data to detemrine the state.
name: 'cannotdeterminestatus', name: 'cannotdeterminestatus',
class: 'core-question-unknown',
status: 'cannotdeterminestatus', status: 'cannotdeterminestatus',
stateclass: undefined, stateclass: undefined,
active: true, active: true,
@ -594,7 +581,6 @@ export const CoreQuestion = makeSingleton(CoreQuestionProvider);
*/ */
export type CoreQuestionState = { export type CoreQuestionState = {
name: string; // Name of the state. name: string; // Name of the state.
class: string; // Class to style the state.
status: string; // The string key to translate the state. status: string; // The string key to translate the state.
stateclass: // A machine-readable class name for the state that this question attempt is in. stateclass: // A machine-readable class name for the state that this question attempt is in.
typeof QUESTION_TODO_STATE_CLASSES[number] | typeof QUESTION_TODO_STATE_CLASSES[number] |