side-porting latest UI services code from web staff project
authorBill Erickson <berick@esilibrary.com>
Mon, 4 Nov 2013 14:28:50 +0000 (09:28 -0500)
committerBill Erickson <berick@esilibrary.com>
Mon, 4 Nov 2013 14:28:50 +0000 (09:28 -0500)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/web/js/ui/default/staff/services/auth.js
Open-ILS/web/js/ui/default/staff/services/env.js
Open-ILS/web/js/ui/default/staff/services/idl.js
Open-ILS/web/js/ui/default/staff/services/list.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/services/org.js
Open-ILS/web/js/ui/default/staff/services/startup.js
Open-ILS/web/js/ui/default/staff/services/user.js

index 800ba73..57cdaca 100644 (file)
@@ -1,5 +1,7 @@
 /* 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
@@ -9,29 +11,16 @@ angular.module('egCoreMod')
 
 .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');
         }
     };
 
@@ -47,7 +36,7 @@ function($q, $cookies, $timeout, $location,
                 '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]; 
@@ -94,7 +83,16 @@ function($q, $cookies, $timeout, $location,
     };
 
     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;
index ca55f08..0e1f2b7 100644 (file)
@@ -1,67 +1,64 @@
 /**
  * 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
@@ -81,16 +78,24 @@ function($q, egEnvCache, egNet, egAuth, egPCRUD) {
     /** 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']}}
@@ -98,17 +103,18 @@ function($q, egEnvCache, egNet, egAuth, egPCRUD) {
                 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;
index fbc3e77..3d88924 100644 (file)
@@ -21,7 +21,7 @@ angular.module('egCoreMod')
     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;
diff --git a/Open-ILS/web/js/ui/default/staff/services/list.js b/Open-ILS/web/js/ui/default/staff/services/list.js
new file mode 100644 (file)
index 0000000..18e7f97
--- /dev/null
@@ -0,0 +1,226 @@
+/** 
+ * 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) 
+        }
+    };
+});
+
index a9b38fa..4627f6a 100644 (file)
@@ -13,11 +13,11 @@ function(egEnv, egAuth, egPCRUD) {
     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) {
index dee9e3c..d7820da 100644 (file)
 
 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();
@@ -45,7 +35,7 @@ function(
 
                 // testAuthToken resolved
                 function() { 
-                    egEnv.load(args.load_classes).then(
+                    egEnv.load().then(
                         function() { deferred.resolve() }, 
                         function() { 
                             deferred.reject('egEnv did not resolve')
@@ -57,7 +47,7 @@ function(
                 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.
@@ -72,7 +62,7 @@ function(
                 }
             );
 
-            return deferred.promise;
+            return this.promise;
         }
     };
 }]);
index 61db10a..7aa6bdc 100644 (file)
@@ -1,23 +1,19 @@
-/** 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);
 
@@ -29,10 +25,10 @@ function($q, $timeout, egNet, egAuth, egUserCache, egOrg) {
                 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);
                     }
                 }
@@ -46,8 +42,8 @@ function($q, $timeout, egNet, egAuth, egUserCache, egOrg) {
      * 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();