MOBILE-3757 course: Display completion in course page

main
Dani Palou 2021-05-06 12:35:32 +02:00
parent 962cd43d9e
commit 3f825db799
18 changed files with 472 additions and 165 deletions

View File

@ -71,5 +71,12 @@ export class AddonModLabelModuleHandlerService implements CoreCourseModuleHandle
return;
}
/**
* @inheritdoc
*/
manualCompletionAlwaysShown(): boolean {
return true;
}
}
export const AddonModLabelModuleHandler = makeSingleton(AddonModLabelModuleHandlerService);

View File

@ -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,
};
}

View File

@ -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();
}
}
}

View File

@ -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,

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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;
};

View File

@ -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>

View File

@ -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) {

View File

@ -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?",

View File

@ -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;
}

View File

@ -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 = {

View File

@ -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);

View File

@ -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;
}
}

View File

@ -848,6 +848,7 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo
coursepagemethod?: string;
ptrenabled?: boolean;
supportedfeatures?: Record<string, unknown>;
manualcompletionalwaysshown?: boolean;
};
/**