diff --git a/scripts/langindex.json b/scripts/langindex.json index d773eddd2..5b18e0fbf 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -872,6 +872,7 @@ "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", @@ -902,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", @@ -1864,6 +1865,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 +2562,7 @@ "core.strftimetime12": "langconfig", "core.strftimetime24": "langconfig", "core.submit": "moodle", + "core.submittedoffline": "local_moodlemobileapp", "core.success": "moodle", "core.summary": "moodle", "core.swipenavigationtourdescription": "local_moodlemobileapp", diff --git a/src/addons/mod/quiz/components/attempt-info/attempt-info.html b/src/addons/mod/quiz/components/attempt-info/attempt-info.html new file mode 100644 index 000000000..a23777ddd --- /dev/null +++ b/src/addons/mod/quiz/components/attempt-info/attempt-info.html @@ -0,0 +1,78 @@ + + +

{{ 'addon.mod_quiz.attemptstate' | translate }}

+ +
+
+ + + +

{{ 'addon.mod_quiz.startedon' | translate }}

+

{{ attempt.timestart! * 1000 | coreFormatDate }}

+
+
+ +@if (isFinished) { + + +

{{ 'addon.mod_quiz.completedon' | translate }}

+

{{ attempt.timefinish! * 1000 | coreFormatDate }}

+
+
+ +@if (timeTaken) { + + +

{{ 'addon.mod_quiz.attemptduration' | translate }}

+

{{ timeTaken }}

+
+
+} + +@if (overTime) { + + +

{{ 'addon.mod_quiz.overdue' | translate }}

+

{{ overTime }}

+
+
+} + +@for (gradeItemMark of gradeItemMarks; track $index) { + + +

{{ gradeItemMark.name }}

+

+
+
+} + +@if (quiz.showAttemptsMarks && readableMark && attempt?.sumgrades !== null) { + + +

{{ 'addon.mod_quiz.marks' | translate }}

+

{{ readableMark }}

+
+
+} + +@if (readableGrade) { + + +

{{ 'addon.mod_quiz.grade' | translate }}

+

+
+
+} + +@for (data of additionalData; track data.id) { + + +

{{ data.title }}

+ +
+
+} + +} diff --git a/src/addons/mod/quiz/components/attempt-info/attempt-info.ts b/src/addons/mod/quiz/components/attempt-info/attempt-info.ts new file mode 100644 index 000000000..ebb7be876 --- /dev/null +++ b/src/addons/mod/quiz/components/attempt-info/attempt-info.ts @@ -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 { + 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: '' + AddonModQuiz.formatGrade(gradeItemMark.grade, this.quiz?.decimalpoints) + '', + 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 = { + grade: '' + AddonModQuiz.formatGrade(this.attempt.rescaledGrade, this.quiz.decimalpoints) + '', + 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 = '' + AddonModQuiz.formatGrade(percentage, this.quiz.decimalpoints) + ''; + this.readableGrade = Translate.instant('addon.mod_quiz.outofpercent', { $a: gradeObject }); + } else { + this.readableGrade = Translate.instant('addon.mod_quiz.outof', { $a: gradeObject }); + } + } + +} diff --git a/src/addons/mod/quiz/components/attempt-state/attempt-state.html b/src/addons/mod/quiz/components/attempt-state/attempt-state.html new file mode 100644 index 000000000..eeedb8767 --- /dev/null +++ b/src/addons/mod/quiz/components/attempt-state/attempt-state.html @@ -0,0 +1,6 @@ + + @if (finishedOffline) { + diff --git a/src/addons/mod/quiz/components/attempt-state/attempt-state.scss b/src/addons/mod/quiz/components/attempt-state/attempt-state.scss new file mode 100644 index 000000000..8f9843ae0 --- /dev/null +++ b/src/addons/mod/quiz/components/attempt-state/attempt-state.scss @@ -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)); + } + } + +} diff --git a/src/addons/mod/quiz/components/attempt-state/attempt-state.ts b/src/addons/mod/quiz/components/attempt-state/attempt-state.ts new file mode 100644 index 000000000..dd165198a --- /dev/null +++ b/src/addons/mod/quiz/components/attempt-state/attempt-state.ts @@ -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 { + this.readableState = AddonModQuiz.getAttemptReadableStateName(this.state, this.finishedOffline); + this.color = AddonModQuiz.getAttemptStateColor(this.state, this.finishedOffline); + } + +} diff --git a/src/addons/mod/quiz/components/components.module.ts b/src/addons/mod/quiz/components/components.module.ts index 9b167b237..5eade44a0 100644 --- a/src/addons/mod/quiz/components/components.module.ts +++ b/src/addons/mod/quiz/components/components.module.ts @@ -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, diff --git a/src/addons/mod/quiz/components/index/addon-mod-quiz-index.html b/src/addons/mod/quiz/components/index/addon-mod-quiz-index.html index b3bc4b6fa..61e5cd8ba 100644 --- a/src/addons/mod/quiz/components/index/addon-mod-quiz-index.html +++ b/src/addons/mod/quiz/components/index/addon-mod-quiz-index.html @@ -45,64 +45,74 @@ - + {{ 'addon.mod_quiz.summaryofattempts' | translate }} - - - - - - - - {{ 'addon.mod_quiz.attemptnumber' | translate }} - {{ 'addon.mod_quiz.attemptnumber' | translate }} - - - {{ 'addon.mod_quiz.attemptstate' | translate }} - - - {{ 'addon.mod_quiz.marks' | translate }} / {{ quiz.sumGradesFormatted }} - - - {{ 'addon.mod_quiz.grade' | translate }} / {{ quiz.gradeFormatted }} - - - - -
- - + + + @for (attempt of attempts; track attempt.id) { + + - - - {{ 'addon.mod_quiz.preview' | translate }} - - - {{ attempt.attempt }} - - -

{{ sentence }}

-
- -

{{ attempt.readableMark }}

-
- -

{{ attempt.readableGrade }}

-
-
+

{{ 'addon.mod_quiz.attempt' | translate:{ $a: attempt.attempt } }}

+
+ + @if (attempt.finished && quiz.showAttemptsGrades) { + @if (attempt.rescaledGrade !== undefined && attempt.rescaledGrade >= 0) { +

+ {{ 'core.grades.gradelong' | translate: { $a: { + grade: attempt.formattedGrade, + max: quiz.gradeFormatted, + } } }} +

+ } @else { +

{{ 'addon.mod_quiz.notyetgraded' | translate }}

+ } + } +
-
-
+
+ + + @if (attempt.canReview) { +
+ + + } @else if (attempt.completed) { +
+ + +

+

+
+
+ } @else if (attempt.finishedOffline) { +
+ + +

+

+
+
+ } +
+ + } +
+ (gradeResult || gradeOverridden || gradebookFeedback || (quiz.showFeedback && overallFeedback))"> {{ gradeResult }} @@ -119,7 +129,7 @@

- +

{{ 'addon.mod_quiz.overallfeedback' | translate }}

diff --git a/src/addons/mod/quiz/components/index/index.scss b/src/addons/mod/quiz/components/index/index.scss index c153eae1b..b7640a4aa 100644 --- a/src/addons/mod/quiz/components/index/index.scss +++ b/src/addons/mod/quiz/components/index/index.scss @@ -1,33 +1,43 @@ +@use "theme/globals" as *; + :host { - .addon-mod_quiz-table { - ion-card-content { - padding-left: 0; - padding-right: 0; - } + .addon-mod_quiz-attempt-title-info { + text-align: end; + padding-top: 8px; + padding-bottom: 8px; - .item:nth-child(even) { - --background: var(--light); - } - - .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); + p { + margin: 0px; + margin-top: 4px; } } -} -: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); + .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 16px; + } + + ion-accordion:nth-child(odd) { + background-color: var(--core-table-odd-cell-background); + + ::ng-deep ion-item { + --background: var(--core-table-odd-cell-background); } } + + ion-accordion:nth-child(even) { + background-color: var(--core-table-even-cell-background); + + ::ng-deep ion-item { + --background: var(--core-table-even-cell-background); + } + } + } diff --git a/src/addons/mod/quiz/components/index/index.ts b/src/addons/mod/quiz/components/index/index.ts index 77d14dacd..f0b27e252 100644 --- a/src/addons/mod/quiz/components/index/index.ts +++ b/src/addons/mod/quiz/components/index/index.ts @@ -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,6 +36,7 @@ import { AddonModQuizGetAttemptAccessInformationWSResponse, AddonModQuizGetQuizAccessInformationWSResponse, AddonModQuizGetUserBestGradeWSResponse, + AddonModQuizWSAdditionalData, } from '../../services/quiz'; import { AddonModQuizAttempt, AddonModQuizHelper, AddonModQuizQuizData } from '../../services/quiz-helper'; import { @@ -44,7 +45,7 @@ import { AddonModQuizSyncProvider, AddonModQuizSyncResult, } from '../../services/quiz-sync'; -import { ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT, ADDON_MOD_QUIZ_COMPONENT, AddonModQuizGradeMethods } from '../../constants'; +import { ADDON_MOD_QUIZ_ATTEMPT_FINISHED_EVENT, ADDON_MOD_QUIZ_COMPONENT, AddonModQuizAttemptStates } from '../../constants'; import { QuestionDisplayOptionsMarks } from '@features/question/constants'; /** @@ -78,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. @@ -263,7 +263,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp // Check if user can create/continue attempts. if (this.attempts.length) { - const last = this.attempts[this.attempts.length - 1]; + const last = this.attempts[0]; this.moreAttempts = !AddonModQuiz.isAttemptCompleted(last.state) || !this.attemptAccessInfo.isfinished; } else { this.moreAttempts = !this.attemptAccessInfo.isfinished; @@ -283,7 +283,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp this.buttonText = ''; if (quiz.hasquestions !== 0) { - if (this.attempts.length && !AddonModQuiz.isAttemptCompleted(this.attempts[this.attempts.length - 1].state)) { + if (this.attempts.length && !AddonModQuiz.isAttemptCompleted(this.attempts[0].state)) { // Last attempt is unfinished. if (this.quizAccessInfo?.canattempt) { this.buttonText = 'addon.mod_quiz.continueattemptquiz'; @@ -331,7 +331,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp * @returns Promise resolved when done. */ protected async getResultInfo(quiz: AddonModQuizQuizData): Promise { - 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; @@ -372,7 +372,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp } } - 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, @@ -583,7 +583,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp quiz: AddonModQuizQuizData, accessInfo: AddonModQuizGetQuizAccessInformationWSResponse, attempts: AddonModQuizAttemptWSData[], - ): Promise { + ): Promise { if (!attempts || !attempts.length) { // There are no attempts to treat. quiz.gradeFormatted = AddonModQuiz.formatGrade(quiz.grade, quiz.decimalpoints); @@ -609,25 +609,43 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp ]); 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 = !!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 === AddonModQuizGradeMethods.HIGHEST_GRADE && - 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, + AddonModQuizHelper.canReviewAttempt(quiz, accessInfo, attempt), + ]); - return AddonModQuizHelper.setAttemptCalculatedData(quiz, accessInfo, attempt, shouldHighlight, quizGrade, isLast); + formattedAttempt.canReview = canReview; + + if (quiz.showFeedback && attempt.state === AddonModQuizAttemptStates.FINISHED && + options.someoptions.overallfeedback && isSafeNumber(formattedAttempt.rescaledGrade)) { + + // Feedback should be displayed, get the feedback for the grade. + const response = await AddonModQuiz.getFeedbackForGrade(quiz.id, formattedAttempt.rescaledGrade, { + cmId: quiz.coursemodule, + }); + + if (response.feedbacktext) { + formattedAttempt.additionalData = [ + { + id: 'feedback', + title: Translate.instant('addon.mod_quiz.feedback'), + content: response.feedbacktext, + }, + ]; + } + } + + return formattedAttempt; })); - return formattedAttempts; + return formattedAttempts.reverse(); } /** @@ -657,13 +675,13 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp } /** - * Go to page to view the attempt details. + * Go to page to review the attempt. * * @returns Promise resolved when done. */ - async viewAttempt(attemptId: number): Promise { + async reviewAttempt(attemptId: number): Promise { await CoreNavigator.navigateToSitePath( - `${AddonModQuizModuleHandlerService.PAGE_NAME}/${this.courseId}/${this.module.id}/attempt/${attemptId}`, + `${AddonModQuizModuleHandlerService.PAGE_NAME}/${this.courseId}/${this.module.id}/review/${attemptId}`, ); } @@ -677,3 +695,8 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp } } + +type QuizAttempt = AddonModQuizAttempt & { + canReview?: boolean; + additionalData?: AddonModQuizWSAdditionalData[]; // Additional data to display for the attempt. +}; diff --git a/src/addons/mod/quiz/lang.json b/src/addons/mod/quiz/lang.json index 0301345ce..44e232d1e 100644 --- a/src/addons/mod/quiz/lang.json +++ b/src/addons/mod/quiz/lang.json @@ -1,5 +1,6 @@ { "answercolon": "Answer:", + "attempt": "Attempt {{$a}}", "attemptduration": "Duration", "attemptfirst": "First attempt", "attemptlast": "Last attempt", @@ -30,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", diff --git a/src/addons/mod/quiz/pages/attempt/attempt.html b/src/addons/mod/quiz/pages/attempt/attempt.html deleted file mode 100644 index b7ebab77d..000000000 --- a/src/addons/mod/quiz/pages/attempt/attempt.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - -

- -

- - - - - - - - - - - -

{{ 'addon.mod_quiz.attemptnumber' | translate }}

-

{{ 'addon.mod_quiz.preview' | translate }}

-

{{ attempt.attempt }}

-
-
- - -

{{ 'addon.mod_quiz.attemptstate' | translate }}

-

{{ sentence }}

-
-
- - - -

{{ gradeItemMark.name }} / {{ gradeItemMark.maxgrade }}

-

{{ gradeItemMark.grade }}

-
-
-
- - -

{{ 'addon.mod_quiz.marks' | translate }} / {{ quiz!.sumGradesFormatted }}

-

{{ attempt.readableMark }}

-
-
- - -

{{ 'addon.mod_quiz.grade' | translate }} / {{ quiz!.gradeFormatted }}

-

{{ attempt.readableGrade }}

-
-
- - -

{{ 'addon.mod_quiz.feedback' | translate }}

-

- -

-
-
- - - -

{{ 'addon.mod_quiz.noreviewattempt' | translate }}

-
-
-
- -
-
- - -
-
-
-
diff --git a/src/addons/mod/quiz/pages/attempt/attempt.ts b/src/addons/mod/quiz/pages/attempt/attempt.ts deleted file mode 100644 index eb84bee9a..000000000 --- a/src/addons/mod/quiz/pages/attempt/attempt.ts +++ /dev/null @@ -1,221 +0,0 @@ -// (C) Copyright 2015 Moodle Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { isSafeNumber } from '@/core/utils/types'; -import { Component, OnInit } from '@angular/core'; -import { CoreError } from '@classes/errors/error'; -import { CoreNavigator } from '@services/navigator'; -import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; -import { Translate } from '@singletons'; -import { - AddonModQuiz, - AddonModQuizAttemptWSData, - AddonModQuizGetQuizAccessInformationWSResponse, -} from '../../services/quiz'; -import { AddonModQuizAttempt, AddonModQuizHelper, AddonModQuizQuizData } from '../../services/quiz-helper'; -import { ADDON_MOD_QUIZ_COMPONENT } from '../../constants'; - -/** - * Page that displays some summary data about an attempt. - */ -@Component({ - selector: 'page-addon-mod-quiz-attempt', - templateUrl: 'attempt.html', -}) -export class AddonModQuizAttemptPage implements OnInit { - - courseId!: number; // The course ID the quiz belongs to. - quiz?: AddonModQuizQuizData; // The quiz the attempt belongs to. - attempt?: AddonModQuizAttempt; // The attempt to view. - component = ADDON_MOD_QUIZ_COMPONENT; // Component to link the files to. - componentId?: number; // Component ID to use in conjunction with the component. - loaded = false; // Whether data has been loaded. - feedback?: string; // Attempt feedback. - showReviewColumn = false; - cmId!: number; // Course module id the attempt belongs to. - - protected attemptId!: number; // Attempt to view. - - /** - * @inheritdoc - */ - ngOnInit(): void { - try { - this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); - this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); - this.attemptId = CoreNavigator.getRequiredRouteNumberParam('attemptId'); - } catch (error) { - CoreDomUtils.showErrorModal(error); - - CoreNavigator.back(); - - return; - } - - this.fetchQuizData().finally(() => { - this.loaded = true; - }); - } - - /** - * Refresh the data. - * - * @param refresher Refresher. - */ - doRefresh(refresher: HTMLIonRefresherElement): void { - this.refreshData().finally(() => { - refresher.complete(); - }); - } - - /** - * Get quiz data and attempt data. - * - * @returns Promise resolved when done. - */ - protected async fetchQuizData(): Promise { - try { - this.quiz = await AddonModQuiz.getQuiz(this.courseId, this.cmId); - - this.componentId = this.quiz.coursemodule; - - // Load attempt data. - const [options, accessInfo, attempt] = await Promise.all([ - AddonModQuiz.getCombinedReviewOptions(this.quiz.id, { cmId: this.quiz.coursemodule }), - this.fetchAccessInfo(this.quiz), - this.fetchAttempt(this.quiz.id), - ]); - - // Set calculated data. - this.showReviewColumn = accessInfo.canreviewmyattempts; - AddonModQuizHelper.setQuizCalculatedData(this.quiz, options); - - this.attempt = await AddonModQuizHelper.setAttemptCalculatedData( - this.quiz, - accessInfo, - attempt, - false, - undefined, - true, - ); - - // Check if the feedback should be displayed. - const grade = Number(this.attempt.rescaledGrade); - - if (this.quiz.showFeedbackColumn && AddonModQuiz.isAttemptCompleted(this.attempt.state) && - options.someoptions.overallfeedback && isSafeNumber(grade)) { - - // Feedback should be displayed, get the feedback for the grade. - const response = await AddonModQuiz.getFeedbackForGrade(this.quiz.id, grade, { - cmId: this.quiz.coursemodule, - }); - - this.feedback = response.feedbacktext; - } else { - delete this.feedback; - } - } catch (error) { - CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetattempt', true); - } - } - - /** - * Get the attempt. - * - * @param quizId Quiz ID. - * @returns Promise resolved when done. - */ - protected async fetchAttempt(quizId: number): Promise { - // 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 { - 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 { - const promises: Promise[] = []; - - 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 { - if (!this.attempt) { - return; - } - - CoreNavigator.navigate(`../../review/${this.attempt.id}`); - } - -} diff --git a/src/addons/mod/quiz/pages/player/player.scss b/src/addons/mod/quiz/pages/player/player.scss index fb6ba1e5f..3242512cc 100644 --- a/src/addons/mod/quiz/pages/player/player.scss +++ b/src/addons/mod/quiz/pages/player/player.scss @@ -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 { diff --git a/src/addons/mod/quiz/pages/player/player.ts b/src/addons/mod/quiz/pages/player/player.ts index b84849c98..4221be887 100644 --- a/src/addons/mod/quiz/pages/player/player.ts +++ b/src/addons/mod/quiz/pages/player/player.ts @@ -40,7 +40,7 @@ import { AddonModQuizGetQuizAccessInformationWSResponse, 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'; @@ -66,7 +66,7 @@ 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 = ADDON_MOD_QUIZ_COMPONENT; // Component to link the files to. loaded = false; // Whether data has been loaded. @@ -91,7 +91,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { protected preflightData: Record = {}; // 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. @@ -381,14 +381,9 @@ 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, - this.quizAccessInfo, - attempts[attempts.length - 1], - false, - undefined, - true, - ); + this.lastAttempt = attempts[attempts.length - 1]; + + this.lastAttempt.finishedOffline = await AddonModQuiz.isAttemptFinishedOffline(this.lastAttempt.id); this.newAttempt = AddonModQuiz.isAttemptCompleted(this.lastAttempt.state); } @@ -945,3 +940,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; +}; diff --git a/src/addons/mod/quiz/pages/review/review.html b/src/addons/mod/quiz/pages/review/review.html index 3eb7a8e35..217705533 100644 --- a/src/addons/mod/quiz/pages/review/review.html +++ b/src/addons/mod/quiz/pages/review/review.html @@ -24,61 +24,7 @@ - - -

{{ 'addon.mod_quiz.startedon' | translate }}

-

{{ attempt.timestart! * 1000 | coreFormatDate }}

-
-
- - -

{{ 'addon.mod_quiz.attemptstate' | translate }}

-

{{ readableState }}

-
-
- - -

{{ 'addon.mod_quiz.completedon' | translate }}

-

{{ attempt.timefinish! * 1000 | coreFormatDate }}

-
-
- - -

{{ 'addon.mod_quiz.attemptduration' | translate }}

-

{{ timeTaken }}

-
-
- - -

{{ 'addon.mod_quiz.overdue' | translate }}

-

{{ overTime }}

-
-
- - -

{{ gradeItemMark.name }}

-

{{ gradeItemMark.grade }}

-
-
- - -

{{ 'addon.mod_quiz.marks' | translate }}

-

{{ readableMark }}

-
-
- - -

{{ 'addon.mod_quiz.grade' | translate }}

-

{{ readableGrade }}

-
-
- - -

{{ data.title }}

- -
-
+
diff --git a/src/addons/mod/quiz/pages/review/review.ts b/src/addons/mod/quiz/pages/review/review.ts index a8b6ee23e..66b2291df 100644 --- a/src/addons/mod/quiz/pages/review/review.ts +++ b/src/addons/mod/quiz/pages/review/review.ts @@ -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,14 +30,12 @@ import { AddonModQuiz, AddonModQuizAttemptWSData, AddonModQuizCombinedReviewOptions, - AddonModQuizGetAttemptReviewResponse, AddonModQuizQuizWSData, AddonModQuizWSAdditionalData, } from '../../services/quiz'; import { AddonModQuizHelper } from '../../services/quiz-helper'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { AddonModQuizAttemptStates, ADDON_MOD_QUIZ_COMPONENT } from '../../constants'; -import { QuestionDisplayOptionsMarks } from '@features/question/constants'; +import { ADDON_MOD_QUIZ_COMPONENT } from '../../constants'; /** * Page that allows reviewing a quiz attempt. @@ -63,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; @@ -159,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(); @@ -178,15 +176,17 @@ export class AddonModQuizReviewPage implements OnInit { * @returns Promise resolved when done. */ protected async loadPage(page: number): Promise { - 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; @@ -254,91 +254,6 @@ export class AddonModQuizReviewPage implements OnInit { ); } - /** - * Calculate review summary data. - * - * @param data Result of getAttemptReview. - */ - protected setSummaryCalculatedData(data: AddonModQuizGetAttemptReviewResponse): void { - if (!this.attempt || !this.quiz) { - return; - } - - this.readableState = AddonModQuiz.getAttemptReadableStateName(this.attempt.state ?? ''); - - if (this.attempt.state !== AddonModQuizAttemptStates.FINISHED) { - return; - } - - this.showCompleted = true; - this.additionalData = data.additionaldata; - - const timeTaken = (this.attempt.timefinish || 0) - (this.attempt.timestart || 0); - if (timeTaken > 0) { - // Format time taken. - this.timeTaken = CoreTime.formatTime(timeTaken); - - // Calculate overdue time. - if (this.quiz.timelimit && timeTaken > this.quiz.timelimit + 60) { - this.overTime = CoreTime.formatTime(timeTaken - this.quiz.timelimit); - } - } else { - this.timeTaken = undefined; - } - - // Treat grade item marks. - if (this.attempt.sumgrades === null || !this.attempt.gradeitemmarks) { - this.gradeItemMarks = []; - } else { - this.gradeItemMarks = this.attempt.gradeitemmarks.map((gradeItemMark) => ({ - name: gradeItemMark.name, - grade: Translate.instant('addon.mod_quiz.outof', { $a: { - grade: AddonModQuiz.formatGrade(gradeItemMark.grade, this.quiz?.decimalpoints), - maxgrade: AddonModQuiz.formatGrade(gradeItemMark.maxgrade, this.quiz?.decimalpoints), - } }), - })); - } - - // Treat grade. - if (this.options && this.options.someoptions.marks >= QuestionDisplayOptionsMarks.MARK_AND_MAX && - AddonModQuiz.quizHasGrades(this.quiz)) { - - if (data.grade === null || data.grade === undefined) { - this.readableGrade = AddonModQuiz.formatGrade(data.grade, this.quiz.decimalpoints); - } else { - // Show raw marks only if they are different from the grade (like on the entry page). - if (this.quiz.grade != this.quiz.sumgrades) { - this.readableMark = Translate.instant('addon.mod_quiz.outofshort', { $a: { - grade: AddonModQuiz.formatGrade(this.attempt.sumgrades, this.quiz.decimalpoints), - maxgrade: AddonModQuiz.formatGrade(this.quiz.sumgrades, this.quiz.decimalpoints), - } }); - } - - // Now the scaled grade. - const gradeObject: Record = { - 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. */ diff --git a/src/addons/mod/quiz/quiz-lazy.module.ts b/src/addons/mod/quiz/quiz-lazy.module.ts index 51f1e3d04..99f8d8607 100644 --- a/src/addons/mod/quiz/quiz-lazy.module.ts +++ b/src/addons/mod/quiz/quiz-lazy.module.ts @@ -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, ], diff --git a/src/addons/mod/quiz/services/quiz-helper.ts b/src/addons/mod/quiz/services/quiz-helper.ts index e3e3c9506..9f2fd4cb9 100644 --- a/src/addons/mod/quiz/services/quiz-helper.ts +++ b/src/addons/mod/quiz/services/quiz-helper.ts @@ -359,53 +359,28 @@ export class AddonModQuizHelperProvider { * Add some calculated data to the attempt. * * @param quiz Quiz. - * @param accessInfo Quiz access info. * @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, - accessInfo: AddonModQuizGetQuizAccessInformationWSResponse, attempt: AddonModQuizAttemptWSData, - highlight?: boolean, - bestGrade?: string, - isLastAttempt?: boolean, siteId?: string, ): Promise { const formattedAttempt = attempt; - formattedAttempt.rescaledGrade = AddonModQuiz.rescaleGrade(attempt.sumgrades, quiz, false); + formattedAttempt.finished = attempt.state === AddonModQuizAttemptStates.FINISHED; formattedAttempt.completed = AddonModQuiz.isAttemptCompleted(attempt.state); - formattedAttempt.readableState = AddonModQuiz.getAttemptReadableState(quiz, attempt); + formattedAttempt.rescaledGrade = Number(AddonModQuiz.rescaleGrade(attempt.sumgrades, quiz, false)); - if (quiz.showMarkColumn && formattedAttempt.completed) { - 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.completed) { - formattedAttempt.readableGrade = AddonModQuiz.formatGrade( - Number(formattedAttempt.rescaledGrade), - quiz.decimalpoints, - ); - - // Highlight the highest grade if appropriate. - formattedAttempt.highlightGrade = !!(highlight && !attempt.preview && - attempt.state === AddonModQuizAttemptStates.FINISHED && formattedAttempt.readableGrade == bestGrade); - } else { - formattedAttempt.readableGrade = ''; - } - - if (isLastAttempt || isLastAttempt === undefined) { - formattedAttempt.finishedOffline = await AddonModQuiz.isAttemptFinishedOffline(attempt.id, siteId); - } - - formattedAttempt.canReview = await this.canReviewAttempt(quiz, accessInfo, attempt); + formattedAttempt.finishedOffline = await AddonModQuiz.isAttemptFinishedOffline(attempt.id, siteId); return formattedAttempt; } @@ -423,11 +398,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 >= QuestionDisplayOptionsMarks.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; } @@ -523,10 +497,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; }; /** @@ -534,11 +507,8 @@ export type AddonModQuizQuizData = AddonModQuizQuizWSData & { */ export type AddonModQuizAttempt = AddonModQuizAttemptWSData & { finishedOffline?: boolean; - rescaledGrade?: string; + rescaledGrade?: number; + finished?: boolean; completed?: boolean; - readableState?: string[]; - readableMark?: string; - readableGrade?: string; - highlightGrade?: boolean; - canReview?: boolean; + formattedGrade?: string; }; diff --git a/src/addons/mod/quiz/services/quiz.ts b/src/addons/mod/quiz/services/quiz.ts index 32c48e954..66419643b 100644 --- a/src/addons/mod/quiz/services/quiz.ts +++ b/src/addons/mod/quiz/services/quiz.ts @@ -37,7 +37,6 @@ 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'; @@ -55,6 +54,7 @@ import { AddonModQuizDisplayOptionsAttemptStates, ADDON_MOD_QUIZ_IMMEDIATELY_AFTER_PERIOD, } from '../constants'; +import { CoreIonicColorNames } from '@singletons/colors'; declare module '@singletons/events' { @@ -412,61 +412,17 @@ export class AddonModQuizProvider { } /** - * Turn attempt's state into a readable state, including some extra data depending on the state. - * - * @param quiz Quiz. - * @param attempt Attempt. - * @returns List of state sentences. - */ - getAttemptReadableState(quiz: AddonModQuizQuizWSData, attempt: AddonModQuizAttempt): string[] { - if (attempt.finishedOffline) { - return [Translate.instant('addon.mod_quiz.finishnotsynced')]; - } - - switch (attempt.state) { - case AddonModQuizAttemptStates.IN_PROGRESS: - return [Translate.instant('addon.mod_quiz.stateinprogress')]; - - case AddonModQuizAttemptStates.OVERDUE: { - const sentences: string[] = []; - const dueDate = this.getAttemptDueDate(quiz, attempt); - - sentences.push(Translate.instant('addon.mod_quiz.stateoverdue')); - - if (dueDate) { - sentences.push(Translate.instant( - 'addon.mod_quiz.stateoverduedetails', - { $a: CoreTimeUtils.userDate(dueDate) }, - )); - } - - return sentences; - } - - case AddonModQuizAttemptStates.FINISHED: - return [ - Translate.instant('addon.mod_quiz.statefinished'), - Translate.instant( - 'addon.mod_quiz.statefinisheddetails', - { $a: CoreTimeUtils.userDate((attempt.timefinish ?? 0) * 1000) }, - ), - ]; - - case AddonModQuizAttemptStates.ABANDONED: - return [Translate.instant('addon.mod_quiz.stateabandoned')]; - - default: - return []; - } - } - - /** - * Turn attempt's state into a readable state name, without any more data. + * 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): string { + getAttemptReadableStateName(state: string, finishedOffline = false): string { + if (finishedOffline) { + return Translate.instant('core.submittedoffline'); + } + switch (state) { case AddonModQuizAttemptStates.IN_PROGRESS: return Translate.instant('addon.mod_quiz.stateinprogress'); @@ -485,6 +441,36 @@ export class AddonModQuizProvider { } } + /** + * Get the color to apply to the attempt state. + * + * @param state State. + * @param finishedOffline Whether the attempt was finished offline. + * @returns State color. + */ + getAttemptStateColor(state: string, finishedOffline = false): string { + if (finishedOffline) { + return CoreIonicColorNames.MEDIUM; + } + + switch (state) { + case AddonModQuizAttemptStates.IN_PROGRESS: + return CoreIonicColorNames.WARNING; + + case AddonModQuizAttemptStates.OVERDUE: + return CoreIonicColorNames.INFO; + + case AddonModQuizAttemptStates.FINISHED: + return CoreIonicColorNames.SUCCESS; + + case AddonModQuizAttemptStates.ABANDONED: + return CoreIonicColorNames.DANGER; + + default: + return ''; + } + } + /** * Get cache key for get attempt review WS calls. * diff --git a/src/core/features/grades/lang.json b/src/core/features/grades/lang.json index ab8013e76..c24a0fdb4 100644 --- a/src/core/features/grades/lang.json +++ b/src/core/features/grades/lang.json @@ -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", diff --git a/src/core/lang.json b/src/core/lang.json index dc987b022..99e49b466 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -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.",