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; }; /**