From 45eca2c92e761922ea063b3460e772e6f8bcc3c5 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 29 Jul 2014 16:12:57 -0400 Subject: [PATCH] removing inline log / report files before merge Signed-off-by: Bill Erickson --- web-staff-log.txt | 1168 -------------------------------------------------- web-staff-report.txt | 262 ----------- 2 files changed, 1430 deletions(-) delete mode 100644 web-staff-log.txt delete mode 100644 web-staff-report.txt diff --git a/web-staff-log.txt b/web-staff-log.txt deleted file mode 100644 index 4605d2ce09..0000000000 --- a/web-staff-log.txt +++ /dev/null @@ -1,1168 +0,0 @@ -Browser-Based Staff Client Development Log -========================================== - -2013-11-20 Templates and Apache -------------------------------- - -When a path is requested from the server, e.g. -/eg/staff/circ/patron/search, there are 2 different aspects of the file -that are of intest to us: the content of the HTML file and the path used -to retrieve the file. - -Page Content -~~~~~~~~~~~~ - -For Angular apps, the HTML page only needs to provide enough information -for Angular to load the correct application -- a seed document. A seed -document might look something like this: - -[source,html] ------------------------------------------------------------------------------ - - - - Evergreen Staff Patron - - - - - -
- - - ------------------------------------------------------------------------------ - -Note the body is a single div with an 'ng-view' tag. - -Building Pages from Seeds -~~~~~~~~~~~~~~~~~~~~~~~~~ - -With the above document we know the Angular application (egPatronApp), -we have an Angular Controller (PatronCtrl -- not strictly required -here). If we assume this page was fetched using the path -'/eg/staff/circ/patron/search', then we have all we need to build the -real page. - -The Angular App will contain a series of app-specific routes based on -the URL path. Here, our (relative) path will be '/circ/patron/search', -since the base path of the application is '/eg/staff/'. For our route -configuration we have a chunk of code like this: - -[source,js] ------------------------------------------------------------------------------ -$routeProvider.when('/circ/patron/search', { - templateUrl: './circ/patron/t_search', - controller: 'PatronSearchCtrl', - resolve : resolver // more on resolvers later... -}); ------------------------------------------------------------------------------ - -When the browser lands on '/eg/staff/circ/patron/search', Angular will -locate the template file at './circ/patron/t_search' (the app-relative -path to a Template Toolkit template), by performing an HTTP request and, -once fetched, will drive the display of the Angular template within with -the JS controller called PatronSearchCtrl and insert the content of the -template into the body of the page at the location of the
-div. - -**** -'Note': For speed, it's sometimes better to include Angular templates -directly in the delivered document so that one less HTTP request is -needed. More on that later. -**** - -Fetching the Same Page at Different URLs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The egPatronApp might support a variety of different route-specific -interfaces that are all driven by the same seed document. This means -we have to tell Apache to always deliver the same file when we access -files within a given range of URL paths. The secret to this is in -Apache Rewrite configuration. - -For example: - -[source,conf] ------------------------------------------------------------------------------ - - Options -MultiViews - RewriteEngine On - RewriteCond %{PATH_INFO} !/staff/circ/patron/index - RewriteCond %{PATH_INFO} !/staff/circ/patron/t_* - RewriteRule .* /eg/staff/index [L,DPI] - ------------------------------------------------------------------------------ - -In short, any URL path that does not map to the index file or to a file -whose name starts with "t_" (more on 't_' below) will result in Apache -rewriting the request to deliver the index file (i.e. our seed -document). - -So, in our example, a request for '/eg/staff/circ/patron/search', will return -the index file found at '/eg/staff/circ/patron/index', which maps on the -server side to the Template Toolkit file at -'/path/to/templates/staff/circ/patron/index.tt2'. - -Two complications arise from this approach. Help appreciated! -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Simpler rewrite rules exist in the wild... -++++++++++++++++++++++++++++++++++++++++++ - -But, they do not take into account that we are fetching Template -Toolkit-generated files instead of vanilla HTML. The rules I found -online take the form of "if it's not a real file, return the index", but -none of the files we fetch are real files, since they are internally -mapped to Template Toolkit files. This is why I'm using the 't_' -prefix. It makes the mapping trivial. I'm all ears for better solution. - -Configuration Explosion -+++++++++++++++++++++++ - -This I see as a real problem, but one that certainly has a solution. -The configuration chunk above is such that we need a new chunk for each -top-level Angular app. This will quickly get out of hand. A single, -dynamic configuration that can map elemenents of arbitrarily-nested -paths (or possibly a small set with predefined path depths) to the -correct index file would be ideal. - -UPDATE: 2013-12-12 Apache Config Repaired -+++++++++++++++++++++++++++++++++++++++++ - -I finally got around to fixing the Apache configuration. It now supports -arbitrary nesting via EGWeb.pm extension. - -[source,conf] ------------------------------------------------------------------------------ - - Options -MultiViews - PerlSetVar OILSWebStopAtIndex "true" - # ... - ------------------------------------------------------------------------------ - - -2013-11-21 Angular $scope inheritance -------------------------------------- - -Consider the following document: - -[source,html] ------------------------------------------------------------------------------ -
-
Top: {{attr1}}
-
-
Sub: {{attr1}}
-
-
------------------------------------------------------------------------------ - -And the following code: - -[source,js] ------------------------------------------------------------------------------ -.controller('TopCtrl', function($scope) { $scope.attr1 = 'top-attr' }) -.controller('SubCtrl', function($scope) { }) ------------------------------------------------------------------------------ - -The output: - -[source,sh] ------------------------------------------------------------------------------ -Top: top-attr -Sub: top-attr ------------------------------------------------------------------------------ - -Now, if we apply a value in the child: - -[source,js] ------------------------------------------------------------------------------ -.controller('SubCtrl', function($scope) { $scope.attr1 = 'sub-attr' }) ------------------------------------------------------------------------------ -[source,sh] ------------------------------------------------------------------------------ -Top: top-attr -Sub: sub-attr ------------------------------------------------------------------------------ - -Setting a value in the child does not change the value in the parent. -Scopes are inherited prototypically, which means attributes from a -parent scope are copied into the child scope and the child's version of -the attribute masks that of the parent. - -For both scopes to share a single value, either the parent needs to -provide a setter function on the value: - -[source,js] ------------------------------------------------------------------------------ -.controller('TopCtrl', function($scope) { - $scope.attr1 = 'top-attr'; - $scope.setAttr1 = function(val) { - $scope.attr1 = val; - } -}) -.controller('SubCtrl', function($scope) { - $scope.setAttr1('sub-attr'); -}) ------------------------------------------------------------------------------ - -Produces.. - -[source,sh] ------------------------------------------------------------------------------ -Top: sub-attr -Sub: sub-attr ------------------------------------------------------------------------------ - -Or the value in question needs to be stored within a structure. - -[source,html] ------------------------------------------------------------------------------ -
-
Top: {{attrs.attr1}}
-
-
Sub: {{attrs.attr1}}
-
-
------------------------------------------------------------------------------ - -[source,js] ------------------------------------------------------------------------------ -.controller('TopCtrl', function($scope) { $scope.attrs = {attr1 : 'top-attr'} }) -.controller('SubCtrl', function($scope) { $scope.attrs.attr1 = 'sub-attr' }) ------------------------------------------------------------------------------ - -Also produces.. - -[source,sh] ------------------------------------------------------------------------------ -Top: sub-attr -Sub: sub-attr ------------------------------------------------------------------------------ - -Since the child scope is not clobbering the 'attrs' attribute, both -scopes share the value, which is a reference to a single object. - -This last is approach is the best for providing two-way binding across -both scopes. For example: - -[source,html] ------------------------------------------------------------------------------ -
-
Top:
-
-
Sub:
-
-
------------------------------------------------------------------------------ - -With this, typing a value into the first input, will set the value -for both scopes. - -For more, see -https://github.com/angular/angular.js/wiki/Understanding-Scopes[Understanding-Scopes]. - -2013-11-25 Prototype Install and Hacking ----------------------------------------- - -Installing the code -~~~~~~~~~~~~~~~~~~~ - -The code lives at http://git.evergreen-ils.org/?p=working/Evergreen.git;a=shortlog;h=refs/heads/collab/berick/web-staff-proto[working => collab/berick/web-staff-proto] - -**** -The branch is a child of Evergreen master and will be kept up to date -with master as development progresses. -**** - -Install the code in the usual Evergreen manner -- No DB upgrades are -required to date, but there are some IDL changes -- and apply the Apache -configuration changes found in eg_vhost.conf: - -[source,conf] ------------------------------------------------------------------------------ -# see notes above on how we can condense the configuration... -# TODO: some of this is clearly redundant and needs to be rearranged - - Options -MultiViews - RewriteEngine On - RewriteCond %{PATH_INFO} !/staff/index - RewriteCond %{PATH_INFO} !/staff/t_* - RewriteRule .* /eg/staff/index [L,DPI] - - Header append Cache-Control "public" - - - SetOutputFilter DEFLATE - BrowserMatch ^Mozilla/4 gzip-only-text/html - BrowserMatch ^Mozilla/4\.0[678] no-gzip - BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html - SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary - - Header append Vary User-Agent env=!dont-vary - - - - - Options -MultiViews - RewriteEngine On - RewriteCond %{PATH_INFO} !/staff/cat/bucket/record/index - RewriteCond %{PATH_INFO} !/staff/cat/bucket/record/t_* - RewriteRule .* /eg/staff/cat/bucket/record/index [L,DPI] - - - Options -MultiViews - RewriteEngine On - RewriteCond %{PATH_INFO} !/staff/circ/patron/index - RewriteCond %{PATH_INFO} !/staff/circ/patron/t_* - RewriteRule .* /eg/staff/circ/patron/index [L,DPI] - - - - Header append Cache-Control "public" - - - SetOutputFilter DEFLATE - BrowserMatch ^Mozilla/4 gzip-only-text/html - BrowserMatch ^Mozilla/4\.0[678] no-gzip - BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html - SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary - - Header append Vary User-Agent env=!dont-vary - - - ------------------------------------------------------------------------------ - -Once installed, point Chrome or Firefox at https://hostname/eg/staff/ to see -the splash page. - -Hacking -~~~~~~~ - -Most of the code should seem familiar. The Template Toolkit templates -behave as usual. For modifying scripts and Angular-loaded templates, I -highly recommend Chrome's "Disable Cache (while dev tools is open)" option -found under the Javascript console (dev tools) configuration menu (gear -icon, bottom right) since Chrome aggressively caches the templates. - -2013-11-26 Getting Started with Testing ---------------------------------------- - -Today I wrote one unit test for a stub controller from -https://github.com/angular/angular-seed[angular-seed]. -Here's what I did. I'm still figuring this out, so bear with me... - - * On my desktop, I installed node.js and npm (node package manager) and -the node plugin 'karma'. - -[source,sh] ------------------------------------------------------------------------------ -% sudo apt-get install nodejs npm -# node.js is installed at 'nodejs' -- npm, etc. assume 'node' -% sudo ln -s /usr/bin/nodejs /usr/bin/node -% sudo npm install -g karma ------------------------------------------------------------------------------ - - * Clone the angular-seed repository, which provides a stub test environment - -[source,sh] ------------------------------------------------------------------------------ -% git clone https://github.com/angular/angular-seed ------------------------------------------------------------------------------ - - * Modify the angular-seed test script, which makes some odd assumptions - about the location of binaries - -[source,diff] ------------------------------------------------------------------------------ -diff --git a/scripts/test.sh b/scripts/test.sh -index 972001f..f6db762 100755 ---- a/scripts/test.sh -+++ b/scripts/test.sh -@@ -6,4 +6,5 @@ echo "" -echo "Starting Karma Server (http://karma-runner.github.io)" -echo "-------------------------------------------------------------------" - --$BASE_DIR/../node_modules/karma/bin/karma start $BASE_DIR/../config/karma.conf.js $* -+#$BASE_DIR/../node_modules/karma/bin/karma start $BASE_DIR/../config/karma.conf.js $* -+karma start $BASE_DIR/../config/karma.conf.js $* ------------------------------------------------------------------------------ - - * Modify the stock controller and controller unit test to do something - (very simple). - -[source,diff] ------------------------------------------------------------------------------ -diff --git a/app/js/controllers.js b/app/js/controllers.js -index cc9d305..679570d 100644 ---- a/app/js/controllers.js -+++ b/app/js/controllers.js -@@ -3,9 +3,10 @@ - /* Controllers */ - - angular.module('myApp.controllers', []). -- controller('MyCtrl1', [function() { -+ controller('MyCtrl1', ['$scope', function($scope) { -+ $scope.foo = 'hello'; - - }]) -diff --git a/test/unit/controllersSpec.js b/test/unit/controllersSpec.js -index 23f6b09..ec741a6 100644 ---- a/test/unit/controllersSpec.js -+++ b/test/unit/controllersSpec.js -@@ -3,11 +3,18 @@ - /* jasmine specs for controllers go here */ - - describe('controllers', function(){ - beforeEach(module('myApp.controllers')); - -+ // create the scope, instantiate the controller -+ var ctrl, scope; -+ beforeEach(inject(function ($rootScope, $controller) { -+ scope = $rootScope.$new(); -+ ctrl = $controller('MyCtrl1', {$scope: scope}); -+ })); - - it('should ....', inject(function() { - //spec body -+ expect(scope.foo).toEqual('hello'); - })); ------------------------------------------------------------------------------ - - * Launched the test, which fires up Chrome and logs the output to the - terminal - -[source,sh] ------------------------------------------------------------------------------ -CHROME_BIN=/usr/bin/chromium-browser scripts/test.sh - -Starting Karma Server (http://karma-runner.github.io) -INFO [karma]: Karma v0.10.5 server started at http://localhost:9876/ -INFO [launcher]: Starting browser Chrome -INFO [Chromium 30.0.1599 (Ubuntu)]: Connected on socket 344641UsyCbWCVxk_28H -Chromium 30.0.1599 (Ubuntu): Executed 5 of 5 SUCCESS (0.612 secs / 0.088 secs) ------------------------------------------------------------------------------ - -All 5 tests succeeded, even my rigorous $scope.foo='hello' test. - -Next steps for testing will be to create an environment around the staff -client code so we can write some real tests... - -2013-12-02 Scopes, Services, and Routing ----------------------------------------- - -If you have two routes like so, - -[source,js] ------------------------------------------------------------------------------ -$routeProvider.when('/circ/patron/1/items_out', {...}); -$routeProvider.when('/circ/patron/1/holds', {...}); ------------------------------------------------------------------------------ - -You can access these two pages by changing your browser's URL or you can -use links/actions in the page to jump between routes using pushstate -routing. - -Pushsate routing allows you to change the browser URL and have the JS -code respond to the change without actually reloading the web page. In -short, it makes page switching faster. It does this in two main ways. - -1. The browser does not have to fetch the page and scripts from the -server (or cache) or re-parse and execute scripts that run at page load -time. - -2. If two pages share data, which is common for route-linked pages, then -it's possible for the second page to access data retrieved by the first, -without having to re-fetch it from the server. - -How do we manage routing and caching with angular scopes and services? - -Any time a path is loaded in Angular, regardless of how the path was -accessed (routing or initial page load), all controllers within the page -are re-initialized with an empty $scope. Naturally, this means that any -data that was loaded into a $scope will no longer be available when the -next route is accessed. - -To persist data across routes, it has to be managed outside of the -controller. The Angular solution is to use a service. Generally -speaking, Angular services provide re-usable chunks of logic that are -scope and controller-agnostic. They are factory classes which produce -singleton objects, instantiated once at page load, and persisted through -all routes. Because of this, they double as a great place to store -route-global data (i.e. data that should be cached between all of our -known routes). - -Some examples, with lots of extra stuff left out: - -[source,js] ------------------------------------------------------------------------------ -.controller('PatronItemsOut', function($scope) { - fetchPatronFromServer(id).then( - function(p) { $scope.patron = p }); -}) - -.controller('PatronHolds', function($scope) { - fetchPatronFromServer(id).then( - function(p) { $scope.patron = p }); -}) ------------------------------------------------------------------------------ - -Here we fetch the patron object from the server every time the -PatronItemsOut or PatronHolds controllers are instantiated. Since -these two controllers are managed within the same angular app, they are -accessible via routing and can theoretically share date. Fetching the -user every time is wasteful. - -Here's an example where we mange user retrieval with a service. - -[source,js] ------------------------------------------------------------------------------ -.factory('patronService', function() { - var service = { - // only cache the last-accessed user. - // caching all accessed users is a recipe for memory leaks - current : null - }; - service.fetchPatron = function(id) { - // on initial page load, service.current will be null - if (service.current && service.current.id() == id) { - // no need to fetch! - } else { - // fetch patron and set service.current = patron - } - } - return service; -}) - -.controller('PatronItemsOut', function($scope, patronService) { - patronService.fetchPatron(id) - $scope.patron = function() { return patronService.current } -}) - -.controller('PatronHolds', function($scope, patronService) { - patronService.fetchPatron(id) - $scope.patron = function() { return patronService.current } -}) ------------------------------------------------------------------------------ - -Now the patron object lives within our patronService Angular service. It -will persist through page routes until we replace the value or reload -the page. Note that we are now accessing our patron at $scope.patron() -instead of $scope.patron, because $scope.patron() is guaranteed to always -return the correct value for the current patron. Angular templates are -perfectly happy to work with functions intead of attributes. - -[source,html] ------------------------------------------------------------------------------ -
Username: {{patron().usrname()}}
------------------------------------------------------------------------------ - -2013-12-03 What About Workstations? Printing? ----------------------------------------------- - -Why is the workstation optional in the prototype login page? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -I mentioned this briefly toward the bottom of my old "Hey, let's build a -prototype!" -http://yeti.esilibrary.com/dev/pub/EvergreenWebStaffClientPrototype.pdf[proposal]. -I've heard the question again from a number of people, so I'll answer it -here in a little more detail. - -Short answer first, yes, the plan is to continue requiring workstations -for the browser-based client. The fact that they are not required in -the prototype is the result of an unresolved technical hurdle. - -What's the problem, exactly? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -One of the main benefits of XULRunner is that it runs on your computer -as a trusted application. XULRunner can open files on your computer, -create raw network sockets, and it can talk directly to your printers. -Web browsers, being tasked with executing all kinds of untrusted code, -have a whole host of restrictions that prevent that level of access. -This is a good thing for security, but it makes accomplishing some tasks -understandably much more difficult. This lack of access in the browser -may be the biggest challenge we face in developing a browser-based -client. - -There are two classes of problems that we need to resolve: access to -files and access to printers. - -File access is important for things like storing the workstation -information in a way that is not easily deleted on accident and can be -shared between more than one browser. We need the same level of control -for offline transaction data as well. There are probably more... - -Modern browsers will let you store quite a bit of data (~5MB for -localStorage), and in some browsers you can manually raise the storage -limit, but they won't let you control where that data lives or allow -other applications to access the data. They also make it fairly easy to -delete the data en masse within the various debug consoles, which is -obviously bad for files we want to stick around indefinitely. - -For printing, all you can do from the browser is say "print, please" -and then the user is shown a print dialog. Different browsers have -different settings for controlling whether the dialog appears, etc. but -they are non-standard and still not as flexible as we need. What we -need is a way for the web application to send print data to different -printers based on the context of the action, without requiring the user -to manually select the printer each time. - -What's the solution? -~~~~~~~~~~~~~~~~~~~~ - -There are probably multiple solutions. The most promising so far, which -was discussed at the http://goo.gl/oKo5yt[Evergreen Hackaway] is that of -a small service which runs on the client machine independent of the browser -and performs privileged tasks that the browser cannot. It would likely -operate as a small, local web service, accepting connections from clients -(i.e. browsers) on the local machine. Those connections may take the form -of Websocket connections or XMLHttpRequest via -http://en.wikipedia.org/wiki/Cross-origin_resource_sharing[cross-origin resource sharing] -(though, I'm not sure the latter approach would actually work). - -With this approach, we get to resolve all of the browser limitations -with one, cross-platform, browser-agnostic add-on. Depending on how -it's implemented, we may also get to leverage printing APIs which are -more powerful than those found in XULRunner. - -This probably goes without saying, but such an add-on, whatever form -it takes, will *not* be a *requirement* for using the staff client. -One aspect of a browser-based client that I find most appealing is the -ease of access from different devices and locations. If I were using a -tablet, where the add-on is not (or conceivably cannot be) installed, -I would still expect to be able to log in, possibly with a workstation -(via bookmark/URL or typed in), possibly without, and perform, say, a -patron lookup. If I don't need the extended features, I should not be -required to have them installed. - -Finally, it needs a better name than "add-on" (Add-On 2.0!) and it needs -developers/sponsors, and all that fun stuff. - -2013-12-04 Promises in AngularJS --------------------------------- - -One of my favorite new-ish tools in the Javascript world are called -"Promises". - -**** -If a function cannot return a value or throw an exception without -blocking, it can return a promise instead. A promise is an object that -represents the return value or the thrown exception that the function -may eventually provide. A promise can also be used as a proxy for a -remote object to overcome latency. -- https://github.com/kriskowal/q -**** - -Typically, with asynchronous network calls, the caller will make the -call and pass in a callback function, which is invoked when the async -call returns. This works well enough for single calls, but it does not -scale well. Promises allow us to manage collections of asynchronous -calls much more elegantly. - -Here's a quick example: - -[source,js] ------------------------------------------------------------------------------ -// non-promise api call -request(service, method, params, {oncomplete : callback}); - -// promise-based call -request(service, method, params).then(callback); ------------------------------------------------------------------------------ - -At first, the difference seems trivial, but it becomes more -pronounced as more requests are added. - -Batch Parallel Request Example -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -[source,js] ------------------------------------------------------------------------------ -// non-promise batch -// send three, parallel requests and track the -// responses to see when all are done -var expected = 3; -var checkComplete = function() { - if (--expected == 0) { // decrement w/ each response - // all responses received, move on to other things - } -} -request(s, m, p, function(r) { /*do stuff*/; checkComplete() }); -request(s, m, p, function(r) { /*do stuff*/; checkComplete() }); -request(s, m, p, function(r) { /*do stuff*/; checkComplete() }); - -// promise-based batch -var promises = []; -promises.push(request(s, m, p).then(function(r) {/*do stuff*/})); -promises.push(request(s, m, p).then(function(r) {/*do stuff*/})); -promises.push(request(s, m, p).then(function(r) {/*do stuff*/})); -$q.all(promises).then( - function() { - // all responses received, move on to other things - } -); - -// $q.all() takes a collection of promise objects and returns a new -// promise which is resolved when all of the embedded promises are resolved ------------------------------------------------------------------------------ - -Not only is the code a little cleaner, but it flows in the same -direction as execution. In other words, checkComplete(), which is the -last-executed code in the non-promise example is defined before the -requests are sent. This creates a spaghetti effect in the code, where -you have to jump around to follow the logic. Not so in the promise -example. It flows top to bottom. - -Attaching to Promises -~~~~~~~~~~~~~~~~~~~~~ - -Another neat trick you can do w/ promises is create a promise to -represent a long-running task. When clients ask for the result of the -task and the task is not yet complete, the promise can be returned to -any number of clients. - -[source,js] ------------------------------------------------------------------------------ -function longTask() { - if (this.promise) { - return this.promise; - } else { - this.promise = performAsyncTask(); - return this.promise; - } -} - -// the first call kicks off the long-running process -longTask().then(/*func*/); - -// subsequent calls from other locations simply pick up the existing promise - -// different location in the code -longTask().then(/*func*/); - -// different location in the code -longTask().then(/*func*/); - -// different location in the code -longTask().then(/*func*/); - -// when longTask() finally completes, all promise handlers are run -// Also, if longTask() was *already* complete, the promise handler -// will be immediately run. ------------------------------------------------------------------------------ - -This structure is used to great effect in the prototype page -initialization code. Independent controllers that all want to ensure -the client has authenticated and retrieved necessary data will call the -startup code, the first will kick it off, and the others will latch on -the outstanding promise. - -As a final bonus to using promises within Angular, promise resolution -causes another $digest() run in Angular, which causes templates to get -updated. - -[source,js] ------------------------------------------------------------------------------ -egNet.request( - 'open-ils.actor', - 'open-ils.actor.user.retrieve', - egAuth.token(), userId -).then( - function(user) { - $scope.patron = patron; - } -); ------------------------------------------------------------------------------ -[source,html] ------------------------------------------------------------------------------ - -
Username: {{patron.usrname()}}
------------------------------------------------------------------------------ - -Promises also support reject() and notify() handlers for failed requests -and intermediate messages. In my egNet module, I'm leveraging notify() -for streaming responses. - -[source,js] ------------------------------------------------------------------------------ -egNet.request(service, method, params).then( - function(finalResp) { console.log('all done') }, - function() { console.error('request failed!') }, - function(resp) { console.log('got a response ' + resp) } -); -// The egNet module has additional examples and docs. ------------------------------------------------------------------------------ - -For the full technical rundown, see also -http://docs.angularjs.org/api/ng.$q[Angular $q Docs]. - -2013-12-06 Template Cornucopia and Dynamic Strings Experiment -------------------------------------------------------------- - -Using Angular on top of Template Toolkit gives us lots of options for -managing templates. TT lets us INCLUDE (etc.) shared templates on the -server. AngularJS lets us ng-include (etc.) both inline templates -(i.e. delivered within the main document) and lazy-loaded templates, -fetched as needed from the server. - -When to use each approach comes down to how each template is used in the -application. Is it needed on page load? Is it needed on every page? -Is it needed often, but not on page load? Is it needed rarely? You -have to weigh the cost of adding the template contents into the main -page body (does it add a lot of bytes?) to the cost of (potentially) -having to fetch it over the network as a separate document. - -Some examples: - -Classic TT Template -~~~~~~~~~~~~~~~~~~~ - -In the prototype, I'm loading the navigation template (t_navbar.tt2) -within the base document, i.e. the document used as the base template -for all top-level index files. - -[source,sh] ------------------------------------------------------------------------------ -[% INCLUDE "staff/t_navbar.tt2" %] ------------------------------------------------------------------------------ - -I'm doing this because the navbar is used on every page. It makes no -sense to load the template as a separate file from the server, because -that just increases the network overhead. - -Inline Angular Template -~~~~~~~~~~~~~~~~~~~~~~~ - -Angular templates, regardless of origin, must be represented as -addressable chunks. To give an inline chunk of HTML an "address", it -can be inserted into a ------------------------------------------------------------------------------ - -The address is simply the ID, so in this example, the modal dialog is -invoked like so: - -[source,js] ------------------------------------------------------------------------------ -$modal.open({templateUrl : 'uncat_alert_dialog', controller : [...]}); ------------------------------------------------------------------------------ - -**** -$modal is an angular-ui-bootstrap service. It's not part of core AngularJS. -**** - -Lazy-Loaded Angular Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -These behave much like inline templates, except the template file is -fetched from the server as needed. - -To change the inline template example above into a lazy-loaded template, -drop the ------------------------------------------------------------------------------ - -And here's the confirm dialog code: - -[source,js] ------------------------------------------------------------------------------ -egConfirmDialog.open( - egCheckinStrings.COPY_ALERT_MSG_DIALOG_TITLE, - evt.payload, // copy alert message text - { copy_barcode : args.copy_barcode, - ok : function() { - // on confirm, redo checkout w/ override - performCheckin(args, true) - }, - cancel : function() { - // on cancel, push the event on the list - // to show that it happened - checkinSvc.checkins.items.push(evt); - } - } -); ------------------------------------------------------------------------------ - -You may wonder why I created these as services instead of directives. -http://www.befundoo.com/blog/angularjs-popup-dialog/[These folks], who -developed some similar code, do a pretty good job of explaining why a service -is best in this context. (See "Why create the AngularJS Popup Dialog -Service?"). More on Angular directives later. - -2013-12-13 Unit Tests with AngularJS/Karma/Jasmine --------------------------------------------------- - -We now have 4 unit tests in the repository! For now, the files live -under Open-ILS/tests/staffweb/. - -Running Unit Tests -~~~~~~~~~~~~~~~~~~~ - -These instructions replace my earlier instructions at -link:web-staff-log.html#_2013_11_26_getting_started_with_testing[2013-11-26 Getting Started with Testing]. - -[source,sh] ------------------------------------------------------------------------------ -# install node.js -% sudo apt-get install nodejs npm - -# node.js is installed at 'nodejs' -- npm, etc. assume 'node' -% sudo ln -s /usr/bin/nodejs /usr/bin/node - -# install karma test engine node plugin -% sudo npm install -g karma - -# fetch a copy of angular / angular-ui-bootstrap -% sudo apt-get install curl # if needed -% cd Open-ILS/tests/staffweb/ -% ./update-angular.sh 1.2.4 0.7.0 - -# run the tests -% CHROME_BIN=chromium-browser scripts/test.sh ------------------------------------------------------------------------------ - -The output... - -[source,sh] ------------------------------------------------------------------------------ -Starting Karma Server (http://karma-runner.github.io) -\------------------------------------------------------------------- -INFO [karma]: Karma v0.10.5 server started at http://localhost:9876/ -INFO [launcher]: Starting browser Chrome -INFO [Chromium 30.0.1599 (Ubuntu)]: Connected on socket XmJAj4CwHlta3NuSQwC9 -Chromium 30.0.1599 (Ubuntu): Executed 4 of 4 SUCCESS (0.623 secs / 0.073 secs) -\----------------------------------------------------------------------------- ------------------------------------------------------------------------------ - -Sample Tests -~~~~~~~~~~~~ - -[source,js] ------------------------------------------------------------------------------ -/** patronSvc tests **/ -describe('patronSvcTests', function() { - - it('patronSvc should start with empty lists', inject(function(patronSvc) { - expect(patronSvc.patrons.count()).toEqual(0); - })); - - it('patronSvc reset should clear data', inject(function(patronSvc) { - patronSvc.checkout_overrides.a = 1; - expect(Object.keys(patronSvc.checkout_overrides).length).toBe(1); - patronSvc.resetPatronLists(); - expect(Object.keys(patronSvc.checkout_overrides).length).toBe(0); - expect(patronSvc.holds.items.length).toBe(0); - })); -}) ------------------------------------------------------------------------------ - -These tests are very basic. They ensure that the patronSvc service -(defined in circ/patron/app.js) is properly initialized and that the -resetPatronLists() function behaves as expected. - -Initial Reactions -~~~~~~~~~~~~~~~~~ - -These types of tests can be very useful for testing complex, client-side -code. However, just like with Evergreen Perl/C unit tests, JS unit -tests are not meant to be executed in a live environment. You can test -code that does not require network IO (as above) and you have the -option of creating mock data which is used in place of network-retrieved -data. - -I believe the best long-term approach, however, will be full coverage -testing with the live, end-to-end test structure, also supported by Angular. -It requires more setup and I hope to have time -to research it more fully soon. I say this because Evergreen has fairly -complex data requirements (IDL required, data fetched through opensrf -instead of bare XHR) and practically all of the prototype code uses -network IO or assumes the presence of a variety of network-fetched data, -which will be non-trivial to mock up and will only grow over time. -Fingers crossed that it's not a beast to get running. More on this as -the story develops.... - -2013-12-13 #2 - Brief Update ----------------------------- - - * Last week the prototype acquired a locale selector. It uses the - existing locale bits from EGWeb.pm, so it was easy to add. - - * The prototype is nearing completion! - - * Beware testing on small screens, as the CSS causes the screen to - break and flow (e.g. for mobile devices) a little too aggressively - right now. TODO. - -2013-12-16 JS Minification --------------------------- - -Angular has some built-in magic which forces us to take special care when -preparing for Javascript minification. For the details, see the "A Note -On Minification" section of the -http://docs.angularjs.org/tutorial/step_05[Angular Totorial Docs]. Briefly, -there's a short (auto-magic) way and a long way to declare dependencies -for services, controllers, etc. To use minificaiton, you have to use the -long way. To make sure everything is working as expected, I've set up -a test. - -Testing Minification -~~~~~~~~~~~~~~~~~~~~ - - * Download jar file from https://github.com/yui/yuicompressor/releases[YUICompressor Releases] - * Create a staff.min.js bundle from commonly loaded files: - -[source,sh] ------------------------------------------------------------------------------ -% cd /openils/var/web/js/ui/default/staff - -# note the files have to be combined in a specific order -% cat /openils/var/web/js/dojo/opensrf/JSON_v1.js \ -/openils/var/web/js/dojo/opensrf/opensrf.js \ -/openils/var/web/js/dojo/opensrf/opensrf_xhr.js \ -services/core.js \ -services/idl.js \ -services/event.js \ -services/net.js \ -services/auth.js \ -services/pcrud.js \ -services/env.js \ -services/org.js \ -services/startup.js \ -services/ui.js \ -navbar.js \ -| java -jar yuicompressor-2.4.8.jar --type js -o staff.min.js ------------------------------------------------------------------------------ - - * modify /openils/var/templates/staff/t_base_js.tt2 - ** comment out ------------------------------------------------------------------------------ - - * Reload the page. - -I'm happy to report all pages load successfully with (mostly) minimized JS. -Next on the minification front would be baking minification into the -release-building process. - -Future Topics... ----------------- - - * My (currently) preferred parent scope, child scope, service pattern - * Displaying bib records in the prototype - - -//// -vim: ft=asciidoc -//// diff --git a/web-staff-report.txt b/web-staff-report.txt deleted file mode 100644 index 2e8ac73fed..0000000000 --- a/web-staff-report.txt +++ /dev/null @@ -1,262 +0,0 @@ -Browser-Based Staff Client After-Action Report -============================================== -:Author: Bill Erickson -:Email: berick@esilibrary.com -:Date: 2013-12-18 - -References ----------- - - * http://yeti.esilibrary.com/dev/pub/techspecs/web-staff-prototype.html[Prototype Tech Specs] - * http://git.evergreen-ils.org/?p=working/Evergreen.git;a=shortlog;h=refs/heads/collab/berick/web-staff-proto[Code in Evergreen Working Repository] - * http://yeti.esilibrary.com/dev/pub/web-staff-log.html[Development Log] - * http://angularjs.org/[AngularJS] - * http://getbootstrap.com/[Bootstrap CSS] - * http://angular-ui.github.io/bootstrap/[AngularJS for Bootstrap] - -Prototype Development Goals ---------------------------- - -In addition to basic functionality, a number of specific development goals -were listed in the tech specs. Below is a breakdown of how each of these -were met and what remains to do for each. - -Speed / Responsiveness to user actions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - * Applications are developed in functional groups. Within each group, - page reloads are not required. This allows navigation between - tightly-linked functional areas without page reloads, e.g. jumping - between the patron items out list and the patron holds list. - - * All data-fetching server communication runs asynchronously, allowing - other browser processing to occur while waiting for responses. This - also prevents the possibility of browser lock-up on long-running - requests. (See: egNet and egPCRUD). - - * When possible, batches of data requests are fired at the - same time, instead of serially, to maximally leverage browser - parallel XMLHttpRequest connections. (E.g. egEnv.load()) - - * Special care was taken to avoid fetching data from the server until - needed, reducing the cost of up-front data loading. - - * In some cases, primarily in the Record Buckets UI, traditional Perl API - calls were replaced with PCRUD calls. PRCUD is much faster than Perl. - - * The code was implemented to support Javascript minification and it has - been confirmed to function correctly. When configured, this reduces - page load times. - - * Apache gzip compression and cache optimizations added to sample Apache - configs. These were added because the existing settings were not - sufficient to satisfy standard web optimization practices. (See Chrome - Audits). Apache configuration cleanup and consolidation should be - considered so that other parts of the site get the same benefits. - - -TODO Items -^^^^^^^^^^ - - * Improved support for streaming and paging need to be added to some API calls - * New API calls could be developed to better deliver targeted data sets to - reduce the overall number of API calls. - * To leverage streaming responses (which can reduce the number of API calls, - in some cases by a wide margin, and allow long-running calls, like - Vandelay imports), support for WebSockets is still suggested. - - -Standards Compliance -~~~~~~~~~~~~~~~~~~~~ - - * Prototype developed for and tested with Chrome and Firefox. - ** Incidentally, also confirmed to work on Safari and Opera. - * No exotic components were used. - - -Ease/Speed of Development -~~~~~~~~~~~~~~~~~~~~~~~~~ - -**** -This is just my opinion. -**** - -Learning AngularJS (and Bootstrap) is neither easy or terribly -difficult, but it does take some getting used to, especially if you -are accustomed to writing code for the existing Dojo-driven Evergreen -interfaces. However, once you're used to it, building interfaces is -considerably easier in Angular, basically because there is less -code to write. - -Probably the biggest hurdle for new developers will be resisting the urge to -fall back on manual DOM manipulation and bypassing the Angular environment -altogether, when it suits his/her needs. This should be avoided, as a mix of Angular and non-Angular -code will be difficult to support in the long run. There are varying levels -of Angular code purity and the more Angular-correct the code is, the easier -unit testing, etc. will be, but more important than Angular perfection is -simply staying within the Angular universe. - -There are two basic ways to do this: - - * Avoid global variables/functions - * Avoid referencing DOM elements (outside of Angular directives). - -With these rules in place, you're pretty much forced to ask, "what would -Angular do?" - -Getting Help -^^^^^^^^^^^^ - -There are numerous online resources, including tutorials, videos, -and documentation for AngularJS (and Bootstrap). Compared to Dojo, -AngularJS has a much more visible online community presence. For -example, as of today (2013-12-18) there are 19,803 threads tagged with -"AngularJS" on http://stackoverflow.com/tags[Stack Overflow], compared -with 6,096 tagged with "Dojo". (This says nothing about the quality of -the questions, of course, only that there are more of them. It could -be that Angular is more difficult, thus more questions, but my personal -experience with Dojo suggests otherwise). This is especially telling -when you consider that Angular is about half the age of Dojo and has -much fewer features. - -Practically every challenge I've encountered with Angular was resolved -with a quick Google search. - -Comprehensive Support for Internationalization / Localization -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Much like the HTML and CSS templates, the prototype uses the same i18n -structure as the TPAC. All strings are defined in the templates and -are thus translated via the TPAC-standard .po file process. - -Two Items of Note -^^^^^^^^^^^^^^^^^ - - * There is one example of dynamic (key-based) string use, described - http://yeti.esilibrary.com/dev/pub/web-staff-log.html#_i18n_exeriment[here] - * Angular also supports a pluralization directive, which gives us the - opportunity to apply dynamic plurilization. There is an example of - this http://git.evergreen-ils.org/?p=working/Evergreen.git;a=blob;f=Open-ILS/src/templates/staff/cat/bucket/record/t_bucket_info.tt2;h=1adee8c5b7fa1d0508d345a03bc383b7f9816f82;hb=refs/heads/collab/berick/web-staff-proto[here (see: ng-pluralize)] - - -Accessibility / Screen Reader support -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Browser-based applications have access to a number of accessibility -features out of the box, like true global font scaling, page zooming, -search-in-page, full-text history searching, and broad support for -assistive technology software, like screen readers. - -Ensuring that the application is developed in such a way that all such -features may be used to their fullest potential is up to us. The -prototype takes simple steps, like applying labels, titles, alt tags and -attributes as necessary, but there are larger tasks to address before -full-blown development ensues. For example, reviewing the CSS to allow -for high-contrast themes, support for cursor-based navigation, and -keyboard shortcuts, to name a few. - -Additionally, basic tests were performed against the prototype using -https://addons.mozilla.org/en-us/firefox/addon/fangs-screen-reader-emulator/[Firefox Fangs Screen Reader Emulator]. Fangs produced the expected output, in as much as it -was able to produce the correct textual values in the expected structure, -but the interface would clearly benefit form further community review and -live screen reader testing, etc. as we look ahead to designing the best -information structure and flow for assistive software. - -Quality Assurance Testing -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Unit test components live in the repository under Open-ILS/tests/staffweb/. -The basics are described in the -http://yeti.esilibrary.com/dev/pub/web-staff-log.html#_2013_12_13_unit_tests_with_angularjs_karma_jasmine[web log entry]. - -TODO Items -^^^^^^^^^^ - -Investigate end-to-end live testing, as this is likely the only way to -provide automated testing for most of the code. - -Mobile / Responsive Design Best Practices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - * The prototype uses Bootstrap CSS, which is designed specifically for - supporting mobile-friendly UIs, for the vast majority of CSS. - * All layout is div (grid) based, using Bootstrap column definitions for - mobile flow. - * Some data is represented with tables, as they naturally perform better - than divs at displaying tabular data. However, the future of such - tables is unclear. It's possible they will be replaced with more - feature-rich, div-based tabular structure. - * The interfaces are not designed specifically to look great on mobile - devices. However, UIs which need to be mobilized, should be much - simpler to modify than non-Bootstrap UIs - * I have confirmed that the UIs work on my Android phone. Some UIs look OK, - but any UIs we wish to make truly mobile-friendly would need additional - work. - -Ease of Local Customization -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Customization of the staff prototype works much like the TPAC. It -uses override-able Template Toolkit templates and template-driven CSS. -Additionally, with the use of AngularJS for template content, it is -considerably easier to customize the templates, because the link -between the script and the templates is less strict than with traditional -dynamic HTML. - -For example, AngularJS does not rely on the existence of elements, it -only responds to the content it encounters. If an HTML element is -removed or fails to be added as part of an upgrade, the lack of the -element will not break page rendering. - -[source,html] ------------------------------------------------------------------------------ - -
- - - - -
{{username}}
- - ------------------------------------------------------------------------------ - - -Consistent Look and Feel Across Interfaces -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Bootstrap CSS classes are used wherever possible. Very little local CSS -was created. The local CSS we do have is template-driven, like the TPAC. -Similarly, Bootstrap versions of standard HTML elements (e.g. ) -are used across all UIs for visual consistency. - -Prototype Testing ------------------ - - * The prototype can be installed by installing the -http://git.evergreen-ils.org/?p=working/Evergreen.git;a=shortlog;h=refs/heads/collab/berick/web-staff-proto[Evergreen Working Branch] - and modifying the Apache configuration to match the samples, also - in the branch. - * My dev server is available for light testing as well: - ** https://bill-dev2.esilibrary.com/eg/staff/login?ws=BR1-jupiter - ** username: admin; password: demo123 - * 'NOTE': The current CSS is set to cause the screen to break into the - vertical mobile flow at or below around 970px in width. This may be too - aggressive, particularly since we are not primarily targeting mobile - devices. The breakpoint can be modified. - * Known Issues - ** The record bucket UIs use the reporter simple record extracts for - bibliographic record display. This is done to more easily support - sorting and paging and to speed up data retrieval. This causes two - notable issues: - *** The data and column labels are not as friendly as they could be - (e.g. "Title Proper (normalized)") - *** Buckets with duplicate bib records have some issues with paging. - *** The solution to these issues (and generally, how we wish to fetch - and display bib records going forward, since it's important that - we stop using the old-style MVR's) depend on the resolution of - https://bugs.launchpad.net/evergreen/+bug/1251394[LP1251394 Metabib Display Fields] - -//// -vim: ft=asciidoc -//// -- 2.11.0