diff --git a/src/addons/badges/badgeclass-lazy.module.ts b/src/addons/badges/badgeclass-lazy.module.ts new file mode 100644 index 000000000..c8d0901f3 --- /dev/null +++ b/src/addons/badges/badgeclass-lazy.module.ts @@ -0,0 +1,36 @@ +// (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 { AddonBadgesBadgeClassPage } from './pages/badge-class/badge-class'; +import { CoreSharedModule } from '@/core/shared.module'; + +const routes: Routes = [ + { + path: ':badgeId', + component: AddonBadgesBadgeClassPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + ], + declarations: [ + AddonBadgesBadgeClassPage, + ], +}) +export class AddonBadgeClassLazyModule {} diff --git a/src/addons/badges/badges.module.ts b/src/addons/badges/badges.module.ts index 149b428e5..179319ef6 100644 --- a/src/addons/badges/badges.module.ts +++ b/src/addons/badges/badges.module.ts @@ -17,6 +17,7 @@ import { Routes } from '@angular/router'; import { AddonBadgesMyBadgesLinkHandler } from './services/handlers/mybadges-link'; import { AddonBadgesBadgeLinkHandler } from './services/handlers/badge-link'; +import { AddonBadgesBadgeClassLinkHandler } from './services/handlers/badgeclass-link'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreUserDelegate } from '@features/user/services/user-delegate'; import { AddonBadgesUserHandler } from './services/handlers/user'; @@ -48,6 +49,10 @@ const mainMenuRoutes: Routes = [ path: 'badges', loadChildren: () => import('./badges-lazy.module').then(m => m.AddonBadgesLazyModule), }, + { + path: 'badgeclass', + loadChildren: () => import('./badgeclass-lazy.module').then(m => m.AddonBadgeClassLazyModule), + }, ]; @NgModule({ @@ -61,6 +66,7 @@ const mainMenuRoutes: Routes = [ useValue: () => { CoreContentLinksDelegate.registerHandler(AddonBadgesMyBadgesLinkHandler.instance); CoreContentLinksDelegate.registerHandler(AddonBadgesBadgeLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonBadgesBadgeClassLinkHandler.instance); CoreUserDelegate.registerHandler(AddonBadgesUserHandler.instance); CorePushNotificationsDelegate.registerClickHandler(AddonBadgesPushClickHandler.instance); CoreTagAreaDelegate.registerHandler(AddonBadgesTagAreaHandler.instance); diff --git a/src/addons/badges/pages/badge-class/badge-class.html b/src/addons/badges/pages/badge-class/badge-class.html new file mode 100644 index 000000000..3526081ea --- /dev/null +++ b/src/addons/badges/pages/badge-class/badge-class.html @@ -0,0 +1,68 @@ + + + + + + +

{{ badge.name }}

+

{{ 'addon.badges.badgedetails' | translate }}

+
+
+
+ + + + + + + + + + + + + + +

{{ 'core.name' | translate}}

+

{{ badge.name }}

+
+
+ + +

{{ 'addon.badges.issuername' | translate}}

+

{{ badge.issuer }}

+
+
+ + +

{{ 'core.course' | translate}}

+

+ +

+
+
+ + +

{{ 'core.description' | translate}}

+

{{ badge.description }}

+
+
+
+ + + + + +

{{ 'addon.badges.alignment' | translate}}

+
+
+ + +

{{ alignment.targetName }}

+
+
+
+
+
+
diff --git a/src/addons/badges/pages/badge-class/badge-class.ts b/src/addons/badges/pages/badge-class/badge-class.ts new file mode 100644 index 000000000..0184cda39 --- /dev/null +++ b/src/addons/badges/pages/badge-class/badge-class.ts @@ -0,0 +1,91 @@ +// (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, OnInit } from '@angular/core'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreNavigator } from '@services/navigator'; +import { ActivatedRoute } from '@angular/router'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; +import { AddonBadges, AddonBadgesBadgeClass } from '../../services/badges'; + +/** + * Page that displays a badge class. + */ +@Component({ + selector: 'page-addon-badges-badge-class', + templateUrl: 'badge-class.html', +}) +export class AddonBadgesBadgeClassPage implements OnInit { + + protected badgeId = 0; + protected logView: (badge: AddonBadgesBadgeClass) => void; + + badge?: AddonBadgesBadgeClass; + badgeLoaded = false; + currentTime = 0; + + constructor(protected route: ActivatedRoute) { + this.badgeId = CoreNavigator.getRequiredRouteNumberParam('badgeId'); + + this.logView = CoreTime.once((badge) => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_badges_get_badge', + name: badge.name, + data: { id: this.badgeId, category: 'badges' }, + url: `/badges/badgeclass.php?id=${this.badgeId}`, + }); + }); + } + + /** + * View loaded. + */ + ngOnInit(): void { + this.fetchBadgeClass().finally(() => { + this.badgeLoaded = true; + }); + } + + /** + * Fetch the badge class required for the view. + * + * @returns Promise resolved when done. + */ + async fetchBadgeClass(): Promise { + try { + this.badge = await AddonBadges.getBadgeClass(this.badgeId); + + this.logView(this.badge); + } catch (message) { + CoreDomUtils.showErrorModalDefault(message, 'Error getting badge data.'); + } + } + + /** + * Refresh the badge class. + * + * @param refresher Refresher. + */ + async refreshBadgeClass(refresher?: HTMLIonRefresherElement): Promise { + await CoreUtils.ignoreErrors(AddonBadges.invalidateBadgeClass(this.badgeId)); + + await this.fetchBadgeClass(); + + refresher?.complete(); + } + +} diff --git a/src/addons/badges/pages/issued-badge/issued-badge.html b/src/addons/badges/pages/issued-badge/issued-badge.html index a4a2fb942..7f0aa1ba1 100644 --- a/src/addons/badges/pages/issued-badge/issued-badge.html +++ b/src/addons/badges/pages/issued-badge/issued-badge.html @@ -111,11 +111,11 @@

{{ badge.imagecaption }}

- +

{{ 'core.course' | translate}}

- +

@@ -217,21 +217,16 @@ - +

{{ 'addon.badges.alignment' | translate}}

- -

{{ alignment.targetname }}

-
-
- - -

{{ 'addon.badges.noalignment' | translate}}

+

{{ alignment.targetName }}

diff --git a/src/addons/badges/pages/issued-badge/issued-badge.ts b/src/addons/badges/pages/issued-badge/issued-badge.ts index ff2c0d376..0b94c5874 100644 --- a/src/addons/badges/pages/issued-badge/issued-badge.ts +++ b/src/addons/badges/pages/issued-badge/issued-badge.ts @@ -19,7 +19,7 @@ import { CoreSites } from '@services/sites'; 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'; +import { CoreCourses } from '@features/courses/services/courses'; import { CoreNavigator } from '@services/navigator'; import { ActivatedRoute } from '@angular/router'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; @@ -30,7 +30,7 @@ import { CoreTime } from '@singletons/time'; import { CoreSharedModule } from '@/core/shared.module'; /** - * Page that displays the list of calendar events. + * Page that displays an issued badge. */ @Component({ selector: 'page-addon-badges-issued-badge', @@ -47,7 +47,6 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy { protected logView: (badge: AddonBadgesUserBadge) => void; courseId = 0; - course?: CoreEnrolledCourseData; badge?: AddonBadgesUserBadge; badges?: CoreSwipeNavigationItemsManager; badgeLoaded = false; @@ -128,16 +127,18 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy { } } - this.badge = badge; - if (badge.courseid) { + // Try to get course full name if not returned by the WS. + if (badge.courseid && !badge.coursefullname) { try { - this.course = await CoreCourses.getUserCourse(badge.courseid, true); + const course = await CoreCourses.getUserCourse(badge.courseid, true); + badge.coursefullname = course.fullname; } catch { - // Maybe an old deleted course. - this.course = undefined; + // User is not enrolled in the course. } } + this.badge = badge; + this.logView(badge); } catch (message) { CoreDomUtils.showErrorModalDefault(message, 'Error getting badge data.'); diff --git a/src/addons/badges/services/badges.ts b/src/addons/badges/services/badges.ts index 6588d87f1..9ed978713 100644 --- a/src/addons/badges/services/badges.ts +++ b/src/addons/badges/services/badges.ts @@ -78,15 +78,18 @@ export class AddonBadgesProvider { throw new CoreError('Invalid badges response'); } - // In 3.7, competencies was renamed to alignment. Rename the property in 3.6 too. response.badges.forEach((badge) => { + // In 3.7, competencies was renamed to alignment. + if (!badge.alignment && badge.competencies) { + badge.alignment = badge.competencies.map((competency) => ({ + targetName: competency.targetname, + targetUrl: competency.targeturl, + })); + } badge.alignment = badge.alignment || badge.competencies; - // Check that the alignment is valid, they were broken in 3.7. - if (badge.alignment && badge.alignment[0] && badge.alignment[0].targetname === undefined) { - // If any badge lacks targetname it means they are affected by the Moodle bug, don't display them. - delete badge.alignment; - } + // Exclude alignments without targetName, we can't display them. + badge.alignment = badge.alignment?.filter((alignment) => alignment.targetName); }); return response.badges; @@ -138,11 +141,15 @@ export class AddonBadgesProvider { data, preSets, ); - if (!response || !response.badge?.[0]) { + const badge = response?.badge?.[0]; + if (!badge) { throw new CoreError('Invalid badge response'); } - return response.badge[0]; + // Exclude alignments without targetName, we can't display them. + badge.alignment = badge.alignment?.filter((alignment) => alignment.targetName); + + return badge; } /** @@ -158,6 +165,76 @@ export class AddonBadgesProvider { await site.invalidateWsCacheForKey(this.getUserBadgeByHashCacheKey(hash)); } + /** + * Get the cache key for the get badge class WS call. + * + * @param id Badge ID. + * @returns Cache key. + */ + protected getBadgeClassCacheKey(id: number): string { + return ROOT_CACHE_KEY + 'badgeclass:' + id; + } + + /** + * Get badge class. + * + * @param id Badge ID. + * @param siteId Site ID. If not defined, current site. + * @returns Promise to be resolved when the badge is retrieved. + * @since 4.5 + */ + async getBadgeClass(id: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + const data: AddonBadgesGetBadgeClassWSParams = { + id, + }; + const preSets = { + cacheKey: this.getBadgeClassCacheKey(id), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + + const response = await site.read( + 'core_badges_get_badge', + data, + preSets, + ); + const badge = response?.badge; + if (!badge) { + throw new CoreError('Invalid badge response'); + } + + // Exclude alignments without targetName, we can't display them. + badge.alignment = badge.alignment?.filter((alignment) => alignment.targetName); + + return badge; + } + + /** + * Invalidate get badge class WS call. + * + * @param id Badge ID. + * @param siteId Site ID. If not defined, current site. + * @returns Promise resolved when data is invalidated.รง + * @since 4.5 + */ + async invalidateBadgeClass(id: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getBadgeClassCacheKey(id)); + } + + /** + * Returns whether get badge class WS is available. + * + * @param siteId Site ID. If not defined, current site. + * @returns If WS is available. + */ + async isGetBadgeClassAvailable(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.wsAvailable('core_badges_get_badge'); + } + } export const AddonBadges = makeSingleton(AddonBadgesProvider); @@ -208,6 +285,7 @@ export type AddonBadgesUserBadge = { expireperiod?: number; // Expire period. type?: number; // Type. courseid?: number; // Course id. + coursefullname?: string; // Full name of the course. message?: string; // Message. messagesubject?: string; // Message subject. attachment?: number; // Attachment. @@ -242,11 +320,11 @@ export type AddonBadgesUserBadge = { alignment?: { // @since 3.7. Calculated by the app for 3.6 sites. Badge alignments. id?: number; // Alignment id. badgeid?: number; // Badge id. - targetname?: string; // Target name. - targeturl?: string; // Target URL. - targetdescription?: string; // Target description. - targetframework?: string; // Target framework. - targetcode?: string; // Target code. + targetName?: string; // Target name. + targetUrl?: string; // Target URL. + targetDescription?: string; // Target description. + targetFramework?: string; // Target framework. + targetCode?: string; // Target code. }[]; competencies?: { // @deprecatedonmoodle since 3.7. @since 3.6. In 3.7 it was renamed to alignment. id?: number; // Alignment id. @@ -280,3 +358,44 @@ type AddonBadgesGetUserBadgeByHashWSResponse = { badge: AddonBadgesUserBadge[]; warnings?: CoreWSExternalWarning[]; }; + +/** + * Params of core_badges_get_badge WS. + */ +type AddonBadgesGetBadgeClassWSParams = { + id: number; // Badge ID. +}; + +/** + * Data returned by core_badges_get_badge WS. + */ +type AddonBadgesGetBadgeClassWSResponse = { + badge: AddonBadgesBadgeClass; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Badge data returned by core_badges_get_badge WS. + */ +export type AddonBadgesBadgeClass = { + type: string; // BadgeClass. + id: string; // Unique identifier for this badgeclass (URL). + issuer?: string; // Issuer for this badgeclass. + name: string; // Name of the badgeclass. + image: string; // URL to the image. + description: string; // Description of the badge class. + hostedUrl?: string; // Identifier of the open badge for this assertion. + courseid?: number; // Course ID. + coursefullname?: string; // Full name of the course. + alignment?: { // Badge alignments. + id?: number; // Alignment id. + badgeid?: number; // Badge id. + targetName?: string; // Target name. + targetUrl?: string; // Target URL. + targetDescription?: string; // Target description. + targetFramework?: string; // Target framework. + targetCode?: string; // Target code. + }[]; + criteriaUrl?: string; // Criteria URL. + criteriaNarrative?: string; // Criteria narrative. +}; diff --git a/src/addons/badges/services/handlers/badge-link.ts b/src/addons/badges/services/handlers/badge-link.ts index d45554cf0..29c750408 100644 --- a/src/addons/badges/services/handlers/badge-link.ts +++ b/src/addons/badges/services/handlers/badge-link.ts @@ -21,7 +21,7 @@ import { makeSingleton } from '@singletons'; import { AddonBadgesHelper } from '../badges-helper'; /** - * Handler to treat links to user participants page. + * Handler to treat links to issued badges. */ @Injectable({ providedIn: 'root' }) export class AddonBadgesBadgeLinkHandlerService extends CoreContentLinksHandlerBase { diff --git a/src/addons/badges/services/handlers/badgeclass-link.ts b/src/addons/badges/services/handlers/badgeclass-link.ts new file mode 100644 index 000000000..ed830742a --- /dev/null +++ b/src/addons/badges/services/handlers/badgeclass-link.ts @@ -0,0 +1,56 @@ +// (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 { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { AddonBadges } from '../badges'; + +/** + * Handler to treat links to badge classes. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBadgesBadgeClassLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonBadgesBadgeClassLinkHandler'; + pattern = /\/badges\/badgeclass\.php.*([?&]id=)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + + return [{ + action: async (siteId: string): Promise => { + await CoreNavigator.navigateToSitePath(`/badgeclass/${params.id}`, { siteId }); + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string): Promise { + const pluginEnabled = await AddonBadges.isPluginEnabled(siteId); + const wsAvailable = await AddonBadges.isGetBadgeClassAvailable(siteId); + + return pluginEnabled && wsAvailable; + } + +} + +export const AddonBadgesBadgeClassLinkHandler = makeSingleton(AddonBadgesBadgeClassLinkHandlerService);