MOBILE-3757 course: Display completion in course page
parent
962cd43d9e
commit
3f825db799
|
@ -71,5 +71,12 @@ export class AddonModLabelModuleHandlerService implements CoreCourseModuleHandle
|
|||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
manualCompletionAlwaysShown(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModLabelModuleHandler = makeSingleton(AddonModLabelModuleHandlerService);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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<CoreCourseModuleCompletionData>(); // 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<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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<img *ngIf="completion && completion.tracking !== 1" [src]="completionImage" [alt]="completionDescription">
|
||||
|
||||
<ion-button
|
||||
fill="clear"
|
||||
*ngIf="completion && completion.tracking === 1"
|
||||
(click)="completionClicked($event)"
|
||||
[title]="completionDescription">
|
||||
<img [src]="completionImage" role="presentation" alt="">
|
||||
</ion-button>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
*
|
||||
* <core-course-module-completion-legacy [completion]="module.completiondata" [moduleName]="module.name"
|
||||
* (completionChanged)="completionChanged()"></core-course-module-completion-legacy>
|
||||
*/
|
||||
@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<void> {
|
||||
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<string, unknown> = {
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +1,53 @@
|
|||
<img *ngIf="completion && completion.tracking !== 1" [src]="completionImage" [alt]="completionDescription">
|
||||
<div *ngIf="showCompletionConditions && completion && completion.isautomatic" class="core-module-automatic-completion-conditions"
|
||||
role="list" [attr.aria-label]="'core.course.completionrequirements' | translate:{ $a: moduleName }">
|
||||
|
||||
<ion-button
|
||||
fill="clear"
|
||||
*ngIf="completion && completion.tracking === 1"
|
||||
(click)="completionClicked($event)"
|
||||
[title]="completionDescription">
|
||||
<img [src]="completionImage" role="presentation" alt="">
|
||||
</ion-button>
|
||||
<ng-container *ngIf="completion.istrackeduser">
|
||||
<ng-container *ngFor="let rule of details">
|
||||
<ion-badge *ngIf="rule.statuscomplete" color="success" role="listitem"
|
||||
[attr.aria-label]="rule.accessibleDescription">
|
||||
<strong>{{ 'core.course.completion_automatic:done' | translate }}</strong> {{ rule.rulevalue.description }}
|
||||
</ion-badge>
|
||||
|
||||
<ion-badge *ngIf="rule.statuscompletefail" color="danger" role="listitem"
|
||||
[attr.aria-label]="rule.accessibleDescription">
|
||||
<strong>{{ 'core.course.completion_automatic:failed' | translate }}</strong> {{ rule.rulevalue.description }}
|
||||
</ion-badge>
|
||||
|
||||
<ion-badge *ngIf="rule.statusincomplete" color="medium" role="listitem"
|
||||
[attr.aria-label]="rule.accessibleDescription">
|
||||
<strong>{{ 'core.course.completion_automatic:todo' | translate }}</strong> {{ rule.rulevalue.description }}
|
||||
</ion-badge>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!completion.istrackeduser">
|
||||
<ion-badge *ngFor="let rule of details" color="light" role="listitem">
|
||||
{{ rule.rulevalue.description }}
|
||||
</ion-badge>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="completion && !completion.isautomatic && (showCompletionConditions || showManualCompletion)"
|
||||
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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<CoreCourseModuleCompletionData>(); // 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<void> {
|
||||
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<void> {
|
||||
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<string, unknown> = {
|
||||
$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;
|
||||
};
|
||||
|
|
|
@ -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}"
|
||||
>
|
||||
<!-- Module completion. -->
|
||||
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata"
|
||||
[moduleName]="module.name" [moduleId]="module.id" (completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-completion>
|
||||
<!-- Module completion (legacy). -->
|
||||
<core-course-module-completion-legacy *ngIf="module.completiondata && showLegacyCompletion"
|
||||
[completion]="module.completiondata" [moduleName]="module.name" [moduleId]="module.id"
|
||||
(completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-completion-legacy>
|
||||
|
||||
<div class="core-module-buttons-more">
|
||||
<core-download-refresh [status]="downloadStatus" [enabled]="downloadEnabled"
|
||||
|
@ -84,11 +85,19 @@
|
|||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<!-- Activity dates. -->
|
||||
<div *ngIf="showActivityDates && 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]="showCompletionConditions"
|
||||
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-completion>
|
||||
|
||||
<core-format-text class="core-module-description" *ngIf="module.description" maxHeight="80" [text]="module.description"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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?",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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<CoreCourseModu
|
|||
return result ?? defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return !!this.executeFunctionOnEnabled<boolean>(module.modname, 'manualCompletionAlwaysShown', [module]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreCourseModuleDelegate = makeSingleton(CoreCourseModuleDelegateService);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -848,6 +848,7 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo
|
|||
coursepagemethod?: string;
|
||||
ptrenabled?: boolean;
|
||||
supportedfeatures?: Record<string, unknown>;
|
||||
manualcompletionalwaysshown?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue