diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html
index f890b24f3..2cf256aee 100644
--- a/src/core/course/pages/section/section.html
+++ b/src/core/course/pages/section/section.html
@@ -18,8 +18,8 @@
diff --git a/src/core/courses/pages/course-preview/course-preview.html b/src/core/courses/pages/course-preview/course-preview.html
index ec071512d..0e9916dd7 100644
--- a/src/core/courses/pages/course-preview/course-preview.html
+++ b/src/core/courses/pages/course-preview/course-preview.html
@@ -50,12 +50,12 @@
{{ 'core.course.contents' | translate }}
-
+
diff --git a/src/core/user/lang/en.json b/src/core/user/lang/en.json
index d5390b8ac..0c5fb6889 100644
--- a/src/core/user/lang/en.json
+++ b/src/core/user/lang/en.json
@@ -15,6 +15,8 @@
"lastname": "Surname",
"manager": "Manager",
"newpicture": "New picture",
+ "noparticipants": "No participants found for this course.",
+ "participants": "Participants",
"phone1": "Phone",
"phone2": "Mobile phone",
"roles": "Roles",
diff --git a/src/core/user/pages/participants/participants.html b/src/core/user/pages/participants/participants.html
new file mode 100644
index 000000000..67cc2b8b6
--- /dev/null
+++ b/src/core/user/pages/participants/participants.html
@@ -0,0 +1,30 @@
+
+
+ {{ 'core.user.participants' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+ 0">
+
+
+
+
+
+ {{ 'core.lastaccess' | translate }}: {{ participant.lastaccess * 1000 | coreFormatDate:"dfmediumdate"}}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/core/user/pages/participants/participants.module.ts b/src/core/user/pages/participants/participants.module.ts
new file mode 100644
index 000000000..ceaea818a
--- /dev/null
+++ b/src/core/user/pages/participants/participants.module.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 { NgModule } from '@angular/core';
+import { IonicPageModule } 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 { CoreUserParticipantsPage } from './participants';
+
+@NgModule({
+ declarations: [
+ CoreUserParticipantsPage,
+ ],
+ imports: [
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ CorePipesModule,
+ IonicPageModule.forChild(CoreUserParticipantsPage),
+ TranslateModule.forChild()
+ ],
+})
+export class CoreUserParticipantsPageModule {}
diff --git a/src/core/user/pages/participants/participants.ts b/src/core/user/pages/participants/participants.ts
new file mode 100644
index 000000000..c82d69703
--- /dev/null
+++ b/src/core/user/pages/participants/participants.ts
@@ -0,0 +1,104 @@
+// (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, ViewChild } from '@angular/core';
+import { IonicPage, Content, NavParams } from 'ionic-angular';
+import { CoreUserProvider } from '../../providers/user';
+import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
+import { CoreSplitViewComponent } from '../../../../components/split-view/split-view';
+
+/**
+ * Page that displays the list of course participants.
+ */
+@IonicPage({segment: 'core-user-participants'})
+@Component({
+ selector: 'page-core-user-participants',
+ templateUrl: 'participants.html',
+})
+export class CoreUserParticipantsPage {
+ @ViewChild(Content) content: Content;
+ @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
+
+ courseId: number;
+ participantId: number;
+ participants = [];
+ canLoadMore = false;
+ participantsLoaded = false;
+
+ constructor(private userProvider: CoreUserProvider, private domUtils: CoreDomUtilsProvider,
+ navParams: NavParams) {
+ this.courseId = navParams.get('courseId');
+ }
+
+ /**
+ * View loaded.
+ */
+ ionViewDidLoad(): void {
+ // Get first participants.
+ this.fetchData(true).then(() => {
+ if (!this.participantId && this.splitviewCtrl.isOn() && this.participants.length > 0) {
+ // Take first and load it.
+ this.gotoParticipant(this.participants[0].id);
+ }
+ // Add log in Moodle.
+ this.userProvider.logView(this.courseId);
+ }).finally(() => {
+ this.participantsLoaded = true;
+ });
+ }
+
+ /**
+ * Fetch all the data required for the view.
+ *
+ * @param {boolean} [refresh] Empty events array first.
+ * @return {Promise} Resolved when done.
+ */
+ fetchData(refresh: boolean = false): Promise {
+ const firstToGet = refresh ? 0 : this.participants.length;
+
+ return this.userProvider.getParticipants(this.courseId, firstToGet).then((data) => {
+ if (refresh) {
+ this.participants = data.participants;
+ } else {
+ this.participants = this.participants.concat(data.participants);
+ }
+ this.canLoadMore = data.canLoadMore;
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error loading participants');
+ this.canLoadMore = false; // Set to false to prevent infinite calls with infinite-loading.
+ });
+ }
+
+ /**
+ * Refresh data.
+ *
+ * @param {any} refresher Refresher.
+ */
+ refreshParticipants(refresher: any): void {
+ this.userProvider.invalidateParticipantsList(this.courseId).finally(() => {
+ this.fetchData(true).finally(() => {
+ refresher.complete();
+ });
+ });
+ }
+
+ /**
+ * Navigate to a particular user profile.
+ * @param {number} userId User Id where to navigate.
+ */
+ gotoParticipant(userId: number): void {
+ this.participantId = userId;
+ this.splitviewCtrl.push('CoreUserProfilePage', {userId: userId, courseId: this.courseId});
+ }
+}
diff --git a/src/core/user/providers/course-option-handler.ts b/src/core/user/providers/course-option-handler.ts
new file mode 100644
index 000000000..44c03a75c
--- /dev/null
+++ b/src/core/user/providers/course-option-handler.ts
@@ -0,0 +1,117 @@
+// (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 { NavController } from 'ionic-angular';
+import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '../../course/providers/options-delegate';
+import { CoreCourseProvider } from '../../course/providers/course';
+import { CoreUserProvider } from './user';
+import { CoreLoginHelperProvider } from '../../login/providers/helper';
+
+/**
+ * Course nav handler.
+ */
+@Injectable()
+export class CoreUserParticipantsCourseOptionHandler implements CoreCourseOptionsHandler {
+ name = 'AddonParticipants';
+ priority = 600;
+
+ constructor(private userProvider: CoreUserProvider, private loginHelper: CoreLoginHelperProvider) {}
+
+ /**
+ * 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 {
+ if (navOptions && typeof navOptions.participants != 'undefined') {
+ // No need to invalidate anything.
+ return Promise.resolve();
+ }
+
+ return this.userProvider.invalidateParticipantsList(courseId);
+ }
+
+ /**
+ * Check if the handler is enabled on a site level.
+ *
+ * @return {boolean} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return true;
+ }
+
+ /**
+ * Whether or not the handler is enabled for a certain course.
+ * For perfomance reasons, do NOT call WebServices in here, call them in shouldDisplayForCourse.
+ *
+ * @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.
+ }
+
+ if (navOptions && typeof navOptions.participants != 'undefined') {
+ return navOptions.participants;
+ }
+
+ // Assume it's enabled for now, further checks will be done in shouldDisplayForCourse.
+ return true;
+ }
+
+ /**
+ * Whether or not the handler should be displayed for a course. If not implemented, assume it's true.
+ *
+ * @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.
+ */
+ shouldDisplayForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise {
+ if (navOptions && typeof navOptions.participants != 'undefined') {
+ return navOptions.participants;
+ }
+
+ return this.userProvider.isPluginEnabledForCourse(courseId);
+ }
+
+ /**
+ * Returns the data needed to render the handler.
+ *
+ * @return {CoreMainMenuHandlerData} Data needed to render the handler.
+ */
+ getDisplayData(): CoreCourseOptionsHandlerData {
+ return {
+ icon: 'person',
+ title: 'core.user.participants',
+ class: 'core-user-participants-handler',
+ action: (course: any): void => {
+ const pageParams = {
+ courseId: course.id
+ };
+ // Always use redirect to make it the new history root (to avoid "loops" in history).
+ this.loginHelper.redirect('CoreUserParticipantsPage', pageParams);
+ }
+ };
+ }
+}
diff --git a/src/core/user/providers/participants-link-handler.ts b/src/core/user/providers/participants-link-handler.ts
new file mode 100644
index 000000000..d2d46c695
--- /dev/null
+++ b/src/core/user/providers/participants-link-handler.ts
@@ -0,0 +1,74 @@
+// (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 { CoreContentLinksHandlerBase } from '../../../core/contentlinks/classes/base-handler';
+import { CoreContentLinksAction } from '../../../core/contentlinks/providers/delegate';
+import { CoreLoginHelperProvider } from '../../../core/login/providers/helper';
+import { CoreUserProvider } from './user';
+
+/**
+ * Handler to treat links to user participants page.
+ */
+@Injectable()
+export class CoreUserParticipantsLinkHandler extends CoreContentLinksHandlerBase {
+ name = 'AddonParticipants';
+ featureName = '$mmCoursesDelegate_mmaParticipants';
+ pattern = /\/user\/index\.php/;
+
+ constructor(private userProvider: CoreUserProvider, private loginHelper: CoreLoginHelperProvider) {
+ super();
+ }
+
+ /**
+ * Get the list of actions for a link (url).
+ *
+ * @param {string[]} siteIds List of sites the URL belongs to.
+ * @param {string} url The URL to treat.
+ * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+ * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions.
+ */
+ getActions(siteIds: string[], url: string, params: any, courseId?: number):
+ CoreContentLinksAction[] | Promise {
+ courseId = parseInt(params.id, 10) || courseId;
+
+ return [{
+ action: (siteId, navCtrl?): void => {
+ // Always use redirect to make it the new history root (to avoid "loops" in history).
+ this.loginHelper.redirect('AddonParticipantsListPage', {courseId: courseId}, siteId);
+ }
+ }];
+ }
+
+ /**
+ * Check if the handler is enabled for a certain site (site + user) and a URL.
+ * If not defined, defaults to true.
+ *
+ * @param {string} siteId The site ID.
+ * @param {string} url The URL to treat.
+ * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+ * @return {boolean|Promise} Whether the handler is enabled for the URL and site.
+ */
+ isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise {
+ courseId = parseInt(params.id, 10) || courseId;
+
+ if (!courseId || url.indexOf('/grade/report/') != -1) {
+ return false;
+ }
+
+ return this.userProvider.isPluginEnabledForCourse(courseId, siteId);
+ }
+}
diff --git a/src/core/user/providers/user.ts b/src/core/user/providers/user.ts
index b2502f788..b6af8f055 100644
--- a/src/core/user/providers/user.ts
+++ b/src/core/user/providers/user.ts
@@ -23,6 +23,7 @@ import { CoreUtilsProvider } from '../../../providers/utils/utils';
*/
@Injectable()
export class CoreUserProvider {
+ static PARTICIPANTS_LIST_LIMIT = 50; // Max of participants to retrieve in each WS call.
static PROFILE_REFRESHED = 'CoreUserProfileRefreshed';
static PROFILE_PICTURE_UPDATED = 'CoreUserProfilePictureUpdated';
protected ROOT_CACHE_KEY = 'mmUser:';
@@ -106,6 +107,59 @@ export class CoreUserProvider {
return Promise.all(promises);
}
+ /**
+ * Get participants for a certain course.
+ *
+ * @param {number} courseId ID of the course.
+ * @param {number} limitFrom Position of the first participant to get.
+ * @param {number} limitNumber Number of participants to get.
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise to be resolved when the participants are retrieved.
+ */
+ getParticipants(courseId: number, limitFrom: number = 0, limitNumber: number = CoreUserProvider.PARTICIPANTS_LIST_LIMIT,
+ siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ this.logger.debug(`Get participants for course '${courseId}' starting at '${limitFrom}'`);
+
+ const data = {
+ courseid: courseId,
+ options: [
+ {
+ name: 'limitfrom',
+ value: limitFrom
+ },
+ {
+ name: 'limitnumber',
+ value: limitNumber
+ },
+ {
+ name: 'sortby',
+ value: 'siteorder'
+ }
+ ]
+ }, preSets = {
+ cacheKey: this.getParticipantsListCacheKey(courseId)
+ };
+
+ return site.read('core_enrol_get_enrolled_users', data, preSets).then((users) => {
+ const canLoadMore = users.length >= limitNumber;
+ this.storeUsers(users, siteId);
+
+ return { participants: users, canLoadMore: canLoadMore };
+ });
+ });
+ }
+
+ /**
+ * Get cache key for participant list WS calls.
+ *
+ * @param {number} courseId Course ID.
+ * @return {string} Cache key.
+ */
+ protected getParticipantsListCacheKey(courseId: number): string {
+ return this.ROOT_CACHE_KEY + 'list:' + courseId;
+ }
+
/**
* Get user profile. The type of profile retrieved depends on the params.
*
@@ -218,6 +272,59 @@ export class CoreUserProvider {
});
}
+ /**
+ * Invalidates participant list for a certain course.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved when the list is invalidated.
+ */
+ invalidateParticipantsList(courseId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKey(this.getParticipantsListCacheKey(courseId));
+ });
+ }
+
+ /**
+ * Check if course participants is disabled in a certain site.
+ *
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise.
+ */
+ isParticipantsDisabled(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return this.isParticipantsDisabledInSite(site);
+ });
+ }
+
+ /**
+ * Check if course participants is disabled in a certain site.
+ *
+ * @param {CoreSite} [site] Site. If not defined, use current site.
+ * @return {boolean} Whether it's disabled.
+ */
+ isParticipantsDisabledInSite(site?: any): boolean {
+ site = site || this.sitesProvider.getCurrentSite();
+
+ return site.isFeatureDisabled('$mmCoursesDelegate_mmaParticipants');
+ }
+
+ /**
+ * Returns whether or not the participants addon is enabled for a certain course.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
+ */
+ isPluginEnabledForCourse(courseId: number, siteId?: string): Promise {
+ if (!courseId) {
+ return Promise.reject(null);
+ }
+
+ // Retrieving one participant will fail if browsing users is disabled by capabilities.
+ return this.utils.promiseWorks(this.getParticipants(courseId, 0, 1, siteId));
+ }
+
/**
* Check if update profile picture is disabled in a certain site.
*
@@ -248,6 +355,17 @@ export class CoreUserProvider {
return this.sitesProvider.getCurrentSite().write('core_user_view_user_profile', params);
}
+ /**
+ * Log Participants list view in Moodle.
+ * @param {number} courseId Course ID.
+ * @return {Promise} Promise resolved when done.
+ */
+ logParticipantsView(courseId?: number): Promise {
+ return this.sitesProvider.getCurrentSite().write('core_user_view_user_list', {
+ courseid: courseId
+ });
+ }
+
/**
* Store user basic information in local DB to be retrieved if the WS call fails.
*
@@ -268,4 +386,23 @@ export class CoreUserProvider {
return site.getDb().insertOrUpdateRecord(this.USERS_TABLE, userRecord, { id: userId });
});
}
+
+ /**
+ * Store users basic information in local DB.
+ *
+ * @param {any[]} users Users to store. Fields stored: id, fullname, profileimageurl.
+ * @param {string} [siteId] ID of the site. If not defined, use current site.
+ * @return {Promise} Promise resolve when the user is stored.
+ */
+ storeUsers(users: any[], siteId?: string): Promise {
+ const promises = [];
+
+ users.forEach((user) => {
+ if (typeof user.id != 'undefined') {
+ promises.push(this.storeUser(user.id, user.fullname, user.profileimageurl, siteId));
+ }
+ });
+
+ return Promise.all(promises);
+ }
}
diff --git a/src/core/user/user.module.ts b/src/core/user/user.module.ts
index 4f86bb899..6a4fc9730 100644
--- a/src/core/user/user.module.ts
+++ b/src/core/user/user.module.ts
@@ -22,6 +22,9 @@ import { CoreEventsProvider } from '../../providers/events';
import { CoreSitesProvider } from '../../providers/sites';
import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate';
import { CoreUserProfileLinkHandler } from './providers/user-link-handler';
+import { CoreUserParticipantsCourseOptionHandler } from './providers/course-option-handler';
+import { CoreUserParticipantsLinkHandler } from './providers/participants-link-handler';
+import { CoreCourseOptionsDelegate } from '../course/providers/options-delegate';
@NgModule({
declarations: [
@@ -34,15 +37,22 @@ import { CoreUserProfileLinkHandler } from './providers/user-link-handler';
CoreUserProfileMailHandler,
CoreUserProvider,
CoreUserHelperProvider,
- CoreUserProfileLinkHandler
+ CoreUserProfileLinkHandler,
+ CoreUserParticipantsCourseOptionHandler,
+ CoreUserParticipantsLinkHandler
]
})
export class CoreUserModule {
constructor(userDelegate: CoreUserDelegate, userProfileMailHandler: CoreUserProfileMailHandler,
eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, userProvider: CoreUserProvider,
- contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler) {
+ contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler,
+ courseOptionHandler: CoreUserParticipantsCourseOptionHandler, linkHandler: CoreUserParticipantsLinkHandler,
+ courseOptionsDelegate: CoreCourseOptionsDelegate) {
+
userDelegate.registerHandler(userProfileMailHandler);
+ courseOptionsDelegate.registerHandler(courseOptionHandler);
contentLinksDelegate.registerHandler(userLinkHandler);
+ contentLinksDelegate.registerHandler(linkHandler);
eventsProvider.on(CoreEventsProvider.USER_DELETED, (data) => {
// Search for userid in params.
diff --git a/src/lang/en.json b/src/lang/en.json
index 32ec7dfd3..6ca3ca4cf 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -96,6 +96,7 @@
"info": "Info",
"ios": "iOS",
"labelsep": ": ",
+ "lastaccess": "Last access",
"lastdownloaded": "Last downloaded",
"lastmodified": "Last modified",
"lastsync": "Last synchronization",