MOBILE-3757 course: Display dates and completion inside activity

main
Dani Palou 2021-05-07 10:09:31 +02:00
parent 3f825db799
commit 61a908216d
40 changed files with 439 additions and 102 deletions

View File

@ -30,6 +30,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<!-- Description and intro attachments. --> <!-- Description and intro attachments. -->
<ion-card *ngIf="description"> <ion-card *ngIf="description">
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
@ -141,5 +146,4 @@
<addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId" <addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId"
[moduleId]="module.id"> [moduleId]="module.id">
</addon-mod-assign-submission> </addon-mod-assign-submission>
</core-loading> </core-loading>

View File

@ -120,7 +120,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT,
(data) => { (data) => {
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { 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); CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
// Reload data since it can have offline data now. // Reload data since it can have offline data now.

View File

@ -54,7 +54,7 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item class="ion-text-wrap" *ngIf="fromDate && !isSubmittedForGrading"> <ion-item class="ion-text-wrap" *ngIf="showDates && fromDate && !isSubmittedForGrading">
<ion-label> <ion-label>
<p *ngIf="assign!.intro" <p *ngIf="assign!.intro"
[innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}"> [innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}">
@ -66,7 +66,7 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item class="ion-text-wrap" *ngIf="assign!.duedate && !isSubmittedForGrading"> <ion-item class="ion-text-wrap" *ngIf="showDates && assign!.duedate && !isSubmittedForGrading">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_assign.duedate' | translate }}</h2> <h2>{{ 'addon.mod_assign.duedate' | translate }}</h2>
<p *ngIf="assign!.duedate" >{{ assign!.duedate * 1000 | coreFormatDate }}</p> <p *ngIf="assign!.duedate" >{{ assign!.duedate * 1000 | coreFormatDate }}</p>

View File

@ -122,6 +122,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
allowAddAttempt = false; // Allow adding a new attempt when grading. allowAddAttempt = false; // Allow adding a new attempt when grading.
gradeUrl?: string; // URL to grade in browser. gradeUrl?: string; // URL to grade in browser.
isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission. isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission.
showDates = false; // Whether to show some dates.
// Some constants. // Some constants.
statusNew = AddonModAssignProvider.SUBMISSION_STATUS_NEW; statusNew = AddonModAssignProvider.SUBMISSION_STATUS_NEW;
@ -181,6 +182,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
*/ */
ngOnInit(): void { ngOnInit(): void {
this.isSubmittedForGrading = !!this.submitId; this.isSubmittedForGrading = !!this.submitId;
this.showDates = !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.11');
this.loadData(true); this.loadData(true);
} }

View File

@ -23,6 +23,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-course-module-description> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-course-module-description>

View File

@ -26,6 +26,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page"> <core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -28,6 +28,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -39,6 +39,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -28,6 +28,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -25,6 +25,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -42,6 +42,11 @@
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="discussions.loaded" class="core-loading-center"> <core-loading [hideUntil]="discussions.loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description *ngIf="forum && forum.type != 'single'" <core-course-module-description *ngIf="forum && forum.type != 'single'"
[description]="description" [component]="component" [componentId]="componentId" [note]="descriptionNote" [description]="description" [component]="component" [componentId]="componentId" [note]="descriptionNote"
contextLevel="module" [contextInstanceId]="module && module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module && module.id" [courseId]="courseId">

View File

@ -50,6 +50,11 @@
</core-search-box> </core-search-box>
<core-loading [hideUntil]="entries.loaded" class="core-loading-center"> <core-loading [hideUntil]="entries.loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -32,6 +32,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page"> <core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -28,6 +28,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page"> <core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<ion-card class="core-warning-card" *ngIf="warning"> <ion-card class="core-warning-card" *ngIf="warning">
<ion-item> <ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>

View File

@ -31,6 +31,12 @@
<!-- Index/Preview tab. --> <!-- Index/Preview tab. -->
<core-tab [title]="'addon.mod_lesson.preview' | translate" (ionSelect)="indexSelected()"> <core-tab [title]="'addon.mod_lesson.preview' | translate" (ionSelect)="indexSelected()">
<ng-template> <ng-template>
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -18,6 +18,12 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page"> <core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description *ngIf="lti && lti.showdescriptionlaunch" [description]="description" [component]="component" <core-course-module-description *ngIf="lti && lti.showdescriptionlaunch" [description]="description" [component]="component"
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -25,6 +25,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page"> <core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description *ngIf="displayDescription" [description]="description" [component]="component" <core-course-module-description *ngIf="displayDescription" [description]="description" [component]="component"
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -27,6 +27,12 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -20,6 +20,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page"> <core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description *ngIf="mode != 'iframe' && (mode != 'embedded' || displayDescription)" <core-course-module-description *ngIf="mode != 'iframe' && (mode != 'embedded' || displayDescription)"
[description]="description" [component]="component" [componentId]="componentId" contextLevel="module" [description]="description" [component]="component" [componentId]="componentId" contextLevel="module"
[contextInstanceId]="module.id" [courseId]="courseId"> [contextInstanceId]="module.id" [courseId]="courseId">

View File

@ -28,6 +28,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -29,6 +29,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page"> <core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description *ngIf="survey && !survey.surveydone && !hasOffline" [description]="description" <core-course-module-description *ngIf="survey && !survey.surveydone && !hasOffline" [description]="description"
[component]="component" [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [component]="component" [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId"> [courseId]="courseId">

View File

@ -15,6 +15,11 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<core-course-module-description *ngIf="displayDescription" [description]="description" [component]="component" <core-course-module-description *ngIf="displayDescription" [description]="description" [component]="component"
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>

View File

@ -47,6 +47,12 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<div *ngIf="description || pageIsOffline || hasOffline || pageWarning"> <div *ngIf="description || pageIsOffline || hasOffline || pageWarning">
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">

View File

@ -27,6 +27,12 @@
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Activity info. -->
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
(completionChanged)="onCompletionChange()">
</core-course-module-info>
<ion-card class="with-borders" *ngIf="phases"> <ion-card class="with-borders" *ngIf="phases">
<ion-item button (click)="viewPhaseInfo()" detail="true"> <ion-item button (click)="viewPhaseInfo()" detail="true">
<ion-label> <ion-label>

View File

@ -122,7 +122,14 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
this.syncIcon = CoreConstants.ICON_LOADING; this.syncIcon = CoreConstants.ICON_LOADING;
try { 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); await this.loadContent(true, sync, showErrors);
} finally { } finally {

View File

@ -70,12 +70,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
isDestroyed = false; // Whether the component is destroyed, used when calling fillContextMenu. isDestroyed = false; // Whether the component is destroyed, used when calling fillContextMenu.
contextMenuStatusObserver?: CoreEventObserver; // Observer of package status, used when calling fillContextMenu. contextMenuStatusObserver?: CoreEventObserver; // Observer of package status, used when calling fillContextMenu.
contextFileStatusObserver?: CoreEventObserver; // Observer of file 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 fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents.
protected isCurrentView = false; // Whether the component is in the current view. protected isCurrentView = false; // Whether the component is in the current view.
protected siteId?: string; // Current Site ID. protected siteId?: string; // Current Site ID.
protected statusObserver?: CoreEventObserver; // Observer of package status. Only if setStatusListener is called. 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 currentStatus?: string; // The current status of the module. Only if setStatusListener is called.
protected completionObserver?: CoreEventObserver;
protected logger: CoreLogger; protected logger: CoreLogger;
constructor( constructor(
@ -94,6 +96,18 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
this.componentId = this.module.id; this.componentId = this.module.id;
this.externalUrl = this.module.url; this.externalUrl = this.module.url;
this.courseId = this.courseId || this.module.course!; 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(); this.blog = await AddonBlog.isPluginEnabled();
} }
@ -140,7 +154,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
this.refreshIcon = CoreConstants.ICON_LOADING; this.refreshIcon = CoreConstants.ICON_LOADING;
try { 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); await this.loadContent(true);
} finally { } finally {
@ -376,6 +397,16 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
return result; return result;
} }
/**
* The completion of the modules has changed.
*
* @return Promise resolved when done.
*/
async onCompletionChange(): Promise<void> {
// Nothing to do.
return;
}
/** /**
* Component being destroyed. * Component being destroyed.
*/ */
@ -384,6 +415,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
this.contextMenuStatusObserver?.off(); this.contextMenuStatusObserver?.off();
this.contextFileStatusObserver?.off(); this.contextFileStatusObserver?.off();
this.statusObserver?.off(); this.statusObserver?.off();
this.completionObserver?.off();
} }
/** /**

View File

@ -14,8 +14,6 @@
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; 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'; import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
/** /**
@ -32,7 +30,7 @@ export class CoreCourseModuleCompletionBaseComponent implements OnChanges {
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when completion changes. @Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when completion changes.
/** /**
* Detect changes on input properties. * @inheritdoc
*/ */
ngOnChanges(changes: { [name: string]: SimpleChange }): void { ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (changes.completion && this.completion) { if (changes.completion && this.completion) {
@ -47,47 +45,4 @@ export class CoreCourseModuleCompletionBaseComponent implements OnChanges {
return; return;
} }
/**
* Completion clicked.
*
* @param e The click event.
*/
async completionClicked(e: Event): Promise<void> {
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();
}
}
} }

View File

@ -24,6 +24,8 @@ import { CoreCourseSectionSelectorComponent } from './section-selector/section-s
import { CoreCourseTagAreaComponent } from './tag-area/tag-area'; import { CoreCourseTagAreaComponent } from './tag-area/tag-area';
import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module'; import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module';
import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; 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({ @NgModule({
declarations: [ declarations: [
@ -32,6 +34,8 @@ import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-l
CoreCourseModuleCompletionComponent, CoreCourseModuleCompletionComponent,
CoreCourseModuleCompletionLegacyComponent, CoreCourseModuleCompletionLegacyComponent,
CoreCourseModuleDescriptionComponent, CoreCourseModuleDescriptionComponent,
CoreCourseModuleInfoComponent,
CoreCourseModuleManualCompletionComponent,
CoreCourseSectionSelectorComponent, CoreCourseSectionSelectorComponent,
CoreCourseTagAreaComponent, CoreCourseTagAreaComponent,
CoreCourseUnsupportedModuleComponent, CoreCourseUnsupportedModuleComponent,
@ -46,6 +50,8 @@ import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-l
CoreCourseModuleCompletionComponent, CoreCourseModuleCompletionComponent,
CoreCourseModuleCompletionLegacyComponent, CoreCourseModuleCompletionLegacyComponent,
CoreCourseModuleDescriptionComponent, CoreCourseModuleDescriptionComponent,
CoreCourseModuleInfoComponent,
CoreCourseModuleManualCompletionComponent,
CoreCourseSectionSelectorComponent, CoreCourseSectionSelectorComponent,
CoreCourseTagAreaComponent, CoreCourseTagAreaComponent,
CoreCourseUnsupportedModuleComponent, CoreCourseUnsupportedModuleComponent,

View File

@ -19,6 +19,7 @@ import { CoreCourseProvider } from '@features/course/services/course';
import { CoreFilterHelper } from '@features/filter/services/filter-helper'; import { CoreFilterHelper } from '@features/filter/services/filter-helper';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion'; 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. * 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); this.completionDescription = Translate.instant(langKey, translateParams);
} }
/**
* Completion clicked.
*
* @param event The click event.
*/
async completionClicked(event: Event): Promise<void> {
if (!this.completion) {
return;
}
await CoreCourseHelper.changeManualCompletion(this.completion, event);
this.calculateData();
this.completionChanged.emit(this.completion);
}
} }

View File

@ -27,27 +27,6 @@
</ng-container> </ng-container>
</div> </div>
<div *ngIf="completion && !completion.isautomatic && (showCompletionConditions || showManualCompletion)" <core-course-module-manual-completion *ngIf="showManualCompletion" [completion]="completion" [moduleName]="moduleName"
class="core-module-manual-completion"> (completionChanged)="completionChanged.emit($event)">
</core-course-module-manual-completion>
<ng-container *ngIf="completion.istrackeduser">
<ng-container *ngIf="completion.state">
<ion-button color="success" fill="outline" [attr.aria-label]="accessibleDescription"
(click)="completionClicked($event)">
<ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon>
{{ 'core.course.completion_manual:done' | translate }}
</ion-button>
</ng-container>
<ng-container *ngIf="!completion.state">
<ion-button color="light" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)">
{{ 'core.course.completion_manual:markdone' | translate }}
</ion-button>
</ng-container>
</ng-container>
<ng-container *ngIf="!completion.istrackeduser">
<ion-button disabled="true" color="light">
{{ 'core.course.completion_manual:markdone' | translate }}
</ion-button>
</ng-container>
</div>

View File

@ -9,10 +9,4 @@
} }
} }
} }
.core-module-manual-completion {
ion-button {
text-transform: none;
}
}
} }

View File

@ -35,7 +35,7 @@ import { Translate } from '@singletons';
export class CoreCourseModuleCompletionComponent extends CoreCourseModuleCompletionBaseComponent { export class CoreCourseModuleCompletionComponent extends CoreCourseModuleCompletionBaseComponent {
@Input() showCompletionConditions = false; // Whether to show activity completion conditions. @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[]; details?: CompletionRule[];
accessibleDescription: string | null = null; accessibleDescription: string | null = null;
@ -48,21 +48,6 @@ export class CoreCourseModuleCompletionComponent extends CoreCourseModuleComplet
return; 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. // Format rules.
this.details = this.completion.details.map((rule: CompletionRule) => { this.details = this.completion.details.map((rule: CompletionRule) => {
rule.statuscomplete = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE || rule.statuscomplete = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE ||

View File

@ -0,0 +1,18 @@
<ion-card *ngIf="module.dates?.length || (module.completiondata && (module.completiondata.isautomatic || showManualCompletion))">
<ion-item class="ion-text-wrap">
<ion-label>
<!-- Activity dates. -->
<div *ngIf="module.dates && module.dates.length" class="core-module-dates">
<p *ngFor="let date of module.dates">
<strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}
</p>
</div>
<!-- Module completion. -->
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata"
[moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="true"
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
</core-course-module-completion>
</ion-label>
</ion-item>
</ion-card>

View File

@ -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<CoreCourseModuleCompletionData>(); // Notify when completion changes.
}

View File

@ -0,0 +1,23 @@
<div *ngIf="completion && !completion.isautomatic" class="core-module-manual-completion">
<ng-container *ngIf="completion.istrackeduser">
<ng-container *ngIf="completion.state">
<ion-button color="success" fill="outline" [attr.aria-label]="accessibleDescription"
(click)="completionClicked($event)">
<ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon>
{{ 'core.course.completion_manual:done' | translate }}
</ion-button>
</ng-container>
<ng-container *ngIf="!completion.state">
<ion-button color="light" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)">
{{ 'core.course.completion_manual:markdone' | translate }}
</ion-button>
</ng-container>
</ng-container>
<ng-container *ngIf="!completion.istrackeduser">
<ion-button disabled="true" color="light">
{{ 'core.course.completion_manual:markdone' | translate }}
</ion-button>
</ng-container>
</div>

View File

@ -0,0 +1,5 @@
:host {
ion-button {
text-transform: none;
}
}

View File

@ -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<CoreCourseModuleCompletionData>(); // 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<void> {
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();
}
}

View File

@ -67,6 +67,7 @@ import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreSiteHome } from '@features/sitehome/services/sitehome'; import { CoreSiteHome } from '@features/sitehome/services/sitehome';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreSiteHomeHomeHandlerService } from '@features/sitehome/services/handlers/sitehome-home'; import { CoreSiteHomeHomeHandlerService } from '@features/sitehome/services/handlers/sitehome-home';
import { CoreStatusWithWarningsWSResponse } from '@services/ws';
/** /**
* Prefetch info of a module. * Prefetch info of a module.
@ -1890,6 +1891,52 @@ export class CoreCourseHelperProvider {
await Promise.all(promises); 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<CoreStatusWithWarningsWSResponse | void> {
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); export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider);

View File

@ -150,9 +150,12 @@ export class CoreCourseProvider {
* @param completion Completion status of the module. * @param completion Completion status of the module.
*/ */
checkModuleCompletion(courseId: number, completion?: CoreCourseModuleCompletionData): void { 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(() => { 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. // Ignore errors, shouldn't happen.
} }
// Invalidate module now, completion has changed.
await this.invalidateModule(cmId, siteId);
return result; return result;
} catch (error) { } catch (error) {
if (CoreUtils.isWebServiceError(error) || !courseId) { if (CoreUtils.isWebServiceError(error) || !courseId) {

View File

@ -18,6 +18,7 @@ import { CoreLogger } from '@singletons/logger';
import { CoreSite, CoreSiteInfoResponse, CoreSitePublicConfigResponse } from '@classes/site'; import { CoreSite, CoreSiteInfoResponse, CoreSitePublicConfigResponse } from '@classes/site';
import { CoreFilepoolComponentFileEventData } from '@services/filepool'; import { CoreFilepoolComponentFileEventData } from '@services/filepool';
import { CoreNavigationOptions } from '@services/navigator'; import { CoreNavigationOptions } from '@services/navigator';
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
/** /**
* Observer instance to stop listening to an event. * Observer instance to stop listening to an event.
@ -45,6 +46,7 @@ export interface CoreEventsData {
[CoreEvents.NOTIFICATION_SOUND_CHANGED]: CoreEventNotificationSoundChangedData; [CoreEvents.NOTIFICATION_SOUND_CHANGED]: CoreEventNotificationSoundChangedData;
[CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData; [CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData;
[CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData; [CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData;
[CoreEvents.MANUAL_COMPLETION_CHANGED]: CoreEventManualCompletionChangedData;
[CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData; [CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData;
[CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData; [CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData;
[CoreEvents.IAB_LOAD_START]: InAppBrowserEvent; [CoreEvents.IAB_LOAD_START]: InAppBrowserEvent;
@ -53,7 +55,7 @@ export interface CoreEventsData {
[CoreEvents.COMPONENT_FILE_ACTION]: CoreFilepoolComponentFileEventData; [CoreEvents.COMPONENT_FILE_ACTION]: CoreFilepoolComponentFileEventData;
[CoreEvents.FILE_SHARED]: CoreEventFileSharedData; [CoreEvents.FILE_SHARED]: CoreEventFileSharedData;
[CoreEvents.APP_LAUNCHED_URL]: CoreEventAppLaunchedData; [CoreEvents.APP_LAUNCHED_URL]: CoreEventAppLaunchedData;
}; }
/* /*
* Service to send and listen to events. * Service to send and listen to events.
@ -72,6 +74,7 @@ export class CoreEvents {
static readonly SITE_UPDATED = 'site_updated'; static readonly SITE_UPDATED = 'site_updated';
static readonly SITE_DELETED = 'site_deleted'; static readonly SITE_DELETED = 'site_deleted';
static readonly COMPLETION_MODULE_VIEWED = 'completion_module_viewed'; static readonly COMPLETION_MODULE_VIEWED = 'completion_module_viewed';
static readonly MANUAL_COMPLETION_CHANGED = 'manual_completion_changed';
static readonly USER_DELETED = 'user_deleted'; static readonly USER_DELETED = 'user_deleted';
static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed'; static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed';
static readonly COURSE_STATUS_CHANGED = 'course_status_changed'; static readonly COURSE_STATUS_CHANGED = 'course_status_changed';
@ -330,7 +333,15 @@ export type CoreEventSelectCourseTabData = {
* Data passed to COMPLETION_MODULE_VIEWED event. * Data passed to COMPLETION_MODULE_VIEWED event.
*/ */
export type CoreEventCompletionModuleViewedData = { export type CoreEventCompletionModuleViewedData = {
courseId?: number; courseId: number;
cmId?: number;
};
/**
* Data passed to MANUAL_COMPLETION_CHANGED event.
*/
export type CoreEventManualCompletionChangedData = {
completion: CoreCourseModuleCompletionData;
}; };
/** /**