diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 1ae558f27..bcabc9893 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -15,5 +15,6 @@ jobs: node-version: '12.x' - run: npm ci - run: npm run lint + - run: npx tslint -c ionic-migration.json -p tsconfig.json - run: npm run test:ci - run: npm run build:prod diff --git a/.travis.yml b/.travis.yml index 64da6f723..3cefd498a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,4 @@ script: - npm run lint - npm run test:ci - npm run build:prod + diff --git a/gulpfile.js b/gulpfile.js index ae1ef0a8c..739e799c2 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -41,5 +41,5 @@ gulp.task('push', (done) => { gulp.task('default', gulp.parallel('lang')); gulp.task('watch', () => { - gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang')); + gulp.watch(paths.lang, { interval: 500 }, gulp.parallel('lang')); }); diff --git a/ionic-migration.json b/ionic-migration.json new file mode 100644 index 000000000..8da798098 --- /dev/null +++ b/ionic-migration.json @@ -0,0 +1,43 @@ +{ + "rulesDirectory": ["@ionic/v4-migration-tslint/rules"], + "rules": { + "ion-action-sheet-method-create-parameters-renamed": true, + "ion-alert-method-create-parameters-renamed": true, + "ion-back-button-not-added-by-default": { "options": [true], "severity": "warning" }, + "ion-button-attributes-renamed": true, + "ion-button-is-now-an-element": true, + "ion-buttons-attributes-renamed": true, + "ion-col-attributes-renamed": true, + "ion-datetime-capitalization-changed": true, + "ion-fab-attributes-renamed": true, + "ion-fab-button-is-now-an-element": true, + "ion-fab-fixed-content": true, + "ion-icon-attribute-is-active-removed": true, + "ion-item-attributes-renamed": true, + "ion-item-divider-ion-label-required": true, + "ion-item-ion-label-required": true, + "ion-item-is-now-an-element": true, + "ion-item-option-is-now-an-element": true, + "ion-item-option-method-get-sliding-percent-renamed": true, + "ion-item-options-attribute-values-renamed": true, + "ion-label-attributes-renamed": true, + "ion-list-header-ion-label-required": true, + "ion-loading-method-create-parameters-renamed": true, + "ion-menu-events-renamed": true, + "ion-menu-toggle-is-now-an-element": true, + "ion-navbar-is-now-ion-toolbar": true, + "ion-option-is-now-ion-select-option": true, + "ion-overlay-method-create-should-use-await": true, + "ion-overlay-method-present-should-use-await": { "options": [true], "severity": "warning" }, + "ion-radio-attributes-renamed": true, + "ion-radio-group-is-now-an-element": true, + "ion-radio-slot-required": true, + "ion-range-attributes-renamed": true, + "ion-segment-button-ion-label-required": true, + "ion-spinner-attribute-values-renamed": true, + "ion-tabs-refactored": { "options": [true], "severity": "warning" }, + "ion-text-is-now-an-element": true + } +} + + diff --git a/package-lock.json b/package-lock.json index 1d2552ff9..66642e987 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2439,6 +2439,16 @@ } } }, + "@ionic/v4-migration-tslint": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@ionic/v4-migration-tslint/-/v4-migration-tslint-1.7.1.tgz", + "integrity": "sha512-1vwBmf0czHvG+vKboxHtYtPJ3Stc7wP8tB2i7qmLslKqMqVFzaTkbNiakt40mHJ/UtbfAzFOkBD6RZZHTWEuzQ==", + "dev": true, + "requires": { + "codelyzer": "^4.4.4", + "tslint": "^5.0.0" + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3974,6 +3984,12 @@ "picomatch": "^2.0.4" } }, + "app-root-path": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", + "integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==", + "dev": true + }, "append-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", @@ -5061,6 +5077,12 @@ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, "builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -5626,6 +5648,34 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "codelyzer": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.5.0.tgz", + "integrity": "sha512-oO6vCkjqsVrEsmh58oNlnJkRXuA30hF8cdNAQV9DytEalDwyOFRvHMnlKFzmOStNerOmPGZU9GAHnBo4tGvtiQ==", + "dev": true, + "requires": { + "app-root-path": "^2.1.0", + "css-selector-tokenizer": "^0.7.0", + "cssauron": "^1.4.0", + "semver-dsl": "^1.0.1", + "source-map": "^0.5.7", + "sprintf-js": "^1.1.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + } + } + }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -6885,6 +6935,16 @@ "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", "dev": true }, + "css-selector-tokenizer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, "css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -6909,6 +6969,15 @@ "integrity": "sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g==", "dev": true }, + "cssauron": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "dev": true, + "requires": { + "through": "X.X.X" + } + }, "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -8766,6 +8835,12 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, "fastq": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", @@ -17659,6 +17734,23 @@ } } }, + "semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "dev": true, + "requires": { + "semver": "^5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "semver-greatest-satisfied-range": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", @@ -19491,6 +19583,100 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" }, + "tslint": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, "tsutils": { "version": "3.17.1", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", diff --git a/package.json b/package.json index 4f7510f3e..6da455346 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,7 @@ "@angular/compiler-cli": "~10.0.0", "@angular/language-service": "~10.0.0", "@ionic/angular-toolkit": "^2.3.0", + "@ionic/v4-migration-tslint": "^1.7.1", "@types/faker": "^5.1.3", "@types/node": "^12.12.64", "@types/webpack-env": "^1.16.0", diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index 6686059a2..82761f055 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -39,9 +39,11 @@ import { AddonBlockTagsModule } from './block/tags/tags.module'; import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module'; import { AddonFilterModule } from './filter/filter.module'; import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module'; +import { AddonBadgesModule } from './badges/badges.module'; @NgModule({ imports: [ + AddonBadgesModule, AddonPrivateFilesModule, AddonFilterModule, AddonBlockActivityResultsModule, diff --git a/src/addons/badges/badges-lazy.module.ts b/src/addons/badges/badges-lazy.module.ts new file mode 100644 index 000000000..e1aee5b89 --- /dev/null +++ b/src/addons/badges/badges-lazy.module.ts @@ -0,0 +1,37 @@ +// (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'; + +const routes: Routes = [ + { + path: '', + redirectTo: 'user', + pathMatch: 'full', + }, + { + path: 'issue', + loadChildren: () => import('./pages/issued-badge/issued-badge.module').then( m => m.AddonBadgesIssuedBadgePageModule), + }, + { + path: 'user', + loadChildren: () => import('./pages/user-badges/user-badges.module').then( m => m.AddonBadgesUserBadgesPageModule), + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], +}) +export class AddonBadgesLazyModule {} diff --git a/src/addons/badges/badges.module.ts b/src/addons/badges/badges.module.ts new file mode 100644 index 000000000..b0b192970 --- /dev/null +++ b/src/addons/badges/badges.module.ts @@ -0,0 +1,54 @@ +// (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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { Routes } from '@angular/router'; + +import { AddonBadgesMyBadgesLinkHandler } from './services/handlers/mybadges-link'; +import { AddonBadgesBadgeLinkHandler } from './services/handlers/badge-link'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreUserDelegate } from '@features/user/services/user-delegate'; +import { AddonBadgesUserHandler } from './services/handlers/user'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +// @todo import { CorePushNotificationsDelegate } from '@core/pushnotifications/services/delegate'; +// import { AddonBadgesPushClickHandler } from './services/push-click-handler'; + +const mainMenuHomeSiblingRoutes: Routes = [ + { + path: 'badges', + loadChildren: () => import('./badges-lazy.module').then(m => m.AddonBadgesLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild({ + siblings: mainMenuHomeSiblingRoutes, + }), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreContentLinksDelegate.instance.registerHandler(AddonBadgesMyBadgesLinkHandler.instance); + CoreContentLinksDelegate.instance.registerHandler(AddonBadgesBadgeLinkHandler.instance); + CoreUserDelegate.instance.registerHandler(AddonBadgesUserHandler.instance); + // CorePushNotificationsDelegate.instance.registerHandler(AddonBadgesPushClickHandler.instance); + }, + }, + ], +}) +export class AddonBadgesModule {} diff --git a/src/addons/badges/lang.json b/src/addons/badges/lang.json new file mode 100644 index 000000000..6c8a2b856 --- /dev/null +++ b/src/addons/badges/lang.json @@ -0,0 +1,29 @@ +{ + "alignment": "Alignment", + "badgedetails": "Badge details", + "badges": "Badges", + "bendorsement": "Endorsement", + "claimcomment": "Endorsement comment", + "claimid": "Claim URL", + "contact": "Contact", + "dateawarded": "Date issued", + "expired": "Expired", + "expirydate": "Expiry date", + "imageauthoremail": "Image author's email", + "imageauthorname": "Image author's name", + "imageauthorurl": "Image author's URL", + "imagecaption": "Image caption", + "issuancedetails": "Badge expiry", + "issuerdetails": "Issuer details", + "issueremail": "Email", + "issuername": "Issuer name", + "issuerurl": "Issuer URL", + "language": "Language", + "noalignment": "This badge does not have any external skills or standards specified.", + "nobadges": "There are no badges available.", + "norelated": "This badge does not have any related badges.", + "recipientdetails": "Recipient details", + "relatedbages": "Related badges", + "version": "Version", + "warnexpired": "(This badge has expired!)" +} \ No newline at end of file diff --git a/src/addons/badges/pages/issued-badge/issued-badge.html b/src/addons/badges/pages/issued-badge/issued-badge.html new file mode 100644 index 000000000..545ca1fc2 --- /dev/null +++ b/src/addons/badges/pages/issued-badge/issued-badge.html @@ -0,0 +1,228 @@ + + + + + + {{ badge.name }} + {{ 'addon.badges.badges' | translate }} + + + + + + + + + + + + + {{ 'addon.badges.expired' | translate }} + + + + + + + + +

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

+
+
+ + +

{{ 'core.name' | translate}}

+

{{ user.fullname }}

+
+
+
+ + + + + +

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

+
+
+ + +

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

+

{{ badge.issuername }}

+
+
+ + +

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

+

{{ badge.issuercontact }}

+
+
+
+ + + + +

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

+
+
+ + +

{{ 'core.name' | translate}}

+

{{ badge.name }}

+
+
+ + +

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

+

{{ badge.version }}

+
+
+ + +

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

+

{{ badge.language }}

+
+
+ + +

{{ 'core.description' | translate}}

+

{{ badge.description }}

+
+
+ + +

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

+

{{ badge.imageauthorname }}

+
+
+ + +

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

+

{{ badge.imageauthoremail }}

+
+
+ + +

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

+

{{ badge.imageauthorurl }}

+
+
+ + +

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

+

{{ badge.imagecaption }}

+
+
+ + +

{{ 'core.course' | translate}}

+

+ + +

+
+
+ +
+ + + + +

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

+
+
+ + +

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

+

{{badge.dateissued * 1000 | coreFormatDate }}

+
+
+ + +

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

+

+ {{ badge.dateexpire * 1000 | coreFormatDate }} + + {{ 'addon.badges.warnexpired' | translate }} + +

+
+
+ +
+ + + + +

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

+
+ + +

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

+

{{ badge.endorsement.issuername }}

+
+
+ + +

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

+

+ + {{ badge.endorsement.issueremail }} + +

+
+
+ + +

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

+

{{ badge.endorsement.issuerurl }}

+
+
+ + +

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

+

{{ badge.endorsement.dateissued * 1000 | coreFormatDate }}

+
+
+ + +

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

+

{{ badge.endorsement.claimid }}

+
+
+ + +

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

+

{{ badge.endorsement.claimcomment }}

+
+
+
+ + + + +

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

+
+ +

{{ relatedBadge.name }}

+
+ +

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

+
+
+ + + + +

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

+
+ +

{{ alignment.targetname }}

+
+ +

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

+
+
+
+
+
diff --git a/src/addons/badges/pages/issued-badge/issued-badge.module.ts b/src/addons/badges/pages/issued-badge/issued-badge.module.ts new file mode 100644 index 000000000..e29cbe77d --- /dev/null +++ b/src/addons/badges/pages/issued-badge/issued-badge.module.ts @@ -0,0 +1,49 @@ +// (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 { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; + +import { AddonBadgesIssuedBadgePage } from './issued-badge.page'; + +const routes: Routes = [ + { + path: '', + component: AddonBadgesIssuedBadgePage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + ], + declarations: [ + AddonBadgesIssuedBadgePage, + ], + exports: [RouterModule], +}) +export class AddonBadgesIssuedBadgePageModule {} diff --git a/src/addons/badges/pages/issued-badge/issued-badge.page.ts b/src/addons/badges/pages/issued-badge/issued-badge.page.ts new file mode 100644 index 000000000..70f90e8b2 --- /dev/null +++ b/src/addons/badges/pages/issued-badge/issued-badge.page.ts @@ -0,0 +1,112 @@ +// (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 { IonRefresher } from '@ionic/angular'; +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 { AddonBadges, AddonBadgesUserBadge } from '../../services/badges'; +import { CoreUtils } from '@services/utils/utils'; +import { ActivatedRoute } from '@angular/router'; +import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses'; + +/** + * Page that displays the list of calendar events. + */ +@Component({ + selector: 'page-addon-badges-issued-badge', + templateUrl: 'issued-badge.html', +}) +export class AddonBadgesIssuedBadgePage implements OnInit { + + protected badgeHash = ''; + protected userId!: number; + + courseId = 0; + user?: CoreUserProfile; + course?: CoreEnrolledCourseData; + badge?: AddonBadgesUserBadge; + badgeLoaded = false; + currentTime = 0; + + constructor( + protected route: ActivatedRoute, + ) { } + + /** + * View loaded. + */ + ngOnInit(): void { + this.courseId = this.route.snapshot.queryParams['courseId'] || this.courseId; // Use 0 for site badges. + this.userId = this.route.snapshot.queryParams['userId'] || + CoreSites.instance.getCurrentSite()?.getUserId(); + this.badgeHash = this.route.snapshot.queryParams['badgeHash']; + + this.fetchIssuedBadge().finally(() => { + this.badgeLoaded = true; + }); + } + + /** + * Fetch the issued badge required for the view. + * + * @return Promise resolved when done. + */ + async fetchIssuedBadge(): Promise { + this.currentTime = CoreTimeUtils.instance.timestamp(); + + this.user = await CoreUser.instance.getProfile(this.userId, this.courseId, true); + + try { + const badges = await AddonBadges.instance.getUserBadges(this.courseId, this.userId); + const badge = badges.find((badge) => this.badgeHash == badge.uniquehash); + + if (!badge) { + return; + } + + this.badge = badge; + if (badge.courseid) { + try { + this.course = await CoreCourses.instance.getUserCourse(badge.courseid, true); + } catch { + // Maybe an old deleted course. + this.course = undefined; + } + } + } catch (message) { + CoreDomUtils.instance.showErrorModalDefault(message, 'Error getting badge data.'); + } + } + + /** + * Refresh the badges. + * + * @param refresher Refresher. + */ + async refreshBadges(refresher?: CustomEvent): Promise { + await CoreUtils.instance.ignoreErrors(Promise.all([ + AddonBadges.instance.invalidateUserBadges(this.courseId, this.userId), + ])); + + await CoreUtils.instance.ignoreErrors(Promise.all([ + this.fetchIssuedBadge(), + ])); + + refresher?.detail.complete(); + } + +} diff --git a/src/addons/badges/pages/user-badges/user-badges.html b/src/addons/badges/pages/user-badges/user-badges.html new file mode 100644 index 000000000..355c64698 --- /dev/null +++ b/src/addons/badges/pages/user-badges/user-badges.html @@ -0,0 +1,34 @@ + + + + + + {{ 'addon.badges.badges' | translate }} + + + + + + + + + + + + + + + + + +

{{ badge.name }}

+

{{ badge.dateissued * 1000 | coreFormatDate :'strftimedatetimeshort' }}

+
+ + {{ 'addon.badges.expired' | translate }} + +
+
+
+
diff --git a/src/addons/badges/pages/user-badges/user-badges.module.ts b/src/addons/badges/pages/user-badges/user-badges.module.ts new file mode 100644 index 000000000..92a9b6978 --- /dev/null +++ b/src/addons/badges/pages/user-badges/user-badges.module.ts @@ -0,0 +1,49 @@ +// (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 { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; + +import { AddonBadgesUserBadgesPage } from './user-badges.page'; + +const routes: Routes = [ + { + path: '', + component: AddonBadgesUserBadgesPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + ], + declarations: [ + AddonBadgesUserBadgesPage, + ], + exports: [RouterModule], +}) +export class AddonBadgesUserBadgesPageModule {} diff --git a/src/addons/badges/pages/user-badges/user-badges.page.ts b/src/addons/badges/pages/user-badges/user-badges.page.ts new file mode 100644 index 000000000..d1ea67af6 --- /dev/null +++ b/src/addons/badges/pages/user-badges/user-badges.page.ts @@ -0,0 +1,113 @@ +// (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 { IonRefresher } from '@ionic/angular'; +import { AddonBadges, AddonBadgesUserBadge } from '../../services/badges'; +import { CoreTimeUtils } from '@services/utils/time'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreNavHelper } from '@services/nav-helper'; +import { ActivatedRoute } from '@angular/router'; +// @todo import { CoreSplitViewComponent } from '@components/split-view/split-view'; + +/** + * Page that displays the list of calendar events. + */ +@Component({ + selector: 'page-addon-badges-user-badges', + templateUrl: 'user-badges.html', +}) +export class AddonBadgesUserBadgesPage implements OnInit { + + // @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + courseId = 0; + userId!: number; + + badgesLoaded = false; + badges: AddonBadgesUserBadge[] = []; + currentTime = 0; + badgeHash!: string; + + constructor( + protected route: ActivatedRoute, + ) { } + + /** + * View loaded. + */ + ngOnInit(): void { + + this.courseId = this.route.snapshot.queryParams['courseId'] || this.courseId; // Use 0 for site badges. + this.userId = this.route.snapshot.queryParams['userId'] || + CoreSites.instance.getCurrentSite()?.getUserId(); + + this.fetchBadges().finally(() => { + // @todo splitview + /* if (!this.badgeHash && this.splitviewCtrl.isOn() && this.badges.length > 0) { + // Take first and load it. + this.loadIssuedBadge(this.badges[0].uniquehash); + }*/ + this.badgesLoaded = true; + }); + } + + /** + * Fetch all the badges required for the view. + * + * @return Promise resolved when done. + */ + async fetchBadges(): Promise { + this.currentTime = CoreTimeUtils.instance.timestamp(); + + try { + this.badges = await AddonBadges.instance.getUserBadges(this.courseId, this.userId); + } catch (message) { + CoreDomUtils.instance.showErrorModalDefault(message, 'Error getting badges data.'); + } + } + + /** + * Refresh the badges. + * + * @param refresher Refresher. + */ + async refreshBadges(refresher?: CustomEvent): Promise { + await CoreUtils.instance.ignoreErrors(Promise.all([ + AddonBadges.instance.invalidateUserBadges(this.courseId, this.userId), + ])); + + await CoreUtils.instance.ignoreErrors(Promise.all([ + this.fetchBadges(), + ])); + + refresher?.detail.complete(); + } + + /** + * Navigate to a particular badge. + * + * @param badgeHash Badge to load. + */ + loadIssuedBadge(badgeHash: string): void { + this.badgeHash = badgeHash; + const params = { courseId: this.courseId, userId: this.userId, badgeHash: badgeHash }; + // @todo use splitview. + // this.splitviewCtrl.push('AddonBadgesIssuedBadgePage', params); + CoreNavHelper.instance.goInSite('/badges/issue', params); + } + +} diff --git a/src/addons/badges/services/badges.ts b/src/addons/badges/services/badges.ts new file mode 100644 index 000000000..2488e5a85 --- /dev/null +++ b/src/addons/badges/services/badges.ts @@ -0,0 +1,213 @@ +// (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 { CoreSites } from '@services/sites'; +import { CoreWSExternalWarning } from '@services/ws'; +import { CoreSite } from '@classes/site'; +import { makeSingleton } from '@singletons'; +import { CoreError } from '@classes/errors/error'; + +const ROOT_CACHE_KEY = 'mmaBadges:'; + +/** + * Service to handle badges. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBadgesProvider { + + /** + * Returns whether or not the badge plugin is enabled for a certain site. + * + * This method is called quite often and thus should only perform a quick + * check, we should not be calling WS from here. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if enabled, false otherwise. + */ + async isPluginEnabled(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return site.canUseAdvancedFeature('enablebadges') && site.wsAvailable('core_course_get_user_navigation_options'); + } + + /** + * Get the cache key for the get badges call. + * + * @param courseId ID of the course to get the badges from. + * @param userId ID of the user to get the badges from. + * @return Cache key. + */ + protected getBadgesCacheKey(courseId: number, userId: number): string { + return ROOT_CACHE_KEY + 'badges:' + courseId + ':' + userId; + } + + /** + * Get issued badges for a certain user in a course. + * + * @param courseId ID of the course to get the badges from. + * @param userId ID of the user to get the badges from. + * @param siteId Site ID. If not defined, current site. + * @return Promise to be resolved when the badges are retrieved. + */ + async getUserBadges(courseId: number, userId: number, siteId?: string): Promise { + + const site = await CoreSites.instance.getSite(siteId); + const data: AddonBadgesGetUserBadgesWSParams = { + courseid: courseId, + userid: userId, + }; + const preSets = { + cacheKey: this.getBadgesCacheKey(courseId, userId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + + const response = await site.read('core_badges_get_user_badges', data, preSets); + if (!response || !response.badges) { + 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) => { + badge.alignment = badge.alignment || badge.competencies; + + // Check that the alignment is valid, they were broken in 3.7. + if (badge.alignment && badge.alignment[0] && typeof 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; + } + }); + + return response.badges; + } + + /** + * Invalidate get badges WS call. + * + * @param courseId Course ID. + * @param userId ID of the user to get the badges from. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when data is invalidated. + */ + async invalidateUserBadges(courseId: number, userId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getBadgesCacheKey(courseId, userId)); + } + +} + +export class AddonBadges extends makeSingleton(AddonBadgesProvider) {} + +/** + * Params of core_badges_get_user_badges WS. + */ +type AddonBadgesGetUserBadgesWSParams = { + userid?: number; // Badges only for this user id, empty for current user. + courseid?: number; // Filter badges by course id, empty all the courses. + page?: number; // The page of records to return. + perpage?: number; // The number of records to return per page. + search?: string; // A simple string to search for. + onlypublic?: boolean; // Whether to return only public badges. +}; + +/** + * Data returned by core_badges_get_user_badges WS. + */ +type AddonBadgesGetUserBadgesWSResponse = { + badges: AddonBadgesUserBadge[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS core_badges_get_user_badges. + */ +export type AddonBadgesGetUserBadgesResult = { + badges: AddonBadgesUserBadge[]; // List of badges. + warnings?: CoreWSExternalWarning[]; // List of warnings. +}; + +/** + * Badge data returned by WS core_badges_get_user_badges. + */ +export type AddonBadgesUserBadge = { + id?: number; // Badge id. + name: string; // Badge name. + description: string; // Badge description. + timecreated?: number; // Time created. + timemodified?: number; // Time modified. + usercreated?: number; // User created. + usermodified?: number; // User modified. + issuername: string; // Issuer name. + issuerurl: string; // Issuer URL. + issuercontact: string; // Issuer contact. + expiredate?: number; // Expire date. + expireperiod?: number; // Expire period. + type?: number; // Type. + courseid?: number; // Course id. + message?: string; // Message. + messagesubject?: string; // Message subject. + attachment?: number; // Attachment. + notification?: number; // @since 3.6. Whether to notify when badge is awarded. + nextcron?: number; // @since 3.6. Next cron. + status?: number; // Status. + issuedid?: number; // Issued id. + uniquehash: string; // Unique hash. + dateissued: number; // Date issued. + dateexpire: number; // Date expire. + visible?: number; // Visible. + email?: string; // @since 3.6. User email. + version?: string; // @since 3.6. Version. + language?: string; // @since 3.6. Language. + imageauthorname?: string; // @since 3.6. Name of the image author. + imageauthoremail?: string; // @since 3.6. Email of the image author. + imageauthorurl?: string; // @since 3.6. URL of the image author. + imagecaption?: string; // @since 3.6. Caption of the image. + badgeurl: string; // Badge URL. + endorsement?: { // @since 3.6. + id: number; // Endorsement id. + badgeid: number; // Badge id. + issuername: string; // Endorsement issuer name. + issuerurl: string; // Endorsement issuer URL. + issueremail: string; // Endorsement issuer email. + claimid: string; // Claim URL. + claimcomment: string; // Claim comment. + dateissued: number; // Date issued. + }; + 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. + }[]; + competencies?: { // @deprecated from 3.7. @since 3.6. In 3.7 it was renamed to alignment. + 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. + }[]; + relatedbadges?: { // @since 3.6. Related badges. + id: number; // Badge id. + name: string; // Badge name. + version?: string; // Version. + language?: string; // Language. + type?: number; // Type. + }[]; +}; diff --git a/src/addons/badges/services/handlers/badge-link.ts b/src/addons/badges/services/handlers/badge-link.ts new file mode 100644 index 000000000..13d54b376 --- /dev/null +++ b/src/addons/badges/services/handlers/badge-link.ts @@ -0,0 +1,71 @@ +// (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 { Params } from '@angular/router'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreNavHelper } from '@services/nav-helper'; +import { makeSingleton } from '@singletons'; +import { AddonBadges } from '../badges'; + + +/** + * Handler to treat links to user participants page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBadgesBadgeLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonBadgesBadgeLinkHandler'; + pattern = /\/badges\/badge\.php.*([?&]hash=)/; + + /** + * Get the list of actions for a link (url). + * + * @param siteIds List of sites the URL belongs to. + * @param url The URL to treat. + * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param courseId Course ID related to the URL. Optional but recommended. + * @return List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] { + + return [{ + action: (siteId: string): void => { + CoreNavHelper.instance.goInSite( + '/badges/issue', + { courseId: 0, badgeHash: params.hash }, + siteId, + ); + }, + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param siteId The site ID. + * @param url The URL to treat. + * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param courseId Course ID related to the URL. Optional but recommended. + * @return Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string): Promise { + return AddonBadges.instance.isPluginEnabled(siteId); + } + +} + +export class AddonBadgesBadgeLinkHandler extends makeSingleton(AddonBadgesBadgeLinkHandlerService) {} diff --git a/src/addons/badges/services/handlers/mybadges-link.ts b/src/addons/badges/services/handlers/mybadges-link.ts new file mode 100644 index 000000000..9e0c873c9 --- /dev/null +++ b/src/addons/badges/services/handlers/mybadges-link.ts @@ -0,0 +1,58 @@ +// (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 { CoreNavHelper } from '@services/nav-helper'; +import { makeSingleton } from '@singletons'; +import { AddonBadges } from '../badges'; + +/** + * Handler to treat links to user badges page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBadgesMyBadgesLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonBadgesMyBadgesLinkHandler'; + featureName = 'CoreUserDelegate_AddonBadges'; + pattern = /\/badges\/mybadges\.php/; + + /** + * Get the list of actions for a link (url). + * + * @return List of (or promise resolved with list of) actions. + */ + getActions(): CoreContentLinksAction[] { + return [{ + action: (siteId: string): void => { + CoreNavHelper.instance.goInSite('/badges/user', {}, siteId); + }, + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param siteId The site ID. + * @return Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string): boolean | Promise { + return AddonBadges.instance.isPluginEnabled(siteId); + } + +} + +export class AddonBadgesMyBadgesLinkHandler extends makeSingleton(AddonBadgesMyBadgesLinkHandlerService) {} diff --git a/src/addons/badges/services/handlers/user.ts b/src/addons/badges/services/handlers/user.ts new file mode 100644 index 000000000..aad3c5ac2 --- /dev/null +++ b/src/addons/badges/services/handlers/user.ts @@ -0,0 +1,82 @@ +// (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 { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; +import { CoreUserProfile } from '@features/user/services/user'; +import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; +import { CoreNavHelper } from '@services/nav-helper'; +import { makeSingleton } from '@singletons'; +import { AddonBadges } from '../badges'; + +/** + * Profile badges handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBadgesUserHandlerService implements CoreUserProfileHandler { + + name = 'AddonBadges'; + priority = 50; + type = CoreUserDelegateService.TYPE_NEW_PAGE; + + /** + * Check if handler is enabled. + * + * @return Always enabled. + */ + isEnabled(): Promise { + return AddonBadges.instance.isPluginEnabled(); + } + + /** + * Check if handler is enabled for this user in this context. + * + * @param user User to check. + * @param courseId Course ID. + * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @return True if enabled, false otherwise. + */ + async isEnabledForUser( + user: CoreUserProfile, + courseId: number, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise { + if (navOptions && typeof navOptions.badges != 'undefined') { + return navOptions.badges; + } + + // If we reach here, it means we are opening the user site profile. + return true; + } + + /** + * Returns the data needed to render the handler. + * + * @return Data needed to render the handler. + */ + getDisplayData(): CoreUserProfileHandlerData { + return { + icon: 'fas-trophy', + title: 'addon.badges.badges', + action: (event, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); + CoreNavHelper.instance.goInSite('/badges/user', { courseId: courseId || 0, userId: user.id }); + }, + }; + } + +} + +export class AddonBadgesUserHandler extends makeSingleton(AddonBadgesUserHandlerService) {} diff --git a/src/addons/block/myoverview/components/myoverview/myoverview.ts b/src/addons/block/myoverview/components/myoverview/myoverview.ts index 63a6956c8..c4b8eae72 100644 --- a/src/addons/block/myoverview/components/myoverview/myoverview.ts +++ b/src/addons/block/myoverview/components/myoverview/myoverview.ts @@ -231,7 +231,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem protected async fetchContent(): Promise { const config = this.block.configsRecord || {}; - const showCategories = config && config.displaycategories && config.displaycategories.value == '1'; + const showCategories = config?.displaycategories?.value == '1'; const courses = await CoreCoursesHelper.instance.getUserCoursesWithOptions(this.sort, undefined, undefined, showCategories); @@ -257,26 +257,26 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.showFilter = false; this.showFilters.all = this.getShowFilterValue( - !config || config.displaygroupingall.value == '1', + !config || config.displaygroupingall?.value == '1', this.courses.all.length === 0, ); // Do not show allincludinghiddenif config it's not present (before 3.8). this.showFilters.allincludinghidden = this.getShowFilterValue( - config.displaygroupingallincludinghidden.value == '1', + config?.displaygroupingallincludinghidden?.value == '1', this.courses.allincludinghidden.length === 0, ); this.showFilters.inprogress = this.getShowFilterValue( - !config || config.displaygroupinginprogress.value == '1', + !config || config.displaygroupinginprogress?.value == '1', this.courses.inprogress.length === 0, ); this.showFilters.past = this.getShowFilterValue( - !config || config.displaygroupingpast.value == '1', + !config || config.displaygroupingpast?.value == '1', this.courses.past.length === 0, ); this.showFilters.future = this.getShowFilterValue( - !config || config.displaygroupingfuture.value == '1', + !config || config.displaygroupingfuture?.value == '1', this.courses.future.length === 0, ); @@ -285,24 +285,22 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.showFilters.hidden = this.getShowFilterValue( this.showSelectorFilter && typeof courses[0].hidden != 'undefined' && - (!config || config.displaygroupinghidden.value == '1'), + (!config || config.displaygroupinghidden?.value == '1'), this.courses.hidden.length === 0, ); this.showFilters.favourite = this.getShowFilterValue( this.showSelectorFilter && typeof courses[0].isfavourite != 'undefined' && - (!config || (config.displaygroupingstarred && config.displaygroupingstarred.value == '1') || - (config.displaygroupingfavourites && config.displaygroupingfavourites.value == '1')), + (!config || config.displaygroupingstarred?.value == '1' || config.displaygroupingfavourites?.value == '1'), this.courses.favourite.length === 0, ); this.showFilters.custom = this.getShowFilterValue( - this.showSelectorFilter && config?.displaygroupingcustomfield.value == '1' && - !!config?.customfieldsexport && !!config?.customfieldsexport.value, + this.showSelectorFilter && config?.displaygroupingcustomfield?.value == '1' && !!config?.customfieldsexport?.value, false, ); if (this.showFilters.custom == 'show') { - this.customFilter = CoreTextUtils.instance.parseJSON(config.customfieldsexport.value, []); + this.customFilter = CoreTextUtils.instance.parseJSON(config?.customfieldsexport?.value, []); } else { this.customFilter = []; } diff --git a/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html b/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html index eceb7a099..a9e637d33 100644 --- a/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html +++ b/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html @@ -6,7 +6,10 @@ - + + + diff --git a/src/addons/coursecompletion/services/coursecompletion.ts b/src/addons/coursecompletion/services/coursecompletion.ts index 6dc4e6811..6ae4d2b92 100644 --- a/src/addons/coursecompletion/services/coursecompletion.ts +++ b/src/addons/coursecompletion/services/coursecompletion.ts @@ -103,7 +103,7 @@ export class AddonCourseCompletionProvider { userId = userId || site.getUserId(); this.logger.debug('Get completion for course ' + courseId + ' and user ' + userId); - const data: CoreCompletionGetCourseCompletionStatusWSParams = { + const data: AddonCourseCompletionGetCourseCompletionStatusWSParams = { courseid: courseId, userid: userId, }; @@ -112,7 +112,7 @@ export class AddonCourseCompletionProvider { preSets.updateFrequency = preSets.updateFrequency || CoreSite.FREQUENCY_SOMETIMES; preSets.cacheErrors = ['notenroled']; - const result: CoreCompletionGetCourseCompletionStatusWSResponse = + const result: AddonCourseCompletionGetCourseCompletionStatusWSResponse = await site.read('core_completion_get_course_completion_status', data, preSets); if (result.completionstatus) { return result.completionstatus; @@ -253,7 +253,7 @@ export class AddonCourseCompletionProvider { async markCourseAsSelfCompleted(courseId: number, siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); - const params: CoreCompletionMarkCourseSelfCompletedWSParams = { + const params: AddonCourseCompletionMarkCourseSelfCompletedWSParams = { courseid: courseId, }; @@ -292,7 +292,7 @@ export type AddonCourseCompletionCourseCompletionStatus = { /** * Params of core_completion_get_course_completion_status WS. */ -export type CoreCompletionGetCourseCompletionStatusWSParams = { +export type AddonCourseCompletionGetCourseCompletionStatusWSParams = { courseid: number; // Course ID. userid: number; // User ID. }; @@ -300,7 +300,7 @@ export type CoreCompletionGetCourseCompletionStatusWSParams = { /** * Data returned by core_completion_get_course_completion_status WS. */ -export type CoreCompletionGetCourseCompletionStatusWSResponse = { +export type AddonCourseCompletionGetCourseCompletionStatusWSResponse = { completionstatus: AddonCourseCompletionCourseCompletionStatus; // Course status. warnings?: CoreWSExternalWarning[]; }; @@ -308,6 +308,6 @@ export type CoreCompletionGetCourseCompletionStatusWSResponse = { /** * Params of core_completion_mark_course_self_completed WS. */ -export type CoreCompletionMarkCourseSelfCompletedWSParams = { +export type AddonCourseCompletionMarkCourseSelfCompletedWSParams = { courseid: number; // Course ID. }; diff --git a/src/core/components/context-menu/core-context-menu-popover.html b/src/core/components/context-menu/core-context-menu-popover.html index 36cfcacdd..971d6a9ef 100644 --- a/src/core/components/context-menu/core-context-menu-popover.html +++ b/src/core/components/context-menu/core-context-menu-popover.html @@ -1,11 +1,13 @@ - {{title}} + + {{title}} + - + diff --git a/src/core/components/show-password/show-password.scss b/src/core/components/show-password/show-password.scss index 1f98c810c..a0960f099 100644 --- a/src/core/components/show-password/show-password.scss +++ b/src/core/components/show-password/show-password.scss @@ -6,7 +6,7 @@ padding: 0 calc(var(--padding-start) / 2); position: absolute; right: 0; - bottom: calc(var(--padding-bottom) / 2); + bottom: 8px; margin-top: 0; margin-bottom: 0; z-index: 3; diff --git a/src/core/features/contentlinks/pages/choose-site/choose-site.html b/src/core/features/contentlinks/pages/choose-site/choose-site.html index c88916023..488670d69 100644 --- a/src/core/features/contentlinks/pages/choose-site/choose-site.html +++ b/src/core/features/contentlinks/pages/choose-site/choose-site.html @@ -10,8 +10,10 @@ -

{{ 'core.contentlinks.chooseaccounttoopenlink' | translate }}

-

{{ url }}

+ +

{{ 'core.contentlinks.chooseaccounttoopenlink' | translate }}

+

{{ url }}

+
@@ -19,12 +21,16 @@ alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}" role="presentation" onError="this.src='assets/img/user-avatar.png'"> -

{{site.fullName}}

-

-

{{site.siteUrl}}

+ +

{{site.fullName}}

+

+

{{site.siteUrl}}

+
- {{ 'core.login.cancel' | translate }} + + {{ 'core.login.cancel' | translate }} +
diff --git a/src/core/features/courses/components/course-progress/course-progress.ts b/src/core/features/courses/components/course-progress/course-progress.ts index 057156451..54f9755fd 100644 --- a/src/core/features/courses/components/course-progress/course-progress.ts +++ b/src/core/features/courses/components/course-progress/course-progress.ts @@ -213,7 +213,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { }, event: e, }); - popover.present(); + await popover.present(); const action = await popover.onDidDismiss(); diff --git a/src/core/features/courses/components/self-enrol-password/self-enrol-password.html b/src/core/features/courses/components/self-enrol-password/self-enrol-password.html index 3012c1939..ee4c213f3 100644 --- a/src/core/features/courses/components/self-enrol-password/self-enrol-password.html +++ b/src/core/features/courses/components/self-enrol-password/self-enrol-password.html @@ -15,17 +15,19 @@
- - - - + + + + + +
{{ 'core.courses.enrolme' | translate }} diff --git a/src/core/features/courses/pages/course-preview/course-preview.html b/src/core/features/courses/pages/course-preview/course-preview.html index 0baa7286b..4f2a6a09a 100644 --- a/src/core/features/courses/pages/course-preview/course-preview.html +++ b/src/core/features/courses/pages/course-preview/course-preview.html @@ -52,7 +52,7 @@

{{contact.fullname}}

- + diff --git a/src/core/features/emulator/services/capture-helper.ts b/src/core/features/emulator/services/capture-helper.ts index d282ae4d6..7b9079727 100644 --- a/src/core/features/emulator/services/capture-helper.ts +++ b/src/core/features/emulator/services/capture-helper.ts @@ -107,7 +107,7 @@ export class CoreEmulatorCaptureHelperProvider { componentProps: params, }); - modal.present(); + await modal.present(); const result = await modal.onDidDismiss(); diff --git a/src/core/features/fileuploader/services/fileuploader-helper.ts b/src/core/features/fileuploader/services/fileuploader-helper.ts index 36d2924ec..a303ee0bd 100644 --- a/src/core/features/fileuploader/services/fileuploader-helper.ts +++ b/src/core/features/fileuploader/services/fileuploader-helper.ts @@ -420,7 +420,7 @@ export class CoreFileUploaderHelperProvider { header: title ? title : Translate.instance.instant('core.fileuploader.' + (upload ? 'uploadafile' : 'selectafile')), buttons: buttons, }); - this.actionSheet.present(); + await this.actionSheet.present(); // Call afterRender for each button. setTimeout(() => { diff --git a/src/core/features/fileuploader/services/fileuploader.ts b/src/core/features/fileuploader/services/fileuploader.ts index 58dfa0e7e..6cbc4bb91 100644 --- a/src/core/features/fileuploader/services/fileuploader.ts +++ b/src/core/features/fileuploader/services/fileuploader.ts @@ -130,7 +130,7 @@ export class CoreFileUploaderProvider { backdropDismiss: false, }); - modal.present(); + await modal.present(); const result = await modal.onWillDismiss(); diff --git a/src/core/features/login/pages/credentials/credentials.html b/src/core/features/login/pages/credentials/credentials.html index 3ba70db0b..498b4f7ec 100644 --- a/src/core/features/login/pages/credentials/credentials.html +++ b/src/core/features/login/pages/credentials/credentials.html @@ -31,17 +31,21 @@ - + + + - - - - + + + + + + - - - - + + + + + + diff --git a/src/core/features/login/pages/site/site.html b/src/core/features/login/pages/site/site.html index 29def2140..51d15e67d 100644 --- a/src/core/features/login/pages/site/site.html +++ b/src/core/features/login/pages/site/site.html @@ -74,7 +74,11 @@ - {{ 'core.login.connect' | translate }} + + + {{ 'core.login.connect' | translate }} + + diff --git a/src/core/features/mainmenu/pages/more/more.html b/src/core/features/mainmenu/pages/more/more.html index aba57a084..20a7eb21b 100644 --- a/src/core/features/mainmenu/pages/more/more.html +++ b/src/core/features/mainmenu/pages/more/more.html @@ -20,9 +20,9 @@

{{ siteUrl }}

- + - + @@ -83,7 +83,7 @@

{{ logoutLabel | translate }}

- + diff --git a/src/core/features/search/components/search-box/core-search-box.html b/src/core/features/search/components/search-box/core-search-box.html index e8f4b3aa5..70b6e55da 100644 --- a/src/core/features/search/components/search-box/core-search-box.html +++ b/src/core/features/search/components/search-box/core-search-box.html @@ -21,7 +21,7 @@ (click)="historyClicked($event, item.searchedtext)" tabindex="1" detail> - {{item.searchedtext}} + {{item.searchedtext}} diff --git a/src/core/features/settings/pages/general/general.html b/src/core/features/settings/pages/general/general.html index 141186cd4..9f1b8ccab 100644 --- a/src/core/features/settings/pages/general/general.html +++ b/src/core/features/settings/pages/general/general.html @@ -24,9 +24,11 @@ - {{ 'core.settings.fontsizecharacter' | translate }} - - + + {{ 'core.settings.fontsizecharacter' | translate }} + + + diff --git a/src/core/features/settings/pages/site/site.html b/src/core/features/settings/pages/site/site.html index b20ac3539..aafde72c6 100644 --- a/src/core/features/settings/pages/site/site.html +++ b/src/core/features/settings/pages/site/site.html @@ -24,7 +24,7 @@

{{ siteUrl }}

- + - {{ 'core.user.contact' | translate}} + {{ 'core.user.contact' | translate}}

{{ 'core.user.email' | translate }}

@@ -60,7 +60,7 @@
- {{ 'core.userdetails' | translate}} + {{ 'core.userdetails' | translate}}

{{ 'core.user.webpage' | translate}}

@@ -80,7 +80,7 @@
- {{ 'core.user.description' | translate}} + {{ 'core.user.description' | translate}}

diff --git a/src/core/features/user/pages/profile/profile.html b/src/core/features/user/pages/profile/profile.html index d7f1c466b..256ce3568 100644 --- a/src/core/features/user/pages/profile/profile.html +++ b/src/core/features/user/pages/profile/profile.html @@ -34,7 +34,7 @@ -

{{handler.title | translate}}

@@ -49,35 +49,37 @@
- + + [ngClass]="['core-user-profile-handler', handler.class || '']" [hidden]="handler.hidden" + title="{{ handler.title | translate }}" detail> + -

{{ handler.title | translate }}

- - + + - {{ handler.title | translate }} - - - + + {{ handler.title | translate }} + + + + diff --git a/src/core/features/user/pages/profile/profile.scss b/src/core/features/user/pages/profile/profile.scss index d3bcabffe..dde354360 100644 --- a/src/core/features/user/pages/profile/profile.scss +++ b/src/core/features/user/pages/profile/profile.scss @@ -9,7 +9,7 @@ --core-avatar-size: var(--core-large-avatar-size); img { - margin: 0; + margin: 8px auto; } .contact-status { diff --git a/src/core/features/user/services/user-delegate.ts b/src/core/features/user/services/user-delegate.ts index 08b3f799f..4992fa802 100644 --- a/src/core/features/user/services/user-delegate.ts +++ b/src/core/features/user/services/user-delegate.ts @@ -20,6 +20,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreEvents } from '@singletons/events'; import { CoreUserProfile } from './user'; import { makeSingleton } from '@singletons'; +import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; /** * Interface that all user profile handlers must implement. @@ -48,7 +49,12 @@ export interface CoreUserProfileHandler extends CoreDelegateHandler { * @param admOptions Admin options for the course. * @return Whether or not the handler is enabled for a user. */ - isEnabledForUser(user: CoreUserProfile, courseId: number, navOptions?: unknown, admOptions?: unknown): Promise; + isEnabledForUser( + user: CoreUserProfile, + courseId: number, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + admOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise; /** * Returns the data needed to render the handler. diff --git a/src/core/services/nav-helper.ts b/src/core/services/nav-helper.ts index 2a98bd871..69819bac4 100644 --- a/src/core/services/nav-helper.ts +++ b/src/core/services/nav-helper.ts @@ -93,19 +93,20 @@ export class CoreNavHelperService { * Goes to a certain page in a certain site. If the site is current site it will perform a regular navigation, * otherwise it will load the other site and open the page in main menu. * - * @param pageName Name of the page to go. + * @param path Path to go. * @param pageParams Params to send to the page. * @param siteId Site ID. If not defined, current site. - * @param checkMenu If true, check if the root page of a main menu tab. Only the page name will be checked. + * @param checkMenu If true, check if the root page is on a main menu tab. Only the path will be checked. * @return Promise resolved when done. */ - async goInSite(pageName: string, pageParams: Params, siteId?: string, checkMenu?: boolean): Promise { + async goInSite(path: string, pageParams: Params, siteId?: string, checkMenu?: boolean): Promise { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); // @todo: When this function was in ContentLinksHelper, this code was inside NgZone. Check if it's needed. if (!CoreSites.instance.isLoggedIn() || siteId != CoreSites.instance.getCurrentSiteId()) { - await this.openInSiteMainMenu(pageName, pageParams, siteId); + await this.openInSiteMainMenu(path, pageParams, siteId); return; } @@ -114,20 +115,20 @@ export class CoreNavHelperService { let isInMenu = false; // Check if the page is in the main menu. try { - isInMenu = await CoreMainMenu.instance.isCurrentMainMenuHandler(pageName); + isInMenu = await CoreMainMenu.instance.isCurrentMainMenuHandler(path); } catch { isInMenu = false; } if (isInMenu) { // Just select the tab. @todo test. - CoreNavHelper.instance.loadPageInMainMenu(pageName, pageParams); + CoreNavHelper.instance.loadPageInMainMenu(path, pageParams); return; } } - await this.goInCurrentMainMenuTab(pageName, pageParams); + await this.goInCurrentMainMenuTab(path, pageParams); } /** diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index cff84e7d1..9e9db3227 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -1574,7 +1574,7 @@ export class CoreDomUtilsProvider { cssClass: cssClass, }); - loader.present(); + await loader.present(); return loader; } diff --git a/src/theme/app.scss b/src/theme/app.scss index e88fd2d8c..c42f9f5bd 100644 --- a/src/theme/app.scss +++ b/src/theme/app.scss @@ -205,7 +205,7 @@ ion-card.core-danger-card { img.large-avatar, .large-avatar img { display: block; - margin: auto; + margin: 8px auto; width: var(--core-large-avatar-size); height: var(--core-large-avatar-size); max-width: var(--core-large-avatar-size);