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 @@
- {{ 'core.course.contents' | translate }} - {{ handler.data.title || translate }} + {{ 'core.course.contents' | translate }} + {{ handler.data.title | translate }}
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 }}

-
- - -

+ +
+ +

{{ handler.data.title | 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 }} + + + + + + + + + + + + + + + + +

+

{{ '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",