MOBILE-2348 quiz: Implement review page
parent
29d96743c0
commit
0b50cdfc21
|
@ -12,12 +12,12 @@
|
|||
<nav>
|
||||
<ion-list>
|
||||
<!-- In player, show button to finish attempt. -->
|
||||
<a ion-item text-wrap *ngIf="!pageInstance.isReview" (click)="loadPage(-1)">
|
||||
<a ion-item text-wrap *ngIf="!isReview" (click)="loadPage(-1)">
|
||||
{{ 'addon.mod_quiz.finishattemptdots' | translate }}
|
||||
</a>
|
||||
|
||||
<!-- In review we can toggle between all questions in same page or one page at a time. -->
|
||||
<a ion-item text-wrap *ngIf="pageInstance.isReview && pageInstance.numPages > 1" (click)="switchMode()">
|
||||
<a ion-item text-wrap *ngIf="isReview && pageInstance.numPages > 1" (click)="switchMode()">
|
||||
<span *ngIf="!pageInstance.showAll">{{ 'addon.mod_quiz.showall' | translate }}</span>
|
||||
<span *ngIf="pageInstance.showAll">{{ 'addon.mod_quiz.showeachpage' | translate }}</span>
|
||||
</a>
|
||||
|
@ -27,14 +27,14 @@
|
|||
</a>
|
||||
|
||||
<!-- In player, show button to finish attempt. -->
|
||||
<a ion-item text-wrap *ngIf="!pageInstance.isReview" (click)="loadPage(-1)">
|
||||
<a ion-item text-wrap *ngIf="!isReview" (click)="loadPage(-1)">
|
||||
{{ 'addon.mod_quiz.finishattemptdots' | translate }}
|
||||
</a>
|
||||
|
||||
<!-- In review we can toggle between all questions in same page or one page at a time. -->
|
||||
<a ion-item text-wrap *ngIf="pageInstance.isReview && pageInstance.numPages > 1" (click)="switchMode()">
|
||||
<span ng-if="!pageInstance.showAll">{{ 'mma.mod_quiz.showall' | translate }}</span>
|
||||
<span ng-if="pageInstance.showAll">{{ 'mma.mod_quiz.showeachpage' | translate }}</span>
|
||||
<a ion-item text-wrap *ngIf="isReview && pageInstance.numPages > 1" (click)="switchMode()">
|
||||
<span *ngIf="!pageInstance.showAll">{{ 'addon.mod_quiz.showall' | translate }}</span>
|
||||
<span *ngIf="pageInstance.showAll">{{ 'addon.mod_quiz.showeachpage' | translate }}</span>
|
||||
</a>
|
||||
</ion-list>
|
||||
</nav>
|
||||
|
|
|
@ -34,7 +34,10 @@ export class AddonModQuizNavigationModalPage {
|
|||
*/
|
||||
pageInstance: any;
|
||||
|
||||
isReview: boolean; // Whether the user is reviewing the attempt.
|
||||
|
||||
constructor(params: NavParams, protected viewCtrl: ViewController) {
|
||||
this.isReview = !!params.get('isReview');
|
||||
this.pageInstance = params.get('page');
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core';
|
||||
import { IonicPage, NavParams, Content, PopoverController, ModalController, Modal, NavController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
|
@ -45,7 +45,6 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
|
|||
component = AddonModQuizProvider.COMPONENT; // Component to link the files to.
|
||||
loaded: boolean; // Whether data has been loaded.
|
||||
quizAborted: boolean; // Whether the quiz was aborted due to an error.
|
||||
preflightData: any = {}; // Preflight data to attempt the quiz.
|
||||
offline: boolean; // Whether the quiz is being attempted in offline mode.
|
||||
navigation: any[]; // List of questions to navigate them.
|
||||
questions: any[]; // Questions of the current page.
|
||||
|
@ -57,11 +56,11 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
|
|||
preventSubmitMessages: string[]; // List of messages explaining why the quiz cannot be submitted.
|
||||
endTime: number; // The time when the attempt must be finished.
|
||||
autoSaveError: boolean; // Whether there's been an error in auto-save.
|
||||
navigationModal: Modal; // Modal to navigate through the questions.
|
||||
|
||||
protected element: HTMLElement; // Host element of the page.
|
||||
protected courseId: number; // The course ID the quiz belongs to.
|
||||
protected quizId: number; // Quiz ID to attempt.
|
||||
protected isTimed: boolean; // Whether the quiz has a time limit.
|
||||
protected preflightData: any = {}; // Preflight data to attempt the quiz.
|
||||
protected quizAccessInfo: any; // Quiz access information.
|
||||
protected attemptAccessInfo: any; // Attempt access info.
|
||||
protected lastAttempt: any; // Last user attempt before a new one is created (if needed).
|
||||
|
@ -70,16 +69,15 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
|
|||
protected timeUpCalled: boolean; // Whether the time up function has been called.
|
||||
protected autoSave: AddonModQuizAutoSave; // Class to auto-save answers every certain time.
|
||||
protected autoSaveErrorSubscription: Subscription; // To be notified when an error happens in auto-save.
|
||||
protected navigationModal: Modal; // Modal to navigate through the questions.
|
||||
protected forceLeave = false; // If true, don't perform any check when leaving the view.
|
||||
|
||||
constructor(navParams: NavParams, element: ElementRef, logger: CoreLoggerProvider, protected translate: TranslateService,
|
||||
constructor(navParams: NavParams, logger: CoreLoggerProvider, protected translate: TranslateService,
|
||||
protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider,
|
||||
protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, popoverCtrl: PopoverController,
|
||||
protected timeUtils: CoreTimeUtilsProvider, protected quizProvider: AddonModQuizProvider,
|
||||
protected quizHelper: AddonModQuizHelperProvider, protected quizSync: AddonModQuizSyncProvider,
|
||||
protected questionHelper: CoreQuestionHelperProvider, protected cdr: ChangeDetectorRef,
|
||||
protected modalCtrl: ModalController, protected navCtrl: NavController) {
|
||||
modalCtrl: ModalController, protected navCtrl: NavController) {
|
||||
|
||||
this.quizId = navParams.get('quizId');
|
||||
this.courseId = navParams.get('courseId');
|
||||
|
@ -93,7 +91,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
|
|||
logger, popoverCtrl, questionHelper, quizProvider);
|
||||
|
||||
// Create the navigation modal.
|
||||
this.navigationModal = this.modalCtrl.create('AddonModQuizNavigationModalPage', {
|
||||
this.navigationModal = modalCtrl.create('AddonModQuizNavigationModalPage', {
|
||||
page: this
|
||||
});
|
||||
}
|
||||
|
@ -288,7 +286,6 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
|
|||
this.offline = offlineMode;
|
||||
|
||||
if (this.quiz.timelimit > 0) {
|
||||
this.isTimed = true;
|
||||
this.quiz.readableTimeLimit = this.timeUtils.formatTime(this.quiz.timelimit);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title>{{ 'addon.mod_quiz.review' | translate }}</ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<button *ngIf="navigation && navigation.length" ion-button icon-only [attr.aria-label]="'addon.mod_quiz.opentoc' | translate" (click)="navigationModal.present()">
|
||||
<ion-icon name="bookmark"></ion-icon>
|
||||
</button>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshData($event)">
|
||||
<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 text-wrap>
|
||||
<h2 *ngIf="attempt.preview">{{ 'addon.mod_quiz.reviewofpreview' | translate }}</h2>
|
||||
<h2 *ngIf="!attempt.preview">{{ 'addon.mod_quiz.reviewofattempt' | translate:{$a: attempt.attempt} }}</h2>
|
||||
</ion-card-header>
|
||||
<ion-list>
|
||||
<ion-item text-wrap>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.startedon' | translate }}</p>
|
||||
<p>{{ attempt.timestart * 1000 | coreFormatDate:"dfmediumdate" }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.attemptstate' | translate }}</p>
|
||||
<p>{{ attempt.readableState }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="showCompleted">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.completedon' | translate }}</p>
|
||||
<p>{{ attempt.timefinish * 1000 | coreFormatDate:"dfmediumdate" }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="attempt.timeTaken">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.timetaken' | translate }}</p>
|
||||
<p>{{ attempt.timeTaken }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="attempt.overTime">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.overdue' | translate }}</p>
|
||||
<p>{{ attempt.overTime }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="attempt.readableMark">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.marks' | translate }}</p>
|
||||
<p><core-format-text [text]="attempt.readableMark"></core-format-text></p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="attempt.readableGrade">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.grade' | translate }}</p>
|
||||
<p>{{ attempt.readableGrade }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngFor="let data of additionalData">
|
||||
<p class="item-heading">{{ data.title }}</p>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="data.content"></core-format-text>
|
||||
</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 color="light">
|
||||
<h2 *ngIf="question.number" class="inline">{{ 'core.question.questionno' | translate:{$a: question.number} }}</h2>
|
||||
<h2 *ngIf="!question.number" class="inline">{{ 'core.question.information' | translate }}</h2>
|
||||
<ion-note text-wrap item-end *ngIf="question.status || question.readableMark">
|
||||
<p *ngIf="question.status" class="block">{{question.status}}</p>
|
||||
<p *ngIf="question.readableMark"><core-format-text [text]="question.readableMark"></core-format-text></p>
|
||||
</ion-note>
|
||||
</ion-item-divider>
|
||||
<!-- Body of the question. -->
|
||||
<core-question text-wrap [question]="question" [component]="component" [componentId]="componentId" [attemptId]="attempt.id" [offlineEnabled]="false"></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-row align-items-center>
|
||||
<ion-col>
|
||||
<a ion-button icon-only color="light" *ngIf="previousPage >= 0" (click)="changePage(previousPage)" [title]="'core.previous' | translate">
|
||||
<ion-icon name="arrow-back" md="ios-arrow-back"></ion-icon>
|
||||
</a>
|
||||
</ion-col>
|
||||
<ion-col text-right>
|
||||
<a ion-button icon-only color="light" *ngIf="nextPage >= -1" (click)="changePage(nextPage)" [title]="'core.next' | translate">
|
||||
<ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon>
|
||||
</a>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ng-template>
|
|
@ -0,0 +1,37 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
import { CoreQuestionComponentsModule } from '@core/question/components/components.module';
|
||||
import { AddonModQuizReviewPage } from './review';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizReviewPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CorePipesModule,
|
||||
CoreQuestionComponentsModule,
|
||||
IonicPageModule.forChild(AddonModQuizReviewPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModQuizReviewPageModule {}
|
|
@ -0,0 +1,11 @@
|
|||
page-addon-mod-quiz-review {
|
||||
.item-radio-disabled,
|
||||
.item-checkbox-disabled,
|
||||
.text-input[disabled] {
|
||||
opacity: 1;
|
||||
|
||||
.label, .radio, .checkbox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,295 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, OnInit, ViewChild } from '@angular/core';
|
||||
import { IonicPage, NavParams, Content, ModalController, Modal } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
|
||||
import { AddonModQuizProvider } from '../../providers/quiz';
|
||||
import { AddonModQuizHelperProvider } from '../../providers/helper';
|
||||
|
||||
/**
|
||||
* Page that allows reviewing a quiz attempt.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-quiz-review' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-quiz-review',
|
||||
templateUrl: 'review.html',
|
||||
})
|
||||
export class AddonModQuizReviewPage implements OnInit {
|
||||
@ViewChild(Content) content: Content;
|
||||
|
||||
attempt: any; // The attempt being reviewed.
|
||||
component = AddonModQuizProvider.COMPONENT; // Component to link the files to.
|
||||
componentId: number; // ID to use in conjunction with the component.
|
||||
showAll: boolean; // Whether to view all questions in the same page.
|
||||
numPages: number; // Number of pages.
|
||||
showCompleted: boolean; // Whether to show completed time.
|
||||
additionalData: any[]; // Additional data to display for the attempt.
|
||||
loaded: boolean; // Whether data has been loaded.
|
||||
navigation: any[]; // List of questions to navigate them.
|
||||
questions: any[]; // Questions of the current page.
|
||||
nextPage: number; // Next page.
|
||||
previousPage: number; // Previous page.
|
||||
navigationModal: Modal; // Modal to navigate through the questions.
|
||||
|
||||
protected quiz: any; // The quiz the attempt belongs to.
|
||||
protected 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: any; // Review options.
|
||||
|
||||
constructor(navParams: NavParams, modalCtrl: ModalController, protected translate: TranslateService,
|
||||
protected domUtils: CoreDomUtilsProvider, protected timeUtils: CoreTimeUtilsProvider,
|
||||
protected quizProvider: AddonModQuizProvider, protected quizHelper: AddonModQuizHelperProvider,
|
||||
protected questionHelper: CoreQuestionHelperProvider, protected textUtils: CoreTextUtilsProvider) {
|
||||
|
||||
this.quizId = navParams.get('quizId');
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.attemptId = navParams.get('attemptId');
|
||||
this.currentPage = navParams.get('page') || -1;
|
||||
this.showAll = this.currentPage == -1;
|
||||
|
||||
// Create the navigation modal.
|
||||
this.navigationModal = modalCtrl.create('AddonModQuizNavigationModalPage', {
|
||||
isReview: true,
|
||||
page: this
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.fetchData().then(() => {
|
||||
this.quizProvider.logViewAttemptReview(this.attemptId);
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current page. If slot is supplied, try to scroll to that question.
|
||||
*
|
||||
* @param {number} page Page to load. -1 means all questions in same page.
|
||||
* @param {boolean} [fromModal] Whether the page was selected using the navigation modal.
|
||||
* @param {number} [slot] Slot of the question to scroll to.
|
||||
*/
|
||||
changePage(page: number, fromModal?: boolean, slot?: number): 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();
|
||||
|
||||
this.loadPage(page).catch((error) => {
|
||||
this.domUtils.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<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchData(): Promise<any> {
|
||||
return this.quizProvider.getQuizById(this.courseId, this.quizId).then((quizData) => {
|
||||
this.quiz = quizData;
|
||||
this.componentId = this.quiz.coursemodule;
|
||||
|
||||
return this.quizProvider.getCombinedReviewOptions(this.quizId).then((result) => {
|
||||
this.options = result;
|
||||
|
||||
// Load the navigation data.
|
||||
return this.loadNavigation().then(() => {
|
||||
// Load questions.
|
||||
return this.loadPage(this.currentPage);
|
||||
});
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a page questions.
|
||||
*
|
||||
* @param {number} page The page to load.
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected loadPage(page: number): Promise<void> {
|
||||
return this.quizProvider.getAttemptReview(this.attemptId, page).then((data) => {
|
||||
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 ? undefined : page + 1;
|
||||
this.previousPage = page - 1;
|
||||
|
||||
this.questions.forEach((question) => {
|
||||
// Get the readable mark for each question.
|
||||
question.readableMark = this.quizHelper.getQuestionMarkFromHtml(question.html);
|
||||
|
||||
// Extract the question info box.
|
||||
this.questionHelper.extractQuestionInfoBox(question, '.info');
|
||||
|
||||
// Set the preferred behaviour.
|
||||
question.preferredBehaviour = this.quiz.preferredbehaviour;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data to navigate the questions using the navigation modal.
|
||||
*
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected loadNavigation(): Promise<void> {
|
||||
// Get all questions in single page to retrieve all the questions.
|
||||
return this.quizProvider.getAttemptReview(this.attemptId, -1).then((data) => {
|
||||
const lastQuestion = data.questions[data.questions.length - 1];
|
||||
|
||||
data.questions.forEach((question) => {
|
||||
question.stateClass = this.questionHelper.getQuestionStateClass(question.state);
|
||||
});
|
||||
|
||||
this.navigation = data.questions;
|
||||
this.numPages = lastQuestion ? lastQuestion.page + 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes data.
|
||||
*
|
||||
* @param {any} refresher Refresher
|
||||
*/
|
||||
refreshData(refresher: any): void {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.quizProvider.invalidateQuizData(this.courseId));
|
||||
promises.push(this.quizProvider.invalidateCombinedReviewOptionsForUser(this.quizId));
|
||||
promises.push(this.quizProvider.invalidateAttemptReview(this.attemptId));
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
return this.fetchData();
|
||||
}).finally(() => {
|
||||
refresher.complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to a certain question.
|
||||
*
|
||||
* @param {number} slot Slot of the question to scroll to.
|
||||
*/
|
||||
protected scrollToQuestion(slot: number): void {
|
||||
this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_quiz-question-' + slot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate review summary data.
|
||||
*
|
||||
* @param {any} data Result of getAttemptReview.
|
||||
*/
|
||||
protected setSummaryCalculatedData(data: any): void {
|
||||
|
||||
this.attempt.readableState = this.quizProvider.getAttemptReadableStateName(this.attempt.state);
|
||||
|
||||
if (this.attempt.state == AddonModQuizProvider.ATTEMPT_FINISHED) {
|
||||
this.showCompleted = true;
|
||||
this.additionalData = data.additionaldata;
|
||||
|
||||
const timeTaken = this.attempt.timefinish - this.attempt.timestart;
|
||||
if (timeTaken) {
|
||||
// Format time taken.
|
||||
this.attempt.timeTaken = this.timeUtils.formatTime(timeTaken);
|
||||
|
||||
// Calculate overdue time.
|
||||
if (this.quiz.timelimit && timeTaken > this.quiz.timelimit + 60) {
|
||||
this.attempt.overTime = this.timeUtils.formatTime(timeTaken - this.quiz.timelimit);
|
||||
}
|
||||
}
|
||||
|
||||
// Treat grade.
|
||||
if (this.options.someoptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX &&
|
||||
this.quizProvider.quizHasGrades(this.quiz)) {
|
||||
|
||||
if (data.grade === null || typeof data.grade == 'undefined') {
|
||||
this.attempt.readableGrade = this.quizProvider.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.attempt.readableMark = this.translate.instant('addon.mod_quiz.outofshort', {$a: {
|
||||
grade: this.quizProvider.formatGrade(this.attempt.sumgrades, this.quiz.decimalpoints),
|
||||
maxgrade: this.quizProvider.formatGrade(this.quiz.sumgrades, this.quiz.decimalpoints)
|
||||
}});
|
||||
}
|
||||
|
||||
// Now the scaled grade.
|
||||
const gradeObject: any = {
|
||||
grade: this.quizProvider.formatGrade(data.grade, this.quiz.decimalpoints),
|
||||
maxgrade: this.quizProvider.formatGrade(this.quiz.grade, this.quiz.decimalpoints)
|
||||
};
|
||||
|
||||
if (this.quiz.grade != 100) {
|
||||
gradeObject.percent = this.textUtils.roundToDecimals(this.attempt.sumgrades * 100 / this.quiz.sumgrades, 0);
|
||||
this.attempt.readableGrade = this.translate.instant('addon.mod_quiz.outofpercent', {$a: gradeObject});
|
||||
} else {
|
||||
this.attempt.readableGrade = this.translate.instant('addon.mod_quiz.outof', {$a: gradeObject});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Treat additional data.
|
||||
this.additionalData.forEach((data) => {
|
||||
// Remove help links from additional data.
|
||||
data.content = this.domUtils.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);
|
||||
}
|
||||
}
|
|
@ -21,11 +21,12 @@
|
|||
|
||||
<!-- Radio buttons for single choice. -->
|
||||
<div *ngIf="!question.multi" radio-group [(ngModel)]="question.singleChoiceModel" [name]="question.optionsName">
|
||||
<ion-item text-wrap *ngFor="let option of question.options">
|
||||
<ion-label><core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text></ion-label>
|
||||
<ion-radio [value]="option.value" [disabled]="option.disabled" [ngClass]='{"core-question-answer-correct": option.isCorrect === 1, "core-question-answer-incorrect": option.isCorrect === 0}'>
|
||||
<ion-item text-wrap *ngFor="let option of question.options" [ngClass]='{"core-question-answer-correct": option.isCorrect === 1, "core-question-answer-incorrect": option.isCorrect === 0}'>
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text>
|
||||
<p *ngIf="option.feedback" class="core-question-feedback-container"><core-format-text [component]="component" [componentId]="componentId" [text]="option.feedback"></core-format-text></p>
|
||||
</ion-radio>
|
||||
</ion-label>
|
||||
<ion-radio [value]="option.value" [disabled]="option.disabled"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
|
||||
|
|
|
@ -356,6 +356,84 @@ ion-select {
|
|||
}
|
||||
}
|
||||
|
||||
// Question.
|
||||
// -------------------------
|
||||
|
||||
.core-question-answer-correct,
|
||||
.core-question-comment {
|
||||
color: $core-question-correct-color;
|
||||
background-color: $core-question-correct-color-bg;
|
||||
|
||||
.label, ion-label.label {
|
||||
color: $core-question-correct-color;
|
||||
}
|
||||
}
|
||||
|
||||
.core-question-answer-incorrect,
|
||||
.core-question-incorrect {
|
||||
color: $core-question-incorrect-color;
|
||||
background-color: $core-question-incorrect-color-bg;
|
||||
|
||||
.label, ion-label.label {
|
||||
color: $core-question-incorrect-color;
|
||||
}
|
||||
}
|
||||
|
||||
.core-question-feedback-container {
|
||||
background-color: $core-question-feedback-color-bg;
|
||||
color: $core-question-feedback-color;
|
||||
|
||||
.specificfeedback, .rightanswer, .im-feedback, .feedback, .generalfeedback {
|
||||
margin: 0 0 .5em;
|
||||
}
|
||||
|
||||
.correctness {
|
||||
display: inline-block;
|
||||
padding: 2px 4px;
|
||||
font-weight: bold;
|
||||
line-height: 14px;
|
||||
color: $white;
|
||||
text-shadow: 0 -1px 0 rgba(0,0,0,0.25);
|
||||
background-color: $gray-dark;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
|
||||
&.incorrect {
|
||||
background-color: $red;
|
||||
}
|
||||
&.correct {
|
||||
background-color: $green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.core-question-feedback-inline {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.core-question-feedback-padding {
|
||||
padding: 8px 35px 8px 14px;
|
||||
}
|
||||
|
||||
.core-question-correct {
|
||||
background-color: $core-question-state-correct-color;
|
||||
}
|
||||
.core-question-partiallycorrect {
|
||||
background-color: $core-question-state-partial-color;
|
||||
}
|
||||
.core-question-notanswered,
|
||||
.core-question-incorrect {
|
||||
background-color: $core-question-state-incorrect-color;
|
||||
}
|
||||
|
||||
.core-question-warning {
|
||||
color: $core-question-warning-color;
|
||||
}
|
||||
|
||||
.questioncorrectnessicon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
// Atto styles
|
||||
// -------------------------
|
||||
.atto_image_preview {
|
||||
|
|
|
@ -396,6 +396,18 @@ export class CoreQuestionHelperProvider {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CSS class for a question based on its state.
|
||||
*
|
||||
* @param {string} name Question's state name.
|
||||
* @return {string} State class.
|
||||
*/
|
||||
getQuestionStateClass(name: string): string {
|
||||
const state = this.questionProvider.getState(name);
|
||||
|
||||
return state ? state.class : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message from a question HTML if it's there.
|
||||
*
|
||||
|
|
|
@ -213,6 +213,19 @@ $core-timer-warn-color: $red !default;
|
|||
$core-timer-color: $white !default;
|
||||
$core-timer-iterations: 15 !default;
|
||||
|
||||
// Question variables.
|
||||
$core-question-correct-color: $green-dark !default;
|
||||
$core-question-correct-color-bg: $green-light !default;
|
||||
$core-question-incorrect-color: $red !default;
|
||||
$core-question-incorrect-color-bg: $red-light !default;
|
||||
$core-question-feedback-color: $yellow-dark !default;
|
||||
$core-question-feedback-color-bg: $yellow-light !default;
|
||||
$core-question-warning-color: $red !default;
|
||||
|
||||
$core-question-state-correct-color: $green-light !default;
|
||||
$core-question-state-partial-color: $yellow-light !default;
|
||||
$core-question-state-incorrect-color: $red-light !default;
|
||||
|
||||
// Mixins
|
||||
// -------------------------
|
||||
@mixin core-transition($where: all, $time: 500ms) {
|
||||
|
|
Loading…
Reference in New Issue