MOBILE-3629 coursecompletion: Implement coursecompletion feature
parent
211e15d59c
commit
70809d79ec
|
@ -20,6 +20,7 @@ import { AddonFilterModule } from './filter/filter.module';
|
||||||
import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module';
|
import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module';
|
||||||
import { AddonBadgesModule } from './badges/badges.module';
|
import { AddonBadgesModule } from './badges/badges.module';
|
||||||
import { AddonCalendarModule } from './calendar/calendar.module';
|
import { AddonCalendarModule } from './calendar/calendar.module';
|
||||||
|
import { AddonCourseCompletionModule } from './coursecompletion/coursecompletion.module';
|
||||||
import { AddonNotificationsModule } from './notifications/notifications.module';
|
import { AddonNotificationsModule } from './notifications/notifications.module';
|
||||||
import { AddonMessageOutputModule } from './messageoutput/messageoutput.module';
|
import { AddonMessageOutputModule } from './messageoutput/messageoutput.module';
|
||||||
import { AddonMessagesModule } from './messages/messages.module';
|
import { AddonMessagesModule } from './messages/messages.module';
|
||||||
|
@ -35,6 +36,7 @@ import { AddonRemoteThemesModule } from './remotethemes/remotethemes.module';
|
||||||
AddonBadgesModule,
|
AddonBadgesModule,
|
||||||
AddonBlogModule,
|
AddonBlogModule,
|
||||||
AddonCalendarModule,
|
AddonCalendarModule,
|
||||||
|
AddonCourseCompletionModule,
|
||||||
AddonMessagesModule,
|
AddonMessagesModule,
|
||||||
AddonPrivateFilesModule,
|
AddonPrivateFilesModule,
|
||||||
AddonFilterModule,
|
AddonFilterModule,
|
||||||
|
|
|
@ -29,21 +29,14 @@ export class AddonBlockCompletionStatusHandlerService extends CoreBlockBaseHandl
|
||||||
blockName = 'completionstatus';
|
blockName = 'completionstatus';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the data needed to render the block.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param block The block to render.
|
|
||||||
* @param contextLevel The context where the block will be used.
|
|
||||||
* @param instanceId The instance ID associated with the context level.
|
|
||||||
* @return Data or promise resolved with the data.
|
|
||||||
*/
|
*/
|
||||||
getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData {
|
getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData {
|
||||||
// @todo
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: 'addon.block_completionstatus.pluginname',
|
title: 'addon.block_completionstatus.pluginname',
|
||||||
class: 'addon-block-completion-status',
|
class: 'addon-block-completion-status',
|
||||||
component: CoreBlockOnlyTitleComponent,
|
component: CoreBlockOnlyTitleComponent,
|
||||||
link: 'AddonCourseCompletionReportPage',
|
link: 'coursecompletion',
|
||||||
linkParams: {
|
linkParams: {
|
||||||
courseId: instanceId,
|
courseId: instanceId,
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,22 +29,17 @@ export class AddonBlockSelfCompletionHandlerService extends CoreBlockBaseHandler
|
||||||
blockName = 'selfcompletion';
|
blockName = 'selfcompletion';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the data needed to render the block.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param block The block to render.
|
|
||||||
* @param contextLevel The context where the block will be used.
|
|
||||||
* @param instanceId The instance ID associated with the context level.
|
|
||||||
* @return Data or promise resolved with the data.
|
|
||||||
*/
|
*/
|
||||||
getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData {
|
getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData {
|
||||||
// @todo
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: 'addon.block_selfcompletion.pluginname',
|
title: 'addon.block_selfcompletion.pluginname',
|
||||||
class: 'addon-block-self-completion',
|
class: 'addon-block-self-completion',
|
||||||
component: CoreBlockOnlyTitleComponent,
|
component: CoreBlockOnlyTitleComponent,
|
||||||
link: 'AddonCourseCompletionReportPage',
|
link: 'coursecompletion',
|
||||||
linkParams: { courseId: instanceId },
|
linkParams: {
|
||||||
|
courseId: instanceId,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
|
||||||
|
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
|
||||||
|
import { AddonCourseCompletionReportPage } from './pages/report/report';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: AddonCourseCompletionReportPage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CoreSharedModule,
|
||||||
|
CoreCommentsComponentsModule,
|
||||||
|
CoreTagComponentsModule,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
declarations: [
|
||||||
|
AddonCourseCompletionReportPage,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonCourseCompletionLazyModule {}
|
|
@ -12,33 +12,42 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
|
||||||
// @todo import { AddonCourseCompletionCourseOptionHandler } from './services/course-option-handler';
|
import { Routes } from '@angular/router';
|
||||||
// @todo import { AddonCourseCompletionUserHandler } from './services/user-handler';
|
import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module';
|
||||||
// @todo import { AddonCourseCompletionComponentsModule } from './components/components.module';
|
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||||
// @todo import { CoreCourseOptionsDelegate } from '@features/course/services/options-delegate';
|
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||||
// @todo import { CoreUserDelegate } from '@features/user/services/user-delegate';
|
import { CoreUserDelegate } from '@features/user/services/user-delegate';
|
||||||
|
import { AddonCourseCompletionProvider } from './services/coursecompletion';
|
||||||
|
import { AddonCourseCompletionCourseOptionHandler } from './services/handlers/course-option';
|
||||||
|
import { AddonCourseCompletionUserHandler } from './services/handlers/user';
|
||||||
|
|
||||||
|
export const ADDON_COURSECOMPLETION_SERVICES: Type<unknown>[] = [
|
||||||
|
AddonCourseCompletionProvider,
|
||||||
|
];
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'coursecompletion',
|
||||||
|
loadChildren: () => import('./coursecompletion-lazy.module').then(m => m.AddonCourseCompletionLazyModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
// AddonCourseCompletionComponentsModule,
|
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||||
|
CoreCourseIndexRoutingModule.forChild({ children: routes }),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// AddonCourseCompletionCourseOptionHandler,
|
{
|
||||||
// AddonCourseCompletionUserHandler,
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
deps: [],
|
||||||
|
useFactory: () => async () => {
|
||||||
|
CoreUserDelegate.registerHandler(AddonCourseCompletionUserHandler.instance);
|
||||||
|
CoreCourseOptionsDelegate.registerHandler(AddonCourseCompletionCourseOptionHandler.instance);
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AddonCourseCompletionModule {
|
export class AddonCourseCompletionModule {}
|
||||||
|
|
||||||
/* @todo constructor(
|
|
||||||
courseOptionsDelegate: CoreCourseOptionsDelegate,
|
|
||||||
courseOptionHandler: AddonCourseCompletionCourseOptionHandler,
|
|
||||||
userDelegate: CoreUserDelegate,
|
|
||||||
userHandler: AddonCourseCompletionUserHandler,
|
|
||||||
) {
|
|
||||||
// Register handlers.
|
|
||||||
courseOptionsDelegate.registerHandler(courseOptionHandler);
|
|
||||||
userDelegate.registerHandler(userHandler);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>{{ 'addon.coursecompletion.coursecompletion' | translate }}</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!completionLoaded" (ionRefresh)="refreshCompletion($event.target)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="completionLoaded">
|
||||||
|
<ion-card *ngIf="completion && tracked">
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ 'addon.coursecompletion.status' | translate }}</h2>
|
||||||
|
<p>{{ statusText! | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ 'addon.coursecompletion.required' | translate }}</h2>
|
||||||
|
<p *ngIf="completion.aggregation === 1">{{ 'addon.coursecompletion.criteriarequiredall' | translate }}</p>
|
||||||
|
<p *ngIf="completion.aggregation === 2">{{ 'addon.coursecompletion.criteriarequiredany' | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
<ion-card *ngIf="completion && tracked">
|
||||||
|
<ion-item-divider>
|
||||||
|
<ion-label>{{ 'addon.coursecompletion.requiredcriteria' | translate }}</ion-label>
|
||||||
|
</ion-item-divider>
|
||||||
|
<ion-item class="ion-hide-md-up ion-text-wrap" *ngFor="let criteria of completion.completions">
|
||||||
|
<ion-label>
|
||||||
|
<h2><core-format-text clean="true" [text]="criteria.details.criteria" [filter]="false"></core-format-text></h2>
|
||||||
|
<p><core-format-text clean="true" [text]="criteria.details.requirement" [filter]="false"></core-format-text></p>
|
||||||
|
</ion-label>
|
||||||
|
<strong slot="end">{{ criteria.status }}</strong>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-hide-md-down ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<ion-row>
|
||||||
|
<ion-col><strong>{{ 'addon.coursecompletion.criteriagroup' | translate }}</strong></ion-col>
|
||||||
|
<ion-col><strong>{{ 'addon.coursecompletion.criteria' | translate }}</strong></ion-col>
|
||||||
|
<ion-col><strong>{{ 'addon.coursecompletion.requirement' | translate }}</strong></ion-col>
|
||||||
|
<ion-col><strong>{{ 'addon.coursecompletion.status' | translate }}</strong></ion-col>
|
||||||
|
<ion-col><strong>{{ 'addon.coursecompletion.complete' | translate }}</strong></ion-col>
|
||||||
|
<ion-col><strong>{{ 'addon.coursecompletion.completiondate' | translate }}</strong></ion-col>
|
||||||
|
</ion-row>
|
||||||
|
<ion-row *ngFor="let criteria of completion.completions">
|
||||||
|
<ion-col>
|
||||||
|
<core-format-text clean="true" [text]="criteria.title" [filter]="false"></core-format-text>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<core-format-text clean="true" [text]="criteria.details.criteria" [filter]="false"></core-format-text>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<core-format-text clean="true" [text]="criteria.details.requirement" [filter]="false"></core-format-text>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<core-format-text [text]="criteria.details.status" [filter]="false"></core-format-text>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>{{ criteria.status }}</ion-col>
|
||||||
|
<ion-col *ngIf="criteria.timecompleted">
|
||||||
|
{{ criteria.timecompleted * 1000 | coreFormatDate :'strftimedatetimeshort' }}
|
||||||
|
</ion-col>
|
||||||
|
<ion-col *ngIf="!criteria.timecompleted"></ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
<ion-card *ngIf="showSelfComplete && tracked">
|
||||||
|
<ion-item-divider>
|
||||||
|
<ion-label>{{ 'addon.coursecompletion.manualselfcompletion' | translate }}</ion-label>
|
||||||
|
</ion-item-divider>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>
|
||||||
|
<ion-button expand="block" (click)="completeCourse()">
|
||||||
|
{{ 'addon.coursecompletion.completecourse' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ion-card class="core-warning-card" *ngIf="!tracked">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||||
|
<ion-label>{{ 'addon.coursecompletion.nottracked' | translate }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,112 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import {
|
||||||
|
AddonCourseCompletion,
|
||||||
|
AddonCourseCompletionCourseCompletionStatus,
|
||||||
|
} from '@addons/coursecompletion/services/coursecompletion';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the course completion report.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-course-completion-report',
|
||||||
|
templateUrl: 'report.html',
|
||||||
|
})
|
||||||
|
export class AddonCourseCompletionReportPage implements OnInit {
|
||||||
|
|
||||||
|
protected courseId!: number;
|
||||||
|
protected userId!: number;
|
||||||
|
|
||||||
|
completionLoaded = false;
|
||||||
|
completion?: AddonCourseCompletionCourseCompletionStatus;
|
||||||
|
showSelfComplete = false;
|
||||||
|
tracked = true; // Whether completion is tracked.
|
||||||
|
statusText?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
|
||||||
|
this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getCurrentSiteUserId();
|
||||||
|
|
||||||
|
if (!this.userId) {
|
||||||
|
this.userId = CoreSites.getCurrentSiteUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchCompletion().finally(() => {
|
||||||
|
this.completionLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch compleiton data.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchCompletion(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.completion = await AddonCourseCompletion.getCompletion(this.courseId, this.userId);
|
||||||
|
|
||||||
|
this.statusText = AddonCourseCompletion.getCompletedStatusText(this.completion);
|
||||||
|
this.showSelfComplete = AddonCourseCompletion.canMarkSelfCompleted(this.userId, this.completion);
|
||||||
|
|
||||||
|
this.tracked = true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error && error.errorcode == 'notenroled') {
|
||||||
|
// Not enrolled error, probably a teacher.
|
||||||
|
this.tracked = false;
|
||||||
|
} else {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'addon.coursecompletion.couldnotloadreport', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh completion data on PTR.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher instance.
|
||||||
|
*/
|
||||||
|
async refreshCompletion(refresher?: IonRefresher): Promise<void> {
|
||||||
|
await AddonCourseCompletion.invalidateCourseCompletion(this.courseId, this.userId).finally(() => {
|
||||||
|
this.fetchCompletion().finally(() => {
|
||||||
|
refresher?.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark course as completed.
|
||||||
|
*/
|
||||||
|
async completeCourse(): Promise<void> {
|
||||||
|
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AddonCourseCompletion.markCourseAsSelfCompleted(this.courseId);
|
||||||
|
|
||||||
|
await this.refreshCompletion();
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
} finally {
|
||||||
|
modal.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
// (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 { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonCourseCompletion } from '../coursecompletion';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to inject an option into the course main menu.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonCourseCompletionCourseOptionHandlerService implements CoreCourseOptionsHandler {
|
||||||
|
|
||||||
|
name = 'AddonCourseCompletion';
|
||||||
|
priority = 200;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return AddonCourseCompletion.isPluginViewEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async isEnabledForCourse(courseId: number, accessData: CoreCourseAccess): Promise<boolean> {
|
||||||
|
if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) {
|
||||||
|
return false; // Not enabled for guests.
|
||||||
|
}
|
||||||
|
|
||||||
|
const courseEnabled = await AddonCourseCompletion.isPluginViewEnabledForCourse(courseId);
|
||||||
|
// If is not enabled in the course, is not enabled for the user.
|
||||||
|
if (!courseEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AddonCourseCompletion.isPluginViewEnabledForUser(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getDisplayData(): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData> {
|
||||||
|
return {
|
||||||
|
title: 'addon.coursecompletion.completionmenuitem',
|
||||||
|
class: 'addon-coursecompletion-course-handler',
|
||||||
|
page: 'coursecompletion',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async invalidateEnabledForCourse(courseId: number): Promise<void> {
|
||||||
|
await AddonCourseCompletion.invalidateCourseCompletion(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async prefetch(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise<void> {
|
||||||
|
try {
|
||||||
|
await AddonCourseCompletion.getCompletion(course.id, undefined, {
|
||||||
|
getFromCache: false,
|
||||||
|
emergencyCache: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error && error.errorcode == 'notenroled') {
|
||||||
|
// Not enrolled error, probably a teacher. Ignore error.
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
export const AddonCourseCompletionCourseOptionHandler = makeSingleton(AddonCourseCompletionCourseOptionHandlerService);
|
|
@ -0,0 +1,98 @@
|
||||||
|
// (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, CoreUserProvider } 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 { CoreEvents } from '@singletons/events';
|
||||||
|
import { AddonCourseCompletion } from '../coursecompletion';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile course completion handler.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonCourseCompletionUserHandlerService implements CoreUserProfileHandler {
|
||||||
|
|
||||||
|
name = 'AddonCourseCompletion';
|
||||||
|
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
||||||
|
priority = 200;
|
||||||
|
|
||||||
|
protected enabledCache = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
CoreEvents.on(CoreEvents.LOGOUT, () => {
|
||||||
|
this.enabledCache = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => {
|
||||||
|
const cacheKey = data.userId + '-' + data.courseId;
|
||||||
|
|
||||||
|
delete this.enabledCache[cacheKey];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return AddonCourseCompletion.isPluginViewEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async isEnabledForUser(user: CoreUserProfile, courseId?: number): Promise<boolean> {
|
||||||
|
if (!courseId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const courseEnabled = await AddonCourseCompletion.isPluginViewEnabledForCourse(courseId);
|
||||||
|
// If is not enabled in the course, is not enabled for the user.
|
||||||
|
if (!courseEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = user.id + '-' + courseId;
|
||||||
|
if (typeof this.enabledCache[cacheKey] !== 'undefined') {
|
||||||
|
return this.enabledCache[cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabled = await AddonCourseCompletion.isPluginViewEnabledForUser(courseId, user.id);
|
||||||
|
this.enabledCache[cacheKey] = enabled;
|
||||||
|
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getDisplayData(): CoreUserProfileHandlerData {
|
||||||
|
return {
|
||||||
|
icon: 'fas-tasks',
|
||||||
|
title: 'addon.coursecompletion.coursecompletion',
|
||||||
|
class: 'addon-coursecompletion-handler',
|
||||||
|
action: (event, user, courseId): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
CoreNavigator.navigateToSitePath('/coursecompletion', {
|
||||||
|
params: { courseId, userId: user.id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
export const AddonCourseCompletionUserHandler = makeSingleton(AddonCourseCompletionUserHandlerService);
|
|
@ -117,6 +117,7 @@ import { CoreSitePluginsAssignSubmissionComponent } from '@features/siteplugins/
|
||||||
// Import addon providers. Do not import database module because it causes circular dependencies.
|
// Import addon providers. Do not import database module because it causes circular dependencies.
|
||||||
import { ADDON_BADGES_SERVICES } from '@addons/badges/badges.module';
|
import { ADDON_BADGES_SERVICES } from '@addons/badges/badges.module';
|
||||||
import { ADDON_CALENDAR_SERVICES } from '@addons/calendar/calendar.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';
|
// @todo import { ADDON_COMPETENCY_SERVICES } from '@addons/competency/competency.module';
|
||||||
import { ADDON_MESSAGEOUTPUT_SERVICES } from '@addons/messageoutput/messageoutput.module';
|
import { ADDON_MESSAGEOUTPUT_SERVICES } from '@addons/messageoutput/messageoutput.module';
|
||||||
import { ADDON_MESSAGES_SERVICES } from '@addons/messages/messages.module';
|
import { ADDON_MESSAGES_SERVICES } from '@addons/messages/messages.module';
|
||||||
|
@ -281,6 +282,7 @@ export class CoreCompileProvider {
|
||||||
...extraProviders,
|
...extraProviders,
|
||||||
...ADDON_BADGES_SERVICES,
|
...ADDON_BADGES_SERVICES,
|
||||||
...ADDON_CALENDAR_SERVICES,
|
...ADDON_CALENDAR_SERVICES,
|
||||||
|
...ADDON_COURSECOMPLETION_SERVICES,
|
||||||
// @todo ...ADDON_COMPETENCY_SERVICES,
|
// @todo ...ADDON_COMPETENCY_SERVICES,
|
||||||
...ADDON_MESSAGEOUTPUT_SERVICES,
|
...ADDON_MESSAGEOUTPUT_SERVICES,
|
||||||
...ADDON_MESSAGES_SERVICES,
|
...ADDON_MESSAGES_SERVICES,
|
||||||
|
|
|
@ -820,9 +820,22 @@ export class CoreUserProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CoreUser = makeSingleton(CoreUserProvider);
|
export const CoreUser = makeSingleton(CoreUserProvider);
|
||||||
|
|
||||||
|
declare module '@singletons/events' {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Augment CoreEventsData interface with events specific to this service.
|
||||||
|
*
|
||||||
|
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||||
|
*/
|
||||||
|
export interface CoreEventsData {
|
||||||
|
[CoreUserProvider.PROFILE_REFRESHED]: CoreUserProfileRefreshedData;
|
||||||
|
[CoreUserProvider.PROFILE_PICTURE_UPDATED]: CoreUserProfilePictureUpdatedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data passed to PROFILE_REFRESHED event.
|
* Data passed to PROFILE_REFRESHED event.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -309,7 +309,8 @@ img[alt] {
|
||||||
// Activity modules
|
// Activity modules
|
||||||
.core-module-icon {
|
.core-module-icon {
|
||||||
--size: 24px;
|
--size: 24px;
|
||||||
width: auto;
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
max-width: var(--size);
|
max-width: var(--size);
|
||||||
max-height: var(--size);
|
max-height: var(--size);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue