commit
023db99e2b
|
@ -872,6 +872,8 @@
|
||||||
"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.attemptfirst": "quiz",
|
"addon.mod_quiz.attemptfirst": "quiz",
|
||||||
"addon.mod_quiz.attemptlast": "quiz",
|
"addon.mod_quiz.attemptlast": "quiz",
|
||||||
"addon.mod_quiz.attemptnumber": "quiz",
|
"addon.mod_quiz.attemptnumber": "quiz",
|
||||||
|
@ -901,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",
|
||||||
|
@ -912,6 +914,8 @@
|
||||||
"addon.mod_quiz.mustbesubmittedby": "quiz",
|
"addon.mod_quiz.mustbesubmittedby": "quiz",
|
||||||
"addon.mod_quiz.noquestions": "quiz",
|
"addon.mod_quiz.noquestions": "quiz",
|
||||||
"addon.mod_quiz.noreviewattempt": "quiz",
|
"addon.mod_quiz.noreviewattempt": "quiz",
|
||||||
|
"addon.mod_quiz.noreviewuntil": "quiz",
|
||||||
|
"addon.mod_quiz.noreviewuntilshort": "quiz",
|
||||||
"addon.mod_quiz.notyetgraded": "quiz",
|
"addon.mod_quiz.notyetgraded": "quiz",
|
||||||
"addon.mod_quiz.opentoc": "local_moodlemobileapp",
|
"addon.mod_quiz.opentoc": "local_moodlemobileapp",
|
||||||
"addon.mod_quiz.outof": "quiz",
|
"addon.mod_quiz.outof": "quiz",
|
||||||
|
@ -945,7 +949,6 @@
|
||||||
"addon.mod_quiz.summaryofattempt": "quiz",
|
"addon.mod_quiz.summaryofattempt": "quiz",
|
||||||
"addon.mod_quiz.summaryofattempts": "quiz",
|
"addon.mod_quiz.summaryofattempts": "quiz",
|
||||||
"addon.mod_quiz.timeleft": "quiz",
|
"addon.mod_quiz.timeleft": "quiz",
|
||||||
"addon.mod_quiz.timetaken": "quiz",
|
|
||||||
"addon.mod_quiz.unit": "quiz",
|
"addon.mod_quiz.unit": "quiz",
|
||||||
"addon.mod_quiz.warningattemptfinished": "local_moodlemobileapp",
|
"addon.mod_quiz.warningattemptfinished": "local_moodlemobileapp",
|
||||||
"addon.mod_quiz.warningdatadiscarded": "local_moodlemobileapp",
|
"addon.mod_quiz.warningdatadiscarded": "local_moodlemobileapp",
|
||||||
|
@ -1864,6 +1867,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 +2564,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",
|
||||||
|
|
|
@ -15,8 +15,9 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { AddonModQuizAccessRuleHandler } from '@addons/mod/quiz/services/access-rules-delegate';
|
import { AddonModQuizAccessRuleHandler } from '@addons/mod/quiz/services/access-rules-delegate';
|
||||||
import { AddonModQuizAttemptWSData, AddonModQuizProvider } from '@addons/mod/quiz/services/quiz';
|
import { AddonModQuizAttemptWSData } from '@addons/mod/quiz/services/quiz';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { ADDON_MOD_QUIZ_SHOW_TIME_BEFORE_DEADLINE } from '@addons/mod/quiz/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to support open/close date access rule.
|
* Handler to support open/close date access rule.
|
||||||
|
@ -50,8 +51,8 @@ export class AddonModQuizAccessOpenCloseDateHandlerService implements AddonModQu
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the time left only if it's less than QUIZ_SHOW_TIME_BEFORE_DEADLINE.
|
// Show the time left only if it's less than ADDON_MOD_QUIZ_SHOW_TIME_BEFORE_DEADLINE.
|
||||||
if (timeNow > endTime - AddonModQuizProvider.QUIZ_SHOW_TIME_BEFORE_DEADLINE) {
|
if (timeNow > endTime - ADDON_MOD_QUIZ_SHOW_TIME_BEFORE_DEADLINE) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,81 +45,52 @@
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<!-- List of user attempts. -->
|
<!-- List of user attempts. -->
|
||||||
<ion-card class="addon-mod_quiz-table" *ngIf="quiz && attempts.length">
|
@if (quiz && attempts.length) {
|
||||||
|
<ion-card class="addon-mod_quiz-attempts-summary">
|
||||||
<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-item class="ion-text-wrap addon-mod_quiz-table-header hide-detail" [detail]="true">
|
|
||||||
<ion-label role="rowgroup">
|
|
||||||
<ion-row class="ion-align-items-center" role="row">
|
|
||||||
<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-row class="ion-align-items-center" role="row">
|
|
||||||
<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-item>
|
|
||||||
</div>
|
|
||||||
</ion-card-content>
|
|
||||||
</ion-card>
|
|
||||||
|
|
||||||
<!-- Result info. -->
|
<!-- Quiz result info. -->
|
||||||
<ion-card *ngIf="quiz && showResults &&
|
@if (quiz && showResults && (gradeResult || gradeOverridden || gradebookFeedback || (quiz.showFeedback && overallFeedback))) {
|
||||||
(gradeResult || gradeOverridden || gradebookFeedback || (quiz.showFeedbackColumn && overallFeedback))">
|
|
||||||
<ion-list>
|
@if (overallStats && gradeResult) {
|
||||||
<ion-item class="ion-text-wrap" *ngIf="gradeResult">
|
<ion-item class="ion-text-wrap addon-mod_quiz-grade-result">
|
||||||
<ion-label>{{ gradeResult }}</ion-label>
|
<ion-label>
|
||||||
|
<div class="addon-mod_quiz-grade-result-grade">
|
||||||
|
@if (moreAttempts) {
|
||||||
|
<span>{{ gradeMethodReadable }}</span>
|
||||||
|
<span>{{ gradeResult }}</span>
|
||||||
|
} @else {
|
||||||
|
<span>{{ 'addon.mod_quiz.yourfinalgradeis' | translate:{ $a: gradeResult } }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (gradeOverridden) {
|
||||||
|
<p class="addon-mod_quiz-grade-overridden-notice">
|
||||||
|
<ion-icon name="fas-circle-info" color="info" slot="start" aria-hidden="true" />
|
||||||
|
{{ 'core.course.overriddennotice' | translate }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="gradeOverridden">
|
}
|
||||||
<ion-label>{{ 'core.course.overriddennotice' | translate }}</ion-label>
|
|
||||||
</ion-item>
|
@if (gradebookFeedback) {
|
||||||
<ion-item class="ion-text-wrap" *ngIf="gradebookFeedback">
|
<ion-item class="ion-text-wrap addon-mod_quiz-gradebook-feedback">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p class="item-heading">{{ 'addon.mod_quiz.comment' | translate }}</p>
|
<p class="item-heading">{{ 'addon.mod_quiz.comment' | translate }}</p>
|
||||||
<p>
|
<p>
|
||||||
<core-format-text [component]="component" [componentId]="componentId" [text]="gradebookFeedback"
|
<core-format-text [component]="component" [componentId]="componentId" [text]="gradebookFeedback" contextLevel="module"
|
||||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId" />
|
[contextInstanceId]="module.id" [courseId]="courseId" />
|
||||||
</p>
|
</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="quiz.showFeedbackColumn && overallFeedback">
|
}
|
||||||
|
|
||||||
|
@if (quiz.showFeedback && overallFeedback) {
|
||||||
|
<hr>
|
||||||
|
<ion-item class="ion-text-wrap addon-mod_quiz-overall-feedback">
|
||||||
<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>
|
||||||
|
@ -128,8 +99,69 @@
|
||||||
</p>
|
</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
<ion-accordion-group>
|
||||||
|
@for (attempt of attempts; track attempt.id) {
|
||||||
|
<ion-accordion [value]="attempt.id" toggleIconSlot="start">
|
||||||
|
<ion-item slot="header" class="ion-text-wrap addon-mod_quiz-attempt-title" lines="none">
|
||||||
|
<ion-label>
|
||||||
|
<h3>{{ 'addon.mod_quiz.attempt' | translate:{ $a: attempt.attempt } }}</h3>
|
||||||
|
</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>
|
||||||
|
<div class="addon-mod_quiz-attempt-details" slot="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 && attempt.cannotReviewMessage) {
|
||||||
|
<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" />
|
||||||
|
{{ attempt.cannotReviewMessage }}
|
||||||
|
</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>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- More data. -->
|
<!-- More data. -->
|
||||||
<ng-container *ngIf="quiz">
|
<ng-container *ngIf="quiz">
|
||||||
|
|
|
@ -1,33 +1,75 @@
|
||||||
|
@use "theme/globals" as *;
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
|
|
||||||
.addon-mod_quiz-table {
|
.addon-mod_quiz-attempts-summary {
|
||||||
ion-card-content {
|
ion-card-header {
|
||||||
padding-left: 0;
|
border-bottom: 1px solid var(--stroke);
|
||||||
padding-right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item:nth-child(even) {
|
.addon-mod_quiz-grade-result {
|
||||||
--background: var(--light);
|
margin-top: var(--mdl-spacing-2);
|
||||||
|
|
||||||
|
|
||||||
|
.addon-mod_quiz-grade-overridden-notice {
|
||||||
|
margin-top: var(--mdl-spacing-2);
|
||||||
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addon-mod_quiz-highlighted,
|
.addon-mod_quiz-grade-result-grade {
|
||||||
.item.addon-mod_quiz-highlighted,
|
display: flex;
|
||||||
.addon-mod_quiz-highlighted p,
|
|
||||||
.item.addon-mod_quiz-highlighted p {
|
span:first-child {
|
||||||
--background: var(--primary-tint);
|
flex-grow: 1;
|
||||||
color: var(--primary-shade);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(html.dark) {
|
|
||||||
.addon-mod_quiz-table {
|
|
||||||
.addon-mod_quiz-highlighted,
|
|
||||||
.item.addon-mod_quiz-highlighted,
|
|
||||||
.addon-mod_quiz-highlighted p,
|
|
||||||
.item.addon-mod_quiz-highlighted p {
|
|
||||||
--background: var(--primary-shade);
|
|
||||||
color: var(--primary-tint);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-mod_quiz-attempt-title-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 60px;
|
||||||
|
padding-top: var(--mdl-spacing-2);
|
||||||
|
padding-bottom: var(--mdl-spacing-2);
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0px;
|
||||||
|
margin-top: var(--mdl-spacing-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-accordion-group {
|
||||||
|
border-top: 1px solid var(--stroke);
|
||||||
|
|
||||||
|
.accordion-expanded .addon-mod_quiz-attempt-title-info,
|
||||||
|
.accordion-expanding .addon-mod_quiz-attempt-title-info {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
background-color: var(--stroke);
|
||||||
|
height: 1px;
|
||||||
|
margin: 0px var(-mdl-spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,7 +36,7 @@ import {
|
||||||
AddonModQuizGetAttemptAccessInformationWSResponse,
|
AddonModQuizGetAttemptAccessInformationWSResponse,
|
||||||
AddonModQuizGetQuizAccessInformationWSResponse,
|
AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
AddonModQuizGetUserBestGradeWSResponse,
|
AddonModQuizGetUserBestGradeWSResponse,
|
||||||
AddonModQuizProvider,
|
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 {
|
||||||
|
@ -45,6 +45,8 @@ import {
|
||||||
AddonModQuizSyncProvider,
|
AddonModQuizSyncProvider,
|
||||||
AddonModQuizSyncResult,
|
AddonModQuizSyncResult,
|
||||||
} from '../../services/quiz-sync';
|
} from '../../services/quiz-sync';
|
||||||
|
import { ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT, ADDON_MOD_QUIZ_COMPONENT, AddonModQuizAttemptStates } from '../../constants';
|
||||||
|
import { QuestionDisplayOptionsMarks } from '@features/question/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays a quiz entry page.
|
* Component that displays a quiz entry page.
|
||||||
|
@ -56,7 +58,7 @@ import {
|
||||||
})
|
})
|
||||||
export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
|
export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
component = AddonModQuizProvider.COMPONENT;
|
component = ADDON_MOD_QUIZ_COMPONENT;
|
||||||
pluginName = 'quiz';
|
pluginName = 'quiz';
|
||||||
quiz?: AddonModQuizQuizData; // The quiz.
|
quiz?: AddonModQuizQuizData; // The quiz.
|
||||||
now?: number; // Current time.
|
now?: number; // Current time.
|
||||||
|
@ -77,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.
|
||||||
|
@ -110,7 +111,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
|
|
||||||
// Listen for attempt finished events.
|
// Listen for attempt finished events.
|
||||||
this.finishedObserver = CoreEvents.on(
|
this.finishedObserver = CoreEvents.on(
|
||||||
AddonModQuizProvider.ATTEMPT_FINISHED_EVENT,
|
ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT,
|
||||||
(data) => {
|
(data) => {
|
||||||
// Go to review attempt if an attempt in this quiz was finished and synced.
|
// Go to review attempt if an attempt in this quiz was finished and synced.
|
||||||
if (this.quiz && data.quizId == this.quiz.id) {
|
if (this.quiz && data.quizId == this.quiz.id) {
|
||||||
|
@ -195,15 +196,9 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
if (AddonModQuiz.isQuizOffline(quiz)) {
|
if (AddonModQuiz.isQuizOffline(quiz)) {
|
||||||
if (sync) {
|
if (sync) {
|
||||||
// Try to sync the quiz.
|
// Try to sync the quiz.
|
||||||
try {
|
await CoreUtils.ignoreErrors(this.syncActivity(showErrors));
|
||||||
await this.syncActivity(showErrors);
|
|
||||||
} catch {
|
|
||||||
// Ignore errors, keep getting data even if sync fails.
|
|
||||||
this.autoReview = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.autoReview = undefined;
|
|
||||||
this.showStatusSpinner = false;
|
this.showStatusSpinner = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +228,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
this.unsupportedQuestions = AddonModQuiz.getUnsupportedQuestions(types);
|
this.unsupportedQuestions = AddonModQuiz.getUnsupportedQuestions(types);
|
||||||
this.hasSupportedQuestions = !!types.find((type) => type != 'random' && this.unsupportedQuestions.indexOf(type) == -1);
|
this.hasSupportedQuestions = !!types.find((type) => type != 'random' && this.unsupportedQuestions.indexOf(type) == -1);
|
||||||
|
|
||||||
await this.getAttempts(quiz);
|
await this.getAttempts(quiz, this.quizAccessInfo);
|
||||||
|
|
||||||
// Quiz is ready to be shown, move it to the variable that is displayed.
|
// Quiz is ready to be shown, move it to the variable that is displayed.
|
||||||
this.quiz = quiz;
|
this.quiz = quiz;
|
||||||
|
@ -245,7 +240,10 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
* @param quiz Quiz instance.
|
* @param quiz Quiz instance.
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async getAttempts(quiz: AddonModQuizQuizData): Promise<void> {
|
protected async getAttempts(
|
||||||
|
quiz: AddonModQuizQuizData,
|
||||||
|
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
|
): Promise<void> {
|
||||||
// Always get the best grade because it includes the grade to pass.
|
// Always get the best grade because it includes the grade to pass.
|
||||||
this.bestGrade = await AddonModQuiz.getUserBestGrade(quiz.id, { cmId: this.module.id });
|
this.bestGrade = await AddonModQuiz.getUserBestGrade(quiz.id, { cmId: this.module.id });
|
||||||
|
|
||||||
|
@ -255,12 +253,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
// Get attempts.
|
// Get attempts.
|
||||||
const attempts = await AddonModQuiz.getUserAttempts(quiz.id, { cmId: this.module.id });
|
const attempts = await AddonModQuiz.getUserAttempts(quiz.id, { cmId: this.module.id });
|
||||||
|
|
||||||
this.attempts = await this.treatAttempts(quiz, attempts);
|
this.attempts = await this.treatAttempts(quiz, accessInfo, attempts);
|
||||||
|
|
||||||
// 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.isAttemptFinished(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;
|
||||||
}
|
}
|
||||||
|
@ -279,7 +277,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
this.buttonText = '';
|
this.buttonText = '';
|
||||||
|
|
||||||
if (quiz.hasquestions !== 0) {
|
if (quiz.hasquestions !== 0) {
|
||||||
if (this.attempts.length && !AddonModQuiz.isAttemptFinished(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';
|
||||||
|
@ -327,7 +325,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;
|
||||||
|
|
||||||
|
@ -350,25 +348,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
gradeToShow = formattedBestGrade;
|
gradeToShow = formattedBestGrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.overallStats) {
|
this.gradeResult = Translate.instant('core.grades.gradelong', { $a: {
|
||||||
// Show the quiz grade. The message shown is different if the quiz is finished.
|
|
||||||
if (this.moreAttempts) {
|
|
||||||
this.gradeResult = Translate.instant('addon.mod_quiz.gradesofar', { $a: {
|
|
||||||
method: this.gradeMethodReadable,
|
|
||||||
mygrade: gradeToShow,
|
|
||||||
quizgrade: quiz.gradeFormatted,
|
|
||||||
} });
|
|
||||||
} else {
|
|
||||||
const outOfShort = Translate.instant('addon.mod_quiz.outofshort', { $a: {
|
|
||||||
grade: gradeToShow,
|
grade: gradeToShow,
|
||||||
maxgrade: quiz.gradeFormatted,
|
max: quiz.gradeFormatted,
|
||||||
} });
|
} });
|
||||||
|
|
||||||
this.gradeResult = Translate.instant('addon.mod_quiz.yourfinalgradeis', { $a: outOfShort });
|
if (quiz.showFeedback) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (quiz.showFeedbackColumn) {
|
|
||||||
// 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,
|
||||||
|
@ -396,7 +381,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
*
|
*
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async goToAutoReview(): Promise<void> {
|
protected async goToAutoReview(attempts: AddonModQuizAttemptWSData[]): Promise<void> {
|
||||||
if (!this.autoReview) {
|
if (!this.autoReview) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -405,20 +390,19 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
this.checkCompletion();
|
this.checkCompletion();
|
||||||
|
|
||||||
// Verify that user can see the review.
|
// Verify that user can see the review.
|
||||||
const attemptId = this.autoReview.attemptId;
|
const attempt = attempts.find(attempt => attempt.id === this.autoReview?.attemptId);
|
||||||
this.autoReview = undefined;
|
this.autoReview = undefined;
|
||||||
|
|
||||||
if (this.quizAccessInfo?.canreviewmyattempts) {
|
if (!this.quiz || !this.quizAccessInfo || !attempt) {
|
||||||
try {
|
return;
|
||||||
await AddonModQuiz.getAttemptReview(attemptId, { page: -1, cmId: this.module.id });
|
}
|
||||||
|
|
||||||
await CoreNavigator.navigateToSitePath(
|
const canReview = await AddonModQuizHelper.canReviewAttempt(this.quiz, this.quizAccessInfo, attempt);
|
||||||
`${AddonModQuizModuleHandlerService.PAGE_NAME}/${this.courseId}/${this.module.id}/review/${attemptId}`,
|
if (!canReview) {
|
||||||
);
|
return;
|
||||||
} catch {
|
|
||||||
// Ignore errors.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.reviewAttempt(attempt.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -447,22 +431,15 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hasPlayed = false;
|
this.hasPlayed = false;
|
||||||
let promise = Promise.resolve();
|
|
||||||
|
|
||||||
// Update data when we come back from the player since the attempt status could have changed.
|
|
||||||
// Check if we need to go to review an attempt automatically.
|
|
||||||
if (this.autoReview && this.autoReview.synced) {
|
|
||||||
promise = this.goToAutoReview();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh data.
|
// Refresh data.
|
||||||
this.showLoading = true;
|
this.showLoading = true;
|
||||||
this.content?.scrollToTop();
|
this.content?.scrollToTop();
|
||||||
|
|
||||||
await promise;
|
|
||||||
await CoreUtils.ignoreErrors(this.refreshContent(true));
|
await CoreUtils.ignoreErrors(this.refreshContent(true));
|
||||||
|
|
||||||
this.showLoading = false;
|
this.showLoading = false;
|
||||||
|
this.autoReview = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -571,13 +548,15 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
* Treat user attempts.
|
* Treat user attempts.
|
||||||
*
|
*
|
||||||
* @param quiz Quiz data.
|
* @param quiz Quiz data.
|
||||||
|
* @param accessInfo Quiz access information.
|
||||||
* @param attempts The attempts to treat.
|
* @param attempts The attempts to treat.
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async treatAttempts(
|
protected async treatAttempts(
|
||||||
quiz: AddonModQuizQuizData,
|
quiz: AddonModQuizQuizData,
|
||||||
|
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);
|
||||||
|
@ -585,10 +564,10 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastFinished = AddonModQuiz.getLastFinishedAttemptFromList(attempts);
|
const lastCompleted = AddonModQuiz.getLastCompletedAttemptFromList(attempts);
|
||||||
let openReview = false;
|
let openReview = false;
|
||||||
|
|
||||||
if (this.autoReview && lastFinished && lastFinished.id >= this.autoReview.attemptId) {
|
if (this.autoReview && lastCompleted && lastCompleted.id >= this.autoReview.attemptId) {
|
||||||
// User just finished an attempt in offline and it seems it's been synced, since it's finished in online.
|
// User just finished an attempt in offline and it seems it's been synced, since it's finished in online.
|
||||||
// Go to the review of this attempt if the user hasn't left this view.
|
// Go to the review of this attempt if the user hasn't left this view.
|
||||||
if (!this.isDestroyed && this.isCurrentView) {
|
if (!this.isDestroyed && this.isCurrentView) {
|
||||||
|
@ -599,29 +578,50 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
const [options] = await Promise.all([
|
const [options] = await Promise.all([
|
||||||
AddonModQuiz.getCombinedReviewOptions(quiz.id, { cmId: this.module.id }),
|
AddonModQuiz.getCombinedReviewOptions(quiz.id, { cmId: this.module.id }),
|
||||||
this.getQuizGrade(),
|
this.getQuizGrade(),
|
||||||
openReview ? this.goToAutoReview() : undefined,
|
openReview ? this.goToAutoReview(attempts) : undefined,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
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 = !!lastFinished && this.options.alloptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_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 == AddonModQuizProvider.GRADEHIGHEST &&
|
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, attempt, shouldHighlight, quizGrade, isLast);
|
formattedAttempt.canReview = canReview;
|
||||||
|
if (!canReview) {
|
||||||
|
formattedAttempt.cannotReviewMessage = AddonModQuizHelper.getCannotReviewMessage(quiz, attempt, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -651,13 +651,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}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -671,3 +671,9 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QuizAttempt = AddonModQuizAttempt & {
|
||||||
|
canReview?: boolean;
|
||||||
|
cannotReviewMessage?: string;
|
||||||
|
additionalData?: AddonModQuizWSAdditionalData[]; // Additional data to display for the attempt.
|
||||||
|
};
|
||||||
|
|
|
@ -12,4 +12,41 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
export const ADDON_MOD_QUIZ_COMPONENT = 'mmaModQuiz';
|
||||||
|
|
||||||
export const ADDON_MOD_QUIZ_FEATURE_NAME = 'CoreCourseModuleDelegate_AddonModQuiz';
|
export const ADDON_MOD_QUIZ_FEATURE_NAME = 'CoreCourseModuleDelegate_AddonModQuiz';
|
||||||
|
|
||||||
|
export const ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT = 'addon_mod_quiz_attempt_finished';
|
||||||
|
|
||||||
|
export const ADDON_MOD_QUIZ_SHOW_TIME_BEFORE_DEADLINE = 3600;
|
||||||
|
export const ADDON_MOD_QUIZ_IMMEDIATELY_AFTER_PERIOD = 120; // Time considered 'immedately after the attempt', in seconds.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible grade methods for a quiz.
|
||||||
|
*/
|
||||||
|
export const enum AddonModQuizGradeMethods {
|
||||||
|
HIGHEST_GRADE = 1,
|
||||||
|
AVERAGE_GRADE = 2,
|
||||||
|
FIRST_ATTEMPT = 3,
|
||||||
|
LAST_ATTEMPT = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible states for an attempt.
|
||||||
|
*/
|
||||||
|
export const enum AddonModQuizAttemptStates {
|
||||||
|
IN_PROGRESS = 'inprogress',
|
||||||
|
OVERDUE = 'overdue',
|
||||||
|
FINISHED = 'finished',
|
||||||
|
ABANDONED = 'abandoned',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bitmask patterns to determine if data should be displayed based on the attempt state.
|
||||||
|
*/
|
||||||
|
export const enum AddonModQuizDisplayOptionsAttemptStates {
|
||||||
|
DURING = 0x10000,
|
||||||
|
IMMEDIATELY_AFTER = 0x01000,
|
||||||
|
LATER_WHILE_OPEN = 0x00100,
|
||||||
|
AFTER_CLOSE = 0x00010,
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
{
|
{
|
||||||
"answercolon": "Answer:",
|
"answercolon": "Answer:",
|
||||||
|
"attempt": "Attempt {{$a}}",
|
||||||
|
"attemptduration": "Duration",
|
||||||
"attemptfirst": "First attempt",
|
"attemptfirst": "First attempt",
|
||||||
"attemptlast": "Last attempt",
|
"attemptlast": "Last attempt",
|
||||||
"attemptnumber": "Attempt",
|
"attemptnumber": "Attempt",
|
||||||
"attemptquiznow": "Attempt quiz now",
|
"attemptquiznow": "Attempt quiz now",
|
||||||
"attemptstate": "State",
|
"attemptstate": "Status",
|
||||||
"canattemptbutnotsubmit": "You can attempt this quiz in the app, but you will need to submit the attempt in browser for the following reasons:",
|
"canattemptbutnotsubmit": "You can attempt this quiz in the app, but you will need to submit the attempt in browser for the following reasons:",
|
||||||
"cannotsubmitquizdueto": "This quiz attempt cannot be submitted for the following reasons:",
|
"cannotsubmitquizdueto": "This quiz attempt cannot be submitted for the following reasons:",
|
||||||
"clearchoice": "Clear my choice",
|
"clearchoice": "Clear my choice",
|
||||||
"comment": "Comment",
|
"comment": "Comment",
|
||||||
"completedon": "Completed on",
|
"completedon": "Completed",
|
||||||
"confirmclose": "Once you submit your answers, you won’t be able to change them.",
|
"confirmclose": "Once you submit your answers, you won’t be able to change them.",
|
||||||
"confirmcontinueoffline": "This attempt has not been synchronised since {{$a}}. If you have continued this attempt in another device since then, you may lose data.",
|
"confirmcontinueoffline": "This attempt has not been synchronised since {{$a}}. If you have continued this attempt in another device since then, you may lose data.",
|
||||||
"confirmleavequizonerror": "An error occurred while saving the answers. Are you sure you want to leave the quiz?",
|
"confirmleavequizonerror": "An error occurred while saving the answers. Are you sure you want to leave the quiz?",
|
||||||
|
@ -29,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",
|
||||||
|
@ -40,6 +42,8 @@
|
||||||
"mustbesubmittedby": "This attempt must be submitted by {{$a}}.",
|
"mustbesubmittedby": "This attempt must be submitted by {{$a}}.",
|
||||||
"noquestions": "No questions have been added yet",
|
"noquestions": "No questions have been added yet",
|
||||||
"noreviewattempt": "You are not allowed to review this attempt.",
|
"noreviewattempt": "You are not allowed to review this attempt.",
|
||||||
|
"noreviewuntil": "You are not allowed to review this quiz until {{$a}}",
|
||||||
|
"noreviewuntilshort": "Available {{$a}}",
|
||||||
"notyetgraded": "Not yet graded",
|
"notyetgraded": "Not yet graded",
|
||||||
"opentoc": "Open navigation popover",
|
"opentoc": "Open navigation popover",
|
||||||
"outof": "{{$a.grade}} out of {{$a.maxgrade}}",
|
"outof": "{{$a.grade}} out of {{$a.maxgrade}}",
|
||||||
|
@ -60,7 +64,7 @@
|
||||||
"showall": "Show all questions on one page",
|
"showall": "Show all questions on one page",
|
||||||
"showeachpage": "Show one page at a time",
|
"showeachpage": "Show one page at a time",
|
||||||
"startattempt": "Start attempt",
|
"startattempt": "Start attempt",
|
||||||
"startedon": "Started on",
|
"startedon": "Started",
|
||||||
"stateabandoned": "Never submitted",
|
"stateabandoned": "Never submitted",
|
||||||
"statefinished": "Finished",
|
"statefinished": "Finished",
|
||||||
"statefinisheddetails": "Submitted {{$a}}",
|
"statefinisheddetails": "Submitted {{$a}}",
|
||||||
|
@ -71,9 +75,8 @@
|
||||||
"submission_confirmation_unanswered": "Questions without a response: {{$a}}",
|
"submission_confirmation_unanswered": "Questions without a response: {{$a}}",
|
||||||
"submitallandfinish": "Submit all and finish",
|
"submitallandfinish": "Submit all and finish",
|
||||||
"summaryofattempt": "Summary of attempt",
|
"summaryofattempt": "Summary of attempt",
|
||||||
"summaryofattempts": "Summary of your previous attempts",
|
"summaryofattempts": "Your attempts",
|
||||||
"timeleft": "Time left",
|
"timeleft": "Time left",
|
||||||
"timetaken": "Time taken",
|
|
||||||
"unit": "Unit",
|
"unit": "Unit",
|
||||||
"warningattemptfinished": "Offline attempt discarded as it was finished on the site or not found.",
|
"warningattemptfinished": "Offline attempt discarded as it was finished on the site or not found.",
|
||||||
"warningdatadiscarded": "Some offline answers were discarded because the questions were modified online.",
|
"warningdatadiscarded": "Some offline answers were discarded because the questions were modified online.",
|
||||||
|
|
|
@ -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.finished && 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.finished" 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,214 +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,
|
|
||||||
AddonModQuizProvider,
|
|
||||||
} from '../../services/quiz';
|
|
||||||
import { AddonModQuizAttempt, AddonModQuizHelper, AddonModQuizQuizData } from '../../services/quiz-helper';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = AddonModQuizProvider.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, attempt, false, undefined, true);
|
|
||||||
|
|
||||||
// Check if the feedback should be displayed.
|
|
||||||
const grade = Number(this.attempt.rescaledGrade);
|
|
||||||
|
|
||||||
if (this.quiz.showFeedbackColumn && AddonModQuiz.isAttemptFinished(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 {
|
||||||
|
|
|
@ -38,10 +38,9 @@ import {
|
||||||
AddonModQuizAttemptWSData,
|
AddonModQuizAttemptWSData,
|
||||||
AddonModQuizGetAttemptAccessInformationWSResponse,
|
AddonModQuizGetAttemptAccessInformationWSResponse,
|
||||||
AddonModQuizGetQuizAccessInformationWSResponse,
|
AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
AddonModQuizProvider,
|
|
||||||
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';
|
||||||
|
@ -50,6 +49,7 @@ import { CoreTime } from '@singletons/time';
|
||||||
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
||||||
import { CoreWSError } from '@classes/errors/wserror';
|
import { CoreWSError } from '@classes/errors/wserror';
|
||||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||||
|
import { ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT, AddonModQuizAttemptStates, ADDON_MOD_QUIZ_COMPONENT } from '../../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that allows attempting a quiz.
|
* Page that allows attempting a quiz.
|
||||||
|
@ -66,9 +66,9 @@ 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 = AddonModQuizProvider.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.
|
||||||
quizAborted = false; // Whether the quiz was aborted due to an error.
|
quizAborted = false; // Whether the quiz was aborted due to an error.
|
||||||
offline = false; // Whether the quiz is being attempted in offline mode.
|
offline = false; // Whether the quiz is being attempted in offline mode.
|
||||||
|
@ -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.
|
||||||
|
@ -146,7 +146,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
|
|
||||||
if (this.quiz) {
|
if (this.quiz) {
|
||||||
// Unblock the quiz so it can be synced.
|
// Unblock the quiz so it can be synced.
|
||||||
CoreSync.unblockOperation(AddonModQuizProvider.COMPONENT, this.quiz.id);
|
CoreSync.unblockOperation(ADDON_MOD_QUIZ_COMPONENT, this.quiz.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,7 +263,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page != -1 && (this.attempt.state == AddonModQuizProvider.ATTEMPT_OVERDUE || this.attempt.finishedOffline)) {
|
if (page != -1 && (this.attempt.state === AddonModQuizAttemptStates.OVERDUE || this.attempt.finishedOffline)) {
|
||||||
// We can't load a page if overdue or the local attempt is finished.
|
// We can't load a page if overdue or the local attempt is finished.
|
||||||
return;
|
return;
|
||||||
} else if (page == this.attempt.currentpage && !this.showSummary && slot !== undefined) {
|
} else if (page == this.attempt.currentpage && !this.showSummary && slot !== undefined) {
|
||||||
|
@ -341,7 +341,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
this.quiz = await AddonModQuiz.getQuiz(this.courseId, this.cmId);
|
this.quiz = await AddonModQuiz.getQuiz(this.courseId, this.cmId);
|
||||||
|
|
||||||
// Block the quiz so it cannot be synced.
|
// Block the quiz so it cannot be synced.
|
||||||
CoreSync.blockOperation(AddonModQuizProvider.COMPONENT, this.quiz.id);
|
CoreSync.blockOperation(ADDON_MOD_QUIZ_COMPONENT, this.quiz.id);
|
||||||
|
|
||||||
// Wait for any ongoing sync to finish. We won't sync a quiz while it's being played.
|
// Wait for any ongoing sync to finish. We won't sync a quiz while it's being played.
|
||||||
await AddonModQuizSync.waitForSync(this.quiz.id);
|
await AddonModQuizSync.waitForSync(this.quiz.id);
|
||||||
|
@ -381,15 +381,11 @@ 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,
|
|
||||||
attempts[attempts.length - 1],
|
|
||||||
false,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.newAttempt = AddonModQuiz.isAttemptFinished(this.lastAttempt.state);
|
this.lastAttempt.finishedOffline = await AddonModQuiz.isAttemptFinishedOffline(this.lastAttempt.id);
|
||||||
|
|
||||||
|
this.newAttempt = AddonModQuiz.isAttemptCompleted(this.lastAttempt.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -408,7 +404,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Show confirm if the user clicked the finish button and the quiz is in progress.
|
// Show confirm if the user clicked the finish button and the quiz is in progress.
|
||||||
if (!timeUp && this.attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
|
if (!timeUp && this.attempt.state === AddonModQuizAttemptStates.IN_PROGRESS) {
|
||||||
let message = Translate.instant('addon.mod_quiz.confirmclose');
|
let message = Translate.instant('addon.mod_quiz.confirmclose');
|
||||||
|
|
||||||
const unansweredCount = this.summaryQuestions
|
const unansweredCount = this.summaryQuestions
|
||||||
|
@ -444,7 +440,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
await this.processAttempt(userFinish, timeUp);
|
await this.processAttempt(userFinish, timeUp);
|
||||||
|
|
||||||
// Trigger an event to notify the attempt was finished.
|
// Trigger an event to notify the attempt was finished.
|
||||||
CoreEvents.trigger(AddonModQuizProvider.ATTEMPT_FINISHED_EVENT, {
|
CoreEvents.trigger(ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT, {
|
||||||
quizId: this.quiz.id,
|
quizId: this.quiz.id,
|
||||||
attemptId: this.attempt.id,
|
attemptId: this.attempt.id,
|
||||||
synced: !this.offline,
|
synced: !this.offline,
|
||||||
|
@ -679,7 +675,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.showSummary = true;
|
this.showSummary = true;
|
||||||
this.canReturn = this.attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS && !this.attempt.finishedOffline;
|
this.canReturn = this.attempt.state === AddonModQuizAttemptStates.IN_PROGRESS && !this.attempt.finishedOffline;
|
||||||
this.preventSubmitMessages = AddonModQuiz.getPreventSubmitMessages(this.summaryQuestions);
|
this.preventSubmitMessages = AddonModQuiz.getPreventSubmitMessages(this.summaryQuestions);
|
||||||
|
|
||||||
this.dueDateWarning = AddonModQuiz.getAttemptDueDateWarning(this.quiz, this.attempt);
|
this.dueDateWarning = AddonModQuiz.getAttemptDueDateWarning(this.quiz, this.attempt);
|
||||||
|
@ -888,10 +884,12 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
this.quiz,
|
this.quiz,
|
||||||
this.quizAccessInfo,
|
this.quizAccessInfo,
|
||||||
this.preflightData,
|
this.preflightData,
|
||||||
|
{
|
||||||
attempt,
|
attempt,
|
||||||
this.offline,
|
offline: this.offline,
|
||||||
false,
|
finishedOffline: attempt?.finishedOffline,
|
||||||
'addon.mod_quiz.startattempt',
|
title: 'addon.mod_quiz.startattempt',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Re-fetch attempt access information with the right attempt (might have changed because a new attempt was created).
|
// Re-fetch attempt access information with the right attempt (might have changed because a new attempt was created).
|
||||||
|
@ -904,7 +902,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
||||||
|
|
||||||
await this.loadNavigation();
|
await this.loadNavigation();
|
||||||
|
|
||||||
if (this.attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !this.attempt.finishedOffline) {
|
if (this.attempt.state !== AddonModQuizAttemptStates.OVERDUE && !this.attempt.finishedOffline) {
|
||||||
// Attempt not overdue and not finished in offline, load page.
|
// Attempt not overdue and not finished in offline, load page.
|
||||||
await this.loadPage(this.attempt.currentpage ?? 0);
|
await this.loadPage(this.attempt.currentpage ?? 0);
|
||||||
|
|
||||||
|
@ -944,3 +942,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.timetaken' | 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,13 +30,12 @@ import {
|
||||||
AddonModQuiz,
|
AddonModQuiz,
|
||||||
AddonModQuizAttemptWSData,
|
AddonModQuizAttemptWSData,
|
||||||
AddonModQuizCombinedReviewOptions,
|
AddonModQuizCombinedReviewOptions,
|
||||||
AddonModQuizGetAttemptReviewResponse,
|
|
||||||
AddonModQuizProvider,
|
|
||||||
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 { ADDON_MOD_QUIZ_COMPONENT } from '../../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that allows reviewing a quiz attempt.
|
* Page that allows reviewing a quiz attempt.
|
||||||
|
@ -52,7 +50,7 @@ export class AddonModQuizReviewPage implements OnInit {
|
||||||
@ViewChild(IonContent) content?: IonContent;
|
@ViewChild(IonContent) content?: IonContent;
|
||||||
|
|
||||||
attempt?: AddonModQuizAttemptWSData; // The attempt being reviewed.
|
attempt?: AddonModQuizAttemptWSData; // The attempt being reviewed.
|
||||||
component = AddonModQuizProvider.COMPONENT; // Component to link the files to.
|
component = ADDON_MOD_QUIZ_COMPONENT; // Component to link the files to.
|
||||||
showAll = false; // Whether to view all questions in the same page.
|
showAll = false; // Whether to view all questions in the same page.
|
||||||
numPages = 1; // Number of pages.
|
numPages = 1; // Number of pages.
|
||||||
showCompleted = false; // Whether to show completed time.
|
showCompleted = false; // Whether to show completed time.
|
||||||
|
@ -62,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;
|
||||||
|
@ -158,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();
|
||||||
|
|
||||||
|
@ -177,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;
|
||||||
|
@ -253,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 != AddonModQuizProvider.ATTEMPT_FINISHED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showCompleted = true;
|
|
||||||
this.additionalData = data.additionaldata;
|
|
||||||
|
|
||||||
const timeTaken = (this.attempt.timefinish || 0) - (this.attempt.timestart || 0);
|
|
||||||
if (timeTaken > 0) {
|
|
||||||
// Format time taken.
|
|
||||||
this.timeTaken = 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 >= AddonModQuizProvider.QUESTION_OPTIONS_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,
|
||||||
],
|
],
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { AddonModQuizPrefetchHandler } from './services/handlers/prefetch';
|
||||||
import { AddonModQuizPushClickHandler } from './services/handlers/push-click';
|
import { AddonModQuizPushClickHandler } from './services/handlers/push-click';
|
||||||
import { AddonModQuizReviewLinkHandler } from './services/handlers/review-link';
|
import { AddonModQuizReviewLinkHandler } from './services/handlers/review-link';
|
||||||
import { AddonModQuizSyncCronHandler } from './services/handlers/sync-cron';
|
import { AddonModQuizSyncCronHandler } from './services/handlers/sync-cron';
|
||||||
import { AddonModQuizProvider } from './services/quiz';
|
import { ADDON_MOD_QUIZ_COMPONENT } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get mod Quiz services.
|
* Get mod Quiz services.
|
||||||
|
@ -98,7 +98,7 @@ const routes: Routes = [
|
||||||
CorePushNotificationsDelegate.registerClickHandler(AddonModQuizPushClickHandler.instance);
|
CorePushNotificationsDelegate.registerClickHandler(AddonModQuizPushClickHandler.instance);
|
||||||
CoreCronDelegate.register(AddonModQuizSyncCronHandler.instance);
|
CoreCronDelegate.register(AddonModQuizSyncCronHandler.instance);
|
||||||
|
|
||||||
CoreCourseHelper.registerModuleReminderClick(AddonModQuizProvider.COMPONENT);
|
CoreCourseHelper.registerModuleReminderClick(ADDON_MOD_QUIZ_COMPONENT);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -32,11 +32,11 @@ import {
|
||||||
AddonModQuiz,
|
AddonModQuiz,
|
||||||
AddonModQuizAttemptWSData,
|
AddonModQuizAttemptWSData,
|
||||||
AddonModQuizGetQuizAccessInformationWSResponse,
|
AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
AddonModQuizProvider,
|
|
||||||
AddonModQuizQuizWSData,
|
AddonModQuizQuizWSData,
|
||||||
} from '../quiz';
|
} from '../quiz';
|
||||||
import { AddonModQuizHelper } from '../quiz-helper';
|
import { AddonModQuizHelper } from '../quiz-helper';
|
||||||
import { AddonModQuizSync, AddonModQuizSyncResult } from '../quiz-sync';
|
import { AddonModQuizSync, AddonModQuizSyncResult } from '../quiz-sync';
|
||||||
|
import { AddonModQuizAttemptStates, ADDON_MOD_QUIZ_COMPONENT } from '../../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to prefetch quizzes.
|
* Handler to prefetch quizzes.
|
||||||
|
@ -46,7 +46,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
|
|
||||||
name = 'AddonModQuiz';
|
name = 'AddonModQuiz';
|
||||||
modName = 'quiz';
|
modName = 'quiz';
|
||||||
component = AddonModQuizProvider.COMPONENT;
|
component = ADDON_MOD_QUIZ_COMPONENT;
|
||||||
updatesNames = /^configuration$|^.*files$|^grades$|^gradeitems$|^questions$|^attempts$/;
|
updatesNames = /^configuration$|^.*files$|^grades$|^gradeitems$|^questions$|^attempts$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -115,8 +115,8 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
let files: CoreWSFile[] = [];
|
let files: CoreWSFile[] = [];
|
||||||
|
|
||||||
await Promise.all(attempts.map(async (attempt) => {
|
await Promise.all(attempts.map(async (attempt) => {
|
||||||
if (!AddonModQuiz.isAttemptFinished(attempt.state)) {
|
if (!AddonModQuiz.isAttemptCompleted(attempt.state)) {
|
||||||
// Attempt not finished, no feedback files.
|
// Attempt not completed, no feedback files.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,11 +167,12 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
quiz,
|
quiz,
|
||||||
accessInfo,
|
accessInfo,
|
||||||
preflightData,
|
preflightData,
|
||||||
|
{
|
||||||
attempt,
|
attempt,
|
||||||
false,
|
prefetch: true,
|
||||||
true,
|
|
||||||
title,
|
title,
|
||||||
siteId,
|
siteId,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Get some fixed preflight data from access rules (data that doesn't require user interaction).
|
// Get some fixed preflight data from access rules (data that doesn't require user interaction).
|
||||||
|
@ -241,9 +242,9 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
siteId,
|
siteId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isLastFinished = !attempts.length || AddonModQuiz.isAttemptFinished(attempts[attempts.length - 1].state);
|
const isLastCompleted = !attempts.length || AddonModQuiz.isAttemptCompleted(attempts[attempts.length - 1].state);
|
||||||
|
|
||||||
return quiz.attempts === 0 || (quiz.attempts ?? 0) > attempts.length || !isLastFinished;
|
return quiz.attempts === 0 || (quiz.attempts ?? 0) > attempts.length || !isLastCompleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -321,7 +322,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
AddonModQuiz.getUserAttempts(quiz.id, modOptions),
|
AddonModQuiz.getUserAttempts(quiz.id, modOptions),
|
||||||
AddonModQuiz.getAttemptAccessInformation(quiz.id, 0, modOptions),
|
AddonModQuiz.getAttemptAccessInformation(quiz.id, 0, modOptions),
|
||||||
AddonModQuiz.getQuizRequiredQtypes(quiz.id, modOptions),
|
AddonModQuiz.getQuizRequiredQtypes(quiz.id, modOptions),
|
||||||
CoreFilepool.addFilesToQueue(siteId, introFiles, AddonModQuizProvider.COMPONENT, module.id),
|
CoreFilepool.addFilesToQueue(siteId, introFiles, ADDON_MOD_QUIZ_COMPONENT, module.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if we need to start a new attempt.
|
// Check if we need to start a new attempt.
|
||||||
|
@ -330,7 +331,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
let startAttempt = false;
|
let startAttempt = false;
|
||||||
|
|
||||||
if (canStart || attempt) {
|
if (canStart || attempt) {
|
||||||
if (canStart && (!attempt || AddonModQuiz.isAttemptFinished(attempt.state))) {
|
if (canStart && (!attempt || AddonModQuiz.isAttemptCompleted(attempt.state))) {
|
||||||
// Check if the user can attempt the quiz.
|
// Check if the user can attempt the quiz.
|
||||||
if (attemptAccessInfo.preventnewattemptreasons.length) {
|
if (attemptAccessInfo.preventnewattemptreasons.length) {
|
||||||
throw new CoreError(CoreTextUtils.buildMessage(attemptAccessInfo.preventnewattemptreasons));
|
throw new CoreError(CoreTextUtils.buildMessage(attemptAccessInfo.preventnewattemptreasons));
|
||||||
|
@ -353,17 +354,17 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
|
|
||||||
const attemptFiles = await this.getAttemptsFeedbackFiles(quiz, attempts, siteId);
|
const attemptFiles = await this.getAttemptsFeedbackFiles(quiz, attempts, siteId);
|
||||||
|
|
||||||
return CoreFilepool.addFilesToQueue(siteId, attemptFiles, AddonModQuizProvider.COMPONENT, module.id);
|
return CoreFilepool.addFilesToQueue(siteId, attemptFiles, ADDON_MOD_QUIZ_COMPONENT, module.id);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update the download time to prevent detecting the new attempt as an update.
|
// Update the download time to prevent detecting the new attempt as an update.
|
||||||
promises.push(CoreUtils.ignoreErrors(
|
promises.push(CoreUtils.ignoreErrors(
|
||||||
CoreFilepool.updatePackageDownloadTime(siteId, AddonModQuizProvider.COMPONENT, module.id),
|
CoreFilepool.updatePackageDownloadTime(siteId, ADDON_MOD_QUIZ_COMPONENT, module.id),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
// Use the already fetched attempts.
|
// Use the already fetched attempts.
|
||||||
promises.push(this.getAttemptsFeedbackFiles(quiz, attempts, siteId).then((attemptFiles) =>
|
promises.push(this.getAttemptsFeedbackFiles(quiz, attempts, siteId).then((attemptFiles) =>
|
||||||
CoreFilepool.addFilesToQueue(siteId, attemptFiles, AddonModQuizProvider.COMPONENT, module.id)));
|
CoreFilepool.addFilesToQueue(siteId, attemptFiles, ADDON_MOD_QUIZ_COMPONENT, module.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch attempt related data.
|
// Fetch attempt related data.
|
||||||
|
@ -379,7 +380,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
|
|
||||||
// We have quiz data, now we'll get specific data for each attempt.
|
// We have quiz data, now we'll get specific data for each attempt.
|
||||||
await Promise.all(attempts.map(async (attempt) => {
|
await Promise.all(attempts.map(async (attempt) => {
|
||||||
await this.prefetchAttempt(quiz, attempt, preflightData, siteId);
|
await this.prefetchAttempt(quiz, quizAccessInfo, attempt, preflightData, siteId);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!canStart) {
|
if (!canStart) {
|
||||||
|
@ -399,6 +400,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
* Prefetch all WS data for an attempt.
|
* Prefetch all WS data for an attempt.
|
||||||
*
|
*
|
||||||
* @param quiz Quiz.
|
* @param quiz Quiz.
|
||||||
|
* @param accessInfo Quiz access info.
|
||||||
* @param attempt Attempt.
|
* @param attempt Attempt.
|
||||||
* @param preflightData Preflight required data (like password).
|
* @param preflightData Preflight required data (like password).
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
@ -406,11 +408,11 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
*/
|
*/
|
||||||
async prefetchAttempt(
|
async prefetchAttempt(
|
||||||
quiz: AddonModQuizQuizWSData,
|
quiz: AddonModQuizQuizWSData,
|
||||||
|
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
attempt: AddonModQuizAttemptWSData,
|
attempt: AddonModQuizAttemptWSData,
|
||||||
preflightData: Record<string, string>,
|
preflightData: Record<string, string>,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const pages = AddonModQuiz.getPagesFromLayout(attempt.layout);
|
|
||||||
const isSequential = AddonModQuiz.isNavigationSequential(quiz);
|
const isSequential = AddonModQuiz.isNavigationSequential(quiz);
|
||||||
let promises: Promise<unknown>[] = [];
|
let promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
@ -420,7 +422,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
siteId,
|
siteId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (AddonModQuiz.isAttemptFinished(attempt.state)) {
|
if (AddonModQuiz.isAttemptCompleted(attempt.state)) {
|
||||||
// Attempt is finished, get feedback and review data.
|
// Attempt is finished, get feedback and review data.
|
||||||
const attemptGrade = AddonModQuiz.rescaleGrade(attempt.sumgrades, quiz, false);
|
const attemptGrade = AddonModQuiz.rescaleGrade(attempt.sumgrades, quiz, false);
|
||||||
const attemptGradeNumber = attemptGrade !== undefined && Number(attemptGrade);
|
const attemptGradeNumber = attemptGrade !== undefined && Number(attemptGrade);
|
||||||
|
@ -428,24 +430,17 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
promises.push(AddonModQuiz.getFeedbackForGrade(quiz.id, attemptGradeNumber, modOptions));
|
promises.push(AddonModQuiz.getFeedbackForGrade(quiz.id, attemptGradeNumber, modOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the review for each page.
|
promises.push(this.prefetchAttemptReview(quiz, accessInfo, attempt, modOptions));
|
||||||
pages.forEach((page) => {
|
|
||||||
promises.push(CoreUtils.ignoreErrors(AddonModQuiz.getAttemptReview(attempt.id, {
|
|
||||||
page,
|
|
||||||
...modOptions, // Include all options.
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the review for all questions in same page.
|
|
||||||
promises.push(this.prefetchAttemptReviewFiles(quiz, attempt, modOptions, siteId));
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Attempt not finished, get data needed to continue the attempt.
|
// Attempt not finished, get data needed to continue the attempt.
|
||||||
promises.push(AddonModQuiz.getAttemptAccessInformation(quiz.id, attempt.id, modOptions));
|
promises.push(AddonModQuiz.getAttemptAccessInformation(quiz.id, attempt.id, modOptions));
|
||||||
promises.push(AddonModQuiz.getAttemptSummary(attempt.id, preflightData, modOptions));
|
promises.push(AddonModQuiz.getAttemptSummary(attempt.id, preflightData, modOptions));
|
||||||
|
|
||||||
if (attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
|
if (attempt.state === AddonModQuizAttemptStates.IN_PROGRESS) {
|
||||||
// Get data for each page.
|
// Get data for each page.
|
||||||
|
const pages = AddonModQuiz.getPagesFromLayout(attempt.layout);
|
||||||
|
|
||||||
promises = promises.concat(pages.map(async (page) => {
|
promises = promises.concat(pages.map(async (page) => {
|
||||||
if (isSequential && typeof attempt.currentpage === 'number' && page < attempt.currentpage) {
|
if (isSequential && typeof attempt.currentpage === 'number' && page < attempt.currentpage) {
|
||||||
// Sequential quiz, cannot get pages before the current one.
|
// Sequential quiz, cannot get pages before the current one.
|
||||||
|
@ -472,20 +467,57 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch attempt review data.
|
||||||
|
*
|
||||||
|
* @param quiz Quiz.
|
||||||
|
* @param accessInfo Quiz access info.
|
||||||
|
* @param attempt Attempt.
|
||||||
|
* @param modOptions Other options.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @returns Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async prefetchAttemptReview(
|
||||||
|
quiz: AddonModQuizQuizWSData,
|
||||||
|
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
|
attempt: AddonModQuizAttemptWSData,
|
||||||
|
modOptions: CoreCourseCommonModWSOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
// Check if attempt can be reviewed.
|
||||||
|
const canReview = await AddonModQuizHelper.canReviewAttempt(quiz, accessInfo, attempt);
|
||||||
|
if (!canReview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = AddonModQuiz.getPagesFromLayout(attempt.layout);
|
||||||
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
// Get the review for each page.
|
||||||
|
pages.forEach((page) => {
|
||||||
|
promises.push(CoreUtils.ignoreErrors(AddonModQuiz.getAttemptReview(attempt.id, {
|
||||||
|
page,
|
||||||
|
...modOptions, // Include all options.
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the review for all questions in same page.
|
||||||
|
promises.push(this.prefetchAttemptReviewFiles(quiz, attempt, modOptions));
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prefetch attempt review and its files.
|
* Prefetch attempt review and its files.
|
||||||
*
|
*
|
||||||
* @param quiz Quiz.
|
* @param quiz Quiz.
|
||||||
* @param attempt Attempt.
|
* @param attempt Attempt.
|
||||||
* @param modOptions Other options.
|
* @param modOptions Other options.
|
||||||
* @param siteId Site ID.
|
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async prefetchAttemptReviewFiles(
|
protected async prefetchAttemptReviewFiles(
|
||||||
quiz: AddonModQuizQuizWSData,
|
quiz: AddonModQuizQuizWSData,
|
||||||
attempt: AddonModQuizAttemptWSData,
|
attempt: AddonModQuizAttemptWSData,
|
||||||
modOptions: CoreCourseCommonModWSOptions,
|
modOptions: CoreCourseCommonModWSOptions,
|
||||||
siteId?: string,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Get the review for all questions in same page.
|
// Get the review for all questions in same page.
|
||||||
const data = await CoreUtils.ignoreErrors(AddonModQuiz.getAttemptReview(attempt.id, {
|
const data = await CoreUtils.ignoreErrors(AddonModQuiz.getAttemptReview(attempt.id, {
|
||||||
|
@ -502,7 +534,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
question,
|
question,
|
||||||
this.component,
|
this.component,
|
||||||
quiz.coursemodule,
|
quiz.coursemodule,
|
||||||
siteId,
|
modOptions.siteId,
|
||||||
attempt.uniqueid,
|
attempt.uniqueid,
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
@ -568,7 +600,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
preflightData = await this.getPreflightData(quiz, quizAccessInfo, lastAttempt, askPreflight, 'core.download', siteId);
|
preflightData = await this.getPreflightData(quiz, quizAccessInfo, lastAttempt, askPreflight, 'core.download', siteId);
|
||||||
|
|
||||||
// Get data for last attempt.
|
// Get data for last attempt.
|
||||||
await this.prefetchAttempt(quiz, lastAttempt, preflightData, siteId);
|
await this.prefetchAttempt(quiz, quizAccessInfo, lastAttempt, preflightData, siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefetch finished, set the right status.
|
// Prefetch finished, set the right status.
|
||||||
|
@ -611,8 +643,8 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
||||||
// Quiz was downloaded, set the new status.
|
// Quiz was downloaded, set the new status.
|
||||||
// If no attempts or last is finished we'll mark it as not downloaded to show download icon.
|
// If no attempts or last is finished we'll mark it as not downloaded to show download icon.
|
||||||
const lastAttempt = attempts[attempts.length - 1];
|
const lastAttempt = attempts[attempts.length - 1];
|
||||||
const isLastFinished = !lastAttempt || AddonModQuiz.isAttemptFinished(lastAttempt.state);
|
const isLastCompleted = !lastAttempt || AddonModQuiz.isAttemptCompleted(lastAttempt.state);
|
||||||
const newStatus = isLastFinished ? DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED : DownloadStatus.DOWNLOADED;
|
const newStatus = isLastCompleted ? DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED : DownloadStatus.DOWNLOADED;
|
||||||
|
|
||||||
await CoreFilepool.storePackageStatus(options.siteId, newStatus, this.component, quiz.coursemodule);
|
await CoreFilepool.storePackageStatus(options.siteId, newStatus, this.component, quiz.coursemodule);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,10 +30,17 @@ import {
|
||||||
AddonModQuizAttemptWSData,
|
AddonModQuizAttemptWSData,
|
||||||
AddonModQuizCombinedReviewOptions,
|
AddonModQuizCombinedReviewOptions,
|
||||||
AddonModQuizGetQuizAccessInformationWSResponse,
|
AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
AddonModQuizProvider,
|
|
||||||
AddonModQuizQuizWSData,
|
AddonModQuizQuizWSData,
|
||||||
} from './quiz';
|
} from './quiz';
|
||||||
import { AddonModQuizOffline } from './quiz-offline';
|
import { AddonModQuizOffline } from './quiz-offline';
|
||||||
|
import {
|
||||||
|
ADDON_MOD_QUIZ_IMMEDIATELY_AFTER_PERIOD,
|
||||||
|
AddonModQuizAttemptStates,
|
||||||
|
AddonModQuizDisplayOptionsAttemptStates,
|
||||||
|
} from '../constants';
|
||||||
|
import { QuestionDisplayOptionsMarks } from '@features/question/constants';
|
||||||
|
import { CoreGroups } from '@services/groups';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper service that provides some features for quiz.
|
* Helper service that provides some features for quiz.
|
||||||
|
@ -41,6 +48,125 @@ import { AddonModQuizOffline } from './quiz-offline';
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AddonModQuizHelperProvider {
|
export class AddonModQuizHelperProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current user can review an attempt.
|
||||||
|
*
|
||||||
|
* @param quiz Quiz.
|
||||||
|
* @param accessInfo Access info.
|
||||||
|
* @param attempt Attempt.
|
||||||
|
* @returns Whether user can review the attempt.
|
||||||
|
*/
|
||||||
|
async canReviewAttempt(
|
||||||
|
quiz: AddonModQuizQuizWSData,
|
||||||
|
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
|
attempt: AddonModQuizAttemptWSData,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!this.hasReviewCapabilityForAttempt(quiz, accessInfo, attempt)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt.userid !== CoreSites.getCurrentSiteUserId()) {
|
||||||
|
return this.canReviewOtherUserAttempt(quiz, accessInfo, attempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AddonModQuiz.isAttemptCompleted(attempt.state)) {
|
||||||
|
// Cannot review own uncompleted attempts.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt.preview && accessInfo.canpreview) {
|
||||||
|
// A teacher can always review their own preview no matter the review options settings.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!attempt.preview && accessInfo.canviewreports) {
|
||||||
|
// Users who can see reports should be shown everything, except during preview.
|
||||||
|
// In LMS, the capability 'moodle/grade:viewhidden' is also checked but the app doesn't have this info.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = AddonModQuiz.getDisplayOptionsForQuiz(quiz, AddonModQuiz.getAttemptStateDisplayOption(quiz, attempt));
|
||||||
|
|
||||||
|
return options.attempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current user can review another user attempt.
|
||||||
|
*
|
||||||
|
* @param quiz Quiz.
|
||||||
|
* @param accessInfo Access info.
|
||||||
|
* @param attempt Attempt.
|
||||||
|
* @returns Whether user can review the attempt.
|
||||||
|
*/
|
||||||
|
protected async canReviewOtherUserAttempt(
|
||||||
|
quiz: AddonModQuizQuizWSData,
|
||||||
|
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
|
attempt: AddonModQuizAttemptWSData,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!accessInfo.canviewreports) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const groupInfo = await CoreGroups.getActivityGroupInfo(quiz.coursemodule);
|
||||||
|
if (groupInfo.canAccessAllGroups || !groupInfo.separateGroups) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the current user and the attempt's user share any group.
|
||||||
|
if (!groupInfo.groups.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attemptUserGroups = await CoreGroups.getUserGroupsInCourse(quiz.course, undefined, attempt.userid);
|
||||||
|
|
||||||
|
return attemptUserGroups.some(attemptUserGroup => groupInfo.groups.find(group => attemptUserGroup.id === group.id));
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cannot review message.
|
||||||
|
*
|
||||||
|
* @param quiz Quiz.
|
||||||
|
* @param attempt Attempt.
|
||||||
|
* @param short Whether to use a short message or not.
|
||||||
|
* @returns Cannot review message, or empty string if no message to display.
|
||||||
|
*/
|
||||||
|
getCannotReviewMessage(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttemptWSData, short = false): string {
|
||||||
|
const displayOption = AddonModQuiz.getAttemptStateDisplayOption(quiz, attempt);
|
||||||
|
|
||||||
|
let reviewFrom = 0;
|
||||||
|
switch (displayOption) {
|
||||||
|
case AddonModQuizDisplayOptionsAttemptStates.DURING:
|
||||||
|
return '';
|
||||||
|
|
||||||
|
case AddonModQuizDisplayOptionsAttemptStates.IMMEDIATELY_AFTER:
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
if ((quiz.reviewattempt ?? 0) & AddonModQuizDisplayOptionsAttemptStates.LATER_WHILE_OPEN) {
|
||||||
|
reviewFrom = (attempt.timefinish ?? Date.now()) + ADDON_MOD_QUIZ_IMMEDIATELY_AFTER_PERIOD;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Fall through.
|
||||||
|
|
||||||
|
case AddonModQuizDisplayOptionsAttemptStates.LATER_WHILE_OPEN:
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
if (quiz.timeclose && ((quiz.reviewattempt ?? 0) & AddonModQuizDisplayOptionsAttemptStates.AFTER_CLOSE)) {
|
||||||
|
reviewFrom = quiz.timeclose;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reviewFrom) {
|
||||||
|
return Translate.instant('addon.mod_quiz.noreviewuntil' + (short ? 'short' : ''), {
|
||||||
|
$a: CoreTimeUtils.userDate(reviewFrom * 1000, short ? 'core.strftimedatetimeshort': undefined),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Translate.instant('addon.mod_quiz.noreviewattempt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a preflight data or show a modal to input the preflight data if required.
|
* Validate a preflight data or show a modal to input the preflight data if required.
|
||||||
* It calls AddonModQuizProvider.startAttempt if a new attempt is needed.
|
* It calls AddonModQuizProvider.startAttempt if a new attempt is needed.
|
||||||
|
@ -48,24 +174,14 @@ export class AddonModQuizHelperProvider {
|
||||||
* @param quiz Quiz.
|
* @param quiz Quiz.
|
||||||
* @param accessInfo Quiz access info.
|
* @param accessInfo Quiz access info.
|
||||||
* @param preflightData Object where to store the preflight data.
|
* @param preflightData Object where to store the preflight data.
|
||||||
* @param attempt Attempt to continue. Don't pass any value if the user needs to start a new attempt.
|
* @param options Options.
|
||||||
* @param offline Whether the attempt is offline.
|
|
||||||
* @param prefetch Whether user is prefetching.
|
|
||||||
* @param title The title to display in the modal and in the submit button.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
|
||||||
* @param retrying Whether we're retrying after a failure.
|
|
||||||
* @returns Promise resolved when the preflight data is validated. The resolve param is the attempt.
|
* @returns Promise resolved when the preflight data is validated. The resolve param is the attempt.
|
||||||
*/
|
*/
|
||||||
async getAndCheckPreflightData(
|
async getAndCheckPreflightData(
|
||||||
quiz: AddonModQuizQuizWSData,
|
quiz: AddonModQuizQuizWSData,
|
||||||
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
preflightData: Record<string, string>,
|
preflightData: Record<string, string>,
|
||||||
attempt?: AddonModQuizAttemptWSData,
|
options: GetAndCheckPreflightOptions = {},
|
||||||
offline?: boolean,
|
|
||||||
prefetch?: boolean,
|
|
||||||
title?: string,
|
|
||||||
siteId?: string,
|
|
||||||
retrying?: boolean,
|
|
||||||
): Promise<AddonModQuizAttemptWSData> {
|
): Promise<AddonModQuizAttemptWSData> {
|
||||||
|
|
||||||
const rules = accessInfo?.activerulenames;
|
const rules = accessInfo?.activerulenames;
|
||||||
|
@ -74,30 +190,37 @@ export class AddonModQuizHelperProvider {
|
||||||
const preflightCheckRequired = await AddonModQuizAccessRuleDelegate.isPreflightCheckRequired(
|
const preflightCheckRequired = await AddonModQuizAccessRuleDelegate.isPreflightCheckRequired(
|
||||||
rules,
|
rules,
|
||||||
quiz,
|
quiz,
|
||||||
attempt,
|
options.attempt,
|
||||||
prefetch,
|
options.prefetch,
|
||||||
siteId,
|
options.siteId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (preflightCheckRequired) {
|
if (preflightCheckRequired) {
|
||||||
// Preflight check is required. Show a modal with the preflight form.
|
// Preflight check is required. Show a modal with the preflight form.
|
||||||
const data = await this.getPreflightData(quiz, accessInfo, attempt, prefetch, title, siteId);
|
const data = await this.getPreflightData(quiz, accessInfo, options);
|
||||||
|
|
||||||
// Data entered by the user, add it to preflight data and check it again.
|
// Data entered by the user, add it to preflight data and check it again.
|
||||||
Object.assign(preflightData, data);
|
Object.assign(preflightData, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get some fixed preflight data from access rules (data that doesn't require user interaction).
|
// Get some fixed preflight data from access rules (data that doesn't require user interaction).
|
||||||
await AddonModQuizAccessRuleDelegate.getFixedPreflightData(rules, quiz, preflightData, attempt, prefetch, siteId);
|
await AddonModQuizAccessRuleDelegate.getFixedPreflightData(
|
||||||
|
rules,
|
||||||
|
quiz,
|
||||||
|
preflightData,
|
||||||
|
options.attempt,
|
||||||
|
options.prefetch,
|
||||||
|
options.siteId,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// All the preflight data is gathered, now validate it.
|
// All the preflight data is gathered, now validate it.
|
||||||
return await this.validatePreflightData(quiz, accessInfo, preflightData, attempt, offline, prefetch, siteId);
|
return await this.validatePreflightData(quiz, accessInfo, preflightData, options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
if (prefetch) {
|
if (options.prefetch) {
|
||||||
throw error;
|
throw error;
|
||||||
} else if (retrying && !preflightCheckRequired) {
|
} else if (options.retrying && !preflightCheckRequired) {
|
||||||
// We're retrying after a failure, but the preflight check wasn't required.
|
// We're retrying after a failure, but the preflight check wasn't required.
|
||||||
// This means there's something wrong with some access rule or user is offline and data isn't cached.
|
// This means there's something wrong with some access rule or user is offline and data isn't cached.
|
||||||
// Don't retry again because it would lead to an infinite loop.
|
// Don't retry again because it would lead to an infinite loop.
|
||||||
|
@ -110,17 +233,10 @@ export class AddonModQuizHelperProvider {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'core.error', true);
|
CoreDomUtils.showErrorModalDefault(error, 'core.error', true);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return this.getAndCheckPreflightData(
|
return this.getAndCheckPreflightData(quiz, accessInfo, preflightData, {
|
||||||
quiz,
|
...options,
|
||||||
accessInfo,
|
retrying: true,
|
||||||
preflightData,
|
});
|
||||||
attempt,
|
|
||||||
offline,
|
|
||||||
prefetch,
|
|
||||||
title,
|
|
||||||
siteId,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,19 +245,13 @@ export class AddonModQuizHelperProvider {
|
||||||
*
|
*
|
||||||
* @param quiz Quiz.
|
* @param quiz Quiz.
|
||||||
* @param accessInfo Quiz access info.
|
* @param accessInfo Quiz access info.
|
||||||
* @param attempt The attempt started/continued. If not supplied, user is starting a new attempt.
|
* @param options Options.
|
||||||
* @param prefetch Whether the user is prefetching the quiz.
|
|
||||||
* @param title The title to display in the modal and in the submit button.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
|
||||||
* @returns Promise resolved with the preflight data. Rejected if user cancels.
|
* @returns Promise resolved with the preflight data. Rejected if user cancels.
|
||||||
*/
|
*/
|
||||||
async getPreflightData(
|
async getPreflightData(
|
||||||
quiz: AddonModQuizQuizWSData,
|
quiz: AddonModQuizQuizWSData,
|
||||||
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
attempt?: AddonModQuizAttemptWSData,
|
options: GetPreflightOptions = {},
|
||||||
prefetch?: boolean,
|
|
||||||
title?: string,
|
|
||||||
siteId?: string,
|
|
||||||
): Promise<Record<string, string>> {
|
): Promise<Record<string, string>> {
|
||||||
const notSupported: string[] = [];
|
const notSupported: string[] = [];
|
||||||
const rules = accessInfo?.activerulenames;
|
const rules = accessInfo?.activerulenames;
|
||||||
|
@ -163,11 +273,11 @@ export class AddonModQuizHelperProvider {
|
||||||
const modalData = await CoreDomUtils.openModal<Record<string, string>>({
|
const modalData = await CoreDomUtils.openModal<Record<string, string>>({
|
||||||
component: AddonModQuizPreflightModalComponent,
|
component: AddonModQuizPreflightModalComponent,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
title: title,
|
title: options.title,
|
||||||
quiz,
|
quiz,
|
||||||
attempt,
|
attempt: options.attempt,
|
||||||
prefetch: !!prefetch,
|
prefetch: !!options.prefetch,
|
||||||
siteId: siteId,
|
siteId: options.siteId,
|
||||||
rules: rules,
|
rules: rules,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -252,53 +362,55 @@ export class AddonModQuizHelperProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current user has the necessary capabilities to review an attempt.
|
||||||
|
*
|
||||||
|
* @param quiz Quiz.
|
||||||
|
* @param accessInfo Access info.
|
||||||
|
* @param attempt Attempt.
|
||||||
|
* @returns Whether user has the capability.
|
||||||
|
*/
|
||||||
|
hasReviewCapabilityForAttempt(
|
||||||
|
quiz: AddonModQuizQuizWSData,
|
||||||
|
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
|
attempt: AddonModQuizAttemptWSData,
|
||||||
|
): boolean {
|
||||||
|
if (accessInfo.canviewreports || accessInfo.canpreview) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayOption = AddonModQuiz.getAttemptStateDisplayOption(quiz, attempt);
|
||||||
|
|
||||||
|
return displayOption === AddonModQuizDisplayOptionsAttemptStates.IMMEDIATELY_AFTER ?
|
||||||
|
accessInfo.canattempt : accessInfo.canreviewmyattempts;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add some calculated data to the attempt.
|
* Add some calculated data to the attempt.
|
||||||
*
|
*
|
||||||
* @param quiz Quiz.
|
* @param quiz Quiz.
|
||||||
* @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,
|
||||||
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.finished = AddonModQuiz.isAttemptFinished(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.finished) {
|
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.finished) {
|
|
||||||
formattedAttempt.readableGrade = AddonModQuiz.formatGrade(
|
|
||||||
Number(formattedAttempt.rescaledGrade),
|
|
||||||
quiz.decimalpoints,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Highlight the highest grade if appropriate.
|
|
||||||
formattedAttempt.highlightGrade = !!(highlight && !attempt.preview &&
|
|
||||||
attempt.state == AddonModQuizProvider.ATTEMPT_FINISHED && formattedAttempt.readableGrade == bestGrade);
|
|
||||||
} else {
|
|
||||||
formattedAttempt.readableGrade = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLastAttempt || isLastAttempt === undefined) {
|
|
||||||
formattedAttempt.finishedOffline = await AddonModQuiz.isAttemptFinishedOffline(attempt.id, siteId);
|
formattedAttempt.finishedOffline = await AddonModQuiz.isAttemptFinishedOffline(attempt.id, siteId);
|
||||||
}
|
|
||||||
|
|
||||||
return formattedAttempt;
|
return formattedAttempt;
|
||||||
}
|
}
|
||||||
|
@ -316,11 +428,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 >= AddonModQuizProvider.QUESTION_OPTIONS_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;
|
||||||
}
|
}
|
||||||
|
@ -331,36 +442,32 @@ export class AddonModQuizHelperProvider {
|
||||||
* @param quiz Quiz.
|
* @param quiz Quiz.
|
||||||
* @param accessInfo Quiz access info.
|
* @param accessInfo Quiz access info.
|
||||||
* @param preflightData Object where to store the preflight data.
|
* @param preflightData Object where to store the preflight data.
|
||||||
* @param attempt Attempt to continue. Don't pass any value if the user needs to start a new attempt.
|
* @param options Options
|
||||||
* @param offline Whether the attempt is offline.
|
|
||||||
* @param prefetch Whether user is prefetching.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
|
||||||
* @returns Promise resolved when the preflight data is validated.
|
* @returns Promise resolved when the preflight data is validated.
|
||||||
*/
|
*/
|
||||||
async validatePreflightData(
|
async validatePreflightData(
|
||||||
quiz: AddonModQuizQuizWSData,
|
quiz: AddonModQuizQuizWSData,
|
||||||
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||||
preflightData: Record<string, string>,
|
preflightData: Record<string, string>,
|
||||||
attempt?: AddonModQuizAttempt,
|
options: ValidatePreflightOptions = {},
|
||||||
offline?: boolean,
|
|
||||||
prefetch?: boolean,
|
|
||||||
siteId?: string,
|
|
||||||
): Promise<AddonModQuizAttempt> {
|
): Promise<AddonModQuizAttempt> {
|
||||||
|
|
||||||
const rules = accessInfo.activerulenames;
|
const rules = accessInfo.activerulenames;
|
||||||
const modOptions = {
|
const modOptions = {
|
||||||
cmId: quiz.coursemodule,
|
cmId: quiz.coursemodule,
|
||||||
readingStrategy: offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
|
readingStrategy: options.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
|
||||||
siteId,
|
siteId: options.siteId,
|
||||||
};
|
};
|
||||||
|
let attempt = options.attempt;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (attempt) {
|
if (attempt) {
|
||||||
if (attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !attempt.finishedOffline) {
|
if (attempt.state !== AddonModQuizAttemptStates.OVERDUE && !options.finishedOffline) {
|
||||||
// We're continuing an attempt. Call getAttemptData to validate the preflight data.
|
// We're continuing an attempt. Call getAttemptData to validate the preflight data.
|
||||||
await AddonModQuiz.getAttemptData(attempt.id, attempt.currentpage ?? 0, preflightData, modOptions);
|
await AddonModQuiz.getAttemptData(attempt.id, attempt.currentpage ?? 0, preflightData, modOptions);
|
||||||
|
|
||||||
if (offline) {
|
if (options.offline) {
|
||||||
// Get current page stored in local.
|
// Get current page stored in local.
|
||||||
const storedAttempt = await CoreUtils.ignoreErrors(
|
const storedAttempt = await CoreUtils.ignoreErrors(
|
||||||
AddonModQuizOffline.getAttemptById(attempt.id),
|
AddonModQuizOffline.getAttemptById(attempt.id),
|
||||||
|
@ -375,7 +482,7 @@ export class AddonModQuizHelperProvider {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// We're starting a new attempt, call startAttempt.
|
// We're starting a new attempt, call startAttempt.
|
||||||
attempt = await AddonModQuiz.startAttempt(quiz.id, preflightData, false, siteId);
|
attempt = await AddonModQuiz.startAttempt(quiz.id, preflightData, false, options.siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preflight data validated.
|
// Preflight data validated.
|
||||||
|
@ -384,8 +491,8 @@ export class AddonModQuizHelperProvider {
|
||||||
quiz,
|
quiz,
|
||||||
attempt,
|
attempt,
|
||||||
preflightData,
|
preflightData,
|
||||||
prefetch,
|
options.prefetch,
|
||||||
siteId,
|
options.siteId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return attempt;
|
return attempt;
|
||||||
|
@ -397,8 +504,8 @@ export class AddonModQuizHelperProvider {
|
||||||
quiz,
|
quiz,
|
||||||
attempt,
|
attempt,
|
||||||
preflightData,
|
preflightData,
|
||||||
prefetch,
|
options.prefetch,
|
||||||
siteId,
|
options.siteId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,10 +523,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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -427,10 +533,32 @@ export type AddonModQuizQuizData = AddonModQuizQuizWSData & {
|
||||||
*/
|
*/
|
||||||
export type AddonModQuizAttempt = AddonModQuizAttemptWSData & {
|
export type AddonModQuizAttempt = AddonModQuizAttemptWSData & {
|
||||||
finishedOffline?: boolean;
|
finishedOffline?: boolean;
|
||||||
rescaledGrade?: string;
|
rescaledGrade?: number;
|
||||||
finished?: boolean;
|
finished?: boolean;
|
||||||
readableState?: string[];
|
completed?: boolean;
|
||||||
readableMark?: string;
|
formattedGrade?: string;
|
||||||
readableGrade?: string;
|
|
||||||
highlightGrade?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to validate preflight data.
|
||||||
|
*/
|
||||||
|
type ValidatePreflightOptions = {
|
||||||
|
attempt?: AddonModQuizAttemptWSData; // Attempt to continue. Don't pass any value if the user needs to start a new attempt.
|
||||||
|
offline?: boolean; // Whether the attempt is offline.
|
||||||
|
finishedOffline?: boolean; // Whether the attempt is finished offline.
|
||||||
|
prefetch?: boolean; // Whether user is prefetching.
|
||||||
|
siteId?: string; // Site ID. If not defined, current site.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to check preflight data.
|
||||||
|
*/
|
||||||
|
type GetAndCheckPreflightOptions = ValidatePreflightOptions & {
|
||||||
|
title?: string; // The title to display in the modal and in the submit button.
|
||||||
|
retrying?: boolean; // Whether we're retrying after a failure.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to get preflight data.
|
||||||
|
*/
|
||||||
|
type GetPreflightOptions = Omit<GetAndCheckPreflightOptions, 'offline'|'finishedOffline'|'retrying'>;
|
||||||
|
|
|
@ -23,7 +23,8 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
import { AddonModQuizAttemptDBRecord, ATTEMPTS_TABLE_NAME } from './database/quiz';
|
import { AddonModQuizAttemptDBRecord, ATTEMPTS_TABLE_NAME } from './database/quiz';
|
||||||
import { AddonModQuizAttemptWSData, AddonModQuizProvider, AddonModQuizQuizWSData } from './quiz';
|
import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from './quiz';
|
||||||
|
import { ADDON_MOD_QUIZ_COMPONENT } from '../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to handle offline quiz.
|
* Service to handle offline quiz.
|
||||||
|
@ -103,7 +104,7 @@ export class AddonModQuizOfflineProvider {
|
||||||
* @returns Promise resolved with the answers.
|
* @returns Promise resolved with the answers.
|
||||||
*/
|
*/
|
||||||
getAttemptAnswers(attemptId: number, siteId?: string): Promise<CoreQuestionAnswerDBRecord[]> {
|
getAttemptAnswers(attemptId: number, siteId?: string): Promise<CoreQuestionAnswerDBRecord[]> {
|
||||||
return CoreQuestion.getAttemptAnswers(AddonModQuizProvider.COMPONENT, attemptId, siteId);
|
return CoreQuestion.getAttemptAnswers(ADDON_MOD_QUIZ_COMPONENT, attemptId, siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -149,7 +150,7 @@ export class AddonModQuizOfflineProvider {
|
||||||
|
|
||||||
await Promise.all(questions.map(async (question) => {
|
await Promise.all(questions.map(async (question) => {
|
||||||
const dbQuestion = await CoreUtils.ignoreErrors(
|
const dbQuestion = await CoreUtils.ignoreErrors(
|
||||||
CoreQuestion.getQuestion(AddonModQuizProvider.COMPONENT, attemptId, question.slot, siteId),
|
CoreQuestion.getQuestion(ADDON_MOD_QUIZ_COMPONENT, attemptId, question.slot, siteId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!dbQuestion) {
|
if (!dbQuestion) {
|
||||||
|
@ -230,8 +231,8 @@ export class AddonModQuizOfflineProvider {
|
||||||
const db = await CoreSites.getSiteDb(siteId);
|
const db = await CoreSites.getSiteDb(siteId);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
CoreQuestion.removeAttemptAnswers(AddonModQuizProvider.COMPONENT, attemptId, siteId),
|
CoreQuestion.removeAttemptAnswers(ADDON_MOD_QUIZ_COMPONENT, attemptId, siteId),
|
||||||
CoreQuestion.removeAttemptQuestions(AddonModQuizProvider.COMPONENT, attemptId, siteId),
|
CoreQuestion.removeAttemptQuestions(ADDON_MOD_QUIZ_COMPONENT, attemptId, siteId),
|
||||||
db.deleteRecords(ATTEMPTS_TABLE_NAME, { id: attemptId }),
|
db.deleteRecords(ATTEMPTS_TABLE_NAME, { id: attemptId }),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -248,8 +249,8 @@ export class AddonModQuizOfflineProvider {
|
||||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
CoreQuestion.removeQuestion(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId),
|
CoreQuestion.removeQuestion(ADDON_MOD_QUIZ_COMPONENT, attemptId, slot, siteId),
|
||||||
CoreQuestion.removeQuestionAnswers(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId),
|
CoreQuestion.removeQuestionAnswers(ADDON_MOD_QUIZ_COMPONENT, attemptId, slot, siteId),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,7 +300,7 @@ export class AddonModQuizOfflineProvider {
|
||||||
|
|
||||||
const state = await CoreQuestionBehaviourDelegate.determineNewState(
|
const state = await CoreQuestionBehaviourDelegate.determineNewState(
|
||||||
quiz.preferredbehaviour ?? '',
|
quiz.preferredbehaviour ?? '',
|
||||||
AddonModQuizProvider.COMPONENT,
|
ADDON_MOD_QUIZ_COMPONENT,
|
||||||
attempt.id,
|
attempt.id,
|
||||||
question,
|
question,
|
||||||
quiz.coursemodule,
|
quiz.coursemodule,
|
||||||
|
@ -312,12 +313,12 @@ export class AddonModQuizOfflineProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete previously stored answers for this question.
|
// Delete previously stored answers for this question.
|
||||||
await CoreQuestion.removeQuestionAnswers(AddonModQuizProvider.COMPONENT, attempt.id, question.slot, siteId);
|
await CoreQuestion.removeQuestionAnswers(ADDON_MOD_QUIZ_COMPONENT, attempt.id, question.slot, siteId);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Now save the answers.
|
// Now save the answers.
|
||||||
await CoreQuestion.saveAnswers(
|
await CoreQuestion.saveAnswers(
|
||||||
AddonModQuizProvider.COMPONENT,
|
ADDON_MOD_QUIZ_COMPONENT,
|
||||||
quiz.id,
|
quiz.id,
|
||||||
attempt.id,
|
attempt.id,
|
||||||
attempt.userid ?? CoreSites.getCurrentSiteUserId(),
|
attempt.userid ?? CoreSites.getCurrentSiteUserId(),
|
||||||
|
@ -332,7 +333,7 @@ export class AddonModQuizOfflineProvider {
|
||||||
const question = questionsWithAnswers[Number(slot)];
|
const question = questionsWithAnswers[Number(slot)];
|
||||||
|
|
||||||
await CoreQuestion.saveQuestion(
|
await CoreQuestion.saveQuestion(
|
||||||
AddonModQuizProvider.COMPONENT,
|
ADDON_MOD_QUIZ_COMPONENT,
|
||||||
quiz.id,
|
quiz.id,
|
||||||
attempt.id,
|
attempt.id,
|
||||||
attempt.userid ?? CoreSites.getCurrentSiteUserId(),
|
attempt.userid ?? CoreSites.getCurrentSiteUserId(),
|
||||||
|
|
|
@ -29,8 +29,9 @@ import { makeSingleton, Translate } from '@singletons';
|
||||||
import { CoreEvents } from '@singletons/events';
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { AddonModQuizAttemptDBRecord } from './database/quiz';
|
import { AddonModQuizAttemptDBRecord } from './database/quiz';
|
||||||
import { AddonModQuizPrefetchHandler } from './handlers/prefetch';
|
import { AddonModQuizPrefetchHandler } from './handlers/prefetch';
|
||||||
import { AddonModQuiz, AddonModQuizAttemptWSData, AddonModQuizProvider, AddonModQuizQuizWSData } from './quiz';
|
import { AddonModQuiz, AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from './quiz';
|
||||||
import { AddonModQuizOffline, AddonModQuizQuestionsWithAnswers } from './quiz-offline';
|
import { AddonModQuizOffline, AddonModQuizQuestionsWithAnswers } from './quiz-offline';
|
||||||
|
import { ADDON_MOD_QUIZ_COMPONENT } from '../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to sync quizzes.
|
* Service to sync quizzes.
|
||||||
|
@ -79,7 +80,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
||||||
for (const slot in options.onlineQuestions) {
|
for (const slot in options.onlineQuestions) {
|
||||||
promises.push(CoreQuestionDelegate.deleteOfflineData(
|
promises.push(CoreQuestionDelegate.deleteOfflineData(
|
||||||
options.onlineQuestions[slot],
|
options.onlineQuestions[slot],
|
||||||
AddonModQuizProvider.COMPONENT,
|
ADDON_MOD_QUIZ_COMPONENT,
|
||||||
quiz.coursemodule,
|
quiz.coursemodule,
|
||||||
siteId,
|
siteId,
|
||||||
));
|
));
|
||||||
|
@ -104,13 +105,13 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
||||||
|
|
||||||
// Check if online attempt was finished because of the sync.
|
// Check if online attempt was finished because of the sync.
|
||||||
let attemptFinished = false;
|
let attemptFinished = false;
|
||||||
if (options.onlineAttempt && !AddonModQuiz.isAttemptFinished(options.onlineAttempt.state)) {
|
if (options.onlineAttempt && !AddonModQuiz.isAttemptCompleted(options.onlineAttempt.state)) {
|
||||||
// Attempt wasn't finished at start. Check if it's finished now.
|
// Attempt wasn't finished at start. Check if it's finished now.
|
||||||
const attempts = await AddonModQuiz.getUserAttempts(quiz.id, { cmId: quiz.coursemodule, siteId });
|
const attempts = await AddonModQuiz.getUserAttempts(quiz.id, { cmId: quiz.coursemodule, siteId });
|
||||||
|
|
||||||
const attempt = attempts.find(attempt => attempt.id == options?.onlineAttempt?.id);
|
const attempt = attempts.find(attempt => attempt.id == options?.onlineAttempt?.id);
|
||||||
|
|
||||||
attemptFinished = attempt ? AddonModQuiz.isAttemptFinished(attempt.state) : false;
|
attemptFinished = attempt ? AddonModQuiz.isAttemptCompleted(attempt.state) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { warnings, attemptFinished, updated: !!options.updated || !!options.removeAttempt };
|
return { warnings, attemptFinished, updated: !!options.updated || !!options.removeAttempt };
|
||||||
|
@ -204,7 +205,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
||||||
}
|
}
|
||||||
quizIds[attempt.quizid] = true;
|
quizIds[attempt.quizid] = true;
|
||||||
|
|
||||||
if (CoreSync.isBlocked(AddonModQuizProvider.COMPONENT, attempt.quizid, siteId)) {
|
if (CoreSync.isBlocked(ADDON_MOD_QUIZ_COMPONENT, attempt.quizid, siteId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,7 +269,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that quiz isn't blocked.
|
// Verify that quiz isn't blocked.
|
||||||
if (CoreSync.isBlocked(AddonModQuizProvider.COMPONENT, quiz.id, siteId)) {
|
if (CoreSync.isBlocked(ADDON_MOD_QUIZ_COMPONENT, quiz.id, siteId)) {
|
||||||
this.logger.debug('Cannot sync quiz ' + quiz.id + ' because it is blocked.');
|
this.logger.debug('Cannot sync quiz ' + quiz.id + ' because it is blocked.');
|
||||||
|
|
||||||
throw new CoreError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
|
throw new CoreError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
|
||||||
|
@ -300,7 +301,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
||||||
|
|
||||||
// Sync offline logs.
|
// Sync offline logs.
|
||||||
await CoreUtils.ignoreErrors(
|
await CoreUtils.ignoreErrors(
|
||||||
CoreCourseLogHelper.syncActivity(AddonModQuizProvider.COMPONENT, quiz.id, siteId),
|
CoreCourseLogHelper.syncActivity(ADDON_MOD_QUIZ_COMPONENT, quiz.id, siteId),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get all the offline attempts for the quiz. It should always be 0 or 1 attempt
|
// Get all the offline attempts for the quiz. It should always be 0 or 1 attempt
|
||||||
|
@ -323,7 +324,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
||||||
const lastAttemptId = onlineAttempts.length ? onlineAttempts[onlineAttempts.length - 1].id : undefined;
|
const lastAttemptId = onlineAttempts.length ? onlineAttempts[onlineAttempts.length - 1].id : undefined;
|
||||||
const onlineAttempt = onlineAttempts.find((attempt) => attempt.id == offlineAttempt.id);
|
const onlineAttempt = onlineAttempts.find((attempt) => attempt.id == offlineAttempt.id);
|
||||||
|
|
||||||
if (!onlineAttempt || AddonModQuiz.isAttemptFinished(onlineAttempt.state)) {
|
if (!onlineAttempt || AddonModQuiz.isAttemptCompleted(onlineAttempt.state)) {
|
||||||
// Attempt not found or it's finished in online. Discard it.
|
// Attempt not found or it's finished in online. Discard it.
|
||||||
warnings.push(Translate.instant('addon.mod_quiz.warningattemptfinished'));
|
warnings.push(Translate.instant('addon.mod_quiz.warningattemptfinished'));
|
||||||
|
|
||||||
|
@ -381,7 +382,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
||||||
await CoreQuestionDelegate.prepareSyncData(
|
await CoreQuestionDelegate.prepareSyncData(
|
||||||
onlineQuestion,
|
onlineQuestion,
|
||||||
offlineQuestions[slot].answers,
|
offlineQuestions[slot].answers,
|
||||||
AddonModQuizProvider.COMPONENT,
|
ADDON_MOD_QUIZ_COMPONENT,
|
||||||
quiz.coursemodule,
|
quiz.coursemodule,
|
||||||
siteId,
|
siteId,
|
||||||
);
|
);
|
||||||
|
|
|
@ -37,13 +37,24 @@ 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';
|
||||||
import { QUESTION_INVALID_STATE_CLASSES, QUESTION_TODO_STATE_CLASSES } from '@features/question/constants';
|
import {
|
||||||
|
QUESTION_INVALID_STATE_CLASSES,
|
||||||
const ROOT_CACHE_KEY = 'mmaModQuiz:';
|
QUESTION_TODO_STATE_CLASSES,
|
||||||
|
QuestionDisplayOptionsMarks,
|
||||||
|
QuestionDisplayOptionsValues,
|
||||||
|
} from '@features/question/constants';
|
||||||
|
import {
|
||||||
|
ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT,
|
||||||
|
AddonModQuizAttemptStates,
|
||||||
|
ADDON_MOD_QUIZ_COMPONENT,
|
||||||
|
AddonModQuizGradeMethods,
|
||||||
|
AddonModQuizDisplayOptionsAttemptStates,
|
||||||
|
ADDON_MOD_QUIZ_IMMEDIATELY_AFTER_PERIOD,
|
||||||
|
} from '../constants';
|
||||||
|
import { CoreIonicColorNames } from '@singletons/colors';
|
||||||
|
|
||||||
declare module '@singletons/events' {
|
declare module '@singletons/events' {
|
||||||
|
|
||||||
|
@ -53,7 +64,7 @@ declare module '@singletons/events' {
|
||||||
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||||
*/
|
*/
|
||||||
export interface CoreEventsData {
|
export interface CoreEventsData {
|
||||||
[AddonModQuizProvider.ATTEMPT_FINISHED_EVENT]: AddonModQuizAttemptFinishedData;
|
[ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT]: AddonModQuizAttemptFinishedData;
|
||||||
[AddonModQuizSyncProvider.AUTO_SYNCED]: AddonModQuizAutoSyncData;
|
[AddonModQuizSyncProvider.AUTO_SYNCED]: AddonModQuizAutoSyncData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,27 +76,7 @@ declare module '@singletons/events' {
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AddonModQuizProvider {
|
export class AddonModQuizProvider {
|
||||||
|
|
||||||
static readonly COMPONENT = 'mmaModQuiz';
|
protected static readonly ROOT_CACHE_KEY = 'mmaModQuiz:';
|
||||||
static readonly ATTEMPT_FINISHED_EVENT = 'addon_mod_quiz_attempt_finished';
|
|
||||||
|
|
||||||
// Grade methods.
|
|
||||||
static readonly GRADEHIGHEST = 1;
|
|
||||||
static readonly GRADEAVERAGE = 2;
|
|
||||||
static readonly ATTEMPTFIRST = 3;
|
|
||||||
static readonly ATTEMPTLAST = 4;
|
|
||||||
|
|
||||||
// Question options.
|
|
||||||
static readonly QUESTION_OPTIONS_MAX_ONLY = 1;
|
|
||||||
static readonly QUESTION_OPTIONS_MARK_AND_MAX = 2;
|
|
||||||
|
|
||||||
// Attempt state.
|
|
||||||
static readonly ATTEMPT_IN_PROGRESS = 'inprogress';
|
|
||||||
static readonly ATTEMPT_OVERDUE = 'overdue';
|
|
||||||
static readonly ATTEMPT_FINISHED = 'finished';
|
|
||||||
static readonly ATTEMPT_ABANDONED = 'abandoned';
|
|
||||||
|
|
||||||
// Show the countdown timer if there is less than this amount of time left before the the quiz close date.
|
|
||||||
static readonly QUIZ_SHOW_TIME_BEFORE_DEADLINE = 3600;
|
|
||||||
|
|
||||||
protected logger: CoreLogger;
|
protected logger: CoreLogger;
|
||||||
|
|
||||||
|
@ -164,7 +155,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getAttemptAccessInformationCommonCacheKey(quizId: number): string {
|
protected getAttemptAccessInformationCommonCacheKey(quizId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'attemptAccessInformation:' + quizId;
|
return AddonModQuizProvider.ROOT_CACHE_KEY + 'attemptAccessInformation:' + quizId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -189,7 +180,7 @@ export class AddonModQuizProvider {
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getAttemptAccessInformationCacheKey(quizId, attemptId),
|
cacheKey: this.getAttemptAccessInformationCacheKey(quizId, attemptId),
|
||||||
component: AddonModQuizProvider.COMPONENT,
|
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
};
|
};
|
||||||
|
@ -215,7 +206,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getAttemptDataCommonCacheKey(attemptId: number): string {
|
protected getAttemptDataCommonCacheKey(attemptId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'attemptData:' + attemptId;
|
return AddonModQuizProvider.ROOT_CACHE_KEY + 'attemptData:' + attemptId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -248,7 +239,7 @@ export class AddonModQuizProvider {
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getAttemptDataCacheKey(attemptId, page),
|
cacheKey: this.getAttemptDataCacheKey(attemptId, page),
|
||||||
component: AddonModQuizProvider.COMPONENT,
|
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
};
|
};
|
||||||
|
@ -288,10 +279,10 @@ export class AddonModQuizProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (attempt.state) {
|
switch (attempt.state) {
|
||||||
case AddonModQuizProvider.ATTEMPT_IN_PROGRESS:
|
case AddonModQuizAttemptStates.IN_PROGRESS:
|
||||||
return dueDate * 1000;
|
return dueDate * 1000;
|
||||||
|
|
||||||
case AddonModQuizProvider.ATTEMPT_OVERDUE:
|
case AddonModQuizAttemptStates.OVERDUE:
|
||||||
return (dueDate + (quiz.graceperiod ?? 0)) * 1000;
|
return (dueDate + (quiz.graceperiod ?? 0)) * 1000;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -311,7 +302,7 @@ export class AddonModQuizProvider {
|
||||||
getAttemptDueDateWarning(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttemptWSData): string | undefined {
|
getAttemptDueDateWarning(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttemptWSData): string | undefined {
|
||||||
const dueDate = this.getAttemptDueDate(quiz, attempt);
|
const dueDate = this.getAttemptDueDate(quiz, attempt);
|
||||||
|
|
||||||
if (attempt.state === AddonModQuizProvider.ATTEMPT_OVERDUE) {
|
if (attempt.state === AddonModQuizAttemptStates.OVERDUE) {
|
||||||
return Translate.instant(
|
return Translate.instant(
|
||||||
'addon.mod_quiz.overduemustbesubmittedby',
|
'addon.mod_quiz.overduemustbesubmittedby',
|
||||||
{ $a: CoreTimeUtils.userDate(dueDate) },
|
{ $a: CoreTimeUtils.userDate(dueDate) },
|
||||||
|
@ -322,73 +313,158 @@ export class AddonModQuizProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turn attempt's state into a readable state, including some extra data depending on the state.
|
* Get the display option value related to the attempt state.
|
||||||
|
* Equivalent to LMS quiz_attempt_state.
|
||||||
*
|
*
|
||||||
* @param quiz Quiz.
|
* @param quiz Quiz.
|
||||||
* @param attempt Attempt.
|
* @param attempt Attempt.
|
||||||
* @returns List of state sentences.
|
* @returns Display option value.
|
||||||
*/
|
*/
|
||||||
getAttemptReadableState(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttempt): string[] {
|
getAttemptStateDisplayOption(
|
||||||
if (attempt.finishedOffline) {
|
quiz: AddonModQuizQuizWSData,
|
||||||
return [Translate.instant('addon.mod_quiz.finishnotsynced')];
|
attempt: AddonModQuizAttemptWSData,
|
||||||
|
): AddonModQuizDisplayOptionsAttemptStates {
|
||||||
|
if (attempt.state === AddonModQuizAttemptStates.IN_PROGRESS) {
|
||||||
|
return AddonModQuizDisplayOptionsAttemptStates.DURING;
|
||||||
|
} else if (quiz.timeclose && Date.now() >= quiz.timeclose * 1000) {
|
||||||
|
return AddonModQuizDisplayOptionsAttemptStates.AFTER_CLOSE;
|
||||||
|
} else if (Date.now() < ((attempt.timefinish ?? 0) + ADDON_MOD_QUIZ_IMMEDIATELY_AFTER_PERIOD) * 1000) {
|
||||||
|
return AddonModQuizDisplayOptionsAttemptStates.IMMEDIATELY_AFTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (attempt.state) {
|
return AddonModQuizDisplayOptionsAttemptStates.LATER_WHILE_OPEN;
|
||||||
case AddonModQuizProvider.ATTEMPT_IN_PROGRESS:
|
|
||||||
return [Translate.instant('addon.mod_quiz.stateinprogress')];
|
|
||||||
|
|
||||||
case AddonModQuizProvider.ATTEMPT_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;
|
/**
|
||||||
|
* Get display options for a certain quiz.
|
||||||
|
* Equivalent to LMS display_options::make_from_quiz.
|
||||||
|
*
|
||||||
|
* @param quiz Quiz.
|
||||||
|
* @param state State.
|
||||||
|
* @returns Display options.
|
||||||
|
*/
|
||||||
|
getDisplayOptionsForQuiz(
|
||||||
|
quiz: AddonModQuizQuizWSData,
|
||||||
|
state: AddonModQuizDisplayOptionsAttemptStates,
|
||||||
|
): AddonModQuizDisplayOptions {
|
||||||
|
const marksOption = this.calculateDisplayOptionValue(
|
||||||
|
quiz.reviewmarks ?? 0,
|
||||||
|
state,
|
||||||
|
QuestionDisplayOptionsMarks.MARK_AND_MAX,
|
||||||
|
QuestionDisplayOptionsMarks.MAX_ONLY,
|
||||||
|
);
|
||||||
|
const feedbackOption = this.calculateDisplayOptionValue(quiz.reviewspecificfeedback ?? 0, state);
|
||||||
|
|
||||||
|
return {
|
||||||
|
attempt: this.calculateDisplayOptionValue(quiz.reviewattempt ?? 0, state, true, false),
|
||||||
|
correctness: this.calculateDisplayOptionValue(quiz.reviewcorrectness ?? 0, state),
|
||||||
|
marks: quiz.reviewmaxmarks !== undefined ?
|
||||||
|
this.calculateDisplayOptionValue<QuestionDisplayOptionsMarks | QuestionDisplayOptionsValues>(
|
||||||
|
quiz.reviewmaxmarks,
|
||||||
|
state,
|
||||||
|
marksOption,
|
||||||
|
QuestionDisplayOptionsValues.HIDDEN,
|
||||||
|
) :
|
||||||
|
marksOption,
|
||||||
|
feedback: feedbackOption,
|
||||||
|
generalfeedback: this.calculateDisplayOptionValue(quiz.reviewgeneralfeedback ?? 0, state),
|
||||||
|
rightanswer: this.calculateDisplayOptionValue(quiz.reviewrightanswer ?? 0, state),
|
||||||
|
overallfeedback: this.calculateDisplayOptionValue(quiz.reviewoverallfeedback ?? 0, state),
|
||||||
|
numpartscorrect: feedbackOption,
|
||||||
|
manualcomment: feedbackOption,
|
||||||
|
markdp: quiz.questiondecimalpoints !== undefined && quiz.questiondecimalpoints !== -1 ?
|
||||||
|
quiz.questiondecimalpoints :
|
||||||
|
(quiz.decimalpoints ?? 0),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case AddonModQuizProvider.ATTEMPT_FINISHED:
|
/**
|
||||||
return [
|
* Calculate the value for a certain display option.
|
||||||
Translate.instant('addon.mod_quiz.statefinished'),
|
*
|
||||||
Translate.instant(
|
* @param setting Setting value related to the option.
|
||||||
'addon.mod_quiz.statefinisheddetails',
|
* @param state Display options state.
|
||||||
{ $a: CoreTimeUtils.userDate((attempt.timefinish ?? 0) * 1000) },
|
* @param whenSet Value to return if setting is set.
|
||||||
),
|
* @param whenNotSet Value to return if setting is not set.
|
||||||
];
|
* @returns Display option.
|
||||||
|
*/
|
||||||
|
protected calculateDisplayOptionValue<T = AddonModQuizDisplayOptionValue>(
|
||||||
|
setting: number,
|
||||||
|
state: AddonModQuizDisplayOptionsAttemptStates,
|
||||||
|
whenSet: T,
|
||||||
|
whenNotSet: T,
|
||||||
|
): T;
|
||||||
|
protected calculateDisplayOptionValue(
|
||||||
|
setting: number,
|
||||||
|
state: AddonModQuizDisplayOptionsAttemptStates,
|
||||||
|
): QuestionDisplayOptionsValues;
|
||||||
|
protected calculateDisplayOptionValue(
|
||||||
|
setting: number,
|
||||||
|
state: AddonModQuizDisplayOptionsAttemptStates,
|
||||||
|
whenSet: AddonModQuizDisplayOptionValue = QuestionDisplayOptionsValues.VISIBLE,
|
||||||
|
whenNotSet: AddonModQuizDisplayOptionValue = QuestionDisplayOptionsValues.HIDDEN,
|
||||||
|
): AddonModQuizDisplayOptionValue {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
if (setting & state) {
|
||||||
|
return whenSet;
|
||||||
|
}
|
||||||
|
|
||||||
case AddonModQuizProvider.ATTEMPT_ABANDONED:
|
return whenNotSet;
|
||||||
return [Translate.instant('addon.mod_quiz.stateabandoned')];
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn attempt's state into a readable state name.
|
||||||
|
*
|
||||||
|
* @param state State.
|
||||||
|
* @param finishedOffline Whether the attempt was finished offline.
|
||||||
|
* @returns Readable state name.
|
||||||
|
*/
|
||||||
|
getAttemptReadableStateName(state: string, finishedOffline = false): string {
|
||||||
|
if (finishedOffline) {
|
||||||
|
return Translate.instant('core.submittedoffline');
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case AddonModQuizAttemptStates.IN_PROGRESS:
|
||||||
|
return Translate.instant('addon.mod_quiz.stateinprogress');
|
||||||
|
|
||||||
|
case AddonModQuizAttemptStates.OVERDUE:
|
||||||
|
return Translate.instant('addon.mod_quiz.stateoverdue');
|
||||||
|
|
||||||
|
case AddonModQuizAttemptStates.FINISHED:
|
||||||
|
return Translate.instant('addon.mod_quiz.statefinished');
|
||||||
|
|
||||||
|
case AddonModQuizAttemptStates.ABANDONED:
|
||||||
|
return Translate.instant('addon.mod_quiz.stateabandoned');
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return [];
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turn attempt's state into a readable state name, without any more data.
|
* Get the color to apply to the attempt state.
|
||||||
*
|
*
|
||||||
* @param state State.
|
* @param state State.
|
||||||
* @returns Readable state name.
|
* @param finishedOffline Whether the attempt was finished offline.
|
||||||
|
* @returns State color.
|
||||||
*/
|
*/
|
||||||
getAttemptReadableStateName(state: string): string {
|
getAttemptStateColor(state: string, finishedOffline = false): string {
|
||||||
|
if (finishedOffline) {
|
||||||
|
return CoreIonicColorNames.MEDIUM;
|
||||||
|
}
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case AddonModQuizProvider.ATTEMPT_IN_PROGRESS:
|
case AddonModQuizAttemptStates.IN_PROGRESS:
|
||||||
return Translate.instant('addon.mod_quiz.stateinprogress');
|
return CoreIonicColorNames.WARNING;
|
||||||
|
|
||||||
case AddonModQuizProvider.ATTEMPT_OVERDUE:
|
case AddonModQuizAttemptStates.OVERDUE:
|
||||||
return Translate.instant('addon.mod_quiz.stateoverdue');
|
return CoreIonicColorNames.INFO;
|
||||||
|
|
||||||
case AddonModQuizProvider.ATTEMPT_FINISHED:
|
case AddonModQuizAttemptStates.FINISHED:
|
||||||
return Translate.instant('addon.mod_quiz.statefinished');
|
return CoreIonicColorNames.SUCCESS;
|
||||||
|
|
||||||
case AddonModQuizProvider.ATTEMPT_ABANDONED:
|
case AddonModQuizAttemptStates.ABANDONED:
|
||||||
return Translate.instant('addon.mod_quiz.stateabandoned');
|
return CoreIonicColorNames.DANGER;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
|
@ -413,7 +489,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getAttemptReviewCommonCacheKey(attemptId: number): string {
|
protected getAttemptReviewCommonCacheKey(attemptId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'attemptReview:' + attemptId;
|
return AddonModQuizProvider.ROOT_CACHE_KEY + 'attemptReview:' + attemptId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -437,8 +513,7 @@ export class AddonModQuizProvider {
|
||||||
};
|
};
|
||||||
const preSets = {
|
const preSets = {
|
||||||
cacheKey: this.getAttemptReviewCacheKey(attemptId, page),
|
cacheKey: this.getAttemptReviewCacheKey(attemptId, page),
|
||||||
cacheErrors: ['noreview'],
|
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||||
component: AddonModQuizProvider.COMPONENT,
|
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
};
|
};
|
||||||
|
@ -457,7 +532,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getAttemptSummaryCacheKey(attemptId: number): string {
|
protected getAttemptSummaryCacheKey(attemptId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'attemptSummary:' + attemptId;
|
return AddonModQuizProvider.ROOT_CACHE_KEY + 'attemptSummary:' + attemptId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -487,7 +562,7 @@ export class AddonModQuizProvider {
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getAttemptSummaryCacheKey(attemptId),
|
cacheKey: this.getAttemptSummaryCacheKey(attemptId),
|
||||||
component: AddonModQuizProvider.COMPONENT,
|
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
};
|
};
|
||||||
|
@ -521,7 +596,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getCombinedReviewOptionsCommonCacheKey(quizId: number): string {
|
protected getCombinedReviewOptionsCommonCacheKey(quizId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'combinedReviewOptions:' + quizId;
|
return AddonModQuizProvider.ROOT_CACHE_KEY + 'combinedReviewOptions:' + quizId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -544,7 +619,7 @@ export class AddonModQuizProvider {
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getCombinedReviewOptionsCacheKey(quizId, userId),
|
cacheKey: this.getCombinedReviewOptionsCacheKey(quizId, userId),
|
||||||
component: AddonModQuizProvider.COMPONENT,
|
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
};
|
};
|
||||||
|
@ -581,7 +656,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getFeedbackForGradeCommonCacheKey(quizId: number): string {
|
protected getFeedbackForGradeCommonCacheKey(quizId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'feedbackForGrade:' + quizId;
|
return AddonModQuizProvider.ROOT_CACHE_KEY + 'feedbackForGrade:' + quizId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -606,7 +681,7 @@ export class AddonModQuizProvider {
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getFeedbackForGradeCacheKey(quizId, grade),
|
cacheKey: this.getFeedbackForGradeCacheKey(quizId, grade),
|
||||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
component: AddonModQuizProvider.COMPONENT,
|
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
};
|
};
|
||||||
|
@ -664,12 +739,12 @@ export class AddonModQuizProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of attempts, returns the last finished attempt.
|
* Given a list of attempts, returns the last completed attempt.
|
||||||
*
|
*
|
||||||
* @param attempts Attempts sorted. First attempt should be the first on the list.
|
* @param attempts Attempts sorted. First attempt should be the first on the list.
|
||||||
* @returns Last finished attempt.
|
* @returns Last completed attempt.
|
||||||
*/
|
*/
|
||||||
getLastFinishedAttemptFromList(attempts?: AddonModQuizAttemptWSData[]): AddonModQuizAttemptWSData | undefined {
|
getLastCompletedAttemptFromList(attempts?: AddonModQuizAttemptWSData[]): AddonModQuizAttemptWSData | undefined {
|
||||||
if (!attempts) {
|
if (!attempts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -677,7 +752,7 @@ export class AddonModQuizProvider {
|
||||||
for (let i = attempts.length - 1; i >= 0; i--) {
|
for (let i = attempts.length - 1; i >= 0; i--) {
|
||||||
const attempt = attempts[i];
|
const attempt = attempts[i];
|
||||||
|
|
||||||
if (this.isAttemptFinished(attempt.state)) {
|
if (this.isAttemptCompleted(attempt.state)) {
|
||||||
return attempt;
|
return attempt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -719,7 +794,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getQuizDataCacheKey(courseId: number): string {
|
protected getQuizDataCacheKey(courseId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'quiz:' + courseId;
|
return AddonModQuizProvider.ROOT_CACHE_KEY + 'quiz:' + courseId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -746,7 +821,7 @@ export class AddonModQuizProvider {
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getQuizDataCacheKey(courseId),
|
cacheKey: this.getQuizDataCacheKey(courseId),
|
||||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
component: AddonModQuizProvider.COMPONENT,
|
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -797,7 +872,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getQuizAccessInformationCacheKey(quizId: number): string {
|
protected getQuizAccessInformationCacheKey(quizId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'quizAccessInformation:' + quizId;
|
return AddonModQuizProvider.ROOT_CACHE_KEY + 'quizAccessInformation:' + quizId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -818,7 +893,7 @@ export class AddonModQuizProvider {
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getQuizAccessInformationCacheKey(quizId),
|
cacheKey: this.getQuizAccessInformationCacheKey(quizId),
|
||||||
component: AddonModQuizProvider.COMPONENT,
|
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
};
|
};
|
||||||
|
@ -842,13 +917,13 @@ export class AddonModQuizProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case AddonModQuizProvider.GRADEHIGHEST:
|
case AddonModQuizGradeMethods.HIGHEST_GRADE:
|
||||||
return Translate.instant('addon.mod_quiz.gradehighest');
|
return Translate.instant('addon.mod_quiz.gradehighest');
|
||||||
case AddonModQuizProvider.GRADEAVERAGE:
|
case AddonModQuizGradeMethods.AVERAGE_GRADE:
|
||||||
return Translate.instant('addon.mod_quiz.gradeaverage');
|
return Translate.instant('addon.mod_quiz.gradeaverage');
|
||||||
case AddonModQuizProvider.ATTEMPTFIRST:
|
case AddonModQuizGradeMethods.FIRST_ATTEMPT:
|
||||||
return Translate.instant('addon.mod_quiz.attemptfirst');
|
return Translate.instant('addon.mod_quiz.attemptfirst');
|
||||||
case AddonModQuizProvider.ATTEMPTLAST:
|
case AddonModQuizGradeMethods.LAST_ATTEMPT:
|
||||||
return Translate.instant('addon.mod_quiz.attemptlast');
|
return Translate.instant('addon.mod_quiz.attemptlast');
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
|
@ -862,7 +937,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getQuizRequiredQtypesCacheKey(quizId: number): string {
|
protected getQuizRequiredQtypesCacheKey(quizId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'quizRequiredQtypes:' + quizId;
|
return AddonModQuizProvider.ROOT_CACHE_KEY + 'quizRequiredQtypes:' + quizId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -881,7 +956,7 @@ export class AddonModQuizProvider {
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getQuizRequiredQtypesCacheKey(quizId),
|
cacheKey: this.getQuizRequiredQtypesCacheKey(quizId),
|
||||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
component: AddonModQuizProvider.COMPONENT,
|
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
};
|
};
|
||||||
|
@ -1015,7 +1090,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getUserAttemptsCommonCacheKey(quizId: number): string {
|
protected getUserAttemptsCommonCacheKey(quizId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'userAttempts:' + quizId;
|
return AddonModQuizProvider.ROOT_CACHE_KEY + 'userAttempts:' + quizId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1045,7 +1120,7 @@ export class AddonModQuizProvider {
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getUserAttemptsCacheKey(quizId, userId),
|
cacheKey: this.getUserAttemptsCacheKey(quizId, userId),
|
||||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
component: AddonModQuizProvider.COMPONENT,
|
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
};
|
};
|
||||||
|
@ -1073,7 +1148,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getUserBestGradeCommonCacheKey(quizId: number): string {
|
protected getUserBestGradeCommonCacheKey(quizId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'userBestGrade:' + quizId;
|
return AddonModQuizProvider.ROOT_CACHE_KEY + 'userBestGrade:' + quizId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1093,7 +1168,7 @@ export class AddonModQuizProvider {
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getUserBestGradeCacheKey(quizId, userId),
|
cacheKey: this.getUserBestGradeCacheKey(quizId, userId),
|
||||||
component: AddonModQuizProvider.COMPONENT,
|
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
};
|
};
|
||||||
|
@ -1424,13 +1499,13 @@ export class AddonModQuizProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an attempt is finished based on its state.
|
* Check if an attempt is "completed": finished or abandoned.
|
||||||
*
|
*
|
||||||
* @param state Attempt's state.
|
* @param state Attempt's state.
|
||||||
* @returns Whether it's finished.
|
* @returns Whether it's finished.
|
||||||
*/
|
*/
|
||||||
isAttemptFinished(state?: string): boolean {
|
isAttemptCompleted(state?: string): boolean {
|
||||||
return state == AddonModQuizProvider.ATTEMPT_FINISHED || state == AddonModQuizProvider.ATTEMPT_ABANDONED;
|
return state === AddonModQuizAttemptStates.FINISHED || state === AddonModQuizAttemptStates.ABANDONED;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1461,7 +1536,7 @@ export class AddonModQuizProvider {
|
||||||
* @returns Whether it's nearly over or over.
|
* @returns Whether it's nearly over or over.
|
||||||
*/
|
*/
|
||||||
isAttemptTimeNearlyOver(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttemptWSData): boolean {
|
isAttemptTimeNearlyOver(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttemptWSData): boolean {
|
||||||
if (attempt.state != AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
|
if (attempt.state !== AddonModQuizAttemptStates.IN_PROGRESS) {
|
||||||
// Attempt not in progress, return true.
|
// Attempt not in progress, return true.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -1600,7 +1675,7 @@ export class AddonModQuizProvider {
|
||||||
return CoreCourseLogHelper.log(
|
return CoreCourseLogHelper.log(
|
||||||
'mod_quiz_view_attempt_review',
|
'mod_quiz_view_attempt_review',
|
||||||
params,
|
params,
|
||||||
AddonModQuizProvider.COMPONENT,
|
ADDON_MOD_QUIZ_COMPONENT,
|
||||||
quizId,
|
quizId,
|
||||||
siteId,
|
siteId,
|
||||||
);
|
);
|
||||||
|
@ -1633,7 +1708,7 @@ export class AddonModQuizProvider {
|
||||||
return CoreCourseLogHelper.log(
|
return CoreCourseLogHelper.log(
|
||||||
'mod_quiz_view_attempt_summary',
|
'mod_quiz_view_attempt_summary',
|
||||||
params,
|
params,
|
||||||
AddonModQuizProvider.COMPONENT,
|
ADDON_MOD_QUIZ_COMPONENT,
|
||||||
quizId,
|
quizId,
|
||||||
siteId,
|
siteId,
|
||||||
);
|
);
|
||||||
|
@ -1654,7 +1729,7 @@ export class AddonModQuizProvider {
|
||||||
return CoreCourseLogHelper.log(
|
return CoreCourseLogHelper.log(
|
||||||
'mod_quiz_view_quiz',
|
'mod_quiz_view_quiz',
|
||||||
params,
|
params,
|
||||||
AddonModQuizProvider.COMPONENT,
|
ADDON_MOD_QUIZ_COMPONENT,
|
||||||
id,
|
id,
|
||||||
siteId,
|
siteId,
|
||||||
);
|
);
|
||||||
|
@ -1897,7 +1972,7 @@ export class AddonModQuizProvider {
|
||||||
shouldShowTimeLeft(rules: string[], attempt: AddonModQuizAttemptWSData, endTime: number): boolean {
|
shouldShowTimeLeft(rules: string[], attempt: AddonModQuizAttemptWSData, endTime: number): boolean {
|
||||||
const timeNow = CoreTimeUtils.timestamp();
|
const timeNow = CoreTimeUtils.timestamp();
|
||||||
|
|
||||||
if (attempt.state != AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
|
if (attempt.state !== AddonModQuizAttemptStates.IN_PROGRESS) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2205,6 +2280,7 @@ export type AddonModQuizQuizWSData = {
|
||||||
questiondecimalpoints?: number; // Number of decimal points to use when displaying question grades.
|
questiondecimalpoints?: number; // Number of decimal points to use when displaying question grades.
|
||||||
reviewattempt?: number; // Whether users are allowed to review their quiz attempts at various times.
|
reviewattempt?: number; // Whether users are allowed to review their quiz attempts at various times.
|
||||||
reviewcorrectness?: number; // Whether users are allowed to review their quiz attempts at various times.
|
reviewcorrectness?: number; // Whether users are allowed to review their quiz attempts at various times.
|
||||||
|
reviewmaxmarks?: number; // @since 4.3. Whether users are allowed to review their quiz attempts at various times.
|
||||||
reviewmarks?: number; // Whether users are allowed to review their quiz attempts at various times.
|
reviewmarks?: number; // Whether users are allowed to review their quiz attempts at various times.
|
||||||
reviewspecificfeedback?: number; // Whether users are allowed to review their quiz attempts at various times.
|
reviewspecificfeedback?: number; // Whether users are allowed to review their quiz attempts at various times.
|
||||||
reviewgeneralfeedback?: number; // Whether users are allowed to review their quiz attempts at various times.
|
reviewgeneralfeedback?: number; // Whether users are allowed to review their quiz attempts at various times.
|
||||||
|
@ -2392,10 +2468,31 @@ export type AddonModQuizViewQuizWSParams = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data passed to ATTEMPT_FINISHED_EVENT event.
|
* Data passed to ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT event.
|
||||||
*/
|
*/
|
||||||
export type AddonModQuizAttemptFinishedData = {
|
export type AddonModQuizAttemptFinishedData = {
|
||||||
quizId: number;
|
quizId: number;
|
||||||
attemptId: number;
|
attemptId: number;
|
||||||
synced: boolean;
|
synced: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quiz display option value.
|
||||||
|
*/
|
||||||
|
export type AddonModQuizDisplayOptionValue = QuestionDisplayOptionsMarks | QuestionDisplayOptionsValues | boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quiz display options, it can be used to determine which options to display.
|
||||||
|
*/
|
||||||
|
export type AddonModQuizDisplayOptions = {
|
||||||
|
attempt: boolean;
|
||||||
|
correctness: QuestionDisplayOptionsValues;
|
||||||
|
marks: QuestionDisplayOptionsMarks | QuestionDisplayOptionsValues;
|
||||||
|
feedback: QuestionDisplayOptionsValues;
|
||||||
|
generalfeedback: QuestionDisplayOptionsValues;
|
||||||
|
rightanswer: QuestionDisplayOptionsValues;
|
||||||
|
overallfeedback: QuestionDisplayOptionsValues;
|
||||||
|
numpartscorrect: QuestionDisplayOptionsValues;
|
||||||
|
manualcomment: QuestionDisplayOptionsValues;
|
||||||
|
markdp: number;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
@addon_mod_quiz @app @javascript
|
||||||
|
Feature: View list of attempts in the app
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "courses" exist:
|
||||||
|
| fullname | shortname |
|
||||||
|
| Course 1 | C1 |
|
||||||
|
And the following "users" exist:
|
||||||
|
| username |
|
||||||
|
| student1 |
|
||||||
|
And the following "course enrolments" exist:
|
||||||
|
| user | course | role |
|
||||||
|
| student1 | C1 | student |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber |
|
||||||
|
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
|
||||||
|
And the following "question categories" exist:
|
||||||
|
| contextlevel | reference | name |
|
||||||
|
| Course | C1 | Test questions |
|
||||||
|
And the following "questions" exist:
|
||||||
|
| questioncategory | qtype | name | questiontext |
|
||||||
|
| Test questions | truefalse | TF1 | Text of the first question |
|
||||||
|
And quiz "Quiz 1" contains the following questions:
|
||||||
|
| question | page |
|
||||||
|
| TF1 | 1 |
|
||||||
|
And user "student1" has attempted "Quiz 1" with responses:
|
||||||
|
| slot | response |
|
||||||
|
| 1 | True |
|
||||||
|
And user "student1" has started an attempt at quiz "Quiz 1"
|
||||||
|
|
||||||
|
Scenario: View finished and in progress attempts
|
||||||
|
Given I entered the quiz activity "Quiz 1" on course "Course 1" as "student1" in the app
|
||||||
|
Then I should find "In progress" within "Attempt 2" "ion-item" in the app
|
||||||
|
And I should find "Finished" within "Attempt 1" "ion-item" in the app
|
||||||
|
And I should find "100 / 100" within "Attempt 1" "ion-item" in the app
|
||||||
|
But I should not find "100" within "Attempt 2" "ion-item" in the app
|
||||||
|
And I should not find "Started" within "Your attempts" "ion-card" in the app
|
||||||
|
And I should not find "Completed" within "Your attempts" "ion-card" in the app
|
||||||
|
And I should not find "Marks" within "Your attempts" "ion-card" in the app
|
||||||
|
And I should not be able to press "Review" in the app
|
||||||
|
|
||||||
|
When I press "Attempt 1" in the app
|
||||||
|
Then I should find "Started" within "Your attempts" "ion-card" in the app
|
||||||
|
And I should find "Completed" in the app
|
||||||
|
And I should find "1/1" within "Marks" "ion-item" in the app
|
||||||
|
And I should be able to press "Review" in the app
|
||||||
|
|
||||||
|
Scenario: View abandoned attempts
|
||||||
|
Given the attempt at "Quiz 1" by "student1" was never submitted
|
||||||
|
And I entered the quiz activity "Quiz 1" on course "Course 1" as "student1" in the app
|
||||||
|
Then I should find "Never submitted" within "Attempt 2" "ion-item" in the app
|
||||||
|
But I should not find "100" within "Attempt 2" "ion-item" in the app
|
||||||
|
|
||||||
|
When I press "Attempt 2" in the app
|
||||||
|
Then I should find "Started" within "Your attempts" "ion-card" in the app
|
||||||
|
And I should be able to press "Review" in the app
|
||||||
|
But I should not find "Completed" in the app
|
||||||
|
And I should not find "Marks" in the app
|
||||||
|
And I should not find "Grade" in the app
|
|
@ -126,10 +126,10 @@ Feature: Attempt a quiz in app
|
||||||
When I press "Submit all and finish" in the app
|
When I press "Submit all and finish" in the app
|
||||||
And I press "Submit" near "Once you submit" in the app
|
And I press "Submit" near "Once you submit" in the app
|
||||||
Then I should find "Review" in the app
|
Then I should find "Review" in the app
|
||||||
And I should find "Started on" in the app
|
And I should find "Started" in the app
|
||||||
And I should find "State" in the app
|
And I should find "Status" in the app
|
||||||
And I should find "Completed on" in the app
|
And I should find "Completed" in the app
|
||||||
And I should find "Time taken" in the app
|
And I should find "Duration" in the app
|
||||||
And I should find "Marks" in the app
|
And I should find "Marks" in the app
|
||||||
And I should find "Grade" in the app
|
And I should find "Grade" in the app
|
||||||
And I should find "Question 1" in the app
|
And I should find "Question 1" in the app
|
||||||
|
@ -203,8 +203,9 @@ Feature: Attempt a quiz in app
|
||||||
And I press "Submit" in the app
|
And I press "Submit" in the app
|
||||||
Then I should find "Review" in the app
|
Then I should find "Review" in the app
|
||||||
|
|
||||||
When I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(1) p:nth-child(2)" with "[Started on date]"
|
When I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(2) p:nth-child(2)" with "[Started date]"
|
||||||
And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]"
|
And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed date]"
|
||||||
|
And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(4) p:nth-child(2)" with "[Duration]"
|
||||||
Then the UI should match the snapshot
|
Then the UI should match the snapshot
|
||||||
|
|
||||||
Given I open a browser tab with url "$WWWROOT"
|
Given I open a browser tab with url "$WWWROOT"
|
||||||
|
|
|
@ -127,10 +127,10 @@ Feature: Attempt a quiz in app
|
||||||
When I press "Submit all and finish" in the app
|
When I press "Submit all and finish" in the app
|
||||||
And I press "Submit" near "Once you submit" in the app
|
And I press "Submit" near "Once you submit" in the app
|
||||||
Then I should find "Review" in the app
|
Then I should find "Review" in the app
|
||||||
And I should find "Started on" in the app
|
And I should find "Started" in the app
|
||||||
And I should find "State" in the app
|
And I should find "Status" in the app
|
||||||
And I should find "Completed on" in the app
|
And I should find "Completed" in the app
|
||||||
And I should find "Time taken" in the app
|
And I should find "Duration" in the app
|
||||||
And I should find "Marks" in the app
|
And I should find "Marks" in the app
|
||||||
And I should find "Grade" in the app
|
And I should find "Grade" in the app
|
||||||
And I should find "Question 1" in the app
|
And I should find "Question 1" in the app
|
||||||
|
@ -203,9 +203,6 @@ Feature: Attempt a quiz in app
|
||||||
And I press "Submit" in the app
|
And I press "Submit" in the app
|
||||||
Then I should find "Review" in the app
|
Then I should find "Review" in the app
|
||||||
|
|
||||||
When I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(1) p:nth-child(2)" with "[Started on date]"
|
|
||||||
And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]"
|
|
||||||
|
|
||||||
Given I open a browser tab with url "$WWWROOT"
|
Given I open a browser tab with url "$WWWROOT"
|
||||||
When I am on the "quiz1" Activity page logged in as teacher1
|
When I am on the "quiz1" Activity page logged in as teacher1
|
||||||
And I follow "Attempts: 1"
|
And I follow "Attempts: 1"
|
||||||
|
|
|
@ -129,10 +129,10 @@ Feature: Attempt a quiz in app
|
||||||
When I press "Submit all and finish" in the app
|
When I press "Submit all and finish" in the app
|
||||||
And I press "Submit" near "Once you submit" in the app
|
And I press "Submit" near "Once you submit" in the app
|
||||||
Then I should find "Review" in the app
|
Then I should find "Review" in the app
|
||||||
And I should find "Started on" in the app
|
And I should find "Started" in the app
|
||||||
And I should find "Completed on" in the app
|
And I should find "Completed" in the app
|
||||||
And I should find "Time taken" in the app
|
And I should find "Duration" in the app
|
||||||
And I should find "Finished" within "State" "ion-item" in the app
|
And I should find "Finished" within "Status" "ion-item" in the app
|
||||||
And I should find "0 out of 1" within "Logic" "ion-item" in the app
|
And I should find "0 out of 1" within "Logic" "ion-item" in the app
|
||||||
And I should find "0 out of 1" within "Cognition" "ion-item" in the app
|
And I should find "0 out of 1" within "Cognition" "ion-item" in the app
|
||||||
And I should find "0/2" within "Marks" "ion-item" in the app
|
And I should find "0/2" within "Marks" "ion-item" in the app
|
||||||
|
@ -151,14 +151,14 @@ Feature: Attempt a quiz in app
|
||||||
| \mod_quiz\event\attempt_summary_viewed | quiz | Quiz 1 | Course 1 | |
|
| \mod_quiz\event\attempt_summary_viewed | quiz | Quiz 1 | Course 1 | |
|
||||||
|
|
||||||
When I press the back button in the app
|
When I press the back button in the app
|
||||||
And I press "Finished" in the app
|
And I press "Attempt 1" in the app
|
||||||
Then I should find "1" within "Attempt" "ion-item" in the app
|
Then I should find "1" within "Attempt" "ion-item" in the app
|
||||||
And I should find "Finished" within "State" "ion-item" in the app
|
And I should find "Finished" within "Status" "ion-item" in the app
|
||||||
And I should find "0" within "Logic / 1" "ion-item" in the app
|
And I should find "0 out of 1" within "Logic" "ion-item" in the app
|
||||||
And I should find "0" within "Cognition / 1" "ion-item" in the app
|
And I should find "0 out of 1" within "Cognition" "ion-item" in the app
|
||||||
And I should find "0" within "Marks / 2" "ion-item" in the app
|
And I should find "0/2" within "Marks" "ion-item" in the app
|
||||||
And I should find "0" within "Grade / 100" "ion-item" in the app
|
And I should find "0 out of 100" within "Grade" "ion-item" in the app
|
||||||
And I should find "Review" in the app
|
And I should be able to press "Review" in the app
|
||||||
|
|
||||||
Scenario: Attempt a quiz (all question types)
|
Scenario: Attempt a quiz (all question types)
|
||||||
Given I entered the quiz activity "Quiz 2" on course "Course 1" as "student1" in the app
|
Given I entered the quiz activity "Quiz 2" on course "Course 1" as "student1" in the app
|
||||||
|
@ -233,8 +233,9 @@ Feature: Attempt a quiz in app
|
||||||
When I press "Submit" in the app
|
When I press "Submit" in the app
|
||||||
Then I should find "Review" in the app
|
Then I should find "Review" in the app
|
||||||
|
|
||||||
When I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(1) p:nth-child(2)" with "[Started on date]"
|
When I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(2) p:nth-child(2)" with "[Started date]"
|
||||||
And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]"
|
And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed date]"
|
||||||
|
And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(4) p:nth-child(2)" with "[Duration]"
|
||||||
Then the UI should match the snapshot
|
Then the UI should match the snapshot
|
||||||
|
|
||||||
Given I open a browser tab with url "$WWWROOT"
|
Given I open a browser tab with url "$WWWROOT"
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
@addon_mod_quiz @app @javascript
|
||||||
|
Feature: Users can only review attempts that are allowed to be reviewed
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "courses" exist:
|
||||||
|
| fullname | shortname |
|
||||||
|
| Course 1 | C1 |
|
||||||
|
And the following "users" exist:
|
||||||
|
| username |
|
||||||
|
| student1 |
|
||||||
|
And the following "course enrolments" exist:
|
||||||
|
| user | course | role |
|
||||||
|
| student1 | C1 | student |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | course | idnumber | timeclose | attemptimmediately | attemptopen | attemptclosed |
|
||||||
|
| quiz | Quiz never review | C1 | quiz1 | ## 31 December 2035 23:59 ## | 0 | 0 | 0 |
|
||||||
|
| quiz | Quiz review when closed | C1 | quiz2 | ## 31 December 2035 23:59 ## | 0 | 0 | 1 |
|
||||||
|
| quiz | Quiz review after immed | C1 | quiz3 | 0 | 0 | 1 | 1 |
|
||||||
|
| quiz | Quiz review only immed | C1 | quiz4 | 0 | 1 | 0 | 0 |
|
||||||
|
And the following "question categories" exist:
|
||||||
|
| contextlevel | reference | name |
|
||||||
|
| Course | C1 | Test questions |
|
||||||
|
And the following "questions" exist:
|
||||||
|
| questioncategory | qtype | name | questiontext |
|
||||||
|
| Test questions | truefalse | TF1 | Text of the first question |
|
||||||
|
And quiz "Quiz never review" contains the following questions:
|
||||||
|
| question | page |
|
||||||
|
| TF1 | 1 |
|
||||||
|
And quiz "Quiz review when closed" contains the following questions:
|
||||||
|
| question | page |
|
||||||
|
| TF1 | 1 |
|
||||||
|
And quiz "Quiz review after immed" contains the following questions:
|
||||||
|
| question | page |
|
||||||
|
| TF1 | 1 |
|
||||||
|
And quiz "Quiz review only immed" contains the following questions:
|
||||||
|
| question | page |
|
||||||
|
| TF1 | 1 |
|
||||||
|
And user "student1" has attempted "Quiz never review" with responses:
|
||||||
|
| slot | response |
|
||||||
|
| 1 | True |
|
||||||
|
And user "student1" has attempted "Quiz review when closed" with responses:
|
||||||
|
| slot | response |
|
||||||
|
| 1 | True |
|
||||||
|
And user "student1" has attempted "Quiz review after immed" with responses:
|
||||||
|
| slot | response |
|
||||||
|
| 1 | True |
|
||||||
|
And user "student1" has attempted "Quiz review only immed" with responses:
|
||||||
|
| slot | response |
|
||||||
|
| 1 | True |
|
||||||
|
|
||||||
|
Scenario: Can review only when the attempt is allowed to be reviewed
|
||||||
|
Given I entered the quiz activity "Quiz review after immed" on course "Course 1" as "student1" in the app
|
||||||
|
And I press "Attempt 1" in the app
|
||||||
|
Then I should not be able to press "Review" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Quiz review only immed" in the app
|
||||||
|
And I press "Attempt 1" in the app
|
||||||
|
And I press "Review" in the app
|
||||||
|
Then I should find "Question 1" in the app
|
||||||
|
|
||||||
|
# Wait the "immediate after" time and check that now the behaviour is the opposite.
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press the back button in the app
|
||||||
|
And I wait "120" seconds
|
||||||
|
And I press "Quiz review only immed" in the app
|
||||||
|
And I press "Attempt 1" in the app
|
||||||
|
Then I should find "You are not allowed to review this attempt" in the app
|
||||||
|
And I should not be able to press "Review" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Quiz review after immed" in the app
|
||||||
|
And I press "Attempt 1" in the app
|
||||||
|
And I press "Review" in the app
|
||||||
|
Then I should find "Question 1" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press the back button in the app
|
||||||
|
And I press "Quiz never review" in the app
|
||||||
|
And I press "Attempt 1" in the app
|
||||||
|
Then I should find "You are not allowed to review this attempt" in the app
|
||||||
|
And I should not be able to press "Review" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Quiz review when closed" in the app
|
||||||
|
And I press "Attempt 1" in the app
|
||||||
|
Then I should find "Available 31/12/35, 23:59" in the app
|
||||||
|
And I should not be able to press "Review" in the app
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Binary file not shown.
Before Width: | Height: | Size: 36 KiB |
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
|
@ -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",
|
||||||
|
|
|
@ -19,3 +19,21 @@ export const QUESTION_NEEDS_GRADING_STATE_CLASSES = ['requiresgrading', 'complet
|
||||||
export const QUESTION_FINISHED_STATE_CLASSES = ['complete'] as const;
|
export const QUESTION_FINISHED_STATE_CLASSES = ['complete'] as const;
|
||||||
export const QUESTION_GAVE_UP_STATE_CLASSES = ['notanswered'] as const;
|
export const QUESTION_GAVE_UP_STATE_CLASSES = ['notanswered'] as const;
|
||||||
export const QUESTION_GRADED_STATE_CLASSES = ['complete', 'incorrect', 'partiallycorrect', 'correct'] as const;
|
export const QUESTION_GRADED_STATE_CLASSES = ['complete', 'incorrect', 'partiallycorrect', 'correct'] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible values to display marks in a question.
|
||||||
|
*/
|
||||||
|
export const enum QuestionDisplayOptionsMarks {
|
||||||
|
MAX_ONLY = 1,
|
||||||
|
MARK_AND_MAX = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible values that most of the display options take.
|
||||||
|
*/
|
||||||
|
export const enum QuestionDisplayOptionsValues {
|
||||||
|
SHOW_ALL = -1,
|
||||||
|
HIDDEN = 0,
|
||||||
|
VISIBLE = 1,
|
||||||
|
EDITABLE = 2,
|
||||||
|
}
|
||||||
|
|
|
@ -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.",
|
||||||
|
|
|
@ -56,6 +56,10 @@ export class TestingBehatDomUtilsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (element.slot === 'content' && element.parentElement?.tagName === 'ION-ACCORDION') {
|
||||||
|
return element.parentElement.classList.contains('accordion-expanded');
|
||||||
|
}
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue