Skip to content

Commit

Permalink
Merge pull request #4159 from albertgasset/MOBILE-4574
Browse files Browse the repository at this point in the history
MOBILE-4574 badges: Support links to badges by hash
  • Loading branch information
dpalou authored Sep 2, 2024
2 parents e49f10e + 4a3fd0f commit d4a34ef
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 31 deletions.
33 changes: 33 additions & 0 deletions src/addons/badges/badge-lazy.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// (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 { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { AddonBadgesIssuedBadgePage } from './pages/issued-badge/issued-badge';

const routes: Routes = [
{
path: ':badgeHash',
component: AddonBadgesIssuedBadgePage,
data: { usesSwipeNavigation: false },
},
];

@NgModule({
imports: [
RouterModule.forChild(routes),
],
})
export class AddonBadgeLazyModule {}
3 changes: 2 additions & 1 deletion src/addons/badges/badges-lazy.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const mobileRoutes: Routes = [
{
path: ':badgeHash',
component: AddonBadgesIssuedBadgePage,
data: { usesSwipeNavigation: true },
},
];

Expand All @@ -42,6 +43,7 @@ const tabletRoutes: Routes = [
{
path: ':badgeHash',
component: AddonBadgesIssuedBadgePage,
data: { usesSwipeNavigation: true },
},
],
},
Expand All @@ -59,7 +61,6 @@ const routes: Routes = [
],
declarations: [
AddonBadgesUserBadgesPage,
AddonBadgesIssuedBadgePage,
],
})
export class AddonBadgesLazyModule {}
4 changes: 4 additions & 0 deletions src/addons/badges/badges.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export async function getBadgesServices(): Promise<Type<unknown>[]> {
}

const mainMenuRoutes: Routes = [
{
path: 'badge',
loadChildren: () => import('./badge-lazy.module').then(m => m.AddonBadgeLazyModule),
},
{
path: 'badges',
loadChildren: () => import('./badges-lazy.module').then(m => m.AddonBadgesLazyModule),
Expand Down
15 changes: 7 additions & 8 deletions src/addons/badges/pages/issued-badge/issued-badge.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@ <h1 *ngIf="!badge">{{ 'addon.badges.badges' | translate }}</h1>
</ion-item>
</ion-item-group>

<ion-item *ngIf="user">
<ion-label>
<p class="item-heading">
{{ 'addon.badges.awardedto' | translate: {$a: user.fullname } }}
</p>
</ion-label>
</ion-item>

<ng-container *ngIf="badge">
<ion-item>
<ion-label>
<p class="item-heading">
{{ 'addon.badges.awardedto' | translate: {$a: badge.recipientfullname } }}
</p>
</ion-label>
</ion-item>
<ion-item-group>
<ion-item-divider>
<ion-label>
Expand Down
55 changes: 38 additions & 17 deletions src/addons/badges/pages/issued-badge/issued-badge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreSites } from '@services/sites';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { CoreUser } from '@features/user/services/user';
import { AddonBadges, AddonBadgesUserBadge } from '../../services/badges';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
Expand All @@ -27,13 +27,18 @@ import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
import { CoreTime } from '@singletons/time';
import { CoreSharedModule } from '@/core/shared.module';

/**
* Page that displays the list of calendar events.
*/
@Component({
selector: 'page-addon-badges-issued-badge',
templateUrl: 'issued-badge.html',
standalone: true,
imports: [
CoreSharedModule,
],
})
export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {

Expand All @@ -42,10 +47,9 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
protected logView: (badge: AddonBadgesUserBadge) => void;

courseId = 0;
user?: CoreUserProfile;
course?: CoreEnrolledCourseData;
badge?: AddonBadgesUserBadge;
badges: CoreSwipeNavigationItemsManager;
badges?: CoreSwipeNavigationItemsManager;
badgeLoaded = false;
currentTime = 0;

Expand All @@ -54,12 +58,15 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getRequiredCurrentSite().getUserId();
this.badgeHash = CoreNavigator.getRouteParam('badgeHash') || '';

const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
AddonBadgesUserBadgesSource,
[this.courseId, this.userId],
);
const routeData = CoreNavigator.getRouteData(this.route);
if (routeData.usesSwipeNavigation) {
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
AddonBadgesUserBadgesSource,
[this.courseId, this.userId],
);

this.badges = new CoreSwipeNavigationItemsManager(source);
this.badges = new CoreSwipeNavigationItemsManager(source);
}

this.logView = CoreTime.once((badge) => {
CoreAnalytics.logEvent({
Expand All @@ -80,14 +87,14 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
this.badgeLoaded = true;
});

this.badges.start();
this.badges?.start();
}

/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.badges.destroy();
this.badges?.destroy();
}

/**
Expand All @@ -96,16 +103,29 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
* @returns Promise resolved when done.
*/
async fetchIssuedBadge(): Promise<void> {
const site = CoreSites.getRequiredCurrentSite();
this.currentTime = CoreTimeUtils.timestamp();

this.user = await CoreUser.getProfile(this.userId, this.courseId, true);

try {
// Search the badge in the user badges.
const badges = await AddonBadges.getUserBadges(this.courseId, this.userId);
const badge = badges.find((badge) => this.badgeHash == badge.uniquehash);
let badge = badges.find((badge) => this.badgeHash == badge.uniquehash);

if (!badge) {
return;
if (badge) {
if (!site.isVersionGreaterEqualThan('4.5')) {
// Web service does not return the name of the recipient.
const user = await CoreUser.getProfile(this.userId, this.courseId, true);
badge.recipientfullname = user.fullname;
}
} else {
// The badge is awarded to another user, try to fetch the badge by hash.
if (site.isVersionGreaterEqualThan('4.5')) {
badge = await AddonBadges.getUserBadgeByHash(this.badgeHash);
}
if (!badge) {
// Should never happen. The app opens the badge in the browser if it can't be fetched.
throw new Error('Error getting badge data.');
}
}

this.badge = badge;
Expand All @@ -130,9 +150,10 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
* @param refresher Refresher.
*/
async refreshBadges(refresher?: HTMLIonRefresherElement): Promise<void> {
await CoreUtils.ignoreErrors(Promise.all([
await CoreUtils.allPromisesIgnoringErrors([
AddonBadges.invalidateUserBadges(this.courseId, this.userId),
]));
AddonBadges.invalidateUserBadgeByHash(this.badgeHash),
]);

await CoreUtils.ignoreErrors(Promise.all([
this.fetchIssuedBadge(),
Expand Down
55 changes: 55 additions & 0 deletions src/addons/badges/services/badges-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// (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 { Injectable } from '@angular/core';

import { makeSingleton } from '@singletons';
import { AddonBadges } from './badges';
import { CoreSites } from '@services/sites';

/**
* Helper service that provides some features for badges.
*/
@Injectable({ providedIn: 'root' })
export class AddonBadgesHelperProvider {

/**
* Return whether the badge can be opened in the app.
*
* @param badgeHash Badge hash.
* @param siteId Site ID. If not defined, current site.
* @returns Whether the badge can be opened in the app.
*/
async canOpenBadge(badgeHash: string, siteId?: string): Promise<boolean> {
if (!AddonBadges.isPluginEnabled(siteId)) {
return false;
}

const site = await CoreSites.getSite(siteId);

if (site.isVersionGreaterEqualThan('4.5')) {
// The WS to fetch a badge by hash is available and it returns the name of the recipient.
return true;
}

// Open in app if badge is one of the user badges.
const badges = await AddonBadges.getUserBadges(0, site.getUserId());
const badge = badges.find((badge) => badgeHash == badge.uniquehash);

return badge !== undefined;
}

}

export const AddonBadgesHelper = makeSingleton(AddonBadgesHelperProvider);
69 changes: 69 additions & 0 deletions src/addons/badges/services/badges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,58 @@ export class AddonBadgesProvider {
await site.invalidateWsCacheForKey(this.getBadgesCacheKey(courseId, userId));
}

/**
* Get the cache key for the get badge by hash WS call.
*
* @param hash Badge issued hash.
* @returns Cache key.
*/
protected getUserBadgeByHashCacheKey(hash: string): string {
return ROOT_CACHE_KEY + 'badge:' + hash;
}

/**
* Get issued badge by hash.
*
* @param hash Badge issued hash.
* @returns Promise to be resolved when the badge is retrieved.
* @since 4.5 with the recpient name, 4.3 without the recipient name.
*/
async getUserBadgeByHash(hash: string, siteId?: string): Promise<AddonBadgesUserBadge> {
const site = await CoreSites.getSite(siteId);
const data: AddonBadgesGetUserBadgeByHashWSParams = {
hash,
};
const preSets = {
cacheKey: this.getUserBadgeByHashCacheKey(hash),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};

const response = await site.read<AddonBadgesGetUserBadgeByHashWSResponse>(
'core_badges_get_user_badge_by_hash',
data,
preSets,
);
if (!response || !response.badge?.[0]) {
throw new CoreError('Invalid badge response');
}

return response.badge[0];
}

/**
* Invalidate get badge by hash WS call.
*
* @param hash Badge issued hash.
* @param siteId Site ID. If not defined, current site.
* @returns Promise resolved when data is invalidated.
*/
async invalidateUserBadgeByHash(hash: string, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);

await site.invalidateWsCacheForKey(this.getUserBadgeByHashCacheKey(hash));
}

}

export const AddonBadges = makeSingleton(AddonBadgesProvider);
Expand Down Expand Up @@ -167,6 +219,8 @@ export type AddonBadgesUserBadge = {
dateissued: number; // Date issued.
dateexpire: number; // Date expire.
visible?: number; // Visible.
recipientid?: number; // @since 4.5. Id of the awarded user.
recipientfullname?: string; // @since 4.5. Full name of the awarded user.
email?: string; // @since 3.6. User email.
version?: string; // @since 3.6. Version.
language?: string; // @since 3.6. Language.
Expand Down Expand Up @@ -211,3 +265,18 @@ export type AddonBadgesUserBadge = {
type?: number; // Type.
}[];
};

/**
* Params of core_badges_get_user_badge_by_hash WS.
*/
type AddonBadgesGetUserBadgeByHashWSParams = {
hash: string; // Badge issued hash.
};

/**
* Data returned by core_badges_get_user_badge_by_hash WS.
*/
type AddonBadgesGetUserBadgeByHashWSResponse = {
badge: AddonBadgesUserBadge[];
warnings?: CoreWSExternalWarning[];
};
8 changes: 4 additions & 4 deletions src/addons/badges/services/handlers/badge-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons';
import { AddonBadges } from '../badges';
import { AddonBadgesHelper } from '../badges-helper';

/**
* Handler to treat links to user participants page.
Expand All @@ -36,16 +36,16 @@ export class AddonBadgesBadgeLinkHandlerService extends CoreContentLinksHandlerB

return [{
action: async (siteId: string): Promise<void> => {
await CoreNavigator.navigateToSitePath(`/badges/${params.hash}`, { siteId });
await CoreNavigator.navigateToSitePath(`/badge/${params.hash}`, { siteId });
},
}];
}

/**
* @inheritdoc
*/
isEnabled(siteId: string): Promise<boolean> {
return AddonBadges.isPluginEnabled(siteId);
async isEnabled(siteId: string, url: string, params: Record<string, string>): Promise<boolean> {
return AddonBadgesHelper.canOpenBadge(params.hash, siteId);
}

}
Expand Down
Loading

0 comments on commit d4a34ef

Please sign in to comment.