Merge pull request #4226 from crazyserver/MOBILE-4616
MOBILE-4616 quiz: Improve summary and navigation info to match LMSmain
commit
c120c290f5
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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] |
|
||||||
|
|
Loading…
Reference in New Issue