diff --git a/src/core/user/providers/offline.ts b/src/core/user/providers/offline.ts new file mode 100644 index 000000000..36ccc4c05 --- /dev/null +++ b/src/core/user/providers/offline.ts @@ -0,0 +1,114 @@ +// (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 { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; + +/** + * Structure of offline user preferences. + */ +export interface CoreUserOfflinePreference { + name: string; + value: string; + onlinevalue: string; +} + +/** + * Service to handle offline user preferences. + */ +@Injectable() +export class CoreUserOfflineProvider { + + // Variables for database. + static PREFERENCES_TABLE = 'user_preferences'; + protected siteSchema: CoreSiteSchema = { + name: 'CoreUserOfflineProvider', + version: 1, + tables: [ + { + name: CoreUserOfflineProvider.PREFERENCES_TABLE, + columns: [ + { + name: 'name', + type: 'TEXT', + unique: true, + notNull: true + }, + { + name: 'value', + type: 'TEXT' + }, + { + name: 'onlinevalue', + type: 'TEXT' + }, + ] + } + ] + }; + + constructor(private sitesProvider: CoreSitesProvider) { + this.sitesProvider.registerSiteSchema(this.siteSchema); + } + + /** + * Get preferences that were changed offline. + * + * @return {Promise} Promise resolved with list of preferences. + */ + getChangedPreferences(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecordsSelect(CoreUserOfflineProvider.PREFERENCES_TABLE, 'value != onlineValue'); + }); + } + + /** + * Get an offline preference. + * + * @param {string} name Name of the preference. + * @return {Promise} Promise resolved with the preference, rejected if not found. + */ + getPreference(name: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { name }; + + return site.getDb().getRecord(CoreUserOfflineProvider.PREFERENCES_TABLE, conditions); + }); + } + + /** + * Set an offline preference. + * + * @param {string} name Name of the preference. + * @param {string} value Value of the preference. + * @param {string} onlineValue Online value of the preference. If unedfined, preserve previously stored value. + * @return {Promise} Promise resolved when done. + */ + setPreference(name: string, value: string, onlineValue?: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + let promise: Promise; + if (typeof onlineValue == 'undefined') { + promise = this.getPreference(name, site.id).then((preference) => preference.onlinevalue); + } else { + promise = Promise.resolve(onlineValue); + } + + return promise.then((onlineValue) => { + const record = { name, value, onlinevalue: onlineValue }; + + return site.getDb().insertRecord(CoreUserOfflineProvider.PREFERENCES_TABLE, record); + }); + }); + } +} diff --git a/src/core/user/providers/sync-cron-handler.ts b/src/core/user/providers/sync-cron-handler.ts new file mode 100644 index 000000000..a51b2da8b --- /dev/null +++ b/src/core/user/providers/sync-cron-handler.ts @@ -0,0 +1,48 @@ +// (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 { CoreCronHandler } from '@providers/cron'; +import { CoreUserSyncProvider } from './sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class CoreUserSyncCronHandler implements CoreCronHandler { + name = 'CoreUserSyncCronHandler'; + + constructor(private userSync: CoreUserSyncProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @param {boolean} [force] Wether the execution is forced (manual sync). + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string, force?: boolean): Promise { + return this.userSync.syncPreferences(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return 300000; // 5 minutes. + } +} diff --git a/src/core/user/providers/sync.ts b/src/core/user/providers/sync.ts new file mode 100644 index 000000000..c63e8dc61 --- /dev/null +++ b/src/core/user/providers/sync.ts @@ -0,0 +1,100 @@ +// (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 { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUserOfflineProvider } from './offline'; +import { CoreUserProvider } from './user'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSyncProvider } from '@providers/sync'; + +/** + * Service to sync user preferences. + */ +@Injectable() +export class CoreUserSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'core_user_autom_synced'; + + constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, + translate: TranslateService, syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, + private userOffline: CoreUserOfflineProvider, private userProvider: CoreUserProvider, + private utils: CoreUtilsProvider, timeUtils: CoreTimeUtilsProvider) { + super('CoreUserSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); + } + + /** + * Try to synchronize user preferences in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncPreferences(siteId?: string): Promise { + const syncFunctionLog = 'all user preferences'; + + return this.syncOnSites(syncFunctionLog, this.syncPreferencesFunc.bind(this), [], siteId); + } + + /** + * Sync user preferences of a site. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @param {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + protected syncPreferencesFunc(siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const syncId = 'preferences'; + + if (this.isSyncing(syncId, siteId)) { + // There's already a sync ongoing, return the promise. + return this.getOngoingSync(syncId, siteId); + } + + const warnings = []; + + this.logger.debug(`Try to sync user preferences`); + + const syncPromise = this.userOffline.getChangedPreferences(siteId).then((preferences) => { + return this.utils.allPromises(preferences.map((preference) => { + return this.userProvider.getUserPreferenceOnline(preference.name, siteId).then((onlineValue) => { + if (preference.onlinevalue != onlineValue) { + // Prefernce was changed on web while the app was offline, do not sync. + return this.userOffline.setPreference(preference.name, onlineValue, onlineValue, siteId); + } + + return this.userProvider.setUserPreference(name, preference.value, siteId).catch((error) => { + if (this.utils.isWebServiceError(error)) { + warnings.push(this.textUtils.getErrorMessageFromError(error)); + } else { + // Couldn't connect to server, reject. + return Promise.reject(error); + } + }); + }); + })); + }).then(() => { + // All done, return the warnings. + return warnings; + }); + + return this.addOngoingSync(syncId, syncPromise, siteId); + } +} diff --git a/src/core/user/providers/user.ts b/src/core/user/providers/user.ts index dd87758d8..085a73502 100644 --- a/src/core/user/providers/user.ts +++ b/src/core/user/providers/user.ts @@ -15,9 +15,11 @@ import { Injectable } from '@angular/core'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSite } from '@classes/site'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUserOfflineProvider } from './offline'; /** * Service to provide user functionalities. @@ -59,7 +61,8 @@ export class CoreUserProvider { protected logger; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, - private filepoolProvider: CoreFilepoolProvider) { + private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider, + private userOffline: CoreUserOfflineProvider) { this.logger = logger.getInstance('CoreUserProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); } @@ -272,6 +275,67 @@ export class CoreUserProvider { }); } + /** + * Get a user preference (online or offline). + * + * @param {string} name Name of the preference. + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {string} Preference value or null if preference not set. + */ + getUserPreference(name: string, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.userOffline.getPreference(name, siteId).catch(() => { + return null; + }).then((preference) => { + if (preference && !this.appProvider.isOnline()) { + // Offline, return stored value. + return preference.value; + } + + return this.getUserPreferenceOnline(name, siteId).then((wsValue) => { + if (preference && preference.value != preference.onlinevalue && preference.onlinevalue == wsValue) { + // Sync is pending for this preference, return stored value. + return preference.value; + } + + return this.userOffline.setPreference(name, wsValue, wsValue).then(() => { + return wsValue; + }); + }); + }); + } + + /** + * Get cache key for a user preference WS call. + * + * @param {string} name Preference name. + * @return {string} Cache key. + */ + protected getUserPreferenceCacheKey(name: string): string { + return this.ROOT_CACHE_KEY + 'preference:' + name; + } + + /** + * Get a user preference online. + * + * @param {string} name Name of the preference. + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {string} Preference value or null if preference not set. + */ + getUserPreferenceOnline(name: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { name }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getUserPreferenceCacheKey(data.name) + }; + + return site.read('core_user_get_user_preferences', data, preSets).then((result) => { + return result.preferences[0] ? result.preferences[0].value : null; + }); + }); + } + /** * Invalidates user WS calls. * @@ -298,6 +362,19 @@ export class CoreUserProvider { }); } + /** + * Invalidate user preference. + * + * @param {string} name Name of the preference. + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateUserPreference(name: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getUserPreferenceCacheKey(name)); + }); + } + /** * Check if course participants is disabled in a certain site. * @@ -455,6 +532,45 @@ export class CoreUserProvider { return Promise.all(promises); } + /** + * Set a user preference (online or offline). + * + * @param {string} name Name of the preference. + * @param {string} value Value of the preference. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved on success. + */ + setUserPreference(name: string, value: string, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + let isOnline = this.appProvider.isOnline(); + let promise: Promise; + + if (isOnline) { + const preferences = [{type: name, value}]; + promise = this.updateUserPreferences(preferences, undefined, undefined, siteId).catch((error) => { + // Preference not saved online. + isOnline = false; + + return Promise.reject(error); + }); + } else { + promise = Promise.resolve(); + } + + return promise.finally(() => { + // Update stored online value if saved online. + const onlineValue = isOnline ? value : undefined; + + return Promise.all([ + this.userOffline.setPreference(name, value, onlineValue), + this.invalidateUserPreference(name).catch(() => { + // Ignore error. + }) + ]); + }); + } + /** * Update a preference for a user. * @@ -478,13 +594,14 @@ export class CoreUserProvider { /** * Update some preferences for a user. * - * @param {any} preferences List of preferences. + * @param {{name: string, value: string}[]} preferences List of preferences. * @param {boolean} [disableNotifications] Whether to disable all notifications. Undefined to not update this value. * @param {number} [userId] User ID. If not defined, site's current user. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved if success. */ - updateUserPreferences(preferences: any, disableNotifications: boolean, userId?: number, siteId?: string): Promise { + updateUserPreferences(preferences: {type: string, value: string}[], disableNotifications?: boolean, userId?: number, + siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); diff --git a/src/core/user/user.module.ts b/src/core/user/user.module.ts index d2886ffe3..845f2179e 100644 --- a/src/core/user/user.module.ts +++ b/src/core/user/user.module.ts @@ -26,6 +26,10 @@ import { CoreUserParticipantsCourseOptionHandler } from './providers/course-opti import { CoreUserParticipantsLinkHandler } from './providers/participants-link-handler'; import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreUserComponentsModule } from './components/components.module'; +import { CoreCronDelegate } from '@providers/cron'; +import { CoreUserOfflineProvider } from './providers/offline'; +import { CoreUserSyncProvider } from './providers/sync'; +import { CoreUserSyncCronHandler } from './providers/sync-cron-handler'; // List of providers (without handlers). export const CORE_USER_PROVIDERS: any[] = [ @@ -33,6 +37,8 @@ export const CORE_USER_PROVIDERS: any[] = [ CoreUserProfileFieldDelegate, CoreUserProvider, CoreUserHelperProvider, + CoreUserOfflineProvider, + CoreUserSyncProvider ]; @NgModule({ @@ -46,10 +52,13 @@ export const CORE_USER_PROVIDERS: any[] = [ CoreUserProfileFieldDelegate, CoreUserProvider, CoreUserHelperProvider, + CoreUserOfflineProvider, + CoreUserSyncProvider, CoreUserProfileMailHandler, CoreUserProfileLinkHandler, CoreUserParticipantsCourseOptionHandler, - CoreUserParticipantsLinkHandler + CoreUserParticipantsLinkHandler, + CoreUserSyncCronHandler, ] }) export class CoreUserModule { @@ -57,12 +66,14 @@ export class CoreUserModule { eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, userProvider: CoreUserProvider, contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreUserProfileLinkHandler, courseOptionHandler: CoreUserParticipantsCourseOptionHandler, linkHandler: CoreUserParticipantsLinkHandler, - courseOptionsDelegate: CoreCourseOptionsDelegate) { + courseOptionsDelegate: CoreCourseOptionsDelegate, cronDelegate: CoreCronDelegate, + syncHandler: CoreUserSyncCronHandler) { userDelegate.registerHandler(userProfileMailHandler); courseOptionsDelegate.registerHandler(courseOptionHandler); contentLinksDelegate.registerHandler(userLinkHandler); contentLinksDelegate.registerHandler(linkHandler); + cronDelegate.register(syncHandler); eventsProvider.on(CoreEventsProvider.USER_DELETED, (data) => { // Search for userid in params.