From 962cd43d9e842ff177cf7cbf07060e982df77ac4 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 5 May 2021 16:12:28 +0200 Subject: [PATCH 1/5] MOBILE-3757 course: Display activity dates in course page --- .../course/components/format/core-course-format.html | 3 ++- .../course/components/module/core-course-module.html | 11 ++++++++--- .../features/course/components/module/module.scss | 8 ++++---- src/core/features/course/components/module/module.ts | 9 ++++++++- src/core/features/course/services/course.ts | 6 ++++++ src/core/features/courses/services/courses.ts | 6 ++++++ 6 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/core/features/course/components/format/core-course-format.html b/src/core/features/course/components/format/core-course-format.html index 8f7e56516..0200201de 100644 --- a/src/core/features/course/components/format/core-course-format.html +++ b/src/core/features/course/components/format/core-course-format.html @@ -146,7 +146,8 @@ + (statusChanged)="onModuleStatusChange()" [showActivityDates]="course?.showactivitydates" + [showCompletionConditions]="course?.showcompletionconditions"> diff --git a/src/core/features/course/components/module/core-course-module.html b/src/core/features/course/components/module/core-course-module.html index 453f5098c..9d19cb47c 100644 --- a/src/core/features/course/components/module/core-course-module.html +++ b/src/core/features/course/components/module/core-course-module.html @@ -6,7 +6,7 @@ (click)="moduleClicked($event)" [attr.aria-label]="module.handlerData.a11yTitle" [ngClass]="{ - 'has-module-description': module.description, + 'has-module-info': hasInfo, 'item-media': module.handlerData.icon, 'item-dimmed': module.visible === 0 || module.uservisible === false }" @@ -74,9 +74,9 @@ +
+

+ {{ date.label }} {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }} +

+
diff --git a/src/core/features/course/components/module/module.scss b/src/core/features/course/components/module/module.scss index 017a6d617..369c22a54 100644 --- a/src/core/features/course/components/module/module.scss +++ b/src/core/features/course/components/module/module.scss @@ -31,7 +31,7 @@ } } - .core-module-module-description { + .core-module-module-info { ion-badge { text-align: start; } @@ -51,15 +51,15 @@ clear: both; } - .core-module-main-item + .core-module-module-description ion-label { + .core-module-main-item + .core-module-module-info ion-label { margin-top: 0px; } - .core-module-main-item.has-module-description { + .core-module-main-item.has-module-info { --inner-border-width: 0; } - .core-module-module-description ion-label { + .core-module-module-info ion-label { margin-inline-start: 50px; } diff --git a/src/core/features/course/components/module/module.ts b/src/core/features/course/components/module/module.ts index abe5646e4..9a19926bc 100644 --- a/src/core/features/course/components/module/module.ts +++ b/src/core/features/course/components/module/module.ts @@ -47,6 +47,8 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { @Input() module!: CoreCourseModule; // The module to render. @Input() courseId?: number; // The course the module belongs to. @Input() section?: CoreCourseSection; // The section the module belongs to. + @Input() showActivityDates = false; // Whether to show activity dates. + @Input() showCompletionConditions = false; // Whether to show activity completion conditions. // eslint-disable-next-line @angular-eslint/no-input-rename @Input('downloadEnabled') set enabled(value: boolean) { this.downloadEnabled = value; @@ -61,7 +63,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { // Get current status to decide which icon should be shown. this.calculateAndShowStatus(); - }; + } @Output() completionChanged = new EventEmitter(); // Notify when module completion changes. @Output() statusChanged = new EventEmitter(); // Notify when the download status changes. @@ -71,6 +73,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { spinner?: boolean; // Whether to display a loading spinner. downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. modNameTranslated = ''; + hasInfo = false; protected prefetchHandler?: CoreCourseModulePrefetchHandler; protected statusObserver?: CoreEventObserver; @@ -89,6 +92,10 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { } this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title; + this.hasInfo = !!( + this.module.description || + (this.showActivityDates && this.module.dates && this.module.dates.length) + ); if (this.module.handlerData.showDownloadButton) { // Listen for changes on this module status, even if download isn't enabled. diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 4be90a405..f0548ec0e 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -1262,6 +1262,8 @@ export type CoreCourseSummary = { timeaccess?: number; // @since 3.6. Timeaccess. showshortname: boolean; // @since 3.6. Showshortname. coursecategory: string; // @since 3.7. Coursecategory. + showactivitydates: boolean | null; // @since 3.11. Whether the activity dates are shown or not. + showcompletionconditions: boolean | null; // @since 3.11. Whether the activity completion conditions are shown or not. }; /** @@ -1442,6 +1444,10 @@ export type CoreCourseWSModule = { completion?: number; // Type of completion tracking: 0 means none, 1 manual, 2 automatic. completiondata?: CoreCourseModuleWSCompletionData; // Module completion data. contents: CoreCourseModuleContentFile[]; + dates?: { + label: string; + timestamp: number; + }[]; // @since 3.11. Activity dates. contentsinfo?: { // Contents summary information. filescount: number; // Total number of files. filessize: number; // Total files size. diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index acf54ef00..b0a2df9d0 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -1264,6 +1264,8 @@ export type CoreEnrolledCourseData = CoreEnrolledCourseBasicData & { isfavourite?: boolean; // If the user marked this course a favourite. hidden?: boolean; // If the user hide the course from the dashboard. overviewfiles?: CoreWSExternalFile[]; + showactivitydates?: boolean; // @since 3.11. Whether the activity dates are shown or not. + showcompletionconditions?: boolean; // @since 3.11. Whether the activity completion conditions are shown or not. }; /** @@ -1281,6 +1283,8 @@ export type CoreCourseBasicSearchedData = CoreCourseBasicData & { }[]; enrollmentmethods: string[]; // Enrollment methods list. customfields?: CoreCourseCustomField[]; // Custom fields and associated values. + showactivitydates?: boolean; // @since 3.11. Whether the activity dates are shown or not. + showcompletionconditions?: boolean; // @since 3.11. Whether the activity completion conditions are shown or not. }; export type CoreCourseSearchedData = CoreCourseBasicSearchedData & { @@ -1342,6 +1346,8 @@ export type CoreCourseGetCoursesData = CoreEnrolledCourseBasicData & { forcetheme?: string; // Name of the force theme. courseformatoptions?: CoreCourseFormatOption[]; // Additional options for particular course format. customfields?: CoreCourseCustomField[]; // Custom fields and associated values. + showactivitydates?: boolean; // @since 3.11. Whether the activity dates are shown or not. + showcompletionconditions?: boolean; // @since 3.11. Whether the activity completion conditions are shown or not. }; /** From 3f825db799f5eac0333794e1c9be0fd8dc37073c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 6 May 2021 12:35:32 +0200 Subject: [PATCH 2/5] MOBILE-3757 course: Display completion in course page --- .../mod/label/services/handlers/module.ts | 7 + src/core/classes/site.ts | 6 +- .../course/classes/module-completion.ts | 93 +++++++++ .../course/components/components.module.ts | 3 + .../core-course-module-completion-legacy.html | 9 + .../module-completion-legacy.scss | 20 ++ .../module-completion-legacy.ts | 118 ++++++++++++ .../core-course-module-completion.html | 60 +++++- .../module-completion/module-completion.scss | 26 ++- .../module-completion/module-completion.ts | 179 +++++------------- .../components/module/core-course-module.html | 19 +- .../course/components/module/module.ts | 12 +- src/core/features/course/lang.json | 12 ++ .../course/pages/contents/contents.ts | 3 + src/core/features/course/services/course.ts | 33 +++- .../course/services/module-delegate.ts | 20 ++ .../classes/handlers/module-handler.ts | 16 ++ .../siteplugins/services/siteplugins.ts | 1 + 18 files changed, 472 insertions(+), 165 deletions(-) create mode 100644 src/core/features/course/classes/module-completion.ts create mode 100644 src/core/features/course/components/module-completion-legacy/core-course-module-completion-legacy.html create mode 100644 src/core/features/course/components/module-completion-legacy/module-completion-legacy.scss create mode 100644 src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts diff --git a/src/addons/mod/label/services/handlers/module.ts b/src/addons/mod/label/services/handlers/module.ts index 1bd7161e6..e93f13b26 100644 --- a/src/addons/mod/label/services/handlers/module.ts +++ b/src/addons/mod/label/services/handlers/module.ts @@ -71,5 +71,12 @@ export class AddonModLabelModuleHandlerService implements CoreCourseModuleHandle return; } + /** + * @inheritdoc + */ + manualCompletionAlwaysShown(): boolean { + return true; + } + } export const AddonModLabelModuleHandler = makeSingleton(AddonModLabelModuleHandlerService); diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index 7fe336b26..2f86521f9 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -1826,15 +1826,15 @@ export class CoreSite { * @return Object with major and minor. Returns false if invalid version. */ protected getMajorAndMinor(version: string): {major: string; minor: number} | false { - const match = version.match(/(\d)+(?:\.(\d)+)?(?:\.(\d)+)?/); + const match = version.match(/^(\d+)(\.(\d+)(\.\d+)?)?/); if (!match || !match[1]) { // Invalid version. return false; } return { - major: match[1] + '.' + (match[2] || '0'), - minor: parseInt(match[3], 10) || 0, + major: match[1] + '.' + (match[3] || '0'), + minor: parseInt(match[5], 10) || 0, }; } diff --git a/src/core/features/course/classes/module-completion.ts b/src/core/features/course/classes/module-completion.ts new file mode 100644 index 000000000..c78de3695 --- /dev/null +++ b/src/core/features/course/classes/module-completion.ts @@ -0,0 +1,93 @@ +// (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, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; + +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; + +/** + * Base class for completion components. + */ +@Component({ + template: '', +}) +export class CoreCourseModuleCompletionBaseComponent implements OnChanges { + + @Input() completion?: CoreCourseModuleCompletionData; // The completion status. + @Input() moduleId?: number; // The name of the module this completion affects. + @Input() moduleName?: string; // The name of the module this completion affects. + @Output() completionChanged = new EventEmitter(); // Notify when completion changes. + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: { [name: string]: SimpleChange }): void { + if (changes.completion && this.completion) { + this.calculateData(); + } + } + + /** + * Calculate data to render the completion. + */ + protected calculateData(): void { + return; + } + + /** + * Completion clicked. + * + * @param e The click event. + */ + async completionClicked(e: Event): Promise { + if (!this.completion) { + return; + } + + if (typeof this.completion.cmid == 'undefined' || this.completion.tracking !== 1) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + const modal = await CoreDomUtils.showModalLoading(); + this.completion.state = this.completion.state === 1 ? 0 : 1; + + try { + const response = await CoreCourse.markCompletedManually( + this.completion.cmid, + this.completion.state === 1, + this.completion.courseId!, + this.completion.courseName, + ); + + if (this.completion.valueused === false) { + this.calculateData(); + if (response.offline) { + this.completion.offline = true; + } + } + this.completionChanged.emit(this.completion); + } catch (error) { + this.completion.state = this.completion.state === 1 ? 0 : 1; + CoreDomUtils.showErrorModalDefault(error, 'core.errorchangecompletion', true); + } finally { + modal.dismiss(); + } + } + +} diff --git a/src/core/features/course/components/components.module.ts b/src/core/features/course/components/components.module.ts index e8fa6f23d..29e90f3cc 100644 --- a/src/core/features/course/components/components.module.ts +++ b/src/core/features/course/components/components.module.ts @@ -23,12 +23,14 @@ import { CoreCourseModuleDescriptionComponent } from './module-description/modul import { CoreCourseSectionSelectorComponent } from './section-selector/section-selector'; import { CoreCourseTagAreaComponent } from './tag-area/tag-area'; import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module'; +import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; @NgModule({ declarations: [ CoreCourseFormatComponent, CoreCourseModuleComponent, CoreCourseModuleCompletionComponent, + CoreCourseModuleCompletionLegacyComponent, CoreCourseModuleDescriptionComponent, CoreCourseSectionSelectorComponent, CoreCourseTagAreaComponent, @@ -42,6 +44,7 @@ import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsup CoreCourseFormatComponent, CoreCourseModuleComponent, CoreCourseModuleCompletionComponent, + CoreCourseModuleCompletionLegacyComponent, CoreCourseModuleDescriptionComponent, CoreCourseSectionSelectorComponent, CoreCourseTagAreaComponent, diff --git a/src/core/features/course/components/module-completion-legacy/core-course-module-completion-legacy.html b/src/core/features/course/components/module-completion-legacy/core-course-module-completion-legacy.html new file mode 100644 index 000000000..32a0dacae --- /dev/null +++ b/src/core/features/course/components/module-completion-legacy/core-course-module-completion-legacy.html @@ -0,0 +1,9 @@ + + + + + diff --git a/src/core/features/course/components/module-completion-legacy/module-completion-legacy.scss b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.scss new file mode 100644 index 000000000..a0fd0020d --- /dev/null +++ b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.scss @@ -0,0 +1,20 @@ +:host { + min-width: var(--a11y-min-target-size); + min-height: var(--a11y-min-target-size); + --size: 30px; + + img { + padding: 5px; + width: var(--size); + vertical-align: middle; + max-width: none; + margin: 7px; + } + + ion-button { + --padding-top: 0; + --padding-start: 0; + --padding-end: 0; + --padding-bottom: 0; + } +} diff --git a/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts new file mode 100644 index 000000000..a44dced24 --- /dev/null +++ b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts @@ -0,0 +1,118 @@ +// (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 } from '@angular/core'; + +import { CoreUser } from '@features/user/services/user'; +import { CoreCourseProvider } from '@features/course/services/course'; +import { CoreFilterHelper } from '@features/filter/services/filter-helper'; +import { Translate } from '@singletons'; +import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion'; + +/** + * Component to handle activity completion in sites previous to 3.11. + * It shows a checkbox with the current status, and allows manually changing the completion if it's allowed. + * + * Example usage: + * + * + */ +@Component({ + selector: 'core-course-module-completion-legacy', + templateUrl: 'core-course-module-completion-legacy.html', + styleUrls: ['module-completion-legacy.scss'], +}) +export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent { + + completionImage?: string; + completionDescription?: string; + + /** + * @inheritdoc + */ + protected async calculateData(): Promise { + if (!this.completion) { + return; + } + + const moduleName = this.moduleName || ''; + let langKey: string | undefined; + let image: string | undefined; + + if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL && + this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) { + image = 'completion-manual-n'; + langKey = 'core.completion-alt-manual-n'; + } else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL && + this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) { + image = 'completion-manual-y'; + langKey = 'core.completion-alt-manual-y'; + } else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC && + this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) { + image = 'completion-auto-n'; + langKey = 'core.completion-alt-auto-n'; + } else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC && + this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) { + image = 'completion-auto-y'; + langKey = 'core.completion-alt-auto-y'; + } else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC && + this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_PASS) { + image = 'completion-auto-pass'; + langKey = 'core.completion-alt-auto-pass'; + } else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC && + this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_FAIL) { + image = 'completion-auto-fail'; + langKey = 'core.completion-alt-auto-fail'; + } + + if (image) { + if (this.completion.overrideby && this.completion.overrideby > 0) { + image += '-override'; + } + this.completionImage = 'assets/img/completion/' + image + '.svg'; + } + + if (!moduleName || !this.moduleId || !langKey) { + return; + } + + const result = await CoreFilterHelper.getFiltersAndFormatText( + moduleName, + 'module', + this.moduleId, + { clean: true, singleLine: true, shortenLength: 50, courseId: this.completion.courseId }, + ); + + let translateParams: Record = { + $a: result.text, + }; + + if (this.completion.overrideby && this.completion.overrideby > 0) { + langKey += '-override'; + + const profile = await CoreUser.getProfile(this.completion.overrideby, this.completion.courseId, true); + + translateParams = { + $a: { + overrideuser: profile.fullname, + modname: result.text, + }, + }; + } + + this.completionDescription = Translate.instant(langKey, translateParams); + } + +} diff --git a/src/core/features/course/components/module-completion/core-course-module-completion.html b/src/core/features/course/components/module-completion/core-course-module-completion.html index 32a0dacae..4951f341d 100644 --- a/src/core/features/course/components/module-completion/core-course-module-completion.html +++ b/src/core/features/course/components/module-completion/core-course-module-completion.html @@ -1,9 +1,53 @@ - +
- - - + + + + {{ 'core.course.completion_automatic:done' | translate }} {{ rule.rulevalue.description }} + + + + {{ 'core.course.completion_automatic:failed' | translate }} {{ rule.rulevalue.description }} + + + + {{ 'core.course.completion_automatic:todo' | translate }} {{ rule.rulevalue.description }} + + + + + + + {{ rule.rulevalue.description }} + + +
+ +
+ + + + + + {{ 'core.course.completion_manual:done' | translate }} + + + + + {{ 'core.course.completion_manual:markdone' | translate }} + + + + + + + {{ 'core.course.completion_manual:markdone' | translate }} + + +
diff --git a/src/core/features/course/components/module-completion/module-completion.scss b/src/core/features/course/components/module-completion/module-completion.scss index a0fd0020d..eeded5aaa 100644 --- a/src/core/features/course/components/module-completion/module-completion.scss +++ b/src/core/features/course/components/module-completion/module-completion.scss @@ -1,20 +1,18 @@ :host { - min-width: var(--a11y-min-target-size); - min-height: var(--a11y-min-target-size); - --size: 30px; + .core-module-automatic-completion-conditions { + ion-badge { + font-weight: normal; + margin-right: 5px; - img { - padding: 5px; - width: var(--size); - vertical-align: middle; - max-width: none; - margin: 7px; + &[color="medium"] { + color: black; + } + } } - ion-button { - --padding-top: 0; - --padding-start: 0; - --padding-end: 0; - --padding-bottom: 0; + .core-module-manual-completion { + ion-button { + text-transform: none; + } } } diff --git a/src/core/features/course/components/module-completion/module-completion.ts b/src/core/features/course/components/module-completion/module-completion.ts index 4646dc0dc..4fa8ffdcb 100644 --- a/src/core/features/course/components/module-completion/module-completion.ts +++ b/src/core/features/course/components/module-completion/module-completion.ts @@ -12,13 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; +import { Component, Input } from '@angular/core'; -import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUser } from '@features/user/services/user'; -import { CoreCourse, CoreCourseProvider } from '@features/course/services/course'; -import { CoreFilterHelper } from '@features/filter/services/filter-helper'; -import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; +import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion'; +import { CoreCourseModuleWSRuleDetails, CoreCourseProvider } from '@features/course/services/course'; import { Translate } from '@singletons'; /** @@ -35,142 +32,66 @@ import { Translate } from '@singletons'; templateUrl: 'core-course-module-completion.html', styleUrls: ['module-completion.scss'], }) -export class CoreCourseModuleCompletionComponent implements OnChanges { +export class CoreCourseModuleCompletionComponent extends CoreCourseModuleCompletionBaseComponent { - @Input() completion?: CoreCourseModuleCompletionData; // The completion status. - @Input() moduleId?: number; // The name of the module this completion affects. - @Input() moduleName?: string; // The name of the module this completion affects. - @Output() completionChanged = new EventEmitter(); // Notify when completion changes. + @Input() showCompletionConditions = false; // Whether to show activity completion conditions. + @Input() showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled. - completionImage?: string; - completionDescription?: string; + details?: CompletionRule[]; + accessibleDescription: string | null = null; /** - * Detect changes on input properties. + * @inheritdoc */ - ngOnChanges(changes: { [name: string]: SimpleChange }): void { - if (changes.completion && this.completion) { - this.showStatus(); - } - } - - /** - * Completion clicked. - * - * @param e The click event. - */ - async completionClicked(e: Event): Promise { - if (!this.completion) { + protected calculateData(): void { + if (!this.completion?.details) { return; } - if (typeof this.completion.cmid == 'undefined' || this.completion.tracking !== 1) { - return; - } - - e.preventDefault(); - e.stopPropagation(); - - const modal = await CoreDomUtils.showModalLoading(); - this.completion.state = this.completion.state === 1 ? 0 : 1; - - try { - const response = await CoreCourse.markCompletedManually( - this.completion.cmid, - this.completion.state === 1, - this.completion.courseId!, - this.completion.courseName, - ); - - if (this.completion.valueused === false) { - this.showStatus(); - if (response.offline) { - this.completion.offline = true; - } - } - this.completionChanged.emit(this.completion); - } catch (error) { - this.completion.state = this.completion.state === 1 ? 0 : 1; - CoreDomUtils.showErrorModalDefault(error, 'core.errorchangecompletion', true); - } finally { - modal.dismiss(); - } - } - - /** - * Set image and description to show as completion icon. - */ - protected async showStatus(): Promise { - if (!this.completion) { - return; - } - - const moduleName = this.moduleName || ''; - let langKey: string | undefined; - let image: string | undefined; - - if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL && - this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) { - image = 'completion-manual-n'; - langKey = 'core.completion-alt-manual-n'; - } else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL && - this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) { - image = 'completion-manual-y'; - langKey = 'core.completion-alt-manual-y'; - } else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC && - this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) { - image = 'completion-auto-n'; - langKey = 'core.completion-alt-auto-n'; - } else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC && - this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) { - image = 'completion-auto-y'; - langKey = 'core.completion-alt-auto-y'; - } else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC && - this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_PASS) { - image = 'completion-auto-pass'; - langKey = 'core.completion-alt-auto-pass'; - } else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC && - this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_FAIL) { - image = 'completion-auto-fail'; - langKey = 'core.completion-alt-auto-fail'; - } - - if (image) { - if (this.completion.overrideby > 0) { - image += '-override'; - } - this.completionImage = 'assets/img/completion/' + image + '.svg'; - } - - if (!moduleName || !this.moduleId || !langKey) { - return; - } - - const result = await CoreFilterHelper.getFiltersAndFormatText( - moduleName, - 'module', - this.moduleId, - { clean: true, singleLine: true, shortenLength: 50, courseId: this.completion.courseId }, - ); - - let translateParams: Record = { - $a: result.text, - }; - - if (this.completion.overrideby > 0) { - langKey += '-override'; - - const profile = await CoreUser.getProfile(this.completion.overrideby, this.completion.courseId, true); - - translateParams = { + // Set an accessible description for manual completions with overridden completion state. + if (!this.completion.isautomatic && this.completion.overrideby) { + const setByData = { $a: { - overrideuser: profile.fullname, - modname: result.text, + activityname: this.moduleName, + setby: this.completion.overrideby, }, }; + const setByLangKey = this.completion.state ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone'; + this.accessibleDescription = Translate.instant('core.course.' + setByLangKey, setByData); + } else { + const langKey = this.completion.state ? 'completion_manual:aria:done' : 'completion_manual:aria:markdone'; + this.accessibleDescription = Translate.instant('core.course.' + langKey, { $a: this.moduleName }); } - this.completionDescription = Translate.instant(langKey, translateParams); + // Format rules. + this.details = this.completion.details.map((rule: CompletionRule) => { + rule.statuscomplete = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE || + rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_PASS; + rule.statuscompletefail = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_FAIL; + rule.statusincomplete = rule.rulevalue.status == CoreCourseProvider.COMPLETION_INCOMPLETE; + rule.accessibleDescription = null; + + if (this.completion!.overrideby) { + const setByData = { + $a: { + condition: rule.rulevalue.description, + setby: this.completion!.overrideby, + }, + }; + const overrideStatus = rule.statuscomplete ? 'done' : 'todo'; + + rule.accessibleDescription = Translate.instant('core.course.completion_setby:auto:' + overrideStatus, setByData); + } + + return rule; + }); } } + +type CompletionRule = CoreCourseModuleWSRuleDetails & { + statuscomplete?: boolean; + statuscompletefail?: boolean; + statusincomplete?: boolean; + accessibleDescription?: string | null; +}; diff --git a/src/core/features/course/components/module/core-course-module.html b/src/core/features/course/components/module/core-course-module.html index 9d19cb47c..4035c3865 100644 --- a/src/core/features/course/components/module/core-course-module.html +++ b/src/core/features/course/components/module/core-course-module.html @@ -50,12 +50,13 @@ slot="end" *ngIf="module.uservisible !== false" class="buttons core-module-buttons" - [ngClass]="{'core-button-completion': module.completiondata}" + [ngClass]="{'core-button-completion': module.completiondata && showLegacyCompletion}" > - - - + + +
+

{{ date.label }} {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}

+ + + + + diff --git a/src/core/features/course/components/module/module.ts b/src/core/features/course/components/module/module.ts index 9a19926bc..8c7201bd9 100644 --- a/src/core/features/course/components/module/module.ts +++ b/src/core/features/course/components/module/module.ts @@ -24,7 +24,7 @@ import { CoreCourseSection, } from '@features/course/services/course-helper'; import { CoreCourse } from '@features/course/services/course'; -import { CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate'; +import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate'; import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler, @@ -48,7 +48,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { @Input() courseId?: number; // The course the module belongs to. @Input() section?: CoreCourseSection; // The section the module belongs to. @Input() showActivityDates = false; // Whether to show activity dates. - @Input() showCompletionConditions = false; // Whether to show activity completion conditions. + @Input() showCompletionConditions = false; // Whether to show activity completion conditions. // eslint-disable-next-line @angular-eslint/no-input-rename @Input('downloadEnabled') set enabled(value: boolean) { this.downloadEnabled = value; @@ -74,6 +74,8 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. modNameTranslated = ''; hasInfo = false; + showLegacyCompletion = false; // Whether to show module completion in the old format. + showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled. protected prefetchHandler?: CoreCourseModulePrefetchHandler; protected statusObserver?: CoreEventObserver; @@ -86,6 +88,9 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { ngOnInit(): void { this.courseId = this.courseId || this.module.course; this.modNameTranslated = CoreCourse.translateModuleName(this.module.modname) || ''; + this.showLegacyCompletion = !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.11'); + this.showManualCompletion = + this.showCompletionConditions || CoreCourseModuleDelegate.manualCompletionAlwaysShown(this.module); if (!this.module.handlerData) { return; @@ -94,7 +99,8 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title; this.hasInfo = !!( this.module.description || - (this.showActivityDates && this.module.dates && this.module.dates.length) + (this.showActivityDates && this.module.dates && this.module.dates.length) || + this.module.completiondata ); if (this.module.handlerData.showDownloadButton) { diff --git a/src/core/features/course/lang.json b/src/core/features/course/lang.json index a01cbcf86..54dac5574 100644 --- a/src/core/features/course/lang.json +++ b/src/core/features/course/lang.json @@ -6,6 +6,18 @@ "askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.", "availablespace": " You currently have about {{available}} free space.", "cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.", + "completion_automatic:done": "Done:", + "completion_automatic:failed": "Failed:", + "completion_automatic:todo": "To do:", + "completion_manual:aria:done": "{{$a}} is marked as done. Press to undo.", + "completion_manual:aria:markdone": "Mark {{$a}} as done", + "completion_manual:done": "Done", + "completion_manual:markdone": "Mark as done", + "completion_setby:auto:done": "Done: {{$a.condition}} (set by {{$a.setby}})", + "completion_setby:auto:todo": "To do: {{$a.condition}} (set by {{$a.setby}})", + "completion_setby:manual:done": "{{$a.activityname}} is marked by {{$a.setby}} as done. Press to undo.", + "completion_setby:manual:markdone": "{{$a.activityname}} is marked by {{$a.setby}} as not done. Press to mark as done.", + "completionrequirements": "Completion requirements for {{$a}}", "confirmdeletemodulefiles": "Are you sure you want to delete these files?", "confirmdeletestoreddata": "Are you sure you want to delete the stored data?", "confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?", diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index e09f8b038..9a9c2fb75 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -353,6 +353,9 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { const shouldReload = typeof completionData.valueused == 'undefined' || completionData.valueused; if (!shouldReload) { + // Invalidate the completion. + await CoreUtils.ignoreErrors(CoreCourse.invalidateSections(this.course.id)); + return; } diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index f0548ec0e..2c67cb4f5 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -1296,14 +1296,25 @@ export type CoreCourseCompletionActivityStatusWSResponse = { * Activity status. */ export type CoreCourseCompletionActivityStatus = { - cmid: number; // Comment ID. + cmid: number; // Course module ID. modname: string; // Activity module name. instance: number; // Instance ID. state: number; // Completion state value: 0 means incomplete, 1 complete, 2 complete pass, 3 complete fail. timecompleted: number; // Timestamp for completed activity. tracking: number; // Type of tracking: 0 means none, 1 manual, 2 automatic. - overrideby?: number; // The user id who has overriden the status, or null. + overrideby?: number | null; // The user id who has overriden the status, or null. valueused?: boolean; // Whether the completion status affects the availability of another activity. + hascompletion?: boolean; // @since 3.11. Whether this activity module has completion enabled. + isautomatic?: boolean; // @since 3.11. Whether this activity module instance tracks completion automatically. + istrackeduser?: boolean; // @since 3.11. Whether completion is being tracked for this user. + uservisible?: boolean; // @since 3.11. Whether this activity is visible to the user. + details?: { // @since 3.11. An array of completion details containing the description and status. + rulename: string; // Rule name. + rulevalue: { + status: number; // Completion status. + description: string; // Completion description. + }; + }[]; offline?: boolean; // Whether the completions is offline and not yet synced. }; @@ -1463,8 +1474,24 @@ export type CoreCourseWSModule = { export type CoreCourseModuleWSCompletionData = { state: number; // Completion state value: 0 means incomplete, 1 complete, 2 complete pass, 3 complete fail. timecompleted: number; // Timestamp for completion status. - overrideby: number; // The user id who has overriden the status. + overrideby: number | null; // The user id who has overriden the status. valueused?: boolean; // Whether the completion status affects the availability of another activity. + hascompletion?: boolean; // @since 3.11. Whether this activity module has completion enabled. + isautomatic?: boolean; // @since 3.11. Whether this activity module instance tracks completion automatically. + istrackeduser?: boolean; // @since 3.11. Whether completion is being tracked for this user. + uservisible?: boolean; // @since 3.11. Whether this activity is visible to the user. + details?: CoreCourseModuleWSRuleDetails[]; // @since 3.11. An array of completion details. +}; + +/** + * Module completion rule details. + */ +export type CoreCourseModuleWSRuleDetails = { + rulename: string; // Rule name. + rulevalue: { + status: number; // Completion status. + description: string; // Completion description. + }; }; export type CoreCourseModuleContentFile = { diff --git a/src/core/features/course/services/module-delegate.ts b/src/core/features/course/services/module-delegate.ts index 547ff1c1a..0930be852 100644 --- a/src/core/features/course/services/module-delegate.ts +++ b/src/core/features/course/services/module-delegate.ts @@ -92,6 +92,15 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler { * @return The result of the supports check. */ supportsFeature?(feature: string): unknown; + + /** + * Return true to show the manual completion regardless of the course's showcompletionconditions setting. + * Returns false by default. + * + * @param module Module. + * @return Whether the manual completion should always be displayed. + */ + manualCompletionAlwaysShown?(module: CoreCourseModule): boolean; } /** @@ -366,6 +375,17 @@ export class CoreCourseModuleDelegateService extends CoreDelegate(module.modname, 'manualCompletionAlwaysShown', [module]); + } + } export const CoreCourseModuleDelegate = makeSingleton(CoreCourseModuleDelegateService); diff --git a/src/core/features/siteplugins/classes/handlers/module-handler.ts b/src/core/features/siteplugins/classes/handlers/module-handler.ts index 11009129f..20f2434bf 100644 --- a/src/core/features/siteplugins/classes/handlers/module-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/module-handler.ts @@ -165,4 +165,20 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp return CoreSitePluginsModuleIndexComponent; } + /** + * @inheritdoc + */ + manualCompletionAlwaysShown(module: CoreCourseModule): boolean { + if (this.handlerSchema.manualcompletionalwaysshown !== undefined) { + return this.handlerSchema.manualcompletionalwaysshown; + } + + if (this.initResult?.jsResult && this.initResult.jsResult.manualCompletionAlwaysShown) { + // The init result defines a function to check if a feature is supported, use it. + return this.initResult.jsResult.manualCompletionAlwaysShown(module); + } + + return false; + } + } diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts index 892b9de56..f4903cb16 100644 --- a/src/core/features/siteplugins/services/siteplugins.ts +++ b/src/core/features/siteplugins/services/siteplugins.ts @@ -848,6 +848,7 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo coursepagemethod?: string; ptrenabled?: boolean; supportedfeatures?: Record; + manualcompletionalwaysshown?: boolean; }; /** From 61a908216d31b2956ecb122752ed2940bf7ef06a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 7 May 2021 10:09:31 +0200 Subject: [PATCH 3/5] MOBILE-3757 course: Display dates and completion inside activity --- .../index/addon-mod-assign-index.html | 6 +- .../mod/assign/components/index/index.ts | 2 +- .../addon-mod-assign-submission.html | 4 +- .../components/submission/submission.ts | 2 + .../index/addon-mod-book-index.html | 5 + .../index/addon-mod-chat-index.html | 5 + .../index/addon-mod-choice-index.html | 5 + .../index/addon-mod-data-index.html | 5 + .../index/addon-mod-feedback-index.html | 5 + .../index/addon-mod-folder-index.html | 5 + .../mod/forum/components/index/index.html | 5 + .../index/addon-mod-glossary-index.html | 5 + .../index/addon-mod-h5pactivity-index.html | 5 + .../index/addon-mod-imscp-index.html | 5 + .../index/addon-mod-lesson-index.html | 6 + .../components/index/addon-mod-lti-index.html | 6 + .../index/addon-mod-page-index.html | 5 + .../index/addon-mod-quiz-index.html | 6 + .../index/addon-mod-resource-index.html | 5 + .../index/addon-mod-scorm-index.html | 5 + .../index/addon-mod-survey-index.html | 5 + .../components/index/addon-mod-url-index.html | 5 + .../index/addon-mod-wiki-index.html | 6 + .../index/addon-mod-workshop-index.html | 6 + .../course/classes/main-activity-component.ts | 9 +- .../course/classes/main-resource-component.ts | 34 +++++- .../course/classes/module-completion.ts | 47 +------- .../course/components/components.module.ts | 6 + .../module-completion-legacy.ts | 18 +++ .../core-course-module-completion.html | 27 +---- .../module-completion/module-completion.scss | 6 - .../module-completion/module-completion.ts | 17 +-- .../module-info/core-course-module-info.html | 18 +++ .../components/module-info/module-info.ts | 31 +++++ .../core-course-module-manual-completion.html | 23 ++++ .../module-manual-completion.scss | 5 + .../module-manual-completion.ts | 109 ++++++++++++++++++ .../features/course/services/course-helper.ts | 47 ++++++++ src/core/features/course/services/course.ts | 10 +- src/core/singletons/events.ts | 15 ++- 40 files changed, 439 insertions(+), 102 deletions(-) create mode 100644 src/core/features/course/components/module-info/core-course-module-info.html create mode 100644 src/core/features/course/components/module-info/module-info.ts create mode 100644 src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html create mode 100644 src/core/features/course/components/module-manual-completion/module-manual-completion.scss create mode 100644 src/core/features/course/components/module-manual-completion/module-manual-completion.ts diff --git a/src/addons/mod/assign/components/index/addon-mod-assign-index.html b/src/addons/mod/assign/components/index/addon-mod-assign-index.html index da491f51d..cff439ab9 100644 --- a/src/addons/mod/assign/components/index/addon-mod-assign-index.html +++ b/src/addons/mod/assign/components/index/addon-mod-assign-index.html @@ -30,6 +30,11 @@ + + + + @@ -141,5 +146,4 @@ - diff --git a/src/addons/mod/assign/components/index/index.ts b/src/addons/mod/assign/components/index/index.ts index 86da7c0c7..83b84b236 100644 --- a/src/addons/mod/assign/components/index/index.ts +++ b/src/addons/mod/assign/components/index/index.ts @@ -120,7 +120,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, (data) => { if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { - // Assignment submitted, check completion. + // Assignment submitted, check completion. CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); // Reload data since it can have offline data now. diff --git a/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html b/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html index 2d540a627..dd8f77e82 100644 --- a/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html +++ b/src/addons/mod/assign/components/submission/addon-mod-assign-submission.html @@ -54,7 +54,7 @@
- +

@@ -66,7 +66,7 @@ - +

{{ 'addon.mod_assign.duedate' | translate }}

{{ assign!.duedate * 1000 | coreFormatDate }}

diff --git a/src/addons/mod/assign/components/submission/submission.ts b/src/addons/mod/assign/components/submission/submission.ts index e98b0c5a8..75e78f0d7 100644 --- a/src/addons/mod/assign/components/submission/submission.ts +++ b/src/addons/mod/assign/components/submission/submission.ts @@ -122,6 +122,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can allowAddAttempt = false; // Allow adding a new attempt when grading. gradeUrl?: string; // URL to grade in browser. isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission. + showDates = false; // Whether to show some dates. // Some constants. statusNew = AddonModAssignProvider.SUBMISSION_STATUS_NEW; @@ -181,6 +182,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can */ ngOnInit(): void { this.isSubmittedForGrading = !!this.submitId; + this.showDates = !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.11'); this.loadData(true); } diff --git a/src/addons/mod/book/components/index/addon-mod-book-index.html b/src/addons/mod/book/components/index/addon-mod-book-index.html index 83c7ae129..263711c8b 100644 --- a/src/addons/mod/book/components/index/addon-mod-book-index.html +++ b/src/addons/mod/book/components/index/addon-mod-book-index.html @@ -23,6 +23,11 @@ + + + + diff --git a/src/addons/mod/chat/components/index/addon-mod-chat-index.html b/src/addons/mod/chat/components/index/addon-mod-chat-index.html index 8300e280a..211421e4e 100644 --- a/src/addons/mod/chat/components/index/addon-mod-chat-index.html +++ b/src/addons/mod/chat/components/index/addon-mod-chat-index.html @@ -26,6 +26,11 @@ + + + + diff --git a/src/addons/mod/choice/components/index/addon-mod-choice-index.html b/src/addons/mod/choice/components/index/addon-mod-choice-index.html index fa86bf6d2..b8df0763b 100644 --- a/src/addons/mod/choice/components/index/addon-mod-choice-index.html +++ b/src/addons/mod/choice/components/index/addon-mod-choice-index.html @@ -28,6 +28,11 @@ + + + + diff --git a/src/addons/mod/data/components/index/addon-mod-data-index.html b/src/addons/mod/data/components/index/addon-mod-data-index.html index 1e94a2ca6..c42ea2560 100644 --- a/src/addons/mod/data/components/index/addon-mod-data-index.html +++ b/src/addons/mod/data/components/index/addon-mod-data-index.html @@ -39,6 +39,11 @@ + + + + diff --git a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html index 2429222ee..b7a1109f7 100644 --- a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html +++ b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html @@ -28,6 +28,11 @@ + + + + diff --git a/src/addons/mod/folder/components/index/addon-mod-folder-index.html b/src/addons/mod/folder/components/index/addon-mod-folder-index.html index 46a73cbd4..29460ae3d 100644 --- a/src/addons/mod/folder/components/index/addon-mod-folder-index.html +++ b/src/addons/mod/folder/components/index/addon-mod-folder-index.html @@ -25,6 +25,11 @@ + + + + diff --git a/src/addons/mod/forum/components/index/index.html b/src/addons/mod/forum/components/index/index.html index 64662a516..c66bb6cf4 100644 --- a/src/addons/mod/forum/components/index/index.html +++ b/src/addons/mod/forum/components/index/index.html @@ -42,6 +42,11 @@ + + + + diff --git a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html index ffa5e9542..8da5fbf43 100644 --- a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html @@ -50,6 +50,11 @@ + + + + diff --git a/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html b/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html index 3b97f1c3c..86dbd3f42 100644 --- a/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html +++ b/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html @@ -32,6 +32,11 @@ + + + + diff --git a/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html b/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html index 60362e216..4c23879ea 100644 --- a/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html +++ b/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html @@ -28,6 +28,11 @@ + + + + diff --git a/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html b/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html index 9dd4e70f0..cd3ae76bb 100644 --- a/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html +++ b/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html @@ -31,6 +31,12 @@ + + + + + diff --git a/src/addons/mod/lti/components/index/addon-mod-lti-index.html b/src/addons/mod/lti/components/index/addon-mod-lti-index.html index 4a8188253..ab922bcde 100644 --- a/src/addons/mod/lti/components/index/addon-mod-lti-index.html +++ b/src/addons/mod/lti/components/index/addon-mod-lti-index.html @@ -18,6 +18,12 @@ + + + + + diff --git a/src/addons/mod/page/components/index/addon-mod-page-index.html b/src/addons/mod/page/components/index/addon-mod-page-index.html index 49d132065..a5928e7cb 100644 --- a/src/addons/mod/page/components/index/addon-mod-page-index.html +++ b/src/addons/mod/page/components/index/addon-mod-page-index.html @@ -25,6 +25,11 @@ + + + + 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 f2148d8f3..9bd1a4621 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 @@ -27,6 +27,12 @@ + + + + + diff --git a/src/addons/mod/resource/components/index/addon-mod-resource-index.html b/src/addons/mod/resource/components/index/addon-mod-resource-index.html index a89d25990..efddbf59d 100644 --- a/src/addons/mod/resource/components/index/addon-mod-resource-index.html +++ b/src/addons/mod/resource/components/index/addon-mod-resource-index.html @@ -20,6 +20,11 @@ + + + + diff --git a/src/addons/mod/scorm/components/index/addon-mod-scorm-index.html b/src/addons/mod/scorm/components/index/addon-mod-scorm-index.html index c35c961f5..c5dfad566 100644 --- a/src/addons/mod/scorm/components/index/addon-mod-scorm-index.html +++ b/src/addons/mod/scorm/components/index/addon-mod-scorm-index.html @@ -28,6 +28,11 @@ + + + + diff --git a/src/addons/mod/survey/components/index/addon-mod-survey-index.html b/src/addons/mod/survey/components/index/addon-mod-survey-index.html index 3112a4e7d..0c6ff53ba 100644 --- a/src/addons/mod/survey/components/index/addon-mod-survey-index.html +++ b/src/addons/mod/survey/components/index/addon-mod-survey-index.html @@ -29,6 +29,11 @@ + + + + diff --git a/src/addons/mod/url/components/index/addon-mod-url-index.html b/src/addons/mod/url/components/index/addon-mod-url-index.html index 6ded3f48f..7fe1cae8d 100644 --- a/src/addons/mod/url/components/index/addon-mod-url-index.html +++ b/src/addons/mod/url/components/index/addon-mod-url-index.html @@ -15,6 +15,11 @@ + + + + diff --git a/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html index 343dcdf04..eb47dffa2 100644 --- a/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html +++ b/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html @@ -47,6 +47,12 @@ + + + + +
diff --git a/src/addons/mod/workshop/components/index/addon-mod-workshop-index.html b/src/addons/mod/workshop/components/index/addon-mod-workshop-index.html index 5227b46db..5dd6b0e66 100644 --- a/src/addons/mod/workshop/components/index/addon-mod-workshop-index.html +++ b/src/addons/mod/workshop/components/index/addon-mod-workshop-index.html @@ -27,6 +27,12 @@ + + + + + diff --git a/src/core/features/course/classes/main-activity-component.ts b/src/core/features/course/classes/main-activity-component.ts index 4fdcfe9e9..64c256e90 100644 --- a/src/core/features/course/classes/main-activity-component.ts +++ b/src/core/features/course/classes/main-activity-component.ts @@ -122,7 +122,14 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR this.syncIcon = CoreConstants.ICON_LOADING; try { - await CoreUtils.ignoreErrors(this.invalidateContent()); + await CoreUtils.ignoreErrors(Promise.all([ + this.invalidateContent(), + this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined, + ])); + + if (this.showCompletion) { + this.module = await CoreCourse.getModule(this.module.id, this.courseId); + } await this.loadContent(true, sync, showErrors); } finally { diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts index ad336718e..b53613f17 100644 --- a/src/core/features/course/classes/main-resource-component.ts +++ b/src/core/features/course/classes/main-resource-component.ts @@ -70,12 +70,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, isDestroyed = false; // Whether the component is destroyed, used when calling fillContextMenu. contextMenuStatusObserver?: CoreEventObserver; // Observer of package status, used when calling fillContextMenu. contextFileStatusObserver?: CoreEventObserver; // Observer of file status, used when calling fillContextMenu. + showCompletion = false; // Whether to show completion inside the activity. protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents. protected isCurrentView = false; // Whether the component is in the current view. protected siteId?: string; // Current Site ID. protected statusObserver?: CoreEventObserver; // Observer of package status. Only if setStatusListener is called. protected currentStatus?: string; // The current status of the module. Only if setStatusListener is called. + protected completionObserver?: CoreEventObserver; protected logger: CoreLogger; constructor( @@ -94,6 +96,18 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, this.componentId = this.module.id; this.externalUrl = this.module.url; this.courseId = this.courseId || this.module.course!; + this.showCompletion = !!CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.11'); + + if (this.showCompletion) { + this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_MODULE_VIEWED, async (data) => { + if (data && data.cmId == this.module.id) { + await CoreCourse.invalidateModule(this.module.id); + + this.module = await CoreCourse.getModule(this.module.id, this.courseId); + } + }); + } + this.blog = await AddonBlog.isPluginEnabled(); } @@ -140,7 +154,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, this.refreshIcon = CoreConstants.ICON_LOADING; try { - await CoreUtils.ignoreErrors(this.invalidateContent()); + await CoreUtils.ignoreErrors(Promise.all([ + this.invalidateContent(), + this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined, + ])); + + if (this.showCompletion) { + this.module = await CoreCourse.getModule(this.module.id, this.courseId); + } await this.loadContent(true); } finally { @@ -376,6 +397,16 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, return result; } + /** + * The completion of the modules has changed. + * + * @return Promise resolved when done. + */ + async onCompletionChange(): Promise { + // Nothing to do. + return; + } + /** * Component being destroyed. */ @@ -384,6 +415,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, this.contextMenuStatusObserver?.off(); this.contextFileStatusObserver?.off(); this.statusObserver?.off(); + this.completionObserver?.off(); } /** diff --git a/src/core/features/course/classes/module-completion.ts b/src/core/features/course/classes/module-completion.ts index c78de3695..161da7e94 100644 --- a/src/core/features/course/classes/module-completion.ts +++ b/src/core/features/course/classes/module-completion.ts @@ -14,8 +14,6 @@ import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; -import { CoreDomUtils } from '@services/utils/dom'; -import { CoreCourse } from '@features/course/services/course'; import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; /** @@ -32,7 +30,7 @@ export class CoreCourseModuleCompletionBaseComponent implements OnChanges { @Output() completionChanged = new EventEmitter(); // Notify when completion changes. /** - * Detect changes on input properties. + * @inheritdoc */ ngOnChanges(changes: { [name: string]: SimpleChange }): void { if (changes.completion && this.completion) { @@ -47,47 +45,4 @@ export class CoreCourseModuleCompletionBaseComponent implements OnChanges { return; } - /** - * Completion clicked. - * - * @param e The click event. - */ - async completionClicked(e: Event): Promise { - if (!this.completion) { - return; - } - - if (typeof this.completion.cmid == 'undefined' || this.completion.tracking !== 1) { - return; - } - - e.preventDefault(); - e.stopPropagation(); - - const modal = await CoreDomUtils.showModalLoading(); - this.completion.state = this.completion.state === 1 ? 0 : 1; - - try { - const response = await CoreCourse.markCompletedManually( - this.completion.cmid, - this.completion.state === 1, - this.completion.courseId!, - this.completion.courseName, - ); - - if (this.completion.valueused === false) { - this.calculateData(); - if (response.offline) { - this.completion.offline = true; - } - } - this.completionChanged.emit(this.completion); - } catch (error) { - this.completion.state = this.completion.state === 1 ? 0 : 1; - CoreDomUtils.showErrorModalDefault(error, 'core.errorchangecompletion', true); - } finally { - modal.dismiss(); - } - } - } diff --git a/src/core/features/course/components/components.module.ts b/src/core/features/course/components/components.module.ts index 29e90f3cc..3680f6f36 100644 --- a/src/core/features/course/components/components.module.ts +++ b/src/core/features/course/components/components.module.ts @@ -24,6 +24,8 @@ import { CoreCourseSectionSelectorComponent } from './section-selector/section-s import { CoreCourseTagAreaComponent } from './tag-area/tag-area'; import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module'; import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; +import { CoreCourseModuleInfoComponent } from './module-info/module-info'; +import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion'; @NgModule({ declarations: [ @@ -32,6 +34,8 @@ import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-l CoreCourseModuleCompletionComponent, CoreCourseModuleCompletionLegacyComponent, CoreCourseModuleDescriptionComponent, + CoreCourseModuleInfoComponent, + CoreCourseModuleManualCompletionComponent, CoreCourseSectionSelectorComponent, CoreCourseTagAreaComponent, CoreCourseUnsupportedModuleComponent, @@ -46,6 +50,8 @@ import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-l CoreCourseModuleCompletionComponent, CoreCourseModuleCompletionLegacyComponent, CoreCourseModuleDescriptionComponent, + CoreCourseModuleInfoComponent, + CoreCourseModuleManualCompletionComponent, CoreCourseSectionSelectorComponent, CoreCourseTagAreaComponent, CoreCourseUnsupportedModuleComponent, diff --git a/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts index a44dced24..8eca20fe0 100644 --- a/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts +++ b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts @@ -19,6 +19,7 @@ import { CoreCourseProvider } from '@features/course/services/course'; import { CoreFilterHelper } from '@features/filter/services/filter-helper'; import { Translate } from '@singletons'; import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; /** * Component to handle activity completion in sites previous to 3.11. @@ -115,4 +116,21 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC this.completionDescription = Translate.instant(langKey, translateParams); } + /** + * Completion clicked. + * + * @param event The click event. + */ + async completionClicked(event: Event): Promise { + if (!this.completion) { + return; + } + + await CoreCourseHelper.changeManualCompletion(this.completion, event); + + this.calculateData(); + + this.completionChanged.emit(this.completion); + } + } diff --git a/src/core/features/course/components/module-completion/core-course-module-completion.html b/src/core/features/course/components/module-completion/core-course-module-completion.html index 4951f341d..ffce38a8b 100644 --- a/src/core/features/course/components/module-completion/core-course-module-completion.html +++ b/src/core/features/course/components/module-completion/core-course-module-completion.html @@ -27,27 +27,6 @@
-
- - - - - - {{ 'core.course.completion_manual:done' | translate }} - - - - - {{ 'core.course.completion_manual:markdone' | translate }} - - - - - - - {{ 'core.course.completion_manual:markdone' | translate }} - - -
+ + diff --git a/src/core/features/course/components/module-completion/module-completion.scss b/src/core/features/course/components/module-completion/module-completion.scss index eeded5aaa..946eac3bd 100644 --- a/src/core/features/course/components/module-completion/module-completion.scss +++ b/src/core/features/course/components/module-completion/module-completion.scss @@ -9,10 +9,4 @@ } } } - - .core-module-manual-completion { - ion-button { - text-transform: none; - } - } } diff --git a/src/core/features/course/components/module-completion/module-completion.ts b/src/core/features/course/components/module-completion/module-completion.ts index 4fa8ffdcb..a147c7b03 100644 --- a/src/core/features/course/components/module-completion/module-completion.ts +++ b/src/core/features/course/components/module-completion/module-completion.ts @@ -35,7 +35,7 @@ import { Translate } from '@singletons'; export class CoreCourseModuleCompletionComponent extends CoreCourseModuleCompletionBaseComponent { @Input() showCompletionConditions = false; // Whether to show activity completion conditions. - @Input() showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled. + @Input() showManualCompletion = false; // Whether to show manual completion. details?: CompletionRule[]; accessibleDescription: string | null = null; @@ -48,21 +48,6 @@ export class CoreCourseModuleCompletionComponent extends CoreCourseModuleComplet return; } - // Set an accessible description for manual completions with overridden completion state. - if (!this.completion.isautomatic && this.completion.overrideby) { - const setByData = { - $a: { - activityname: this.moduleName, - setby: this.completion.overrideby, - }, - }; - const setByLangKey = this.completion.state ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone'; - this.accessibleDescription = Translate.instant('core.course.' + setByLangKey, setByData); - } else { - const langKey = this.completion.state ? 'completion_manual:aria:done' : 'completion_manual:aria:markdone'; - this.accessibleDescription = Translate.instant('core.course.' + langKey, { $a: this.moduleName }); - } - // Format rules. this.details = this.completion.details.map((rule: CompletionRule) => { rule.statuscomplete = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE || diff --git a/src/core/features/course/components/module-info/core-course-module-info.html b/src/core/features/course/components/module-info/core-course-module-info.html new file mode 100644 index 000000000..d69b3148a --- /dev/null +++ b/src/core/features/course/components/module-info/core-course-module-info.html @@ -0,0 +1,18 @@ + + + + +
+

+ {{ date.label }} {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }} +

+
+ + + + +
+
+
diff --git a/src/core/features/course/components/module-info/module-info.ts b/src/core/features/course/components/module-info/module-info.ts new file mode 100644 index 000000000..c4748fe4c --- /dev/null +++ b/src/core/features/course/components/module-info/module-info.ts @@ -0,0 +1,31 @@ +// (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, EventEmitter, Input, Output } from '@angular/core'; +import { CoreCourseModule, CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; + +/** + * Display info about a module: dates and completion. + */ +@Component({ + selector: 'core-course-module-info', + templateUrl: 'core-course-module-info.html', +}) +export class CoreCourseModuleInfoComponent { + + @Input() module!: CoreCourseModule; // The module to render. + @Input() showManualCompletion = false; // Whether to show manual completion. + @Output() completionChanged = new EventEmitter(); // Notify when completion changes. + +} diff --git a/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html new file mode 100644 index 000000000..0f0ce4be8 --- /dev/null +++ b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html @@ -0,0 +1,23 @@ +
+ + + + + + {{ 'core.course.completion_manual:done' | translate }} + + + + + {{ 'core.course.completion_manual:markdone' | translate }} + + + + + + + {{ 'core.course.completion_manual:markdone' | translate }} + + +
diff --git a/src/core/features/course/components/module-manual-completion/module-manual-completion.scss b/src/core/features/course/components/module-manual-completion/module-manual-completion.scss new file mode 100644 index 000000000..0aa279fc8 --- /dev/null +++ b/src/core/features/course/components/module-manual-completion/module-manual-completion.scss @@ -0,0 +1,5 @@ +:host { + ion-button { + text-transform: none; + } +} diff --git a/src/core/features/course/components/module-manual-completion/module-manual-completion.ts b/src/core/features/course/components/module-manual-completion/module-manual-completion.ts new file mode 100644 index 000000000..3cab6465e --- /dev/null +++ b/src/core/features/course/components/module-manual-completion/module-manual-completion.ts @@ -0,0 +1,109 @@ +// (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, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core'; + +import { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; +import { Translate } from '@singletons'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; + +/** + * Component to display a button for manual completion. + */ +@Component({ + selector: 'core-course-module-manual-completion', + templateUrl: 'core-course-module-manual-completion.html', + styleUrls: ['module-manual-completion.scss'], +}) +export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChanges, OnDestroy { + + @Input() completion?: CoreCourseModuleCompletionData; // The completion status. + @Input() moduleName?: string; // The name of the module this completion affects. + @Output() completionChanged = new EventEmitter(); // Notify when completion changes. + + accessibleDescription: string | null = null; + + protected manualChangedObserver?: CoreEventObserver; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => { + if (this.completion && this.completion.cmid == data.completion.cmid) { + this.completion = data.completion; + this.calculateData(); + } + }); + } + + /** + * @inheritdoc + */ + ngOnChanges(changes: { [name: string]: SimpleChange }): void { + if (changes.completion && this.completion) { + this.calculateData(); + } + } + + /** + * @inheritdoc + */ + protected calculateData(): void { + if (!this.completion?.isautomatic) { + return; + } + + // Set an accessible description for manual completions with overridden completion state. + if (this.completion.overrideby) { + const setByData = { + $a: { + activityname: this.moduleName, + setby: this.completion.overrideby, + }, + }; + const setByLangKey = this.completion.state ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone'; + this.accessibleDescription = Translate.instant('core.course.' + setByLangKey, setByData); + } else { + const langKey = this.completion.state ? 'completion_manual:aria:done' : 'completion_manual:aria:markdone'; + this.accessibleDescription = Translate.instant('core.course.' + langKey, { $a: this.moduleName }); + } + } + + /** + * Completion clicked. + * + * @param event The click event. + */ + async completionClicked(event: Event): Promise { + if (!this.completion) { + return; + } + + await CoreCourseHelper.changeManualCompletion(this.completion, event); + + this.calculateData(); + + CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion }); + this.completionChanged.emit(this.completion); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.manualChangedObserver?.off(); + } + +} diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 4b647b267..b8e1ee6b4 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -67,6 +67,7 @@ import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreSiteHome } from '@features/sitehome/services/sitehome'; import { CoreNavigator } from '@services/navigator'; import { CoreSiteHomeHomeHandlerService } from '@features/sitehome/services/handlers/sitehome-home'; +import { CoreStatusWithWarningsWSResponse } from '@services/ws'; /** * Prefetch info of a module. @@ -1890,6 +1891,52 @@ export class CoreCourseHelperProvider { await Promise.all(promises); } + /** + * Completion clicked. + * + * @param completion The completion. + * @param event The click event. + * @return Promise resolved with the result. + */ + async changeManualCompletion( + completion: CoreCourseModuleCompletionData, + event?: Event, + ): Promise { + if (!completion) { + return; + } + + if (typeof completion.cmid == 'undefined' || completion.tracking !== 1) { + return; + } + + event?.preventDefault(); + event?.stopPropagation(); + + const modal = await CoreDomUtils.showModalLoading(); + completion.state = completion.state === 1 ? 0 : 1; + + try { + const response = await CoreCourse.markCompletedManually( + completion.cmid, + completion.state === 1, + completion.courseId!, + completion.courseName, + ); + + if (response.offline) { + completion.offline = true; + } + + return response; + } catch (error) { + completion.state = completion.state === 1 ? 0 : 1; + CoreDomUtils.showErrorModalDefault(error, 'core.errorchangecompletion', true); + } finally { + modal.dismiss(); + } + } + } export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider); diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 2c67cb4f5..45fd0aa6b 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -150,9 +150,12 @@ export class CoreCourseProvider { * @param completion Completion status of the module. */ checkModuleCompletion(courseId: number, completion?: CoreCourseModuleCompletionData): void { - if (completion && completion.tracking === 2 && completion.state === 0) { + if (completion && completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC && completion.state === 0) { this.invalidateSections(courseId).finally(() => { - CoreEvents.trigger(CoreEvents.COMPLETION_MODULE_VIEWED, { courseId: courseId }); + CoreEvents.trigger(CoreEvents.COMPLETION_MODULE_VIEWED, { + courseId: courseId, + cmId: completion.cmid, + }); }); } } @@ -969,6 +972,9 @@ export class CoreCourseProvider { // Ignore errors, shouldn't happen. } + // Invalidate module now, completion has changed. + await this.invalidateModule(cmId, siteId); + return result; } catch (error) { if (CoreUtils.isWebServiceError(error) || !courseId) { diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index ee72d67e9..0c622f77b 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -18,6 +18,7 @@ import { CoreLogger } from '@singletons/logger'; import { CoreSite, CoreSiteInfoResponse, CoreSitePublicConfigResponse } from '@classes/site'; import { CoreFilepoolComponentFileEventData } from '@services/filepool'; import { CoreNavigationOptions } from '@services/navigator'; +import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; /** * Observer instance to stop listening to an event. @@ -45,6 +46,7 @@ export interface CoreEventsData { [CoreEvents.NOTIFICATION_SOUND_CHANGED]: CoreEventNotificationSoundChangedData; [CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData; [CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData; + [CoreEvents.MANUAL_COMPLETION_CHANGED]: CoreEventManualCompletionChangedData; [CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData; [CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData; [CoreEvents.IAB_LOAD_START]: InAppBrowserEvent; @@ -53,7 +55,7 @@ export interface CoreEventsData { [CoreEvents.COMPONENT_FILE_ACTION]: CoreFilepoolComponentFileEventData; [CoreEvents.FILE_SHARED]: CoreEventFileSharedData; [CoreEvents.APP_LAUNCHED_URL]: CoreEventAppLaunchedData; -}; +} /* * Service to send and listen to events. @@ -72,6 +74,7 @@ export class CoreEvents { static readonly SITE_UPDATED = 'site_updated'; static readonly SITE_DELETED = 'site_deleted'; static readonly COMPLETION_MODULE_VIEWED = 'completion_module_viewed'; + static readonly MANUAL_COMPLETION_CHANGED = 'manual_completion_changed'; static readonly USER_DELETED = 'user_deleted'; static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed'; static readonly COURSE_STATUS_CHANGED = 'course_status_changed'; @@ -330,7 +333,15 @@ export type CoreEventSelectCourseTabData = { * Data passed to COMPLETION_MODULE_VIEWED event. */ export type CoreEventCompletionModuleViewedData = { - courseId?: number; + courseId: number; + cmId?: number; +}; + +/** + * Data passed to MANUAL_COMPLETION_CHANGED event. + */ +export type CoreEventManualCompletionChangedData = { + completion: CoreCourseModuleCompletionData; }; /** From ac8cc189f75f0741f3f958b3ece29f27d747bc20 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 7 May 2021 11:59:55 +0200 Subject: [PATCH 4/5] MOBILE-3757 course: Update cached data after commpletion changed --- scripts/langindex.json | 11 ++++ .../course/classes/main-resource-component.ts | 28 +++++++++-- .../module-completion/module-completion.ts | 11 ++-- .../module-manual-completion.ts | 21 ++++---- .../course/pages/contents/contents.ts | 21 ++++++++ .../features/course/services/course-helper.ts | 50 +++++++++++++++++-- src/core/features/user/lang.json | 3 +- src/core/features/user/services/user.ts | 21 +++++++- 8 files changed, 142 insertions(+), 24 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index e96142599..8d321dcc6 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1447,6 +1447,17 @@ "core.course.askadmintosupport": "local_moodlemobileapp", "core.course.availablespace": "local_moodlemobileapp", "core.course.cannotdeletewhiledownloading": "local_moodlemobileapp", + "core.course.completion_automatic:done": "course", + "core.course.completion_automatic:failed": "course", + "core.course.completion_automatic:todo": "course", + "core.course.completion_manual:aria:done": "course", + "core.course.completion_manual:aria:markdone": "course", + "core.course.completion_manual:markdone": "course", + "core.course.completion_setby:auto:done": "course", + "core.course.completion_setby:auto:todo": "course", + "core.course.completion_setby:manual:done": "course", + "core.course.completion_setby:manual:markdone": "course", + "core.course.completionrequirements": "course", "core.course.confirmdeletemodulefiles": "local_moodlemobileapp", "core.course.confirmdeletestoreddata": "local_moodlemobileapp", "core.course.confirmdownload": "local_moodlemobileapp", diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts index b53613f17..f94976061 100644 --- a/src/core/features/course/classes/main-resource-component.ts +++ b/src/core/features/course/classes/main-resource-component.ts @@ -79,6 +79,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, protected currentStatus?: string; // The current status of the module. Only if setStatusListener is called. protected completionObserver?: CoreEventObserver; protected logger: CoreLogger; + protected debouncedUpdateModule?: () => void; // Update the module after a certain time. constructor( @Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent', @@ -103,9 +104,13 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, if (data && data.cmId == this.module.id) { await CoreCourse.invalidateModule(this.module.id); - this.module = await CoreCourse.getModule(this.module.id, this.courseId); + this.fetchModule(); } }); + + this.debouncedUpdateModule = CoreUtils.debounce(() => { + this.fetchModule(); + }, 10000); } this.blog = await AddonBlog.isPluginEnabled(); @@ -160,7 +165,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, ])); if (this.showCompletion) { - this.module = await CoreCourse.getModule(this.module.id, this.courseId); + this.fetchModule(); } await this.loadContent(true); @@ -403,8 +408,23 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, * @return Promise resolved when done. */ async onCompletionChange(): Promise { - // Nothing to do. - return; + // Update the module data after a while. + this.debouncedUpdateModule?.(); + } + + /** + * Fetch module. + * + * @return Promise resolved when done. + */ + protected async fetchModule(): Promise { + const module = await CoreCourse.getModule(this.module.id, this.courseId); + + CoreCourseHelper.calculateModuleCompletionData(module, this.courseId); + + await CoreCourseHelper.loadModuleOfflineCompletion(this.courseId, module); + + this.module = module; } /** diff --git a/src/core/features/course/components/module-completion/module-completion.ts b/src/core/features/course/components/module-completion/module-completion.ts index a147c7b03..92fbb0b0e 100644 --- a/src/core/features/course/components/module-completion/module-completion.ts +++ b/src/core/features/course/components/module-completion/module-completion.ts @@ -16,6 +16,7 @@ import { Component, Input } from '@angular/core'; import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion'; import { CoreCourseModuleWSRuleDetails, CoreCourseProvider } from '@features/course/services/course'; +import { CoreUser } from '@features/user/services/user'; import { Translate } from '@singletons'; /** @@ -43,13 +44,13 @@ export class CoreCourseModuleCompletionComponent extends CoreCourseModuleComplet /** * @inheritdoc */ - protected calculateData(): void { + protected async calculateData(): Promise { if (!this.completion?.details) { return; } // Format rules. - this.details = this.completion.details.map((rule: CompletionRule) => { + this.details = await Promise.all(this.completion.details.map(async (rule: CompletionRule) => { rule.statuscomplete = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE || rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_PASS; rule.statuscompletefail = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_FAIL; @@ -57,10 +58,12 @@ export class CoreCourseModuleCompletionComponent extends CoreCourseModuleComplet rule.accessibleDescription = null; if (this.completion!.overrideby) { + const fullName = await CoreUser.getUserFullNameWithDefault(this.completion!.overrideby, this.completion!.courseId); + const setByData = { $a: { condition: rule.rulevalue.description, - setby: this.completion!.overrideby, + setby: fullName, }, }; const overrideStatus = rule.statuscomplete ? 'done' : 'todo'; @@ -69,7 +72,7 @@ export class CoreCourseModuleCompletionComponent extends CoreCourseModuleComplet } return rule; - }); + })); } } diff --git a/src/core/features/course/components/module-manual-completion/module-manual-completion.ts b/src/core/features/course/components/module-manual-completion/module-manual-completion.ts index 3cab6465e..68e8ff927 100644 --- a/src/core/features/course/components/module-manual-completion/module-manual-completion.ts +++ b/src/core/features/course/components/module-manual-completion/module-manual-completion.ts @@ -15,6 +15,7 @@ import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core'; import { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; +import { CoreUser } from '@features/user/services/user'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -41,10 +42,13 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan */ ngOnInit(): void { this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => { - if (this.completion && this.completion.cmid == data.completion.cmid) { - this.completion = data.completion; - this.calculateData(); + if (!this.completion || this.completion.cmid != data.completion.cmid) { + return; } + + this.completion = data.completion; + this.calculateData(); + this.completionChanged.emit(this.completion); }); } @@ -60,17 +64,19 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan /** * @inheritdoc */ - protected calculateData(): void { - if (!this.completion?.isautomatic) { + protected async calculateData(): Promise { + if (!this.completion || this.completion.isautomatic) { return; } // Set an accessible description for manual completions with overridden completion state. if (this.completion.overrideby) { + const fullName = await CoreUser.getUserFullNameWithDefault(this.completion.overrideby, this.completion.courseId); + const setByData = { $a: { activityname: this.moduleName, - setby: this.completion.overrideby, + setby: fullName, }, }; const setByLangKey = this.completion.state ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone'; @@ -93,10 +99,7 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan await CoreCourseHelper.changeManualCompletion(this.completion, event); - this.calculateData(); - CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion }); - this.completionChanged.emit(this.completion); } /** diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index 9a9c2fb75..3cf7f8da6 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -81,6 +81,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { protected courseStatusObserver?: CoreEventObserver; protected syncObserver?: CoreEventObserver; protected isDestroyed = false; + protected modulesHaveCompletion = false; + protected debouncedUpdateCachedCompletion?: () => void; // Update the cached completion after a certain time. /** * Component being initialized. @@ -104,6 +106,21 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { CoreCourseFormatDelegate.displayEnableDownload(this.course); this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); + this.debouncedUpdateCachedCompletion = CoreUtils.debounce(() => { + if (this.modulesHaveCompletion) { + CoreUtils.ignoreErrors(CoreCourse.getSections(this.course.id, false, true)); + } else { + CoreUtils.ignoreErrors(CoreCourse.getActivitiesCompletionStatus( + this.course.id, + undefined, + undefined, + false, + false, + false, + )); + } + }, 30000); + this.initListeners(); await this.loadData(false, true); @@ -254,6 +271,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { if (sectionWithModules && typeof sectionWithModules.modules[0].completion != 'undefined') { // The module already has completion (3.6 onwards). Load the offline completion. + this.modulesHaveCompletion = true; + await CoreUtils.ignoreErrors(CoreCourseHelper.loadOfflineCompletion(this.course.id, sections)); } else { const fetchedData = await CoreUtils.ignoreErrors( @@ -356,6 +375,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { // Invalidate the completion. await CoreUtils.ignoreErrors(CoreCourse.invalidateSections(this.course.id)); + this.debouncedUpdateCachedCompletion?.(); + return; } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index b8e1ee6b4..fa7f9423d 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -195,11 +195,8 @@ export class CoreCourseHelperProvider { forCoursePage, ); - if (module.completiondata && module.completion && module.completion > 0) { - module.completiondata.courseId = courseId; - module.completiondata.courseName = courseName; - module.completiondata.tracking = module.completion; - module.completiondata.cmid = module.id; + if (module.completiondata) { + this.calculateModuleCompletionData(module, courseId, courseName); } else if (completionStatus && typeof completionStatus[module.id] != 'undefined') { // Should not happen on > 3.6. Check if activity has completions and if it's marked. const activityStatus = completionStatus[module.id]; @@ -224,6 +221,24 @@ export class CoreCourseHelperProvider { return { hasContent, sections: formattedSections }; } + /** + * Calculate completion data of a module. + * + * @param module Module. + * @param courseId Course ID of the module. + * @param courseName Course name. + */ + calculateModuleCompletionData(module: CoreCourseModule, courseId: number, courseName?: string): void { + if (!module.completiondata || !module.completion) { + return; + } + + module.completiondata.courseId = courseId; + module.completiondata.courseName = courseName; + module.completiondata.tracking = module.completion; + module.completiondata.cmid = module.id; + } + /** * Calculate the status of a section. * @@ -1177,6 +1192,31 @@ export class CoreCourseHelperProvider { } } + /** + * Load offline completion for a certain module. + * This should be used in 3.6 sites or higher, where the course contents already include the completion. + * + * @param courseId The course to get the completion. + * @param mmodule The module. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async loadModuleOfflineCompletion(courseId: number, module: CoreCourseModule, siteId?: string): Promise { + if (!module.completiondata) { + return; + } + + const offlineCompletions = await CoreCourseOffline.getCourseManualCompletions(courseId, siteId); + + const offlineCompletion = offlineCompletions.find(completion => completion.cmid == module.id); + + if (offlineCompletion && offlineCompletion.timecompleted >= module.completiondata.timecompleted * 1000) { + // The module has offline completion. Load it. + module.completiondata.state = offlineCompletion.completed; + module.completiondata.offline = true; + } + } + /** * Prefetch all the courses in the array. * diff --git a/src/core/features/user/lang.json b/src/core/features/user/lang.json index 528fe4c5c..610e0fae4 100644 --- a/src/core/features/user/lang.json +++ b/src/core/features/user/lang.json @@ -23,5 +23,6 @@ "sendemail": "Email", "student": "Student", "teacher": "Non-editing teacher", + "userwithid": "User with ID {{id}}", "webpage": "Web page" -} \ No newline at end of file +} diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index 1ba500218..6033f8efd 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -21,7 +21,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreUserOffline } from './user-offline'; import { CoreLogger } from '@singletons/logger'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; -import { makeSingleton } from '@singletons'; +import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; import { CoreError } from '@classes/errors/error'; @@ -305,6 +305,25 @@ export class CoreUserProvider { return site.getDb().getRecord(USERS_TABLE_NAME, { id: userId }); } + /** + * Get a user fullname, using a default text if user not found. + * + * @param userId User ID. + * @param courseId Course ID. + * @param siteId Site ID. + * @return Promise resolved with user name. + */ + async getUserFullNameWithDefault(userId: number, courseId?: number, siteId?: string): Promise { + try { + const user = await CoreUser.getProfile(userId, courseId, true, siteId); + + return user.fullname; + + } catch { + return Translate.instant('core.user.userwithid', { id: userId }); + } + } + /** * Get user profile from WS. * From 70bc246a7f5c1530b76d6e22298c46e89ce10a41 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 12 May 2021 08:41:59 +0200 Subject: [PATCH 5/5] MOBILE-3757 lang: Remove duplicated userwithid string --- scripts/langindex.json | 4 ++-- src/addons/mod/assign/lang.json | 3 +-- src/addons/notes/lang.json | 3 +-- src/addons/notes/services/notes.ts | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 8d321dcc6..7ca7eb8c2 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -408,7 +408,6 @@ "addon.mod_assign.ungroupedusersoptional": "assign", "addon.mod_assign.unlimitedattempts": "assign", "addon.mod_assign.userswhoneedtosubmit": "assign", - "addon.mod_assign.userwithid": "local_moodlemobileapp", "addon.mod_assign.viewsubmission": "assign", "addon.mod_assign.warningsubmissiongrademodified": "local_moodlemobileapp", "addon.mod_assign.warningsubmissionmodified": "local_moodlemobileapp", @@ -1041,7 +1040,6 @@ "addon.notes.personalnotes": "notes", "addon.notes.publishstate": "notes", "addon.notes.sitenotes": "notes", - "addon.notes.userwithid": "local_moodlemobileapp", "addon.notes.warningnotenotsent": "local_moodlemobileapp", "addon.notifications.errorgetnotifications": "local_moodlemobileapp", "addon.notifications.markallread": "moodle", @@ -1481,6 +1479,7 @@ "core.course.nocontentavailable": "local_moodlemobileapp", "core.course.overriddennotice": "grades", "core.course.refreshcourse": "local_moodlemobileapp", + "core.course.section": "moodle", "core.course.sections": "moodle", "core.course.useactivityonbrowser": "local_moodlemobileapp", "core.course.warningmanualcompletionmodified": "local_moodlemobileapp", @@ -2209,6 +2208,7 @@ "core.user.sendemail": "local_moodlemobileapp", "core.user.student": "moodle/defaultcoursestudent", "core.user.teacher": "moodle/noneditingteacher", + "core.user.userwithid": "local_moodlemobileapp", "core.user.webpage": "moodle", "core.userdeleted": "moodle", "core.userdetails": "moodle", diff --git a/src/addons/mod/assign/lang.json b/src/addons/mod/assign/lang.json index 5311cdf8a..37a598c17 100644 --- a/src/addons/mod/assign/lang.json +++ b/src/addons/mod/assign/lang.json @@ -95,10 +95,9 @@ "ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.", "ungroupedusersoptional": "The setting 'Students submit in groups' is enabled and some users are either not a member of any group, or are a member of more than one group. Please be aware that these students will submit as members of the 'Default group'.", "unlimitedattempts": "Unlimited", - "userwithid": "User with ID {{id}}", "userswhoneedtosubmit": "Users who need to submit: {{$a}}", "viewsubmission": "View submission", "warningsubmissionmodified": "The user submission was modified on the site.", "warningsubmissiongrademodified": "The submission grade was modified on the site.", "wordlimit": "Word limit" -} \ No newline at end of file +} diff --git a/src/addons/notes/lang.json b/src/addons/notes/lang.json index c8256d0c4..732140ed1 100644 --- a/src/addons/notes/lang.json +++ b/src/addons/notes/lang.json @@ -10,6 +10,5 @@ "personalnotes": "Personal notes", "publishstate": "Context", "sitenotes": "Site notes", - "userwithid": "User with ID {{id}}", "warningnotenotsent": "Couldn't add note(s) to course {{course}}. {{error}}" -} \ No newline at end of file +} diff --git a/src/addons/notes/services/notes.ts b/src/addons/notes/services/notes.ts index 7d6a05515..27660a22f 100644 --- a/src/addons/notes/services/notes.ts +++ b/src/addons/notes/services/notes.ts @@ -370,7 +370,7 @@ export class AddonNotesProvider { return; }).catch(() => { - note.userfullname = Translate.instant('addon.notes.userwithid', { id: note.userid }); + note.userfullname = Translate.instant('core.user.userwithid', { id: note.userid }); })); await Promise.all(promises);