diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index d5b5264c0..0a0ab6342 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -30,6 +30,7 @@ import { AddonQtypeModule } from './qtype/qtype.module'; import { AddonBlogModule } from './blog/blog.module'; import { AddonRemoteThemesModule } from './remotethemes/remotethemes.module'; import { AddonNotesModule } from './notes/notes.module'; +import { AddonCompetencyModule } from './competency/competency.module'; @NgModule({ imports: [ @@ -37,6 +38,7 @@ import { AddonNotesModule } from './notes/notes.module'; AddonBadgesModule, AddonBlogModule, AddonCalendarModule, + AddonCompetencyModule, AddonCourseCompletionModule, AddonMessagesModule, AddonPrivateFilesModule, diff --git a/src/addons/block/learningplans/services/block-handler.ts b/src/addons/block/learningplans/services/block-handler.ts index 8cdaf501f..774ddad1c 100644 --- a/src/addons/block/learningplans/services/block-handler.ts +++ b/src/addons/block/learningplans/services/block-handler.ts @@ -17,6 +17,7 @@ import { CoreBlockHandlerData } from '@features/block/services/block-delegate'; import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block'; import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; import { makeSingleton } from '@singletons'; +import { AddonCompetencyMainMenuHandlerService } from '@addons/competency/services/handlers/mainmenu'; /** * Block handler. @@ -39,7 +40,7 @@ export class AddonBlockLearningPlansHandlerService extends CoreBlockBaseHandler title: 'addon.block_learningplans.pluginname', class: 'addon-block-learning-plans', component: CoreBlockOnlyTitleComponent, - link: 'AddonCompetencyPlanListPage', + link: AddonCompetencyMainMenuHandlerService.PAGE_NAME, }; } diff --git a/src/addons/competency/competency-course-lazy.module.ts b/src/addons/competency/competency-course-lazy.module.ts new file mode 100644 index 000000000..860401684 --- /dev/null +++ b/src/addons/competency/competency-course-lazy.module.ts @@ -0,0 +1,34 @@ +// (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 { AddonCompetencyCourseCompetenciesPageModule } from './pages/coursecompetencies/coursecompetencies.module'; +import { AddonCompetencyCourseCompetenciesPage } from './pages/coursecompetencies/coursecompetencies.page'; + +const routes: Routes = [ + { + path: '', + component: AddonCompetencyCourseCompetenciesPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + AddonCompetencyCourseCompetenciesPageModule, + ], +}) +export class AddonCompetencyCourseLazyModule {} diff --git a/src/addons/competency/competency-lazy.module.ts b/src/addons/competency/competency-lazy.module.ts new file mode 100644 index 000000000..09dce93a7 --- /dev/null +++ b/src/addons/competency/competency-lazy.module.ts @@ -0,0 +1,108 @@ +// (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 { conditionalRoutes } from '@/app/app-routing.module'; +import { CoreScreen } from '@services/screen'; +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonCompetencyPlanPage } from './pages/plan/plan'; +import { AddonCompetencyPlanListPage } from './pages/planlist/planlist'; +import { AddonCompetencyCompetenciesPage } from './pages/competencies/competencies'; +import { AddonCompetencyCompetencyPage } from './pages/competency/competency'; +import { AddonCompetencyCompetencySummaryPage } from './pages/competencysummary/competencysummary'; +import { AddonCompetencyCourseCompetenciesPage } from './pages/coursecompetencies/coursecompetencies.page'; +import { AddonCompetencyCourseCompetenciesPageModule } from './pages/coursecompetencies/coursecompetencies.module'; + +const mobileRoutes: Routes = [ + { + path: '', + pathMatch: 'full', + component: AddonCompetencyPlanListPage, + }, + { + path: 'competencies', + component: AddonCompetencyCompetenciesPage, + }, + { + path: 'competencies/:competencyId', + component: AddonCompetencyCompetencyPage, + }, + { + path: 'course/:courseId', + component: AddonCompetencyCourseCompetenciesPage, + }, + { + path: 'summary/:competencyId', + component: AddonCompetencyCompetencySummaryPage, + }, + { + path: ':planId', + component: AddonCompetencyPlanPage, + }, +]; + +const tabletRoutes: Routes = [ + { + path: 'summary/:competencyId', + component: AddonCompetencyCompetencySummaryPage, + }, + { + path: 'competencies', + component: AddonCompetencyCompetenciesPage, + children: [ + { + path: ':competencyId', + component: AddonCompetencyCompetencyPage, + }, + ], + }, + { + path: 'course/:courseId', + component: AddonCompetencyCourseCompetenciesPage, + }, + { + path: '', + component: AddonCompetencyPlanListPage, + children: [ + { + path: ':planId', + component: AddonCompetencyPlanPage, + }, + + ], + }, +]; + +const routes: Routes = [ + ...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile), + ...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet), +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + AddonCompetencyCourseCompetenciesPageModule, + ], + declarations: [ + AddonCompetencyPlanPage, + AddonCompetencyPlanListPage, + AddonCompetencyCompetenciesPage, + AddonCompetencyCompetencyPage, + AddonCompetencyCompetencySummaryPage, + ], +}) +export class AddonCompetencyLazyModule {} diff --git a/src/addons/competency/competency.module.ts b/src/addons/competency/competency.module.ts new file mode 100644 index 000000000..e8a96e9e8 --- /dev/null +++ b/src/addons/competency/competency.module.ts @@ -0,0 +1,81 @@ +// (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, Type } from '@angular/core'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; +import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate'; +import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; +import { CoreUserDelegate } from '@features/user/services/user-delegate'; +import { AddonCompetencyProvider } from './services/competency'; +import { AddonCompetencyHelperProvider } from './services/competency-helper'; +import { AddonCompetencyCompetencyLinkHandler } from './services/handlers/competency-link'; +import { AddonCompetencyCourseOptionHandler } from './services/handlers/course-option'; +import { AddonCompetencyMainMenuHandler, AddonCompetencyMainMenuHandlerService } from './services/handlers/mainmenu'; +import { AddonCompetencyPlanLinkHandler } from './services/handlers/plan-link'; +import { AddonCompetencyPlansLinkHandler } from './services/handlers/plans-link'; +import { AddonCompetencyPushClickHandler } from './services/handlers/push-click'; +import { AddonCompetencyUserCompetencyLinkHandler } from './services/handlers/user-competency-link'; +import { AddonCompetencyUserHandler } from './services/handlers/user'; +import { Routes } from '@angular/router'; +import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module'; + +// List of providers (without handlers). +export const ADDON_COMPETENCY_SERVICES: Type[] = [ + AddonCompetencyProvider, + AddonCompetencyHelperProvider, +]; + +const mainMenuChildrenRoutes: Routes = [ + { + path: AddonCompetencyMainMenuHandlerService.PAGE_NAME, + loadChildren: () => import('./competency-lazy.module').then(m => m.AddonCompetencyLazyModule), + }, +]; + +const courseIndexRoutes: Routes = [ + { + path: AddonCompetencyMainMenuHandlerService.PAGE_NAME, + loadChildren: () => import('@addons/competency/competency-course-lazy.module').then(m => m.AddonCompetencyCourseLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(mainMenuChildrenRoutes), + CoreMainMenuRoutingModule.forChild({ children: mainMenuChildrenRoutes }), + CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }), + ], + exports: [CoreMainMenuRoutingModule], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => async () => { + CoreContentLinksDelegate.registerHandler(AddonCompetencyCompetencyLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonCompetencyPlanLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonCompetencyPlansLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonCompetencyUserCompetencyLinkHandler.instance); + CoreMainMenuDelegate.registerHandler(AddonCompetencyMainMenuHandler.instance); + CoreUserDelegate.registerHandler(AddonCompetencyUserHandler.instance); + CoreCourseOptionsDelegate.registerHandler(AddonCompetencyCourseOptionHandler.instance); + CorePushNotificationsDelegate.registerClickHandler(AddonCompetencyPushClickHandler.instance); + }, + }, + ], +}) +export class AddonCompetencyModule {} diff --git a/src/addons/competency/lang.json b/src/addons/competency/lang.json new file mode 100644 index 000000000..e09256cb0 --- /dev/null +++ b/src/addons/competency/lang.json @@ -0,0 +1,50 @@ +{ + "activities": "Activities", + "competencies": "Competencies", + "competenciesmostoftennotproficientincourse": "Competencies most often not proficient in this course", + "coursecompetencies": "Course competencies", + "coursecompetencyratingsarenotpushedtouserplans": "Competency ratings in this course do not affect learning plans.", + "coursecompetencyratingsarepushedtouserplans": "Competency ratings in this course are updated immediately in learning plans.", + "crossreferencedcompetencies": "Cross-referenced competencies", + "duedate": "Due date", + "errornocompetenciesfound": "No competencies found", + "evidence": "Evidence", + "evidence_competencyrule": "The rule of the competency was met.", + "evidence_coursecompleted": "The course '{{$a}}' was completed.", + "evidence_coursemodulecompleted": "The activity '{{$a}}' was completed.", + "evidence_courserestored": "The rating was restored along with the course '{{$a}}'.", + "evidence_evidenceofpriorlearninglinked": "The evidence of prior learning '{{$a}}' was linked.", + "evidence_evidenceofpriorlearningunlinked": "The evidence of prior learning '{{$a}}' was unlinked.", + "evidence_manualoverride": "The competency rating was manually set.", + "evidence_manualoverrideincourse": "The competency rating was manually set in the course '{{$a}}'.", + "evidence_manualoverrideinplan": "The competency rating was manually set in the learning plan '{{$a}}'.", + "learningplancompetencies": "Learning plan competencies", + "learningplans": "Learning plans", + "myplans": "My learning plans", + "noactivities": "No activities", + "nocompetencies": "No competencies", + "nocompetenciesincourse": "No competencies have been linked to this course.", + "nocrossreferencedcompetencies": "No other competencies have been cross-referenced to this competency.", + "noevidence": "No evidence", + "noplanswerecreated": "No learning plans were created.", + "nouserplanswithcompetency": "No learning plans contain this competency.", + "path": "Path:", + "planstatusactive": "Active", + "planstatuscomplete": "Complete", + "planstatusdraft": "Draft", + "planstatusinreview": "In review", + "planstatuswaitingforreview": "Waiting for review", + "proficient": "Proficient", + "progress": "Progress", + "rating": "Rating", + "reviewstatus": "Review status", + "status": "Status", + "template": "Learning plan template", + "uponcoursecompletion": "Upon course completion:", + "usercompetencystatus_idle": "Idle", + "usercompetencystatus_inreview": "In review", + "usercompetencystatus_waitingforreview": "Waiting for review", + "userplans": "Learning plans", + "xcompetenciesproficientoutofy": "{{$a.x}} out of {{$a.y}} competencies are proficient", + "xcompetenciesproficientoutofyincourse": "You are proficient in {{$a.x}} out of {{$a.y}} competencies in this course." +} \ No newline at end of file diff --git a/src/addons/competency/pages/competencies/competencies.html b/src/addons/competency/pages/competencies/competencies.html new file mode 100644 index 000000000..6bf82cfa5 --- /dev/null +++ b/src/addons/competency/pages/competencies/competencies.html @@ -0,0 +1,34 @@ + + + + + + {{ title }} + + + + + + + + + + + +

{{ competency.competency.shortname }} {{competency.competency.idnumber}}

+
+ + {{ competency.usercompetency.gradename }} + + + {{ competency.usercompetencycourse.gradename }} + +
+
+
+
+
diff --git a/src/addons/competency/pages/competencies/competencies.ts b/src/addons/competency/pages/competencies/competencies.ts new file mode 100644 index 000000000..399c5371c --- /dev/null +++ b/src/addons/competency/pages/competencies/competencies.ts @@ -0,0 +1,172 @@ +// (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 { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { IonRefresher } from '@ionic/angular'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { + AddonCompetencyDataForPlanPageCompetency, AddonCompetencyDataForCourseCompetenciesPageCompetency, AddonCompetency, +} from '../../services/competency'; +import { Params, ActivatedRouteSnapshot, ActivatedRoute } from '@angular/router'; +import { CorePageItemsListManager } from '@classes/page-items-list-manager'; +import { Translate } from '@singletons'; +import { CoreNavigator } from '@services/navigator'; +import { CoreError } from '@classes/errors/error'; + +/** + * Page that displays the list of competencies of a learning plan. + */ +@Component({ + selector: 'page-addon-competency-competencies', + templateUrl: 'competencies.html', +}) +export class AddonCompetencyCompetenciesPage implements AfterViewInit, OnDestroy { + + @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; + + protected planId?: number; + protected courseId?: number; + protected userId?: number; + + competenciesLoaded = false; + competencies: AddonCompetencyListManager; + title = ''; + + constructor(protected route: ActivatedRoute) { + this.planId = CoreNavigator.getRouteNumberParam('planId', { route }); + if (!this.planId) { + this.courseId = CoreNavigator.getRouteNumberParam('courseId', { route }); + this.userId = CoreNavigator.getRouteNumberParam('userId', { route }); + } + + this.competencies = + new AddonCompetencyListManager(AddonCompetencyCompetenciesPage, this.planId, this.courseId, this.userId); + } + + /** + * @inheritdoc + */ + async ngAfterViewInit(): Promise { + await this.fetchCompetencies(); + + this.competencies.start(this.splitView); + } + + /** + * Fetches the competencies and updates the view. + * + * @return Promise resolved when done. + */ + protected async fetchCompetencies(): Promise { + try { + if (this.planId) { + + const response = await AddonCompetency.getLearningPlan(this.planId); + + if (response.competencycount <= 0) { + throw new CoreError(Translate.instant('addon.competency.errornocompetenciesfound')); + } + + this.title = response.plan.name; + this.userId = response.plan.userid; + + this.competencies.setItems(response.competencies); + } else if (this.courseId) { + const response = await AddonCompetency.getCourseCompetencies(this.courseId, this.userId); + this.title = Translate.instant('addon.competency.coursecompetencies'); + + this.competencies.setItems(response.competencies); + } else { + throw null; + } + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error getting competencies data.'); + } + } + + /** + * Refreshes the competencies. + * + * @param refresher Refresher. + */ + async refreshCompetencies(refresher?: IonRefresher): Promise { + try { + if (this.planId) { + await AddonCompetency.invalidateLearningPlan(this.planId); + } else { + await AddonCompetency.invalidateCourseCompetencies(this.courseId!, this.userId); + } + + } finally { + this.fetchCompetencies().finally(() => { + refresher?.complete(); + }); + } + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.competencies.destroy(); + } + +} + +type AddonCompetencyDataForPlanPageCompetencyFormatted = + AddonCompetencyDataForPlanPageCompetency | AddonCompetencyDataForCourseCompetenciesPageCompetency; + +/** + * Helper class to manage competencies list. + */ +class AddonCompetencyListManager extends CorePageItemsListManager { + + planId?: number; + courseId?: number; + userId?: number; + + constructor(pageComponent: unknown, planId?: number, courseId?: number, userId?: number) { + super(pageComponent); + this.planId = planId; + this.courseId = courseId; + this.userId = userId; + } + + /** + * @inheritdoc + */ + protected getItemPath(competency: AddonCompetencyDataForPlanPageCompetencyFormatted): string { + return String(competency.competency.id); + } + + /** + * @inheritdoc + */ + protected getItemQueryParams(): Params { + if (this.planId) { + return { planId: this.planId }; + } else { + return { courseId: this.courseId, userId: this.userId }; + } + } + + /** + * @inheritdoc + */ + protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null { + return route.params.competencyId ?? null; + } + +} diff --git a/src/addons/competency/pages/competency/competency.html b/src/addons/competency/pages/competency/competency.html new file mode 100644 index 000000000..c565defd1 --- /dev/null +++ b/src/addons/competency/pages/competency/competency.html @@ -0,0 +1,139 @@ + + + + + + + {{ competency.competency.competency.shortname }} {{ competency.competency.competency.idnumber }} + + + + + + + + + + + +

{{ user.fullname }}

+
+
+ + + + + + + + + + + {{ 'addon.competency.path' | translate }} + + {{ competency.competency.comppath.framework.name }} + + + {{ competency.competency.comppath.framework.name }} + +  /  + + + {{ ancestor.name }} + + {{ ancestor.name }} +  /  + + + + + + {{ 'addon.competency.crossreferencedcompetencies' | translate }}: +
+ {{ 'addon.competency.nocrossreferencedcompetencies' | translate }} +
+ +
+
+ + + {{ 'addon.competency.activities' | translate }} +

+ {{ 'addon.competency.noactivities' | translate }} +

+ + + + + + + +
+
+ + + + {{ 'addon.competency.reviewstatus' | translate }} + {{ competency.usercompetency!.statusname }} + + + + + {{ 'addon.competency.proficient' | translate }} + + + {{ 'core.yes' | translate }} + + + {{ 'core.no' | translate }} + + + + + {{ 'addon.competency.rating' | translate }} + + {{ userCompetency.gradename }} + + +
+ +
+

{{ 'addon.competency.evidence' | translate }}

+

+ {{ 'addon.competency.noevidence' | translate }} +

+ + + + +

{{ evidence.actionuser.fullname }}

+

{{ evidence.timemodified * 1000 | coreFormatDate }}

+
+
+ + +

{{ evidence.gradename }}

+

{{ evidence.description }}

+
{{ evidence.note }}
+
+
+
+
+
+
diff --git a/src/addons/competency/pages/competency/competency.ts b/src/addons/competency/pages/competency/competency.ts new file mode 100644 index 000000000..e4ebe432e --- /dev/null +++ b/src/addons/competency/pages/competency/competency.ts @@ -0,0 +1,186 @@ +// (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 { AddonCompetencyHelper } from '@addons/competency/services/competency-helper'; +import { Component, OnInit } from '@angular/core'; +import { CoreCourseModuleSummary } from '@features/course/services/course'; +import { CoreUserSummary } from '@features/user/services/user'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { Translate } from '@singletons'; +import { + AddonCompetencyDataForUserCompetencySummaryWSResponse, + AddonCompetencyUserCompetencyPlan, + AddonCompetencyUserCompetency, + AddonCompetencyUserCompetencyCourse, + AddonCompetency, + AddonCompetencyDataForUserCompetencySummaryInPlanWSResponse, + AddonCompetencyDataForUserCompetencySummaryInCourseWSResponse, +} from '@addons/competency/services/competency'; +import { CoreNavigator } from '@services/navigator'; +import { IonRefresher } from '@ionic/angular'; +import { ContextLevel } from '@/core/constants'; +import { CoreUtils } from '@services/utils/utils'; +import { AddonCompetencyMainMenuHandlerService } from '@addons/competency/services/handlers/mainmenu'; + +/** + * Page that displays the competency information. + */ +@Component({ + selector: 'page-addon-competency-competency', + templateUrl: 'competency.html', +}) +export class AddonCompetencyCompetencyPage implements OnInit { + + competencyLoaded = false; + competencyId!: number; + planId?: number; + courseId?: number; + userId?: number; + planStatus?: number; + coursemodules?: CoreCourseModuleSummary[]; + user?: CoreUserSummary; + competency?: AddonCompetencyDataForUserCompetencySummaryWSResponse; + userCompetency?: AddonCompetencyUserCompetencyPlan | AddonCompetencyUserCompetency | AddonCompetencyUserCompetencyCourse; + contextLevel?: string; + contextInstanceId?: number; + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + this.competencyId = CoreNavigator.getRouteNumberParam('competencyId')!; + this.planId = CoreNavigator.getRouteNumberParam('planId'); + if (!this.planId) { + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.userId = CoreNavigator.getRouteNumberParam('userId'); + } + + try { + await this.fetchCompetency(); + + const name = this.competency && this.competency.competency && this.competency.competency.competency && + this.competency.competency.competency.shortname; + + if (this.planId) { + CoreUtils.ignoreErrors(AddonCompetency.logCompetencyInPlanView( + this.planId, + this.competencyId, + this.planStatus!, + name, + this.userId, + )); + } else { + CoreUtils.ignoreErrors( + AddonCompetency.logCompetencyInCourseView(this.courseId!, this.competencyId, name, this.userId), + ); + } + } finally { + this.competencyLoaded = true; + } + } + + /** + * Fetches the competency and updates the view. + * + * @return Promise resolved when done. + */ + protected async fetchCompetency(): Promise { + + try { + let competency: AddonCompetencyDataForUserCompetencySummaryInPlanWSResponse | + AddonCompetencyDataForUserCompetencySummaryInCourseWSResponse; + + if (this.planId) { + this.planStatus = undefined; + + competency = await AddonCompetency.getCompetencyInPlan(this.planId, this.competencyId); + } else if (this.courseId) { + competency = await AddonCompetency.getCompetencyInCourse(this.courseId, this.competencyId, this.userId); + } else { + throw null; + } + + // Calculate the context. + if (this.courseId) { + this.contextLevel = ContextLevel.COURSE; + this.contextInstanceId = this.courseId; + } else { + this.contextLevel = ContextLevel.USER; + this.contextInstanceId = this.userId || competency.usercompetencysummary.user.id; + } + + this.competency = competency.usercompetencysummary; + this.userCompetency = this.competency.usercompetencyplan || this.competency.usercompetency; + + if ('plan' in competency) { + this.planStatus = competency.plan.status; + this.competency.usercompetency!.statusname = + AddonCompetencyHelper.getCompetencyStatusName(this.competency.usercompetency!.status); + } else { + this.userCompetency = this.competency.usercompetencycourse; + this.coursemodules = competency.coursemodules; + } + + if (this.competency.user.id != CoreSites.getCurrentSiteUserId()) { + // Get the user profile from the returned object. + this.user = this.competency.user; + } + + this.competency.evidence.forEach((evidence) => { + if (evidence.descidentifier) { + const key = 'addon.competency.' + evidence.descidentifier; + evidence.description = Translate.instant(key, { $a: evidence.desca }); + } + }); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error getting competency data.'); + } + } + + /** + * Refreshes the competency. + * + * @param refresher Refresher. + */ + async refreshCompetency(refresher: IonRefresher): Promise { + try { + if (this.planId) { + await AddonCompetency.invalidateCompetencyInPlan(this.planId, this.competencyId); + } else { + await AddonCompetency.invalidateCompetencyInCourse(this.courseId!, this.competencyId); + } + + } finally { + this.fetchCompetency().finally(() => { + refresher?.complete(); + }); + } + } + + /** + * Opens the summary of a competency. + * + * @param competencyId + */ + openCompetencySummary(competencyId: number): void { + CoreNavigator.navigateToSitePath( + '/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/summary/' + competencyId, + { + params: { contextLevel: this.contextLevel, contextInstanceId: this.contextInstanceId }, + }, + ); + } + +} diff --git a/src/addons/competency/pages/competencysummary/competencysummary.html b/src/addons/competency/pages/competencysummary/competencysummary.html new file mode 100644 index 000000000..df05f5428 --- /dev/null +++ b/src/addons/competency/pages/competencysummary/competencysummary.html @@ -0,0 +1,35 @@ + + + + + + + {{ competency.competency.shortname }} {{ competency.competency.idnumber }} + + + + + + + + + + + + + + + + + + {{ 'addon.competency.path' | translate }} + {{ competency.comppath.framework.name }} + +  / {{ ancestor.name }} + + + + + + diff --git a/src/addons/competency/pages/competencysummary/competencysummary.ts b/src/addons/competency/pages/competencysummary/competencysummary.ts new file mode 100644 index 000000000..451d85553 --- /dev/null +++ b/src/addons/competency/pages/competencysummary/competencysummary.ts @@ -0,0 +1,104 @@ +// (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 { ContextLevel } from '@/core/constants'; +import { AddonCompetencySummary, AddonCompetency } from '@addons/competency/services/competency'; +import { IonRefresher } from '@ionic/angular'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { AddonCompetencyMainMenuHandlerService } from '@addons/competency/services/handlers/mainmenu'; + +/** + * Page that displays the competency summary. + */ +@Component({ + selector: 'page-addon-competency-competency-summary', + templateUrl: 'competencysummary.html', +}) +export class AddonCompetencyCompetencySummaryPage implements OnInit { + + competencyLoaded = false; + competencyId!: number; + competency?: AddonCompetencySummary; + contextLevel?: ContextLevel; + contextInstanceId?: number; + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + this.competencyId = CoreNavigator.getRouteNumberParam('competencyId')!; + this.contextLevel = CoreNavigator.getRouteParam('contextLevel'); + this.contextInstanceId = CoreNavigator.getRouteNumberParam('contextInstanceId'); + + try { + await this.fetchCompetency(); + const name = this.competency!.competency && this.competency!.competency.shortname; + + CoreUtils.ignoreErrors(AddonCompetency.logCompetencyView(this.competencyId, name)); + } finally { + this.competencyLoaded = true; + } + } + + /** + * Fetches the competency summary and updates the view. + * + * @return Promise resolved when done. + */ + protected async fetchCompetency(): Promise { + try { + const result = await AddonCompetency.getCompetencySummary(this.competencyId); + if (!this.contextLevel || typeof this.contextInstanceId == 'undefined') { + // Context not specified, use user context. + this.contextLevel = ContextLevel.USER; + this.contextInstanceId = result.usercompetency!.userid; + } + + this.competency = result.competency; + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error getting competency summary data.'); + } + } + + /** + * Refreshes the competency summary. + * + * @param refresher Refresher. + */ + refreshCompetency(refresher: IonRefresher): void { + AddonCompetency.invalidateCompetencySummary(this.competencyId).finally(() => { + this.fetchCompetency().finally(() => { + refresher?.complete(); + }); + }); + } + + /** + * Opens the summary of a competency. + * + * @param competencyId + */ + openCompetencySummary(competencyId: number): void { + CoreNavigator.navigateToSitePath( + '/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/summary/' + competencyId, + { + params: { contextLevel: this.contextLevel, contextInstanceId: this.contextInstanceId }, + }, + ); + } + +} diff --git a/src/addons/competency/pages/coursecompetencies/coursecompetencies.html b/src/addons/competency/pages/coursecompetencies/coursecompetencies.html new file mode 100644 index 000000000..244afad0f --- /dev/null +++ b/src/addons/competency/pages/coursecompetencies/coursecompetencies.html @@ -0,0 +1,135 @@ + + + + + + {{ 'addon.competency.coursecompetencies' | translate }} + + + + + + + + + + + {{ 'addon.competency.coursecompetencyratingsarepushedtouserplans' | translate }} + + + {{ 'addon.competency.coursecompetencyratingsarenotpushedtouserplans' | translate }} + + + + + {{ 'addon.competency.xcompetenciesproficientoutofyincourse' | translate: {$a: + {x: competencies.statistics.proficientcompetencycount, y: competencies.statistics.competencycount} } }} + + + + + + {{ 'addon.competency.competenciesmostoftennotproficientincourse' | translate }}: +

+ + {{ comp.shortname }} - {{ comp.idnumber }} + +

+
+
+
+ +

+ {{ 'addon.competency.coursecompetencies' | translate }} +

+ + + +

{{ user.fullname }}

+
+
+ + + +
+ + + +

{{competency.competency.shortname}} {{competency.competency.idnumber}}

+
+ + {{ competency.usercompetencycourse.gradename }} + +
+ + +

+ + +

+
+ {{ 'addon.competency.path' | translate }} + + {{ competency.comppath.framework.name }} + + + {{ competency.comppath.framework.name }} + +  /  + + + {{ ancestor.name }} + + {{ ancestor.name }} +  /  + +
+
+ {{ 'addon.competency.uponcoursecompletion' | translate }} + + {{ ruleoutcome.text }} + +
+
+ {{ 'addon.competency.activities' | translate }} +

+ {{ 'addon.competency.noactivities' | translate }} +

+ + + + + + + +
+
+ {{ 'addon.competency.userplans' | translate }} +

+ {{ 'addon.competency.nouserplanswithcompetency' | translate }} +

+ + + + + + +
+
+
+
+
+
+
diff --git a/src/addons/competency/pages/coursecompetencies/coursecompetencies.module.ts b/src/addons/competency/pages/coursecompetencies/coursecompetencies.module.ts new file mode 100644 index 000000000..e4101d0e7 --- /dev/null +++ b/src/addons/competency/pages/coursecompetencies/coursecompetencies.module.ts @@ -0,0 +1,28 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { AddonCompetencyCourseCompetenciesPage } from './coursecompetencies.page'; + +@NgModule({ + imports: [ + CoreSharedModule, + ], + declarations: [ + AddonCompetencyCourseCompetenciesPage, + ], +}) +export class AddonCompetencyCourseCompetenciesPageModule {} diff --git a/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts b/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts new file mode 100644 index 000000000..58c91bd56 --- /dev/null +++ b/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts @@ -0,0 +1,109 @@ +// (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 { AddonCompetencyDataForCourseCompetenciesPageWSResponse, AddonCompetency } from '@addons/competency/services/competency'; +import { AddonCompetencyHelper } from '@addons/competency/services/competency-helper'; +import { CoreUserProfile } from '@features/user/services/user'; +import { IonRefresher } from '@ionic/angular'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { AddonCompetencyMainMenuHandlerService } from '@addons/competency/services/handlers/mainmenu'; +import { ContextLevel } from '@/core/constants'; + +/** + * Page that displays the list of competencies of a course. + */ +@Component({ + selector: 'page-addon-competency-coursecompetencies', + templateUrl: 'coursecompetencies.html', +}) +export class AddonCompetencyCourseCompetenciesPage implements OnInit { + + competenciesLoaded = false; + competencies?: AddonCompetencyDataForCourseCompetenciesPageWSResponse; + user?: CoreUserProfile; + courseId!: number; + + protected userId!: number; + + /** + * View loaded. + */ + ngOnInit(): void { + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.userId = CoreNavigator.getRouteNumberParam('userId')!; + + this.fetchCourseCompetencies().finally(() => { + this.competenciesLoaded = true; + }); + } + + /** + * Fetches the competencies and updates the view. + * + * @return Promise resolved when done. + */ + protected async fetchCourseCompetencies(): Promise { + try { + this.competencies = await AddonCompetency.getCourseCompetencies(this.courseId, this.userId); + + // Get the user profile image. + this.user = await AddonCompetencyHelper.getProfile(this.userId); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error getting course competencies data.'); + } + } + + /** + * Opens a competency. + * + * @param competencyId + */ + openCompetency(competencyId: number): void { + CoreNavigator.navigateToSitePath( + '/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/competencies/' + competencyId, + { + params: { courseId: this.courseId, userId: this.userId }, + }, + ); + } + + /** + * Opens the summary of a competency. + * + * @param competencyId + */ + openCompetencySummary(competencyId: number): void { + CoreNavigator.navigateToSitePath('/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/summary/' + competencyId, { + params: { + contextLevel: ContextLevel.COURSE, + contextInstanceId: this.courseId, + } }); + } + + /** + * Refreshes the competencies. + * + * @param refresher Refresher. + */ + refreshCourseCompetencies(refresher?: IonRefresher): void { + AddonCompetency.invalidateCourseCompetencies(this.courseId, this.userId).finally(() => { + this.fetchCourseCompetencies().finally(() => { + refresher?.complete(); + }); + }); + } + +} diff --git a/src/addons/competency/pages/plan/plan.html b/src/addons/competency/pages/plan/plan.html new file mode 100644 index 000000000..9b207a7f6 --- /dev/null +++ b/src/addons/competency/pages/plan/plan.html @@ -0,0 +1,92 @@ + + + + + + {{plan.plan.name}} + + + + + + + + + + + +

{{ user.fullname }}

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

+ {{ 'addon.competency.status' | translate }}: {{ plan.plan.statusname }} +

+
+
+ + +

+ {{ 'addon.competency.duedate' | translate }}: + {{ plan.plan.duedate * 1000 | coreFormatDate }} +

+
+
+ + +

+ {{ 'addon.competency.template' | translate }}: {{ plan.plan.template.shortname }} +

+
+
+ + +

+ {{ 'addon.competency.progress' | translate }}: + {{ 'addon.competency.xcompetenciesproficientoutofy' | translate: + {$a: {x: plan.proficientcompetencycount, y: plan.competencycount} } }} +

+ +
+
+
+
+ + +

{{ 'addon.competency.learningplancompetencies' | translate }}

+
+ + + +

{{ 'addon.competency.nocompetencies' | translate }}

+
+
+ +

{{competency.competency.shortname}} {{competency.competency.idnumber}}

+ + {{ competency.usercompetencyplan.gradename }} + + + {{ competency.usercompetency.gradename }} + +
+
+
+
+
diff --git a/src/addons/competency/pages/plan/plan.ts b/src/addons/competency/pages/plan/plan.ts new file mode 100644 index 000000000..71fedb68a --- /dev/null +++ b/src/addons/competency/pages/plan/plan.ts @@ -0,0 +1,93 @@ +// (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 { AddonCompetencyDataForPlanPageWSResponse, AddonCompetency } from '../../services/competency'; +import { AddonCompetencyHelper } from '../../services/competency-helper'; +import { CoreNavigator } from '@services/navigator'; +import { CoreUserProfile } from '@features/user/services/user'; +import { IonRefresher } from '@ionic/angular'; +import { AddonCompetencyMainMenuHandlerService } from '@addons/competency/services/handlers/mainmenu'; + +/** + * Page that displays a learning plan. + */ +@Component({ + selector: 'page-addon-competency-plan', + templateUrl: 'plan.html', +}) +export class AddonCompetencyPlanPage implements OnInit { + + protected planId!: number; + loaded = false; + plan?: AddonCompetencyDataForPlanPageWSResponse; + user?: CoreUserProfile; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.planId = CoreNavigator.getRouteNumberParam('planId')!; + + this.fetchLearningPlan().finally(() => { + this.loaded = true; + }); + } + + /** + * Fetches the learning plan and updates the view. + * + * @return Promise resolved when done. + */ + protected async fetchLearningPlan(): Promise { + try { + const plan = await AddonCompetency.getLearningPlan(this.planId); + plan.plan.statusname = AddonCompetencyHelper.getPlanStatusName(plan.plan.status); + + // Get the user profile image. + this.user = await AddonCompetencyHelper.getProfile(plan.plan.userid); + + this.plan = plan; + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error getting learning plan data.'); + } + } + + /** + * Navigates to a particular competency. + * + * @param competencyId + */ + openCompetency(competencyId: number): void { + CoreNavigator.navigateToSitePath( + '/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/competencies/' + competencyId, + { params: { planId: this.planId } }, + ); + } + + /** + * Refreshes the learning plan. + * + * @param refresher Refresher. + */ + refreshLearningPlan(refresher: IonRefresher): void { + AddonCompetency.invalidateLearningPlan(this.planId).finally(() => { + this.fetchLearningPlan().finally(() => { + refresher?.complete(); + }); + }); + } + +} diff --git a/src/addons/competency/pages/planlist/planlist.html b/src/addons/competency/pages/planlist/planlist.html new file mode 100644 index 000000000..96c6b3914 --- /dev/null +++ b/src/addons/competency/pages/planlist/planlist.html @@ -0,0 +1,33 @@ + + + + + + {{ 'addon.competency.userplans' | translate }} + + + + + + + + + + + + + + +

{{ plan.name }}

+

+ {{ 'addon.competency.duedate' | translate }}:  + {{ plan.duedate * 1000 | coreFormatDate :'strftimedatetimeshort' }} +

+
+ {{ plan.statusname }} +
+
+
+
+
diff --git a/src/addons/competency/pages/planlist/planlist.ts b/src/addons/competency/pages/planlist/planlist.ts new file mode 100644 index 000000000..23e3741ae --- /dev/null +++ b/src/addons/competency/pages/planlist/planlist.ts @@ -0,0 +1,140 @@ +// (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 { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { IonRefresher } from '@ionic/angular'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { AddonCompetencyProvider, AddonCompetencyPlan, AddonCompetency } from '../../services/competency'; +import { AddonCompetencyHelper } from '../../services/competency-helper'; +import { CoreNavigator } from '@services/navigator'; +import { CorePageItemsListManager } from '@classes/page-items-list-manager'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** + * Page that displays the list of learning plans. + */ +@Component({ + selector: 'page-addon-competency-planlist', + templateUrl: 'planlist.html', +}) +export class AddonCompetencyPlanListPage implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; + + protected userId?: number; + plans: AddonCompetencyPlanListManager; + + constructor() { + this.plans = new AddonCompetencyPlanListManager(AddonCompetencyPlanListPage); + } + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.userId = CoreNavigator.getRouteNumberParam('userId'); + } + + /** + * @inheritdoc + */ + async ngAfterViewInit(): Promise { + await this.fetchLearningPlans(); + + this.plans.start(this.splitView); + } + + /** + * Fetches the learning plans and updates the view. + * + * @return Promise resolved when done. + */ + protected async fetchLearningPlans(): Promise { + try { + const plans = await AddonCompetency.getLearningPlans(this.userId); + plans.forEach((plan: AddonCompetencyPlanFormatted) => { + plan.statusname = AddonCompetencyHelper.getPlanStatusName(plan.status); + switch (plan.status) { + case AddonCompetencyProvider.STATUS_ACTIVE: + plan.statuscolor = 'success'; + break; + case AddonCompetencyProvider.STATUS_COMPLETE: + plan.statuscolor = 'danger'; + break; + default: + plan.statuscolor = 'warning'; + break; + } + }); + this.plans.setItems(plans); + + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error getting learning plans data.'); + } + } + + /** + * Refreshes the learning plans. + * + * @param refresher Refresher. + */ + refreshLearningPlans(refresher: IonRefresher): void { + AddonCompetency.invalidateLearningPlans(this.userId).finally(() => { + this.fetchLearningPlans().finally(() => { + refresher?.complete(); + }); + }); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.plans.destroy(); + } + +} + +/** + * Competency plan with some calculated data. + */ +type AddonCompetencyPlanFormatted = AddonCompetencyPlan & { + statuscolor?: string; // Calculated in the app. Color of the plan's status. +}; + +/** + * Helper class to manage plan list. + */ +class AddonCompetencyPlanListManager extends CorePageItemsListManager { + + constructor(pageComponent: unknown) { + super(pageComponent); + } + + /** + * @inheritdoc + */ + protected getItemPath(plan: AddonCompetencyPlanFormatted): string { + return String(plan.id); + } + + /** + * @inheritdoc + */ + protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null { + return route.params.planId ?? null; + } + +} diff --git a/src/addons/competency/services/competency-helper.ts b/src/addons/competency/services/competency-helper.ts new file mode 100644 index 000000000..9a9d44b79 --- /dev/null +++ b/src/addons/competency/services/competency-helper.ts @@ -0,0 +1,99 @@ +// (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 { AddonCompetencyProvider } from './competency'; +import { CoreUser, CoreUserProfile } from '@features/user/services/user'; +import { makeSingleton, Translate } from '@singletons'; + +/** + * Service that provides some features regarding learning plans. + */ +@Injectable( { providedIn: 'root' }) +export class AddonCompetencyHelperProvider { + + /** + * Convenient helper to get the user profile image. + * + * @param userId User Id + * @return User profile Image URL or true if default icon. + */ + async getProfile(userId?: number): Promise { + if (!userId || userId == CoreSites.getCurrentSiteUserId()) { + return; + } + + // Get the user profile to retrieve the user image. + return CoreUser.getProfile(userId, undefined, true); + } + + /** + * Get the review status name translated. + * + * @param status + */ + getCompetencyStatusName(status: number): string { + let statusTranslateName: string; + switch (status) { + case AddonCompetencyProvider.REVIEW_STATUS_IDLE: + statusTranslateName = 'idle'; + break; + case AddonCompetencyProvider.REVIEW_STATUS_IN_REVIEW: + statusTranslateName = 'inreview'; + break; + case AddonCompetencyProvider.REVIEW_STATUS_WAITING_FOR_REVIEW: + statusTranslateName = 'waitingforreview'; + break; + default: + // We can use the current status name. + return String(status); + } + + return Translate.instant('addon.competency.usercompetencystatus_' + statusTranslateName); + } + + /** + * Get the status name translated. + * + * @param status + */ + getPlanStatusName(status: number): string { + let statusTranslateName: string; + switch (status) { + case AddonCompetencyProvider.STATUS_DRAFT: + statusTranslateName = 'draft'; + break; + case AddonCompetencyProvider.STATUS_ACTIVE: + statusTranslateName = 'active'; + break; + case AddonCompetencyProvider.STATUS_COMPLETE: + statusTranslateName = 'complete'; + break; + case AddonCompetencyProvider.STATUS_WAITING_FOR_REVIEW: + statusTranslateName = 'waitingforreview'; + break; + case AddonCompetencyProvider.STATUS_IN_REVIEW: + statusTranslateName = 'inreview'; + break; + default: + // We can use the current status name. + return String(status); + } + + return Translate.instant('addon.competency.planstatus' + statusTranslateName); + } + +} +export const AddonCompetencyHelper = makeSingleton(AddonCompetencyHelperProvider); diff --git a/src/addons/competency/services/competency.ts b/src/addons/competency/services/competency.ts new file mode 100644 index 000000000..802c7f925 --- /dev/null +++ b/src/addons/competency/services/competency.ts @@ -0,0 +1,1062 @@ +// (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 { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreCommentsArea } from '@features/comments/services/comments'; +import { CoreCourseSummary, CoreCourseModuleSummary } from '@features/course/services/course'; +import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; +import { CoreUserSummary } from '@features/user/services/user'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton } from '@singletons'; + +const ROOT_CACHE_KEY = 'mmaCompetency:'; + +/** + * Service to handle caompetency learning plans. + */ +@Injectable( { providedIn: 'root' }) +export class AddonCompetencyProvider { + + // Learning plan status. + static readonly STATUS_DRAFT = 0; + static readonly STATUS_ACTIVE = 1; + static readonly STATUS_COMPLETE = 2; + static readonly STATUS_WAITING_FOR_REVIEW = 3; + static readonly STATUS_IN_REVIEW = 4; + + // Competency status. + static readonly REVIEW_STATUS_IDLE = 0; + static readonly REVIEW_STATUS_WAITING_FOR_REVIEW = 1; + static readonly REVIEW_STATUS_IN_REVIEW = 2; + + /** + * Check if all competencies features are disabled. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: whether all competency features are disabled. + */ + async allCompetenciesDisabled(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.isFeatureDisabled('CoreMainMenuDelegate_AddonCompetency') && + site.isFeatureDisabled('CoreCourseOptionsDelegate_AddonCompetency') && + site.isFeatureDisabled('CoreUserDelegate_AddonCompetency'); + } + + /** + * Returns whether current user can see another user competencies in a course. + * + * @param courseId Course ID. + * @param userId User ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: whether the user can view the competencies. + */ + async canViewUserCompetenciesInCourse(courseId: number, userId?: number, siteId?: string): Promise { + if (!CoreSites.isLoggedIn()) { + return false; + } + + try { + const response = await this.getCourseCompetenciesPage(courseId, siteId); + + if (!response.competencies.length) { + // No competencies. + return false; + } + + if (!userId || userId == CoreSites.getCurrentSiteUserId()) { + // Current user. + return true; + } + + // Check if current user can view any competency of the user. + await this.getCompetencyInCourse(courseId, response.competencies[0].competency.id, userId, siteId); + + return true; + } catch { + return false; + } + } + + /** + * Get cache key for user learning plans data WS calls. + * + * @param userId User ID. + * @return Cache key. + */ + protected getLearningPlansCacheKey(userId: number): string { + return ROOT_CACHE_KEY + 'userplans:' + userId; + } + + /** + * Get cache key for learning plan data WS calls. + * + * @param planId Plan ID. + * @return Cache key. + */ + protected getLearningPlanCacheKey(planId: number): string { + return ROOT_CACHE_KEY + 'learningplan:' + planId; + } + + /** + * Get cache key for competency in plan data WS calls. + * + * @param planId Plan ID. + * @param competencyId Competency ID. + * @return Cache key. + */ + protected getCompetencyInPlanCacheKey(planId: number, competencyId: number): string { + return ROOT_CACHE_KEY + 'plancompetency:' + planId + ':' + competencyId; + } + + /** + * Get cache key for competency in course data WS calls. + * + * @param courseId Course ID. + * @param competencyId Competency ID. + * @param userId User ID. + * @return Cache key. + */ + protected getCompetencyInCourseCacheKey(courseId: number, competencyId: number, userId: number): string { + return ROOT_CACHE_KEY + 'coursecompetency:' + userId + ':' + courseId + ':' + competencyId; + } + + /** + * Get cache key for competency summary data WS calls. + * + * @param competencyId Competency ID. + * @param userId User ID. + * @return Cache key. + */ + protected getCompetencySummaryCacheKey(competencyId: number, userId: number): string { + return ROOT_CACHE_KEY + 'competencysummary:' + userId + ':' + competencyId; + } + + /** + * Get cache key for course competencies data WS calls. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getCourseCompetenciesCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'coursecompetencies:' + courseId; + } + + /** + * Returns whether competencies are enabled. + * + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return competencies if enabled for the given course, false otherwise. + */ + async isPluginForCourseEnabled(courseId: number, siteId?: string): Promise { + if (!CoreSites.isLoggedIn()) { + return false; + } + + return CoreUtils.promiseWorks(this.getCourseCompetencies(courseId, undefined, siteId)); + } + + /** + * Get plans for a certain user. + * + * @param userId ID of the user. If not defined, current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise to be resolved when the plans are retrieved. + */ + async getLearningPlans(userId?: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + userId = userId || site.getUserId(); + + const params: AddonCompetencyDataForPlansPageWSParams = { + userid: userId, + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getLearningPlansCacheKey(userId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + + const response = await site.read('tool_lp_data_for_plans_page', params, preSets); + + return response.plans; + } + + /** + * Get a certain plan. + * + * @param planId ID of the plan. + * @param siteId Site ID. If not defined, current site. + * @return Promise to be resolved when the plan is retrieved. + */ + async getLearningPlan(planId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonCompetencyDataForPlanPageWSParams = { + planid: planId, + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getLearningPlanCacheKey(planId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + + return site.read('tool_lp_data_for_plan_page', params, preSets); + } + + /** + * Get a certain competency in a plan. + * + * @param planId ID of the plan. + * @param competencyId ID of the competency. + * @param siteId Site ID. If not defined, current site. + * @return Promise to be resolved when the competency is retrieved. + */ + async getCompetencyInPlan( + planId: number, + competencyId: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonCompetencyDataForUserCompetencySummaryInPlanWSParams = { + planid: planId, + competencyid: competencyId, + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCompetencyInPlanCacheKey(planId, competencyId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + return site.read( + 'tool_lp_data_for_user_competency_summary_in_plan', + params, + preSets, + ); + } + + /** + * Get a certain competency in a course. + * + * @param courseId ID of the course. + * @param competencyId ID of the competency. + * @param userId ID of the user. If not defined, current user. + * @param siteId Site ID. If not defined, current site. + * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @return Promise to be resolved when the competency is retrieved. + */ + async getCompetencyInCourse( + courseId: number, + competencyId: number, + userId?: number, + siteId?: string, + ignoreCache = false, + ): Promise { + const site = await CoreSites.getSite(siteId); + userId = userId || site.getUserId(); + + const params: AddonCompetencyDataForUserCompetencySummaryInCourseWSParams = { + courseid: courseId, + competencyid: competencyId, + userid: userId, + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCompetencyInCourseCacheKey(courseId, competencyId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('tool_lp_data_for_user_competency_summary_in_course', params, preSets); + } + + /** + * Get a certain competency summary. + * + * @param competencyId ID of the competency. + * @param userId ID of the user. If not defined, current user. + * @param siteId Site ID. If not defined, current site. + * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @return Promise to be resolved when the competency summary is retrieved. + */ + async getCompetencySummary( + competencyId: number, + userId?: number, + siteId?: string, + ignoreCache = false, + ): Promise { + const site = await CoreSites.getSite(siteId); + userId = userId || site.getUserId(); + + const params: AddonCompetencyDataForUserCompetencySummaryWSParams = { + competencyid: competencyId, + userid: userId, + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCompetencySummaryCacheKey(competencyId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('tool_lp_data_for_user_competency_summary', params, preSets); + } + + /** + * Get all competencies in a course for a certain user. + * + * @param courseId ID of the course. + * @param userId ID of the user. + * @param siteId Site ID. If not defined, current site. + * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @return Promise to be resolved when the course competencies are retrieved. + */ + async getCourseCompetencies( + courseId: number, + userId?: number, + siteId?: string, + ignoreCache = false, + ): Promise { + + const courseCompetencies = await this.getCourseCompetenciesPage(courseId, siteId, ignoreCache); + + if (!userId || userId == CoreSites.getCurrentSiteUserId()) { + return courseCompetencies; + } + + const userCompetenciesSumaries: AddonCompetencyDataForUserCompetencySummaryInCourseWSResponse[] = + await Promise.all(courseCompetencies.competencies.map((competency) => + this.getCompetencyInCourse(courseId, competency.competency.id, userId, siteId))); + + userCompetenciesSumaries.forEach((userCompetenciesSumary, index) => { + courseCompetencies.competencies[index].usercompetencycourse = + userCompetenciesSumary.usercompetencysummary.usercompetencycourse; + }); + + return courseCompetencies; + } + + /** + * Get all competencies in a course. + * + * @param courseId ID of the course. + * @param siteId Site ID. If not defined, current site. + * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @return Promise to be resolved when the course competencies are retrieved. + */ + async getCourseCompetenciesPage( + courseId: number, + siteId?: string, + ignoreCache = false, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonCompetencyDataForCourseCompetenciesPageWSParams = { + courseid: courseId, + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCourseCompetenciesCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read( + 'tool_lp_data_for_course_competencies_page', + params, + preSets, + ); + } + + /** + * Invalidates User Learning Plans data. + * + * @param userId ID of the user. If not defined, current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateLearningPlans(userId?: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + userId = userId || site.getUserId(); + + await site.invalidateWsCacheForKey(this.getLearningPlansCacheKey(userId)); + } + + /** + * Invalidates Learning Plan data. + * + * @param planId ID of the plan. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateLearningPlan(planId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getLearningPlanCacheKey(planId)); + } + + /** + * Invalidates Competency in Plan data. + * + * @param planId ID of the plan. + * @param competencyId ID of the competency. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateCompetencyInPlan(planId: number, competencyId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getCompetencyInPlanCacheKey(planId, competencyId)); + } + + /** + * Invalidates Competency in Course data. + * + * @param courseId ID of the course. + * @param competencyId ID of the competency. + * @param userId ID of the user. If not defined, current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateCompetencyInCourse(courseId: number, competencyId: number, userId?: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + userId = userId || site.getUserId(); + + await site.invalidateWsCacheForKey(this.getCompetencyInCourseCacheKey(courseId, competencyId, userId)); + } + + /** + * Invalidates Competency Summary data. + * + * @param competencyId ID of the competency. + * @param userId ID of the user. If not defined, current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateCompetencySummary(competencyId: number, userId?: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + userId = userId || site.getUserId(); + + await site.invalidateWsCacheForKey(this.getCompetencySummaryCacheKey(competencyId, userId)); + } + + /** + * Invalidates Course Competencies data. + * + * @param courseId ID of the course. + * @param userId ID of the user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateCourseCompetencies(courseId: number, userId?: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + await site.invalidateWsCacheForKey(this.getCourseCompetenciesCacheKey(courseId)); + + if (!userId || userId == CoreSites.getCurrentSiteUserId()) { + return; + } + + const competencies = await this.getCourseCompetencies(courseId, 0, siteId); + const promises = competencies.competencies.map((competency) => + this.invalidateCompetencyInCourse(courseId, competency.competency.id, userId, siteId)); + + await Promise.all(promises); + } + + /** + * Report the competency as being viewed in plan. + * + * @param planId ID of the plan. + * @param competencyId ID of the competency. + * @param planStatus Current plan Status to decide what action should be logged. + * @param name Name of the competency. + * @param userId User ID. If not defined, current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + async logCompetencyInPlanView( + planId: number, + competencyId: number, + planStatus: number, + name?: string, + userId?: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + userId = userId || site.getUserId(); + + const params: AddonCompetencyUserCompetencyPlanViewedWSParams = { + planid: planId, + competencyid: competencyId, + userid: userId, + }; + + const preSets: CoreSiteWSPreSets = { + typeExpected: 'boolean', + }; + + const wsName = planStatus == AddonCompetencyProvider.STATUS_COMPLETE + ? 'core_competency_user_competency_plan_viewed' + : 'core_competency_user_competency_viewed_in_plan'; + + CorePushNotifications.logViewEvent(competencyId, name, 'competency', wsName, { + planid: planId, + planstatus: planStatus, + userid: userId, + }, siteId); + + await site.write(wsName, params, preSets); + } + + /** + * Report the competency as being viewed in course. + * + * @param courseId ID of the course. + * @param competencyId ID of the competency. + * @param name Name of the competency. + * @param userId User ID. If not defined, current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + async logCompetencyInCourseView( + courseId: number, + competencyId: number, + name?: string, + userId?: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + userId = userId || site.getUserId(); + + const params: AddonCompetencyUserCompetencyViewedInCourseWSParams = { + courseid: courseId, + competencyid: competencyId, + userid: userId, + }; + + const preSets: CoreSiteWSPreSets = { + typeExpected: 'boolean', + }; + + const wsName = 'core_competency_user_competency_viewed_in_course'; + + CorePushNotifications.logViewEvent(competencyId, name, 'competency', 'wsName', { + courseid: courseId, + userid: userId, + }, siteId); + + await site.write(wsName, params, preSets); + } + + /** + * Report the competency as being viewed. + * + * @param competencyId ID of the competency. + * @param name Name of the competency. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + async logCompetencyView(competencyId: number, name?: string, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonCompetencyCompetencyViewedWSParams = { + id: competencyId, + }; + + const preSets: CoreSiteWSPreSets = { + typeExpected: 'boolean', + }; + + const wsName = 'core_competency_competency_viewed'; + CorePushNotifications.logViewEvent(competencyId, name, 'competency', wsName, {}, siteId); + + await site.write(wsName, params, preSets); + } + +} +export const AddonCompetency = makeSingleton(AddonCompetencyProvider); + +/** + * Data returned by competency's plan_exporter. + */ +export type AddonCompetencyPlan = { + name: string; // Name. + description: string; // Description. + descriptionformat?: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + userid: number; // Userid. + templateid: number; // Templateid. + origtemplateid: number; // Origtemplateid. + status: number; // Status. + duedate: number; // Duedate. + reviewerid: number; // Reviewerid. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + statusname: string; // Statusname. + isbasedontemplate: boolean; // Isbasedontemplate. + canmanage: boolean; // Canmanage. + canrequestreview: boolean; // Canrequestreview. + canreview: boolean; // Canreview. + canbeedited: boolean; // Canbeedited. + isactive: boolean; // Isactive. + isdraft: boolean; // Isdraft. + iscompleted: boolean; // Iscompleted. + isinreview: boolean; // Isinreview. + iswaitingforreview: boolean; // Iswaitingforreview. + isreopenallowed: boolean; // Isreopenallowed. + iscompleteallowed: boolean; // Iscompleteallowed. + isunlinkallowed: boolean; // Isunlinkallowed. + isrequestreviewallowed: boolean; // Isrequestreviewallowed. + iscancelreviewrequestallowed: boolean; // Iscancelreviewrequestallowed. + isstartreviewallowed: boolean; // Isstartreviewallowed. + isstopreviewallowed: boolean; // Isstopreviewallowed. + isapproveallowed: boolean; // Isapproveallowed. + isunapproveallowed: boolean; // Isunapproveallowed. + duedateformatted: string; // Duedateformatted. + commentarea: CoreCommentsArea; + reviewer?: CoreUserSummary; + template?: AddonCompetencyTemplate; + url: string; // Url. +}; + +/** + * Data returned by competency's template_exporter. + */ +export type AddonCompetencyTemplate = { + shortname: string; // Shortname. + description: string; // Description. + descriptionformat?: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + duedate: number; // Duedate. + visible: boolean; // Visible. + contextid: number; // Contextid. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + duedateformatted: string; // Duedateformatted. + cohortscount: number; // Cohortscount. + planscount: number; // Planscount. + canmanage: boolean; // Canmanage. + canread: boolean; // Canread. + contextname: string; // Contextname. + contextnamenoprefix: string; // Contextnamenoprefix. +}; + +/** + * Data returned by competency's competency_exporter. + */ +export type AddonCompetencyCompetency = { + shortname: string; // Shortname. + idnumber: string; // Idnumber. + description: string; // Description. + descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + sortorder: number; // Sortorder. + parentid: number; // Parentid. + path: string; // Path. + ruleoutcome: number; // Ruleoutcome. + ruletype: string; // Ruletype. + ruleconfig: string; // Ruleconfig. + scaleid: number; // Scaleid. + scaleconfiguration: string; // Scaleconfiguration. + competencyframeworkid: number; // Competencyframeworkid. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. +}; + +/** + * Data returned by competency's competency_path_exporter. + */ +export type AddonCompetencyPath = { + ancestors: AddonCompetencyPathNode[]; // Ancestors. + framework: AddonCompetencyPathNode; + pluginbaseurl: string; // Pluginbaseurl. + pagecontextid: number; // Pagecontextid. + showlinks: boolean; // @since 3.7. Showlinks. +}; + +/** + * Data returned by competency's path_node_exporter. + */ +export type AddonCompetencyPathNode = { + id: number; // Id. + name: string; // Name. + first: boolean; // First. + last: boolean; // Last. + position: number; // Position. +}; + +/** + * Data returned by competency's user_competency_exporter. + */ +export type AddonCompetencyUserCompetency = { + userid: number; // Userid. + competencyid: number; // Competencyid. + status: number; // Status. + reviewerid: number; // Reviewerid. + proficiency: boolean; // Proficiency. + grade: number; // Grade. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + canrequestreview: boolean; // Canrequestreview. + canreview: boolean; // Canreview. + gradename: string; // Gradename. + isrequestreviewallowed: boolean; // Isrequestreviewallowed. + iscancelreviewrequestallowed: boolean; // Iscancelreviewrequestallowed. + isstartreviewallowed: boolean; // Isstartreviewallowed. + isstopreviewallowed: boolean; // Isstopreviewallowed. + isstatusidle: boolean; // Isstatusidle. + isstatusinreview: boolean; // Isstatusinreview. + isstatuswaitingforreview: boolean; // Isstatuswaitingforreview. + proficiencyname: string; // Proficiencyname. + reviewer?: CoreUserSummary; + statusname: string; // Statusname. + url: string; // Url. +}; + +/** + * Data returned by competency's user_competency_plan_exporter. + */ +export type AddonCompetencyUserCompetencyPlan = { + userid: number; // Userid. + competencyid: number; // Competencyid. + proficiency: boolean; // Proficiency. + grade: number; // Grade. + planid: number; // Planid. + sortorder: number; // Sortorder. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + gradename: string; // Gradename. + proficiencyname: string; // Proficiencyname. +}; + +/** + * Params of tool_lp_data_for_user_competency_summary_in_plan WS. + */ +type AddonCompetencyDataForUserCompetencySummaryInPlanWSParams = { + competencyid: number; // Data base record id for the competency. + planid: number; // Data base record id for the plan. +}; + +/** + * Data returned by competency's user_competency_summary_in_plan_exporter. + */ +export type AddonCompetencyDataForUserCompetencySummaryInPlanWSResponse = { + usercompetencysummary: AddonCompetencyDataForUserCompetencySummaryWSResponse; + plan: AddonCompetencyPlan; +}; + +/** + * Params of tool_lp_data_for_user_competency_summary WS. + */ +type AddonCompetencyDataForUserCompetencySummaryWSParams = { + userid: number; // Data base record id for the user. + competencyid: number; // Data base record id for the competency. +}; + +/** + * Data returned by competency's user_competency_summary_exporter. + */ +export type AddonCompetencyDataForUserCompetencySummaryWSResponse = { + showrelatedcompetencies: boolean; // Showrelatedcompetencies. + cangrade: boolean; // Cangrade. + competency: AddonCompetencySummary; + user: CoreUserSummary; + usercompetency?: AddonCompetencyUserCompetency; + usercompetencyplan?: AddonCompetencyUserCompetencyPlan; + usercompetencycourse?: AddonCompetencyUserCompetencyCourse; + evidence: AddonCompetencyEvidence[]; // Evidence. + commentarea?: CoreCommentsArea; +}; + +/** + * Data returned by competency's competency_summary_exporter. + */ +export type AddonCompetencySummary = { + linkedcourses: CoreCourseSummary; // Linkedcourses. + relatedcompetencies: AddonCompetencyCompetency[]; // Relatedcompetencies. + competency: AddonCompetencyCompetency; + framework: AddonCompetencyFramework; + hascourses: boolean; // Hascourses. + hasrelatedcompetencies: boolean; // Hasrelatedcompetencies. + scaleid: number; // Scaleid. + scaleconfiguration: string; // Scaleconfiguration. + taxonomyterm: string; // Taxonomyterm. + comppath: AddonCompetencyPath; + pluginbaseurl: string; // @since 3.7. Pluginbaseurl. +}; + +/** + * Data returned by competency's competency_framework_exporter. + */ +export type AddonCompetencyFramework = { + shortname: string; // Shortname. + idnumber: string; // Idnumber. + description: string; // Description. + descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + visible: boolean; // Visible. + scaleid: number; // Scaleid. + scaleconfiguration: string; // Scaleconfiguration. + contextid: number; // Contextid. + taxonomies: string; // Taxonomies. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + canmanage: boolean; // Canmanage. + competenciescount: number; // Competenciescount. + contextname: string; // Contextname. + contextnamenoprefix: string; // Contextnamenoprefix. +}; + +/** + * Data returned by competency's user_competency_course_exporter. + */ +export type AddonCompetencyUserCompetencyCourse = { + userid: number; // Userid. + courseid: number; // Courseid. + competencyid: number; // Competencyid. + proficiency: boolean; // Proficiency. + grade: number; // Grade. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + gradename: string; // Gradename. + proficiencyname: string; // Proficiencyname. +}; + +/** + * Data returned by competency's evidence_exporter. + */ +export type AddonCompetencyEvidence = { + usercompetencyid: number; // Usercompetencyid. + contextid: number; // Contextid. + action: number; // Action. + actionuserid: number; // Actionuserid. + descidentifier: string; // Descidentifier. + desccomponent: string; // Desccomponent. + desca: string; // Desca. + url: string; // Url. + grade: number; // Grade. + note: string; // Note. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. + actionuser?: CoreUserSummary; + description: string; // Description. + gradename: string; // Gradename. + userdate: string; // Userdate. + candelete: boolean; // Candelete. +}; + +/** + * Params of tool_lp_data_for_user_competency_summary_in_course WS. + */ +type AddonCompetencyDataForUserCompetencySummaryInCourseWSParams = { + userid: number; // Data base record id for the user. + competencyid: number; // Data base record id for the competency. + courseid: number; // Data base record id for the course. +}; + +/** + * Data returned by competency's user_competency_summary_in_course_exporter. + */ +export type AddonCompetencyDataForUserCompetencySummaryInCourseWSResponse = { + usercompetencysummary: AddonCompetencyDataForUserCompetencySummaryWSResponse; + course: CoreCourseSummary; + coursemodules: CoreCourseModuleSummary[]; // Coursemodules. + plans: AddonCompetencyPlan[]; // @since 3.7. Plans. + pluginbaseurl: string; // @since 3.7. Pluginbaseurl. +}; + +/** + * Data returned by competency's course_competency_settings_exporter. + */ +export type AddonCompetencyCourseCompetencySettings = { + courseid: number; // Courseid. + pushratingstouserplans: boolean; // Pushratingstouserplans. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. +}; + +/** + * Data returned by competency's course_competency_statistics_exporter. + */ +export type AddonCompetencyCourseCompetencyStatistics = { + competencycount: number; // Competencycount. + proficientcompetencycount: number; // Proficientcompetencycount. + proficientcompetencypercentage: number; // Proficientcompetencypercentage. + proficientcompetencypercentageformatted: string; // Proficientcompetencypercentageformatted. + leastproficient: AddonCompetencyCompetency[]; // Leastproficient. + leastproficientcount: number; // Leastproficientcount. + canbegradedincourse: boolean; // Canbegradedincourse. + canmanagecoursecompetencies: boolean; // Canmanagecoursecompetencies. +}; + +/** + * Data returned by competency's course_competency_exporter. + */ +export type AddonCompetencyCourseCompetency = { + courseid: number; // Courseid. + competencyid: number; // Competencyid. + sortorder: number; // Sortorder. + ruleoutcome: number; // Ruleoutcome. + id: number; // Id. + timecreated: number; // Timecreated. + timemodified: number; // Timemodified. + usermodified: number; // Usermodified. +}; + +/** + * Params of tool_lp_data_for_plans_page WS. + */ +type AddonCompetencyDataForPlansPageWSParams = { + userid: number; // The user id. +}; + +/** + * Data returned by tool_lp_data_for_plans_page WS. + */ +export type AddonCompetencyDataForPlansPageWSResponse = { + userid: number; // The learning plan user id. + plans: AddonCompetencyPlan[]; + pluginbaseurl: string; // Url to the tool_lp plugin folder on this Moodle site. + navigation: string[]; + canreaduserevidence: boolean; // Can the current user view the user's evidence. + canmanageuserplans: boolean; // Can the current user manage the user's plans. +}; + +/** + * Params of tool_lp_data_for_plan_page WS. + */ +type AddonCompetencyDataForPlanPageWSParams = { + planid: number; // The plan id. +}; + +/** + * Data returned by tool_lp_data_for_plan_page WS. + */ +export type AddonCompetencyDataForPlanPageWSResponse = { + plan: AddonCompetencyPlan; + contextid: number; // Context ID. + pluginbaseurl: string; // Plugin base URL. + competencies: AddonCompetencyDataForPlanPageCompetency[]; + competencycount: number; // Count of competencies. + proficientcompetencycount: number; // Count of proficientcompetencies. + proficientcompetencypercentage: number; // Percentage of competencies proficient. + proficientcompetencypercentageformatted: string; // Displayable percentage. +}; + +/** + * Competency data returned by tool_lp_data_for_plan_page. + */ +export type AddonCompetencyDataForPlanPageCompetency = { + competency: AddonCompetencyCompetency; + comppath: AddonCompetencyPath; + usercompetency?: AddonCompetencyUserCompetency; + usercompetencyplan?: AddonCompetencyUserCompetencyPlan; +}; + +/** + * Params of tool_lp_data_for_course_competencies_page WS. + */ +type AddonCompetencyDataForCourseCompetenciesPageWSParams = { + courseid: number; // The course id. + moduleid?: number; // The module id. +}; + +/** + * Data returned by tool_lp_data_for_course_competencies_page WS. + */ +export type AddonCompetencyDataForCourseCompetenciesPageWSResponse = { + courseid: number; // The current course id. + pagecontextid: number; // The current page context ID. + gradableuserid?: number; // Current user id, if the user is a gradable user. + canmanagecompetencyframeworks: boolean; // User can manage competency frameworks. + canmanagecoursecompetencies: boolean; // User can manage linked course competencies. + canconfigurecoursecompetencies: boolean; // User can configure course competency settings. + cangradecompetencies: boolean; // User can grade competencies. + settings: AddonCompetencyCourseCompetencySettings; + statistics: AddonCompetencyCourseCompetencyStatistics; + competencies: AddonCompetencyDataForCourseCompetenciesPageCompetency[]; + manageurl: string; // Url to the manage competencies page. + pluginbaseurl: string; // @since 3.6. Url to the course competencies page. +}; + +/** + * Competency data returned by tool_lp_data_for_course_competencies_page. + */ +export type AddonCompetencyDataForCourseCompetenciesPageCompetency = { + competency: AddonCompetencyCompetency; + coursecompetency: AddonCompetencyCourseCompetency; + coursemodules: CoreCourseModuleSummary[]; + usercompetencycourse?: AddonCompetencyUserCompetencyCourse; + ruleoutcomeoptions: { + value: number; // The option value. + text: string; // The name of the option. + selected: boolean; // If this is the currently selected option. + }[]; + comppath: AddonCompetencyPath; + plans: AddonCompetencyPlan[]; // @since 3.7. +}; + +/** + * Params of core_competency_user_competency_plan_viewed and core_competency_user_competency_viewed_in_plan WS. + */ +type AddonCompetencyUserCompetencyPlanViewedWSParams = { + competencyid: number; // The competency id. + userid: number; // The user id. + planid: number; // The plan id. +}; + +/** + * Params of core_competency_user_competency_viewed_in_course WS. + */ +type AddonCompetencyUserCompetencyViewedInCourseWSParams = { + competencyid: number; // The competency id. + userid: number; // The user id. + courseid: number; // The course id. +}; + +/** + * Params of core_competency_competency_viewed WS. + */ +type AddonCompetencyCompetencyViewedWSParams = { + id: number; // The competency id. +}; diff --git a/src/addons/competency/services/handlers/competency-link.ts b/src/addons/competency/services/handlers/competency-link.ts new file mode 100644 index 000000000..dbbdd1df6 --- /dev/null +++ b/src/addons/competency/services/handlers/competency-link.ts @@ -0,0 +1,66 @@ +// (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 { AddonCompetency } from '../competency'; +import { AddonCompetencyMainMenuHandlerService } from './mainmenu'; + +/** + * Handler to treat links to a competency in a plan or in a course. + */ +@Injectable( { providedIn: 'root' }) +export class AddonCompetencyCompetencyLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonCompetencyCompetencyLinkHandler'; + pattern = /\/admin\/tool\/lp\/(user_competency_in_course|user_competency_in_plan)\.php/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record, courseId?: number): CoreContentLinksAction[] { + courseId = courseId || parseInt(params.courseid || params.cid, 10); + + return [{ + action: (siteId: string): void => { + const pageParams = { + planId: params.planid, + courseId: courseId, + userId: params.userid, + }; + + CoreNavigator.navigateToSitePath( + '/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/competencies/' + params.competencyid, + { params: pageParams, siteId }, + ); + + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string): Promise { + // Handler is disabled if all competency features are disabled. + const disabled = await AddonCompetency.allCompetenciesDisabled(siteId); + + return !disabled; + } + +} +export const AddonCompetencyCompetencyLinkHandler = makeSingleton(AddonCompetencyCompetencyLinkHandlerService); diff --git a/src/addons/competency/services/handlers/course-option.ts b/src/addons/competency/services/handlers/course-option.ts new file mode 100644 index 000000000..e5f1e5364 --- /dev/null +++ b/src/addons/competency/services/handlers/course-option.ts @@ -0,0 +1,141 @@ +// (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 { CoreCourseProvider } from '@features/course/services/course'; +import { + CoreCourseAccess, + CoreCourseOptionsHandler, + CoreCourseOptionsHandlerData, +} from '@features/course/services/course-options-delegate'; +import { makeSingleton } from '@singletons'; +import { AddonCompetency } from '../competency'; +import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; +import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; +import { CoreFilterHelper } from '@features/filter/services/filter-helper'; +import { ContextLevel } from '@/core/constants'; +import { AddonCompetencyMainMenuHandlerService } from './mainmenu'; + +/** + * Course nav handler. + */ +@Injectable( { providedIn: 'root' }) +export class AddonCompetencyCourseOptionHandlerService implements CoreCourseOptionsHandler { + + name = 'AddonCompetency'; + priority = 300; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + async isEnabledForCourse( + courseId: number, + accessData: CoreCourseAccess, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise { + if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { + return false; // Not enabled for guests. + } + + if (navOptions && typeof navOptions.competencies != 'undefined') { + return navOptions.competencies; + } + + try { + const competencies = await AddonCompetency.getCourseCompetencies(courseId); + + return competencies ? !competencies.canmanagecoursecompetencies : false; + } catch { + return false; + } + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreCourseOptionsHandlerData { + return { + title: 'addon.competency.competencies', + class: 'addon-competency-course-handler', + page: AddonCompetencyMainMenuHandlerService.PAGE_NAME, + }; + } + + /** + * @inheritdoc + */ + async invalidateEnabledForCourse(courseId: number, navOptions?: CoreCourseUserAdminOrNavOptionIndexed): Promise { + if (navOptions && typeof navOptions.competencies != 'undefined') { + // No need to invalidate anything. + return; + } + + return AddonCompetency.invalidateCourseCompetencies(courseId); + } + + /** + * @inheritdoc + */ + async prefetch(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise { + // Get the competencies in the course. + const competencies = await AddonCompetency.getCourseCompetencies(course.id, undefined, undefined, true); + + if (!competencies || !competencies.competencies) { + return; + } + + const promises: Promise[] = []; + + // Prefetch all the competencies. + competencies.competencies.forEach((competency) => { + promises.push(AddonCompetency.getCompetencyInCourse( + course.id, + competency.competency.id, + undefined, + undefined, + true, + )); + + promises.push(AddonCompetency.getCompetencySummary( + competency.competency.id, + undefined, + undefined, + true, + )); + + if (competency.coursemodules) { + competency.coursemodules.forEach((module) => { + promises.push(CoreFilterHelper.getFilters(ContextLevel.MODULE, module.id, { courseId: course.id })); + }); + } + + if (competency.plans) { + competency.plans.forEach((plan) => { + promises.push(CoreFilterHelper.getFilters(ContextLevel.USER, plan.userid)); + }); + } + }); + + await Promise.all(promises); + } + +} +export const AddonCompetencyCourseOptionHandler = makeSingleton(AddonCompetencyCourseOptionHandlerService); diff --git a/src/addons/competency/services/handlers/mainmenu.ts b/src/addons/competency/services/handlers/mainmenu.ts new file mode 100644 index 000000000..8c4130b83 --- /dev/null +++ b/src/addons/competency/services/handlers/mainmenu.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 { Injectable } from '@angular/core'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate'; +import { makeSingleton } from '@singletons'; +import { AddonCompetency } from '../competency'; + +/** + * Handler to inject an option into main menu. + */ +@Injectable( { providedIn: 'root' }) +export class AddonCompetencyMainMenuHandlerService implements CoreMainMenuHandler { + + static readonly PAGE_NAME = 'competency'; + + name = 'AddonCompetency'; + priority = 500; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + // Check the user has at least one learn plan available. + const plans = await AddonCompetency.getLearningPlans(); + + return plans.length > 0; + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + icon: 'fas-route', + title: 'addon.competency.myplans', + page: AddonCompetencyMainMenuHandlerService.PAGE_NAME, + class: 'addon-competency-handler', + }; + } + +} +export const AddonCompetencyMainMenuHandler = makeSingleton(AddonCompetencyMainMenuHandlerService); diff --git a/src/addons/competency/services/handlers/plan-link.ts b/src/addons/competency/services/handlers/plan-link.ts new file mode 100644 index 000000000..ca4ab179b --- /dev/null +++ b/src/addons/competency/services/handlers/plan-link.ts @@ -0,0 +1,57 @@ +// (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 { AddonCompetency } from '../competency'; +import { AddonCompetencyMainMenuHandlerService } from './mainmenu'; + +/** + * Handler to treat links to a plan. + */ +@Injectable( { providedIn: 'root' }) +export class AddonCompetencyPlanLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonCompetencyPlanLinkHandler'; + pattern = /\/admin\/tool\/lp\/plan\.php.*([?&]id=\d+)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + return [{ + action: (siteId: string): void => { + CoreNavigator.navigateToSitePath( + '/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/' + params.id, + { siteId }, + ); + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string): Promise { + // Handler is disabled if all competency features are disabled. + const disabled = await AddonCompetency.allCompetenciesDisabled(siteId); + + return !disabled; + } + +} +export const AddonCompetencyPlanLinkHandler = makeSingleton(AddonCompetencyPlanLinkHandlerService); diff --git a/src/addons/competency/services/handlers/plans-link.ts b/src/addons/competency/services/handlers/plans-link.ts new file mode 100644 index 000000000..e7d377f61 --- /dev/null +++ b/src/addons/competency/services/handlers/plans-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 { CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { AddonCompetency } from '../competency'; +import { AddonCompetencyMainMenuHandlerService } from './mainmenu'; + +/** + * Handler to treat links to user plans. + */ +@Injectable( { providedIn: 'root' }) +export class AddonCompetencyPlansLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonCompetencyPlansLinkHandler'; + pattern = /\/admin\/tool\/lp\/plans\.php/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + return [{ + action: (siteId: string): void => { + CoreNavigator.navigateToSitePath( + '/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME, + { params: { userId: params.userid }, siteId }, + ); + + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string): Promise { + // Handler is disabled if all competency features are disabled. + const disabled = await AddonCompetency.allCompetenciesDisabled(siteId); + + return !disabled; + } + +} +export const AddonCompetencyPlansLinkHandler = makeSingleton(AddonCompetencyPlansLinkHandlerService); diff --git a/src/addons/competency/services/handlers/push-click.ts b/src/addons/competency/services/handlers/push-click.ts new file mode 100644 index 000000000..7966bbf66 --- /dev/null +++ b/src/addons/competency/services/handlers/push-click.ts @@ -0,0 +1,102 @@ +// (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 { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; +import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; +import { CoreNavigator } from '@services/navigator'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton } from '@singletons'; +import { AddonCompetency } from '../competency'; +import { AddonCompetencyMainMenuHandlerService } from './mainmenu'; + +/** + * Handler for competencies push notifications clicks. + */ +@Injectable( { providedIn: 'root' }) +export class AddonCompetencyPushClickHandlerService implements CorePushNotificationsClickHandler { + + name = 'AddonCompetencyPushClickHandler'; + priority = 200; + + /** + * @inheritdoc + */ + async handles(notification: AddonCompetencyPushNotificationData): Promise { + if (CoreUtils.isTrueOrOne(notification.notif) && notification.moodlecomponent == 'moodle' && + (notification.name == 'competencyplancomment' || notification.name == 'competencyusercompcomment')) { + // If all competency features are disabled, don't handle the click. + return AddonCompetency.allCompetenciesDisabled(notification.site).then((disabled) => !disabled); + } + + return false; + } + + /** + * @inheritdoc + */ + async handleClick(notification: AddonCompetencyPushNotificationData): Promise { + const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl); + + if (notification.name == 'competencyplancomment') { + // Open the learning plan. + const planId = Number(contextUrlParams.id); + + await CoreUtils.ignoreErrors(AddonCompetency.invalidateLearningPlan(planId, notification.site)); + + await CoreNavigator.navigateToSitePath('/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/' + planId, { + siteId: notification.site, + }); + + return; + } + + if (notification.contexturl && notification.contexturl.indexOf('user_competency_in_plan.php') != -1) { + // Open the competency. + const courseId = Number(notification.course); + const competencyId = Number(contextUrlParams.competencyid); + const planId = Number(contextUrlParams.planid); + const userId = Number(contextUrlParams.userid); + + await CoreUtils.ignoreErrors(AddonCompetency.invalidateCompetencyInPlan(planId, competencyId, notification.site)); + await CoreNavigator.navigateToSitePath( + '/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/competencies/' + competencyId, + { + params: { planId, courseId, userId }, + siteId: notification.site, + }, + ); + + return; + } + + // Open the list of plans. + const userId = Number(contextUrlParams.userid); + + await CoreUtils.ignoreErrors(AddonCompetency.invalidateLearningPlans(userId, notification.site)); + + await CoreNavigator.navigateToSitePath('/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME, { + params: { userId }, + siteId: notification.site, + }); + } + +} +export const AddonCompetencyPushClickHandler = makeSingleton(AddonCompetencyPushClickHandlerService); + +type AddonCompetencyPushNotificationData = CorePushNotificationsNotificationBasicData & { + contexturl: string; + course: number; +}; diff --git a/src/addons/competency/services/handlers/user-competency-link.ts b/src/addons/competency/services/handlers/user-competency-link.ts new file mode 100644 index 000000000..7a1380f3a --- /dev/null +++ b/src/addons/competency/services/handlers/user-competency-link.ts @@ -0,0 +1,59 @@ +// (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 { AddonCompetency } from '../competency'; +import { AddonCompetencyMainMenuHandlerService } from './mainmenu'; + +/** + * Handler to treat links to a usr competency. + */ +@Injectable( { providedIn: 'root' }) +export class AddonCompetencyUserCompetencyLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonCompetencyUserCompetencyLinkHandler'; + pattern = /\/admin\/tool\/lp\/user_competency\.php.*([?&]id=\d+)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + + return [{ + action: (siteId: string): void => { + CoreNavigator.navigateToSitePath( + '/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/summary/' + params.id, + { siteId }, + ); + + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string): Promise { + // Handler is disabled if all competency features are disabled. + const disabled = await AddonCompetency.allCompetenciesDisabled(siteId); + + return !disabled; + } + +} +export const AddonCompetencyUserCompetencyLinkHandler = makeSingleton(AddonCompetencyUserCompetencyLinkHandlerService); diff --git a/src/addons/competency/services/handlers/user.ts b/src/addons/competency/services/handlers/user.ts new file mode 100644 index 000000000..1500085ee --- /dev/null +++ b/src/addons/competency/services/handlers/user.ts @@ -0,0 +1,97 @@ +// (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 { CoreUserProfile } from '@features/user/services/user'; +import { CoreUserProfileHandler, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; +import { CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { AddonCompetency } from '../competency'; +import { AddonCompetencyMainMenuHandlerService } from './mainmenu'; + +/** + * Profile competencies handler. + */ +@Injectable( { providedIn: 'root' }) +export class AddonCompetencyUserHandlerService implements CoreUserProfileHandler { + + name = 'AddonCompetency:learningPlan'; + priority = 900; + type = CoreUserDelegateService.TYPE_NEW_PAGE; + cacheEnabled = true; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + async isEnabledForUser(user: CoreUserProfile, courseId?: number): Promise { + try { + if (courseId) { + return AddonCompetency.canViewUserCompetenciesInCourse(courseId, user.id); + } else { + const plans = await AddonCompetency.getLearningPlans(user.id); + + // Check the user has at least one learn plan available. + return plans.length > 0; + } + } catch { + return false; + } + } + + /** + * @inheritdoc + */ + getDisplayData(user: CoreUserProfile, courseId: number): CoreUserProfileHandlerData { + if (courseId) { + return { + icon: 'fas-award', + title: 'addon.competency.competencies', + class: 'addon-competency-handler', + action: (event, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); + CoreNavigator.navigateToSitePath( + '/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/course/' + courseId, + { + params: { userId: user.id }, + }, + ); + + }, + }; + } else { + return { + icon: 'fas-route', + title: 'addon.competency.learningplans', + class: 'addon-competency-handler', + action: (event, user): void => { + event.preventDefault(); + event.stopPropagation(); + CoreNavigator.navigateToSitePath('/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME, { + params: { userId: user.id }, + }); + }, + }; + } + } + +} +export const AddonCompetencyUserHandler = makeSingleton(AddonCompetencyUserHandlerService); diff --git a/src/addons/mod/assign/pages/submission-list/submission-list.page.ts b/src/addons/mod/assign/pages/submission-list/submission-list.page.ts index 26f9f8d22..6b2f49f1a 100644 --- a/src/addons/mod/assign/pages/submission-list/submission-list.page.ts +++ b/src/addons/mod/assign/pages/submission-list/submission-list.page.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnDestroy, AfterViewInit, ViewChild } from '@angular/core'; -import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router'; +import { ActivatedRouteSnapshot, Params } from '@angular/router'; import { CorePageItemsListManager } from '@classes/page-items-list-manager'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { IonRefresher } from '@ionic/angular'; @@ -73,9 +73,7 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro canviewsubmissions: false, }; - constructor( - protected route: ActivatedRoute, - ) { + constructor() { this.submissions = new AddonModAssignSubmissionListManager(AddonModAssignSubmissionListPage); // Update data if some grade changes. diff --git a/src/core/classes/delegate-sorted.ts b/src/core/classes/delegate-sorted.ts index 751573574..3c65e3b73 100644 --- a/src/core/classes/delegate-sorted.ts +++ b/src/core/classes/delegate-sorted.ts @@ -86,14 +86,14 @@ export class CoreSortedDelegate< const handler = this.enabledHandlers[name]; const data = handler.getDisplayData(); - data.priority = handler.priority; + data.priority = handler.priority || 0; data.name = handler.name; displayData.push(data); } // Sort them by priority. - displayData.sort((a, b) => (b.priority || 0) - (a.priority || 0)); + displayData.sort((a, b) => b.priority! - a.priority!); this.loaded = true; this.sortedHandlersRxJs.next(displayData); diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index c4b0aed05..8a10e06a7 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -118,7 +118,7 @@ import { CoreSitePluginsAssignSubmissionComponent } from '@features/siteplugins/ import { ADDON_BADGES_SERVICES } from '@addons/badges/badges.module'; import { ADDON_CALENDAR_SERVICES } from '@addons/calendar/calendar.module'; import { ADDON_COURSECOMPLETION_SERVICES } from '@addons/coursecompletion/coursecompletion.module'; -// @todo import { ADDON_COMPETENCY_SERVICES } from '@addons/competency/competency.module'; +import { ADDON_COMPETENCY_SERVICES } from '@addons/competency/competency.module'; import { ADDON_MESSAGEOUTPUT_SERVICES } from '@addons/messageoutput/messageoutput.module'; import { ADDON_MESSAGES_SERVICES } from '@addons/messages/messages.module'; import { ADDON_MOD_ASSIGN_SERVICES } from '@addons/mod/assign/assign.module'; @@ -283,7 +283,7 @@ export class CoreCompileProvider { ...ADDON_BADGES_SERVICES, ...ADDON_CALENDAR_SERVICES, ...ADDON_COURSECOMPLETION_SERVICES, - // @todo ...ADDON_COMPETENCY_SERVICES, + ...ADDON_COMPETENCY_SERVICES, ...ADDON_MESSAGEOUTPUT_SERVICES, ...ADDON_MESSAGES_SERVICES, ...ADDON_MOD_ASSIGN_SERVICES, diff --git a/src/core/features/course/services/course-options-delegate.ts b/src/core/features/course/services/course-options-delegate.ts index 52fb9d3f8..dc54a21bb 100644 --- a/src/core/features/course/services/course-options-delegate.ts +++ b/src/core/features/course/services/course-options-delegate.ts @@ -450,7 +450,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate { handlersToDisplay.push({ data: data, - priority: handler.priority, + priority: handler.priority || 0, prefetch: handler.prefetch && handler.prefetch.bind(handler), name: handler.name, }); @@ -467,7 +467,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate (b.priority || 0) - (a.priority || 0)); + ) => b.priority! - a.priority!); return handlersToDisplay; } diff --git a/src/core/features/fileuploader/services/fileuploader-delegate.ts b/src/core/features/fileuploader/services/fileuploader-delegate.ts index 7dcc70d74..6bcb5146e 100644 --- a/src/core/features/fileuploader/services/fileuploader-delegate.ts +++ b/src/core/features/fileuploader/services/fileuploader-delegate.ts @@ -183,13 +183,13 @@ export class CoreFileUploaderDelegateService extends CoreDelegate (a.priority || 0) <= (b.priority || 0) ? 1 : -1); + handlers.sort((a, b) => a.priority! <= b.priority! ? 1 : -1); return handlers; } diff --git a/src/core/features/user/services/user-delegate.ts b/src/core/features/user/services/user-delegate.ts index 551e22ad9..38c11a9c3 100644 --- a/src/core/features/user/services/user-delegate.ts +++ b/src/core/features/user/services/user-delegate.ts @@ -311,7 +311,7 @@ export class CoreUserDelegateService extends CoreDelegate (b.priority || 0) - (a.priority || 0)); + userData.handlers.sort((a, b) => b.priority! - a.priority!); userData.loaded = true; userData.observable.next(userData.handlers); }