Merge pull request #2708 from crazyserver/MOBILE-3628

Mobile 3628
main
Dani Palou 2021-03-17 10:20:05 +01:00 committed by GitHub
commit 257808ea8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 3383 additions and 14 deletions

View File

@ -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,

View File

@ -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,
};
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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<unknown>[] = [
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 {}

View File

@ -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."
}

View File

@ -0,0 +1,34 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ title }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-split-view>
<ion-refresher slot="fixed" [disabled]="!competencies.loaded" (ionRefresh)="refreshCompetencies($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="competencies.loaded">
<ion-list>
<ion-item class="ion-text-wrap" *ngFor="let competency of competencies.items"
[title]="competency.competency.shortname" (click)="competencies.select(competency)"
[class.core-selected-item]="competencies.isSelected(competency)">
<ion-label>
<h2>{{ competency.competency.shortname }} <em>{{competency.competency.idnumber}}</em></h2>
</ion-label>
<ion-badge slot="end" *ngIf="competency.usercompetency"
[color]="competency.usercompetency.proficiency ? 'success' : 'danger'">
{{ competency.usercompetency.gradename }}
</ion-badge>
<ion-badge slot="end" *ngIf="competency.usercompetencycourse"
[color]="competency.usercompetencycourse.proficiency ? 'success' : 'danger'">
{{ competency.usercompetencycourse.gradename }}
</ion-badge>
</ion-item>
</ion-list>
</core-loading>
</core-split-view>
</ion-content>

View File

@ -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<void> {
await this.fetchCompetencies();
this.competencies.start(this.splitView);
}
/**
* Fetches the competencies and updates the view.
*
* @return Promise resolved when done.
*/
protected async fetchCompetencies(): Promise<void> {
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<void> {
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<AddonCompetencyDataForPlanPageCompetencyFormatted> {
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;
}
}

View File

@ -0,0 +1,139 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title *ngIf="competency">
{{ competency.competency.competency.shortname }} <small>{{ competency.competency.competency.idnumber }}</small>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!competencyLoaded" (ionRefresh)="refreshCompetency($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="competencyLoaded">
<ion-card *ngIf="user">
<ion-item class="ion-text-wrap">
<core-user-avatar [user]="user" slot="start"></core-user-avatar>
<ion-label><h2>{{ user.fullname }}</h2></ion-label>
</ion-item>
</ion-card>
<ion-card *ngIf="competency">
<ion-item class="ion-text-wrap" *ngIf="competency.competency.competency.description">
<ion-label>
<core-format-text [text]="competency.competency.competency.description" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId">
</core-format-text>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<strong>{{ 'addon.competency.path' | translate }}</strong>
<a *ngIf="competency.competency.comppath.showlinks"
[href]="competency.competency.comppath.pluginbaseurl + '/competencies.php?competencyframeworkid=' +
competency.competency.comppath.framework.id + '&pagecontextid=' +
competency.competency.comppath.pagecontextid"
core-link [title]="competency.competency.comppath.framework.name">
{{ competency.competency.comppath.framework.name }}
</a>
<ng-container *ngIf="!competency.competency.comppath.showlinks">
{{ competency.competency.comppath.framework.name }}
</ng-container>
&nbsp;/&nbsp;
<span *ngFor="let ancestor of competency.competency.comppath.ancestors">
<a *ngIf="competency.competency.comppath.showlinks" (click)="openCompetencySummary(ancestor.id)"
class="core-clickable">
{{ ancestor.name }}
</a>
<ng-container *ngIf="!competency.competency.comppath.showlinks">{{ ancestor.name }}</ng-container>
<ng-container *ngIf="!ancestor.last">&nbsp;/&nbsp;</ng-container>
</span>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<strong>{{ 'addon.competency.crossreferencedcompetencies' | translate }}</strong>:
<div *ngIf="!competency.competency.hasrelatedcompetencies">
{{ 'addon.competency.nocrossreferencedcompetencies' | translate }}
</div>
<div *ngIf="competency.competency.hasrelatedcompetencies">
<p *ngFor="let relatedcomp of competency.competency.relatedcompetencies">
<a (click)="openCompetencySummary(relatedcomp.id)" class="core-clickable">
{{ relatedcomp.shortname }} - {{ relatedcomp.idnumber }}
</a>
</p>
</div>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="coursemodules">
<ion-label>
<strong>{{ 'addon.competency.activities' | translate }}</strong>
<p *ngIf="coursemodules.length == 0">
{{ 'addon.competency.noactivities' | translate }}
</p>
<ion-item class="ion-text-wrap" *ngFor="let activity of coursemodules" [href]="activity.url" [title]="activity.name"
core-link capture="true">
<img slot="start" core-external-content [src]="activity.iconurl" alt="" role="presentation" *ngIf="activity.iconurl"
class="core-module-icon">
<ion-label>
<core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ion-label>
</ion-item>
<ng-container *ngIf="userCompetency">
<ion-item class="ion-text-wrap" *ngIf="competency.usercompetency && competency.usercompetency!.status">
<ion-label>
<strong>{{ 'addon.competency.reviewstatus' | translate }}</strong>
{{ competency.usercompetency!.statusname }}
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<strong>{{ 'addon.competency.proficient' | translate }}</strong>
</ion-label>
<ion-badge slot="end" color="success" *ngIf="userCompetency.proficiency">
{{ 'core.yes' | translate }}
</ion-badge>
<ion-badge slot="end" color="danger" *ngIf="!userCompetency.proficiency">
{{ 'core.no' | translate }}
</ion-badge>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<strong>{{ 'addon.competency.rating' | translate }}</strong>
</ion-label>
<ion-badge color="dark" slot="end">{{ userCompetency.gradename }}</ion-badge>
</ion-item>
</ng-container>
</ion-card>
<div *ngIf="competency">
<h3 class="ion-margin-horizontal">{{ 'addon.competency.evidence' | translate }}</h3>
<p class="ion-margin-horizontal" *ngIf="competency.evidence.length == 0">
{{ 'addon.competency.noevidence' | translate }}
</p>
<ion-card *ngFor="let evidence of competency.evidence">
<ion-item class="ion-text-wrap" *ngIf="evidence.actionuser" core-user-link [userId]="evidence.actionuser.id"
[courseId]="courseId">
<core-user-avatar [user]="evidence.actionuser" slot="start"></core-user-avatar>
<ion-label>
<h2>{{ evidence.actionuser.fullname }}</h2>
<p>{{ evidence.timemodified * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<p><ion-badge color="dark">{{ evidence.gradename }}</ion-badge></p>
<p class="ion-margin-top" *ngIf="evidence.description">{{ evidence.description }}</p>
<blockquote *ngIf="evidence.note">{{ evidence.note }}</blockquote>
</ion-label>
</ion-item>
</ion-card>
</div>
</core-loading>
</ion-content>

View File

@ -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<void> {
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<void> {
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<void> {
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 },
},
);
}
}

View File

@ -0,0 +1,35 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title *ngIf="competency">
{{ competency.competency.shortname }} <small>{{ competency.competency.idnumber }}</small>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!competencyLoaded" (ionRefresh)="refreshCompetency($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="competencyLoaded">
<ion-card *ngIf="competency">
<ion-item class="ion-text-wrap" *ngIf="competency.competency.description">
<ion-label>
<core-format-text [text]="competency.competency.description" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId">
</core-format-text>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<strong>{{ 'addon.competency.path' | translate }}</strong>
{{ competency.comppath.framework.name }}
<span *ngFor="let ancestor of competency.comppath.ancestors">
&nbsp;/&nbsp;<a (click)="openCompetencySummary(ancestor.id)">{{ ancestor.name }}</a>
</span>
</ion-label>
</ion-item>
</ion-card>
</core-loading>
</ion-content>

View File

@ -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<void> {
this.competencyId = CoreNavigator.getRouteNumberParam('competencyId')!;
this.contextLevel = CoreNavigator.getRouteParam<ContextLevel>('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<void> {
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 },
},
);
}
}

View File

@ -0,0 +1,135 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'addon.competency.coursecompetencies' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!competenciesLoaded" (ionRefresh)="refreshCourseCompetencies($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="competenciesLoaded">
<ion-card *ngIf="!user && competencies && competencies.statistics.competencycount > 0">
<ng-container *ngIf="competencies.cangradecompetencies">
<ion-item class="ion-text-wrap" *ngIf="competencies.settings.pushratingstouserplans">
<ion-label>{{ 'addon.competency.coursecompetencyratingsarepushedtouserplans' | translate }}</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="!competencies.settings.pushratingstouserplans" color="danger">
<ion-label>{{ 'addon.competency.coursecompetencyratingsarenotpushedtouserplans' | translate }}</ion-label>
</ion-item>
</ng-container>
<ion-item class="ion-text-wrap" *ngIf="competencies.statistics.canbegradedincourse">
<ion-label>
{{ 'addon.competency.xcompetenciesproficientoutofyincourse' | translate: {$a:
{x: competencies.statistics.proficientcompetencycount, y: competencies.statistics.competencycount} } }}
<core-progress-bar [progress]="competencies.statistics.proficientcompetencypercentage"></core-progress-bar>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap"
*ngIf="competencies.statistics.canmanagecoursecompetencies && competencies.statistics.leastproficientcount > 0">
<ion-label>
<strong>{{ 'addon.competency.competenciesmostoftennotproficientincourse' | translate }}</strong>:
<p *ngFor="let comp of competencies.statistics.leastproficient">
<a (click)="openCompetencySummary(comp.id)">
{{ comp.shortname }} - {{ comp.idnumber }}
</a>
</p>
</ion-label>
</ion-item>
</ion-card>
<h3 class="ion-margin-horizontal" *ngIf="competencies && competencies.statistics.competencycount > 0">
{{ 'addon.competency.coursecompetencies' | translate }}
</h3>
<ion-card *ngIf="user">
<ion-item class="ion-text-wrap">
<core-user-avatar [user]="user" slot="start"></core-user-avatar>
<ion-label><h2>{{ user.fullname }}</h2></ion-label>
</ion-item>
</ion-card>
<core-empty-box *ngIf="competencies && competencies.statistics.competencycount == 0"
icon="fas-award" message="{{ 'addon.competency.nocompetenciesincourse' | translate }}">
</core-empty-box>
<div *ngIf="competencies">
<ion-card *ngFor="let competency of competencies.competencies">
<ion-item class="ion-text-wrap" (click)="openCompetency(competency.competency.id)"
[title]="competency.competency.shortname" detail="true">
<ion-label>
<h2><strong>{{competency.competency.shortname}} <em>{{competency.competency.idnumber}}</em></strong></h2>
</ion-label>
<ion-badge slot="end" *ngIf="competency.usercompetencycourse && competency.usercompetencycourse.gradename"
[color]="competency.usercompetencycourse.proficiency ? 'success' : 'danger'">
{{ competency.usercompetencycourse.gradename }}
</ion-badge>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<p *ngIf="competency.competency.description">
<core-format-text [text]="competency.competency.description" contextLevel="course"
[contextInstanceId]="courseId">
</core-format-text>
</p>
<div>
<strong>{{ 'addon.competency.path' | translate }}</strong>
<a *ngIf="competency.comppath.showlinks"
[href]="competency.comppath.pluginbaseurl + '/competencies.php?competencyframeworkid=' +
competency.comppath.framework.id + '&pagecontextid=' + competency.comppath.pagecontextid"
core-link [title]="competency.comppath.framework.name">
{{ competency.comppath.framework.name }}
</a>
<ng-container *ngIf="!competency.comppath.showlinks">
{{ competency.comppath.framework.name }}
</ng-container>
&nbsp;/&nbsp;
<span *ngFor="let ancestor of competency.comppath.ancestors">
<a *ngIf="competency.comppath.showlinks" (click)="openCompetencySummary(ancestor.id)">
{{ ancestor.name }}
</a>
<ng-container *ngIf="!competency.comppath.showlinks">{{ ancestor.name }}</ng-container>
<ng-container *ngIf="!ancestor.last">&nbsp;/&nbsp;</ng-container>
</span>
</div>
<div *ngIf="competencies.statistics.canmanagecoursecompetencies">
<strong>{{ 'addon.competency.uponcoursecompletion' | translate }}</strong>
<ng-container *ngFor="let ruleoutcome of competency.ruleoutcomeoptions">
<span *ngIf="ruleoutcome.selected">{{ ruleoutcome.text }}</span>
</ng-container>
</div>
<div>
<strong>{{ 'addon.competency.activities' | translate }}</strong>
<p *ngIf="competency.coursemodules.length == 0">
{{ 'addon.competency.noactivities' | translate }}
</p>
<ion-item class="ion-text-wrap core-course-module-handler item-media" [title]="activity.name" core-link
*ngFor="let activity of competency.coursemodules" [href]="activity.url" capture="true">
<img slot="start" [src]="activity.iconurl" core-external-content alt="" role="presentation"
*ngIf="activity.iconurl" class="core-module-icon">
<ion-label>
<core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</div>
<div *ngIf="competency.plans">
<strong>{{ 'addon.competency.userplans' | translate }}</strong>
<p *ngIf="competency.plans.length == 0">
{{ 'addon.competency.nouserplanswithcompetency' | translate }}
</p>
<ion-item class="ion-text-wrap" *ngFor="let plan of competency.plans" [href]="plan.url"
[title]="plan.name" core-link capture="true">
<ion-label>
<core-format-text [text]="plan.name" contextLevel="user" [contextInstanceId]="plan.userid">
</core-format-text>
</ion-label>
</ion-item>
</div>
</ion-label>
</ion-item>
</ion-card>
</div>
</core-loading>
</ion-content>

View File

@ -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 {}

View File

@ -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<void> {
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();
});
});
}
}

View File

@ -0,0 +1,92 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title *ngIf="plan">{{plan.plan.name}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshLearningPlan($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-card *ngIf="user">
<ion-item class="ion-text-wrap">
<ion-label>
<core-user-avatar [user]="user" slot="start"></core-user-avatar>
<h2>{{ user.fullname }}</h2>
</ion-label>
</ion-item>
</ion-card>
<ion-card *ngIf="plan">
<ion-list>
<ion-item class="ion-text-wrap" *ngIf="plan.plan.description" lines="none">
<ion-label>
<core-format-text [text]="plan.plan.description" contextLevel="user"
[contextInstanceId]="plan.plan.userid">
</core-format-text>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" lines="none">
<ion-label>
<p>
<strong>{{ 'addon.competency.status' | translate }}</strong>: {{ plan.plan.statusname }}
</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="plan.plan.duedate > 0" lines="none">
<ion-label>
<p>
<strong>{{ 'addon.competency.duedate' | translate }}</strong>:
{{ plan.plan.duedate * 1000 | coreFormatDate }}
</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="plan.plan.template" lines="none">
<ion-label>
<p>
<strong>{{ 'addon.competency.template' | translate }}</strong>: {{ plan.plan.template.shortname }}
</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" lines="none">
<ion-label>
<p>
<strong>{{ 'addon.competency.progress' | translate }}</strong>:
{{ 'addon.competency.xcompetenciesproficientoutofy' | translate:
{$a: {x: plan.proficientcompetencycount, y: plan.competencycount} } }}
</p>
<core-progress-bar [progress]="plan.proficientcompetencypercentage"
[text]="plan.proficientcompetencypercentageformatted"></core-progress-bar>
</ion-label>
</ion-item>
</ion-list>
</ion-card>
<ion-card *ngIf="plan">
<ion-card-header class="ion-text-wrap">
<h2>{{ 'addon.competency.learningplancompetencies' | translate }}</h2>
</ion-card-header>
<ion-list>
<ion-item class="ion-text-wrap" *ngIf="plan.competencycount == 0">
<ion-label>
<p>{{ 'addon.competency.nocompetencies' | translate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngFor="let competency of plan.competencies"
(click)="openCompetency(competency.competency.id)"
[title]="competency.competency.shortname" detail="true">
<ion-label><h2>{{competency.competency.shortname}} <em>{{competency.competency.idnumber}}</em></h2></ion-label>
<ion-badge *ngIf="competency.usercompetencyplan" slot="end"
[color]="competency.usercompetencyplan.proficiency ? 'success' : 'danger'">
{{ competency.usercompetencyplan.gradename }}
</ion-badge>
<ion-badge *ngIf="!competency.usercompetencyplan" slot="end"
[color]="competency.usercompetency.proficiency ? 'success' : 'danger'">
{{ competency.usercompetency.gradename }}
</ion-badge>
</ion-item>
</ion-list>
</ion-card>
</core-loading>
</ion-content>

View File

@ -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<void> {
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();
});
});
}
}

View File

@ -0,0 +1,33 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'addon.competency.userplans' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-split-view>
<ion-refresher slot="fixed" [disabled]="!plans.loaded" (ionRefresh)="refreshLearningPlans($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="plans.loaded">
<core-empty-box *ngIf="plans.empty" icon="fas-route" [message]="'addon.competency.noplanswerecreated' | translate">
</core-empty-box>
<ion-list *ngIf="!plans.empty" class="ion-no-margin">
<ion-item class="ion-text-wrap" *ngFor="let plan of plans.items" [title]="plan.name" (click)="plans.select(plan)"
[class.core-selected-item]="plans.isSelected(plan)">
<ion-label>
<h2>{{ plan.name }}</h2>
<p *ngIf="plan.duedate > 0">
{{ 'addon.competency.duedate' | translate }}:&nbsp;
{{ plan.duedate * 1000 | coreFormatDate :'strftimedatetimeshort' }}
</p>
</ion-label>
<ion-badge slot="end" class="ion-text-wrap" [color]="plan.statuscolor">{{ plan.statusname }}</ion-badge>
</ion-item>
</ion-list>
</core-loading>
</core-split-view>
</ion-content>

View File

@ -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<void> {
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<void> {
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<AddonCompetencyPlanFormatted> {
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;
}
}

View File

@ -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<CoreUserProfile | undefined> {
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);

File diff suppressed because it is too large Load Diff

View File

@ -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<string, string>, 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<boolean> {
// Handler is disabled if all competency features are disabled.
const disabled = await AddonCompetency.allCompetenciesDisabled(siteId);
return !disabled;
}
}
export const AddonCompetencyCompetencyLinkHandler = makeSingleton(AddonCompetencyCompetencyLinkHandlerService);

View File

@ -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<boolean> {
return true;
}
/**
* @inheritdoc
*/
async isEnabledForCourse(
courseId: number,
accessData: CoreCourseAccess,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
): Promise<boolean> {
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<void> {
if (navOptions && typeof navOptions.competencies != 'undefined') {
// No need to invalidate anything.
return;
}
return AddonCompetency.invalidateCourseCompetencies(courseId);
}
/**
* @inheritdoc
*/
async prefetch(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise<void> {
// Get the competencies in the course.
const competencies = await AddonCompetency.getCourseCompetencies(course.id, undefined, undefined, true);
if (!competencies || !competencies.competencies) {
return;
}
const promises: Promise<unknown>[] = [];
// 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);

View File

@ -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<boolean> {
// 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);

View File

@ -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<string, string>): CoreContentLinksAction[] {
return [{
action: (siteId: string): void => {
CoreNavigator.navigateToSitePath(
'/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/' + params.id,
{ siteId },
);
},
}];
}
/**
* @inheritdoc
*/
async isEnabled(siteId: string): Promise<boolean> {
// Handler is disabled if all competency features are disabled.
const disabled = await AddonCompetency.allCompetenciesDisabled(siteId);
return !disabled;
}
}
export const AddonCompetencyPlanLinkHandler = makeSingleton(AddonCompetencyPlanLinkHandlerService);

View File

@ -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<string, string>): CoreContentLinksAction[] {
return [{
action: (siteId: string): void => {
CoreNavigator.navigateToSitePath(
'/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME,
{ params: { userId: params.userid }, siteId },
);
},
}];
}
/**
* @inheritdoc
*/
async isEnabled(siteId: string): Promise<boolean> {
// Handler is disabled if all competency features are disabled.
const disabled = await AddonCompetency.allCompetenciesDisabled(siteId);
return !disabled;
}
}
export const AddonCompetencyPlansLinkHandler = makeSingleton(AddonCompetencyPlansLinkHandlerService);

View File

@ -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<boolean> {
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<void> {
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;
};

View File

@ -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<string, string>): CoreContentLinksAction[] {
return [{
action: (siteId: string): void => {
CoreNavigator.navigateToSitePath(
'/' + AddonCompetencyMainMenuHandlerService.PAGE_NAME + '/summary/' + params.id,
{ siteId },
);
},
}];
}
/**
* @inheritdoc
*/
async isEnabled(siteId: string): Promise<boolean> {
// Handler is disabled if all competency features are disabled.
const disabled = await AddonCompetency.allCompetenciesDisabled(siteId);
return !disabled;
}
}
export const AddonCompetencyUserCompetencyLinkHandler = makeSingleton(AddonCompetencyUserCompetencyLinkHandlerService);

View File

@ -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<boolean> {
return true;
}
/**
* @inheritdoc
*/
async isEnabledForUser(user: CoreUserProfile, courseId?: number): Promise<boolean> {
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);

View File

@ -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.

View File

@ -86,14 +86,14 @@ export class CoreSortedDelegate<
const handler = this.enabledHandlers[name];
const data = <DisplayType> 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);

View File

@ -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,

View File

@ -450,7 +450,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
promises.push(Promise.resolve(getFunction!.call(handler, courseWithOptions)).then((data) => {
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<CoreCourseOpt
handlersToDisplay.sort((
a: CoreCourseOptionsHandlerToDisplay | CoreCourseOptionsMenuHandlerToDisplay,
b: CoreCourseOptionsHandlerToDisplay | CoreCourseOptionsMenuHandlerToDisplay,
) => (b.priority || 0) - (a.priority || 0));
) => b.priority! - a.priority!);
return handlersToDisplay;
}

View File

@ -183,13 +183,13 @@ export class CoreFileUploaderDelegateService extends CoreDelegate<CoreFileUpload
}
const data: CoreFileUploaderHandlerDataToReturn = handler.getData();
data.priority = handler.priority;
data.priority = handler.priority || 0;
data.mimetypes = supportedMimetypes;
handlers.push(data);
}
// Sort them by priority.
handlers.sort((a, b) => (a.priority || 0) <= (b.priority || 0) ? 1 : -1);
handlers.sort((a, b) => a.priority! <= b.priority! ? 1 : -1);
return handlers;
}

View File

@ -311,7 +311,7 @@ export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler
}));
// Sort them by priority.
userData.handlers.sort((a, b) => (b.priority || 0) - (a.priority || 0));
userData.handlers.sort((a, b) => b.priority! - a.priority!);
userData.loaded = true;
userData.observable.next(userData.handlers);
}