From 9ecbdd22b8d7ca8e68e64838c19715c0e490013a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 12 Nov 2020 09:18:44 +0100 Subject: [PATCH] MOBILE-3592 user: Implement user services --- src/app/core/user/services/user.db.ts | 95 ++ src/app/core/user/services/user.helper.ts | 67 ++ src/app/core/user/services/user.offline.ts | 83 ++ src/app/core/user/services/user.ts | 1046 ++++++++++++++++++++ 4 files changed, 1291 insertions(+) create mode 100644 src/app/core/user/services/user.db.ts create mode 100644 src/app/core/user/services/user.helper.ts create mode 100644 src/app/core/user/services/user.offline.ts create mode 100644 src/app/core/user/services/user.ts diff --git a/src/app/core/user/services/user.db.ts b/src/app/core/user/services/user.db.ts new file mode 100644 index 000000000..8f19b47fd --- /dev/null +++ b/src/app/core/user/services/user.db.ts @@ -0,0 +1,95 @@ +// (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 { CoreSiteSchema, registerSiteSchema } from '@services/sites'; +import { CoreUserBasicData } from './user'; + +/** + * Database variables for CoreUser service. + */ +export const USERS_TABLE_NAME = 'users'; +export const SITE_SCHEMA: CoreSiteSchema = { + name: 'CoreUserProvider', + version: 1, + canBeCleared: [USERS_TABLE_NAME], + tables: [ + { + name: USERS_TABLE_NAME, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true, + }, + { + name: 'fullname', + type: 'TEXT', + }, + { + name: 'profileimageurl', + type: 'TEXT', + }, + ], + }, + ], +}; + +/** + * Database variables for CoreUserOffline service. + */ +export const PREFERENCES_TABLE_NAME = 'user_preferences'; +export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = { + name: 'CoreUserOfflineProvider', + version: 1, + tables: [ + { + name: PREFERENCES_TABLE_NAME, + columns: [ + { + name: 'name', + type: 'TEXT', + unique: true, + notNull: true, + }, + { + name: 'value', + type: 'TEXT', + }, + { + name: 'onlinevalue', + type: 'TEXT', + }, + ], + }, + ], +}; + +/** + * Data stored in DB for users. + */ +export type CoreUserDBRecord = CoreUserBasicData; + +/** + * Structure of offline user preferences. + */ +export type CoreUserPreferenceDBRecord = { + name: string; + value: string; + onlinevalue: string; +}; + +export const initCoreUserDB = (): void => { + registerSiteSchema(SITE_SCHEMA); + registerSiteSchema(OFFLINE_SITE_SCHEMA); +}; diff --git a/src/app/core/user/services/user.helper.ts b/src/app/core/user/services/user.helper.ts new file mode 100644 index 000000000..d41a3e372 --- /dev/null +++ b/src/app/core/user/services/user.helper.ts @@ -0,0 +1,67 @@ +// (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 { makeSingleton, Translate } from '@singletons/core.singletons'; +import { CoreUserRole } from './user'; + +/** + * Service that provides some features regarding users information. + */ +@Injectable({ + providedIn: 'root', +}) +export class CoreUserHelperProvider { + + /** + * Formats a user address, concatenating address, city and country. + * + * @param address Address. + * @param city City. + * @param country Country. + * @return Formatted address. + */ + formatAddress(address: string, city: string, country: string): string { + const separator = Translate.instance.instant('core.listsep'); + let values = [address, city, country]; + + values = values.filter((value) => value?.length > 0); + + return values.join(separator + ' '); + } + + /** + * Formats a user role list, translating and concatenating them. + * + * @param roles List of user roles. + * @return The formatted roles. + */ + formatRoleList(roles?: CoreUserRole[]): string { + if (!roles || roles.length <= 0) { + return ''; + } + + const separator = Translate.instance.instant('core.listsep'); + + return roles.map((value) => { + const translation = Translate.instance.instant('core.user.' + value.shortname); + + return translation.indexOf('core.user.') < 0 ? translation : value.shortname; + }).join(separator + ' '); + } + +} + +export class CoreUserHelper extends makeSingleton(CoreUserHelperProvider) {} diff --git a/src/app/core/user/services/user.offline.ts b/src/app/core/user/services/user.offline.ts new file mode 100644 index 000000000..fbc298cc6 --- /dev/null +++ b/src/app/core/user/services/user.offline.ts @@ -0,0 +1,83 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; + +import { CoreSites } from '@services/sites'; +import { makeSingleton } from '@singletons/core.singletons'; +import { PREFERENCES_TABLE_NAME, CoreUserPreferenceDBRecord } from './user.db'; + +/** + * Service to handle offline user preferences. + */ +@Injectable({ + providedIn: 'root', +}) +export class CoreUserOfflineProvider { + + /** + * Get preferences that were changed offline. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with list of preferences. + */ + async getChangedPreferences(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return site.getDb().getRecordsSelect(PREFERENCES_TABLE_NAME, 'value != onlineValue'); + } + + /** + * Get an offline preference. + * + * @param name Name of the preference. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the preference, rejected if not found. + */ + async getPreference(name: string, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return site.getDb().getRecord(PREFERENCES_TABLE_NAME, { name }); + } + + /** + * Set an offline preference. + * + * @param name Name of the preference. + * @param value Value of the preference. + * @param onlineValue Online value of the preference. If undefined, preserve previously stored value. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async setPreference(name: string, value: string, onlineValue?: string, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + if (typeof onlineValue == 'undefined') { + const preference = await this.getPreference(name, site.id); + + onlineValue = preference.onlinevalue; + } + + const record: CoreUserPreferenceDBRecord = { + name, + value, + onlinevalue: onlineValue, + }; + + await site.getDb().insertRecord(PREFERENCES_TABLE_NAME, record); + } + +} + +export class CoreUserOffline extends makeSingleton(CoreUserOfflineProvider) {} diff --git a/src/app/core/user/services/user.ts b/src/app/core/user/services/user.ts new file mode 100644 index 000000000..7baae9152 --- /dev/null +++ b/src/app/core/user/services/user.ts @@ -0,0 +1,1046 @@ +// (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 { CoreApp } from '@services/app'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreUserOffline } from './user.offline'; +import { CoreLogger } from '@singletons/logger'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { makeSingleton } from '@singletons/core.singletons'; +import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; +import { CoreError } from '@classes/errors/error'; +import { USERS_TABLE_NAME, CoreUserDBRecord } from './user.db'; + +const ROOT_CACHE_KEY = 'mmUser:'; + +/** + * Service to provide user functionalities. + */ +@Injectable({ + providedIn: 'root', +}) +export class CoreUserProvider { + + static readonly PARTICIPANTS_LIST_LIMIT = 50; // Max of participants to retrieve in each WS call. + static readonly PROFILE_REFRESHED = 'CoreUserProfileRefreshed'; + static readonly PROFILE_PICTURE_UPDATED = 'CoreUserProfilePictureUpdated'; + + protected logger: CoreLogger; + + constructor() { + this.logger = CoreLogger.getInstance('CoreUserProvider'); + } + + /** + * Check if WS to search participants is available in site. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: whether it's available. + * @since 3.8 + */ + async canSearchParticipants(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.canSearchParticipantsInSite(site); + } + + /** + * Check if WS to search participants is available in site. + * + * @param site Site. If not defined, current site. + * @return Whether it's available. + * @since 3.8 + */ + canSearchParticipantsInSite(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!site?.wsAvailable('core_enrol_search_users'); + } + + /** + * Change the given user profile picture. + * + * @param draftItemId New picture draft item id. + * @param userId User ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolve with the new profileimageurl + */ + async changeProfilePicture(draftItemId: number, userId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const params: CoreUserUpdatePictureWSParams = { + draftitemid: draftItemId, + delete: false, + userid: userId, + }; + + const result = await site.write('core_user_update_picture', params); + + if (!result.success) { + return Promise.reject(null); + } + + return result.profileimageurl!; + } + + /** + * Store user basic information in local DB to be retrieved if the WS call fails. + * + * @param userId User ID. + * @param siteId ID of the site. If not defined, use current site. + * @return Promise resolve when the user is deleted. + */ + async deleteStoredUser(userId: number, siteId?: string): Promise { + if (isNaN(userId)) { + throw new CoreError('Invalid user ID.'); + } + + const site = await CoreSites.instance.getSite(siteId); + + await Promise.all([ + this.invalidateUserCache(userId, site.getId()), + site.getDb().deleteRecords(USERS_TABLE_NAME, { id: userId }), + ]); + } + + /** + * Get participants for a certain course. + * + * @param courseId ID of the course. + * @param limitFrom Position of the first participant to get. + * @param limitNumber Number of participants to get. + * @param siteId Site Id. If not defined, use current site. + * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @return Promise resolved when the participants are retrieved. + */ + async getParticipants( + courseId: number, + limitFrom: number = 0, + limitNumber: number = CoreUserProvider.PARTICIPANTS_LIST_LIMIT, + siteId?: string, + ignoreCache?: boolean, + ): Promise<{participants: CoreUserParticipant[]; canLoadMore: boolean}> { + + const site = await CoreSites.instance.getSite(siteId); + + this.logger.debug(`Get participants for course '${courseId}' starting at '${limitFrom}'`); + + const params: CoreEnrolGetEnrolledUsersWSParams = { + courseid: courseId, + options: [ + { + name: 'limitfrom', + value: String(limitFrom), + }, + { + name: 'limitnumber', + value: String(limitNumber), + }, + { + name: 'sortby', + value: 'siteorder', + }, + ], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getParticipantsListCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + const users = await site.read('core_enrol_get_enrolled_users', params, preSets); + + const canLoadMore = users.length >= limitNumber; + this.storeUsers(users, siteId); + + return { participants: users, canLoadMore: canLoadMore }; + } + + /** + * Get cache key for participant list WS calls. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getParticipantsListCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'list:' + courseId; + } + + /** + * Get user profile. The type of profile retrieved depends on the params. + * + * @param userId User's ID. + * @param courseId Course ID to get course profile, undefined or 0 to get site profile. + * @param forceLocal True to retrieve the user data from local DB, false to retrieve it from WS. + * @param siteId ID of the site. If not defined, use current site. + * @return Promise resolved with the user data. + */ + async getProfile( + userId: number, + courseId?: number, + forceLocal: boolean = false, + siteId?: string, + ): Promise { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); + + if (forceLocal) { + try { + return await this.getUserFromLocalDb(userId, siteId); + } catch { + return this.getUserFromWS(userId, courseId, siteId); + } + } + + try { + return await this.getUserFromWS(userId, courseId, siteId); + } catch { + return this.getUserFromLocalDb(userId, siteId); + } + } + + /** + * Get cache key for a user WS call. + * + * @param userId User ID. + * @return Cache key. + */ + protected getUserCacheKey(userId: number): string { + return ROOT_CACHE_KEY + 'data:' + userId; + } + + /** + * Get user basic information from local DB. + * + * @param userId User ID. + * @param siteId ID of the site. If not defined, use current site. + * @return Promise resolve when the user is retrieved. + */ + protected async getUserFromLocalDb(userId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return site.getDb().getRecord(USERS_TABLE_NAME, { id: userId }); + } + + /** + * Get user profile from WS. + * + * @param userId User ID. + * @param courseId Course ID to get course profile, undefined or 0 to get site profile. + * @param siteId ID of the site. If not defined, use current site. + * @return Promise resolve when the user is retrieved. + */ + protected async getUserFromWS( + userId: number, + courseId?: number, + siteId?: string, + ): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getUserCacheKey(userId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + let users: CoreUserData[] | CoreUserCourseProfile[] | undefined; + + // Determine WS and data to use. + if (courseId && courseId != site.getSiteHomeId()) { + this.logger.debug(`Get participant with ID '${userId}' in course '${courseId}`); + + const params: CoreUserGetCourseUserProfilesWSParams = { + userlist: [ + { + userid: userId, + courseid: courseId, + }, + ], + }; + + users = await site.read('core_user_get_course_user_profiles', params, preSets); + } else { + this.logger.debug(`Get user with ID '${userId}'`); + + const params: CoreUserGetUsersByFieldWSParams = { + field: 'id', + values: [String(userId)], + }; + + users = await site.read('core_user_get_users_by_field', params, preSets); + } + + if (users.length == 0) { + // Shouldn't happen. + throw new CoreError('Cannot retrieve user info.'); + } + + const user = users[0]; + if (user.country) { + user.country = CoreUtils.instance.getCountryName(user.country); + } + this.storeUser(user.id, user.fullname, user.profileimageurl); + + return user; + } + + /** + * Get a user preference (online or offline). + * + * @param name Name of the preference. + * @param siteId Site Id. If not defined, use current site. + * @return Preference value or null if preference not set. + */ + async getUserPreference(name: string, siteId?: string): Promise { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); + + const preference = await CoreUtils.instance.ignoreErrors(CoreUserOffline.instance.getPreference(name, siteId)); + + if (preference && !CoreApp.instance.isOnline()) { + // Offline, return stored value. + return preference.value; + } + + const wsValue = await this.getUserPreferenceOnline(name, siteId); + + if (!wsValue) { + if (preference) { + // Return the local value. + return preference.value; + } + + throw new CoreError('Preference not found'); + } + + if (preference && preference.value != preference.onlinevalue && preference.onlinevalue == wsValue) { + // Sync is pending for this preference, return stored value. + return preference.value; + } + + await CoreUserOffline.instance.setPreference(name, wsValue, wsValue); + + return wsValue; + } + + /** + * Get cache key for a user preference WS call. + * + * @param name Preference name. + * @return Cache key. + */ + protected getUserPreferenceCacheKey(name: string): string { + return ROOT_CACHE_KEY + 'preference:' + name; + } + + /** + * Get a user preference online. + * + * @param name Name of the preference. + * @param siteId Site Id. If not defined, use current site. + * @return Preference value or null if preference not set. + */ + async getUserPreferenceOnline(name: string, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const params: CoreUserGetUserPreferencesWSParams = { + name, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getUserPreferenceCacheKey(params.name!), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + const result = await site.read('core_user_get_user_preferences', params, preSets); + + return result.preferences[0] ? result.preferences[0].value : null; + } + + /** + * Invalidates user WS calls. + * + * @param userId User ID. + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateUserCache(userId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getUserCacheKey(userId)); + } + + /** + * Invalidates participant list for a certain course. + * + * @param courseId Course ID. + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved when the list is invalidated. + */ + async invalidateParticipantsList(courseId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getParticipantsListCacheKey(courseId)); + } + + /** + * Invalidate user preference. + * + * @param name Name of the preference. + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateUserPreference(name: string, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getUserPreferenceCacheKey(name)); + } + + /** + * Check if course participants is disabled in a certain site. + * + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + async isParticipantsDisabled(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.isParticipantsDisabledInSite(site); + } + + /** + * Check if course participants is disabled in a certain site. + * + * @param site Site. If not defined, use current site. + * @return Whether it's disabled. + */ + isParticipantsDisabledInSite(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!site?.isFeatureDisabled('CoreCourseOptionsDelegate_CoreUserParticipants'); + } + + /** + * Returns whether or not participants is enabled for a certain course. + * + * @param courseId Course ID. + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + */ + isPluginEnabledForCourse(courseId: number, siteId?: string): Promise { + if (!courseId) { + throw new CoreError('Invalid course ID.'); + } + + // Retrieving one participant will fail if browsing users is disabled by capabilities. + return CoreUtils.instance.promiseWorks(this.getParticipants(courseId, 0, 1, siteId)); + } + + /** + * Check if update profile picture is disabled in a certain site. + * + * @param site Site. If not defined, use current site. + * @return True if disabled, false otherwise. + */ + isUpdatePictureDisabledInSite(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!site?.isFeatureDisabled('CoreUserDelegate_picture'); + } + + /** + * Log User Profile View in Moodle. + * + * @param userId User ID. + * @param courseId Course ID. + * @param name Name of the user. + * @return Promise resolved when done. + */ + async logView(userId: number, courseId?: number, name?: string, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const params: CoreUserViewUserProfileWSParams = { + userid: userId, + }; + const wsName = 'core_user_view_user_profile'; + + if (courseId) { + params.courseid = courseId; + } + + // @todo this.pushNotificationsProvider.logViewEvent(userId, name, 'user', wsName, {courseid: courseId}); + + return site.write(wsName, params); + } + + /** + * Log Participants list view in Moodle. + * + * @param courseId Course ID. + * @return Promise resolved when done. + */ + async logParticipantsView(courseId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const params: CoreUserViewUserListWSParams = { + courseid: courseId, + }; + + // @todo this.pushNotificationsProvider.logViewListEvent('user', 'core_user_view_user_list', params); + + return site.write('core_user_view_user_list', params); + } + + /** + * Prefetch user profiles and their images from a certain course. It prevents duplicates. + * + * @param userIds List of user IDs. + * @param courseId Course the users belong to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when prefetched. + */ + async prefetchProfiles(userIds: number[], courseId?: number, siteId?: string): Promise { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); + + if (!siteId) { + return; + } + + const treated: Record = {}; + + await Promise.all(userIds.map(async (userId) => { + if (userId === null) { + return; + } + + userId = Number(userId); // Make sure it's a number. + + // Prevent repeats and errors. + if (isNaN(userId) || treated[userId] || userId <= 0) { + return; + } + + treated[userId] = true; + + try { + const profile = await this.getProfile(userId, courseId, false, siteId); + + if (profile.profileimageurl) { + await CoreFilepool.instance.addToQueueByUrl(siteId!, profile.profileimageurl); + } + } catch (error) { + this.logger.warn(`Ignore error when prefetching user ${userId}`, error); + } + })); + } + + /** + * Prefetch user avatars. It prevents duplicates. + * + * @param entries List of entries that have the images. + * @param propertyName The name of the property that contains the image. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when prefetched. + */ + async prefetchUserAvatars(entries: Record[], propertyName: string, siteId?: string): Promise { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); + + if (!siteId) { + return; + } + + const treated: Record = {}; + + const promises = entries.map(async (entry) => { + const imageUrl = entry[propertyName]; + + if (!imageUrl || treated[imageUrl]) { + // It doesn't have an image or it has already been treated. + return; + } + + treated[imageUrl] = true; + + try { + await CoreFilepool.instance.addToQueueByUrl(siteId!, imageUrl); + } catch (ex) { + this.logger.warn(`Ignore error when prefetching user avatar ${imageUrl}`, entry, ex); + } + }); + + await Promise.all(promises); + } + + /** + * Search participants in a certain course. + * + * @param courseId ID of the course. + * @param search The string to search. + * @param searchAnywhere Whether to find a match anywhere or only at the beginning. + * @param page Page to get. + * @param limitNumber Number of participants to get. + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved when the participants are retrieved. + * @since 3.8 + */ + async searchParticipants( + courseId: number, + search: string, + searchAnywhere: boolean = true, + page: number = 0, + perPage: number = CoreUserProvider.PARTICIPANTS_LIST_LIMIT, + siteId?: string, + ): Promise<{participants: CoreUserData[]; canLoadMore: boolean}> { + const site = await CoreSites.instance.getSite(siteId); + + const params: CoreEnrolSearchUsersWSParams = { + courseid: courseId, + search: search, + searchanywhere: !!searchAnywhere, + page: page, + perpage: perPage, + }; + const preSets: CoreSiteWSPreSets = { + getFromCache: false, // Always try to get updated data. If it fails, it will get it from cache. + }; + + const users = await site.read('core_enrol_search_users', params, preSets); + + const canLoadMore = users.length >= perPage; + this.storeUsers(users, siteId); + + return { participants: users, canLoadMore: canLoadMore }; + } + + /** + * Store user basic information in local DB to be retrieved if the WS call fails. + * + * @param userId User ID. + * @param fullname User full name. + * @param avatar User avatar URL. + * @param siteId ID of the site. If not defined, use current site. + * @return Promise resolve when the user is stored. + */ + protected async storeUser(userId: number, fullname: string, avatar?: string, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const userRecord: CoreUserDBRecord = { + id: userId, + fullname: fullname, + profileimageurl: avatar, + }; + + await site.getDb().insertRecord(USERS_TABLE_NAME, userRecord); + } + + /** + * Store users basic information in local DB. + * + * @param users Users to store. + * @param siteId ID of the site. If not defined, use current site. + * @return Promise resolve when the user is stored. + */ + async storeUsers(users: CoreUserBasicData[], siteId?: string): Promise { + + await Promise.all(users.map((user) => { + if (!user.id || isNaN(Number(user.id))) { + return; + } + + return this.storeUser(Number(user.id), user.fullname, user.profileimageurl, siteId); + })); + } + + /** + * Set a user preference (online or offline). + * + * @param name Name of the preference. + * @param value Value of the preference. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved on success. + */ + async setUserPreference(name: string, value: string, siteId?: string): Promise { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); + + if (!CoreApp.instance.isOnline()) { + // Offline, just update the preference. + return CoreUserOffline.instance.setPreference(name, value); + } + + + try { + // Update the preference in the site. + const preferences = [ + { type: name, value }, + ]; + + await this.updateUserPreferences(preferences, undefined, undefined, siteId); + + // Update preference and invalidate data. + await Promise.all([ + CoreUserOffline.instance.setPreference(name, value, value), + CoreUtils.instance.ignoreErrors(this.invalidateUserPreference(name)), + ]); + } catch (error) { + // Preference not saved online. Update the offline one. + await CoreUserOffline.instance.setPreference(name, value); + } + } + + /** + * Update a preference for a user. + * + * @param name Preference name. + * @param value Preference new value. + * @param userId User ID. If not defined, site's current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if success. + */ + updateUserPreference(name: string, value: string, userId?: number, siteId?: string): Promise { + const preferences = [ + { + type: name, + value: value, + }, + ]; + + return this.updateUserPreferences(preferences, undefined, userId, siteId); + } + + /** + * Update some preferences for a user. + * + * @param preferences List of preferences. + * @param disableNotifications Whether to disable all notifications. Undefined to not update this value. + * @param userId User ID. If not defined, site's current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if success. + */ + async updateUserPreferences( + preferences: { type: string; value: string }[], + disableNotifications?: boolean, + userId?: number, + siteId?: string, + ): Promise { + const site = await CoreSites.instance.getSite(siteId); + + userId = userId || site.getUserId(); + + const params: CoreUserUpdateUserPreferencesWSParams = { + userid: userId, + preferences: preferences, + }; + const preSets: CoreSiteWSPreSets = { + responseExpected: false, + }; + + if (typeof disableNotifications != 'undefined') { + params.emailstop = disableNotifications ? 1 : 0; + } + + await site.write('core_user_update_user_preferences', params, preSets); + } + +} + +export class CoreUser extends makeSingleton(CoreUserProvider) {} + +/** + * Basic data of a user. + */ +export type CoreUserBasicData = { + id: number; // ID of the user. + fullname: string; // The fullname of the user. + profileimageurl?: string; // User image profile URL - big version. +}; + +/** + * User preference. + */ +export type CoreUserPreference = { + name: string; // The name of the preference. + value: string; // The value of the preferenc. +}; + +/** + * User custom profile field. + */ +export type CoreUserProfileField = { + type: string; // The type of the custom field - text field, checkbox... + value: string; // The value of the custom field. + name: string; // The name of the custom field. + shortname: string; // The shortname of the custom field - to be able to build the field class in the code. +}; + +/** + * User group. + */ +export type CoreUserGroup = { + id: number; // Group id. + name: string; // Group name. + description: string; // Group description. + descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). +}; + +/** + * User role. + */ +export type CoreUserRole = { + roleid: number; // Role id. + name: string; // Role name. + shortname: string; // Role shortname. + sortorder: number; // Role sortorder. +}; + +/** + * Basic data of a course the user is enrolled in. + */ +export type CoreUserEnrolledCourse = { + id: number; // Id of the course. + fullname: string; // Fullname of the course. + shortname: string; // Shortname of the course. +}; + +/** + * Common data returned by user_description function. + */ +export type CoreUserData = { + id: number; // ID of the user. + username?: string; // The username. + firstname?: string; // The first name(s) of the user. + lastname?: string; // The family name of the user. + fullname: string; // The fullname of the user. + email?: string; // An email address - allow email as root@localhost. + address?: string; // Postal address. + phone1?: string; // Phone 1. + phone2?: string; // Phone 2. + icq?: string; // Icq number. + skype?: string; // Skype id. + yahoo?: string; // Yahoo id. + aim?: string; // Aim id. + msn?: string; // Msn number. + department?: string; // Department. + institution?: string; // Institution. + idnumber?: string; // An arbitrary ID code number perhaps from the institution. + interests?: string; // User interests (separated by commas). + firstaccess?: number; // First access to the site (0 if never). + lastaccess?: number; // Last access to the site (0 if never). + auth?: string; // Auth plugins include manual, ldap, etc. + suspended?: boolean; // Suspend user account, either false to enable user login or true to disable it. + confirmed?: boolean; // Active user: 1 if confirmed, 0 otherwise. + lang?: string; // Language code such as "en", must exist on server. + calendartype?: string; // Calendar type such as "gregorian", must exist on server. + theme?: string; // Theme name such as "standard", must exist on server. + timezone?: string; // Timezone code such as Australia/Perth, or 99 for default. + mailformat?: number; // Mail format code is 0 for plain text, 1 for HTML etc. + description?: string; // User profile description. + descriptionformat?: number; // Int format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + city?: string; // Home city of the user. + url?: string; // URL of the user. + country?: string; // Home country code of the user, such as AU or CZ. + profileimageurlsmall: string; // User image profile URL - small version. + profileimageurl: string; // User image profile URL - big version. + customfields?: CoreUserProfileField[]; // User custom fields (also known as user profile fields). + preferences?: CoreUserPreference[]; // Users preferences. +}; + +/** + * Data returned by user_summary_exporter. + */ +export type CoreUserSummary = { + id: number; // Id. + email: string; // Email. + idnumber: string; // Idnumber. + phone1: string; // Phone1. + phone2: string; // Phone2. + department: string; // Department. + institution: string; // Institution. + fullname: string; // Fullname. + identity: string; // Identity. + profileurl: string; // Profileurl. + profileimageurl: string; // Profileimageurl. + profileimageurlsmall: string; // Profileimageurlsmall. +}; + +/** + * User data returned by core_enrol_get_enrolled_users WS. + */ +export type CoreUserParticipant = CoreUserBasicData & { + username?: string; // Username policy is defined in Moodle security config. + firstname?: string; // The first name(s) of the user. + lastname?: string; // The family name of the user. + email?: string; // An email address - allow email as root@localhost. + address?: string; // Postal address. + phone1?: string; // Phone 1. + phone2?: string; // Phone 2. + icq?: string; // Icq number. + skype?: string; // Skype id. + yahoo?: string; // Yahoo id. + aim?: string; // Aim id. + msn?: string; // Msn number. + department?: string; // Department. + institution?: string; // Institution. + idnumber?: string; // An arbitrary ID code number perhaps from the institution. + interests?: string; // User interests (separated by commas). + firstaccess?: number; // First access to the site (0 if never). + lastaccess?: number; // Last access to the site (0 if never). + lastcourseaccess?: number; // Last access to the course (0 if never). + description?: string; // User profile description. + descriptionformat?: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + city?: string; // Home city of the user. + url?: string; // URL of the user. + country?: string; // Home country code of the user, such as AU or CZ. + profileimageurlsmall?: string; // User image profile URL - small version. + customfields?: CoreUserProfileField[]; // User custom fields (also known as user profil fields). + groups?: CoreUserGroup[]; // User groups. + roles?: CoreUserRole[]; // User roles. + preferences?: CoreUserPreference[]; // User preferences. + enrolledcourses?: CoreUserEnrolledCourse[]; // Courses where the user is enrolled. +}; + +/** + * User data returned by core_user_get_course_user_profiles WS. + */ +export type CoreUserCourseProfile = CoreUserData & { + groups?: CoreUserGroup[]; // User groups. + roles?: CoreUserRole[]; // User roles. + enrolledcourses?: CoreUserEnrolledCourse[]; // Courses where the user is enrolled. +}; + +/** + * Params of core_user_update_picture WS. + */ +type CoreUserUpdatePictureWSParams = { + draftitemid: number; // Id of the user draft file to use as image. + delete?: boolean; // If we should delete the user picture. + userid?: number; // Id of the user, 0 for current user. +}; + +/** + * Data returned by core_user_update_picture WS. + */ +type CoreUserUpdatePictureWSResponse = { + success: boolean; // True if the image was updated, false otherwise. + profileimageurl?: string; // New profile user image url. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of core_enrol_get_enrolled_users WS. + */ +type CoreEnrolGetEnrolledUsersWSParams = { + courseid: number; // Course id. + options?: { + name: string; // Option name. + value: string; // Option value. + }[]; +}; + +/** + * Data returned by core_enrol_get_enrolled_users WS. + */ +type CoreEnrolGetEnrolledUsersWSResponse = CoreUserParticipant[]; + +/** + * Params of core_user_get_course_user_profiles WS. + */ +type CoreUserGetCourseUserProfilesWSParams = { + userlist: { + userid: number; // Userid. + courseid: number; // Courseid. + }[]; +}; + +/** + * Data returned by core_user_get_course_user_profiles WS. + */ +type CoreUserGetCourseUserProfilesWSResponse = CoreUserCourseProfile[]; + +/** + * Params of core_user_get_users_by_field WS. + */ +type CoreUserGetUsersByFieldWSParams = { + field: string; // The search field can be 'id' or 'idnumber' or 'username' or 'email'. + values: string[]; +}; +/** + * Data returned by core_user_get_users_by_field WS. + */ +type CoreUserGetUsersByFieldWSResponse = CoreUserData[]; + +/** + * Params of core_user_get_user_preferences WS. + */ +type CoreUserGetUserPreferencesWSParams = { + name?: string; // Preference name, empty for all. + userid?: number; // Id of the user, default to current user. +}; + +/** + * Data returned by core_user_get_user_preferences WS. + */ +type CoreUserGetUserPreferencesWSResponse = { + preferences: { // User custom fields (also known as user profile fields). + name: string; // The name of the preference. + value: string; // The value of the preference. + }[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of core_user_view_user_list WS. + */ +type CoreUserViewUserListWSParams = { + courseid: number; // Id of the course, 0 for site. +}; + +/** + * Params of core_user_view_user_profile WS. + */ +type CoreUserViewUserProfileWSParams = { + userid: number; // Id of the user, 0 for current user. + courseid?: number; // Id of the course, default site course. +}; + +/** + * Params of core_user_update_user_preferences WS. + */ +type CoreUserUpdateUserPreferencesWSParams = { + userid?: number; // Id of the user, default to current user. + emailstop?: number; // Enable or disable notifications for this user. + preferences?: { // User preferences. + type: string; // The name of the preference. + value?: string; // The value of the preference, do not set this field if you want to remove (unset) the current value. + }[]; +}; + +/** + * Params of core_enrol_search_users WS. + */ +type CoreEnrolSearchUsersWSParams = { + courseid: number; // Course id. + search: string; // Query. + searchanywhere: boolean; // Find a match anywhere, or only at the beginning. + page: number; // Page number. + perpage: number; // Number per page. +}; + +/** + * Data returned by core_enrol_search_users WS. + */ +type CoreEnrolSearchUsersWSResponse = CoreUserData[]; +