MOBILE-4550 quiz: Change how list of attempts is displayed
parent
ec195696e0
commit
a765f3cb8d
|
@ -872,6 +872,7 @@
|
||||||
"addon.mod_page.errorwhileloadingthepage": "local_moodlemobileapp",
|
"addon.mod_page.errorwhileloadingthepage": "local_moodlemobileapp",
|
||||||
"addon.mod_page.modulenameplural": "page",
|
"addon.mod_page.modulenameplural": "page",
|
||||||
"addon.mod_quiz.answercolon": "qtype_numerical",
|
"addon.mod_quiz.answercolon": "qtype_numerical",
|
||||||
|
"addon.mod_quiz.attempt": "quiz",
|
||||||
"addon.mod_quiz.attemptduration": "quiz",
|
"addon.mod_quiz.attemptduration": "quiz",
|
||||||
"addon.mod_quiz.attemptfirst": "quiz",
|
"addon.mod_quiz.attemptfirst": "quiz",
|
||||||
"addon.mod_quiz.attemptlast": "quiz",
|
"addon.mod_quiz.attemptlast": "quiz",
|
||||||
|
@ -902,7 +903,7 @@
|
||||||
"addon.mod_quiz.errorsaveattempt": "local_moodlemobileapp",
|
"addon.mod_quiz.errorsaveattempt": "local_moodlemobileapp",
|
||||||
"addon.mod_quiz.feedback": "quiz",
|
"addon.mod_quiz.feedback": "quiz",
|
||||||
"addon.mod_quiz.finishattemptdots": "quiz",
|
"addon.mod_quiz.finishattemptdots": "quiz",
|
||||||
"addon.mod_quiz.finishnotsynced": "local_moodlemobileapp",
|
"addon.mod_quiz.finishedofflinenotice": "local_moodlemobileapp",
|
||||||
"addon.mod_quiz.grade": "quiz",
|
"addon.mod_quiz.grade": "quiz",
|
||||||
"addon.mod_quiz.gradeaverage": "quiz",
|
"addon.mod_quiz.gradeaverage": "quiz",
|
||||||
"addon.mod_quiz.gradehighest": "quiz",
|
"addon.mod_quiz.gradehighest": "quiz",
|
||||||
|
@ -1864,6 +1865,7 @@
|
||||||
"core.grades.grade": "grades",
|
"core.grades.grade": "grades",
|
||||||
"core.grades.gradebook": "grades",
|
"core.grades.gradebook": "grades",
|
||||||
"core.grades.gradeitem": "grades",
|
"core.grades.gradeitem": "grades",
|
||||||
|
"core.grades.gradelong": "grades",
|
||||||
"core.grades.gradepass": "grades",
|
"core.grades.gradepass": "grades",
|
||||||
"core.grades.grades": "grades",
|
"core.grades.grades": "grades",
|
||||||
"core.grades.lettergrade": "grades",
|
"core.grades.lettergrade": "grades",
|
||||||
|
@ -2560,6 +2562,7 @@
|
||||||
"core.strftimetime12": "langconfig",
|
"core.strftimetime12": "langconfig",
|
||||||
"core.strftimetime24": "langconfig",
|
"core.strftimetime24": "langconfig",
|
||||||
"core.submit": "moodle",
|
"core.submit": "moodle",
|
||||||
|
"core.submittedoffline": "local_moodlemobileapp",
|
||||||
"core.success": "moodle",
|
"core.success": "moodle",
|
||||||
"core.summary": "moodle",
|
"core.summary": "moodle",
|
||||||
"core.swipenavigationtourdescription": "local_moodlemobileapp",
|
"core.swipenavigationtourdescription": "local_moodlemobileapp",
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-attempt-status">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_quiz.attemptstate' | translate }}</p>
|
||||||
|
<addon-mod-quiz-attempt-state [state]="attempt.state" [finishedOffline]="attempt.finishedOffline" />
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-attempt-startedon">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_quiz.startedon' | translate }}</p>
|
||||||
|
<p>{{ attempt.timestart! * 1000 | coreFormatDate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
@if (isFinished) {
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-attempt-completedon">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_quiz.completedon' | translate }}</p>
|
||||||
|
<p>{{ attempt.timefinish! * 1000 | coreFormatDate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
@if (timeTaken) {
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-attempt-duration">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_quiz.attemptduration' | translate }}</p>
|
||||||
|
<p>{{ timeTaken }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (overTime) {
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-attempt-overdue">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_quiz.overdue' | translate }}</p>
|
||||||
|
<p>{{ overTime }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
}
|
||||||
|
|
||||||
|
@for (gradeItemMark of gradeItemMarks; track $index) {
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-attempt-gradeitemmark">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ gradeItemMark.name }}</p>
|
||||||
|
<p [innerHTML]="gradeItemMark.grade"></p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (quiz.showAttemptsMarks && readableMark && attempt?.sumgrades !== null) {
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-attempt-mark">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_quiz.marks' | translate }}</p>
|
||||||
|
<p>{{ readableMark }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (readableGrade) {
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-attempt-grade">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_quiz.grade' | translate }}</p>
|
||||||
|
<p [innerHTML]="readableGrade"></p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
}
|
||||||
|
|
||||||
|
@for (data of additionalData; track data.id) {
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-attempt-{{data.id}}">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ data.title }}</p>
|
||||||
|
<core-format-text [component]="component" [componentId]="quiz.coursemodule" [text]="data.content" contextLevel="module"
|
||||||
|
[contextInstanceId]="quiz.coursemodule" [courseId]="quiz.course" />
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
// (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, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
|
import { AddonModQuizAttempt, AddonModQuizQuizData } from '../../services/quiz-helper';
|
||||||
|
import { AddonModQuiz, AddonModQuizWSAdditionalData } from '../../services/quiz';
|
||||||
|
import { ADDON_MOD_QUIZ_COMPONENT, AddonModQuizAttemptStates } from '../../constants';
|
||||||
|
import { CoreTime } from '@singletons/time';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { isSafeNumber } from '@/core/utils/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays an attempt info.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-quiz-attempt-info',
|
||||||
|
templateUrl: 'attempt-info.html',
|
||||||
|
})
|
||||||
|
export class AddonModQuizAttemptInfoComponent implements OnChanges {
|
||||||
|
|
||||||
|
@Input() quiz!: AddonModQuizQuizData;
|
||||||
|
@Input() attempt!: AddonModQuizAttempt;
|
||||||
|
@Input() additionalData?: AddonModQuizWSAdditionalData[]; // Additional data to display for the attempt.
|
||||||
|
|
||||||
|
isFinished = false;
|
||||||
|
readableMark = '';
|
||||||
|
readableGrade = '';
|
||||||
|
timeTaken?: string;
|
||||||
|
overTime?: string;
|
||||||
|
gradeItemMarks: { name: string; grade: string }[] = [];
|
||||||
|
component = ADDON_MOD_QUIZ_COMPONENT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnChanges(changes: SimpleChanges): Promise<void> {
|
||||||
|
if (changes.additionalData) {
|
||||||
|
this.additionalData?.forEach((data) => {
|
||||||
|
// Remove help links from additional data.
|
||||||
|
data.content = CoreDomUtils.removeElementFromHtml(data.content, '.helptooltip');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changes.attempt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isFinished = this.attempt.state === AddonModQuizAttemptStates.FINISHED;
|
||||||
|
if (!this.isFinished) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeTaken = (this.attempt.timefinish || 0) - (this.attempt.timestart || 0);
|
||||||
|
if (timeTaken > 0) {
|
||||||
|
// Format time taken.
|
||||||
|
this.timeTaken = CoreTime.formatTime(timeTaken);
|
||||||
|
|
||||||
|
// Calculate overdue time.
|
||||||
|
if (this.quiz.timelimit && timeTaken > this.quiz.timelimit + 60) {
|
||||||
|
this.overTime = CoreTime.formatTime(timeTaken - this.quiz.timelimit);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.timeTaken = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat grade item marks.
|
||||||
|
if (this.attempt.sumgrades === null || !this.attempt.gradeitemmarks) {
|
||||||
|
this.gradeItemMarks = [];
|
||||||
|
} else {
|
||||||
|
this.gradeItemMarks = this.attempt.gradeitemmarks.map((gradeItemMark) => ({
|
||||||
|
name: gradeItemMark.name,
|
||||||
|
grade: Translate.instant('addon.mod_quiz.outof', { $a: {
|
||||||
|
grade: '<strong>' + AddonModQuiz.formatGrade(gradeItemMark.grade, this.quiz?.decimalpoints) + '</strong>',
|
||||||
|
maxgrade: AddonModQuiz.formatGrade(gradeItemMark.maxgrade, this.quiz?.decimalpoints),
|
||||||
|
} }),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.quiz.showAttemptsGrades) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat grade and mark.
|
||||||
|
if (!isSafeNumber(this.attempt.rescaledGrade)) {
|
||||||
|
this.readableGrade = Translate.instant('addon.mod_quiz.notyetgraded');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.quiz.showAttemptsMarks) {
|
||||||
|
this.readableMark = Translate.instant('addon.mod_quiz.outofshort', { $a: {
|
||||||
|
grade: AddonModQuiz.formatGrade(this.attempt.sumgrades, this.quiz.decimalpoints),
|
||||||
|
maxgrade: AddonModQuiz.formatGrade(this.quiz.sumgrades, this.quiz.decimalpoints),
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
const gradeObject: Record<string, unknown> = {
|
||||||
|
grade: '<strong>' + AddonModQuiz.formatGrade(this.attempt.rescaledGrade, this.quiz.decimalpoints) + '</strong>',
|
||||||
|
maxgrade: AddonModQuiz.formatGrade(this.quiz.grade, this.quiz.decimalpoints),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.quiz.grade != 100) {
|
||||||
|
const percentage = (this.attempt.sumgrades ?? 0) * 100 / (this.quiz.sumgrades ?? 1);
|
||||||
|
gradeObject.percent = '<strong>' + AddonModQuiz.formatGrade(percentage, this.quiz.decimalpoints) + '</strong>';
|
||||||
|
this.readableGrade = Translate.instant('addon.mod_quiz.outofpercent', { $a: gradeObject });
|
||||||
|
} else {
|
||||||
|
this.readableGrade = Translate.instant('addon.mod_quiz.outof', { $a: gradeObject });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<ion-badge [color]="color">
|
||||||
|
@if (finishedOffline) {
|
||||||
|
<ion-icon name="fas-clock" aria-hidden="true" />
|
||||||
|
}
|
||||||
|
{{ readableState }}
|
||||||
|
</ion-badge>
|
|
@ -0,0 +1,14 @@
|
||||||
|
@use "theme/globals" as *;
|
||||||
|
|
||||||
|
:host {
|
||||||
|
|
||||||
|
ion-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
@include margin-horizontal(0px, var(--mdl-spacing-1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// (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, Input, OnChanges } from '@angular/core';
|
||||||
|
import { AddonModQuiz } from '../../services/quiz';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays an attempt state.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-quiz-attempt-state',
|
||||||
|
templateUrl: 'attempt-state.html',
|
||||||
|
styleUrls: ['attempt-state.scss'],
|
||||||
|
})
|
||||||
|
export class AddonModQuizAttemptStateComponent implements OnChanges {
|
||||||
|
|
||||||
|
@Input() state = '';
|
||||||
|
@Input() finishedOffline = false;
|
||||||
|
|
||||||
|
readableState = '';
|
||||||
|
color = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnChanges(): Promise<void> {
|
||||||
|
this.readableState = AddonModQuiz.getAttemptReadableStateName(this.state, this.finishedOffline);
|
||||||
|
this.color = AddonModQuiz.getAttemptStateColor(this.state, this.finishedOffline);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -20,9 +20,13 @@ import { AddonModQuizConnectionErrorComponent } from './connection-error/connect
|
||||||
import { AddonModQuizIndexComponent } from './index/index';
|
import { AddonModQuizIndexComponent } from './index/index';
|
||||||
import { AddonModQuizNavigationModalComponent } from './navigation-modal/navigation-modal';
|
import { AddonModQuizNavigationModalComponent } from './navigation-modal/navigation-modal';
|
||||||
import { AddonModQuizPreflightModalComponent } from './preflight-modal/preflight-modal';
|
import { AddonModQuizPreflightModalComponent } from './preflight-modal/preflight-modal';
|
||||||
|
import { AddonModQuizAttemptInfoComponent } from './attempt-info/attempt-info';
|
||||||
|
import { AddonModQuizAttemptStateComponent } from './attempt-state/attempt-state';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
AddonModQuizAttemptInfoComponent,
|
||||||
|
AddonModQuizAttemptStateComponent,
|
||||||
AddonModQuizIndexComponent,
|
AddonModQuizIndexComponent,
|
||||||
AddonModQuizConnectionErrorComponent,
|
AddonModQuizConnectionErrorComponent,
|
||||||
AddonModQuizNavigationModalComponent,
|
AddonModQuizNavigationModalComponent,
|
||||||
|
@ -35,6 +39,8 @@ import { AddonModQuizPreflightModalComponent } from './preflight-modal/preflight
|
||||||
providers: [
|
providers: [
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
AddonModQuizAttemptInfoComponent,
|
||||||
|
AddonModQuizAttemptStateComponent,
|
||||||
AddonModQuizIndexComponent,
|
AddonModQuizIndexComponent,
|
||||||
AddonModQuizConnectionErrorComponent,
|
AddonModQuizConnectionErrorComponent,
|
||||||
AddonModQuizNavigationModalComponent,
|
AddonModQuizNavigationModalComponent,
|
||||||
|
|
|
@ -45,64 +45,74 @@
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<!-- List of user attempts. -->
|
<!-- List of user attempts. -->
|
||||||
<ion-card class="addon-mod_quiz-table" *ngIf="quiz && attempts.length">
|
<ion-card class="addon-mod_quiz-attempts-summary" *ngIf="quiz && attempts.length">
|
||||||
<ion-card-header class="ion-text-wrap">
|
<ion-card-header class="ion-text-wrap">
|
||||||
<ion-card-title>{{ 'addon.mod_quiz.summaryofattempts' | translate }}</ion-card-title>
|
<ion-card-title>{{ 'addon.mod_quiz.summaryofattempts' | translate }}</ion-card-title>
|
||||||
</ion-card-header>
|
</ion-card-header>
|
||||||
<ion-card-content role="table">
|
|
||||||
<!-- "Header" of the table -->
|
<ion-accordion-group>
|
||||||
<ion-item class="ion-text-wrap addon-mod_quiz-table-header hide-detail" [detail]="true">
|
@for (attempt of attempts; track attempt.id) {
|
||||||
<ion-label role="rowgroup">
|
<ion-accordion [value]="attempt.id" toggleIconSlot="start">
|
||||||
<ion-row class="ion-align-items-center" role="row">
|
<ion-item slot="header" class="ion-text-wrap addon-mod_quiz-attempt-title" lines="none">
|
||||||
<ion-col class="ion-text-center" *ngIf="quiz.showAttemptColumn" role="columnheader">
|
|
||||||
<strong class="ion-hide-md-up" aria-hidden="true">#</strong>
|
|
||||||
<span class="sr-only ion-hide-md-up">{{ 'addon.mod_quiz.attemptnumber' | translate }}</span>
|
|
||||||
<strong class="ion-hide-md-down">{{ 'addon.mod_quiz.attemptnumber' | translate }}</strong>
|
|
||||||
</ion-col>
|
|
||||||
<ion-col size="7" role="columnheader">
|
|
||||||
<strong>{{ 'addon.mod_quiz.attemptstate' | translate }}</strong>
|
|
||||||
</ion-col>
|
|
||||||
<ion-col class="ion-text-center ion-hide-md-down" *ngIf="quiz.showMarkColumn" role="columnheader">
|
|
||||||
<strong>{{ 'addon.mod_quiz.marks' | translate }} / {{ quiz.sumGradesFormatted }}</strong>
|
|
||||||
</ion-col>
|
|
||||||
<ion-col class="ion-text-center" *ngIf="quiz.showGradeColumn" role="columnheader">
|
|
||||||
<strong>{{ 'addon.mod_quiz.grade' | translate }} / {{ quiz.gradeFormatted }}</strong>
|
|
||||||
</ion-col>
|
|
||||||
</ion-row>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<div role="rowgroup">
|
|
||||||
<!-- List of attempts. -->
|
|
||||||
<ion-item button [detail]="true" *ngFor="let attempt of attempts" class="ion-text-wrap"
|
|
||||||
[ngClass]='{"addon-mod_quiz-highlighted": attempt.highlightGrade}' [attr.aria-label]="'core.seemoredetail' | translate"
|
|
||||||
(click)="viewAttempt(attempt.id)">
|
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<ion-row class="ion-align-items-center" role="row">
|
<h3>{{ 'addon.mod_quiz.attempt' | translate:{ $a: attempt.attempt } }}</h3>
|
||||||
<ion-col class="ion-text-center" *ngIf="quiz.showAttemptColumn && attempt.preview" role="cell">
|
|
||||||
{{ 'addon.mod_quiz.preview' | translate }}
|
|
||||||
</ion-col>
|
|
||||||
<ion-col class="ion-text-center" *ngIf="quiz.showAttemptColumn && !attempt.preview" role="cell">
|
|
||||||
{{ attempt.attempt }}
|
|
||||||
</ion-col>
|
|
||||||
<ion-col size="7" role="cell">
|
|
||||||
<p *ngFor="let sentence of attempt.readableState">{{ sentence }}</p>
|
|
||||||
</ion-col>
|
|
||||||
<ion-col class="ion-text-center ion-hide-md-down" *ngIf="quiz.showMarkColumn" role="cell">
|
|
||||||
<p>{{ attempt.readableMark }}</p>
|
|
||||||
</ion-col>
|
|
||||||
<ion-col class="ion-text-center" *ngIf="quiz.showGradeColumn" role="cell">
|
|
||||||
<p>{{ attempt.readableGrade }}</p>
|
|
||||||
</ion-col>
|
|
||||||
</ion-row>
|
|
||||||
</ion-label>
|
</ion-label>
|
||||||
|
<div slot="end" class="addon-mod_quiz-attempt-title-info">
|
||||||
|
<addon-mod-quiz-attempt-state [state]="attempt.state" [finishedOffline]="attempt.finishedOffline" />
|
||||||
|
@if (attempt.finished && quiz.showAttemptsGrades) {
|
||||||
|
@if (attempt.rescaledGrade !== undefined && attempt.rescaledGrade >= 0) {
|
||||||
|
<p>
|
||||||
|
{{ 'core.grades.gradelong' | translate: { $a: {
|
||||||
|
grade: attempt.formattedGrade,
|
||||||
|
max: quiz.gradeFormatted,
|
||||||
|
} } }}
|
||||||
|
</p>
|
||||||
|
} @else {
|
||||||
|
<p>{{ 'addon.mod_quiz.notyetgraded' | translate }}</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</div>
|
<div class="addon-mod_quiz-attempt-details" slot="content">
|
||||||
</ion-card-content>
|
<addon-mod-quiz-attempt-info [quiz]="quiz" [attempt]="attempt" [additionalData]="attempt.additionalData" />
|
||||||
|
|
||||||
|
@if (attempt.canReview) {
|
||||||
|
<hr>
|
||||||
|
<ion-button class="ion-text-wrap ion-margin" expand="block" (click)="reviewAttempt(attempt.id)" fill="outline">
|
||||||
|
<ion-icon name="fas-magnifying-glass" slot="start" aria-hidden="true" />
|
||||||
|
{{ 'addon.mod_quiz.review' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
} @else if (attempt.completed) {
|
||||||
|
<hr>
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-attempt-noreview">
|
||||||
|
<ion-label>
|
||||||
|
<p>
|
||||||
|
<ion-icon name="fas-circle-info" color="info" slot="start" aria-hidden="true" />
|
||||||
|
{{ 'addon.mod_quiz.noreviewattempt' | translate }}
|
||||||
|
<!-- TODO: Check if we can calculate the time when the attempt can be reviewed. -->
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
} @else if (attempt.finishedOffline) {
|
||||||
|
<hr>
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-attempt-finishedoffline">
|
||||||
|
<ion-label>
|
||||||
|
<p>
|
||||||
|
<ion-icon name="fas-clock" slot="start" aria-hidden="true" />
|
||||||
|
{{ 'addon.mod_quiz.finishedofflinenotice' | translate }}
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ion-accordion>
|
||||||
|
}
|
||||||
|
</ion-accordion-group>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<!-- Result info. -->
|
<!-- Result info. -->
|
||||||
<ion-card *ngIf="quiz && showResults &&
|
<ion-card *ngIf="quiz && showResults &&
|
||||||
(gradeResult || gradeOverridden || gradebookFeedback || (quiz.showFeedbackColumn && overallFeedback))">
|
(gradeResult || gradeOverridden || gradebookFeedback || (quiz.showFeedback && overallFeedback))">
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="gradeResult">
|
<ion-item class="ion-text-wrap" *ngIf="gradeResult">
|
||||||
<ion-label>{{ gradeResult }}</ion-label>
|
<ion-label>{{ gradeResult }}</ion-label>
|
||||||
|
@ -119,7 +129,7 @@
|
||||||
</p>
|
</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="quiz.showFeedbackColumn && overallFeedback">
|
<ion-item class="ion-text-wrap" *ngIf="quiz.showFeedback && overallFeedback">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.overallfeedback' | translate }}</p>
|
<p class="item-heading">{{ 'addon.mod_quiz.overallfeedback' | translate }}</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -1,33 +1,43 @@
|
||||||
|
@use "theme/globals" as *;
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
|
|
||||||
.addon-mod_quiz-table {
|
.addon-mod_quiz-attempt-title-info {
|
||||||
ion-card-content {
|
text-align: end;
|
||||||
padding-left: 0;
|
padding-top: 8px;
|
||||||
padding-right: 0;
|
padding-bottom: 8px;
|
||||||
}
|
|
||||||
|
|
||||||
.item:nth-child(even) {
|
p {
|
||||||
--background: var(--light);
|
margin: 0px;
|
||||||
}
|
margin-top: 4px;
|
||||||
|
|
||||||
.addon-mod_quiz-highlighted,
|
|
||||||
.item.addon-mod_quiz-highlighted,
|
|
||||||
.addon-mod_quiz-highlighted p,
|
|
||||||
.item.addon-mod_quiz-highlighted p {
|
|
||||||
--background: var(--primary-tint);
|
|
||||||
color: var(--primary-shade);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(html.dark) {
|
.accordion-expanded .addon-mod_quiz-attempt-title-info,
|
||||||
.addon-mod_quiz-table {
|
.accordion-expanding .addon-mod_quiz-attempt-title-info {
|
||||||
.addon-mod_quiz-highlighted,
|
visibility: hidden;
|
||||||
.item.addon-mod_quiz-highlighted,
|
}
|
||||||
.addon-mod_quiz-highlighted p,
|
|
||||||
.item.addon-mod_quiz-highlighted p {
|
hr {
|
||||||
--background: var(--primary-shade);
|
background-color: var(--stroke);
|
||||||
color: var(--primary-tint);
|
height: 1px;
|
||||||
|
margin: 0px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-accordion:nth-child(odd) {
|
||||||
|
background-color: var(--core-table-odd-cell-background);
|
||||||
|
|
||||||
|
::ng-deep ion-item {
|
||||||
|
--background: var(--core-table-odd-cell-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-accordion:nth-child(even) {
|
||||||
|
background-color: var(--core-table-even-cell-background);
|
||||||
|
|
||||||
|
::ng-deep ion-item {
|
||||||
|
--background: var(--core-table-even-cell-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { DownloadStatus } from '@/core/constants';
|
import { DownloadStatus } from '@/core/constants';
|
||||||
import { safeNumber, SafeNumber } from '@/core/utils/types';
|
import { isSafeNumber, safeNumber, SafeNumber } from '@/core/utils/types';
|
||||||
import { Component, OnDestroy, OnInit, Optional } from '@angular/core';
|
import { Component, OnDestroy, OnInit, Optional } from '@angular/core';
|
||||||
|
|
||||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||||
|
@ -36,6 +36,7 @@ import {
|
||||||
AddonModQuizGetAttemptAccessInformationWSResponse,
|
AddonModQuizGetAttemptAccessInformationWSResponse,
|
||||||
AddonModQuizGetQuizAccessInformationWSResponse,
|
AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
AddonModQuizGetUserBestGradeWSResponse,
|
AddonModQuizGetUserBestGradeWSResponse,
|
||||||
|
AddonModQuizWSAdditionalData,
|
||||||
} from '../../services/quiz';
|
} from '../../services/quiz';
|
||||||
import { AddonModQuizAttempt, AddonModQuizHelper, AddonModQuizQuizData } from '../../services/quiz-helper';
|
import { AddonModQuizAttempt, AddonModQuizHelper, AddonModQuizQuizData } from '../../services/quiz-helper';
|
||||||
import {
|
import {
|
||||||
|
@ -44,7 +45,7 @@ import {
|
||||||
AddonModQuizSyncProvider,
|
AddonModQuizSyncProvider,
|
||||||
AddonModQuizSyncResult,
|
AddonModQuizSyncResult,
|
||||||
} from '../../services/quiz-sync';
|
} from '../../services/quiz-sync';
|
||||||
import { ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT, ADDON_MOD_QUIZ_COMPONENT, AddonModQuizGradeMethods } from '../../constants';
|
import { ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT, ADDON_MOD_QUIZ_COMPONENT, AddonModQuizAttemptStates } from '../../constants';
|
||||||
import { QuestionDisplayOptionsMarks } from '@features/question/constants';
|
import { QuestionDisplayOptionsMarks } from '@features/question/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -78,13 +79,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
showStatusSpinner = true; // Whether to show a spinner due to quiz status.
|
showStatusSpinner = true; // Whether to show a spinner due to quiz status.
|
||||||
gradeMethodReadable?: string; // Grade method in a readable format.
|
gradeMethodReadable?: string; // Grade method in a readable format.
|
||||||
showReviewColumn = false; // Whether to show the review column.
|
showReviewColumn = false; // Whether to show the review column.
|
||||||
attempts: AddonModQuizAttempt[] = []; // List of attempts the user has made.
|
attempts: QuizAttempt[] = []; // List of attempts the user has made.
|
||||||
bestGrade?: AddonModQuizGetUserBestGradeWSResponse; // Best grade data.
|
bestGrade?: AddonModQuizGetUserBestGradeWSResponse; // Best grade data.
|
||||||
|
|
||||||
protected fetchContentDefaultError = 'addon.mod_quiz.errorgetquiz'; // Default error to show when loading contents.
|
protected fetchContentDefaultError = 'addon.mod_quiz.errorgetquiz'; // Default error to show when loading contents.
|
||||||
protected syncEventName = AddonModQuizSyncProvider.AUTO_SYNCED;
|
protected syncEventName = AddonModQuizSyncProvider.AUTO_SYNCED;
|
||||||
|
|
||||||
// protected quizData: any; // Quiz instance. This variable will store the quiz instance until it's ready to be shown
|
|
||||||
protected autoReview?: AddonModQuizAttemptFinishedData; // Data to auto-review an attempt after finishing.
|
protected autoReview?: AddonModQuizAttemptFinishedData; // Data to auto-review an attempt after finishing.
|
||||||
protected quizAccessInfo?: AddonModQuizGetQuizAccessInformationWSResponse; // Quiz access info.
|
protected quizAccessInfo?: AddonModQuizGetQuizAccessInformationWSResponse; // Quiz access info.
|
||||||
protected attemptAccessInfo?: AddonModQuizGetAttemptAccessInformationWSResponse; // Last attempt access info.
|
protected attemptAccessInfo?: AddonModQuizGetAttemptAccessInformationWSResponse; // Last attempt access info.
|
||||||
|
@ -263,7 +263,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
|
|
||||||
// Check if user can create/continue attempts.
|
// Check if user can create/continue attempts.
|
||||||
if (this.attempts.length) {
|
if (this.attempts.length) {
|
||||||
const last = this.attempts[this.attempts.length - 1];
|
const last = this.attempts[0];
|
||||||
this.moreAttempts = !AddonModQuiz.isAttemptCompleted(last.state) || !this.attemptAccessInfo.isfinished;
|
this.moreAttempts = !AddonModQuiz.isAttemptCompleted(last.state) || !this.attemptAccessInfo.isfinished;
|
||||||
} else {
|
} else {
|
||||||
this.moreAttempts = !this.attemptAccessInfo.isfinished;
|
this.moreAttempts = !this.attemptAccessInfo.isfinished;
|
||||||
|
@ -283,7 +283,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
this.buttonText = '';
|
this.buttonText = '';
|
||||||
|
|
||||||
if (quiz.hasquestions !== 0) {
|
if (quiz.hasquestions !== 0) {
|
||||||
if (this.attempts.length && !AddonModQuiz.isAttemptCompleted(this.attempts[this.attempts.length - 1].state)) {
|
if (this.attempts.length && !AddonModQuiz.isAttemptCompleted(this.attempts[0].state)) {
|
||||||
// Last attempt is unfinished.
|
// Last attempt is unfinished.
|
||||||
if (this.quizAccessInfo?.canattempt) {
|
if (this.quizAccessInfo?.canattempt) {
|
||||||
this.buttonText = 'addon.mod_quiz.continueattemptquiz';
|
this.buttonText = 'addon.mod_quiz.continueattemptquiz';
|
||||||
|
@ -331,7 +331,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async getResultInfo(quiz: AddonModQuizQuizData): Promise<void> {
|
protected async getResultInfo(quiz: AddonModQuizQuizData): Promise<void> {
|
||||||
if (!this.attempts.length || !quiz.showGradeColumn || !this.bestGrade?.hasgrade ||
|
if (!this.attempts.length || !quiz.showAttemptsGrades || !this.bestGrade?.hasgrade ||
|
||||||
this.gradebookData?.grade === undefined) {
|
this.gradebookData?.grade === undefined) {
|
||||||
this.showResults = false;
|
this.showResults = false;
|
||||||
|
|
||||||
|
@ -372,7 +372,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quiz.showFeedbackColumn) {
|
if (quiz.showFeedback) {
|
||||||
// Get the quiz overall feedback.
|
// Get the quiz overall feedback.
|
||||||
const response = await AddonModQuiz.getFeedbackForGrade(quiz.id, this.gradebookData.grade, {
|
const response = await AddonModQuiz.getFeedbackForGrade(quiz.id, this.gradebookData.grade, {
|
||||||
cmId: this.module.id,
|
cmId: this.module.id,
|
||||||
|
@ -583,7 +583,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
quiz: AddonModQuizQuizData,
|
quiz: AddonModQuizQuizData,
|
||||||
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
attempts: AddonModQuizAttemptWSData[],
|
attempts: AddonModQuizAttemptWSData[],
|
||||||
): Promise<AddonModQuizAttempt[]> {
|
): Promise<QuizAttempt[]> {
|
||||||
if (!attempts || !attempts.length) {
|
if (!attempts || !attempts.length) {
|
||||||
// There are no attempts to treat.
|
// There are no attempts to treat.
|
||||||
quiz.gradeFormatted = AddonModQuiz.formatGrade(quiz.grade, quiz.decimalpoints);
|
quiz.gradeFormatted = AddonModQuiz.formatGrade(quiz.grade, quiz.decimalpoints);
|
||||||
|
@ -609,25 +609,43 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.options = options;
|
this.options = options;
|
||||||
const grade = this.gradebookData?.grade !== undefined ? this.gradebookData.grade : this.bestGrade?.grade;
|
|
||||||
const quizGrade = AddonModQuiz.formatGrade(grade, quiz.decimalpoints);
|
|
||||||
|
|
||||||
// Calculate data to construct the header of the attempts table.
|
|
||||||
AddonModQuizHelper.setQuizCalculatedData(quiz, this.options);
|
AddonModQuizHelper.setQuizCalculatedData(quiz, this.options);
|
||||||
|
|
||||||
this.overallStats = !!lastCompleted && this.options.alloptions.marks >= QuestionDisplayOptionsMarks.MARK_AND_MAX;
|
this.overallStats = !!lastCompleted && this.options.alloptions.marks >= QuestionDisplayOptionsMarks.MARK_AND_MAX;
|
||||||
|
|
||||||
// Calculate data to show for each attempt.
|
// Calculate data to show for each attempt.
|
||||||
const formattedAttempts = await Promise.all(attempts.map((attempt, index) => {
|
const formattedAttempts = await Promise.all(attempts.map(async (attempt) => {
|
||||||
// Highlight the highest grade if appropriate.
|
const [formattedAttempt, canReview] = await Promise.all([
|
||||||
const shouldHighlight = this.overallStats && quiz.grademethod === AddonModQuizGradeMethods.HIGHEST_GRADE &&
|
AddonModQuizHelper.setAttemptCalculatedData(quiz, attempt) as Promise<QuizAttempt>,
|
||||||
attempts.length > 1;
|
AddonModQuizHelper.canReviewAttempt(quiz, accessInfo, attempt),
|
||||||
const isLast = index == attempts.length - 1;
|
]);
|
||||||
|
|
||||||
return AddonModQuizHelper.setAttemptCalculatedData(quiz, accessInfo, attempt, shouldHighlight, quizGrade, isLast);
|
formattedAttempt.canReview = canReview;
|
||||||
|
|
||||||
|
if (quiz.showFeedback && attempt.state === AddonModQuizAttemptStates.FINISHED &&
|
||||||
|
options.someoptions.overallfeedback && isSafeNumber(formattedAttempt.rescaledGrade)) {
|
||||||
|
|
||||||
|
// Feedback should be displayed, get the feedback for the grade.
|
||||||
|
const response = await AddonModQuiz.getFeedbackForGrade(quiz.id, formattedAttempt.rescaledGrade, {
|
||||||
|
cmId: quiz.coursemodule,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.feedbacktext) {
|
||||||
|
formattedAttempt.additionalData = [
|
||||||
|
{
|
||||||
|
id: 'feedback',
|
||||||
|
title: Translate.instant('addon.mod_quiz.feedback'),
|
||||||
|
content: response.feedbacktext,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedAttempt;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return formattedAttempts;
|
return formattedAttempts.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -657,13 +675,13 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go to page to view the attempt details.
|
* Go to page to review the attempt.
|
||||||
*
|
*
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async viewAttempt(attemptId: number): Promise<void> {
|
async reviewAttempt(attemptId: number): Promise<void> {
|
||||||
await CoreNavigator.navigateToSitePath(
|
await CoreNavigator.navigateToSitePath(
|
||||||
`${AddonModQuizModuleHandlerService.PAGE_NAME}/${this.courseId}/${this.module.id}/attempt/${attemptId}`,
|
`${AddonModQuizModuleHandlerService.PAGE_NAME}/${this.courseId}/${this.module.id}/review/${attemptId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -677,3 +695,8 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QuizAttempt = AddonModQuizAttempt & {
|
||||||
|
canReview?: boolean;
|
||||||
|
additionalData?: AddonModQuizWSAdditionalData[]; // Additional data to display for the attempt.
|
||||||
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"answercolon": "Answer:",
|
"answercolon": "Answer:",
|
||||||
|
"attempt": "Attempt {{$a}}",
|
||||||
"attemptduration": "Duration",
|
"attemptduration": "Duration",
|
||||||
"attemptfirst": "First attempt",
|
"attemptfirst": "First attempt",
|
||||||
"attemptlast": "Last attempt",
|
"attemptlast": "Last attempt",
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
"errorsaveattempt": "An error occurred while saving the attempt data.",
|
"errorsaveattempt": "An error occurred while saving the attempt data.",
|
||||||
"feedback": "Feedback",
|
"feedback": "Feedback",
|
||||||
"finishattemptdots": "Finish attempt...",
|
"finishattemptdots": "Finish attempt...",
|
||||||
"finishnotsynced": "Finished but not synchronised",
|
"finishedofflinenotice": "Your attempt has been submitted and saved. It will be sent to the site when you're online again.",
|
||||||
"grade": "Grade",
|
"grade": "Grade",
|
||||||
"gradeaverage": "Average grade",
|
"gradeaverage": "Average grade",
|
||||||
"gradehighest": "Highest grade",
|
"gradehighest": "Highest grade",
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
<ion-header>
|
|
||||||
<ion-toolbar>
|
|
||||||
<ion-buttons slot="start">
|
|
||||||
<ion-back-button [text]="'core.back' | translate" />
|
|
||||||
</ion-buttons>
|
|
||||||
<ion-title>
|
|
||||||
<h1>
|
|
||||||
<core-format-text *ngIf="quiz" [text]="quiz.name" contextLevel="module" [contextInstanceId]="quiz.coursemodule"
|
|
||||||
[courseId]="courseId" />
|
|
||||||
</h1>
|
|
||||||
</ion-title>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
<ion-content class="limited-width">
|
|
||||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
|
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" />
|
|
||||||
</ion-refresher>
|
|
||||||
<core-loading [hideUntil]="loaded">
|
|
||||||
<ion-list *ngIf="attempt">
|
|
||||||
<ion-item class="ion-text-wrap">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.attemptnumber' | translate }}</p>
|
|
||||||
<p *ngIf="attempt.preview">{{ 'addon.mod_quiz.preview' | translate }}</p>
|
|
||||||
<p *ngIf="!attempt.preview">{{ attempt.attempt }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.attemptstate' | translate }}</p>
|
|
||||||
<p *ngFor="let sentence of attempt.readableState">{{ sentence }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ng-container *ngIf="attempt.completed && attempt.sumgrades !== null">
|
|
||||||
<ion-item *ngFor="let gradeItemMark of attempt.gradeitemmarks ?? []" class="ion-text-wrap">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ gradeItemMark.name }} / {{ gradeItemMark.maxgrade }}</p>
|
|
||||||
<p>{{ gradeItemMark.grade }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ng-container>
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="quiz!.showMarkColumn && attempt.readableMark !== '' && attempt.sumgrades !== null">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.marks' | translate }} / {{ quiz!.sumGradesFormatted }}</p>
|
|
||||||
<p>{{ attempt.readableMark }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="quiz!.showGradeColumn && attempt.readableGrade !== ''">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.grade' | translate }} / {{ quiz!.gradeFormatted }}</p>
|
|
||||||
<p>{{ attempt.readableGrade }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="quiz!.showFeedbackColumn && feedback">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.feedback' | translate }}</p>
|
|
||||||
<p>
|
|
||||||
<core-format-text [component]="component" [componentId]="componentId" [text]="feedback" contextLevel="module"
|
|
||||||
[contextInstanceId]="cmId" [courseId]="courseId" />
|
|
||||||
</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-item class="ion-text-wrap core-danger-item" *ngIf="!showReviewColumn">
|
|
||||||
<ion-label>
|
|
||||||
<p>{{ 'addon.mod_quiz.noreviewattempt' | translate }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-list>
|
|
||||||
|
|
||||||
<div collapsible-footer appearOnBottom *ngIf="loaded && attempt && showReviewColumn && attempt.completed" slot="fixed">
|
|
||||||
<div class="list-item-limited-width">
|
|
||||||
<ion-button class="ion-margin ion-text-wrap" expand="block" (click)="reviewAttempt()">
|
|
||||||
<ion-icon name="fas-magnifying-glass" slot="start" aria-hidden="true" />
|
|
||||||
{{ 'addon.mod_quiz.review' | translate }}
|
|
||||||
</ion-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</core-loading>
|
|
||||||
</ion-content>
|
|
|
@ -1,221 +0,0 @@
|
||||||
// (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 { isSafeNumber } from '@/core/utils/types';
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { CoreError } from '@classes/errors/error';
|
|
||||||
import { CoreNavigator } from '@services/navigator';
|
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
|
||||||
import { Translate } from '@singletons';
|
|
||||||
import {
|
|
||||||
AddonModQuiz,
|
|
||||||
AddonModQuizAttemptWSData,
|
|
||||||
AddonModQuizGetQuizAccessInformationWSResponse,
|
|
||||||
} from '../../services/quiz';
|
|
||||||
import { AddonModQuizAttempt, AddonModQuizHelper, AddonModQuizQuizData } from '../../services/quiz-helper';
|
|
||||||
import { ADDON_MOD_QUIZ_COMPONENT } from '../../constants';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page that displays some summary data about an attempt.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'page-addon-mod-quiz-attempt',
|
|
||||||
templateUrl: 'attempt.html',
|
|
||||||
})
|
|
||||||
export class AddonModQuizAttemptPage implements OnInit {
|
|
||||||
|
|
||||||
courseId!: number; // The course ID the quiz belongs to.
|
|
||||||
quiz?: AddonModQuizQuizData; // The quiz the attempt belongs to.
|
|
||||||
attempt?: AddonModQuizAttempt; // The attempt to view.
|
|
||||||
component = ADDON_MOD_QUIZ_COMPONENT; // Component to link the files to.
|
|
||||||
componentId?: number; // Component ID to use in conjunction with the component.
|
|
||||||
loaded = false; // Whether data has been loaded.
|
|
||||||
feedback?: string; // Attempt feedback.
|
|
||||||
showReviewColumn = false;
|
|
||||||
cmId!: number; // Course module id the attempt belongs to.
|
|
||||||
|
|
||||||
protected attemptId!: number; // Attempt to view.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
ngOnInit(): void {
|
|
||||||
try {
|
|
||||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
|
||||||
this.attemptId = CoreNavigator.getRequiredRouteNumberParam('attemptId');
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModal(error);
|
|
||||||
|
|
||||||
CoreNavigator.back();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fetchQuizData().finally(() => {
|
|
||||||
this.loaded = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the data.
|
|
||||||
*
|
|
||||||
* @param refresher Refresher.
|
|
||||||
*/
|
|
||||||
doRefresh(refresher: HTMLIonRefresherElement): void {
|
|
||||||
this.refreshData().finally(() => {
|
|
||||||
refresher.complete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get quiz data and attempt data.
|
|
||||||
*
|
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async fetchQuizData(): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.quiz = await AddonModQuiz.getQuiz(this.courseId, this.cmId);
|
|
||||||
|
|
||||||
this.componentId = this.quiz.coursemodule;
|
|
||||||
|
|
||||||
// Load attempt data.
|
|
||||||
const [options, accessInfo, attempt] = await Promise.all([
|
|
||||||
AddonModQuiz.getCombinedReviewOptions(this.quiz.id, { cmId: this.quiz.coursemodule }),
|
|
||||||
this.fetchAccessInfo(this.quiz),
|
|
||||||
this.fetchAttempt(this.quiz.id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Set calculated data.
|
|
||||||
this.showReviewColumn = accessInfo.canreviewmyattempts;
|
|
||||||
AddonModQuizHelper.setQuizCalculatedData(this.quiz, options);
|
|
||||||
|
|
||||||
this.attempt = await AddonModQuizHelper.setAttemptCalculatedData(
|
|
||||||
this.quiz,
|
|
||||||
accessInfo,
|
|
||||||
attempt,
|
|
||||||
false,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if the feedback should be displayed.
|
|
||||||
const grade = Number(this.attempt.rescaledGrade);
|
|
||||||
|
|
||||||
if (this.quiz.showFeedbackColumn && AddonModQuiz.isAttemptCompleted(this.attempt.state) &&
|
|
||||||
options.someoptions.overallfeedback && isSafeNumber(grade)) {
|
|
||||||
|
|
||||||
// Feedback should be displayed, get the feedback for the grade.
|
|
||||||
const response = await AddonModQuiz.getFeedbackForGrade(this.quiz.id, grade, {
|
|
||||||
cmId: this.quiz.coursemodule,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.feedback = response.feedbacktext;
|
|
||||||
} else {
|
|
||||||
delete this.feedback;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetattempt', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the attempt.
|
|
||||||
*
|
|
||||||
* @param quizId Quiz ID.
|
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async fetchAttempt(quizId: number): Promise<AddonModQuizAttemptWSData> {
|
|
||||||
// Get all the attempts and search the one we want.
|
|
||||||
const attempts = await AddonModQuiz.getUserAttempts(quizId, { cmId: this.cmId });
|
|
||||||
|
|
||||||
const attempt = attempts.find(attempt => attempt.id == this.attemptId);
|
|
||||||
|
|
||||||
if (!attempt) {
|
|
||||||
// Attempt not found, error.
|
|
||||||
this.attempt = undefined;
|
|
||||||
|
|
||||||
throw new CoreError(Translate.instant('addon.mod_quiz.errorgetattempt'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return attempt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the access info.
|
|
||||||
*
|
|
||||||
* @param quiz Quiz instance.
|
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async fetchAccessInfo(quiz: AddonModQuizQuizData): Promise<AddonModQuizGetQuizAccessInformationWSResponse> {
|
|
||||||
const accessInfo = await AddonModQuiz.getQuizAccessInformation(quiz.id, { cmId: this.cmId });
|
|
||||||
|
|
||||||
if (!accessInfo.canreviewmyattempts) {
|
|
||||||
return accessInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user can review the attempt.
|
|
||||||
await CoreUtils.ignoreErrors(AddonModQuiz.invalidateAttemptReviewForPage(this.attemptId, -1));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await AddonModQuiz.getAttemptReview(this.attemptId, { page: -1, cmId: quiz.coursemodule });
|
|
||||||
} catch {
|
|
||||||
// Error getting the review, assume the user cannot review the attempt.
|
|
||||||
accessInfo.canreviewmyattempts = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the data.
|
|
||||||
*
|
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async refreshData(): Promise<void> {
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
|
|
||||||
promises.push(AddonModQuiz.invalidateQuizData(this.courseId));
|
|
||||||
promises.push(AddonModQuiz.invalidateAttemptReview(this.attemptId));
|
|
||||||
|
|
||||||
if (this.quiz) {
|
|
||||||
promises.push(AddonModQuiz.invalidateUserAttemptsForUser(this.quiz.id));
|
|
||||||
promises.push(AddonModQuiz.invalidateQuizAccessInformation(this.quiz.id));
|
|
||||||
promises.push(AddonModQuiz.invalidateCombinedReviewOptionsForUser(this.quiz.id));
|
|
||||||
|
|
||||||
if (this.attempt && this.feedback !== undefined) {
|
|
||||||
promises.push(AddonModQuiz.invalidateFeedback(this.quiz.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await CoreUtils.ignoreErrors(Promise.all(promises));
|
|
||||||
|
|
||||||
await this.fetchQuizData();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go to the page to review the attempt.
|
|
||||||
*
|
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
|
||||||
async reviewAttempt(): Promise<void> {
|
|
||||||
if (!this.attempt) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreNavigator.navigate(`../../review/${this.attempt.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -10,6 +10,7 @@ $quiz-timer-iterations: 15 !default;
|
||||||
font-size: var(--mdl-typography-fontSize-md);
|
font-size: var(--mdl-typography-fontSize-md);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
core-timer {
|
core-timer {
|
||||||
|
|
|
@ -40,7 +40,7 @@ import {
|
||||||
AddonModQuizGetQuizAccessInformationWSResponse,
|
AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
AddonModQuizQuizWSData,
|
AddonModQuizQuizWSData,
|
||||||
} from '../../services/quiz';
|
} from '../../services/quiz';
|
||||||
import { AddonModQuizAttempt, AddonModQuizHelper } from '../../services/quiz-helper';
|
import { AddonModQuizHelper } from '../../services/quiz-helper';
|
||||||
import { AddonModQuizSync } from '../../services/quiz-sync';
|
import { AddonModQuizSync } from '../../services/quiz-sync';
|
||||||
import { CanLeave } from '@guards/can-leave';
|
import { CanLeave } from '@guards/can-leave';
|
||||||
import { CoreForms } from '@singletons/form';
|
import { CoreForms } from '@singletons/form';
|
||||||
|
@ -66,7 +66,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
@ViewChild('quizForm') formElement?: ElementRef;
|
@ViewChild('quizForm') formElement?: ElementRef;
|
||||||
|
|
||||||
quiz?: AddonModQuizQuizWSData; // The quiz the attempt belongs to.
|
quiz?: AddonModQuizQuizWSData; // The quiz the attempt belongs to.
|
||||||
attempt?: AddonModQuizAttempt; // The attempt being attempted.
|
attempt?: QuizAttempt; // The attempt being attempted.
|
||||||
moduleUrl?: string; // URL to the module in the site.
|
moduleUrl?: string; // URL to the module in the site.
|
||||||
component = ADDON_MOD_QUIZ_COMPONENT; // Component to link the files to.
|
component = ADDON_MOD_QUIZ_COMPONENT; // Component to link the files to.
|
||||||
loaded = false; // Whether data has been loaded.
|
loaded = false; // Whether data has been loaded.
|
||||||
|
@ -91,7 +91,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
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.
|
||||||
protected attemptAccessInfo?: AddonModQuizGetAttemptAccessInformationWSResponse; // Attempt access info.
|
protected attemptAccessInfo?: AddonModQuizGetAttemptAccessInformationWSResponse; // Attempt access info.
|
||||||
protected lastAttempt?: AddonModQuizAttemptWSData; // Last user attempt before a new one is created (if needed).
|
protected lastAttempt?: QuizAttempt; // Last user attempt before a new one is created (if needed).
|
||||||
protected newAttempt = false; // Whether the user is starting a new attempt.
|
protected newAttempt = false; // Whether the user is starting a new attempt.
|
||||||
protected quizDataLoaded = false; // Whether the quiz data has been loaded.
|
protected quizDataLoaded = false; // Whether the quiz data has been loaded.
|
||||||
protected timeUpCalled = false; // Whether the time up function has been called.
|
protected timeUpCalled = false; // Whether the time up function has been called.
|
||||||
|
@ -381,14 +381,9 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the last attempt. If it's finished, start a new one.
|
// Get the last attempt. If it's finished, start a new one.
|
||||||
this.lastAttempt = await AddonModQuizHelper.setAttemptCalculatedData(
|
this.lastAttempt = attempts[attempts.length - 1];
|
||||||
this.quiz,
|
|
||||||
this.quizAccessInfo,
|
this.lastAttempt.finishedOffline = await AddonModQuiz.isAttemptFinishedOffline(this.lastAttempt.id);
|
||||||
attempts[attempts.length - 1],
|
|
||||||
false,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.newAttempt = AddonModQuiz.isAttemptCompleted(this.lastAttempt.state);
|
this.newAttempt = AddonModQuiz.isAttemptCompleted(this.lastAttempt.state);
|
||||||
}
|
}
|
||||||
|
@ -945,3 +940,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
type QuizQuestion = CoreQuestionQuestionParsed & {
|
type QuizQuestion = CoreQuestionQuestionParsed & {
|
||||||
readableMark?: string;
|
readableMark?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt with some calculated data for the view.
|
||||||
|
*/
|
||||||
|
type QuizAttempt = AddonModQuizAttemptWSData & {
|
||||||
|
finishedOffline?: boolean;
|
||||||
|
};
|
||||||
|
|
|
@ -24,61 +24,7 @@
|
||||||
<!-- Review summary -->
|
<!-- Review summary -->
|
||||||
<ion-card *ngIf="attempt">
|
<ion-card *ngIf="attempt">
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-item class="ion-text-wrap">
|
<addon-mod-quiz-attempt-info [quiz]="quiz" [attempt]="attempt" [additionalData]="additionalData" />
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.startedon' | translate }}</p>
|
|
||||||
<p>{{ attempt.timestart! * 1000 | coreFormatDate }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.attemptstate' | translate }}</p>
|
|
||||||
<p>{{ readableState }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="showCompleted">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.completedon' | translate }}</p>
|
|
||||||
<p>{{ attempt.timefinish! * 1000 | coreFormatDate }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="timeTaken">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.attemptduration' | translate }}</p>
|
|
||||||
<p>{{ timeTaken }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="overTime">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.overdue' | translate }}</p>
|
|
||||||
<p>{{ overTime }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item *ngFor="let gradeItemMark of gradeItemMarks" class="ion-text-wrap">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ gradeItemMark.name }}</p>
|
|
||||||
<p>{{ gradeItemMark.grade }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="readableMark && attempt?.sumgrades !== null">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.marks' | translate }}</p>
|
|
||||||
<p>{{ readableMark }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="readableGrade">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.grade' | translate }}</p>
|
|
||||||
<p>{{ readableGrade }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item class="ion-text-wrap" *ngFor="let data of additionalData">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ data.title }}</p>
|
|
||||||
<core-format-text [component]="component" [componentId]="cmId" [text]="data.content" contextLevel="module"
|
|
||||||
[contextInstanceId]="cmId" [courseId]="courseId" />
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { IonContent } from '@ionic/angular';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { Translate } from '@singletons';
|
|
||||||
import { CoreDom } from '@singletons/dom';
|
import { CoreDom } from '@singletons/dom';
|
||||||
import { CoreTime } from '@singletons/time';
|
import { CoreTime } from '@singletons/time';
|
||||||
import {
|
import {
|
||||||
|
@ -31,14 +30,12 @@ import {
|
||||||
AddonModQuiz,
|
AddonModQuiz,
|
||||||
AddonModQuizAttemptWSData,
|
AddonModQuizAttemptWSData,
|
||||||
AddonModQuizCombinedReviewOptions,
|
AddonModQuizCombinedReviewOptions,
|
||||||
AddonModQuizGetAttemptReviewResponse,
|
|
||||||
AddonModQuizQuizWSData,
|
AddonModQuizQuizWSData,
|
||||||
AddonModQuizWSAdditionalData,
|
AddonModQuizWSAdditionalData,
|
||||||
} from '../../services/quiz';
|
} from '../../services/quiz';
|
||||||
import { AddonModQuizHelper } from '../../services/quiz-helper';
|
import { AddonModQuizHelper } from '../../services/quiz-helper';
|
||||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||||
import { AddonModQuizAttemptStates, ADDON_MOD_QUIZ_COMPONENT } from '../../constants';
|
import { ADDON_MOD_QUIZ_COMPONENT } from '../../constants';
|
||||||
import { QuestionDisplayOptionsMarks } from '@features/question/constants';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that allows reviewing a quiz attempt.
|
* Page that allows reviewing a quiz attempt.
|
||||||
|
@ -63,7 +60,6 @@ export class AddonModQuizReviewPage implements OnInit {
|
||||||
questions: QuizQuestion[] = []; // Questions of the current page.
|
questions: QuizQuestion[] = []; // Questions of the current page.
|
||||||
nextPage = -2; // Next page.
|
nextPage = -2; // Next page.
|
||||||
previousPage = -2; // Previous page.
|
previousPage = -2; // Previous page.
|
||||||
readableState?: string;
|
|
||||||
readableGrade?: string;
|
readableGrade?: string;
|
||||||
readableMark?: string;
|
readableMark?: string;
|
||||||
timeTaken?: string;
|
timeTaken?: string;
|
||||||
|
@ -159,6 +155,8 @@ export class AddonModQuizReviewPage implements OnInit {
|
||||||
|
|
||||||
this.options = await AddonModQuiz.getCombinedReviewOptions(this.quiz.id, { cmId: this.cmId });
|
this.options = await AddonModQuiz.getCombinedReviewOptions(this.quiz.id, { cmId: this.cmId });
|
||||||
|
|
||||||
|
AddonModQuizHelper.setQuizCalculatedData(this.quiz, this.options);
|
||||||
|
|
||||||
// Load the navigation data.
|
// Load the navigation data.
|
||||||
await this.loadNavigation();
|
await this.loadNavigation();
|
||||||
|
|
||||||
|
@ -178,15 +176,17 @@ export class AddonModQuizReviewPage implements OnInit {
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async loadPage(page: number): Promise<void> {
|
protected async loadPage(page: number): Promise<void> {
|
||||||
const data = await AddonModQuiz.getAttemptReview(this.attemptId, { page, cmId: this.quiz?.coursemodule });
|
if (!this.quiz) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.attempt = data.attempt;
|
const data = await AddonModQuiz.getAttemptReview(this.attemptId, { page, cmId: this.quiz.coursemodule });
|
||||||
|
|
||||||
|
this.attempt = await AddonModQuizHelper.setAttemptCalculatedData(this.quiz, data.attempt);
|
||||||
this.attempt.currentpage = page;
|
this.attempt.currentpage = page;
|
||||||
|
this.additionalData = data.additionaldata;
|
||||||
this.currentPage = page;
|
this.currentPage = page;
|
||||||
|
|
||||||
// Set the summary data.
|
|
||||||
this.setSummaryCalculatedData(data);
|
|
||||||
|
|
||||||
this.questions = data.questions;
|
this.questions = data.questions;
|
||||||
this.nextPage = page + 1;
|
this.nextPage = page + 1;
|
||||||
this.previousPage = page - 1;
|
this.previousPage = page - 1;
|
||||||
|
@ -254,91 +254,6 @@ export class AddonModQuizReviewPage implements OnInit {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate review summary data.
|
|
||||||
*
|
|
||||||
* @param data Result of getAttemptReview.
|
|
||||||
*/
|
|
||||||
protected setSummaryCalculatedData(data: AddonModQuizGetAttemptReviewResponse): void {
|
|
||||||
if (!this.attempt || !this.quiz) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.readableState = AddonModQuiz.getAttemptReadableStateName(this.attempt.state ?? '');
|
|
||||||
|
|
||||||
if (this.attempt.state !== AddonModQuizAttemptStates.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 = CoreTime.formatTime(timeTaken);
|
|
||||||
|
|
||||||
// Calculate overdue time.
|
|
||||||
if (this.quiz.timelimit && timeTaken > this.quiz.timelimit + 60) {
|
|
||||||
this.overTime = CoreTime.formatTime(timeTaken - this.quiz.timelimit);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.timeTaken = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat grade item marks.
|
|
||||||
if (this.attempt.sumgrades === null || !this.attempt.gradeitemmarks) {
|
|
||||||
this.gradeItemMarks = [];
|
|
||||||
} else {
|
|
||||||
this.gradeItemMarks = this.attempt.gradeitemmarks.map((gradeItemMark) => ({
|
|
||||||
name: gradeItemMark.name,
|
|
||||||
grade: Translate.instant('addon.mod_quiz.outof', { $a: {
|
|
||||||
grade: AddonModQuiz.formatGrade(gradeItemMark.grade, this.quiz?.decimalpoints),
|
|
||||||
maxgrade: AddonModQuiz.formatGrade(gradeItemMark.maxgrade, this.quiz?.decimalpoints),
|
|
||||||
} }),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat grade.
|
|
||||||
if (this.options && this.options.someoptions.marks >= QuestionDisplayOptionsMarks.MARK_AND_MAX &&
|
|
||||||
AddonModQuiz.quizHasGrades(this.quiz)) {
|
|
||||||
|
|
||||||
if (data.grade === null || data.grade === undefined) {
|
|
||||||
this.readableGrade = AddonModQuiz.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.instant('addon.mod_quiz.outofshort', { $a: {
|
|
||||||
grade: AddonModQuiz.formatGrade(this.attempt.sumgrades, this.quiz.decimalpoints),
|
|
||||||
maxgrade: AddonModQuiz.formatGrade(this.quiz.sumgrades, this.quiz.decimalpoints),
|
|
||||||
} });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now the scaled grade.
|
|
||||||
const gradeObject: Record<string, unknown> = {
|
|
||||||
grade: AddonModQuiz.formatGrade(Number(data.grade), this.quiz.decimalpoints),
|
|
||||||
maxgrade: AddonModQuiz.formatGrade(this.quiz.grade, this.quiz.decimalpoints),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.quiz.grade != 100) {
|
|
||||||
gradeObject.percent = AddonModQuiz.formatGrade(
|
|
||||||
(this.attempt.sumgrades ?? 0) * 100 / (this.quiz.sumgrades ?? 1),
|
|
||||||
this.quiz.decimalpoints,
|
|
||||||
);
|
|
||||||
this.readableGrade = Translate.instant('addon.mod_quiz.outofpercent', { $a: gradeObject });
|
|
||||||
} else {
|
|
||||||
this.readableGrade = Translate.instant('addon.mod_quiz.outof', { $a: gradeObject });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat additional data.
|
|
||||||
this.additionalData.forEach((data) => {
|
|
||||||
// Remove help links from additional data.
|
|
||||||
data.content = CoreDomUtils.removeElementFromHtml(data.content, '.helptooltip');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch mode: all questions in same page OR one page at a time.
|
* Switch mode: all questions in same page OR one page at a time.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { AddonModQuizComponentsModule } from './components/components.module';
|
import { AddonModQuizComponentsModule } from './components/components.module';
|
||||||
|
|
||||||
import { AddonModQuizIndexPage } from './pages/index';
|
import { AddonModQuizIndexPage } from './pages/index';
|
||||||
import { AddonModQuizAttemptPage } from '@addons/mod/quiz/pages/attempt/attempt';
|
|
||||||
import { CoreQuestionComponentsModule } from '@features/question/components/components.module';
|
import { CoreQuestionComponentsModule } from '@features/question/components/components.module';
|
||||||
import { AddonModQuizPlayerPage } from '@addons/mod/quiz/pages/player/player';
|
import { AddonModQuizPlayerPage } from '@addons/mod/quiz/pages/player/player';
|
||||||
import { canLeaveGuard } from '@guards/can-leave';
|
import { canLeaveGuard } from '@guards/can-leave';
|
||||||
|
@ -35,10 +34,6 @@ const routes: Routes = [
|
||||||
component: AddonModQuizPlayerPage,
|
component: AddonModQuizPlayerPage,
|
||||||
canDeactivate: [canLeaveGuard],
|
canDeactivate: [canLeaveGuard],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: ':courseId/:cmId/attempt/:attemptId',
|
|
||||||
component: AddonModQuizAttemptPage,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: ':courseId/:cmId/review/:attemptId',
|
path: ':courseId/:cmId/review/:attemptId',
|
||||||
component: AddonModQuizReviewPage,
|
component: AddonModQuizReviewPage,
|
||||||
|
@ -54,7 +49,6 @@ const routes: Routes = [
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AddonModQuizIndexPage,
|
AddonModQuizIndexPage,
|
||||||
AddonModQuizAttemptPage,
|
|
||||||
AddonModQuizPlayerPage,
|
AddonModQuizPlayerPage,
|
||||||
AddonModQuizReviewPage,
|
AddonModQuizReviewPage,
|
||||||
],
|
],
|
||||||
|
|
|
@ -359,53 +359,28 @@ export class AddonModQuizHelperProvider {
|
||||||
* Add some calculated data to the attempt.
|
* Add some calculated data to the attempt.
|
||||||
*
|
*
|
||||||
* @param quiz Quiz.
|
* @param quiz Quiz.
|
||||||
* @param accessInfo Quiz access info.
|
|
||||||
* @param attempt Attempt.
|
* @param attempt Attempt.
|
||||||
* @param highlight Whether we should check if attempt should be highlighted.
|
|
||||||
* @param bestGrade Quiz's best grade (formatted). Required if highlight=true.
|
|
||||||
* @param isLastAttempt Whether the attempt is the last one.
|
|
||||||
* @param siteId Site ID.
|
* @param siteId Site ID.
|
||||||
* @returns Quiz attemptw with calculated data.
|
* @returns Quiz attempt with calculated data.
|
||||||
*/
|
*/
|
||||||
async setAttemptCalculatedData(
|
async setAttemptCalculatedData(
|
||||||
quiz: AddonModQuizQuizData,
|
quiz: AddonModQuizQuizData,
|
||||||
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
|
||||||
attempt: AddonModQuizAttemptWSData,
|
attempt: AddonModQuizAttemptWSData,
|
||||||
highlight?: boolean,
|
|
||||||
bestGrade?: string,
|
|
||||||
isLastAttempt?: boolean,
|
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<AddonModQuizAttempt> {
|
): Promise<AddonModQuizAttempt> {
|
||||||
const formattedAttempt = <AddonModQuizAttempt> attempt;
|
const formattedAttempt = <AddonModQuizAttempt> attempt;
|
||||||
|
|
||||||
formattedAttempt.rescaledGrade = AddonModQuiz.rescaleGrade(attempt.sumgrades, quiz, false);
|
formattedAttempt.finished = attempt.state === AddonModQuizAttemptStates.FINISHED;
|
||||||
formattedAttempt.completed = AddonModQuiz.isAttemptCompleted(attempt.state);
|
formattedAttempt.completed = AddonModQuiz.isAttemptCompleted(attempt.state);
|
||||||
formattedAttempt.readableState = AddonModQuiz.getAttemptReadableState(quiz, attempt);
|
formattedAttempt.rescaledGrade = Number(AddonModQuiz.rescaleGrade(attempt.sumgrades, quiz, false));
|
||||||
|
|
||||||
if (quiz.showMarkColumn && formattedAttempt.completed) {
|
if (quiz.showAttemptsGrades && formattedAttempt.finished) {
|
||||||
formattedAttempt.readableMark = AddonModQuiz.formatGrade(attempt.sumgrades, quiz.decimalpoints);
|
formattedAttempt.formattedGrade = AddonModQuiz.formatGrade(formattedAttempt.rescaledGrade, quiz.decimalpoints);
|
||||||
} else {
|
} else {
|
||||||
formattedAttempt.readableMark = '';
|
formattedAttempt.formattedGrade = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quiz.showGradeColumn && formattedAttempt.completed) {
|
formattedAttempt.finishedOffline = await AddonModQuiz.isAttemptFinishedOffline(attempt.id, siteId);
|
||||||
formattedAttempt.readableGrade = AddonModQuiz.formatGrade(
|
|
||||||
Number(formattedAttempt.rescaledGrade),
|
|
||||||
quiz.decimalpoints,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Highlight the highest grade if appropriate.
|
|
||||||
formattedAttempt.highlightGrade = !!(highlight && !attempt.preview &&
|
|
||||||
attempt.state === AddonModQuizAttemptStates.FINISHED && formattedAttempt.readableGrade == bestGrade);
|
|
||||||
} else {
|
|
||||||
formattedAttempt.readableGrade = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLastAttempt || isLastAttempt === undefined) {
|
|
||||||
formattedAttempt.finishedOffline = await AddonModQuiz.isAttemptFinishedOffline(attempt.id, siteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedAttempt.canReview = await this.canReviewAttempt(quiz, accessInfo, attempt);
|
|
||||||
|
|
||||||
return formattedAttempt;
|
return formattedAttempt;
|
||||||
}
|
}
|
||||||
|
@ -423,11 +398,10 @@ export class AddonModQuizHelperProvider {
|
||||||
formattedQuiz.sumGradesFormatted = AddonModQuiz.formatGrade(quiz.sumgrades, quiz.decimalpoints);
|
formattedQuiz.sumGradesFormatted = AddonModQuiz.formatGrade(quiz.sumgrades, quiz.decimalpoints);
|
||||||
formattedQuiz.gradeFormatted = AddonModQuiz.formatGrade(quiz.grade, quiz.decimalpoints);
|
formattedQuiz.gradeFormatted = AddonModQuiz.formatGrade(quiz.grade, quiz.decimalpoints);
|
||||||
|
|
||||||
formattedQuiz.showAttemptColumn = quiz.attempts != 1;
|
formattedQuiz.showAttemptsGrades = options.someoptions.marks >= QuestionDisplayOptionsMarks.MARK_AND_MAX &&
|
||||||
formattedQuiz.showGradeColumn = options.someoptions.marks >= QuestionDisplayOptionsMarks.MARK_AND_MAX &&
|
|
||||||
AddonModQuiz.quizHasGrades(quiz);
|
AddonModQuiz.quizHasGrades(quiz);
|
||||||
formattedQuiz.showMarkColumn = formattedQuiz.showGradeColumn && quiz.grade != quiz.sumgrades;
|
formattedQuiz.showAttemptsMarks = formattedQuiz.showAttemptsGrades && quiz.grade !== quiz.sumgrades;
|
||||||
formattedQuiz.showFeedbackColumn = !!quiz.hasfeedback && !!options.alloptions.overallfeedback;
|
formattedQuiz.showFeedback = !!quiz.hasfeedback && !!options.alloptions.overallfeedback;
|
||||||
|
|
||||||
return formattedQuiz;
|
return formattedQuiz;
|
||||||
}
|
}
|
||||||
|
@ -523,10 +497,9 @@ export const AddonModQuizHelper = makeSingleton(AddonModQuizHelperProvider);
|
||||||
export type AddonModQuizQuizData = AddonModQuizQuizWSData & {
|
export type AddonModQuizQuizData = AddonModQuizQuizWSData & {
|
||||||
sumGradesFormatted?: string;
|
sumGradesFormatted?: string;
|
||||||
gradeFormatted?: string;
|
gradeFormatted?: string;
|
||||||
showAttemptColumn?: boolean;
|
showAttemptsGrades?: boolean;
|
||||||
showGradeColumn?: boolean;
|
showAttemptsMarks?: boolean;
|
||||||
showMarkColumn?: boolean;
|
showFeedback?: boolean;
|
||||||
showFeedbackColumn?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -534,11 +507,8 @@ export type AddonModQuizQuizData = AddonModQuizQuizWSData & {
|
||||||
*/
|
*/
|
||||||
export type AddonModQuizAttempt = AddonModQuizAttemptWSData & {
|
export type AddonModQuizAttempt = AddonModQuizAttemptWSData & {
|
||||||
finishedOffline?: boolean;
|
finishedOffline?: boolean;
|
||||||
rescaledGrade?: string;
|
rescaledGrade?: number;
|
||||||
|
finished?: boolean;
|
||||||
completed?: boolean;
|
completed?: boolean;
|
||||||
readableState?: string[];
|
formattedGrade?: string;
|
||||||
readableMark?: string;
|
|
||||||
readableGrade?: string;
|
|
||||||
highlightGrade?: boolean;
|
|
||||||
canReview?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -37,7 +37,6 @@ import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWar
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
|
import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
|
||||||
import { AddonModQuizAttempt } from './quiz-helper';
|
|
||||||
import { AddonModQuizOffline, AddonModQuizQuestionsWithAnswers } from './quiz-offline';
|
import { AddonModQuizOffline, AddonModQuizQuestionsWithAnswers } from './quiz-offline';
|
||||||
import { AddonModQuizAutoSyncData, AddonModQuizSyncProvider } from './quiz-sync';
|
import { AddonModQuizAutoSyncData, AddonModQuizSyncProvider } from './quiz-sync';
|
||||||
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
||||||
|
@ -55,6 +54,7 @@ import {
|
||||||
AddonModQuizDisplayOptionsAttemptStates,
|
AddonModQuizDisplayOptionsAttemptStates,
|
||||||
ADDON_MOD_QUIZ_IMMEDIATELY_AFTER_PERIOD,
|
ADDON_MOD_QUIZ_IMMEDIATELY_AFTER_PERIOD,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
import { CoreIonicColorNames } from '@singletons/colors';
|
||||||
|
|
||||||
declare module '@singletons/events' {
|
declare module '@singletons/events' {
|
||||||
|
|
||||||
|
@ -412,61 +412,17 @@ export class AddonModQuizProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turn attempt's state into a readable state, including some extra data depending on the state.
|
* Turn attempt's state into a readable state name.
|
||||||
*
|
|
||||||
* @param quiz Quiz.
|
|
||||||
* @param attempt Attempt.
|
|
||||||
* @returns List of state sentences.
|
|
||||||
*/
|
|
||||||
getAttemptReadableState(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttempt): string[] {
|
|
||||||
if (attempt.finishedOffline) {
|
|
||||||
return [Translate.instant('addon.mod_quiz.finishnotsynced')];
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (attempt.state) {
|
|
||||||
case AddonModQuizAttemptStates.IN_PROGRESS:
|
|
||||||
return [Translate.instant('addon.mod_quiz.stateinprogress')];
|
|
||||||
|
|
||||||
case AddonModQuizAttemptStates.OVERDUE: {
|
|
||||||
const sentences: string[] = [];
|
|
||||||
const dueDate = this.getAttemptDueDate(quiz, attempt);
|
|
||||||
|
|
||||||
sentences.push(Translate.instant('addon.mod_quiz.stateoverdue'));
|
|
||||||
|
|
||||||
if (dueDate) {
|
|
||||||
sentences.push(Translate.instant(
|
|
||||||
'addon.mod_quiz.stateoverduedetails',
|
|
||||||
{ $a: CoreTimeUtils.userDate(dueDate) },
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return sentences;
|
|
||||||
}
|
|
||||||
|
|
||||||
case AddonModQuizAttemptStates.FINISHED:
|
|
||||||
return [
|
|
||||||
Translate.instant('addon.mod_quiz.statefinished'),
|
|
||||||
Translate.instant(
|
|
||||||
'addon.mod_quiz.statefinisheddetails',
|
|
||||||
{ $a: CoreTimeUtils.userDate((attempt.timefinish ?? 0) * 1000) },
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
case AddonModQuizAttemptStates.ABANDONED:
|
|
||||||
return [Translate.instant('addon.mod_quiz.stateabandoned')];
|
|
||||||
|
|
||||||
default:
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turn attempt's state into a readable state name, without any more data.
|
|
||||||
*
|
*
|
||||||
* @param state State.
|
* @param state State.
|
||||||
|
* @param finishedOffline Whether the attempt was finished offline.
|
||||||
* @returns Readable state name.
|
* @returns Readable state name.
|
||||||
*/
|
*/
|
||||||
getAttemptReadableStateName(state: string): string {
|
getAttemptReadableStateName(state: string, finishedOffline = false): string {
|
||||||
|
if (finishedOffline) {
|
||||||
|
return Translate.instant('core.submittedoffline');
|
||||||
|
}
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case AddonModQuizAttemptStates.IN_PROGRESS:
|
case AddonModQuizAttemptStates.IN_PROGRESS:
|
||||||
return Translate.instant('addon.mod_quiz.stateinprogress');
|
return Translate.instant('addon.mod_quiz.stateinprogress');
|
||||||
|
@ -485,6 +441,36 @@ export class AddonModQuizProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the color to apply to the attempt state.
|
||||||
|
*
|
||||||
|
* @param state State.
|
||||||
|
* @param finishedOffline Whether the attempt was finished offline.
|
||||||
|
* @returns State color.
|
||||||
|
*/
|
||||||
|
getAttemptStateColor(state: string, finishedOffline = false): string {
|
||||||
|
if (finishedOffline) {
|
||||||
|
return CoreIonicColorNames.MEDIUM;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case AddonModQuizAttemptStates.IN_PROGRESS:
|
||||||
|
return CoreIonicColorNames.WARNING;
|
||||||
|
|
||||||
|
case AddonModQuizAttemptStates.OVERDUE:
|
||||||
|
return CoreIonicColorNames.INFO;
|
||||||
|
|
||||||
|
case AddonModQuizAttemptStates.FINISHED:
|
||||||
|
return CoreIonicColorNames.SUCCESS;
|
||||||
|
|
||||||
|
case AddonModQuizAttemptStates.ABANDONED:
|
||||||
|
return CoreIonicColorNames.DANGER;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cache key for get attempt review WS calls.
|
* Get cache key for get attempt review WS calls.
|
||||||
*
|
*
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"grade": "Grade",
|
"grade": "Grade",
|
||||||
"gradebook": "Gradebook",
|
"gradebook": "Gradebook",
|
||||||
"gradeitem": "Grade item",
|
"gradeitem": "Grade item",
|
||||||
|
"gradelong": "{{$a.grade}} / {{$a.max}}",
|
||||||
"gradepass": "Grade to pass",
|
"gradepass": "Grade to pass",
|
||||||
"grades": "Grades",
|
"grades": "Grades",
|
||||||
"lettergrade": "Letter grade",
|
"lettergrade": "Letter grade",
|
||||||
|
|
|
@ -327,6 +327,7 @@
|
||||||
"strftimetime12": "%I:%M %p",
|
"strftimetime12": "%I:%M %p",
|
||||||
"strftimetime24": "%H:%M",
|
"strftimetime24": "%H:%M",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
|
"submittedoffline": "Submitted (Offline)",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"summary": "Summary",
|
"summary": "Summary",
|
||||||
"swipenavigationtourdescription": "Swipe left and right to navigate around.",
|
"swipenavigationtourdescription": "Swipe left and right to navigate around.",
|
||||||
|
|
Loading…
Reference in New Issue