Read user preferences from backend

Remove usage of localStorage to retrieve preferences, and add a
Preference resource that uses the backend API. Preferences are now
bootstrapped in the root state so that our cache is fresh. Also a
couple of UI tweaks to the preferences page.

Change-Id: Icf3072e24fef86848801a5c72f5833605b1afcd7
This commit is contained in:
Michael Krotscheck 2015-02-13 17:20:00 -08:00
parent c6938e5e6b
commit 2c71c40ce5
7 changed files with 403 additions and 248 deletions

View File

@ -110,6 +110,14 @@ angular.module('sb.auth').constant('SessionResolver',
requireCurrentUser: function ($q, $log, CurrentUser) {
$log.debug('Resolving current user...');
return CurrentUser.resolve();
},
/**
* This function resolves the preferences.
*/
resolvePreferences: function ($q, $log, Preference) {
$log.debug('Resolving user preferences...');
return Preference.refresh();
}
};
})());

View File

@ -19,19 +19,29 @@
* individual preferences.
*/
angular.module('sb.profile').controller('ProfilePreferencesController',
function ($scope, Preference) {
function ($scope, Preference, Notification, Severity) {
'use strict';
$scope.preferences = Preference.getAll();
/**
* Save all the preferences.
*/
$scope.save = function () {
$scope.saving = true;
for (var key in $scope.preferences) {
if (Preference.get(key) !== $scope.preferences[key]) {
Preference.set(key, $scope.preferences[key]);
Preference.saveAll($scope.preferences).then(
function () {
Notification.send(
'preferences',
'Preferences Saved!',
Severity.SUCCESS
);
$scope.saving = false;
},
function () {
$scope.saving = false;
}
}
$scope.message = 'Preferences Saved!';
);
};
});

View File

@ -17,129 +17,133 @@
<div class="container" ng-hide="isLoading">
<div class="row">
<div class="col-xs-12">
<h1><i class="fa fa-sb-profile-preferences"></i> Preferences</h1>
<h1><i class="fa fa-sb-profile-preferences"
ng-if="!saving"></i>
<i class="fa fa-spin fa-sb-profile-preferences"
ng-if="saving"></i>
Preferences
</h1>
</div>
</div>
<div class="row">
<div class="col-md-6">
<form name="preferencesForm">
<div class="form-group">
<label>Page size</label>
<div class="col-sm-6">
<div class="form-group">
<label>Timeline events</label>
<p class="help-block">
How many results would you like to see when viewing
lists?
</p>
<p class="help-block">
Which types of Timeline events would you like to be
displayed?
</p>
<div>
<input type="radio" name="pageSize"
id="pageSize10" value="10"
ng-model="preferences.page_size">
<label for="pageSize10">10</label>
&nbsp;
<input type="radio" name="pageSize"
id="pageSize25" value="25"
ng-model="preferences.page_size">
<label for="pageSize25">25</label>
&nbsp;
<input type="radio" name="pageSize"
id="pageSize50" value="50"
ng-model="preferences.page_size">
<label for="pageSize50">50</label>
&nbsp;
<input type="radio" name="pageSize"
id="pageSize100" value="100"
ng-model="preferences.page_size">
<label for="pageSize100">100</label>
</div>
<div>
<input type="checkbox" name="enabledTypes"
id="storyCreated"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_story_created">
<label for="storyCreated">Story created</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="storyDetailsChanged"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_story_details_changed">
<label for="storyDetailsChanged">Story details
changed</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskCreated"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_created">
<label for="taskCreated">Task created</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskAssigneeChanged"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_assignee_changed">
<label for="taskAssigneeChanged">Task assignee
changed</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskStatusChanged"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_status_changed">
<label for="taskStatusChanged">Task status changed</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskPriorityChanged"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_priority_changed">
<label for="taskStatusChanged">Task priority changed</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskDetailsChanged"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_details_changed">
<label for="taskDetailsChanged">Task details changed</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskDeleted"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_deleted">
<label for="taskDeleted">Task deleted</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="userComment"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_user_comment">
<label for="userComment">User comment</label>
</div>
</div>
</div>
<div class="col-sm-6">
<hr class="visible-xs"/>
<div class="form-group">
<label>Page size</label>
<hr/>
<p class="help-block">
How many results would you like to see when viewing
lists?
</p>
<div class="form-group">
<label>Timeline events</label>
<p class="help-block">
Which types of Timeline events would you like to be
displayed?
</p>
<div>
<input type="checkbox" name="enabledTypes"
id="storyCreated"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_story_created">
<label for="storyCreated">Story created</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="storyDetailsChanged"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_story_details_changed">
<label for="storyDetailsChanged">Story details changed</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskCreated"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_created">
<label for="taskCreated">Task created</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskAssigneeChanged"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_assignee_changed">
<label for="taskAssigneeChanged">Task assignee changed</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskStatusChanged"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_status_changed">
<label for="taskStatusChanged">Task status changed</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskPriorityChanged"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_priority_changed">
<label for="taskStatusChanged">Task priority changed</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskDetailsChanged"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_details_changed">
<label for="taskDetailsChanged">Task details changed</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="taskDeleted"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_task_deleted">
<label for="taskDeleted">Task deleted</label>
<br/>
<input type="checkbox" name="enabledTypes"
id="userComment"
ng-true-value="'true'"
ng-false-value="'false'"
ng-model="preferences.display_events_user_comment">
<label for="userComment">User comment</label>
</div>
<div>
<input type="radio" name="pageSize"
id="pageSize10" value="10"
ng-model="preferences.page_size">
<label for="pageSize10">10</label>
&nbsp;
<input type="radio" name="pageSize"
id="pageSize25" value="25"
ng-model="preferences.page_size">
<label for="pageSize25">25</label>
&nbsp;
<input type="radio" name="pageSize"
id="pageSize50" value="50"
ng-model="preferences.page_size">
<label for="pageSize50">50</label>
&nbsp;
<input type="radio" name="pageSize"
id="pageSize100" value="100"
ng-model="preferences.page_size">
<label for="pageSize100">100</label>
</div>
<hr/>
<div class="form-group">
<button type="button" class="btn btn-default"
ng-click="save()">
Save
</button>
<p class="help-block text-success">{{message}}</p>
</div>
</form>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<hr />
<button class="btn btn-primary"
ng-disabled="saving"
ng-click="save()"
>Save</button>
</div>
</div>
</div>

View File

@ -0,0 +1,220 @@
/*
* Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
*
* 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.
*/
/**
* Preference service, a convenience-cashing API in front of our UserPreference
* service, with resolving/refresh functionality.
*/
angular.module('sb.services').provider('Preference',
function () {
'use strict';
/**
* Singleton preference provider.
*/
var preferenceInstance = null;
/**
* Registered default preferences.
*/
var defaults = {};
/**
* Each module can manually declare its own preferences that it would
* like to keep track of, as well as set a default. During the config()
* phase, inject the Preference Provider and call 'addPreference()' to
* do so. An example is available at the bottom of this file.
*/
this.addPreference = function (name, defaultValue) {
defaults[name] = defaultValue;
};
/**
* The actual preference implementation.
*/
function Preference($q, $log, Session, AccessToken, UserPreference,
SessionState) {
// Scope assignment.
var self = this;
/**
* The currently loaded preferences.
*/
this.preferences = {};
/**
* This function resolves the user preferences. If a valid session
* is resolvable, it will load the preferences for the user.
* Otherwise it will resolve the default preferences.
*
* @returns {deferred.promise|*}
*/
this._resolveUserPreferences = function () {
var deferred = $q.defer();
// First resolve the session.
var sessionPromise = Session.resolveSessionState();
sessionPromise.then(
function (state) {
if (state === SessionState.LOGGED_IN) {
UserPreference.get({id: AccessToken.getIdToken()},
function (prefs) {
deferred.resolve(prefs);
}, function () {
deferred.resolve(defaults);
});
} else {
deferred.resolve(defaults);
}
},
function () {
deferred.resolve(defaults);
}
);
return deferred.promise;
};
/**
* Create a composite result of preference defaults and server
* provided defaults.
*/
this.getAll = function () {
var result = {};
for (var def_key in defaults) {
result[def_key] = this.get(def_key);
}
for (var key in this.preferences) {
result[key] = this.preferences[key];
}
return result;
};
/**
* Save all the preferences in the passed hash.
*/
this.saveAll = function (preferences) {
// Update the preferences.
for (var key in defaults) {
if (preferences.hasOwnProperty(key)) {
this.preferences[key] = preferences[key];
}
}
return this._save();
};
/**
* Returns the value for a given preference.
*/
this.get = function (key) {
// Is this a valid preference?
if (!defaults.hasOwnProperty(key)) {
$log.warn('Attempt to get unregistered preference: ' +
key);
return null;
}
// If the value is unset, and we have a default,
// set that.
if (!this.preferences.hasOwnProperty(key)) {
$log.warn('Setting default preference: ',
key, defaults[key]);
this.set(key, defaults[key]);
}
return this.preferences[key];
};
/**
* Save a preference and return the saving promise.
*/
this.set = function (key, value) {
// Is this a valid preference?
if (!defaults.hasOwnProperty(key)) {
$log.warn('Attempt to set unregistered preference: ' +
key);
return null;
}
// Store the preference.
this.preferences[key] = value;
return this._save();
};
/**
* Resolve the preferences.
*/
this.refresh = function () {
var deferred = $q.defer();
// This should never fail, see implementation above.
this._resolveUserPreferences().then(
function (newPrefs) {
self.preferences = angular.copy(newPrefs);
deferred.resolve(newPrefs);
}
);
return deferred.promise;
};
/**
* Private save method.
*/
this._save = function() {
var deferred = $q.defer();
this.preferences.$save({id: AccessToken.getIdToken()},
function () {
deferred.resolve();
}, function () {
deferred.resolve();
});
return deferred.promise;
};
}
/**
* Factory getter - returns a configured instance of preference
* provider, as needed.
*/
this.$get =
function ($injector) {
if (!preferenceInstance) {
preferenceInstance = $injector.instantiate(Preference);
}
return preferenceInstance;
};
})
.config(function (PreferenceProvider) {
'use strict';
// WARNING: In all modules OTHER than the services module, this config
// block can appear anywhere as long as this module is listed as a
// dependency. In the services module, the config() block must appear
// AFTER the provider block. For more information,
// @see https://github.com/angular/angular.js/issues/6723
// Let our preference provider know about page_size.
PreferenceProvider.addPreference('page_size', 10);
});

View File

@ -1,126 +0,0 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* 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.
*/
/**
* A simple preferences service, backed by localStorage.
*/
angular.module('sb.services').provider('Preference',
function () {
'use strict';
/**
* Our preference defaults. We're using underscore naming here in
* anticipation of these keys living on the python side of things.
*/
var defaults = { };
/**
* Preference name key generator. Basically it's poor man's
* namespacing.
*/
function preferenceName(key) {
return 'pref_' + key;
}
/**
* Each module can manually declare its own preferences that it would
* like to keep track of, as well as set a default. During the config()
* phase, inject the Preference Provider and call 'addPreference()' to
* do so. An example is available at the bottom of this file.
*/
this.addPreference = function (preferenceName, preferenceDefault) {
defaults[preferenceName] = preferenceDefault;
};
/**
* The actual preference implementation.
*/
function Preference($log, localStorageService) {
/**
* Get all the preferences.
*/
this.getAll = function () {
var result = {};
for (var key in defaults) {
result[key] = this.get(key);
}
return result;
};
/**
* Get a preference.
*/
this.get = function (key) {
// Is this a valid preference?
if (!defaults.hasOwnProperty(key)) {
$log.warn('Attempt to get unregistered preference: ' +
key);
return null;
}
var value = localStorageService.get(preferenceName(key));
// If the value is unset, and we have a default, set and use
// that.
if (value === null && defaults.hasOwnProperty(key)) {
var defaultValue = defaults[key];
this.set(key, defaultValue);
return defaultValue;
}
return value;
};
/**
* Set a preference.
*/
this.set = function (key, value) {
// Is this a valid preference?
if (!defaults.hasOwnProperty(key)) {
$log.warn('Attempt to set unregistered preference: ' +
key);
return null;
}
return localStorageService.set(preferenceName(key), value);
};
}
/**
* Factory getter - returns a configured instance of preference
* provider, as needed.
*/
this.$get = function ($log, localStorageService) {
return new Preference($log, localStorageService);
};
})
.config(function (PreferenceProvider) {
'use strict';
// WARNING: In all modules OTHER than the services module, this config
// block can appear anywhere as long as this module is listed as a
// dependency. In the services module, the config() block must appear
// AFTER the provider block. For more information,
// @see https://github.com/angular/angular.js/issues/6723
// Let our preference provider know about page_size.
PreferenceProvider.addPreference('page_size', 10);
})
;

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
*
* 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.
*/
/**
* The angular resource abstraction that allows us to consume preferences. This
* resource does not adhere to our ResourceFactory pattern, as preferences are
* treated as a single per-user object.
*/
angular.module('sb.services').factory('UserPreference',
function ($resource, storyboardApiBase) {
'use strict';
return $resource(storyboardApiBase + '/users/:id/preferences',
{
id: '@id'
},
{
'get': {
method: 'GET',
cache: true
}
}
);
}
);

View File

@ -57,7 +57,8 @@ angular.module('storyboard',
url: '',
template: '<div ui-view></div>',
resolve: {
sessionState: SessionResolver.resolveSessionState
sessionState: SessionResolver.resolveSessionState,
preferences: SessionResolver.resolvePreferences
}
});
})