Skip to content

Commit 0b6baec

Browse files
committed
MOBILE-4842 coursecompletion: Use signals in report
1 parent 264d626 commit 0b6baec

File tree

4 files changed

+135
-74
lines changed

4 files changed

+135
-74
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// (C) Copyright 2015 Moodle Pty Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/**
16+
* Course completion criteria aggregation method.
17+
*/
18+
export enum AddonCourseCompletionAggregation {
19+
ALL = 1,
20+
ANY = 2,
21+
}
22+
23+
/**
24+
* Criteria type constant, primarily for storing criteria type in the database.
25+
*/
26+
export enum AddonCourseCompletionCriteriaType {
27+
SELF = 1, // Self completion criteria type.
28+
DATE = 2, // Date completion criteria type.
29+
UNENROL = 3, // Unenrol completion criteria type.
30+
ACTIVITY = 4, // Activity completion criteria type.
31+
DURATION = 5, // Duration completion criteria type.
32+
GRADE = 6, // Grade completion criteria type.
33+
ROLE = 7, // Role completion criteria type.
34+
COURSE = 8, // Course completion criteria type.
35+
}

src/addons/coursecompletion/pages/report/report.html

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,35 @@ <h1>{{ 'addon.coursecompletion.coursecompletion' | translate }}</h1>
99
</ion-toolbar>
1010
</ion-header>
1111
<ion-content>
12-
<ion-refresher slot="fixed" [disabled]="!completionLoaded" (ionRefresh)="refreshCompletion($event.target)">
12+
<ion-refresher slot="fixed" [disabled]="!loaded()" (ionRefresh)="refreshCompletion($event.target)">
1313
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" />
1414
</ion-refresher>
15-
<core-loading [hideUntil]="completionLoaded">
16-
@if (user) {
15+
<core-loading [hideUntil]="loaded()">
16+
@let userDisplay = user();
17+
@if (userDisplay) {
1718
<ion-item class="ion-text-wrap">
18-
<core-user-avatar [user]="user" [courseId]="courseId" slot="start" [linkProfile]="false" />
19+
<core-user-avatar [user]="userDisplay" [courseId]="courseId" slot="start" [linkProfile]="false" />
1920
<ion-label>
20-
<p class="item-heading">{{user.fullname}}</p>
21+
<p class="item-heading">{{userDisplay.fullname}}</p>
2122
</ion-label>
2223
</ion-item>
2324
}
2425

25-
@if (completion && tracked) {
26+
@let completionDisplay = completion();
27+
@if (completionDisplay && tracked()) {
2628
<ion-card>
2729
<ion-item class="ion-text-wrap">
2830
<ion-label>
2931
<p class="item-heading">{{ 'addon.coursecompletion.status' | translate }}</p>
30-
<p>{{ statusText! | translate }}</p>
32+
<p>{{ statusText() | translate }}</p>
3133
</ion-label>
3234
</ion-item>
3335
<ion-item class="ion-text-wrap">
3436
<ion-label>
3537
<p class="item-heading">{{ 'addon.coursecompletion.required' | translate }}</p>
36-
@if (completion.aggregation === 1) {
38+
@if (completionDisplay.aggregation === aggregationType.ALL) {
3739
<p>{{ 'addon.coursecompletion.criteriarequiredall' | translate }}</p>
38-
}
39-
@if (completion.aggregation === 2) {
40+
} @else if (completionDisplay.aggregation === aggregationType.ANY) {
4041
<p>{{ 'addon.coursecompletion.criteriarequiredany' | translate }}</p>
4142
}
4243
</ion-label>
@@ -48,21 +49,25 @@ <h1>{{ 'addon.coursecompletion.coursecompletion' | translate }}</h1>
4849
<h2>{{ 'addon.coursecompletion.requiredcriteria' | translate }}</h2>
4950
</ion-label>
5051
</ion-item-divider>
51-
<ion-item class="ion-hide-md-up ion-text-wrap" *ngFor="let criteria of completion.completions">
52-
<ion-label>
53-
<p class="item-heading">
54-
<core-format-text clean="true" [text]="criteria.details.criteria" [filter]="false" />
55-
</p>
56-
<p>
57-
<core-format-text clean="true" [text]="criteria.details.requirement" [filter]="false" />
58-
</p>
59-
</ion-label>
60-
@if (criteria.complete) {
61-
<strong slot="end">{{ 'core.yes' | translate }}</strong>
62-
} @else {
63-
<strong slot="end">{{ 'core.no' | translate }}</strong>
64-
}
65-
</ion-item>
52+
@for (criteria of completionDisplay.completions; track criteria) {
53+
<ion-item class="ion-hide-md-up ion-text-wrap">
54+
<ion-label>
55+
<p class="item-heading">
56+
<core-format-text clean="true" [text]="criteria.details.criteria" [filter]="false" />
57+
</p>
58+
<p>
59+
<core-format-text clean="true" [text]="criteria.details.requirement" [filter]="false" />
60+
</p>
61+
</ion-label>
62+
<strong slot="end">
63+
@if (criteria.complete) {
64+
{{ 'core.yes' | translate }}
65+
} @else {
66+
{{ 'core.no' | translate }}
67+
}
68+
</strong>
69+
</ion-item>
70+
}
6671
<ion-item class="ion-hide-md-down ion-text-wrap">
6772
<ion-label>
6873
<ion-row>
@@ -73,7 +78,8 @@ <h2>{{ 'addon.coursecompletion.requiredcriteria' | translate }}</h2>
7378
<ion-col><strong>{{ 'addon.coursecompletion.complete' | translate }}</strong></ion-col>
7479
<ion-col><strong>{{ 'addon.coursecompletion.completiondate' | translate }}</strong></ion-col>
7580
</ion-row>
76-
<ion-row *ngFor="let criteria of completion.completions">
81+
@for (criteria of completionDisplay.completions; track criteria) {
82+
<ion-row>
7783
<ion-col>
7884
<core-format-text clean="true" [text]="criteria.details.type" [filter]="false" />
7985
</ion-col>
@@ -86,24 +92,25 @@ <h2>{{ 'addon.coursecompletion.requiredcriteria' | translate }}</h2>
8692
<ion-col>
8793
<core-format-text [text]="criteria.details.status" [filter]="false" />
8894
</ion-col>
89-
@if (criteria.complete) {
90-
<ion-col>{{ 'core.yes' | translate }}</ion-col>
91-
} @else {
92-
<ion-col>{{ 'core.no' | translate }}</ion-col>
93-
}
94-
@if (criteria.timecompleted) {
95-
<ion-col>
95+
<ion-col>
96+
@if (criteria.complete) {
97+
{{ 'core.yes' | translate }}
98+
} @else {
99+
{{ 'core.no' | translate }}
100+
}
101+
</ion-col>
102+
<ion-col>
103+
@if (criteria.timecompleted) {
96104
{{ criteria.timecompleted * 1000 | coreFormatDate :'strftimedatetimeshort' }}
97-
</ion-col>
98-
} @else {
99-
<ion-col />
100-
}
105+
}
106+
</ion-col>
101107
</ion-row>
108+
}
102109
</ion-label>
103110
</ion-item>
104111
</ion-card>
105112
}
106-
@if (showSelfComplete && tracked) {
113+
@if (showSelfComplete() && tracked()) {
107114
<ion-card>
108115
<ion-item-divider>
109116
<ion-label>
@@ -118,7 +125,7 @@ <h2>{{ 'addon.coursecompletion.manualselfcompletion' | translate }}</h2>
118125
</ion-label>
119126
</ion-item>
120127
</ion-card>
121-
} @else if (!tracked) {
128+
} @else if (!tracked()) {
122129
<ion-card class="core-warning-card">
123130
<ion-item>
124131
<ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" />

src/addons/coursecompletion/pages/report/report.ts

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
AddonCourseCompletion,
1717
AddonCourseCompletionCourseCompletionStatus,
1818
} from '@addons/coursecompletion/services/coursecompletion';
19-
import { Component, OnInit } from '@angular/core';
19+
import { Component, computed, OnInit, signal } from '@angular/core';
2020
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
2121
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
2222
import { CoreLoadings } from '@services/overlays/loadings';
@@ -26,6 +26,7 @@ import { Translate } from '@singletons';
2626
import { CoreTime } from '@singletons/time';
2727
import { CoreAlerts } from '@services/overlays/alerts';
2828
import { CoreSharedModule } from '@/core/shared.module';
29+
import { AddonCourseCompletionAggregation } from '@addons/coursecompletion/constants';
2930

3031
/**
3132
* Page that displays the course completion report.
@@ -39,26 +40,56 @@ import { CoreSharedModule } from '@/core/shared.module';
3940
})
4041
export default class AddonCourseCompletionReportPage implements OnInit {
4142

42-
protected userId!: number;
43-
protected logView: () => void;
43+
protected readonly aggregationType = AddonCourseCompletionAggregation;
44+
protected readonly userId = signal(0);
45+
protected logView!: () => void;
4446

45-
courseId!: number;
46-
completionLoaded = false;
47-
completion?: AddonCourseCompletionCourseCompletionStatus;
48-
showSelfComplete = false;
49-
tracked = true; // Whether completion is tracked.
50-
statusText?: string;
51-
user?: CoreUserProfile;
47+
readonly courseId!: number;
48+
readonly loaded = signal(false);
49+
readonly completion = signal<AddonCourseCompletionCourseCompletionStatus | undefined>(undefined);
50+
51+
readonly showSelfComplete = computed(() => {
52+
const completion = this.completion();
53+
const userId = this.userId();
54+
55+
if (!completion) {
56+
return false;
57+
}
58+
59+
return AddonCourseCompletion.canMarkSelfCompleted(userId, completion);
60+
});
61+
62+
readonly tracked = signal(true); // Whether completion is tracked.
63+
readonly statusText = computed(() => {
64+
const completion = this.completion();
65+
if (!completion) {
66+
return '';
67+
}
68+
69+
return AddonCourseCompletion.getCompletedStatusText(completion);
70+
});
71+
72+
readonly user = signal<CoreUserProfile | undefined>(undefined);
5273

5374
constructor() {
75+
try {
76+
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
77+
this.userId.set(CoreNavigator.getRouteNumberParam('userId') || CoreSites.getCurrentSiteUserId());
78+
} catch (error) {
79+
CoreAlerts.showError(error);
80+
CoreNavigator.back();
81+
82+
return;
83+
}
84+
5485
this.logView = CoreTime.once(() => {
5586
CoreAnalytics.logEvent({
5687
type: CoreAnalyticsEventType.VIEW_ITEM,
5788
ws: 'core_completion_get_course_completion_status',
5889
name: Translate.instant('addon.coursecompletion.coursecompletion'),
5990
data: {
6091
course: this.courseId,
61-
user: this.userId,
92+
user: this.userId(),
6293
},
6394
url: `/blocks/completionstatus/details.php?course=${this.courseId}&user=${this.userId}`,
6495
});
@@ -69,18 +100,8 @@ export default class AddonCourseCompletionReportPage implements OnInit {
69100
* @inheritdoc
70101
*/
71102
ngOnInit(): void {
72-
try {
73-
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
74-
this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getCurrentSiteUserId();
75-
} catch (error) {
76-
CoreAlerts.showError(error);
77-
CoreNavigator.back();
78-
79-
return;
80-
}
81-
82103
this.fetchCompletion().finally(() => {
83-
this.completionLoaded = true;
104+
this.loaded.set(true);
84105
});
85106
}
86107

@@ -89,19 +110,16 @@ export default class AddonCourseCompletionReportPage implements OnInit {
89110
*/
90111
protected async fetchCompletion(): Promise<void> {
91112
try {
92-
this.user = await CoreUser.getProfile(this.userId, this.courseId, true);
93-
94-
this.completion = await AddonCourseCompletion.getCompletion(this.courseId, this.userId);
113+
this.user.set(await CoreUser.getProfile(this.userId(), this.courseId, true));
95114

96-
this.statusText = AddonCourseCompletion.getCompletedStatusText(this.completion);
97-
this.showSelfComplete = AddonCourseCompletion.canMarkSelfCompleted(this.userId, this.completion);
115+
this.completion.set(await AddonCourseCompletion.getCompletion(this.courseId, this.userId()));
98116

99-
this.tracked = true;
117+
this.tracked.set(true);
100118
this.logView();
101119
} catch (error) {
102-
if (error && error.errorcode == 'notenroled') {
120+
if (error?.errorcode === 'notenroled') {
103121
// Not enrolled error, probably a teacher.
104-
this.tracked = false;
122+
this.tracked.set(false);
105123
} else {
106124
CoreAlerts.showError(error, { default: Translate.instant('addon.coursecompletion.couldnotloadreport') });
107125
}
@@ -114,7 +132,7 @@ export default class AddonCourseCompletionReportPage implements OnInit {
114132
* @param refresher Refresher instance.
115133
*/
116134
async refreshCompletion(refresher?: HTMLIonRefresherElement): Promise<void> {
117-
await AddonCourseCompletion.invalidateCourseCompletion(this.courseId, this.userId).finally(() => {
135+
await AddonCourseCompletion.invalidateCourseCompletion(this.courseId, this.userId()).finally(() => {
118136
this.fetchCompletion().finally(() => {
119137
refresher?.complete();
120138
});

src/addons/coursecompletion/services/coursecompletion.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-si
2727
import { firstValueFrom } from 'rxjs';
2828
import { CoreCacheUpdateFrequency } from '@/core/constants';
2929
import { CoreCourseCompletion } from '@features/course/services/course-completion';
30+
import { AddonCourseCompletionAggregation, AddonCourseCompletionCriteriaType } from '../constants';
3031

3132
/**
3233
* Service to handle course completion.
@@ -70,15 +71,15 @@ export class AddonCourseCompletionProvider {
7071
* @returns True if user can mark course as self completed, false otherwise.
7172
*/
7273
canMarkSelfCompleted(userId: number, completion: AddonCourseCompletionCourseCompletionStatus): boolean {
73-
if (CoreSites.getCurrentSiteUserId() != userId) {
74+
if (CoreSites.getCurrentSiteUserId() !== userId) {
7475
return false;
7576
}
7677

7778
let selfCompletionActive = false;
7879
let alreadyMarked = false;
7980

8081
completion.completions.forEach((criteria) => {
81-
if (criteria.type === 1) {
82+
if (criteria.type === AddonCourseCompletionCriteriaType.SELF) {
8283
// Self completion criteria found.
8384
selfCompletionActive = true;
8485
alreadyMarked = criteria.complete;
@@ -313,9 +314,9 @@ export const AddonCourseCompletion = makeSingleton(AddonCourseCompletionProvider
313314
*/
314315
export type AddonCourseCompletionCourseCompletionStatus = {
315316
completed: boolean; // True if the course is complete, false otherwise.
316-
aggregation: number; // Aggregation method 1 means all, 2 means any.
317+
aggregation: AddonCourseCompletionAggregation; // Aggregation method 1 means all, 2 means any.
317318
completions: {
318-
type: number; // Completion criteria type.
319+
type: AddonCourseCompletionCriteriaType; // Completion criteria type.
319320
title: string; // Completion criteria Title.
320321
status: string; // Completion status (Yes/No) a % or number.
321322
complete: boolean; // Completion status (true/false).

0 commit comments

Comments
 (0)