Skip to content

Commit 5c6744a

Browse files
committed
MOBILE-4639 badges: Support links to badges/badgeclass.php?id=X
1 parent 38ae12f commit 5c6744a

File tree

7 files changed

+369
-1
lines changed

7 files changed

+369
-1
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
import { NgModule } from '@angular/core';
16+
import { RouterModule, Routes } from '@angular/router';
17+
import { AddonBadgesBadgeClassPage } from './pages/badge-class/badge-class';
18+
import { CoreSharedModule } from '@/core/shared.module';
19+
20+
const routes: Routes = [
21+
{
22+
path: ':badgeId',
23+
component: AddonBadgesBadgeClassPage,
24+
},
25+
];
26+
27+
@NgModule({
28+
imports: [
29+
RouterModule.forChild(routes),
30+
CoreSharedModule,
31+
],
32+
declarations: [
33+
AddonBadgesBadgeClassPage,
34+
],
35+
})
36+
export class AddonBadgeClassLazyModule {}

src/addons/badges/badges.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Routes } from '@angular/router';
1717

1818
import { AddonBadgesMyBadgesLinkHandler } from './services/handlers/mybadges-link';
1919
import { AddonBadgesBadgeLinkHandler } from './services/handlers/badge-link';
20+
import { AddonBadgesBadgeClassLinkHandler } from './services/handlers/badgeclass-link';
2021
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
2122
import { CoreUserDelegate } from '@features/user/services/user-delegate';
2223
import { AddonBadgesUserHandler } from './services/handlers/user';
@@ -48,6 +49,10 @@ const mainMenuRoutes: Routes = [
4849
path: 'badges',
4950
loadChildren: () => import('./badges-lazy.module').then(m => m.AddonBadgesLazyModule),
5051
},
52+
{
53+
path: 'badgeclass',
54+
loadChildren: () => import('./badgeclass-lazy.module').then(m => m.AddonBadgeClassLazyModule),
55+
},
5156
];
5257

5358
@NgModule({
@@ -61,6 +66,7 @@ const mainMenuRoutes: Routes = [
6166
useValue: () => {
6267
CoreContentLinksDelegate.registerHandler(AddonBadgesMyBadgesLinkHandler.instance);
6368
CoreContentLinksDelegate.registerHandler(AddonBadgesBadgeLinkHandler.instance);
69+
CoreContentLinksDelegate.registerHandler(AddonBadgesBadgeClassLinkHandler.instance);
6470
CoreUserDelegate.registerHandler(AddonBadgesUserHandler.instance);
6571
CorePushNotificationsDelegate.registerClickHandler(AddonBadgesPushClickHandler.instance);
6672
CoreTagAreaDelegate.registerHandler(AddonBadgesTagAreaHandler.instance);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<ion-header>
2+
<ion-toolbar>
3+
<ion-buttons slot="start">
4+
<ion-back-button [text]="'core.back' | translate" />
5+
</ion-buttons>
6+
<ion-title>
7+
<h1 *ngIf="badge">{{ badge.name }}</h1>
8+
<h1 *ngIf="!badge">{{ 'addon.badges.badgedetails' | translate }}</h1>
9+
</ion-title>
10+
</ion-toolbar>
11+
</ion-header>
12+
<ion-content class="limited-width">
13+
<ion-refresher slot="fixed" [disabled]="!badgeLoaded" (ionRefresh)="refreshBadgeClass($event.target)">
14+
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" />
15+
</ion-refresher>
16+
<core-loading [hideUntil]="badgeLoaded">
17+
<ng-container *ngIf="badge">
18+
<ion-item-group>
19+
<ion-item class="ion-text-wrap ion-text-center">
20+
<ion-label>
21+
<img *ngIf="badge.image" class="large-avatar" [url]="badge.image" core-external-content [alt]="badge.name" />
22+
</ion-label>
23+
</ion-item>
24+
<ion-item class="ion-text-wrap" *ngIf="badge.name">
25+
<ion-label>
26+
<p class="item-heading">{{ 'core.name' | translate}}</p>
27+
<p>{{ badge.name }}</p>
28+
</ion-label>
29+
</ion-item>
30+
<ion-item class="ion-text-wrap" *ngIf="badge.issuer">
31+
<ion-label>
32+
<p class="item-heading">{{ 'addon.badges.issuername' | translate}}</p>
33+
<p>{{ badge.issuer }}</p>
34+
</ion-label>
35+
</ion-item>
36+
<ion-item class="ion-text-wrap" *ngIf="badge.coursefullname">
37+
<ion-label>
38+
<p class="item-heading">{{ 'core.course' | translate}}</p>
39+
<p>
40+
<core-format-text [text]="badge.coursefullname" contextLevel="course" [contextInstanceId]="badge.courseid" />
41+
</p>
42+
</ion-label>
43+
</ion-item>
44+
<ion-item class="ion-text-wrap" *ngIf="badge.description">
45+
<ion-label>
46+
<p class="item-heading">{{ 'core.description' | translate}}</p>
47+
<p>{{ badge.description }}</p>
48+
</ion-label>
49+
</ion-item>
50+
</ion-item-group>
51+
52+
<!-- Competencies alignment -->
53+
<ion-item-group *ngIf="badge.alignment?.length">
54+
<ion-item-divider>
55+
<ion-label>
56+
<h2>{{ 'addon.badges.alignment' | translate}}</h2>
57+
</ion-label>
58+
</ion-item-divider>
59+
<ion-item class="ion-text-wrap" *ngFor="let alignment of badge.alignment" [href]="alignment.targetUrl" core-link
60+
[autoLogin]="false">
61+
<ion-label>
62+
<p class="item-heading">{{ alignment.targetName }}</p>
63+
</ion-label>
64+
</ion-item>
65+
</ion-item-group>
66+
</ng-container>
67+
</core-loading>
68+
</ion-content>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
import { Component, OnInit } from '@angular/core';
16+
import { CoreDomUtils } from '@services/utils/dom';
17+
import { CoreUtils } from '@services/utils/utils';
18+
import { CoreNavigator } from '@services/navigator';
19+
import { ActivatedRoute } from '@angular/router';
20+
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
21+
import { CoreTime } from '@singletons/time';
22+
import { AddonBadges, AddonBadgesBadgeClass } from '../../services/badges';
23+
24+
/**
25+
* Page that displays a badge class.
26+
*/
27+
@Component({
28+
selector: 'page-addon-badges-badge-class',
29+
templateUrl: 'badge-class.html',
30+
})
31+
export class AddonBadgesBadgeClassPage implements OnInit {
32+
33+
protected badgeId = 0;
34+
protected logView: (badge: AddonBadgesBadgeClass) => void;
35+
36+
badge?: AddonBadgesBadgeClass;
37+
badgeLoaded = false;
38+
currentTime = 0;
39+
40+
constructor(protected route: ActivatedRoute) {
41+
this.badgeId = CoreNavigator.getRequiredRouteNumberParam('badgeId');
42+
43+
this.logView = CoreTime.once((badge) => {
44+
CoreAnalytics.logEvent({
45+
type: CoreAnalyticsEventType.VIEW_ITEM,
46+
ws: 'core_badges_get_badge',
47+
name: badge.name,
48+
data: { id: this.badgeId, category: 'badges' },
49+
url: `/badges/badgeclass.php?id=${this.badgeId}`,
50+
});
51+
});
52+
}
53+
54+
/**
55+
* View loaded.
56+
*/
57+
ngOnInit(): void {
58+
this.fetchBadgeClass().finally(() => {
59+
this.badgeLoaded = true;
60+
});
61+
}
62+
63+
/**
64+
* Fetch the badge class required for the view.
65+
*
66+
* @returns Promise resolved when done.
67+
*/
68+
async fetchBadgeClass(): Promise<void> {
69+
try {
70+
this.badge = await AddonBadges.getBadgeClass(this.badgeId);
71+
72+
this.logView(this.badge);
73+
} catch (message) {
74+
CoreDomUtils.showErrorModalDefault(message, 'Error getting badge data.');
75+
}
76+
}
77+
78+
/**
79+
* Refresh the badge class.
80+
*
81+
* @param refresher Refresher.
82+
*/
83+
async refreshBadgeClass(refresher?: HTMLIonRefresherElement): Promise<void> {
84+
await CoreUtils.ignoreErrors(AddonBadges.invalidateBadgeClass(this.badgeId));
85+
86+
await this.fetchBadgeClass();
87+
88+
refresher?.complete();
89+
}
90+
91+
}

src/addons/badges/services/badges.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,76 @@ export class AddonBadgesProvider {
165165
await site.invalidateWsCacheForKey(this.getUserBadgeByHashCacheKey(hash));
166166
}
167167

168+
/**
169+
* Get the cache key for the get badge class WS call.
170+
*
171+
* @param id Badge ID.
172+
* @returns Cache key.
173+
*/
174+
protected getBadgeClassCacheKey(id: number): string {
175+
return ROOT_CACHE_KEY + 'badgeclass:' + id;
176+
}
177+
178+
/**
179+
* Get badge class.
180+
*
181+
* @param id Badge ID.
182+
* @param siteId Site ID. If not defined, current site.
183+
* @returns Promise to be resolved when the badge is retrieved.
184+
* @since 4.5
185+
*/
186+
async getBadgeClass(id: number, siteId?: string): Promise<AddonBadgesBadgeClass> {
187+
const site = await CoreSites.getSite(siteId);
188+
const data: AddonBadgesGetBadgeClassWSParams = {
189+
id,
190+
};
191+
const preSets = {
192+
cacheKey: this.getBadgeClassCacheKey(id),
193+
updateFrequency: CoreSite.FREQUENCY_RARELY,
194+
};
195+
196+
const response = await site.read<AddonBadgesGetBadgeClassWSResponse>(
197+
'core_badges_get_badge',
198+
data,
199+
preSets,
200+
);
201+
const badge = response?.badge;
202+
if (!badge) {
203+
throw new CoreError('Invalid badge response');
204+
}
205+
206+
// Exclude alignments without targetName, we can't display them.
207+
badge.alignment = badge.alignment?.filter((alignment) => alignment.targetName);
208+
209+
return badge;
210+
}
211+
212+
/**
213+
* Invalidate get badge class WS call.
214+
*
215+
* @param id Badge ID.
216+
* @param siteId Site ID. If not defined, current site.
217+
* @returns Promise resolved when data is invalidated.ç
218+
* @since 4.5
219+
*/
220+
async invalidateBadgeClass(id: number, siteId?: string): Promise<void> {
221+
const site = await CoreSites.getSite(siteId);
222+
223+
await site.invalidateWsCacheForKey(this.getBadgeClassCacheKey(id));
224+
}
225+
226+
/**
227+
* Returns whether get badge class WS is available.
228+
*
229+
* @param siteId Site ID. If not defined, current site.
230+
* @returns If WS is available.
231+
*/
232+
async isGetBadgeClassAvailable(siteId?: string): Promise<boolean> {
233+
const site = await CoreSites.getSite(siteId);
234+
235+
return site.wsAvailable('core_badges_get_badge');
236+
}
237+
168238
}
169239

170240
export const AddonBadges = makeSingleton(AddonBadgesProvider);
@@ -288,3 +358,44 @@ type AddonBadgesGetUserBadgeByHashWSResponse = {
288358
badge: AddonBadgesUserBadge[];
289359
warnings?: CoreWSExternalWarning[];
290360
};
361+
362+
/**
363+
* Params of core_badges_get_badge WS.
364+
*/
365+
type AddonBadgesGetBadgeClassWSParams = {
366+
id: number; // Badge ID.
367+
};
368+
369+
/**
370+
* Data returned by core_badges_get_badge WS.
371+
*/
372+
type AddonBadgesGetBadgeClassWSResponse = {
373+
badge: AddonBadgesBadgeClass;
374+
warnings?: CoreWSExternalWarning[];
375+
};
376+
377+
/**
378+
* Badge data returned by core_badges_get_badge WS.
379+
*/
380+
export type AddonBadgesBadgeClass = {
381+
type: string; // BadgeClass.
382+
id: string; // Unique identifier for this badgeclass (URL).
383+
issuer?: string; // Issuer for this badgeclass.
384+
name: string; // Name of the badgeclass.
385+
image: string; // URL to the image.
386+
description: string; // Description of the badge class.
387+
hostedUrl?: string; // Identifier of the open badge for this assertion.
388+
courseid?: number; // Course ID.
389+
coursefullname?: string; // Full name of the course.
390+
alignment?: { // Badge alignments.
391+
id?: number; // Alignment id.
392+
badgeid?: number; // Badge id.
393+
targetName?: string; // Target name.
394+
targetUrl?: string; // Target URL.
395+
targetDescription?: string; // Target description.
396+
targetFramework?: string; // Target framework.
397+
targetCode?: string; // Target code.
398+
}[];
399+
criteriaUrl?: string; // Criteria URL.
400+
criteriaNarrative?: string; // Criteria narrative.
401+
};

src/addons/badges/services/handlers/badge-link.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { makeSingleton } from '@singletons';
2121
import { AddonBadgesHelper } from '../badges-helper';
2222

2323
/**
24-
* Handler to treat links to user participants page.
24+
* Handler to treat links to issued badges.
2525
*/
2626
@Injectable({ providedIn: 'root' })
2727
export class AddonBadgesBadgeLinkHandlerService extends CoreContentLinksHandlerBase {

0 commit comments

Comments
 (0)