/* Core Sevice - egAuth
*
+ * Manages login and auth session retrieval
+ *
* Angular cookies are still fairly primitive.
* In particular, you can't set the path.
* https://github.com/angular/angular.js/issues/1786
.constant('EG_AUTH_COOKIE', 'ses')
-// auth cache
-.factory('egAuthCache',
- ['$cacheFactory', function($cacheFactory) {
- return $cacheFactory('egAuthCache', {});
-}])
-
-
.factory('egAuth',
-['$q', '$cookies', '$timeout', '$location',
- '$window', 'egAuthCache', 'egNet', 'EG_AUTH_COOKIE',
-function($q, $cookies, $timeout, $location,
- $window, egAuthCache, egNet, EG_AUTH_COOKIE) {
+ ['$q','$cookies','$timeout','$location','$window','egNet','EG_AUTH_COOKIE',
+function($q, $cookies, $timeout, $location, $window, egNet, EG_AUTH_COOKIE) {
var service = {
user : function() {
- return egAuthCache.get('user');
+ return this._user;
},
token : function() {
- // no need to cache, since auth lives in a cookie
return $cookies[EG_AUTH_COOKIE];
- },
- workstation : function() { // TODO
- return egAuthCache.get('workstation');
}
};
'open-ils.auth.session.retrieve', token).then(
function(user) {
if (user && user.classname) {
- egAuthCache.put('user', user);
+ service._user = user;
deferred.resolve();
} else {
delete $cookies[EG_AUTH_COOKIE];
};
service.logout = function() {
+ console.debug('egAuth.logout()');
+ if (service.token()) {
+ egNet.request(
+ 'open-ils.auth',
+ 'open-ils.auth.session.delete',
+ service.token()
+ ); // fire and forget
+ }
delete $cookies[EG_AUTH_COOKIE];
+ service._user = null;
};
return service;
/**
* Core Service - egEnv
*
- * Data that we always want to load at startup goes here.
- * Requests are sents as a swarm of async calls. As each
- * returns, a pending-calls counter is decremented. Once
- * it reaches zero, the promise returned by load() /
- * loadAll() is resolved.
+ * Manages startup data loading. All registered loaders run
+ * simultaneously. When all promises are resolved, the promise
+ * returned by egEnv.load() is resolved.
*
- * App-supplied generic load commands are pushed onto the
- * service.loaders array. Each item in the array is a
- * function which returns a promise. Note that loaders in this
- * array cannot have any dependencies on other startup loaders,
- * since there is no guarantee which will complete first.
+ * Generic and class-based loaders are supported.
*
- * egEnv.loaders.push(function() {
- * return egNet.request(...)
- * .then(function(stuff) { console.log('stuff!') });
+ * To load a registred class, push the class hint onto
+ * egEnv.loadClasses.
+ *
+ * // will cause all 'pgt' objects to be fetched
+ * egEnv.loadClasses.push('pgt');
+ *
+ * To register a new class loader,attach a loader function to
+ * egEnv.classLoaders, keyed on the class hint, which returns a promise.
*
- * Data requets that have requirements loaded by startup
- * should run after startup directly in the application.
+ * egEnv.classLoaders.ccs = function() {
+ * // loads copy status objects, returns promise
+ * };
*
- * TODO: marry egStartup and egEnv?
- * TODO: support a post-org loader so that loaders can be
- * added which rely on the org tree?
+ * Generic loaders go onto the egEnv.loaders array. Each should
+ * return a promise.
+ *
+ * egEnv.loaders.push(function() {
+ * return egNet.request(...)
+ * .then(function(stuff) { console.log('stuff!')
+ * });
*/
angular.module('egCoreMod')
-// env cache
-.factory('egEnvCache', ['$cacheFactory',
-function($cacheFactory) {
- return $cacheFactory('egEnvCache', {});
-}])
-
// env fetcher
.factory('egEnv',
-['$q', 'egEnvCache', 'egNet', 'egAuth', 'egPCRUD',
-function($q, egEnvCache, egNet, egAuth, egPCRUD) {
+ ['$q','egAuth','egPCRUD','egIDL',
+function($q, egAuth, egPCRUD, egIDL) {
var service = {
- loaders : [],
- get : function(class_) {
- return egEnvCache.get(class_);
- }
- };
-
- service.onload = function() {
- if (--this.in_flight == 0)
- this.deferred.resolve();
+ // collection of custom loader functions
+ loaders : []
};
/* returns a promise, loads all of the specified classes */
- service.load = function(classes) {
- if (!classes) classes = Object.keys(this.classLoaders);
- this.deferred = $q.defer();
- this.in_flight = classes.length + this.loaders.length;
+ service.load = function() {
+ // always assume the user is logged in
+ if (!egAuth.user()) return $q.when();
+
+ var allPromises = [];
+ var classes = this.loadClasses;
+ console.debug('egEnv loading classes => ' + classes);
+
angular.forEach(classes, function(cls) {
- service.classLoaders[cls]().then(function(){service.onload()});
+ allPromises.push(service.classLoaders[cls]());
});
angular.forEach(this.loaders, function(loader) {
- loader().then(function(){service.onload()});
+ allPromises.push(loader());
});
- return this.deferred.promise;
+
+ return $q.all(allPromises).then(
+ function() { console.debug('egEnv load complete') });
};
/** given a tree-shaped collection, captures the tree and
/** caches the object list both as the list and an id => object map */
service.absorbList = function(list, class_) {
var blob = {list : list, map : {}};
- angular.forEach(list, function(item) {blob.map[item.id()] = item});
- egEnvCache.put(class_, blob);
+ var pkey = egIDL.classes[class_].pkey;
+ angular.forEach(list, function(item) {blob.map[item[pkey]()] = item});
+ service[class_] = blob;
return blob;
};
- /* Classes (by hint) to load, their loading routines,
- * and their result mungers */
+ /*
+ * list of classes to load on every page, regardless of whether
+ * a page-specific list is provided.
+ */
+ service.loadClasses = ['aou', 'aws'];
+ /*
+ * Default class loaders. Only add classes directly to this file
+ * that are loaded practically always. All other app-specific
+ * classes should be registerd from within the app.
+ */
service.classLoaders = {
-
aou : function() {
return egPCRUD.search('aou', {parent_ou : null},
{flesh : -1, flesh_fields : {aou : ['children', 'ou_type']}}
function(tree) {service.absorbTree(tree, 'aou')}
);
},
-
- // TODO: make me optional -- not all UIs need the PGT
- pgt : function() {
- return egPCRUD.search('pgt', {parent : null},
- {flesh : -1, flesh_fields : {pgt : ['children']}}
- ).then(
- function(tree) {service.absorbTree(tree, 'pgt')}
- );
+ aws : function() {
+ // by default, load only the workstation for the authenticated
+ // user. to load all workstations, override this loader.
+ // TODO: auth.session.retrieve should be capable of returning
+ // the session with the workstation fleshed.
+ if (!egAuth.user().wsid()) {
+ // nothing to fetch.
+ return $q.when();
+ }
+ return egPCRUD.retrieve('aws', egAuth.user().wsid())
+ .then(function(ws) {service.absorbList([ws], 'aws')});
}
-
- // org unit settings, blah, blah
};
return service;
var service = {};
service.parseIDL = function() {
- console.log('egIDL.parseIDL()');
+ console.debug('egIDL.parseIDL()');
// retain a copy of the full IDL within the service
service.classes = $window._preload_fieldmapper_IDL;
--- /dev/null
+/**
+ * Service for generating list management objects.
+ * Each object tracks common list attributes like limit, offset, etc.,
+ * A ListManager is not responsible for collecting data, it's only
+ * there to allow controllers to have a known consistent API
+ * for manage list-related information.
+ *
+ * The service exports a single attribute, which instantiates
+ * a new ListManager object. Controllers using ListManagers
+ * are responsible for providing their own route persistence.
+ *
+ * var list = egList.create();
+ * if (list.hasNextPage()) { ... }
+ *
+ */
+
+angular.module('egListMod', ['egCoreMod'])
+
+.factory('egList', function() {
+
+ function ListManager(args) {
+ var self = this;
+ this.limit = 25;
+ this.offset = 0;
+ this.sort = null;
+ this.totalCount = 0;
+
+ // attribute on each item in our items list which
+ // refers to its unique identifier value
+ this.indexField = 'index';
+
+ // true if the index field name refers to a
+ // function instead of an object attribute
+ this.indexFieldAsFunction = false;
+
+ // per-page list of items
+ this.items = [];
+
+ // collect any defaults passed in
+ if (args) angular.forEach(args,
+ function(val, key) {self[key] = val});
+
+ // sorted list of all available display columns
+ // a column takes form of (at minimum) {name : name, label : label}
+ this.allColumns = [];
+
+ // {name => true} map of visible columns
+ this.displayColumns = {};
+
+ // {index => true} map of selected rows
+ this.selectedRows = {};
+
+ this.indexValue = function(item) {
+ if (this.indexFieldAsFunction) {
+ return item[this.indexField]();
+ } else {
+ return item[this.indexField];
+ }
+ }
+
+ // returns item objects
+ this.selectedItems = function() {
+ var items = [];
+ angular.forEach(
+ this.items,
+ function(item) {
+ if (self.selectedRows[self.indexValue(item)])
+ items.push(item);
+ }
+ );
+ return items;
+ }
+
+ // remove an item from the items list
+ this.removeItem = function(index) {
+ angular.forEach(this.items, function(item, idx) {
+ if (self.indexValue(item) == index)
+ self.items.splice(idx, 1);
+ });
+ delete this.selectedRows[index];
+ }
+
+ this.count = function() { return this.items.length }
+
+ this.reset = function() {
+ this.offset = 0;
+ this.totalCount = 0;
+ this.items = [];
+ this.selectedRows = {};
+ }
+
+ // prepare to draw a new page of data
+ this.resetPageData = function() {
+ this.items = [];
+ this.selectedRows = {};
+ }
+
+ this.showAllColumns = function() {
+ angular.forEach(this.allColumns, function(field) {
+ self.displayColumns[field.name] = true;
+ });
+ }
+
+ this.hideAllColumns = function() {
+ angular.forEach(this.allColumns, function(field) {
+ delete self.displayColumns[field.name]
+ });
+ }
+
+ // selects one row after deselecting all of the others
+ this.selectOneRow = function(index) {
+ this.deselectAllRows();
+ this.selectedRows[index] = true;
+ }
+
+ // selects or deselects a row, without affecting the others
+ this.toggleOneRowSelection = function(index) {
+ if (this.selectedRows[index]) {
+ delete this.selectedRows[index];
+ } else {
+ this.selectedRows[index] = true;
+ }
+ }
+
+ // selects all visible rows
+ this.selectAllRows = function() {
+ angular.forEach(this.items, function(item) {
+ self.selectedRows[self.indexValue(item)] = true
+ });
+ }
+
+ // if all are selected, deselect all, otherwise select all
+ this.toggleSelectAll = function() {
+ if (Object.keys(this.selectedRows).length == this.items.length) {
+ this.deselectAllRows();
+ } else {
+ this.selectAllRows();
+ }
+ }
+
+ // deselects all visible rows
+ this.deselectAllRows = function() {
+ this.selectedRows = {};
+ }
+
+ this.defaultColumns = function(list) {
+ // set the display=true value for the selected columns
+ angular.forEach(list, function(name) {
+ self.displayColumns[name] = true
+ });
+
+ // default columns may be provided before we
+ // know what our columns are. Save them for later.
+ this._defaultColumns = list;
+
+ // setColumns we rearrange the allCollums
+ // list based on the content of this._defaultColums
+ if (this.allColumns.length)
+ this.setColumns(this.allColumns);
+ }
+
+ this.setColumns = function(list) {
+ if (this._defaultColumns) {
+ this.allColumns = [];
+
+ // append the default columns to the front of
+ // our allColumnst list. Any remaining columns
+ // are plopped onto the end.
+ angular.forEach(
+ this._defaultColumns,
+ function(name) {
+ var foundIndex;
+ angular.forEach(list, function(f, idx) {
+ if (f.name == name) {
+ self.allColumns.push(f);
+ foundIndex = idx;
+ }
+ });
+ list.splice(foundIndex, 1);
+ }
+ );
+ this.allColumns = this.allColumns.concat(list);
+ delete this._defaultColumns;
+
+ } else {
+ this.allColumns = list;
+ }
+ }
+
+ this.onFirstPage = function() {
+ return this.offset == 0;
+ }
+
+ this.hasNextPage = function() {
+ // we have less data than requested, there must
+ // not be any more pages
+ if (this.items.length < this.limit) return false;
+
+ // if the total count is not known, assume that a full
+ // page of data implies more pages are available.
+ if (!this.totalCount) return true;
+
+ // we have a full page of data, but is there more?
+ return this.totalCount > (this.offset + this.items.length);
+ }
+
+ this.incrementPage = function() {
+ this.offset += this.limit;
+ }
+
+ this.decrementPage = function() {
+ if (this.offset < this.limit) {
+ this.offset = 0;
+ } else {
+ this.offset -= this.limit;
+ }
+ }
+ }
+
+ return {
+ create : function(args) {
+ return new ListManager(args)
+ }
+ };
+});
+
service.get = function(node_or_id) {
if (typeof node_or_id == 'object')
return node_or_id;
- return egEnv.get('aou').map[node_or_id];
+ return egEnv.aou.map[node_or_id];
};
service.list = function() {
- return egEnv.get('aou').list;
+ return egEnv.aou.list;
};
service.ancestors = function(node_or_id) {
angular.module('egCoreMod')
-.factory('egStartupCache',
- ['$cacheFactory', function($cacheFactory) {
- return $cacheFactory('egStartupCache', {number : 1});
-}])
-
-
.factory('egStartup',
- ['$q', '$rootScope', '$timeout', '$location', '$window',
- 'egStartupCache', 'egIDL', 'egAuth', 'egEnv',
-function(
- $q, $rootScope, $timeout, $location, $window,
- egStartupCache, egIDL, egAuth, egEnv) {
+ ['$q','$rootScope','$location','$window','egIDL','egAuth','egEnv',
+function($q, $rootScope, $location, $window, egIDL, egAuth, egEnv) {
return {
- go : function (args) {
- args = args || {};
-
- if (egStartupCache.get('promise')) {
- // startup is done, return our promise
- return egStartupCache.get('promise').promise;
+ promise : null,
+ go : function () {
+ if (this.promise) {
+ // startup already started, return our existing promise
+ return this.promise;
}
// create a new promise and fire off startup
var deferred = $q.defer();
- egStartupCache.put('promise', deferred);
+ this.promise = deferred.promise;
// IDL parsing is sync. No promises required
egIDL.parseIDL();
// testAuthToken resolved
function() {
- egEnv.load(args.load_classes).then(
+ egEnv.load().then(
function() { deferred.resolve() },
function() {
deferred.reject('egEnv did not resolve')
function() {
console.log('egAuth found no valid authtoken');
if ($location.path() == '/login') {
- console.log('egStartup resolving without authtoken on /login');
+ console.debug('egStartup resolving without authtoken on /login');
deferred.resolve();
} else {
// TODO: this is a little hinky because it causes 2 redirects.
}
);
- return deferred.promise;
+ return this.promise;
}
};
}]);
-/** Service for fetching fleshed user objects.
- * The last user retrieved is kept in the local cache */
+/**
+ * Service for fetching fleshed user objects.
+ * The last user retrieved is kept until replaced by a new user.
+ */
angular.module('egUserMod', ['egCoreMod'])
-.factory('egUserCache',
- ['$cacheFactory', function($cacheFactory) {
- return $cacheFactory('egUserCache', {number : 1});
-}])
-
-
.factory('egUser',
-['$q', '$timeout', 'egNet', 'egAuth', 'egUserCache', 'egOrg',
-function($q, $timeout, egNet, egAuth, egUserCache, egOrg) {
+ ['$q','$timeout','egNet','egAuth','egOrg',
+function($q, $timeout, egNet, egAuth, egOrg) {
- var service = {};
+ var service = {_user : null};
service.get = function(userId) {
var deferred = $q.defer();
- var last = egUserCache.get('last');
+ var last = sevice._user;
if (last && last.id() == userId) {
return $q.when(last);
egAuth.token(), userId).then(
function(user) {
if (user && user.classname == 'au') {
- egUserCache.put('last', user);
+ service._user = user;
deferred.resolve(user);
} else {
- egUserCache.remove('last');
+ service._user = null;
deferred.reject(user);
}
}
* Returns the full list of org unit objects at which the currently
* logged in user has the selected permissions.
* @permList - list or string. If a list, the response object is a
- * hash of perm => orgList maps. If a string, the response is only
- * the final org list.
+ * hash of perm => orgList maps. If a string, the response is the
+ * org list for the requested perm.
*/
service.hasPermAt = function(permList) {
var deferred = $q.defer();