MOBILE-3651 quiz: Implement attempt review page
parent
f682d89e67
commit
b405614cb6
|
@ -413,7 +413,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
try {
|
||||
await AddonModQuiz.instance.getAttemptReview(attemptId, { page: -1, cmId: this.module!.id });
|
||||
|
||||
// @todo this.navCtrl.push('AddonModQuizReviewPage', { courseId: this.courseId, quizId: quiz!.id, attemptId });
|
||||
CoreNavigator.instance.navigate(`../../review/${this.courseId}/${this.quiz!.id}/${attemptId}`);
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
|
|
|
@ -191,7 +191,7 @@ export class AddonModQuizAttemptPage implements OnInit {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
async reviewAttempt(): Promise<void> {
|
||||
// @todo navPush="AddonModQuizReviewPage" [navParams]="{courseId: courseId, quizId: quiz.id, attemptId: attempt.id}"
|
||||
CoreNavigator.instance.navigate(`../../../../review/${this.courseId}/${this.quiz!.id}/${this.attempt!.id}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'addon.mod_quiz.review' | translate }}</ion-title>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button *ngIf="navigation.length" [attr.aria-label]="'addon.mod_quiz.opentoc' | translate"
|
||||
(click)="openNavigation()">
|
||||
<ion-icon name="fas-bookmark" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
|
||||
<!-- Review summary -->
|
||||
<ion-card *ngIf="attempt">
|
||||
<ion-card-header class="ion-text-wrap">
|
||||
<ion-card-title>
|
||||
<span *ngIf="attempt.preview">{{ 'addon.mod_quiz.reviewofpreview' | translate }}</span>
|
||||
<span *ngIf="!attempt.preview">{{ 'addon.mod_quiz.reviewofattempt' | translate:{$a: attempt.attempt} }}</span>
|
||||
</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-list lines="none">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_quiz.startedon' | translate }}</h2>
|
||||
<p>{{ attempt.timestart! * 1000 | coreFormatDate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_quiz.attemptstate' | translate }}</h2>
|
||||
<p>{{ readableState }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="showCompleted">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_quiz.completedon' | translate }}</h2>
|
||||
<p>{{ attempt.timefinish! * 1000 | coreFormatDate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="timeTaken">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_quiz.timetaken' | translate }}</h2>
|
||||
<p>{{ timeTaken }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="overTime">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_quiz.overdue' | translate }}</h2>
|
||||
<p>{{ overTime }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="readableMark">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_quiz.marks' | translate }}</h2>
|
||||
<p>{{ readableMark }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="readableGrade">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_quiz.grade' | translate }}</h2>
|
||||
<p>{{ readableGrade }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let data of additionalData">
|
||||
<ion-label>
|
||||
<h2>{{ data.title }}</h2>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="data.content"
|
||||
contextLevel="module" [contextInstanceId]="quiz?.coursemodule" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-card>
|
||||
|
||||
<!-- Questions -->
|
||||
<div *ngIf="attempt && questions.length">
|
||||
<!-- Arrows to go to next/previous. -->
|
||||
<ng-container *ngTemplateOutlet="navArrows"></ng-container>
|
||||
|
||||
<!-- Questions. -->
|
||||
<div *ngFor="let question of questions">
|
||||
<ion-card id="addon-mod_quiz-question-{{question.slot}}">
|
||||
<!-- "Header" of the question. -->
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2 *ngIf="question.number">{{ 'core.question.questionno' | translate:{$a: question.number} }}</h2>
|
||||
<h2 *ngIf="!question.number">{{ 'core.question.information' | translate }}</h2>
|
||||
</ion-label>
|
||||
<div class="ion-text-wrap ion-margin-horizontal addon-mod_quiz-question-note" slot="end"
|
||||
*ngIf="question.status || question.readableMark">
|
||||
<p *ngIf="question.status">{{question.status}}</p>
|
||||
<p *ngIf="question.readableMark">{{question.readableMark}}</p>
|
||||
</div>
|
||||
</ion-item-divider>
|
||||
|
||||
<!-- Body of the question. -->
|
||||
<core-question class="ion-text-wrap" [question]="question" [component]="component" [componentId]="componentId"
|
||||
[attemptId]="attempt.id" [usageId]="attempt.uniqueid" [offlineEnabled]="false" contextLevel="module"
|
||||
[contextInstanceId]="quiz?.coursemodule" [courseId]="courseId" [review]="true"
|
||||
[preferredBehaviour]="quiz?.preferredbehaviour">
|
||||
</core-question>
|
||||
</ion-card>
|
||||
</div>
|
||||
|
||||
<!-- Arrows to go to next/previous. -->
|
||||
<ng-container *ngTemplateOutlet="navArrows"></ng-container>
|
||||
</div>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
||||
<!-- Arrows to go to next/previous. -->
|
||||
<ng-template #navArrows>
|
||||
<ion-grid>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col class="ion-text-start">
|
||||
<ion-button color="light" *ngIf="previousPage >= 0" (click)="changePage(previousPage)"
|
||||
[title]="'core.previous' | translate">
|
||||
<ion-icon name="fas-chevron-left" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-end">
|
||||
<ion-button color="light" *ngIf="nextPage >= -1" (click)="changePage(nextPage)" [title]="'core.next' | translate">
|
||||
<ion-icon name="fas-chevron-right" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-template>
|
|
@ -0,0 +1,40 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreQuestionComponentsModule } from '@features/question/components/components.module';
|
||||
import { AddonModQuizReviewPage } from './review';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AddonModQuizReviewPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
CoreQuestionComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonModQuizReviewPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AddonModQuizReviewPageModule {}
|
|
@ -0,0 +1,6 @@
|
|||
:host {
|
||||
.addon-mod_quiz-question-note p {
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,366 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { CoreQuestionQuestionParsed } from '@features/question/services/question';
|
||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
|
||||
import { IonContent, IonRefresher } from '@ionic/angular';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { ModalController, Translate } from '@singletons';
|
||||
import {
|
||||
AddonModQuizNavigationModalComponent,
|
||||
AddonModQuizNavigationQuestion,
|
||||
} from '../../components/navigation-modal/navigation-modal';
|
||||
import {
|
||||
AddonModQuiz,
|
||||
AddonModQuizAttemptWSData,
|
||||
AddonModQuizCombinedReviewOptions,
|
||||
AddonModQuizGetAttemptReviewResponse,
|
||||
AddonModQuizProvider,
|
||||
AddonModQuizQuizWSData,
|
||||
AddonModQuizWSAdditionalData,
|
||||
} from '../../services/quiz';
|
||||
import { AddonModQuizHelper } from '../../services/quiz-helper';
|
||||
|
||||
/**
|
||||
* Page that allows reviewing a quiz attempt.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-quiz-review',
|
||||
templateUrl: 'review.html',
|
||||
styleUrls: ['review.scss'],
|
||||
})
|
||||
export class AddonModQuizReviewPage implements OnInit {
|
||||
|
||||
@ViewChild(IonContent) content?: IonContent;
|
||||
|
||||
attempt?: AddonModQuizAttemptWSData; // The attempt being reviewed.
|
||||
component = AddonModQuizProvider.COMPONENT; // Component to link the files to.
|
||||
componentId?: number; // ID to use in conjunction with the component.
|
||||
showAll = false; // Whether to view all questions in the same page.
|
||||
numPages?: number; // Number of pages.
|
||||
showCompleted = false; // Whether to show completed time.
|
||||
additionalData?: AddonModQuizWSAdditionalData[]; // Additional data to display for the attempt.
|
||||
loaded = false; // Whether data has been loaded.
|
||||
navigation: AddonModQuizNavigationQuestion[] = []; // List of questions to navigate them.
|
||||
questions: QuizQuestion[] = []; // Questions of the current page.
|
||||
nextPage = -2; // Next page.
|
||||
previousPage = -2; // Previous page.
|
||||
readableState?: string;
|
||||
readableGrade?: string;
|
||||
readableMark?: string;
|
||||
timeTaken?: string;
|
||||
overTime?: string;
|
||||
quiz?: AddonModQuizQuizWSData; // The quiz the attempt belongs to.
|
||||
courseId!: number; // The course ID the quiz belongs to.
|
||||
|
||||
protected quizId!: number; // Quiz ID the attempt belongs to.
|
||||
protected attemptId!: number; // The attempt being reviewed.
|
||||
protected currentPage!: number; // The current page being reviewed.
|
||||
protected options?: AddonModQuizCombinedReviewOptions; // Review options.
|
||||
|
||||
constructor(
|
||||
protected elementRef: ElementRef,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.quizId = CoreNavigator.instance.getRouteNumberParam('quizId')!;
|
||||
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
|
||||
this.attemptId = CoreNavigator.instance.getRouteNumberParam('attemptId')!;
|
||||
this.currentPage = CoreNavigator.instance.getRouteNumberParam('page') || -1;
|
||||
this.showAll = this.currentPage == -1;
|
||||
|
||||
try {
|
||||
await this.fetchData();
|
||||
|
||||
CoreUtils.instance.ignoreErrors(
|
||||
AddonModQuiz.instance.logViewAttemptReview(this.attemptId, this.quizId, this.quiz!.name),
|
||||
);
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current page. If slot is supplied, try to scroll to that question.
|
||||
*
|
||||
* @param page Page to load. -1 means all questions in same page.
|
||||
* @param fromModal Whether the page was selected using the navigation modal.
|
||||
* @param slot Slot of the question to scroll to.
|
||||
*/
|
||||
async changePage(page: number, fromModal?: boolean, slot?: number): Promise<void> {
|
||||
if (typeof slot != 'undefined' && (this.attempt!.currentpage == -1 || page == this.currentPage)) {
|
||||
// Scrol to a certain question in the current page.
|
||||
this.scrollToQuestion(slot);
|
||||
|
||||
return;
|
||||
} else if (page == this.currentPage) {
|
||||
// If the user is navigating to the current page and no question specified, we do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded = false;
|
||||
this.content?.scrollToTop();
|
||||
|
||||
try {
|
||||
await this.loadPage(page);
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true);
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
|
||||
if (typeof slot != 'undefined') {
|
||||
// Scroll to the question. Give some time to the questions to render.
|
||||
setTimeout(() => {
|
||||
this.scrollToQuestion(slot);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get the quiz data.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchData(): Promise<void> {
|
||||
try {
|
||||
this.quiz = await AddonModQuiz.instance.getQuizById(this.courseId, this.quizId);
|
||||
|
||||
this.componentId = this.quiz.coursemodule;
|
||||
|
||||
this.options = await AddonModQuiz.instance.getCombinedReviewOptions(this.quizId, { cmId: this.quiz.coursemodule });
|
||||
|
||||
// Load the navigation data.
|
||||
await this.loadNavigation();
|
||||
|
||||
// Load questions.
|
||||
await this.loadPage(this.currentPage);
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a page questions.
|
||||
*
|
||||
* @param page The page to load.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadPage(page: number): Promise<void> {
|
||||
const data = await AddonModQuiz.instance.getAttemptReview(this.attemptId, { page, cmId: this.quiz!.coursemodule });
|
||||
|
||||
this.attempt = data.attempt;
|
||||
this.attempt.currentpage = page;
|
||||
this.currentPage = page;
|
||||
|
||||
// Set the summary data.
|
||||
this.setSummaryCalculatedData(data);
|
||||
|
||||
this.questions = data.questions;
|
||||
this.nextPage = page == -1 ? -2 : page + 1;
|
||||
this.previousPage = page - 1;
|
||||
|
||||
this.questions.forEach((question) => {
|
||||
// Get the readable mark for each question.
|
||||
question.readableMark = AddonModQuizHelper.instance.getQuestionMarkFromHtml(question.html);
|
||||
|
||||
// Extract the question info box.
|
||||
CoreQuestionHelper.instance.extractQuestionInfoBox(question, '.info');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data to navigate the questions using the navigation modal.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadNavigation(): Promise<void> {
|
||||
// Get all questions in single page to retrieve all the questions.
|
||||
const data = await AddonModQuiz.instance.getAttemptReview(this.attemptId, { page: -1, cmId: this.quiz!.coursemodule });
|
||||
|
||||
this.navigation = data.questions;
|
||||
|
||||
this.navigation.forEach((question) => {
|
||||
question.stateClass = CoreQuestionHelper.instance.getQuestionStateClass(question.state || '');
|
||||
});
|
||||
|
||||
const lastQuestion = data.questions[data.questions.length - 1];
|
||||
this.numPages = lastQuestion ? lastQuestion.page + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes data.
|
||||
*
|
||||
* @param refresher Refresher
|
||||
*/
|
||||
async refreshData(refresher: IonRefresher): Promise<void> {
|
||||
await CoreUtils.instance.ignoreErrors(Promise.all([
|
||||
AddonModQuiz.instance.invalidateQuizData(this.courseId),
|
||||
AddonModQuiz.instance.invalidateCombinedReviewOptionsForUser(this.quizId),
|
||||
AddonModQuiz.instance.invalidateAttemptReview(this.attemptId),
|
||||
]));
|
||||
|
||||
try {
|
||||
await this.fetchData();
|
||||
} finally {
|
||||
refresher.complete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to a certain question.
|
||||
*
|
||||
* @param slot Slot of the question to scroll to.
|
||||
*/
|
||||
protected scrollToQuestion(slot: number): void {
|
||||
CoreDomUtils.instance.scrollToElementBySelector(
|
||||
this.elementRef.nativeElement,
|
||||
this.content,
|
||||
`#addon-mod_quiz-question-${slot}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate review summary data.
|
||||
*
|
||||
* @param data Result of getAttemptReview.
|
||||
*/
|
||||
protected setSummaryCalculatedData(data: AddonModQuizGetAttemptReviewResponse): void {
|
||||
if (!this.attempt || !this.quiz) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.readableState = AddonModQuiz.instance.getAttemptReadableStateName(this.attempt!.state || '');
|
||||
|
||||
if (this.attempt.state != AddonModQuizProvider.ATTEMPT_FINISHED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showCompleted = true;
|
||||
this.additionalData = data.additionaldata;
|
||||
|
||||
const timeTaken = (this.attempt.timefinish || 0) - (this.attempt.timestart || 0);
|
||||
if (timeTaken > 0) {
|
||||
// Format time taken.
|
||||
this.timeTaken = CoreTimeUtils.instance.formatTime(timeTaken);
|
||||
|
||||
// Calculate overdue time.
|
||||
if (this.quiz.timelimit && timeTaken > this.quiz.timelimit + 60) {
|
||||
this.overTime = CoreTimeUtils.instance.formatTime(timeTaken - this.quiz.timelimit);
|
||||
}
|
||||
} else {
|
||||
this.timeTaken = undefined;
|
||||
}
|
||||
|
||||
// Treat grade.
|
||||
if (this.options!.someoptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX &&
|
||||
AddonModQuiz.instance.quizHasGrades(this.quiz)) {
|
||||
|
||||
if (data.grade === null || typeof data.grade == 'undefined') {
|
||||
this.readableGrade = AddonModQuiz.instance.formatGrade(data.grade, this.quiz.decimalpoints);
|
||||
} else {
|
||||
// Show raw marks only if they are different from the grade (like on the entry page).
|
||||
if (this.quiz.grade != this.quiz.sumgrades) {
|
||||
this.readableMark = Translate.instance.instant('addon.mod_quiz.outofshort', { $a: {
|
||||
grade: AddonModQuiz.instance.formatGrade(this.attempt.sumgrades, this.quiz.decimalpoints),
|
||||
maxgrade: AddonModQuiz.instance.formatGrade(this.quiz.sumgrades, this.quiz.decimalpoints),
|
||||
} });
|
||||
}
|
||||
|
||||
// Now the scaled grade.
|
||||
const gradeObject: Record<string, unknown> = {
|
||||
grade: AddonModQuiz.instance.formatGrade(Number(data.grade), this.quiz.decimalpoints),
|
||||
maxgrade: AddonModQuiz.instance.formatGrade(this.quiz.grade, this.quiz.decimalpoints),
|
||||
};
|
||||
|
||||
if (this.quiz.grade != 100) {
|
||||
gradeObject.percent = CoreTextUtils.instance.roundToDecimals(
|
||||
this.attempt.sumgrades! * 100 / this.quiz.sumgrades!,
|
||||
0,
|
||||
);
|
||||
this.readableGrade = Translate.instance.instant('addon.mod_quiz.outofpercent', { $a: gradeObject });
|
||||
} else {
|
||||
this.readableGrade = Translate.instance.instant('addon.mod_quiz.outof', { $a: gradeObject });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Treat additional data.
|
||||
this.additionalData.forEach((data) => {
|
||||
// Remove help links from additional data.
|
||||
data.content = CoreDomUtils.instance.removeElementFromHtml(data.content, '.helptooltip');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch mode: all questions in same page OR one page at a time.
|
||||
*/
|
||||
switchMode(): void {
|
||||
this.showAll = !this.showAll;
|
||||
|
||||
// Load all questions or first page, depending on the mode.
|
||||
this.loadPage(this.showAll ? -1 : 0);
|
||||
}
|
||||
|
||||
async openNavigation(): Promise<void> {
|
||||
// Create the navigation modal.
|
||||
const modal = await ModalController.instance.create({
|
||||
component: AddonModQuizNavigationModalComponent,
|
||||
componentProps: {
|
||||
navigation: this.navigation,
|
||||
summaryShown: false,
|
||||
currentPage: this.attempt?.currentpage,
|
||||
isReview: true,
|
||||
numPages: this.numPages,
|
||||
showAll: this.showAll,
|
||||
},
|
||||
cssClass: 'core-modal-lateral',
|
||||
showBackdrop: true,
|
||||
backdropDismiss: true,
|
||||
// @todo enterAnimation: 'core-modal-lateral-transition',
|
||||
// @todo leaveAnimation: 'core-modal-lateral-transition',
|
||||
});
|
||||
|
||||
await modal.present();
|
||||
|
||||
const result = await modal.onWillDismiss();
|
||||
|
||||
if (!result.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.data.action == AddonModQuizNavigationModalComponent.CHANGE_PAGE) {
|
||||
this.changePage(result.data.page, true, result.data.slot);
|
||||
} else if (result.data.action == AddonModQuizNavigationModalComponent.SWITCH_MODE) {
|
||||
this.switchMode();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Question with some calculated data for the view.
|
||||
*/
|
||||
type QuizQuestion = CoreQuestionQuestionParsed & {
|
||||
readableMark?: string;
|
||||
};
|
|
@ -28,6 +28,10 @@ const routes: Routes = [
|
|||
path: 'attempt/:courseId/:quizId/:attemptId',
|
||||
loadChildren: () => import('./pages/attempt/attempt.module').then( m => m.AddonModQuizAttemptPageModule),
|
||||
},
|
||||
{
|
||||
path: 'review/:courseId/:quizId/:attemptId',
|
||||
loadChildren: () => import('./pages/review/review.module').then( m => m.AddonModQuizReviewPageModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -423,15 +423,15 @@ export class CoreQuestionBaseComponent {
|
|||
// Check if question is marked as correct.
|
||||
if (input.classList.contains('incorrect')) {
|
||||
question.input.correctClass = 'core-question-incorrect';
|
||||
question.input.correctIcon = 'fa-remove';
|
||||
question.input.correctIcon = 'fas-times';
|
||||
question.input.correctIconColor = 'danger';
|
||||
} else if (input.classList.contains('correct')) {
|
||||
question.input.correctClass = 'core-question-correct';
|
||||
question.input.correctIcon = 'fa-check';
|
||||
question.input.correctIcon = 'fas-check';
|
||||
question.input.correctIconColor = 'success';
|
||||
} else if (input.classList.contains('partiallycorrect')) {
|
||||
question.input.correctClass = 'core-question-partiallycorrect';
|
||||
question.input.correctIcon = 'fa-check-square';
|
||||
question.input.correctIcon = 'fas-check-square';
|
||||
question.input.correctIconColor = 'warning';
|
||||
} else {
|
||||
question.input.correctClass = '';
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
$core-dd-question-colors: $white, $blue-light, #DCDCDC, #D8BFD8, #87CEFA, #DAA520, #FFD700, #F0E68C !default;
|
||||
|
||||
:host {
|
||||
:host ::ng-deep {
|
||||
--core-question-correct-color: var(--green-dark);
|
||||
--core-question-correct-color-bg: var(--green-light);
|
||||
--core-question-incorrect-color: var(--red);
|
||||
|
@ -22,64 +22,19 @@ $core-dd-question-colors: $white, $blue-light, #DCDCDC, #D8BFD8, #87CEFA, #DAA52
|
|||
|
||||
--core-dd-question-selected-shadow: 2px 2px 4px var(--gray-dark);
|
||||
|
||||
// .core-correct-icon {
|
||||
// padding: 0 ($content-padding / 2);
|
||||
// position: absolute;
|
||||
// @include position(null, 0, $content-padding / 2, null);
|
||||
// margin-top: 0;
|
||||
// margin-bottom: 0;
|
||||
// }
|
||||
.core-question-answer-correct {
|
||||
color: var(--core-question-correct-color);
|
||||
}
|
||||
|
||||
|
||||
// .core-question-answer-correct {
|
||||
// color: $core-question-correct-color;
|
||||
// }
|
||||
|
||||
// .core-question-answer-incorrect {
|
||||
// color: $core-question-incorrect-color;
|
||||
// }
|
||||
|
||||
// input, select {
|
||||
// &.core-question-answer-correct, &.core-question-answer-incorrect {
|
||||
// background-color: $gray-lighter;
|
||||
// color: $text-color;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .core-question-correct,
|
||||
// .core-question-comment {
|
||||
// color: $core-question-correct-color;
|
||||
// background-color: $core-question-correct-color-bg;
|
||||
|
||||
// .label, ion-label.label, .select-text, .select-icon .select-icon-inner {
|
||||
// color: $core-question-correct-color;
|
||||
// }
|
||||
// .radio-icon {
|
||||
// border-color: $core-question-correct-color;
|
||||
// }
|
||||
// .radio-inner {
|
||||
// background-color: $core-question-correct-color;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .core-question-incorrect {
|
||||
// color: $core-question-incorrect-color;
|
||||
// background-color: $core-question-incorrect-color-bg;
|
||||
|
||||
// .label, ion-label.label, .select-text, .select-icon .select-icon-inner {
|
||||
// color: $core-question-incorrect-color;
|
||||
// }
|
||||
// .radio-icon {
|
||||
// border-color: $core-question-incorrect-color;
|
||||
// }
|
||||
// .radio-inner {
|
||||
// background-color: $core-question-incorrect-color;
|
||||
// }
|
||||
// }
|
||||
.core-question-answer-incorrect {
|
||||
color: var(--core-question-incorrect-color);
|
||||
}
|
||||
|
||||
.core-question-feedback-container ::ng-deep {
|
||||
--color: var(--core-question-feedback-color);
|
||||
--background: var(--core-question-feedback-background-color);
|
||||
color: var(--core-question-feedback-color);
|
||||
background-color: var(--core-question-feedback-background-color);
|
||||
|
||||
.specificfeedback, .rightanswer, .im-feedback, .feedback, .generalfeedback {
|
||||
margin: 0 0 .5em;
|
||||
|
@ -100,7 +55,7 @@ $core-dd-question-colors: $white, $blue-light, #DCDCDC, #D8BFD8, #87CEFA, #DAA52
|
|||
background-color: var(--red);
|
||||
}
|
||||
&.correct {
|
||||
background-color: var(--green);
|
||||
background-color: var(--green);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,8 +70,11 @@ $core-dd-question-colors: $white, $blue-light, #DCDCDC, #D8BFD8, #87CEFA, #DAA52
|
|||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.core-question-correct {
|
||||
background-color: var(--core-question-state-correct-color);
|
||||
.core-question-correct,
|
||||
.core-question-comment {
|
||||
--background: var(--core-question-correct-color-bg);
|
||||
background-color: var(--core-question-correct-color-bg);
|
||||
color: var(--core-question-correct-color);
|
||||
}
|
||||
.core-question-partiallycorrect {
|
||||
background-color: var(--core-question-state-partial-color);
|
||||
|
@ -139,4 +97,10 @@ $core-dd-question-colors: $white, $blue-light, #DCDCDC, #D8BFD8, #87CEFA, #DAA52
|
|||
.fa.icon.questioncorrectnessicon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.item.item-interactive.item-interactive-disabled ::ng-deep {
|
||||
ion-label, ion-select, ion-checkbox {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue