diff --git a/src/addon/coursecompletion/components/components.module.ts b/src/addon/coursecompletion/components/components.module.ts
new file mode 100644
index 000000000..140b2b3ff
--- /dev/null
+++ b/src/addon/coursecompletion/components/components.module.ts
@@ -0,0 +1,45 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { CommonModule } from '@angular/common';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { CorePipesModule } from '@pipes/pipes.module';
+import { AddonCourseCompletionReportComponent } from './report/report';
+
+@NgModule({
+ declarations: [
+ AddonCourseCompletionReportComponent
+ ],
+ imports: [
+ CommonModule,
+ IonicModule,
+ TranslateModule.forChild(),
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ CorePipesModule
+ ],
+ providers: [
+ ],
+ exports: [
+ AddonCourseCompletionReportComponent
+ ],
+ entryComponents: [
+ AddonCourseCompletionReportComponent
+ ]
+})
+export class AddonCourseCompletionComponentsModule {}
diff --git a/src/addon/coursecompletion/components/report/addon-course-completion-report.html b/src/addon/coursecompletion/components/report/addon-course-completion-report.html
new file mode 100644
index 000000000..af292dc8e
--- /dev/null
+++ b/src/addon/coursecompletion/components/report/addon-course-completion-report.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+ {{ criteria.status }}
+
+
+
+ {{ 'addon.coursecompletion.criteriagroup' | translate }}
+ {{ 'addon.coursecompletion.criteria' | translate }}
+ {{ 'addon.coursecompletion.requirement' | translate }}
+ {{ 'addon.coursecompletion.status' | translate }}
+ {{ 'addon.coursecompletion.complete' | translate }}
+ {{ 'addon.coursecompletion.completiondate' | translate }}
+
+
+
+
+
+
+ {{ criteria.status }}
+ {{ criteria.timecompleted | coreToLocaleString }}
+
+
+
+
+
+ {{ 'addon.coursecompletion.manualselfcompletion' | translate }}
+
+
+
+
+
+
diff --git a/src/addon/coursecompletion/components/report/report.ts b/src/addon/coursecompletion/components/report/report.ts
new file mode 100644
index 000000000..69583770f
--- /dev/null
+++ b/src/addon/coursecompletion/components/report/report.ts
@@ -0,0 +1,96 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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, Input, OnInit } from '@angular/core';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { AddonCourseCompletionProvider } from '../../providers/coursecompletion';
+
+/**
+ * Component that displays the course completion report.
+ */
+@Component({
+ selector: 'addon-course-completion-report',
+ templateUrl: 'addon-course-completion-report.html',
+})
+export class AddonCourseCompletionReportComponent implements OnInit {
+ @Input() courseId: number;
+ @Input() userId: number;
+
+ completionLoaded = false;
+ completion: any;
+ showSelfComplete: boolean;
+
+ constructor(
+ private sitesProvider: CoreSitesProvider,
+ private domUtils: CoreDomUtilsProvider,
+ private courseCompletionProvider: AddonCourseCompletionProvider) {}
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit(): void {
+ if (!this.userId) {
+ this.userId = this.sitesProvider.getCurrentSiteUserId();
+ }
+
+ this.fetchCompletion().finally(() => {
+ this.completionLoaded = true;
+ });
+ }
+
+ /**
+ * Fetch compleiton data.
+ *
+ * @return {Promise} Promise resolved when done.
+ */
+ protected fetchCompletion(): Promise {
+ return this.courseCompletionProvider.getCompletion(this.courseId, this.userId).then((completion) => {
+
+ completion.statusText = this.courseCompletionProvider.getCompletedStatusText(completion);
+
+ this.completion = completion;
+ this.showSelfComplete = this.courseCompletionProvider.canMarkSelfCompleted(this.userId, completion);
+ }).catch((message) => {
+ this.domUtils.showErrorModalDefault(message, 'addon.coursecompletion.couldnotloadreport', true);
+ });
+ }
+
+ /**
+ * Refresh completion data on PTR.
+ *
+ * @param {any} [refresher] Refresher instance.
+ */
+ refreshCompletion(refresher?: any): void {
+ this.courseCompletionProvider.invalidateCourseCompletion(this.courseId, this.userId).finally(() => {
+ this.fetchCompletion().finally(() => {
+ refresher && refresher.complete();
+ });
+ });
+ }
+
+ /**
+ * Mark course as completed.
+ */
+ completeCourse(): void {
+ const modal = this.domUtils.showModalLoading('core.sending', true);
+ this.courseCompletionProvider.markCourseAsSelfCompleted(this.courseId).then(() => {
+ return this.refreshCompletion();
+ }).catch((message) => {
+ this.domUtils.showErrorModal(message);
+ }).finally(() => {
+ modal.dismiss();
+ });
+ }
+}
diff --git a/src/addon/coursecompletion/coursecompletion.module.ts b/src/addon/coursecompletion/coursecompletion.module.ts
new file mode 100644
index 000000000..db3699489
--- /dev/null
+++ b/src/addon/coursecompletion/coursecompletion.module.ts
@@ -0,0 +1,42 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { AddonCourseCompletionProvider } from './providers/coursecompletion';
+import { AddonCourseCompletionCourseOptionHandler } from './providers/course-option-handler';
+import { AddonCourseCompletionUserHandler } from './providers/user-handler';
+import { AddonCourseCompletionComponentsModule } from './components/components.module';
+import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
+import { CoreUserDelegate } from '@core/user/providers/user-delegate';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ AddonCourseCompletionComponentsModule
+ ],
+ providers: [
+ AddonCourseCompletionProvider,
+ AddonCourseCompletionCourseOptionHandler,
+ AddonCourseCompletionUserHandler
+ ]
+})
+export class AddonCourseCompletionModule {
+ constructor(courseOptionsDelegate: CoreCourseOptionsDelegate, courseOptionHandler: AddonCourseCompletionCourseOptionHandler,
+ userDelegate: CoreUserDelegate, userHandler: AddonCourseCompletionUserHandler) {
+ // Register handlers.
+ courseOptionsDelegate.registerHandler(courseOptionHandler);
+ userDelegate.registerHandler(userHandler);
+ }
+}
diff --git a/src/addon/coursecompletion/lang/en.json b/src/addon/coursecompletion/lang/en.json
new file mode 100644
index 000000000..69547f49a
--- /dev/null
+++ b/src/addon/coursecompletion/lang/en.json
@@ -0,0 +1,21 @@
+{
+ "complete": "Complete",
+ "completecourse": "Complete course",
+ "completed": "Completed",
+ "completiondate": "Completion date",
+ "couldnotloadreport": "Could not load the course completion report. Please try again later.",
+ "coursecompletion": "Course completion",
+ "criteria": "Criteria",
+ "criteriagroup": "Criteria group",
+ "criteriarequiredall": "All criteria below are required.",
+ "criteriarequiredany": "Any criteria below are required.",
+ "inprogress": "In progress",
+ "manualselfcompletion": "Manual self completion",
+ "notyetstarted": "Not yet started",
+ "pending": "Pending",
+ "required": "Required",
+ "requiredcriteria": "Required criteria",
+ "requirement": "Requirement",
+ "status": "Status",
+ "viewcoursereport": "View course report"
+}
\ No newline at end of file
diff --git a/src/addon/coursecompletion/pages/report/report.html b/src/addon/coursecompletion/pages/report/report.html
new file mode 100644
index 000000000..eb63cf5a2
--- /dev/null
+++ b/src/addon/coursecompletion/pages/report/report.html
@@ -0,0 +1,6 @@
+
+
+ {{ 'addon.coursecompletion.coursecompletion' | translate }}
+
+
+
diff --git a/src/addon/coursecompletion/pages/report/report.module.ts b/src/addon/coursecompletion/pages/report/report.module.ts
new file mode 100644
index 000000000..83f0a10e6
--- /dev/null
+++ b/src/addon/coursecompletion/pages/report/report.module.ts
@@ -0,0 +1,33 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { AddonCourseCompletionComponentsModule } from '../../components/components.module';
+import { AddonCourseCompletionReportPage } from './report';
+
+@NgModule({
+ declarations: [
+ AddonCourseCompletionReportPage,
+ ],
+ imports: [
+ CoreDirectivesModule,
+ AddonCourseCompletionComponentsModule,
+ IonicPageModule.forChild(AddonCourseCompletionReportPage),
+ TranslateModule.forChild()
+ ],
+})
+export class AddonModFolderIndexPageModule {}
diff --git a/src/addon/coursecompletion/pages/report/report.ts b/src/addon/coursecompletion/pages/report/report.ts
new file mode 100644
index 000000000..d874ae161
--- /dev/null
+++ b/src/addon/coursecompletion/pages/report/report.ts
@@ -0,0 +1,35 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 } from '@angular/core';
+import { IonicPage, NavParams } from 'ionic-angular';
+
+/**
+ * Page that displays the course completion report.
+ */
+@IonicPage({ segment: 'addon-course-completion-report' })
+@Component({
+ selector: 'page-addon-course-completion-report',
+ templateUrl: 'report.html',
+})
+export class AddonCourseCompletionReportPage {
+
+ courseId: number;
+ userId: number;
+
+ constructor(navParams: NavParams) {
+ this.courseId = navParams.get('courseId');
+ this.userId = navParams.get('userId');
+ }
+}
diff --git a/src/addon/coursecompletion/providers/course-option-handler.ts b/src/addon/coursecompletion/providers/course-option-handler.ts
new file mode 100644
index 000000000..52e2ed2cd
--- /dev/null
+++ b/src/addon/coursecompletion/providers/course-option-handler.ts
@@ -0,0 +1,89 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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, Injector } from '@angular/core';
+import { AddonCourseCompletionProvider } from './coursecompletion';
+import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate';
+import { AddonCourseCompletionReportComponent } from '../components/report/report';
+
+/**
+ * Handler to inject an option into the course main menu.
+ */
+@Injectable()
+export class AddonCourseCompletionCourseOptionHandler implements CoreCourseOptionsHandler {
+ name = 'AddonCourseCompletion';
+ priority = 200;
+
+ constructor(private courseCompletionProvider: AddonCourseCompletionProvider) {}
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ * @return {boolean|Promise} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return this.courseCompletionProvider.isPluginViewEnabled();
+ }
+
+ /**
+ * Whether or not the handler is enabled for a certain course.
+ *
+ * @param {number} courseId The course ID.
+ * @param {any} accessData Access type and data. Default, guest, ...
+ * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+ * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+ * @return {boolean|Promise} True or promise resolved with true if enabled.
+ */
+ isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise {
+ if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) {
+ return false; // Not enabled for guests.
+ }
+
+ return this.courseCompletionProvider.isPluginViewEnabledForCourse(courseId).then((courseEnabled) => {
+ // If is not enabled in the course, is not enabled for the user.
+ if (!courseEnabled) {
+ return false;
+ }
+
+ // Check if the user can see his own report, teachers can't.
+ return this.courseCompletionProvider.isPluginViewEnabledForUser(courseId);
+ });
+ }
+
+ /**
+ * Returns the data needed to render the handler.
+ *
+ * @param {number} courseId The course ID.
+ * @return {CoreCourseOptionsHandlerData} Data.
+ */
+ getDisplayData?(injector: Injector, courseId: number): CoreCourseOptionsHandlerData {
+ return {
+ title: 'addon.coursecompletion.coursecompletion',
+ class: 'addon-coursecompletion-course-handler',
+ component: AddonCourseCompletionReportComponent,
+ };
+ }
+
+ /**
+ * Should invalidate the data to determine if the handler is enabled for a certain course.
+ *
+ * @param {number} courseId The course ID.
+ * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+ * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+ * @return {Promise} Promise resolved when done.
+ */
+ invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise {
+ return this.courseCompletionProvider.invalidateCourseCompletion(courseId);
+ }
+}
diff --git a/src/addon/coursecompletion/providers/coursecompletion.ts b/src/addon/coursecompletion/providers/coursecompletion.ts
new file mode 100644
index 000000000..a490f2b95
--- /dev/null
+++ b/src/addon/coursecompletion/providers/coursecompletion.ts
@@ -0,0 +1,221 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { CoreLoggerProvider } from '@providers/logger';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+import { CoreCoursesProvider } from '@core/courses/providers/courses';
+
+/**
+ * Service to handle course completion.
+ */
+@Injectable()
+export class AddonCourseCompletionProvider {
+
+ protected ROOT_CACHE_KEY = 'mmaCourseCompletion:';
+ protected logger;
+
+ constructor(logger: CoreLoggerProvider,
+ private sitesProvider: CoreSitesProvider,
+ private coursesProvider: CoreCoursesProvider,
+ private utils: CoreUtilsProvider) {
+ this.logger = logger.getInstance('AddonCourseCompletionProvider');
+ }
+
+ /**
+ * Returns whether or not the user can mark a course as self completed.
+ * It can if it's configured in the course and it hasn't been completed yet.
+ *
+ * @param {number} userId User ID.
+ * @param {any} completion Course completion.
+ * @return {boolean} True if user can mark course as self completed, false otherwise.
+ */
+ canMarkSelfCompleted(userId: number, completion: any): boolean {
+ let selfCompletionActive = false,
+ alreadyMarked = false;
+
+ if (this.sitesProvider.getCurrentSiteUserId() != userId) {
+ return false;
+ }
+
+ completion.completions.forEach((criteria) => {
+ if (criteria.type === 1) {
+ // Self completion criteria found.
+ selfCompletionActive = true;
+ alreadyMarked = criteria.complete;
+ }
+ });
+
+ return selfCompletionActive && !alreadyMarked;
+ }
+
+ /**
+ * Get completed status text. The language code returned is meant to be translated.
+ *
+ * @param {any} completion Course completion.
+ * @return {string} Language code of the text to show.
+ */
+ getCompletedStatusText(completion: any): string {
+ if (completion.completed) {
+ return 'addon.coursecompletion.completed';
+ } else {
+ // Let's calculate status.
+ let hasStarted = false;
+ completion.completions.forEach((criteria) => {
+ if (criteria.timecompleted || criteria.complete) {
+ hasStarted = true;
+ }
+ });
+ if (hasStarted) {
+ return 'addon.coursecompletion.inprogress';
+ } else {
+ return 'addon.coursecompletion.notyetstarted';
+ }
+ }
+ }
+
+ /**
+ * Get course completion status for a certain course and user.
+ *
+ * @param {number} courseId Course ID.
+ * @param {number} [userId] User ID. If not defined, use current user.
+ * @param {any} [preSets] Presets to use when calling the WebService.
+ * @param {string} [siteId] Site ID. If not defined, use current site.
+ * @return {Promise} Promise to be resolved when the completion is retrieved.
+ */
+ getCompletion(courseId: number, userId?: number, preSets?: any, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ userId = userId || site.getUserId();
+ preSets = preSets || {};
+
+ this.logger.debug('Get completion for course ' + courseId + ' and user ' + userId);
+
+ const data = {
+ courseid: courseId,
+ userid: userId
+ };
+
+ preSets.cacheKey = this.getCompletionCacheKey(courseId, userId);
+
+ return site.read('core_completion_get_course_completion_status', data, preSets).then((data) => {
+ if (data.completionstatus) {
+ return data.completionstatus;
+ }
+
+ return Promise.reject(null);
+ });
+ });
+ }
+
+ /**
+ * Get cache key for get completion WS calls.
+ *
+ * @param {number} courseId Course ID.
+ * @param {number} useIid User ID.
+ * @return {string} Cache key.
+ */
+ protected getCompletionCacheKey(courseId: number, userId: number): string {
+ return this.ROOT_CACHE_KEY + 'view:' + courseId + ':' + userId;
+ }
+
+ /**
+ * Invalidates view course completion WS call.
+ *
+ * @param {number} courseId Course ID.
+ * @param {number} [userId] User ID. If not defined, use current user.
+ * @return {Promise} Promise resolved when the list is invalidated.
+ */
+ invalidateCourseCompletion(courseId: number, userId?: number): Promise {
+ userId = userId || this.sitesProvider.getCurrentSiteUserId();
+
+ return this.sitesProvider.getCurrentSite().invalidateWsCacheForKey(this.getCompletionCacheKey(courseId, userId));
+ }
+
+ /**
+ * Returns whether or not the view course completion plugin is enabled for the current site.
+ *
+ * @return {boolean} True if plugin enabled, false otherwise.
+ */
+ isPluginViewEnabled(): boolean {
+ return this.sitesProvider.isLoggedIn();
+ }
+
+ /**
+ * Returns whether or not the view course completion plugin is enabled for a certain course.
+ *
+ * @param {number} courseId Course ID.
+ * @param {boolean} [preferCache=true] True if shouldn't call WS if data is cached, false otherwise.
+ * @return {Promise} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
+ */
+ isPluginViewEnabledForCourse(courseId: number, preferCache: boolean = true): Promise {
+ if (!courseId) {
+ return Promise.reject(null);
+ }
+
+ return this.coursesProvider.getUserCourse(courseId, preferCache).then((course) => {
+ return !(course && typeof course.enablecompletion != 'undefined' && course.enablecompletion == 0);
+ });
+ }
+
+ /**
+ * Returns whether or not the view course completion plugin is enabled for a certain user.
+ *
+ * @param {number} courseId Course ID.
+ * @param {number} [userId] User ID. If not defined, use current user.
+ * @return {Promise} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
+ */
+ isPluginViewEnabledForUser(courseId: number, userId?: number): Promise {
+ // Disable emergency cache to be able to detect that the plugin has been disabled (WS will fail).
+ const preSets: any = {
+ emergencyCache: 0
+ };
+
+ return this.getCompletion(courseId, userId, preSets).then(() => {
+ return true;
+ }).catch((error) => {
+ if (this.utils.isWebServiceError(error)) {
+ // The WS returned an error, plugin is not enabled.
+ return false;
+ } else {
+ // Not a WS error. Check if we have a cached value.
+ preSets.omitExpires = true;
+
+ return this.getCompletion(courseId, userId, preSets).then(() => {
+ return true;
+ }).catch(() => {
+ return false;
+ });
+ }
+ });
+ }
+
+ /**
+ * Mark a course as self completed.
+ *
+ * @param {number} courseId Course ID.
+ * @return {Promise} Resolved on success.
+ */
+ markCourseAsSelfCompleted(courseId: number): Promise {
+ const params = {
+ courseid: courseId
+ };
+
+ return this.sitesProvider.getCurrentSite().write('core_completion_mark_course_self_completed', params).then((response) => {
+ if (!response.status) {
+ return Promise.reject(null);
+ }
+ });
+ }
+}
diff --git a/src/addon/coursecompletion/providers/user-handler.ts b/src/addon/coursecompletion/providers/user-handler.ts
new file mode 100644
index 000000000..8faf49ccd
--- /dev/null
+++ b/src/addon/coursecompletion/providers/user-handler.ts
@@ -0,0 +1,101 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate';
+import { CoreEventsProvider } from '@providers/events';
+import { CoreUserProvider } from '@core/user/providers/user';
+import { AddonCourseCompletionProvider } from './coursecompletion';
+
+/**
+ * Profile course completion handler.
+ */
+@Injectable()
+export class AddonCourseCompletionUserHandler implements CoreUserProfileHandler {
+ name = 'AddonCourseCompletion';
+ type = CoreUserDelegate.TYPE_NEW_PAGE;
+ priority = 200;
+
+ protected enabledCache = {};
+
+ constructor(eventsProvider: CoreEventsProvider, private courseCompletionProvider: AddonCourseCompletionProvider) {
+ eventsProvider.on(CoreEventsProvider.LOGOUT, () => {
+ this.enabledCache = {};
+ });
+ eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, (data) => {
+ const cacheKey = data.userId + '-' + data.courseId;
+
+ delete this.enabledCache[cacheKey];
+ });
+ }
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ * @return {boolean|Promise} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return this.courseCompletionProvider.isPluginViewEnabled();
+ }
+
+ /**
+ * Check if handler is enabled for this user in this context.
+ *
+ * @param {any} user User to check.
+ * @param {number} courseId Course ID.
+ * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+ * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+ * @return {boolean|Promise} Promise resolved with true if enabled, resolved with false otherwise.
+ */
+ isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise {
+ if (!courseId) {
+ return false;
+ }
+
+ return this.courseCompletionProvider.isPluginViewEnabledForCourse(courseId).then((courseEnabled) => {
+ // 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];
+ }
+
+ return this.courseCompletionProvider.isPluginViewEnabledForUser(courseId, user.id).then((enabled) => {
+ this.enabledCache[cacheKey] = enabled;
+
+ return enabled;
+ });
+ });
+ }
+
+ /**
+ * Returns the data needed to render the handler.
+ *
+ * @return {CoreUserProfileHandlerData} Data needed to render the handler.
+ */
+ getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData {
+ return {
+ icon: 'checkbox-outline',
+ title: 'addon.coursecompletion.coursecompletion',
+ class: 'addon-coursecompletion-handler',
+ action: (event, navCtrl, user, courseId): void => {
+ event.preventDefault();
+ event.stopPropagation();
+ navCtrl.push('AddonCourseCompletionReportPage', {courseId: courseId, userId: user.id });
+ }
+ };
+ }
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index b63339826..ca2323574 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -77,6 +77,7 @@ import { CoreCommentsModule } from '@core/comments/comments.module';
import { AddonBadgesModule } from '@addon/badges/badges.module';
import { AddonCalendarModule } from '@addon/calendar/calendar.module';
import { AddonCompetencyModule } from '@addon/competency/competency.module';
+import { AddonCourseCompletionModule } from '@addon/coursecompletion/coursecompletion.module';
import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofilefield.module';
import { AddonFilesModule } from '@addon/files/files.module';
import { AddonModAssignModule } from '@addon/mod/assign/assign.module';
@@ -184,6 +185,7 @@ export const CORE_PROVIDERS: any[] = [
AddonBadgesModule,
AddonCalendarModule,
AddonCompetencyModule,
+ AddonCourseCompletionModule,
AddonUserProfileFieldModule,
AddonFilesModule,
AddonModAssignModule,