From 13b0c65325bbf47e43ea23ff68df7a6399b69f1d Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Thu, 24 Oct 2013 14:39:33 -0400 Subject: [PATCH] Angular web staff - initial import * TT-ifying the HTML files * Initial Apache rewrite configuration to support routes Signed-off-by: Bill Erickson --- Open-ILS/examples/apache/eg_vhost.conf.in | 15 ++ Open-ILS/examples/apache_24/eg_vhost.conf.in | 14 + Open-ILS/src/templates/staff/README | 6 + Open-ILS/src/templates/staff/index.tt2 | 24 ++ Open-ILS/src/templates/staff/t_base_js.tt2 | 30 +++ Open-ILS/src/templates/staff/t_login.tt2 | 52 ++++ Open-ILS/src/templates/staff/t_navbar.tt2 | 41 +++ Open-ILS/src/templates/staff/t_splash.tt2 | 53 ++++ Open-ILS/web/js/ui/default/staff/app.js | 88 +++++++ Open-ILS/web/js/ui/default/staff/navbar.js | 38 +++ Open-ILS/web/js/ui/default/staff/services/auth.js | 102 ++++++++ Open-ILS/web/js/ui/default/staff/services/env.js | 95 +++++++ Open-ILS/web/js/ui/default/staff/services/idl.js | 60 +++++ Open-ILS/web/js/ui/default/staff/services/net.js | 51 ++++ Open-ILS/web/js/ui/default/staff/services/org.js | 46 ++++ Open-ILS/web/js/ui/default/staff/services/pcrud.js | 288 +++++++++++++++++++++ .../web/js/ui/default/staff/services/startup.js | 77 ++++++ Open-ILS/web/js/ui/default/staff/services/ui.js | 45 ++++ Open-ILS/web/js/ui/default/staff/services/user.js | 47 ++++ 19 files changed, 1172 insertions(+) create mode 100644 Open-ILS/src/templates/staff/README create mode 100644 Open-ILS/src/templates/staff/index.tt2 create mode 100644 Open-ILS/src/templates/staff/t_base_js.tt2 create mode 100644 Open-ILS/src/templates/staff/t_login.tt2 create mode 100644 Open-ILS/src/templates/staff/t_navbar.tt2 create mode 100644 Open-ILS/src/templates/staff/t_splash.tt2 create mode 100644 Open-ILS/web/js/ui/default/staff/app.js create mode 100644 Open-ILS/web/js/ui/default/staff/navbar.js create mode 100644 Open-ILS/web/js/ui/default/staff/services/auth.js create mode 100644 Open-ILS/web/js/ui/default/staff/services/env.js create mode 100644 Open-ILS/web/js/ui/default/staff/services/idl.js create mode 100644 Open-ILS/web/js/ui/default/staff/services/net.js create mode 100644 Open-ILS/web/js/ui/default/staff/services/org.js create mode 100644 Open-ILS/web/js/ui/default/staff/services/pcrud.js create mode 100644 Open-ILS/web/js/ui/default/staff/services/startup.js create mode 100644 Open-ILS/web/js/ui/default/staff/services/ui.js create mode 100644 Open-ILS/web/js/ui/default/staff/services/user.js diff --git a/Open-ILS/examples/apache/eg_vhost.conf.in b/Open-ILS/examples/apache/eg_vhost.conf.in index 1de2212cdf..c39c981591 100644 --- a/Open-ILS/examples/apache/eg_vhost.conf.in +++ b/Open-ILS/examples/apache/eg_vhost.conf.in @@ -787,7 +787,22 @@ RewriteRule ^/openurl$ ${openurl:%1} [NE,PT] +# TODO: as is, each sub-app will require a new Location. +# need to investigate ways to avoid that. + + Options -MultiViews + # any reuest that does not map to a template file + # is redirected to the index. This allows us to + # map multiple routes to the same application. + RewriteEngine On + RewriteCond %{REQUEST_URI} !t_* + RewriteCond %{REQUEST_URI} !index + RewriteRule (.*) /eg/staff/index [L,DPI] + + # Uncomment the following to force SSL for everything. Note that this defeats caching # and you will suffer a performance hit. #RewriteCond %{HTTPS} off #RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [NE,R,L] + + diff --git a/Open-ILS/examples/apache_24/eg_vhost.conf.in b/Open-ILS/examples/apache_24/eg_vhost.conf.in index f530f2935e..8d70ab319e 100644 --- a/Open-ILS/examples/apache_24/eg_vhost.conf.in +++ b/Open-ILS/examples/apache_24/eg_vhost.conf.in @@ -799,6 +799,20 @@ RewriteRule ^/openurl$ ${openurl:%1} [NE,PT] +# TODO: as is, each sub-app will require a new Location. +# need to investigate ways to avoid that. + + Options -MultiViews + # any reuest that does not map to a template file + # is redirected to the index. This allows us to + # map multiple routes to the same application. + RewriteEngine On + RewriteCond %{REQUEST_URI} !t_* + RewriteCond %{REQUEST_URI} !index + RewriteRule (.*) /eg/staff/index [L,DPI] + + + # Uncomment the following to force SSL for everything. Note that this defeats caching # and you will suffer a performance hit. #RewriteCond %{HTTPS} off diff --git a/Open-ILS/src/templates/staff/README b/Open-ILS/src/templates/staff/README new file mode 100644 index 0000000000..920630183f --- /dev/null +++ b/Open-ILS/src/templates/staff/README @@ -0,0 +1,6 @@ +AnguarJS/Web Staff Client +========================= + + * TT templates loaded via JS routes must be preceded with t_* (or similar), + otherwise apache will serve the template at that path instead of the + index file since the path maps to a real template. diff --git a/Open-ILS/src/templates/staff/index.tt2 b/Open-ILS/src/templates/staff/index.tt2 new file mode 100644 index 0000000000..143e1645ac --- /dev/null +++ b/Open-ILS/src/templates/staff/index.tt2 @@ -0,0 +1,24 @@ + + + + [% l('Evergreen Staff') %] + + + + + + + [% + # load the navbar server-side to speed up initial page display + INCLUDE "staff/t_navbar.tt2"; + %] + + +
+ + [% INCLUDE "staff/t_base_js.tt2" %] + + + + + diff --git a/Open-ILS/src/templates/staff/t_base_js.tt2 b/Open-ILS/src/templates/staff/t_base_js.tt2 new file mode 100644 index 0000000000..de516d7d8d --- /dev/null +++ b/Open-ILS/src/templates/staff/t_base_js.tt2 @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/templates/staff/t_login.tt2 b/Open-ILS/src/templates/staff/t_login.tt2 new file mode 100644 index 0000000000..506e3eb3e3 --- /dev/null +++ b/Open-ILS/src/templates/staff/t_login.tt2 @@ -0,0 +1,52 @@ +
+
+
+
+
+ [% l('Sign In') %] + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ + [% l('Login Failed') %] +
+
+
+
+
+
+
+
diff --git a/Open-ILS/src/templates/staff/t_navbar.tt2 b/Open-ILS/src/templates/staff/t_navbar.tt2 new file mode 100644 index 0000000000..7ef2aca06a --- /dev/null +++ b/Open-ILS/src/templates/staff/t_navbar.tt2 @@ -0,0 +1,41 @@ + + + diff --git a/Open-ILS/src/templates/staff/t_splash.tt2 b/Open-ILS/src/templates/staff/t_splash.tt2 new file mode 100644 index 0000000000..113c55f7a1 --- /dev/null +++ b/Open-ILS/src/templates/staff/t_splash.tt2 @@ -0,0 +1,53 @@ +
+
+
+ +
+
+
+
+ +
+
+
+
[% l('Circulation and Patrons') %]
+
+ +
+
+ +
+
+
+
[% l('Item Search and Cataloging') %]
+
+
+
+ + More Things +
+
+
+
+ +
+
+
+
[% l('Administration') %]
+
+
+ +
+
+
+ +
+
diff --git a/Open-ILS/web/js/ui/default/staff/app.js b/Open-ILS/web/js/ui/default/staff/app.js new file mode 100644 index 0000000000..332dfa0ec3 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/app.js @@ -0,0 +1,88 @@ +/** + * App to drive the base page. + * Login Form + * Splash Page + */ + +angular.module('egHome', ['ngRoute', 'egStartupMod', 'egAuthMod', 'egUiMod']) + +.config(function($routeProvider, $locationProvider) { + console.log('config'); + + /** + * Route resolvers allow us to run async commands + * before the page controller is instantiated. + */ + var resolver = {delay : function(egStartup) {return egStartup.go()}}; + + $routeProvider.when('/login', { + templateUrl: './t_login', + controller: 'LoginCtrl', + resolve : {delay : function(egStartup, egAuth) { + // hack for now to kill the base ses cookie where sub-path + // apps were unable to remove it. See note at the top of + // services/auth.js about angular cookies and paths. + egAuth.logout(); + return egStartup.go(); + }} + }); + + // default page + $routeProvider.otherwise({ + templateUrl : './t_splash', + controller : 'SplashCtrl', + resolve : resolver + }); + + // HTML5 pushstate support + $locationProvider.html5Mode(true); +}) + +/** + * Login controller. + * Reads the login form and submits the login request + */ +.controller('LoginCtrl', + /* inject services into our controller. Spelling them + * out like this allows the auto-magic injector to work + * even if the code has been minified */ + ['$scope', '$location', '$window', 'egAuth', + function($scope, $location, $window, egAuth) { + $scope.focusMe = true; + + // for now, workstations may be passed in via URL param + $scope.args = {workstation : $location.search().ws}; + + $scope.login = function(args) { + args.type = 'staff'; + $scope.loginFailed = false; + + egAuth.login(args).then( + function() { + // after login, send the user back to the originally + // requested page or, if none, the home page. + // TODO: this is a little hinky because it causes 2 + // redirects if no route_to is defined. Improve. + $window.location.href = + $location.search().route_to || + $location.path('/').absUrl() + }, + function() { + $scope.args.password = ''; + $scope.loginFailed = true; + $scope.focusMe = true; + } + ); + } + } +]) + +/** + * Splash page dynamic content. + */ +.controller('SplashCtrl', ['$scope', + function($scope) { + console.log('SplashCtrl'); + } +]); + diff --git a/Open-ILS/web/js/ui/default/staff/navbar.js b/Open-ILS/web/js/ui/default/staff/navbar.js new file mode 100644 index 0000000000..d7fd4d2e89 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/navbar.js @@ -0,0 +1,38 @@ +/** + * Free-floating controller which can be used by any app + */ +function NavCtrl($scope, egStartup, egAuth) { + + $scope.logout = function() { + egAuth.logout(); + return true; + }; + + /** + * Two important things happening here. + * + * 1. Since this is a standalone controller, which may execute at + * any time during page load, we have no gaurantee that needed + * startup actions, session retrieval being the main one, have taken + * place yet. So we kick off the startup chain ourselves and run + * actions when it's done. Note this does not mean startup runs + * multiple times. If it's already started, we just pick up the + * existing startup promise. + * + * 2. Since we are returning a promise, whose success handler + * updates a value within our scope, we do not have to manually + * call $scope.$apply() in the handler to update the DOM.. When + * the promise is resolved, another $digest() loop will pick up + * our changes to $scope.username and apply them to the UI. + */ + return egStartup.go().then( + function() { + + // login page will not have a cached + if (!egAuth.user()) return; + + $scope.username = egAuth.user().usrname(); + // TODO: add workstation + } + ); +} diff --git a/Open-ILS/web/js/ui/default/staff/services/auth.js b/Open-ILS/web/js/ui/default/staff/services/auth.js new file mode 100644 index 0000000000..bf053e6c38 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/auth.js @@ -0,0 +1,102 @@ +/* Auth manager + * + * Angular cookies are still fairly primitive. + * In particular, you can't set the path. + * https://github.com/angular/angular.js/issues/1786 + */ + +angular.module('egAuthMod', ['ngCookies', 'egNetMod']) + +.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) { + + var service = { + user : function() { + return egAuthCache.get('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'); + } + }; + + /* Returns a promise, which is resolved if valid + * authtoken is found, otherwise rejected */ + service.testAuthToken = function() { + var deferred = $q.defer(); + var token = service.token(); + + if (token) { + egNet.request( + 'open-ils.auth', + 'open-ils.auth.session.retrieve', token).then( + function(user) { + if (user && user.classname) { + egAuthCache.put('user', user); + deferred.resolve(); + } else { + delete $cookies[EG_AUTH_COOKIE]; + deferred.reject(); + } + } + ); + + } else { + deferred.reject(); + } + + return deferred.promise; + }; + + /** + * Returns a promise, which is resolved on successful + * login and rejected on failed login. + */ + service.login = function(args) { + var deferred = $q.defer(); + egNet.request( + 'open-ils.auth', + 'open-ils.auth.authenticate.init', args.username).then( + function(seed) { + args.password = hex_md5(seed + hex_md5(args.password)) + egNet.request( + 'open-ils.auth', + 'open-ils.auth.authenticate.complete', args).then( + function(evt) { + if (evt.textcode == 'SUCCESS') { + $cookies[EG_AUTH_COOKIE] = evt.payload.authtoken; + deferred.resolve(); + } else { + console.error('login failed ' + js2JSON(evt)); + deferred.reject(); + } + } + ) + } + ); + + return deferred.promise; + }; + + service.logout = function() { + delete $cookies[EG_AUTH_COOKIE]; + }; + + return service; +}]); + diff --git a/Open-ILS/web/js/ui/default/staff/services/env.js b/Open-ILS/web/js/ui/default/staff/services/env.js new file mode 100644 index 0000000000..e79b520797 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/env.js @@ -0,0 +1,95 @@ +/* + * 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. + */ + +angular.module('egEnvMod', ['egNetMod', 'egPCRUDMod']) + +// 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) { + + var service = { + get : function(class_) { + return egEnvCache.get(class_); + } + }; + + service.onload = function() { + if (--this.in_flight == 0) + this.deferred.resolve(); + }; + + /* returns a promise, loads all of the specified classes */ + service.load = function(classes) { + if (!classes) classes = Object.keys(this.loaders); + this.deferred = $q.defer(); + this.in_flight = classes.length; + angular.forEach(classes, function(cls) { + service.loaders[cls]().then(function(){service.onload()}); + }); + return this.deferred.promise; + }; + + /** given a tree-shaped collection, captures the tree and + * flattens the tree for absorption. + */ + service.absorbTree = function(tree, class_) { + var list = []; + function squash(node) { + list.push(node); + angular.forEach(node.children(), squash); + } + squash(tree); + var blob = service.absorbList(list, class_); + blob.tree = tree; + }; + + /** 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); + return blob; + }; + + /* Classes (by hint) to load, their loading routines, + * and their result mungers */ + + service.loaders = { + + aou : function() { + return egPCRUD.search('aou', {parent_ou : null}, + {flesh : -1, flesh_fields : {aou : ['children', 'ou_type']}} + ).then( + 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')} + ); + } + + // org unit settings, blah, blah + } + + return service; +}]); + + + diff --git a/Open-ILS/web/js/ui/default/staff/services/idl.js b/Open-ILS/web/js/ui/default/staff/services/idl.js new file mode 100644 index 0000000000..9d9832da99 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/idl.js @@ -0,0 +1,60 @@ +/** + * IDL parser + * usage: + * var aou = new egIDL.aou(); + * var fullIDL = egIDL.classes; + * + * IDL TODO: + * + * 1. selector field only appears once per class. We could save + * a lot of IDL (network) space storing it only once at the + * class level. + * 2. we don't need to store array_position in /IDL2js since it + * can be derived at parse time. Ditto saving space. + */ +angular.module('egIDLMod', []) + +.factory('egIDL', ['$window', function($window) { + + var service = {}; + + service.parseIDL = function() { + console.log('egIDL.parseIDL()'); + + // retain a copy of the full IDL within the service + service.classes = $window._preload_fieldmapper_IDL; + + // original, global reference no longer needed + $window._preload_fieldmapper_IDL = null; + + /** + * Creates the class constructor and getter/setter + * methods for each IDL class. + */ + function mkclass(cls, fields) { + + service[cls] = function(seed) { + this.a = seed || []; + this.classname = cls; + this._isfieldmapper = true; + } + + /** creates the getter/setter methods for each field */ + angular.forEach(fields, function(field, idx) { + service[cls].prototype[fields[idx].name] = function(n) { + if (arguments.length==1) this.a[idx] = n; + return this.a[idx]; + } + }); + + // global class constructors required for JSON_v1.js + $window[cls] = service[cls]; + } + + for (var cls in service.classes) + mkclass(cls, service.classes[cls].fields); + }; + + return service; +}]); + diff --git a/Open-ILS/web/js/ui/default/staff/services/net.js b/Open-ILS/web/js/ui/default/staff/services/net.js new file mode 100644 index 0000000000..b7cd337ba3 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/net.js @@ -0,0 +1,51 @@ +/** + * Promise wrapper for OpenSRF network calls. + * http://docs.angularjs.org/api/ng.$q + * + * promise.notify() is called with each streamed response. + * + * promise.resolve() is called when the request is complete + * and passes as its value the response received from the + * last call to onresponse(). + * + * Example: Call with one response and no error checking: + * + * egNet.request(service, method, param1, param2).then( + * function(data) { console.log(data) }); + * + * Example: capture streaming responses, error checking + * + * egNet.request(service, method, param1, param2).then( + * function(data) { console.log('all done') }, + * function(err) { console.log('error: ' + err) }, + * functoin(data) { console.log('received stream response ' + data) } + * ); + */ + +angular.module('egNetMod', []) +.factory('egNet', function($q) { + + return { + request : function(service, method) { + var last; + var deferred = $q.defer(); + new OpenSRF.ClientSession(service).request({ + async : true, + method : method, + params : Array.prototype.slice.call(arguments, 2), + oncomplete : function() { + deferred.resolve(last ? last.content() : null); + }, + onresponse : function(r) { + if (last = r.recv()) + deferred.notify(last.content()); + }, + onerror : function(msg) { + deferred.reject(msg); + } + }).send(); + + return deferred.promise; + } + }; +}); diff --git a/Open-ILS/web/js/ui/default/staff/services/org.js b/Open-ILS/web/js/ui/default/staff/services/org.js new file mode 100644 index 0000000000..3941a5193f --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/org.js @@ -0,0 +1,46 @@ +angular.module('egOrgMod', ['egEnvMod', 'egPCRUDMod']) + +.factory('egOrg', ['egEnv', 'egAuth', 'egPCRUD', +function(egEnv, egAuth, egPCRUD) { + + var service = {}; + + service.get = function(node_or_id) { + if (typeof node_or_id == 'object') + return node_or_id; + return egEnv.get('aou').map[node_or_id]; + }; + + service.list = function() { + return egEnv.get('aou').list; + }; + + service.ancestors = function(node_or_id) { + var node = service.get(node_or_id); + if (!node) return []; + var nodes = [node]; + while( (node = service.get(node.parent_ou()))) + nodes.push(node); + return nodes; + }; + + service.descendants = function(node_or_id) { + var node = service.get(node_or_id); + if (!node) return []; + var nodes = []; + function descend(n) { + nodes.push(n); + angular.forEach(n.children(), descend); + } + descend(node); + return nodes; + } + + service.fullPath = function(node_or_id) { + return service.ancestors(node_or_id).concat( + service.descendants(node_or_id).slice(1)); + } + + return service; +}]); + diff --git a/Open-ILS/web/js/ui/default/staff/services/pcrud.js b/Open-ILS/web/js/ui/default/staff/services/pcrud.js new file mode 100644 index 0000000000..774f2523d6 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/pcrud.js @@ -0,0 +1,288 @@ +/** + * PCRUD client. + * + * Factory for PCRUDContext objects with pass-through service-level API. + * + * For most types of communication, where the client expects to make a + * single request which egPCRUD manages internally, use the service- + * level API. + * + * All service-level APIs (except connect()) return a promise, whose + * notfiy() channels individual responses (think: onresponse) and + * whose resolve() channels the last received response (think: + * oncomplete), consistent with egNet.request(). If only one response + * is expected (e.g. retrieve(), or .atomic searches), notify() + * handlers are not required. + * + * egPCRUD.retrieve('aou', 1) + * .then(function(org) { console.log(org.shortname()) }); + * + * egPCRUD.search('aou', {id : [1,2,3]}) + * .then(function(orgs) { console.log(orgs.length) } ); + * + * egPCRUD.search('aou', {id : {'!=' : null}}, {limit : 10}) + * .then(...); + * + * For requests where the caller needs to manually connect and make + * individual API calls, the service.connect() call will create and + * pass a PCRUDContext object as the argument to the connect promise + * resolver. The PCRUDContext object can be used to make subsequent + * pcrud calls directly. + * + * egPCRUD.connnect().then( + * function(ctx) { + * ctx.retrieve('aou', 1).then( + * function(org) { + * console.log(org.id()); + * ctx.disconnect(); + * } + * ) + * } + * ); + */ +angular.module('egPCRUDMod', []) + +// env fetcher +.factory('egPCRUD', ['$q', 'egAuth', 'egIDL', function($q, egAuth, egIDL) { + + var service = {}; + + // create service-level pass through functions + // for one-off PCRUDContext actions. + angular.forEach(['connect', 'retrieve', 'retrieveAll', + 'search', 'create', 'update', 'remove', 'apply'], + function(action) { + service[action] = function() { + var ctx = new PCRUDContext(); + return ctx[action].apply(ctx, arguments); + } + } + ); + + /* + * Since services are singleton objectss, we need an internal + * class to manage individual PCRUD conversations. + */ + var PCRUDContextIdent = 0; // useful for debug logging + function PCRUDContext() { + var self = this; + this.xact_close_mode = 'rollback'; + this.ident = PCRUDContextIdent++; + this.session = new OpenSRF.ClientSession('open-ils.pcrud'); + + this.toString = function() { + return '[PCRUDContext ' + this.ident + ']'; + }; + + this.log = function(msg) { + console.debug(this + ': ' + msg); + }; + + this.err = function(msg) { + console.error(this + ': ' + msg); + }; + + this.connect = function() { + this.log('connect'); + var deferred = $q.defer(); + this.session.connect({onconnect : + function() {deferred.resolve(self)}}); + return deferred.promise; + }; + + this.disconnect = function() { + this.log('disconnect'); + this.session.disconnect(); + }; + + this.retrieve = function(fm_class, pkey, pcrud_ops) { + return this._dispatch( + 'open-ils.pcrud.retrieve.' + fm_class, + [egAuth.token(), pkey, pcrud_ops] + ); + }; + + this.retrieveAll = function(fm_class, pcrud_ops, req_ops) { + var search = {}; + search[egIDL.classes[fm_class].pkey] = {'!=' : null}; + return this.search(fm_class, search, pcrud_ops, req_ops); + }; + + this.search = function (fm_class, search, pcrud_ops, req_ops) { + req_ops = req_ops || {}; + + var return_type = req_ops.idlist ? 'id_list' : 'search'; + var method = 'open-ils.pcrud.' + return_type + '.' + fm_class; + + if (req_ops.atomic) method += '.atomic'; + + return this._dispatch(method, + [egAuth.token(), search, pcrud_ops]); + }; + + this.create = function(list) {return this.CUD('create', list)}; + this.update = function(list) {return this.CUD('update', list)}; + this.remove = function(list) {return this.CUD('delete', list)}; + this.apply = function(list) {return this.CUD('apply', list)}; + + this.xactClose = function() { + return this._send_request( + 'open-ils.pcrud.transaction.' + this.xact_close_mode, + [egAuth.token()] + ); + }; + + this.xactBegin = function() { + return this._send_request( + 'open-ils.pcrud.transaction.begin', + [egAuth.token()] + ); + }; + + this._dispatch = function(method, params) { + if (this.authoritative) { + return this._wrap_xact( + function() { + return self._send_request(method, params); + } + ); + } else { + return this._send_request(method, params) + } + }; + + + // => connect + // => xact_begin + // => action + // => xact_close(commit/rollback) + // => disconnect + // Returns a promise + // main_func should return a promise + this._wrap_xact = function(main_func) { + var deferred = $q.defer(); + + // 1. connect + this.connect().then(function() { + + // 2. start the transaction + self.xactBegin().then(function() { + + // 3. execute the main body + main_func().then( + // main body complete + function(lastResp) { + + // 4. close the transaction + self.xactClose().then(function() { + // 5. disconnect + self.disconnect(); + // 6. all done + deferred.resolve(lastResp); + }); + }, + + // main body error handler + function() {}, + + // main body notify() handler + function(data) {deferred.notify(data)} + ); + + })}); // close 'em all up. + + return deferred.promise; + }; + + this._send_request = function(method, params) { + this.log('_send_request(' + method + ')'); + var deferred = $q.defer(); + var lastResp; + this.session.request({ + method : method, + params : params, + onresponse : function(r) { + var resp = r.recv(); + if (resp && (lastResp = resp.content())) { + deferred.notify(lastResp); + } else { + // pcrud requests should always return something + self.err(method + " returned no response"); + } + }, + oncomplete : function() { + deferred.resolve(lastResp); + }, + onerror : function(e) { + self.err(method + " failed " + e); + deferred.reject(e); + } + }).send(); + + return deferred.promise; + }; + + this.CUD = function (action, list) { + this.log('CUD(): ' + action); + + this.cud_idx = 0; + this.cud_action = action; + this.xact_close_mode = 'commit'; + this.cud_list = list; + this.cud_deferred = $q.defer(); + + if (!angular.isArray(list) || list.classname) + this.cud_list = [list]; + + return this._wrap_xact( + function() { + self._CUD_next_request(); + return self.cud_deferred.promise; + } + ); + } + + /** + * Loops through the list of objects to update and sends + * them one at a time to the server for processing. Once + * all are done, the cud_deferred promise is resolved. + */ + this._CUD_next_request = function() { + + if (this.cud_idx >= this.cud_list.length) { + this.cud_deferred.resolve(this.cud_last); + return; + } + + var action = this.cud_action; + var fm_obj = this.cud_list[this.cud_idx++]; + + if (action == 'auto') { + if (fm_obj.ischanged()) action = 'update'; + if (fm_obj.isnew()) action = 'create'; + if (fm_obj.isdeleted()) action = 'delete'; + + if (action == 'auto') { + // object does not need updating; move along + this._CUD_next_request(); + } + } + + this._send_request( + 'open-ils.pcrud.' + action + '.' + fm_obj.classname, + [egAuth.token(), fm_obj]).then( + function(data) { + // update actions return one response. + // no notify() handler needed. + self.cud_last = data; + self.cud_deferred.notify(data); + self._CUD_next_request(); + } + ); + + }; + } + + return service; +}]); + diff --git a/Open-ILS/web/js/ui/default/staff/services/startup.js b/Open-ILS/web/js/ui/default/staff/services/startup.js new file mode 100644 index 0000000000..adcb416425 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/startup.js @@ -0,0 +1,77 @@ +/** + * Coordinates all startup routines and consolidates them into + * a single startup promise. Startup can be launched from multiple + * controllers, etc., but only one startup routine will be run. + * + * If no valid authtoken is found, startup will exit early and + * change the page href to the login page. Otherwise, the global + * promise returned by startup.go() will be resolved after all + * async data is arrived. + */ + +angular.module('egStartupMod', ['egIDLMod', 'egAuthMod', 'egEnvMod']) + +.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) { + + return { + go : function (args) { + args = args || {}; + + if (egStartupCache.get('promise')) { + // startup is done, return our promise + return egStartupCache.get('promise').promise; + } + + // create a new promise and fire off startup + var deferred = $q.defer(); + egStartupCache.put('promise', deferred); + + // IDL parsing is sync. No promises required + egIDL.parseIDL(); + egAuth.testAuthToken().then( + + // testAuthToken resolved + function() { + egEnv.load(args.load_classes).then( + function() { deferred.resolve() }, + function() { + deferred.reject('egEnv did not resolve') + } + ); + }, + + // testAuthToken rejected + function() { + console.log('egAuth found no valid authtoken'); + if ($location.path() == '/login') { + console.log('egStartup resolving without authtoken on /login'); + deferred.resolve(); + } else { + // TODO: this is a little hinky because it causes 2 redirects. + // the first is the oh-so-convenient call to $location.path(), + // the second is the final href change. + $window.location.href = $location + .path('/login') + .search({route_to : + $window.location.pathname + $window.location.search}) + .absUrl(); + } + } + ); + + return deferred.promise; + } + }; +}]); + diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js new file mode 100644 index 0000000000..ec9938d9e2 --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/ui.js @@ -0,0 +1,45 @@ +/** + * UI tools and directives. + */ +angular.module('egUiMod', []) + + +/** + * + * $scope.iAmOpen = true; + */ +.directive('focusMe', +['$timeout', '$parse', +function($timeout, $parse) { + return { + link: function(scope, element, attrs) { + var model = $parse(attrs.focusMe); + scope.$watch(model, function(value) { + if(value === true) + $timeout(function() {element[0].focus()}); + }); + element.bind('blur', function() { + scope.$apply(model.assign(scope, false)); + }) + } + }; +}]) + +// +// $scope.iWantToBeSelected = true; +.directive('selectMe', +['$timeout', '$parse', +function($timeout, $parse) { + return { + link: function(scope, element, attrs) { + var model = $parse(attrs.focusMe); + scope.$watch(model, function(value) { + if(value === true) + $timeout(function() {element[0].select()}); + }); + element.bind('blur', function() { + scope.$apply(model.assign(scope, false)); + }) + } + }; +}]); diff --git a/Open-ILS/web/js/ui/default/staff/services/user.js b/Open-ILS/web/js/ui/default/staff/services/user.js new file mode 100644 index 0000000000..1503448ade --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/services/user.js @@ -0,0 +1,47 @@ +/** Service for fetching fleshed user objects. + * The last user retrieved is kept in the local cache */ + +angular.module('egUserMod', ['egNetMod', 'egAuthMod']) + +.factory('egUserCache', + ['$cacheFactory', function($cacheFactory) { + return $cacheFactory('egUserCache', {number : 1}); +}]) + + +.factory('egUser', +['$q', '$timeout', 'egNet', 'egAuth', 'egUserCache', +function($q, $timeout, egNet, egAuth, egUserCache) { + + var service = {}; + service.get = function(userId) { + var deferred = $q.defer(); + + var last = egUserCache.get('last'); + if (last && last.id() == userId) { + return $q.when(last); + + } else { + + egNet.request( + 'open-ils.actor', + 'open-ils.actor.user.fleshed.retrieve', + egAuth.token(), userId).then( + function(user) { + if (user && user.classname == 'au') { + egUserCache.put('last', user); + deferred.resolve(user); + } else { + egUserCache.remove('last'); + deferred.reject(user); + } + } + ); + } + + return deferred.promise; + }; + + return service; +}]); + -- 2.11.0