commit
023db99e2b
|
@ -872,6 +872,8 @@
|
|||
"addon.mod_page.errorwhileloadingthepage": "local_moodlemobileapp",
|
||||
"addon.mod_page.modulenameplural": "page",
|
||||
"addon.mod_quiz.answercolon": "qtype_numerical",
|
||||
"addon.mod_quiz.attempt": "quiz",
|
||||
"addon.mod_quiz.attemptduration": "quiz",
|
||||
"addon.mod_quiz.attemptfirst": "quiz",
|
||||
"addon.mod_quiz.attemptlast": "quiz",
|
||||
"addon.mod_quiz.attemptnumber": "quiz",
|
||||
|
@ -901,7 +903,7 @@
|
|||
"addon.mod_quiz.errorsaveattempt": "local_moodlemobileapp",
|
||||
"addon.mod_quiz.feedback": "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.gradeaverage": "quiz",
|
||||
"addon.mod_quiz.gradehighest": "quiz",
|
||||
|
@ -912,6 +914,8 @@
|
|||
"addon.mod_quiz.mustbesubmittedby": "quiz",
|
||||
"addon.mod_quiz.noquestions": "quiz",
|
||||
"addon.mod_quiz.noreviewattempt": "quiz",
|
||||
"addon.mod_quiz.noreviewuntil": "quiz",
|
||||
"addon.mod_quiz.noreviewuntilshort": "quiz",
|
||||
"addon.mod_quiz.notyetgraded": "quiz",
|
||||
"addon.mod_quiz.opentoc": "local_moodlemobileapp",
|
||||
"addon.mod_quiz.outof": "quiz",
|
||||
|
@ -945,7 +949,6 @@
|
|||
"addon.mod_quiz.summaryofattempt": "quiz",
|
||||
"addon.mod_quiz.summaryofattempts": "quiz",
|
||||
"addon.mod_quiz.timeleft": "quiz",
|
||||
"addon.mod_quiz.timetaken": "quiz",
|
||||
"addon.mod_quiz.unit": "quiz",
|
||||
"addon.mod_quiz.warningattemptfinished": "local_moodlemobileapp",
|
||||
"addon.mod_quiz.warningdatadiscarded": "local_moodlemobileapp",
|
||||
|
@ -1864,6 +1867,7 @@
|
|||
"core.grades.grade": "grades",
|
||||
"core.grades.gradebook": "grades",
|
||||
"core.grades.gradeitem": "grades",
|
||||
"core.grades.gradelong": "grades",
|
||||
"core.grades.gradepass": "grades",
|
||||
"core.grades.grades": "grades",
|
||||
"core.grades.lettergrade": "grades",
|
||||
|
@ -2560,6 +2564,7 @@
|
|||
"core.strftimetime12": "langconfig",
|
||||
"core.strftimetime24": "langconfig",
|
||||
"core.submit": "moodle",
|
||||
"core.submittedoffline": "local_moodlemobileapp",
|
||||
"core.success": "moodle",
|
||||
"core.summary": "moodle",
|
||||
"core.swipenavigationtourdescription": "local_moodlemobileapp",
|
||||
|
|
|
@ -15,8 +15,9 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
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 { ADDON_MOD_QUIZ_SHOW_TIME_BEFORE_DEADLINE } from '@addons/mod/quiz/constants';
|
||||
|
||||
/**
|
||||
* Handler to support open/close date access rule.
|
||||
|
@ -50,8 +51,8 @@ export class AddonModQuizAccessOpenCloseDateHandlerService implements AddonModQu
|
|||
return false;
|
||||
}
|
||||
|
||||
// Show the time left only if it's less than QUIZ_SHOW_TIME_BEFORE_DEADLINE.
|
||||
if (timeNow > endTime - AddonModQuizProvider.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 - ADDON_MOD_QUIZ_SHOW_TIME_BEFORE_DEADLINE) {
|
||||
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 { AddonModQuizNavigationModalComponent } from './navigation-modal/navigation-modal';
|
||||
import { AddonModQuizPreflightModalComponent } from './preflight-modal/preflight-modal';
|
||||
import { AddonModQuizAttemptInfoComponent } from './attempt-info/attempt-info';
|
||||
import { AddonModQuizAttemptStateComponent } from './attempt-state/attempt-state';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizAttemptInfoComponent,
|
||||
AddonModQuizAttemptStateComponent,
|
||||
AddonModQuizIndexComponent,
|
||||
AddonModQuizConnectionErrorComponent,
|
||||
AddonModQuizNavigationModalComponent,
|
||||
|
@ -35,6 +39,8 @@ import { AddonModQuizPreflightModalComponent } from './preflight-modal/preflight
|
|||
providers: [
|
||||
],
|
||||
exports: [
|
||||
AddonModQuizAttemptInfoComponent,
|
||||
AddonModQuizAttemptStateComponent,
|
||||
AddonModQuizIndexComponent,
|
||||
AddonModQuizConnectionErrorComponent,
|
||||
AddonModQuizNavigationModalComponent,
|
||||
|
|
|
@ -45,91 +45,123 @@
|
|||
</ion-card>
|
||||
|
||||
<!-- 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-title>{{ 'addon.mod_quiz.summaryofattempts' | translate }}</ion-card-title>
|
||||
</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. -->
|
||||
<ion-card *ngIf="quiz && showResults &&
|
||||
(gradeResult || gradeOverridden || gradebookFeedback || (quiz.showFeedbackColumn && overallFeedback))">
|
||||
<ion-list>
|
||||
<ion-item class="ion-text-wrap" *ngIf="gradeResult">
|
||||
<ion-label>{{ gradeResult }}</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="gradeOverridden">
|
||||
<ion-label>{{ 'core.course.overriddennotice' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="gradebookFeedback">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.comment' | translate }}</p>
|
||||
<p>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="gradebookFeedback"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId" />
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="quiz.showFeedbackColumn && overallFeedback">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.overallfeedback' | translate }}</p>
|
||||
<p>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="overallFeedback" contextLevel="module"
|
||||
[contextInstanceId]="module.id" [courseId]="courseId" />
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<!-- Quiz result info. -->
|
||||
@if (quiz && showResults && (gradeResult || gradeOverridden || gradebookFeedback || (quiz.showFeedback && overallFeedback))) {
|
||||
|
||||
@if (overallStats && gradeResult) {
|
||||
<ion-item class="ion-text-wrap addon-mod_quiz-grade-result">
|
||||
<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>
|
||||
}
|
||||
|
||||
@if (gradebookFeedback) {
|
||||
<ion-item class="ion-text-wrap addon-mod_quiz-gradebook-feedback">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.comment' | translate }}</p>
|
||||
<p>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="gradebookFeedback" contextLevel="module"
|
||||
[contextInstanceId]="module.id" [courseId]="courseId" />
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
}
|
||||
|
||||
@if (quiz.showFeedback && overallFeedback) {
|
||||
<hr>
|
||||
<ion-item class="ion-text-wrap addon-mod_quiz-overall-feedback">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.overallfeedback' | translate }}</p>
|
||||
<p>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="overallFeedback" contextLevel="module"
|
||||
[contextInstanceId]="module.id" [courseId]="courseId" />
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
<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>
|
||||
}
|
||||
|
||||
<!-- More data. -->
|
||||
<ng-container *ngIf="quiz">
|
||||
|
|
|
@ -1,33 +1,75 @@
|
|||
@use "theme/globals" as *;
|
||||
|
||||
:host {
|
||||
|
||||
.addon-mod_quiz-table {
|
||||
ion-card-content {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
.addon-mod_quiz-attempts-summary {
|
||||
ion-card-header {
|
||||
border-bottom: 1px solid var(--stroke);
|
||||
}
|
||||
|
||||
.item:nth-child(even) {
|
||||
--background: var(--light);
|
||||
.addon-mod_quiz-grade-result {
|
||||
margin-top: var(--mdl-spacing-2);
|
||||
|
||||
|
||||
.addon-mod_quiz-grade-overridden-notice {
|
||||
margin-top: var(--mdl-spacing-2);
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.addon-mod_quiz-grade-result-grade {
|
||||
display: flex;
|
||||
|
||||
span:first-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.addon-mod_quiz-highlighted,
|
||||
.item.addon-mod_quiz-highlighted,
|
||||
.addon-mod_quiz-highlighted p,
|
||||
.item.addon-mod_quiz-highlighted p {
|
||||
--background: var(--primary-tint);
|
||||
color: var(--primary-shade);
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
: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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
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 { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
|
@ -36,7 +36,7 @@ import {
|
|||
AddonModQuizGetAttemptAccessInformationWSResponse,
|
||||
AddonModQuizGetQuizAccessInformationWSResponse,
|
||||
AddonModQuizGetUserBestGradeWSResponse,
|
||||
AddonModQuizProvider,
|
||||
AddonModQuizWSAdditionalData,
|
||||
} from '../../services/quiz';
|
||||
import { AddonModQuizAttempt, AddonModQuizHelper, AddonModQuizQuizData } from '../../services/quiz-helper';
|
||||
import {
|
||||
|
@ -45,6 +45,8 @@ import {
|
|||
AddonModQuizSyncProvider,
|
||||
AddonModQuizSyncResult,
|
||||
} 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.
|
||||
|
@ -56,7 +58,7 @@ import {
|
|||
})
|
||||
export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
|
||||
|
||||
component = AddonModQuizProvider.COMPONENT;
|
||||
component = ADDON_MOD_QUIZ_COMPONENT;
|
||||
pluginName = 'quiz';
|
||||
quiz?: AddonModQuizQuizData; // The quiz.
|
||||
now?: number; // Current time.
|
||||
|
@ -77,13 +79,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
showStatusSpinner = true; // Whether to show a spinner due to quiz status.
|
||||
gradeMethodReadable?: string; // Grade method in a readable format.
|
||||
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.
|
||||
|
||||
protected fetchContentDefaultError = 'addon.mod_quiz.errorgetquiz'; // Default error to show when loading contents.
|
||||
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 quizAccessInfo?: AddonModQuizGetQuizAccessInformationWSResponse; // Quiz access info.
|
||||
protected attemptAccessInfo?: AddonModQuizGetAttemptAccessInformationWSResponse; // Last attempt access info.
|
||||
|
@ -110,7 +111,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
|
||||
// Listen for attempt finished events.
|
||||
this.finishedObserver = CoreEvents.on(
|
||||
AddonModQuizProvider.ATTEMPT_FINISHED_EVENT,
|
||||
ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT,
|
||||
(data) => {
|
||||
// Go to review attempt if an attempt in this quiz was finished and synced.
|
||||
if (this.quiz && data.quizId == this.quiz.id) {
|
||||
|
@ -195,15 +196,9 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
if (AddonModQuiz.isQuizOffline(quiz)) {
|
||||
if (sync) {
|
||||
// Try to sync the quiz.
|
||||
try {
|
||||
await this.syncActivity(showErrors);
|
||||
} catch {
|
||||
// Ignore errors, keep getting data even if sync fails.
|
||||
this.autoReview = undefined;
|
||||
}
|
||||
await CoreUtils.ignoreErrors(this.syncActivity(showErrors));
|
||||
}
|
||||
} else {
|
||||
this.autoReview = undefined;
|
||||
this.showStatusSpinner = false;
|
||||
}
|
||||
|
||||
|
@ -233,7 +228,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
this.unsupportedQuestions = AddonModQuiz.getUnsupportedQuestions(types);
|
||||
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.
|
||||
this.quiz = quiz;
|
||||
|
@ -245,7 +240,10 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
* @param quiz Quiz instance.
|
||||
* @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.
|
||||
this.bestGrade = await AddonModQuiz.getUserBestGrade(quiz.id, { cmId: this.module.id });
|
||||
|
||||
|
@ -255,12 +253,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
// Get attempts.
|
||||
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.
|
||||
if (this.attempts.length) {
|
||||
const last = this.attempts[this.attempts.length - 1];
|
||||
this.moreAttempts = !AddonModQuiz.isAttemptFinished(last.state) || !this.attemptAccessInfo.isfinished;
|
||||
const last = this.attempts[0];
|
||||
this.moreAttempts = !AddonModQuiz.isAttemptCompleted(last.state) || !this.attemptAccessInfo.isfinished;
|
||||
} else {
|
||||
this.moreAttempts = !this.attemptAccessInfo.isfinished;
|
||||
}
|
||||
|
@ -279,7 +277,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
this.buttonText = '';
|
||||
|
||||
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.
|
||||
if (this.quizAccessInfo?.canattempt) {
|
||||
this.buttonText = 'addon.mod_quiz.continueattemptquiz';
|
||||
|
@ -327,7 +325,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
* @returns Promise resolved when done.
|
||||
*/
|
||||
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.showResults = false;
|
||||
|
||||
|
@ -350,25 +348,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
gradeToShow = formattedBestGrade;
|
||||
}
|
||||
|
||||
if (this.overallStats) {
|
||||
// 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,
|
||||
maxgrade: quiz.gradeFormatted,
|
||||
} });
|
||||
this.gradeResult = Translate.instant('core.grades.gradelong', { $a: {
|
||||
grade: gradeToShow,
|
||||
max: quiz.gradeFormatted,
|
||||
} });
|
||||
|
||||
this.gradeResult = Translate.instant('addon.mod_quiz.yourfinalgradeis', { $a: outOfShort });
|
||||
}
|
||||
}
|
||||
|
||||
if (quiz.showFeedbackColumn) {
|
||||
if (quiz.showFeedback) {
|
||||
// Get the quiz overall feedback.
|
||||
const response = await AddonModQuiz.getFeedbackForGrade(quiz.id, this.gradebookData.grade, {
|
||||
cmId: this.module.id,
|
||||
|
@ -396,7 +381,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
*
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected async goToAutoReview(): Promise<void> {
|
||||
protected async goToAutoReview(attempts: AddonModQuizAttemptWSData[]): Promise<void> {
|
||||
if (!this.autoReview) {
|
||||
return;
|
||||
}
|
||||
|
@ -405,20 +390,19 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
this.checkCompletion();
|
||||
|
||||
// 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;
|
||||
|
||||
if (this.quizAccessInfo?.canreviewmyattempts) {
|
||||
try {
|
||||
await AddonModQuiz.getAttemptReview(attemptId, { page: -1, cmId: this.module.id });
|
||||
|
||||
await CoreNavigator.navigateToSitePath(
|
||||
`${AddonModQuizModuleHandlerService.PAGE_NAME}/${this.courseId}/${this.module.id}/review/${attemptId}`,
|
||||
);
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
if (!this.quiz || !this.quizAccessInfo || !attempt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canReview = await AddonModQuizHelper.canReviewAttempt(this.quiz, this.quizAccessInfo, attempt);
|
||||
if (!canReview) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.reviewAttempt(attempt.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -447,22 +431,15 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
}
|
||||
|
||||
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.
|
||||
this.showLoading = true;
|
||||
this.content?.scrollToTop();
|
||||
|
||||
await promise;
|
||||
await CoreUtils.ignoreErrors(this.refreshContent(true));
|
||||
|
||||
this.showLoading = false;
|
||||
this.autoReview = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -571,13 +548,15 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
* Treat user attempts.
|
||||
*
|
||||
* @param quiz Quiz data.
|
||||
* @param accessInfo Quiz access information.
|
||||
* @param attempts The attempts to treat.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected async treatAttempts(
|
||||
quiz: AddonModQuizQuizData,
|
||||
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||
attempts: AddonModQuizAttemptWSData[],
|
||||
): Promise<AddonModQuizAttempt[]> {
|
||||
): Promise<QuizAttempt[]> {
|
||||
if (!attempts || !attempts.length) {
|
||||
// There are no attempts to treat.
|
||||
quiz.gradeFormatted = AddonModQuiz.formatGrade(quiz.grade, quiz.decimalpoints);
|
||||
|
@ -585,10 +564,10 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
return [];
|
||||
}
|
||||
|
||||
const lastFinished = AddonModQuiz.getLastFinishedAttemptFromList(attempts);
|
||||
const lastCompleted = AddonModQuiz.getLastCompletedAttemptFromList(attempts);
|
||||
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.
|
||||
// Go to the review of this attempt if the user hasn't left this view.
|
||||
if (!this.isDestroyed && this.isCurrentView) {
|
||||
|
@ -599,29 +578,50 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
const [options] = await Promise.all([
|
||||
AddonModQuiz.getCombinedReviewOptions(quiz.id, { cmId: this.module.id }),
|
||||
this.getQuizGrade(),
|
||||
openReview ? this.goToAutoReview() : undefined,
|
||||
openReview ? this.goToAutoReview(attempts) : undefined,
|
||||
]);
|
||||
|
||||
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);
|
||||
|
||||
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.
|
||||
const formattedAttempts = await Promise.all(attempts.map((attempt, index) => {
|
||||
// Highlight the highest grade if appropriate.
|
||||
const shouldHighlight = this.overallStats && quiz.grademethod == AddonModQuizProvider.GRADEHIGHEST &&
|
||||
attempts.length > 1;
|
||||
const isLast = index == attempts.length - 1;
|
||||
const formattedAttempts = await Promise.all(attempts.map(async (attempt) => {
|
||||
const [formattedAttempt, canReview] = await Promise.all([
|
||||
AddonModQuizHelper.setAttemptCalculatedData(quiz, attempt) as Promise<QuizAttempt>,
|
||||
AddonModQuizHelper.canReviewAttempt(quiz, accessInfo, attempt),
|
||||
]);
|
||||
|
||||
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.
|
||||
*/
|
||||
async viewAttempt(attemptId: number): Promise<void> {
|
||||
async reviewAttempt(attemptId: number): Promise<void> {
|
||||
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
|
||||
// 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_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:",
|
||||
"attempt": "Attempt {{$a}}",
|
||||
"attemptduration": "Duration",
|
||||
"attemptfirst": "First attempt",
|
||||
"attemptlast": "Last attempt",
|
||||
"attemptnumber": "Attempt",
|
||||
"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:",
|
||||
"cannotsubmitquizdueto": "This quiz attempt cannot be submitted for the following reasons:",
|
||||
"clearchoice": "Clear my choice",
|
||||
"comment": "Comment",
|
||||
"completedon": "Completed on",
|
||||
"completedon": "Completed",
|
||||
"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.",
|
||||
"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.",
|
||||
"feedback": "Feedback",
|
||||
"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",
|
||||
"gradeaverage": "Average grade",
|
||||
"gradehighest": "Highest grade",
|
||||
|
@ -40,6 +42,8 @@
|
|||
"mustbesubmittedby": "This attempt must be submitted by {{$a}}.",
|
||||
"noquestions": "No questions have been added yet",
|
||||
"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",
|
||||
"opentoc": "Open navigation popover",
|
||||
"outof": "{{$a.grade}} out of {{$a.maxgrade}}",
|
||||
|
@ -60,7 +64,7 @@
|
|||
"showall": "Show all questions on one page",
|
||||
"showeachpage": "Show one page at a time",
|
||||
"startattempt": "Start attempt",
|
||||
"startedon": "Started on",
|
||||
"startedon": "Started",
|
||||
"stateabandoned": "Never submitted",
|
||||
"statefinished": "Finished",
|
||||
"statefinisheddetails": "Submitted {{$a}}",
|
||||
|
@ -71,9 +75,8 @@
|
|||
"submission_confirmation_unanswered": "Questions without a response: {{$a}}",
|
||||
"submitallandfinish": "Submit all and finish",
|
||||
"summaryofattempt": "Summary of attempt",
|
||||
"summaryofattempts": "Summary of your previous attempts",
|
||||
"summaryofattempts": "Your attempts",
|
||||
"timeleft": "Time left",
|
||||
"timetaken": "Time taken",
|
||||
"unit": "Unit",
|
||||
"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.",
|
||||
|
|
|
@ -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);
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
core-timer {
|
||||
|
|
|
@ -38,10 +38,9 @@ import {
|
|||
AddonModQuizAttemptWSData,
|
||||
AddonModQuizGetAttemptAccessInformationWSResponse,
|
||||
AddonModQuizGetQuizAccessInformationWSResponse,
|
||||
AddonModQuizProvider,
|
||||
AddonModQuizQuizWSData,
|
||||
} from '../../services/quiz';
|
||||
import { AddonModQuizAttempt, AddonModQuizHelper } from '../../services/quiz-helper';
|
||||
import { AddonModQuizHelper } from '../../services/quiz-helper';
|
||||
import { AddonModQuizSync } from '../../services/quiz-sync';
|
||||
import { CanLeave } from '@guards/can-leave';
|
||||
import { CoreForms } from '@singletons/form';
|
||||
|
@ -50,6 +49,7 @@ import { CoreTime } from '@singletons/time';
|
|||
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
||||
import { CoreWSError } from '@classes/errors/wserror';
|
||||
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.
|
||||
|
@ -66,9 +66,9 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
|||
@ViewChild('quizForm') formElement?: ElementRef;
|
||||
|
||||
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.
|
||||
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.
|
||||
quizAborted = false; // Whether the quiz was aborted due to an error.
|
||||
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 quizAccessInfo?: AddonModQuizGetQuizAccessInformationWSResponse; // Quiz access information.
|
||||
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 quizDataLoaded = false; // Whether the quiz data has been loaded.
|
||||
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) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
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.
|
||||
return;
|
||||
} 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);
|
||||
|
||||
// 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.
|
||||
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.
|
||||
this.lastAttempt = await AddonModQuizHelper.setAttemptCalculatedData(
|
||||
this.quiz,
|
||||
attempts[attempts.length - 1],
|
||||
false,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
this.lastAttempt = attempts[attempts.length - 1];
|
||||
|
||||
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 {
|
||||
// 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');
|
||||
|
||||
const unansweredCount = this.summaryQuestions
|
||||
|
@ -444,7 +440,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
|||
await this.processAttempt(userFinish, timeUp);
|
||||
|
||||
// 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,
|
||||
attemptId: this.attempt.id,
|
||||
synced: !this.offline,
|
||||
|
@ -679,7 +675,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
|||
});
|
||||
|
||||
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.dueDateWarning = AddonModQuiz.getAttemptDueDateWarning(this.quiz, this.attempt);
|
||||
|
@ -888,10 +884,12 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
|||
this.quiz,
|
||||
this.quizAccessInfo,
|
||||
this.preflightData,
|
||||
attempt,
|
||||
this.offline,
|
||||
false,
|
||||
'addon.mod_quiz.startattempt',
|
||||
{
|
||||
attempt,
|
||||
offline: this.offline,
|
||||
finishedOffline: attempt?.finishedOffline,
|
||||
title: 'addon.mod_quiz.startattempt',
|
||||
},
|
||||
);
|
||||
|
||||
// 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();
|
||||
|
||||
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.
|
||||
await this.loadPage(this.attempt.currentpage ?? 0);
|
||||
|
||||
|
@ -944,3 +942,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
|
|||
type QuizQuestion = CoreQuestionQuestionParsed & {
|
||||
readableMark?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt with some calculated data for the view.
|
||||
*/
|
||||
type QuizAttempt = AddonModQuizAttemptWSData & {
|
||||
finishedOffline?: boolean;
|
||||
};
|
||||
|
|
|
@ -24,61 +24,7 @@
|
|||
<!-- Review summary -->
|
||||
<ion-card *ngIf="attempt">
|
||||
<ion-list>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<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>
|
||||
<addon-mod-quiz-attempt-info [quiz]="quiz" [attempt]="attempt" [additionalData]="additionalData" />
|
||||
</ion-list>
|
||||
</ion-card>
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import { IonContent } from '@ionic/angular';
|
|||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreDom } from '@singletons/dom';
|
||||
import { CoreTime } from '@singletons/time';
|
||||
import {
|
||||
|
@ -31,13 +30,12 @@ import {
|
|||
AddonModQuiz,
|
||||
AddonModQuizAttemptWSData,
|
||||
AddonModQuizCombinedReviewOptions,
|
||||
AddonModQuizGetAttemptReviewResponse,
|
||||
AddonModQuizProvider,
|
||||
AddonModQuizQuizWSData,
|
||||
AddonModQuizWSAdditionalData,
|
||||
} from '../../services/quiz';
|
||||
import { AddonModQuizHelper } from '../../services/quiz-helper';
|
||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||
import { ADDON_MOD_QUIZ_COMPONENT } from '../../constants';
|
||||
|
||||
/**
|
||||
* Page that allows reviewing a quiz attempt.
|
||||
|
@ -52,7 +50,7 @@ export class AddonModQuizReviewPage implements OnInit {
|
|||
@ViewChild(IonContent) content?: IonContent;
|
||||
|
||||
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.
|
||||
numPages = 1; // Number of pages.
|
||||
showCompleted = false; // Whether to show completed time.
|
||||
|
@ -62,7 +60,6 @@ export class AddonModQuizReviewPage implements OnInit {
|
|||
questions: QuizQuestion[] = []; // Questions of the current page.
|
||||
nextPage = -2; // Next page.
|
||||
previousPage = -2; // Previous page.
|
||||
readableState?: string;
|
||||
readableGrade?: string;
|
||||
readableMark?: string;
|
||||
timeTaken?: string;
|
||||
|
@ -158,6 +155,8 @@ export class AddonModQuizReviewPage implements OnInit {
|
|||
|
||||
this.options = await AddonModQuiz.getCombinedReviewOptions(this.quiz.id, { cmId: this.cmId });
|
||||
|
||||
AddonModQuizHelper.setQuizCalculatedData(this.quiz, this.options);
|
||||
|
||||
// Load the navigation data.
|
||||
await this.loadNavigation();
|
||||
|
||||
|
@ -177,15 +176,17 @@ export class AddonModQuizReviewPage implements OnInit {
|
|||
* @returns Promise resolved when done.
|
||||
*/
|
||||
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.additionalData = data.additionaldata;
|
||||
this.currentPage = page;
|
||||
|
||||
// Set the summary data.
|
||||
this.setSummaryCalculatedData(data);
|
||||
|
||||
this.questions = data.questions;
|
||||
this.nextPage = 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.
|
||||
*/
|
||||
|
|
|
@ -19,7 +19,6 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
import { AddonModQuizComponentsModule } from './components/components.module';
|
||||
|
||||
import { AddonModQuizIndexPage } from './pages/index';
|
||||
import { AddonModQuizAttemptPage } from '@addons/mod/quiz/pages/attempt/attempt';
|
||||
import { CoreQuestionComponentsModule } from '@features/question/components/components.module';
|
||||
import { AddonModQuizPlayerPage } from '@addons/mod/quiz/pages/player/player';
|
||||
import { canLeaveGuard } from '@guards/can-leave';
|
||||
|
@ -35,10 +34,6 @@ const routes: Routes = [
|
|||
component: AddonModQuizPlayerPage,
|
||||
canDeactivate: [canLeaveGuard],
|
||||
},
|
||||
{
|
||||
path: ':courseId/:cmId/attempt/:attemptId',
|
||||
component: AddonModQuizAttemptPage,
|
||||
},
|
||||
{
|
||||
path: ':courseId/:cmId/review/:attemptId',
|
||||
component: AddonModQuizReviewPage,
|
||||
|
@ -54,7 +49,6 @@ const routes: Routes = [
|
|||
],
|
||||
declarations: [
|
||||
AddonModQuizIndexPage,
|
||||
AddonModQuizAttemptPage,
|
||||
AddonModQuizPlayerPage,
|
||||
AddonModQuizReviewPage,
|
||||
],
|
||||
|
|
|
@ -33,7 +33,7 @@ import { AddonModQuizPrefetchHandler } from './services/handlers/prefetch';
|
|||
import { AddonModQuizPushClickHandler } from './services/handlers/push-click';
|
||||
import { AddonModQuizReviewLinkHandler } from './services/handlers/review-link';
|
||||
import { AddonModQuizSyncCronHandler } from './services/handlers/sync-cron';
|
||||
import { AddonModQuizProvider } from './services/quiz';
|
||||
import { ADDON_MOD_QUIZ_COMPONENT } from './constants';
|
||||
|
||||
/**
|
||||
* Get mod Quiz services.
|
||||
|
@ -98,7 +98,7 @@ const routes: Routes = [
|
|||
CorePushNotificationsDelegate.registerClickHandler(AddonModQuizPushClickHandler.instance);
|
||||
CoreCronDelegate.register(AddonModQuizSyncCronHandler.instance);
|
||||
|
||||
CoreCourseHelper.registerModuleReminderClick(AddonModQuizProvider.COMPONENT);
|
||||
CoreCourseHelper.registerModuleReminderClick(ADDON_MOD_QUIZ_COMPONENT);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -32,11 +32,11 @@ import {
|
|||
AddonModQuiz,
|
||||
AddonModQuizAttemptWSData,
|
||||
AddonModQuizGetQuizAccessInformationWSResponse,
|
||||
AddonModQuizProvider,
|
||||
AddonModQuizQuizWSData,
|
||||
} from '../quiz';
|
||||
import { AddonModQuizHelper } from '../quiz-helper';
|
||||
import { AddonModQuizSync, AddonModQuizSyncResult } from '../quiz-sync';
|
||||
import { AddonModQuizAttemptStates, ADDON_MOD_QUIZ_COMPONENT } from '../../constants';
|
||||
|
||||
/**
|
||||
* Handler to prefetch quizzes.
|
||||
|
@ -46,7 +46,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
|
||||
name = 'AddonModQuiz';
|
||||
modName = 'quiz';
|
||||
component = AddonModQuizProvider.COMPONENT;
|
||||
component = ADDON_MOD_QUIZ_COMPONENT;
|
||||
updatesNames = /^configuration$|^.*files$|^grades$|^gradeitems$|^questions$|^attempts$/;
|
||||
|
||||
/**
|
||||
|
@ -115,8 +115,8 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
let files: CoreWSFile[] = [];
|
||||
|
||||
await Promise.all(attempts.map(async (attempt) => {
|
||||
if (!AddonModQuiz.isAttemptFinished(attempt.state)) {
|
||||
// Attempt not finished, no feedback files.
|
||||
if (!AddonModQuiz.isAttemptCompleted(attempt.state)) {
|
||||
// Attempt not completed, no feedback files.
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -167,11 +167,12 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
quiz,
|
||||
accessInfo,
|
||||
preflightData,
|
||||
attempt,
|
||||
false,
|
||||
true,
|
||||
title,
|
||||
siteId,
|
||||
{
|
||||
attempt,
|
||||
prefetch: true,
|
||||
title,
|
||||
siteId,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// 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,
|
||||
});
|
||||
|
||||
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.getAttemptAccessInformation(quiz.id, 0, 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.
|
||||
|
@ -330,7 +331,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
let startAttempt = false;
|
||||
|
||||
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.
|
||||
if (attemptAccessInfo.preventnewattemptreasons.length) {
|
||||
throw new CoreError(CoreTextUtils.buildMessage(attemptAccessInfo.preventnewattemptreasons));
|
||||
|
@ -353,17 +354,17 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
|
||||
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.
|
||||
promises.push(CoreUtils.ignoreErrors(
|
||||
CoreFilepool.updatePackageDownloadTime(siteId, AddonModQuizProvider.COMPONENT, module.id),
|
||||
CoreFilepool.updatePackageDownloadTime(siteId, ADDON_MOD_QUIZ_COMPONENT, module.id),
|
||||
));
|
||||
} else {
|
||||
// Use the already fetched attempts.
|
||||
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.
|
||||
|
@ -379,7 +380,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
|
||||
// We have quiz data, now we'll get specific data for each 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) {
|
||||
|
@ -399,6 +400,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
* Prefetch all WS data for an attempt.
|
||||
*
|
||||
* @param quiz Quiz.
|
||||
* @param accessInfo Quiz access info.
|
||||
* @param attempt Attempt.
|
||||
* @param preflightData Preflight required data (like password).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
|
@ -406,11 +408,11 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
*/
|
||||
async prefetchAttempt(
|
||||
quiz: AddonModQuizQuizWSData,
|
||||
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||
attempt: AddonModQuizAttemptWSData,
|
||||
preflightData: Record<string, string>,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
const pages = AddonModQuiz.getPagesFromLayout(attempt.layout);
|
||||
const isSequential = AddonModQuiz.isNavigationSequential(quiz);
|
||||
let promises: Promise<unknown>[] = [];
|
||||
|
||||
|
@ -420,7 +422,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
siteId,
|
||||
};
|
||||
|
||||
if (AddonModQuiz.isAttemptFinished(attempt.state)) {
|
||||
if (AddonModQuiz.isAttemptCompleted(attempt.state)) {
|
||||
// Attempt is finished, get feedback and review data.
|
||||
const attemptGrade = AddonModQuiz.rescaleGrade(attempt.sumgrades, quiz, false);
|
||||
const attemptGradeNumber = attemptGrade !== undefined && Number(attemptGrade);
|
||||
|
@ -428,24 +430,17 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
promises.push(AddonModQuiz.getFeedbackForGrade(quiz.id, attemptGradeNumber, modOptions));
|
||||
}
|
||||
|
||||
// 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, siteId));
|
||||
promises.push(this.prefetchAttemptReview(quiz, accessInfo, attempt, modOptions));
|
||||
} else {
|
||||
|
||||
// Attempt not finished, get data needed to continue the attempt.
|
||||
promises.push(AddonModQuiz.getAttemptAccessInformation(quiz.id, attempt.id, 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.
|
||||
const pages = AddonModQuiz.getPagesFromLayout(attempt.layout);
|
||||
|
||||
promises = promises.concat(pages.map(async (page) => {
|
||||
if (isSequential && typeof attempt.currentpage === 'number' && page < attempt.currentpage) {
|
||||
// Sequential quiz, cannot get pages before the current one.
|
||||
|
@ -472,20 +467,57 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
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.
|
||||
*
|
||||
* @param quiz Quiz.
|
||||
* @param attempt Attempt.
|
||||
* @param modOptions Other options.
|
||||
* @param siteId Site ID.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected async prefetchAttemptReviewFiles(
|
||||
quiz: AddonModQuizQuizWSData,
|
||||
attempt: AddonModQuizAttemptWSData,
|
||||
modOptions: CoreCourseCommonModWSOptions,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
// Get the review for all questions in same page.
|
||||
const data = await CoreUtils.ignoreErrors(AddonModQuiz.getAttemptReview(attempt.id, {
|
||||
|
@ -502,7 +534,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
question,
|
||||
this.component,
|
||||
quiz.coursemodule,
|
||||
siteId,
|
||||
modOptions.siteId,
|
||||
attempt.uniqueid,
|
||||
);
|
||||
}));
|
||||
|
@ -568,7 +600,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
preflightData = await this.getPreflightData(quiz, quizAccessInfo, lastAttempt, askPreflight, 'core.download', siteId);
|
||||
|
||||
// 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.
|
||||
|
@ -611,8 +643,8 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
|
|||
// 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.
|
||||
const lastAttempt = attempts[attempts.length - 1];
|
||||
const isLastFinished = !lastAttempt || AddonModQuiz.isAttemptFinished(lastAttempt.state);
|
||||
const newStatus = isLastFinished ? DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED : DownloadStatus.DOWNLOADED;
|
||||
const isLastCompleted = !lastAttempt || AddonModQuiz.isAttemptCompleted(lastAttempt.state);
|
||||
const newStatus = isLastCompleted ? DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED : DownloadStatus.DOWNLOADED;
|
||||
|
||||
await CoreFilepool.storePackageStatus(options.siteId, newStatus, this.component, quiz.coursemodule);
|
||||
}
|
||||
|
|
|
@ -30,10 +30,17 @@ import {
|
|||
AddonModQuizAttemptWSData,
|
||||
AddonModQuizCombinedReviewOptions,
|
||||
AddonModQuizGetQuizAccessInformationWSResponse,
|
||||
AddonModQuizProvider,
|
||||
AddonModQuizQuizWSData,
|
||||
} from './quiz';
|
||||
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.
|
||||
|
@ -41,6 +48,125 @@ import { AddonModQuizOffline } from './quiz-offline';
|
|||
@Injectable({ providedIn: 'root' })
|
||||
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.
|
||||
* It calls AddonModQuizProvider.startAttempt if a new attempt is needed.
|
||||
|
@ -48,24 +174,14 @@ export class AddonModQuizHelperProvider {
|
|||
* @param quiz Quiz.
|
||||
* @param accessInfo Quiz access info.
|
||||
* @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 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.
|
||||
* @param options Options.
|
||||
* @returns Promise resolved when the preflight data is validated. The resolve param is the attempt.
|
||||
*/
|
||||
async getAndCheckPreflightData(
|
||||
quiz: AddonModQuizQuizWSData,
|
||||
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||
preflightData: Record<string, string>,
|
||||
attempt?: AddonModQuizAttemptWSData,
|
||||
offline?: boolean,
|
||||
prefetch?: boolean,
|
||||
title?: string,
|
||||
siteId?: string,
|
||||
retrying?: boolean,
|
||||
options: GetAndCheckPreflightOptions = {},
|
||||
): Promise<AddonModQuizAttemptWSData> {
|
||||
|
||||
const rules = accessInfo?.activerulenames;
|
||||
|
@ -74,30 +190,37 @@ export class AddonModQuizHelperProvider {
|
|||
const preflightCheckRequired = await AddonModQuizAccessRuleDelegate.isPreflightCheckRequired(
|
||||
rules,
|
||||
quiz,
|
||||
attempt,
|
||||
prefetch,
|
||||
siteId,
|
||||
options.attempt,
|
||||
options.prefetch,
|
||||
options.siteId,
|
||||
);
|
||||
|
||||
if (preflightCheckRequired) {
|
||||
// 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.
|
||||
Object.assign(preflightData, data);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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) {
|
||||
|
||||
if (prefetch) {
|
||||
if (options.prefetch) {
|
||||
throw error;
|
||||
} else if (retrying && !preflightCheckRequired) {
|
||||
} else if (options.retrying && !preflightCheckRequired) {
|
||||
// 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.
|
||||
// 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);
|
||||
}, 100);
|
||||
|
||||
return this.getAndCheckPreflightData(
|
||||
quiz,
|
||||
accessInfo,
|
||||
preflightData,
|
||||
attempt,
|
||||
offline,
|
||||
prefetch,
|
||||
title,
|
||||
siteId,
|
||||
true,
|
||||
);
|
||||
return this.getAndCheckPreflightData(quiz, accessInfo, preflightData, {
|
||||
...options,
|
||||
retrying: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,19 +245,13 @@ export class AddonModQuizHelperProvider {
|
|||
*
|
||||
* @param quiz Quiz.
|
||||
* @param accessInfo Quiz access info.
|
||||
* @param attempt The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @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.
|
||||
* @param options Options.
|
||||
* @returns Promise resolved with the preflight data. Rejected if user cancels.
|
||||
*/
|
||||
async getPreflightData(
|
||||
quiz: AddonModQuizQuizWSData,
|
||||
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||
attempt?: AddonModQuizAttemptWSData,
|
||||
prefetch?: boolean,
|
||||
title?: string,
|
||||
siteId?: string,
|
||||
options: GetPreflightOptions = {},
|
||||
): Promise<Record<string, string>> {
|
||||
const notSupported: string[] = [];
|
||||
const rules = accessInfo?.activerulenames;
|
||||
|
@ -163,11 +273,11 @@ export class AddonModQuizHelperProvider {
|
|||
const modalData = await CoreDomUtils.openModal<Record<string, string>>({
|
||||
component: AddonModQuizPreflightModalComponent,
|
||||
componentProps: {
|
||||
title: title,
|
||||
title: options.title,
|
||||
quiz,
|
||||
attempt,
|
||||
prefetch: !!prefetch,
|
||||
siteId: siteId,
|
||||
attempt: options.attempt,
|
||||
prefetch: !!options.prefetch,
|
||||
siteId: options.siteId,
|
||||
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.
|
||||
*
|
||||
* @param quiz Quiz.
|
||||
* @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.
|
||||
* @returns Quiz attemptw with calculated data.
|
||||
* @returns Quiz attempt with calculated data.
|
||||
*/
|
||||
async setAttemptCalculatedData(
|
||||
quiz: AddonModQuizQuizData,
|
||||
attempt: AddonModQuizAttemptWSData,
|
||||
highlight?: boolean,
|
||||
bestGrade?: string,
|
||||
isLastAttempt?: boolean,
|
||||
siteId?: string,
|
||||
): Promise<AddonModQuizAttempt> {
|
||||
const formattedAttempt = <AddonModQuizAttempt> attempt;
|
||||
|
||||
formattedAttempt.rescaledGrade = AddonModQuiz.rescaleGrade(attempt.sumgrades, quiz, false);
|
||||
formattedAttempt.finished = AddonModQuiz.isAttemptFinished(attempt.state);
|
||||
formattedAttempt.readableState = AddonModQuiz.getAttemptReadableState(quiz, attempt);
|
||||
formattedAttempt.finished = attempt.state === AddonModQuizAttemptStates.FINISHED;
|
||||
formattedAttempt.completed = AddonModQuiz.isAttemptCompleted(attempt.state);
|
||||
formattedAttempt.rescaledGrade = Number(AddonModQuiz.rescaleGrade(attempt.sumgrades, quiz, false));
|
||||
|
||||
if (quiz.showMarkColumn && formattedAttempt.finished) {
|
||||
formattedAttempt.readableMark = AddonModQuiz.formatGrade(attempt.sumgrades, quiz.decimalpoints);
|
||||
if (quiz.showAttemptsGrades && formattedAttempt.finished) {
|
||||
formattedAttempt.formattedGrade = AddonModQuiz.formatGrade(formattedAttempt.rescaledGrade, quiz.decimalpoints);
|
||||
} 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;
|
||||
}
|
||||
|
@ -316,11 +428,10 @@ export class AddonModQuizHelperProvider {
|
|||
formattedQuiz.sumGradesFormatted = AddonModQuiz.formatGrade(quiz.sumgrades, quiz.decimalpoints);
|
||||
formattedQuiz.gradeFormatted = AddonModQuiz.formatGrade(quiz.grade, quiz.decimalpoints);
|
||||
|
||||
formattedQuiz.showAttemptColumn = quiz.attempts != 1;
|
||||
formattedQuiz.showGradeColumn = options.someoptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX &&
|
||||
formattedQuiz.showAttemptsGrades = options.someoptions.marks >= QuestionDisplayOptionsMarks.MARK_AND_MAX &&
|
||||
AddonModQuiz.quizHasGrades(quiz);
|
||||
formattedQuiz.showMarkColumn = formattedQuiz.showGradeColumn && quiz.grade != quiz.sumgrades;
|
||||
formattedQuiz.showFeedbackColumn = !!quiz.hasfeedback && !!options.alloptions.overallfeedback;
|
||||
formattedQuiz.showAttemptsMarks = formattedQuiz.showAttemptsGrades && quiz.grade !== quiz.sumgrades;
|
||||
formattedQuiz.showFeedback = !!quiz.hasfeedback && !!options.alloptions.overallfeedback;
|
||||
|
||||
return formattedQuiz;
|
||||
}
|
||||
|
@ -331,36 +442,32 @@ export class AddonModQuizHelperProvider {
|
|||
* @param quiz Quiz.
|
||||
* @param accessInfo Quiz access info.
|
||||
* @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 offline Whether the attempt is offline.
|
||||
* @param prefetch Whether user is prefetching.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options Options
|
||||
* @returns Promise resolved when the preflight data is validated.
|
||||
*/
|
||||
async validatePreflightData(
|
||||
quiz: AddonModQuizQuizWSData,
|
||||
accessInfo: AddonModQuizGetQuizAccessInformationWSResponse,
|
||||
preflightData: Record<string, string>,
|
||||
attempt?: AddonModQuizAttempt,
|
||||
offline?: boolean,
|
||||
prefetch?: boolean,
|
||||
siteId?: string,
|
||||
options: ValidatePreflightOptions = {},
|
||||
): Promise<AddonModQuizAttempt> {
|
||||
|
||||
const rules = accessInfo.activerulenames;
|
||||
const modOptions = {
|
||||
cmId: quiz.coursemodule,
|
||||
readingStrategy: offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
|
||||
siteId,
|
||||
readingStrategy: options.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
|
||||
siteId: options.siteId,
|
||||
};
|
||||
let attempt = options.attempt;
|
||||
|
||||
try {
|
||||
|
||||
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.
|
||||
await AddonModQuiz.getAttemptData(attempt.id, attempt.currentpage ?? 0, preflightData, modOptions);
|
||||
|
||||
if (offline) {
|
||||
if (options.offline) {
|
||||
// Get current page stored in local.
|
||||
const storedAttempt = await CoreUtils.ignoreErrors(
|
||||
AddonModQuizOffline.getAttemptById(attempt.id),
|
||||
|
@ -375,7 +482,7 @@ export class AddonModQuizHelperProvider {
|
|||
}
|
||||
} else {
|
||||
// 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.
|
||||
|
@ -384,8 +491,8 @@ export class AddonModQuizHelperProvider {
|
|||
quiz,
|
||||
attempt,
|
||||
preflightData,
|
||||
prefetch,
|
||||
siteId,
|
||||
options.prefetch,
|
||||
options.siteId,
|
||||
);
|
||||
|
||||
return attempt;
|
||||
|
@ -397,8 +504,8 @@ export class AddonModQuizHelperProvider {
|
|||
quiz,
|
||||
attempt,
|
||||
preflightData,
|
||||
prefetch,
|
||||
siteId,
|
||||
options.prefetch,
|
||||
options.siteId,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -416,10 +523,9 @@ export const AddonModQuizHelper = makeSingleton(AddonModQuizHelperProvider);
|
|||
export type AddonModQuizQuizData = AddonModQuizQuizWSData & {
|
||||
sumGradesFormatted?: string;
|
||||
gradeFormatted?: string;
|
||||
showAttemptColumn?: boolean;
|
||||
showGradeColumn?: boolean;
|
||||
showMarkColumn?: boolean;
|
||||
showFeedbackColumn?: boolean;
|
||||
showAttemptsGrades?: boolean;
|
||||
showAttemptsMarks?: boolean;
|
||||
showFeedback?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -427,10 +533,32 @@ export type AddonModQuizQuizData = AddonModQuizQuizWSData & {
|
|||
*/
|
||||
export type AddonModQuizAttempt = AddonModQuizAttemptWSData & {
|
||||
finishedOffline?: boolean;
|
||||
rescaledGrade?: string;
|
||||
rescaledGrade?: number;
|
||||
finished?: boolean;
|
||||
readableState?: string[];
|
||||
readableMark?: string;
|
||||
readableGrade?: string;
|
||||
highlightGrade?: boolean;
|
||||
completed?: boolean;
|
||||
formattedGrade?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 { CoreLogger } from '@singletons/logger';
|
||||
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.
|
||||
|
@ -103,7 +104,7 @@ export class AddonModQuizOfflineProvider {
|
|||
* @returns Promise resolved with the answers.
|
||||
*/
|
||||
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) => {
|
||||
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) {
|
||||
|
@ -230,8 +231,8 @@ export class AddonModQuizOfflineProvider {
|
|||
const db = await CoreSites.getSiteDb(siteId);
|
||||
|
||||
await Promise.all([
|
||||
CoreQuestion.removeAttemptAnswers(AddonModQuizProvider.COMPONENT, attemptId, siteId),
|
||||
CoreQuestion.removeAttemptQuestions(AddonModQuizProvider.COMPONENT, attemptId, siteId),
|
||||
CoreQuestion.removeAttemptAnswers(ADDON_MOD_QUIZ_COMPONENT, attemptId, siteId),
|
||||
CoreQuestion.removeAttemptQuestions(ADDON_MOD_QUIZ_COMPONENT, attemptId, siteId),
|
||||
db.deleteRecords(ATTEMPTS_TABLE_NAME, { id: attemptId }),
|
||||
]);
|
||||
}
|
||||
|
@ -248,8 +249,8 @@ export class AddonModQuizOfflineProvider {
|
|||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
await Promise.all([
|
||||
CoreQuestion.removeQuestion(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId),
|
||||
CoreQuestion.removeQuestionAnswers(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId),
|
||||
CoreQuestion.removeQuestion(ADDON_MOD_QUIZ_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(
|
||||
quiz.preferredbehaviour ?? '',
|
||||
AddonModQuizProvider.COMPONENT,
|
||||
ADDON_MOD_QUIZ_COMPONENT,
|
||||
attempt.id,
|
||||
question,
|
||||
quiz.coursemodule,
|
||||
|
@ -312,12 +313,12 @@ export class AddonModQuizOfflineProvider {
|
|||
}
|
||||
|
||||
// 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.
|
||||
await CoreQuestion.saveAnswers(
|
||||
AddonModQuizProvider.COMPONENT,
|
||||
ADDON_MOD_QUIZ_COMPONENT,
|
||||
quiz.id,
|
||||
attempt.id,
|
||||
attempt.userid ?? CoreSites.getCurrentSiteUserId(),
|
||||
|
@ -332,7 +333,7 @@ export class AddonModQuizOfflineProvider {
|
|||
const question = questionsWithAnswers[Number(slot)];
|
||||
|
||||
await CoreQuestion.saveQuestion(
|
||||
AddonModQuizProvider.COMPONENT,
|
||||
ADDON_MOD_QUIZ_COMPONENT,
|
||||
quiz.id,
|
||||
attempt.id,
|
||||
attempt.userid ?? CoreSites.getCurrentSiteUserId(),
|
||||
|
|
|
@ -29,8 +29,9 @@ import { makeSingleton, Translate } from '@singletons';
|
|||
import { CoreEvents } from '@singletons/events';
|
||||
import { AddonModQuizAttemptDBRecord } from './database/quiz';
|
||||
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 { ADDON_MOD_QUIZ_COMPONENT } from '../constants';
|
||||
|
||||
/**
|
||||
* Service to sync quizzes.
|
||||
|
@ -79,7 +80,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
|||
for (const slot in options.onlineQuestions) {
|
||||
promises.push(CoreQuestionDelegate.deleteOfflineData(
|
||||
options.onlineQuestions[slot],
|
||||
AddonModQuizProvider.COMPONENT,
|
||||
ADDON_MOD_QUIZ_COMPONENT,
|
||||
quiz.coursemodule,
|
||||
siteId,
|
||||
));
|
||||
|
@ -104,13 +105,13 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
|||
|
||||
// Check if online attempt was finished because of the sync.
|
||||
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.
|
||||
const attempts = await AddonModQuiz.getUserAttempts(quiz.id, { cmId: quiz.coursemodule, siteId });
|
||||
|
||||
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 };
|
||||
|
@ -204,7 +205,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
|||
}
|
||||
quizIds[attempt.quizid] = true;
|
||||
|
||||
if (CoreSync.isBlocked(AddonModQuizProvider.COMPONENT, attempt.quizid, siteId)) {
|
||||
if (CoreSync.isBlocked(ADDON_MOD_QUIZ_COMPONENT, attempt.quizid, siteId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -268,7 +269,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
|||
}
|
||||
|
||||
// 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.');
|
||||
|
||||
throw new CoreError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
|
||||
|
@ -300,7 +301,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
|||
|
||||
// Sync offline logs.
|
||||
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
|
||||
|
@ -323,7 +324,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
|||
const lastAttemptId = onlineAttempts.length ? onlineAttempts[onlineAttempts.length - 1].id : undefined;
|
||||
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.
|
||||
warnings.push(Translate.instant('addon.mod_quiz.warningattemptfinished'));
|
||||
|
||||
|
@ -381,7 +382,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
|
|||
await CoreQuestionDelegate.prepareSyncData(
|
||||
onlineQuestion,
|
||||
offlineQuestions[slot].answers,
|
||||
AddonModQuizProvider.COMPONENT,
|
||||
ADDON_MOD_QUIZ_COMPONENT,
|
||||
quiz.coursemodule,
|
||||
siteId,
|
||||
);
|
||||
|
|
|
@ -37,13 +37,24 @@ import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWar
|
|||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
|
||||
import { AddonModQuizAttempt } from './quiz-helper';
|
||||
import { AddonModQuizOffline, AddonModQuizQuestionsWithAnswers } from './quiz-offline';
|
||||
import { AddonModQuizAutoSyncData, AddonModQuizSyncProvider } from './quiz-sync';
|
||||
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
||||
import { QUESTION_INVALID_STATE_CLASSES, QUESTION_TODO_STATE_CLASSES } from '@features/question/constants';
|
||||
|
||||
const ROOT_CACHE_KEY = 'mmaModQuiz:';
|
||||
import {
|
||||
QUESTION_INVALID_STATE_CLASSES,
|
||||
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' {
|
||||
|
||||
|
@ -53,7 +64,7 @@ declare module '@singletons/events' {
|
|||
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||
*/
|
||||
export interface CoreEventsData {
|
||||
[AddonModQuizProvider.ATTEMPT_FINISHED_EVENT]: AddonModQuizAttemptFinishedData;
|
||||
[ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT]: AddonModQuizAttemptFinishedData;
|
||||
[AddonModQuizSyncProvider.AUTO_SYNCED]: AddonModQuizAutoSyncData;
|
||||
}
|
||||
|
||||
|
@ -65,27 +76,7 @@ declare module '@singletons/events' {
|
|||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModQuizProvider {
|
||||
|
||||
static readonly COMPONENT = '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 static readonly ROOT_CACHE_KEY = 'mmaModQuiz:';
|
||||
|
||||
protected logger: CoreLogger;
|
||||
|
||||
|
@ -164,7 +155,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Cache key.
|
||||
*/
|
||||
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 = {
|
||||
cacheKey: this.getAttemptAccessInformationCacheKey(quizId, attemptId),
|
||||
component: AddonModQuizProvider.COMPONENT,
|
||||
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
@ -215,7 +206,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Cache key.
|
||||
*/
|
||||
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 = {
|
||||
cacheKey: this.getAttemptDataCacheKey(attemptId, page),
|
||||
component: AddonModQuizProvider.COMPONENT,
|
||||
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
@ -288,10 +279,10 @@ export class AddonModQuizProvider {
|
|||
}
|
||||
|
||||
switch (attempt.state) {
|
||||
case AddonModQuizProvider.ATTEMPT_IN_PROGRESS:
|
||||
case AddonModQuizAttemptStates.IN_PROGRESS:
|
||||
return dueDate * 1000;
|
||||
|
||||
case AddonModQuizProvider.ATTEMPT_OVERDUE:
|
||||
case AddonModQuizAttemptStates.OVERDUE:
|
||||
return (dueDate + (quiz.graceperiod ?? 0)) * 1000;
|
||||
|
||||
default:
|
||||
|
@ -311,7 +302,7 @@ export class AddonModQuizProvider {
|
|||
getAttemptDueDateWarning(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttemptWSData): string | undefined {
|
||||
const dueDate = this.getAttemptDueDate(quiz, attempt);
|
||||
|
||||
if (attempt.state === AddonModQuizProvider.ATTEMPT_OVERDUE) {
|
||||
if (attempt.state === AddonModQuizAttemptStates.OVERDUE) {
|
||||
return Translate.instant(
|
||||
'addon.mod_quiz.overduemustbesubmittedby',
|
||||
{ $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 attempt Attempt.
|
||||
* @returns List of state sentences.
|
||||
* @returns Display option value.
|
||||
*/
|
||||
getAttemptReadableState(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttempt): string[] {
|
||||
if (attempt.finishedOffline) {
|
||||
return [Translate.instant('addon.mod_quiz.finishnotsynced')];
|
||||
getAttemptStateDisplayOption(
|
||||
quiz: AddonModQuizQuizWSData,
|
||||
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) {
|
||||
case AddonModQuizProvider.ATTEMPT_IN_PROGRESS:
|
||||
return [Translate.instant('addon.mod_quiz.stateinprogress')];
|
||||
return AddonModQuizDisplayOptionsAttemptStates.LATER_WHILE_OPEN;
|
||||
}
|
||||
|
||||
case AddonModQuizProvider.ATTEMPT_OVERDUE: {
|
||||
const sentences: string[] = [];
|
||||
const dueDate = this.getAttemptDueDate(quiz, attempt);
|
||||
/**
|
||||
* 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);
|
||||
|
||||
sentences.push(Translate.instant('addon.mod_quiz.stateoverdue'));
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
if (dueDate) {
|
||||
sentences.push(Translate.instant(
|
||||
'addon.mod_quiz.stateoverduedetails',
|
||||
{ $a: CoreTimeUtils.userDate(dueDate) },
|
||||
));
|
||||
}
|
||||
/**
|
||||
* Calculate the value for a certain display option.
|
||||
*
|
||||
* @param setting Setting value related to the option.
|
||||
* @param state Display options state.
|
||||
* @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;
|
||||
}
|
||||
|
||||
return sentences;
|
||||
}
|
||||
return whenNotSet;
|
||||
}
|
||||
|
||||
case AddonModQuizProvider.ATTEMPT_FINISHED:
|
||||
return [
|
||||
Translate.instant('addon.mod_quiz.statefinished'),
|
||||
Translate.instant(
|
||||
'addon.mod_quiz.statefinisheddetails',
|
||||
{ $a: CoreTimeUtils.userDate((attempt.timefinish ?? 0) * 1000) },
|
||||
),
|
||||
];
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
|
||||
case AddonModQuizProvider.ATTEMPT_ABANDONED:
|
||||
return [Translate.instant('addon.mod_quiz.stateabandoned')];
|
||||
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:
|
||||
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.
|
||||
* @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) {
|
||||
case AddonModQuizProvider.ATTEMPT_IN_PROGRESS:
|
||||
return Translate.instant('addon.mod_quiz.stateinprogress');
|
||||
case AddonModQuizAttemptStates.IN_PROGRESS:
|
||||
return CoreIonicColorNames.WARNING;
|
||||
|
||||
case AddonModQuizProvider.ATTEMPT_OVERDUE:
|
||||
return Translate.instant('addon.mod_quiz.stateoverdue');
|
||||
case AddonModQuizAttemptStates.OVERDUE:
|
||||
return CoreIonicColorNames.INFO;
|
||||
|
||||
case AddonModQuizProvider.ATTEMPT_FINISHED:
|
||||
return Translate.instant('addon.mod_quiz.statefinished');
|
||||
case AddonModQuizAttemptStates.FINISHED:
|
||||
return CoreIonicColorNames.SUCCESS;
|
||||
|
||||
case AddonModQuizProvider.ATTEMPT_ABANDONED:
|
||||
return Translate.instant('addon.mod_quiz.stateabandoned');
|
||||
case AddonModQuizAttemptStates.ABANDONED:
|
||||
return CoreIonicColorNames.DANGER;
|
||||
|
||||
default:
|
||||
return '';
|
||||
|
@ -413,7 +489,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Cache key.
|
||||
*/
|
||||
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 = {
|
||||
cacheKey: this.getAttemptReviewCacheKey(attemptId, page),
|
||||
cacheErrors: ['noreview'],
|
||||
component: AddonModQuizProvider.COMPONENT,
|
||||
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
@ -457,7 +532,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Cache key.
|
||||
*/
|
||||
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 = {
|
||||
cacheKey: this.getAttemptSummaryCacheKey(attemptId),
|
||||
component: AddonModQuizProvider.COMPONENT,
|
||||
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
@ -521,7 +596,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Cache key.
|
||||
*/
|
||||
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 = {
|
||||
cacheKey: this.getCombinedReviewOptionsCacheKey(quizId, userId),
|
||||
component: AddonModQuizProvider.COMPONENT,
|
||||
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
@ -581,7 +656,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Cache key.
|
||||
*/
|
||||
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 = {
|
||||
cacheKey: this.getFeedbackForGradeCacheKey(quizId, grade),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModQuizProvider.COMPONENT,
|
||||
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...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.
|
||||
* @returns Last finished attempt.
|
||||
* @returns Last completed attempt.
|
||||
*/
|
||||
getLastFinishedAttemptFromList(attempts?: AddonModQuizAttemptWSData[]): AddonModQuizAttemptWSData | undefined {
|
||||
getLastCompletedAttemptFromList(attempts?: AddonModQuizAttemptWSData[]): AddonModQuizAttemptWSData | undefined {
|
||||
if (!attempts) {
|
||||
return;
|
||||
}
|
||||
|
@ -677,7 +752,7 @@ export class AddonModQuizProvider {
|
|||
for (let i = attempts.length - 1; i >= 0; i--) {
|
||||
const attempt = attempts[i];
|
||||
|
||||
if (this.isAttemptFinished(attempt.state)) {
|
||||
if (this.isAttemptCompleted(attempt.state)) {
|
||||
return attempt;
|
||||
}
|
||||
}
|
||||
|
@ -719,7 +794,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Cache key.
|
||||
*/
|
||||
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 = {
|
||||
cacheKey: this.getQuizDataCacheKey(courseId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
component: AddonModQuizProvider.COMPONENT,
|
||||
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
||||
|
@ -797,7 +872,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Cache key.
|
||||
*/
|
||||
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 = {
|
||||
cacheKey: this.getQuizAccessInformationCacheKey(quizId),
|
||||
component: AddonModQuizProvider.COMPONENT,
|
||||
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
@ -842,13 +917,13 @@ export class AddonModQuizProvider {
|
|||
}
|
||||
|
||||
switch (method) {
|
||||
case AddonModQuizProvider.GRADEHIGHEST:
|
||||
case AddonModQuizGradeMethods.HIGHEST_GRADE:
|
||||
return Translate.instant('addon.mod_quiz.gradehighest');
|
||||
case AddonModQuizProvider.GRADEAVERAGE:
|
||||
case AddonModQuizGradeMethods.AVERAGE_GRADE:
|
||||
return Translate.instant('addon.mod_quiz.gradeaverage');
|
||||
case AddonModQuizProvider.ATTEMPTFIRST:
|
||||
case AddonModQuizGradeMethods.FIRST_ATTEMPT:
|
||||
return Translate.instant('addon.mod_quiz.attemptfirst');
|
||||
case AddonModQuizProvider.ATTEMPTLAST:
|
||||
case AddonModQuizGradeMethods.LAST_ATTEMPT:
|
||||
return Translate.instant('addon.mod_quiz.attemptlast');
|
||||
default:
|
||||
return '';
|
||||
|
@ -862,7 +937,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Cache key.
|
||||
*/
|
||||
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 = {
|
||||
cacheKey: this.getQuizRequiredQtypesCacheKey(quizId),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||
component: AddonModQuizProvider.COMPONENT,
|
||||
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
@ -1015,7 +1090,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Cache key.
|
||||
*/
|
||||
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 = {
|
||||
cacheKey: this.getUserAttemptsCacheKey(quizId, userId),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||
component: AddonModQuizProvider.COMPONENT,
|
||||
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||
};
|
||||
|
@ -1073,7 +1148,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Cache key.
|
||||
*/
|
||||
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 = {
|
||||
cacheKey: this.getUserBestGradeCacheKey(quizId, userId),
|
||||
component: AddonModQuizProvider.COMPONENT,
|
||||
component: ADDON_MOD_QUIZ_COMPONENT,
|
||||
componentId: options.cmId,
|
||||
...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.
|
||||
* @returns Whether it's finished.
|
||||
*/
|
||||
isAttemptFinished(state?: string): boolean {
|
||||
return state == AddonModQuizProvider.ATTEMPT_FINISHED || state == AddonModQuizProvider.ATTEMPT_ABANDONED;
|
||||
isAttemptCompleted(state?: string): boolean {
|
||||
return state === AddonModQuizAttemptStates.FINISHED || state === AddonModQuizAttemptStates.ABANDONED;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1461,7 +1536,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Whether it's nearly over or over.
|
||||
*/
|
||||
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.
|
||||
return true;
|
||||
}
|
||||
|
@ -1600,7 +1675,7 @@ export class AddonModQuizProvider {
|
|||
return CoreCourseLogHelper.log(
|
||||
'mod_quiz_view_attempt_review',
|
||||
params,
|
||||
AddonModQuizProvider.COMPONENT,
|
||||
ADDON_MOD_QUIZ_COMPONENT,
|
||||
quizId,
|
||||
siteId,
|
||||
);
|
||||
|
@ -1633,7 +1708,7 @@ export class AddonModQuizProvider {
|
|||
return CoreCourseLogHelper.log(
|
||||
'mod_quiz_view_attempt_summary',
|
||||
params,
|
||||
AddonModQuizProvider.COMPONENT,
|
||||
ADDON_MOD_QUIZ_COMPONENT,
|
||||
quizId,
|
||||
siteId,
|
||||
);
|
||||
|
@ -1654,7 +1729,7 @@ export class AddonModQuizProvider {
|
|||
return CoreCourseLogHelper.log(
|
||||
'mod_quiz_view_quiz',
|
||||
params,
|
||||
AddonModQuizProvider.COMPONENT,
|
||||
ADDON_MOD_QUIZ_COMPONENT,
|
||||
id,
|
||||
siteId,
|
||||
);
|
||||
|
@ -1897,7 +1972,7 @@ export class AddonModQuizProvider {
|
|||
shouldShowTimeLeft(rules: string[], attempt: AddonModQuizAttemptWSData, endTime: number): boolean {
|
||||
const timeNow = CoreTimeUtils.timestamp();
|
||||
|
||||
if (attempt.state != AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
|
||||
if (attempt.state !== AddonModQuizAttemptStates.IN_PROGRESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -2205,6 +2280,7 @@ export type AddonModQuizQuizWSData = {
|
|||
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.
|
||||
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.
|
||||
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.
|
||||
|
@ -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 = {
|
||||
quizId: number;
|
||||
attemptId: number;
|
||||
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
|
||||
And I press "Submit" near "Once you submit" in the app
|
||||
Then I should find "Review" in the app
|
||||
And I should find "Started on" in the app
|
||||
And I should find "State" in the app
|
||||
And I should find "Completed on" in the app
|
||||
And I should find "Time taken" in the app
|
||||
And I should find "Started" in the app
|
||||
And I should find "Status" in the app
|
||||
And I should find "Completed" in the app
|
||||
And I should find "Duration" in the app
|
||||
And I should find "Marks" in the app
|
||||
And I should find "Grade" 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
|
||||
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]"
|
||||
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 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
|
||||
|
||||
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
|
||||
And I press "Submit" near "Once you submit" in the app
|
||||
Then I should find "Review" in the app
|
||||
And I should find "Started on" in the app
|
||||
And I should find "State" in the app
|
||||
And I should find "Completed on" in the app
|
||||
And I should find "Time taken" in the app
|
||||
And I should find "Started" in the app
|
||||
And I should find "Status" in the app
|
||||
And I should find "Completed" in the app
|
||||
And I should find "Duration" in the app
|
||||
And I should find "Marks" in the app
|
||||
And I should find "Grade" 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
|
||||
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"
|
||||
When I am on the "quiz1" Activity page logged in as teacher1
|
||||
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
|
||||
And I press "Submit" near "Once you submit" in the app
|
||||
Then I should find "Review" in the app
|
||||
And I should find "Started on" in the app
|
||||
And I should find "Completed on" in the app
|
||||
And I should find "Time taken" in the app
|
||||
And I should find "Finished" within "State" "ion-item" in the app
|
||||
And I should find "Started" in the app
|
||||
And I should find "Completed" in the app
|
||||
And I should find "Duration" 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 "Cognition" "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 | |
|
||||
|
||||
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
|
||||
And I should find "Finished" within "State" "ion-item" in the app
|
||||
And I should find "0" within "Logic / 1" "ion-item" in the app
|
||||
And I should find "0" within "Cognition / 1" "ion-item" in the app
|
||||
And I should find "0" within "Marks / 2" "ion-item" in the app
|
||||
And I should find "0" within "Grade / 100" "ion-item" in the app
|
||||
And I should find "Review" 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 "Cognition" "ion-item" in the app
|
||||
And I should find "0/2" within "Marks" "ion-item" in the app
|
||||
And I should find "0 out of 100" within "Grade" "ion-item" in the app
|
||||
And I should be able to press "Review" in the app
|
||||
|
||||
Scenario: Attempt a quiz (all question types)
|
||||
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
|
||||
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]"
|
||||
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 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
|
||||
|
||||
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",
|
||||
"gradebook": "Gradebook",
|
||||
"gradeitem": "Grade item",
|
||||
"gradelong": "{{$a.grade}} / {{$a.max}}",
|
||||
"gradepass": "Grade to pass",
|
||||
"grades": "Grades",
|
||||
"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_GAVE_UP_STATE_CLASSES = ['notanswered'] 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",
|
||||
"strftimetime24": "%H:%M",
|
||||
"submit": "Submit",
|
||||
"submittedoffline": "Submitted (Offline)",
|
||||
"success": "Success",
|
||||
"summary": "Summary",
|
||||
"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) {
|
||||
return true;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue