</IfModule>
</Location>
+# TODO: as is, each sub-app will require a new Location.
+# need to investigate ways to avoid that.
+<Location /eg/staff/>
+ 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]
+</Location>
+
# 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]
+
+
</IfModule>
</Location>
+# TODO: as is, each sub-app will require a new Location.
+# need to investigate ways to avoid that.
+<Location /eg/staff/>
+ 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]
+</Location>
+
+
# Uncomment the following to force SSL for everything. Note that this defeats caching
# and you will suffer a performance hit.
#RewriteCond %{HTTPS} off
--- /dev/null
+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.
--- /dev/null
+<!doctype html>
+<html ng-app="egHome" lang="[% ctx.locale %]">
+ <head>
+ <title>[% l('Evergreen Staff') %]</title>
+ <base href="/eg/staff/" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <!-- TODO: remote hosted CSS should be hosted locally instead -->
+ <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" />
+ </head>
+ <body>
+ [%
+ # load the navbar server-side to speed up initial page display
+ INCLUDE "staff/t_navbar.tt2";
+ %]
+
+ <!-- angular route-specific view -->
+ <div ng-view></div>
+ </body>
+ [% INCLUDE "staff/t_base_js.tt2" %]
+
+ <!-- angular-driven controllers -->
+ <script src="[% ctx.media_prefix %]/js/ui/default/staff/app.js"></script>
+ <script src="[% ctx.media_prefix %]/js/ui/default/staff/navbar.js"></script>
+</html>
--- /dev/null
+<!-- TODO: remotely hosted JS should be hosted locally -->
+<!-- TODO: combine and minify JS -->
+
+<!-- Bootstrap JS -->
+<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
+<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
+
+<!-- angular -->
+<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.3/angular.min.js"></script>
+<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.3/angular-route.min.js"></script>
+<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.3/angular-cookies.min.js"></script>
+
+<!-- IDL / opensrf (network) -->
+<script src="/IDL2js"></script>
+<script src="[% ctx.media_prefix %]/js/dojo/opensrf/JSON_v1.js"></script>
+<script src="[% ctx.media_prefix %]/js/dojo/opensrf/opensrf.js"></script>
+<script src="[% ctx.media_prefix %]/js/dojo/opensrf/opensrf_xhr.js"></script>
+
+<!-- needed for login -->
+<script src="[% ctx.media_prefix %]/js/dojo/opensrf/md5.js"></script>
+
+<!-- angular-driven shared services -->
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/idl.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/net.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/auth.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/pcrud.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/env.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/startup.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+
--- /dev/null
+<div class="container">
+ <div class="row">
+ <div class="col-lg-3"></div><!-- offset? -->
+ <div class="col-lg-6">
+ <fieldset>
+ <legend>[% l('Sign In') %]</legend>
+ <!--
+ login() hangs off the page $scope.
+ Values entered by the user are put into 'args',
+ which is is autovivicated if needed.
+ The input IDs are there to match the labels.
+ They are not referenced in the Login controller.
+ -->
+ <form ng-submit="login(args)">
+ <div class="form-group row">
+ <label class="col-lg-4 control-label" for="login-username">[% l('Username') %]</label>
+ <div class="col-lg-8">
+ <input type="text" id="login-username" class="form-control"
+ focus-me="focusMe" select-me="focusMe"
+ placeholder="Username" ng-model="args.username"/>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label class="col-lg-4 control-label" for="login-password">[% l('Password') %]</label>
+ <div class="col-lg-8">
+ <input type="password" id="login-password" class="form-control"
+ placeholder="Password" ng-model="args.password"/>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label class="col-lg-4 control-label" for="login-workstation">[% l('Workstation') %]</label>
+ <div class="col-lg-8">
+ <input type="text" id="login-workstation" class="form-control"
+ placeHolder="Optional. Also try ?ws=<name>"
+ ng-model="args.workstation"/>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <div class="col-lg-12">
+ <button type="submit" class="btn">[% l('Sign in') %]</button>
+ <span ng-show="loginFailed">[% l('Login Failed') %]</span>
+ </div>
+ </div>
+ </form>
+ </fieldset>
+ </div>
+ <div class="col-lg-3"></div><!-- offset? -->
+ </div>
+</div>
--- /dev/null
+<div class="navbar navbar-default navbar-static-top" ng-controller="NavCtrl">
+ <div class="navbar-collapse collapse">
+ <ul class="nav navbar-nav">
+ <li class="dropdown">
+ <a href="javascript:;" class="dropdown-toggle"
+ data-toggle="dropdown">[% l('Circulation') %]<b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <!-- note the use of target="_self" - this tells angular to
+ treat the href has a new page and not a intra-page route -->
+ <li><a href="./circ/patron/search" target="_self">[% l('Patron Search') %]</a></li>
+ <li><a href="javascript:;">Stuff 2</a></li>
+ <li><a href="javascript:;">Stuff 3</a></li>
+ <li class="divider"></li>
+ <li class="dropdown-header">A Sub Menu</li>
+ <li><a href="javascript:;">Other Stuff</a></li>
+ </ul>
+ </li>
+ <li class="dropdown">
+ <a href="javascript:;" class="dropdown-toggle"
+ data-toggle="dropdown">Thing 2<b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="javascript:;">Stuff 1</a></li>
+ <li><a href="javascript:;">Stuff 2</a></li>
+ <li><a href="javascript:;">Stuff 3</a></li>
+ <li><a href="javascript:;">...</a></li>
+ <li class="divider"></li>
+ <li class="dropdown-header">A Sub Menu</li>
+ <li><a href="javascript:;">Other Stuff</a></li>
+ </ul>
+ </li>
+ </ul>
+ <ul class="nav navbar-nav navbar-right">
+ <!-- ng-cloak tells angular to hide unresolved page variables.
+ When a value is populated, it will un-hide -->
+ <li><a ng-cloak>{{username}}</a></li>
+ <li><a href="./login" ng-click="logout()" target="_self">[% l('Log Out') %]</a></li>
+ </ul>
+ </div>
+</div>
+
+
--- /dev/null
+<div class="container">
+ <div class="row">
+ <div class="col-lg-12 text-center">
+ <img src="/xul/server/skin/media/images/portal/logo.png"/>
+ </div>
+ </div>
+ <br/>
+ <div class="row">
+
+ <div class="col-lg-4">
+ <div class="panel panel-success">
+ <div class="panel-heading">
+ <div class="panel-title text-center">[% l('Circulation and Patrons') %]</div>
+ </div>
+ <div class="panel-body">
+ <div>
+ <img src="/xul/server/skin/media/images/portal/forward.png"/>
+ <a target="_self" href="./circ/patron/search">[% l('Check Out') %]</a>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="col-lg-4">
+ <div class="panel panel-success">
+ <div class="panel-heading">
+ <div class="panel-title text-center">[% l('Item Search and Cataloging') %]</div>
+ </div>
+ <div class="panel-body">
+ <div>
+ <img src="/xul/server/skin/media/images/portal/bucket.png"/>
+ <a target="_self" href="./circ/patron/search">More Things</a>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="col-lg-4">
+ <div class="panel panel-success">
+ <div class="panel-heading">
+ <div class="panel-title text-center">[% l('Administration') %]</div>
+ </div>
+ <div class="panel-body">
+ <div>
+ <img src="/xul/server/skin/media/images/portal/helpdesk.png"/>
+ <a target="_self" href="./circ/patron/search">All the Things</a>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+</div>
--- /dev/null
+/**
+ * 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');
+ }
+]);
+
--- /dev/null
+/**
+ * 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
+ }
+ );
+}
--- /dev/null
+/* 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;
+}]);
+
--- /dev/null
+/*
+ * 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;
+}]);
+
+
+
--- /dev/null
+/**
+ * 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;
+}]);
+
--- /dev/null
+/**
+ * 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;
+ }
+ };
+});
--- /dev/null
+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;
+}]);
+
--- /dev/null
+/**
+ * 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;
+}]);
+
--- /dev/null
+/**
+ * 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;
+ }
+ };
+}]);
+
--- /dev/null
+/**
+ * UI tools and directives.
+ */
+angular.module('egUiMod', [])
+
+
+/**
+ * <input focus-me="iAmOpen"/>
+ * $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));
+ })
+ }
+ };
+}])
+
+// <input select-me="iWantToBeSelected"/>
+// $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));
+ })
+ }
+ };
+}]);
--- /dev/null
+/** 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;
+}]);
+