]> git.evergreen-ils.org Git - working/Evergreen.git/blob - web-staff-log.txt
hold pull list API cont. flesh copy loc.
[working/Evergreen.git] / web-staff-log.txt
1 Browser-Based Staff Client Development Log
2 ==========================================
3
4 2013-11-20 Templates and Apache
5 -------------------------------
6
7 When a path is requested from the server, e.g.
8 /eg/staff/circ/patron/search, there are 2 different aspects of the file
9 that are of intest to us: the content of the HTML file and the path used
10 to retrieve the file.
11
12 Page Content
13 ~~~~~~~~~~~~
14
15 For Angular apps, the HTML page only needs to provide enough information
16 for Angular to load the correct application -- a seed document.  A seed
17 document might look something like this:
18
19 [source,html]
20 -----------------------------------------------------------------------------
21 <!doctype html>
22 <html lang="en_us" ng-app="egPatronApp" ng-controller="PatronCtrl">
23   <head>
24     <title>Evergreen Staff Patron</title>
25     <base href="/eg/staff/">
26     <meta charset="utf-8">
27     <!-- css... -->
28   </head>
29   <body>
30     <div ng-view></div>
31   </body>
32   <!-- scripts... -->
33 </html>
34 -----------------------------------------------------------------------------
35
36 Note the body is a single div with an 'ng-view' tag.
37
38 Building Pages from Seeds
39 ~~~~~~~~~~~~~~~~~~~~~~~~~
40
41 With the above document we know the Angular application (egPatronApp),
42 we have an Angular Controller (PatronCtrl -- not strictly required
43 here).  If we assume this page was fetched using the path
44 '/eg/staff/circ/patron/search', then we have all we need to build the 
45 real page.
46
47 The Angular App will contain a series of app-specific routes based on
48 the URL path.  Here, our (relative) path will be '/circ/patron/search',
49 since the base path of the application is '/eg/staff/'.  For our route
50 configuration we have a chunk of code like this:
51
52 [source,js]
53 -----------------------------------------------------------------------------
54 $routeProvider.when('/circ/patron/search', {
55     templateUrl: './circ/patron/t_search',
56     controller: 'PatronSearchCtrl',
57     resolve : resolver // more on resolvers later...
58 });
59 -----------------------------------------------------------------------------
60
61 When the browser lands on '/eg/staff/circ/patron/search', Angular will
62 locate the template file at './circ/patron/t_search' (the app-relative
63 path to a Template Toolkit template), by performing an HTTP request and,
64 once fetched, will drive the display of the Angular template within with
65 the JS controller called PatronSearchCtrl and insert the content of the
66 template into the body of the page at the location of the <div ng-view>
67 div.
68
69 ****
70 'Note': For speed, it's sometimes better to include Angular templates
71 directly in the delivered document so that one less HTTP request is
72 needed.  More on that later.
73 ****
74
75 Fetching the Same Page at Different URLs
76 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
77
78 The egPatronApp might support a variety of different route-specific 
79 interfaces that are all driven by the same seed document.  This means
80 we have to tell Apache to always deliver the same file when we access
81 files within a given range of URL paths.  The secret to this is in 
82 Apache Rewrite configuration.
83
84 For example:
85
86 [source,conf]
87 -----------------------------------------------------------------------------
88 <Location /eg/staff/circ/patron/>
89     Options -MultiViews
90     RewriteEngine On
91     RewriteCond %{PATH_INFO} !/staff/circ/patron/index
92     RewriteCond %{PATH_INFO} !/staff/circ/patron/t_*
93     RewriteRule .* /eg/staff/index [L,DPI]
94 </Location>
95 -----------------------------------------------------------------------------
96
97 In short, any URL path that does not map to the index file or to a file
98 whose name starts with "t_" (more on 't_' below) will result in Apache
99 rewriting the request to deliver the index file (i.e. our seed 
100 document).
101
102 So, in our example, a request for '/eg/staff/circ/patron/search', will return
103 the index file found at '/eg/staff/circ/patron/index', which maps on the
104 server side to the Template Toolkit file at 
105 '/path/to/templates/staff/circ/patron/index.tt2'.
106
107 Two complications arise from this approach.  Help appreciated!
108 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
109
110 Simpler rewrite rules exist in the wild...
111 ++++++++++++++++++++++++++++++++++++++++++
112
113 But, they do not take into account that we are fetching Template
114 Toolkit-generated files instead of vanilla HTML.  The rules I found
115 online take the form of "if it's not a real file, return the index", but
116 none of the files we fetch are real files, since they are internally
117 mapped to Template Toolkit files.  This is why I'm using the 't_'
118 prefix.  It makes the mapping trivial.  I'm all ears for better solution.
119
120 Configuration Explosion
121 +++++++++++++++++++++++
122
123 This I see as a real problem, but one that certainly has a solution.
124 The configuration chunk above is such that we need a new chunk for each
125 top-level Angular app.  This will quickly get out of hand.  A single,
126 dynamic configuration that can map elemenents of arbitrarily-nested
127 paths (or possibly a small set with predefined path depths) to the 
128 correct index file would be ideal.
129
130 UPDATE: 2013-12-12 Apache Config Repaired
131 +++++++++++++++++++++++++++++++++++++++++
132
133 I finally got around to fixing the Apache configuration.  It now supports
134 arbitrary nesting via EGWeb.pm extension. 
135
136 [source,conf]
137 -----------------------------------------------------------------------------
138 <LocationMatch /eg/staff/>
139     Options -MultiViews
140     PerlSetVar OILSWebStopAtIndex "true"
141     # ...
142 </LocationMatch>
143 -----------------------------------------------------------------------------
144
145
146 2013-11-21 Angular $scope inheritance
147 -------------------------------------
148
149 Consider the following document:
150
151 [source,html]
152 -----------------------------------------------------------------------------
153 <div ng-controller="TopCtrl">
154   <div>Top: {{attr1}}</div>
155   <div ng-controller="SubCtrl">
156     <div>Sub: {{attr1}}</div>
157   </div>
158 </div>
159 -----------------------------------------------------------------------------
160
161 And the following code:
162
163 [source,js]
164 -----------------------------------------------------------------------------
165 .controller('TopCtrl', function($scope) { $scope.attr1 = 'top-attr' })
166 .controller('SubCtrl', function($scope) { })
167 -----------------------------------------------------------------------------
168
169 The output:
170
171 [source,sh]
172 -----------------------------------------------------------------------------
173 Top: top-attr
174 Sub: top-attr
175 -----------------------------------------------------------------------------
176
177 Now, if we apply a value in the child:
178
179 [source,js]
180 -----------------------------------------------------------------------------
181 .controller('SubCtrl', function($scope) { $scope.attr1 = 'sub-attr' })
182 -----------------------------------------------------------------------------
183 [source,sh]
184 -----------------------------------------------------------------------------
185 Top: top-attr
186 Sub: sub-attr
187 -----------------------------------------------------------------------------
188
189 Setting a value in the child does not change the value in the parent.
190 Scopes are inherited prototypically, which means attributes from a
191 parent scope are copied into the child scope and the child's version of
192 the attribute masks that of the parent.
193
194 For both scopes to share a single value, either the parent needs to 
195 provide a setter function on the value:
196
197 [source,js]
198 -----------------------------------------------------------------------------
199 .controller('TopCtrl', function($scope) {
200   $scope.attr1 = 'top-attr';
201   $scope.setAttr1 = function(val) {
202     $scope.attr1 = val;
203   }
204 })
205 .controller('SubCtrl', function($scope) {
206     $scope.setAttr1('sub-attr');
207 })
208 -----------------------------------------------------------------------------
209
210 Produces..
211
212 [source,sh]
213 -----------------------------------------------------------------------------
214 Top: sub-attr
215 Sub: sub-attr
216 -----------------------------------------------------------------------------
217
218 Or the value in question needs to be stored within a structure.
219
220 [source,html]
221 -----------------------------------------------------------------------------
222 <div ng-controller="TopCtrl">
223   <div>Top: {{attrs.attr1}}</div>
224   <div ng-controller="SubCtrl">
225     <div>Sub: {{attrs.attr1}}</div>
226   </div>
227 </div>
228 -----------------------------------------------------------------------------
229
230 [source,js]
231 -----------------------------------------------------------------------------
232 .controller('TopCtrl', function($scope) { $scope.attrs = {attr1 : 'top-attr'} })
233 .controller('SubCtrl', function($scope) { $scope.attrs.attr1 = 'sub-attr' })
234 -----------------------------------------------------------------------------
235
236 Also produces..
237
238 [source,sh]
239 -----------------------------------------------------------------------------
240 Top: sub-attr
241 Sub: sub-attr
242 -----------------------------------------------------------------------------
243
244 Since the child scope is not clobbering the 'attrs' attribute, both
245 scopes share the value, which is a reference to a single object.
246
247 This last is approach is the best for providing two-way binding across
248 both scopes.  For example:
249
250 [source,html]
251 -----------------------------------------------------------------------------
252 <div ng-controller="TopCtrl">
253   <div>Top: <input ng-model="attrs.attr1" type="text"/></div>
254   <div ng-controller="SubCtrl">
255     <div>Sub: <input ng-model="attrs.attr1" type="text"/></div>
256   </div>
257 </div>
258 -----------------------------------------------------------------------------
259
260 With this, typing a value into the first input, will set the value
261 for both scopes.
262
263 For more, see 
264 https://github.com/angular/angular.js/wiki/Understanding-Scopes[Understanding-Scopes].
265
266 2013-11-25 Prototype Install and Hacking
267 ----------------------------------------
268
269 Installing the code
270 ~~~~~~~~~~~~~~~~~~~
271
272 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]
273
274 ****
275 The branch is a child of Evergreen master and will be kept up to date
276 with master as development progresses.  
277 ****
278
279 Install the code in the usual Evergreen manner -- No DB upgrades are
280 required to date, but there are some IDL changes -- and apply the Apache
281 configuration changes found in eg_vhost.conf:
282
283 [source,conf]
284 -----------------------------------------------------------------------------
285 # see notes above on how we can condense the configuration...
286 # TODO: some of this is clearly redundant and needs to be rearranged
287 <Location /eg/staff/>
288     Options -MultiViews
289     RewriteEngine On
290     RewriteCond %{PATH_INFO} !/staff/index
291     RewriteCond %{PATH_INFO} !/staff/t_*
292     RewriteRule .* /eg/staff/index [L,DPI]
293     <IfModule mod_headers.c>
294         Header append Cache-Control "public"
295     </IFModule>
296     <IfModule mod_deflate.c>
297         SetOutputFilter DEFLATE
298         BrowserMatch ^Mozilla/4 gzip-only-text/html
299         BrowserMatch ^Mozilla/4\.0[678] no-gzip
300         BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
301         SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
302         <IfModule mod_headers.c>
303             Header append Vary User-Agent env=!dont-vary
304         </IfModule>
305     </IfModule>
306 </Location>
307 <Location /eg/staff/cat/bucket/record/>
308     Options -MultiViews
309     RewriteEngine On
310     RewriteCond %{PATH_INFO} !/staff/cat/bucket/record/index
311     RewriteCond %{PATH_INFO} !/staff/cat/bucket/record/t_*
312     RewriteRule .* /eg/staff/cat/bucket/record/index [L,DPI]
313 </Location>
314 <Location /eg/staff/circ/patron/>
315     Options -MultiViews
316     RewriteEngine On
317     RewriteCond %{PATH_INFO} !/staff/circ/patron/index
318     RewriteCond %{PATH_INFO} !/staff/circ/patron/t_*
319     RewriteRule .* /eg/staff/circ/patron/index [L,DPI]
320 </Location>
321 <Location /js/>
322     <IfModule mod_headers.c>
323         Header append Cache-Control "public"
324     </IFModule>
325     <IfModule mod_deflate.c>
326         SetOutputFilter DEFLATE
327         BrowserMatch ^Mozilla/4 gzip-only-text/html
328         BrowserMatch ^Mozilla/4\.0[678] no-gzip
329         BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
330         SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
331         <IfModule mod_headers.c>
332             Header append Vary User-Agent env=!dont-vary
333         </IfModule>
334     </IfModule>
335 </Location>
336 -----------------------------------------------------------------------------
337
338 Once installed, point Chrome or Firefox at https://hostname/eg/staff/ to see
339 the splash page.
340
341 Hacking
342 ~~~~~~~
343
344 Most of the code should seem familiar.  The Template Toolkit templates 
345 behave as usual.  For modifying scripts and Angular-loaded templates, I 
346 highly recommend Chrome's "Disable Cache (while dev tools is open)" option
347 found under the Javascript console (dev tools) configuration menu (gear
348 icon, bottom right) since Chrome aggressively caches the templates.
349
350 2013-11-26 Getting Started with Testing
351 ---------------------------------------
352
353 Today I wrote one unit test for a stub controller from 
354 https://github.com/angular/angular-seed[angular-seed].
355 Here's what I did.  I'm still figuring this out, so bear with me...
356
357  * On my desktop, I installed node.js and npm (node package manager) and
358 the node plugin 'karma'.
359
360 [source,sh]
361 -----------------------------------------------------------------------------
362 % sudo apt-get install nodejs npm
363 # node.js is installed at 'nodejs' -- npm, etc. assume 'node'
364 % sudo ln -s /usr/bin/nodejs /usr/bin/node
365 % sudo npm install -g karma
366 -----------------------------------------------------------------------------
367
368  * Clone the angular-seed repository, which provides a stub test environment
369
370 [source,sh]
371 -----------------------------------------------------------------------------
372 % git clone https://github.com/angular/angular-seed
373 -----------------------------------------------------------------------------
374
375  * Modify the angular-seed test script, which makes some odd assumptions
376    about the location of binaries
377
378 [source,diff]
379 -----------------------------------------------------------------------------
380 diff --git a/scripts/test.sh b/scripts/test.sh
381 index 972001f..f6db762 100755
382 --- a/scripts/test.sh
383 +++ b/scripts/test.sh
384 @@ -6,4 +6,5 @@ echo ""
385 echo "Starting Karma Server (http://karma-runner.github.io)"
386 echo "-------------------------------------------------------------------"
387    
388 -$BASE_DIR/../node_modules/karma/bin/karma start $BASE_DIR/../config/karma.conf.js $*
389 +#$BASE_DIR/../node_modules/karma/bin/karma start $BASE_DIR/../config/karma.conf.js $*
390 +karma start $BASE_DIR/../config/karma.conf.js $*
391 -----------------------------------------------------------------------------
392
393  * Modify the stock controller and controller unit test to do something
394    (very simple).
395
396 [source,diff]
397 -----------------------------------------------------------------------------
398 diff --git a/app/js/controllers.js b/app/js/controllers.js
399 index cc9d305..679570d 100644
400 --- a/app/js/controllers.js
401 +++ b/app/js/controllers.js
402 @@ -3,9 +3,10 @@
403  /* Controllers */
404  
405  angular.module('myApp.controllers', []).
406 -  controller('MyCtrl1', [function() {
407 +  controller('MyCtrl1', ['$scope', function($scope) {
408 +    $scope.foo = 'hello';
409  
410    }])
411 diff --git a/test/unit/controllersSpec.js b/test/unit/controllersSpec.js
412 index 23f6b09..ec741a6 100644
413 --- a/test/unit/controllersSpec.js
414 +++ b/test/unit/controllersSpec.js
415 @@ -3,11 +3,18 @@
416  /* jasmine specs for controllers go here */
417  
418  describe('controllers', function(){
419   beforeEach(module('myApp.controllers'));
420  
421 +    // create the scope, instantiate the controller
422 +    var ctrl, scope;
423 +    beforeEach(inject(function ($rootScope, $controller) {
424 +        scope = $rootScope.$new();
425 +        ctrl = $controller('MyCtrl1', {$scope: scope});
426 +    }));
427  
428    it('should ....', inject(function() {
429      //spec body
430 +    expect(scope.foo).toEqual('hello');
431    }));
432 -----------------------------------------------------------------------------
433
434  * Launched the test, which fires up Chrome and logs the output to the 
435    terminal
436
437 [source,sh]
438 -----------------------------------------------------------------------------
439 CHROME_BIN=/usr/bin/chromium-browser scripts/test.sh
440
441 Starting Karma Server (http://karma-runner.github.io)
442 INFO [karma]: Karma v0.10.5 server started at http://localhost:9876/
443 INFO [launcher]: Starting browser Chrome
444 INFO [Chromium 30.0.1599 (Ubuntu)]: Connected on socket 344641UsyCbWCVxk_28H
445 Chromium 30.0.1599 (Ubuntu): Executed 5 of 5 SUCCESS (0.612 secs / 0.088 secs)
446 -----------------------------------------------------------------------------
447
448 All 5 tests succeeded, even my rigorous $scope.foo='hello' test.  
449
450 Next steps for testing will be to create an environment around the staff
451 client code so we can write some real tests...
452
453 2013-12-02 Scopes, Services, and Routing
454 ----------------------------------------
455
456 If you have two routes like so, 
457
458 [source,js]
459 -----------------------------------------------------------------------------
460 $routeProvider.when('/circ/patron/1/items_out', {...});
461 $routeProvider.when('/circ/patron/1/holds', {...});
462 -----------------------------------------------------------------------------
463
464 You can access these two pages by changing your browser's URL or you can
465 use links/actions in the page to jump between routes using pushstate
466 routing.
467
468 Pushsate routing allows you to change the browser URL and have the JS
469 code respond to the change without actually reloading the web page.  In
470 short, it makes page switching faster.  It does this in two main ways.
471
472 1. The browser does not have to fetch the page and scripts from the
473 server (or cache) or re-parse and execute scripts that run at page load
474 time.
475
476 2. If two pages share data, which is common for route-linked pages, then
477 it's possible for the second page to access data retrieved by the first,
478 without having to re-fetch it from the server.
479
480 How do we manage routing and caching with angular scopes and services?
481
482 Any time a path is loaded in Angular, regardless of how the path was
483 accessed (routing or initial page load), all controllers within the page
484 are re-initialized with an empty $scope.  Naturally, this means that any
485 data that was loaded into a $scope will no longer be available when the
486 next route is accessed.
487
488 To persist data across routes, it has to be managed outside of the
489 controller.  The Angular solution is to use a service.  Generally
490 speaking, Angular services provide re-usable chunks of logic that are
491 scope and controller-agnostic.  They are factory classes which produce
492 singleton objects, instantiated once at page load, and persisted through
493 all routes.  Because of this, they double as a great place to store
494 route-global data (i.e. data that should be cached between all of our
495 known routes).
496
497 Some examples, with lots of extra stuff left out:
498
499 [source,js]
500 -----------------------------------------------------------------------------
501 .controller('PatronItemsOut', function($scope) {
502     fetchPatronFromServer(id).then(
503         function(p) { $scope.patron = p });
504 })
505
506 .controller('PatronHolds', function($scope) {
507     fetchPatronFromServer(id).then(
508         function(p) { $scope.patron = p });
509 })
510 -----------------------------------------------------------------------------
511
512 Here we fetch the patron object from the server every time the
513 PatronItemsOut or PatronHolds controllers are instantiated.  Since
514 these two controllers are managed within the same angular app, they are
515 accessible via routing and can theoretically share date.  Fetching the
516 user every time is wasteful.
517
518 Here's an example where we mange user retrieval with a service.
519
520 [source,js]
521 -----------------------------------------------------------------------------
522 .factory('patronService', function() {
523     var service = {
524         // only cache the last-accessed user.  
525         // caching all accessed users is a recipe for memory leaks
526         current : null
527     };
528     service.fetchPatron = function(id) {
529         // on initial page load, service.current will be null
530         if (service.current && service.current.id() == id) {
531             // no need to fetch!
532         } else {
533             // fetch patron and set service.current = patron
534         }
535     }
536     return service;
537 })
538
539 .controller('PatronItemsOut', function($scope, patronService) {
540     patronService.fetchPatron(id)
541     $scope.patron = function() { return patronService.current }
542 })
543
544 .controller('PatronHolds', function($scope, patronService) {
545     patronService.fetchPatron(id)
546     $scope.patron = function() { return patronService.current }
547 })
548 -----------------------------------------------------------------------------
549
550 Now the patron object lives within our patronService Angular service.  It
551 will persist through page routes until we replace the value or reload
552 the page.  Note that we are now accessing our patron at $scope.patron() 
553 instead of $scope.patron, because $scope.patron() is guaranteed to always 
554 return the correct value for the current patron.  Angular templates are
555 perfectly happy to work with functions intead of attributes.
556
557 [source,html]
558 -----------------------------------------------------------------------------
559 <div>Username: {{patron().usrname()}}</div>
560 -----------------------------------------------------------------------------
561
562 2013-12-03 What About Workstations?  Printing?
563 ----------------------------------------------
564
565 Why is the workstation optional in the prototype login page?
566 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
567
568 I mentioned this briefly toward the bottom of my old "Hey, let's build a 
569 prototype!"  
570 http://yeti.esilibrary.com/dev/pub/EvergreenWebStaffClientPrototype.pdf[proposal].
571 I've heard the question again from a number of people, so I'll answer it
572 here in a little more detail.
573
574 Short answer first, yes, the plan is to continue requiring workstations
575 for the browser-based client.  The fact that they are not required in
576 the prototype is the result of an unresolved technical hurdle.
577
578 What's the problem, exactly?
579 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
580
581 One of the main benefits of XULRunner is that it runs on your computer
582 as a trusted application.  XULRunner can open files on your computer,
583 create raw network sockets, and it can talk directly to your printers.
584 Web browsers, being tasked with executing all kinds of untrusted code,
585 have a whole host of restrictions that prevent that level of access.
586 This is a good thing for security, but it makes accomplishing some tasks
587 understandably much more difficult.  This lack of access in the browser
588 may be the biggest challenge we face in developing a browser-based
589 client.  
590
591 There are two classes of problems that we need to resolve: access to
592 files and access to printers.
593
594 File access is important for things like storing the workstation
595 information in a way that is not easily deleted on accident and can be
596 shared between more than one browser.  We need the same level of control
597 for offline transaction data as well.  There are probably more...
598
599 Modern browsers will let you store quite a bit of data (~5MB for
600 localStorage), and in some browsers you can manually raise the storage
601 limit, but they won't let you control where that data lives or allow
602 other applications to access the data.  They also make it fairly easy to
603 delete the data en masse within the various debug consoles, which is 
604 obviously bad for files we want to stick around indefinitely.
605
606 For printing, all you can do from the browser is say "print, please"
607 and then the user is shown a print dialog.  Different browsers have
608 different settings for controlling whether the dialog appears, etc. but
609 they are non-standard and still not as flexible as we need.  What we
610 need is a way for the web application to send print data to different
611 printers based on the context of the action, without requiring the user
612 to manually select the printer each time.
613
614 What's the solution?
615 ~~~~~~~~~~~~~~~~~~~~
616
617 There are probably multiple solutions.  The most promising so far, which
618 was discussed at the http://goo.gl/oKo5yt[Evergreen Hackaway] is that of
619 a small service which runs on the client machine independent of the browser
620 and performs privileged tasks that the browser cannot.  It would likely
621 operate as a small, local web service, accepting connections from clients
622 (i.e. browsers) on the local machine.  Those connections may take the form
623 of Websocket connections or XMLHttpRequest via 
624 http://en.wikipedia.org/wiki/Cross-origin_resource_sharing[cross-origin resource sharing]
625 (though, I'm not sure the latter approach would actually work).
626
627 With this approach, we get to resolve all of the browser limitations
628 with one, cross-platform, browser-agnostic add-on.  Depending on how
629 it's implemented, we may also get to leverage printing APIs which are
630 more powerful than those found in XULRunner.  
631
632 This probably goes without saying, but such an add-on, whatever form
633 it takes, will *not* be a *requirement* for using the staff client.
634 One aspect of a browser-based client that I find most appealing is the
635 ease of access from different devices and locations.  If I were using a
636 tablet, where the add-on is not (or conceivably cannot be) installed,
637 I would still expect to be able to log in, possibly with a workstation
638 (via bookmark/URL or typed in), possibly without, and perform, say, a
639 patron lookup.  If I don't need the extended features, I should not be
640 required to have them installed.
641
642 Finally, it needs a better name than "add-on" (Add-On 2.0!) and it needs 
643 developers/sponsors, and all that fun stuff.
644
645 2013-12-04 Promises in AngularJS
646 --------------------------------
647
648 One of my favorite new-ish tools in the Javascript world are called
649 "Promises".
650
651 ****
652 If a function cannot return a value or throw an exception without
653 blocking, it can return a promise instead. A promise is an object that
654 represents the return value or the thrown exception that the function
655 may eventually provide. A promise can also be used as a proxy for a
656 remote object to overcome latency. -- https://github.com/kriskowal/q
657 ****
658
659 Typically, with asynchronous network calls, the caller will make the
660 call and pass in a callback function, which is invoked when the async
661 call returns.  This works well enough for single calls, but it does not
662 scale well.  Promises allow us to manage collections of asynchronous
663 calls much more elegantly.
664
665 Here's a quick example:
666
667 [source,js]
668 -----------------------------------------------------------------------------
669 // non-promise api call
670 request(service, method, params, {oncomplete : callback});
671
672 // promise-based call
673 request(service, method, params).then(callback);
674 -----------------------------------------------------------------------------
675
676 At first, the difference seems trivial, but it becomes more
677 pronounced as more requests are added.
678
679 Batch Parallel Request Example
680 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
681
682 [source,js]
683 -----------------------------------------------------------------------------
684 // non-promise batch
685 // send three, parallel requests and track the 
686 // responses to see when all are done
687 var expected = 3;
688 var checkComplete = function() {
689     if (--expected == 0) { // decrement w/ each response
690         // all responses received, move on to other things
691     }
692 }
693 request(s, m, p, function(r) { /*do stuff*/; checkComplete() });
694 request(s, m, p, function(r) { /*do stuff*/; checkComplete() });
695 request(s, m, p, function(r) { /*do stuff*/; checkComplete() });
696
697 // promise-based batch
698 var promises = [];
699 promises.push(request(s, m, p).then(function(r) {/*do stuff*/}));
700 promises.push(request(s, m, p).then(function(r) {/*do stuff*/}));
701 promises.push(request(s, m, p).then(function(r) {/*do stuff*/}));
702 $q.all(promises).then(
703     function() { 
704         // all responses received, move on to other things 
705     }
706 );
707
708 // $q.all() takes a collection of promise objects and returns a new 
709 // promise which is resolved when all of the embedded promises are resolved
710 -----------------------------------------------------------------------------
711
712 Not only is the code a little cleaner, but it flows in the same
713 direction as execution.  In other words, checkComplete(), which is the
714 last-executed code in the non-promise example is defined before the
715 requests are sent.  This creates a spaghetti effect in the code, where
716 you have to jump around to follow the logic.  Not so in the promise
717 example.  It flows top to bottom.
718
719 Attaching to Promises
720 ~~~~~~~~~~~~~~~~~~~~~
721
722 Another neat trick you can do w/ promises is create a promise to
723 represent a long-running task.  When clients ask for the result of the
724 task and the task is not yet complete, the promise can be returned to
725 any number of clients.
726
727 [source,js]
728 -----------------------------------------------------------------------------
729 function longTask() {
730     if (this.promise) {
731         return this.promise;
732     } else {
733         this.promise = performAsyncTask();
734         return this.promise;
735     }
736 }
737
738 // the first call kicks off the long-running process
739 longTask().then(/*func*/);
740
741 // subsequent calls from other locations simply pick up the existing promise
742
743 // different location in the code
744 longTask().then(/*func*/);
745
746 // different location in the code
747 longTask().then(/*func*/);
748
749 // different location in the code
750 longTask().then(/*func*/);
751
752 // when longTask() finally completes, all promise handlers are run
753 // Also, if longTask() was *already* complete, the promise handler 
754 // will be immediately run.
755 -----------------------------------------------------------------------------
756
757 This structure is used to great effect in the prototype page
758 initialization code.  Independent controllers that all want to ensure
759 the client has authenticated and retrieved necessary data will call the
760 startup code, the first will kick it off, and the others will latch on
761 the outstanding promise.
762
763 As a final bonus to using promises within Angular, promise resolution
764 causes another $digest() run in Angular, which causes templates to get
765 updated.
766
767 [source,js]
768 -----------------------------------------------------------------------------
769 egNet.request(
770     'open-ils.actor', 
771     'open-ils.actor.user.retrieve', 
772     egAuth.token(), userId
773 ).then(
774     function(user) {
775         $scope.patron = patron; 
776     }
777 );
778 -----------------------------------------------------------------------------
779 [source,html]
780 -----------------------------------------------------------------------------
781 <!-- username will magically display in the 
782     page after the async call completes -->
783 <div>Username: {{patron.usrname()}}</div>
784 -----------------------------------------------------------------------------
785
786 Promises also support reject() and notify() handlers for failed requests
787 and intermediate messages.  In my egNet module, I'm leveraging notify()
788 for streaming responses.
789
790 [source,js]
791 -----------------------------------------------------------------------------
792 egNet.request(service, method, params).then(
793     function(finalResp) { console.log('all done') },
794     function() { console.error('request failed!') },
795     function(resp) { console.log('got a response ' + resp) }
796 );
797 // The egNet module has additional examples and docs.
798 -----------------------------------------------------------------------------
799
800 For the full technical rundown, see also 
801 http://docs.angularjs.org/api/ng.$q[Angular $q Docs].
802
803 2013-12-06 Template Cornucopia and Dynamic Strings Experiment
804 -------------------------------------------------------------
805
806 Using Angular on top of Template Toolkit gives us lots of options for
807 managing templates. TT lets us INCLUDE (etc.) shared templates on the 
808 server.  AngularJS lets us ng-include (etc.) both inline templates
809 (i.e. delivered within the main document) and lazy-loaded templates,
810 fetched as needed from the server.
811
812 When to use each approach comes down to how each template is used in the
813 application.  Is it needed on page load?  Is it needed on every page?
814 Is it needed often, but not on page load?  Is it needed rarely?  You
815 have to weigh the cost of adding the template contents into the main
816 page body (does it add a lot of bytes?) to the cost of (potentially)
817 having to fetch it over the network as a separate document.
818
819 Some examples:
820
821 Classic TT Template
822 ~~~~~~~~~~~~~~~~~~~
823
824 In the prototype, I'm loading the navigation template (t_navbar.tt2)
825 within the base document, i.e. the document used as the base template
826 for all top-level index files.
827
828 [source,sh]
829 -----------------------------------------------------------------------------
830 [% INCLUDE "staff/t_navbar.tt2" %]
831 -----------------------------------------------------------------------------
832
833 I'm doing this because the navbar is used on every page.  It makes no 
834 sense to load the template as a separate file from the server, because
835 that just increases the network overhead.
836
837 Inline Angular Template
838 ~~~~~~~~~~~~~~~~~~~~~~~
839
840 Angular templates, regardless of origin, must be represented as
841 addressable chunks.  To give an inline chunk of HTML an "address", it
842 can be inserted into a <script> block like so:
843
844 [source,html]
845 -----------------------------------------------------------------------------
846 <script type="text/ng-template" id="uncat_alert_dialog">                       
847   <div class="modal-dialog">
848     <div class="modal-content">
849       <!-- ... content ... -->
850     </div>
851   </div>
852 </script>
853 -----------------------------------------------------------------------------
854
855 The address is simply the ID, so in this example, the modal dialog is 
856 invoked like so:
857
858 [source,js]
859 -----------------------------------------------------------------------------
860 $modal.open({templateUrl : 'uncat_alert_dialog', controller : [...]}); 
861 -----------------------------------------------------------------------------
862
863 ****
864 $modal is an angular-ui-bootstrap service.  It's not part of core AngularJS.
865 ****
866
867 Lazy-Loaded Angular Template
868 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
869
870 These behave much like inline templates, except the template file is 
871 fetched from the server as needed.  
872
873 To change the inline template example above into a lazy-loaded template,
874 drop the <script> tags and move the template to it's own web-accessible
875 file:
876
877 [source,html]
878 -----------------------------------------------------------------------------
879 <!-- New file at circ/checkin/t_uncat_alert_dialog.tt2 -->
880
881 <div class="modal-dialog">
882   <div class="modal-content">
883     <!-- ... content ... -->
884   </div>
885 </div>
886 -----------------------------------------------------------------------------
887
888 Then load the file using the URL as the ID.
889 [source,js]
890 -----------------------------------------------------------------------------
891 $modal.open({
892     templateUrl : './circ/checkin/t_uncat_alert_dialog', 
893     controller : [...]
894 }); 
895 -----------------------------------------------------------------------------
896
897 The template file is not fetched from the server until the first time
898 it's used.  After that, it's cached by the browser.
899
900 Angular Dialog Services w/ Dynamic Strings
901 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
902
903 In the examples above, each type of template is context-specific.  In other
904 words, there is a template file specific to uncataloged item checkin.  
905 Context-specific templates have their place, but we also need
906 a way to present generic alerts and confirmation dialogs, because
907 a) we may not always be aware of the type of message
908 we want to display ahead of time and b) if we always use context-specific
909 dialogs, the number of templates will grow and grow, even though 95% of
910 each template is boilerplate dialog rendering HTML -- the only thing unique
911 about each will be the message and how the user actions are handled.
912
913 I just added two new services: egAlertDialog and egConfirmDialog.  Presumably,
914 these will be used frequently, so they are good candidates for genericizing.
915
916 [source,js]
917 -----------------------------------------------------------------------------
918 // egAlertDialog.open(message_str, dialog_scope)
919 // egConfirmDialog.open(title_str, message_str, dialog_scope)
920
921 egConfirmDialog.open(
922     'Copy Alert Message for {{copy_barcode}}.  Continue checkin?',
923     evt.payload, // copy alert message text 
924     {   copy_barcode : barcode_str,
925         ok : function() { console.log('user clicked OK') }
926         cancel : function() { console.log('user clicked cancel') }
927     }
928 );
929 -----------------------------------------------------------------------------
930
931 Naturally, we don't want hard-coded strings within the code.  Here's where
932 an i18n experiment begins....  
933
934 I18N Exeriment
935 ~~~~~~~~~~~~~~
936
937 In my 
938 http://yeti.esilibrary.com/dev/pub/techspecs/web-staff-prototype.html[original tech specs],
939 I suggested that we don't need dynamic strings, since all strings will live
940 in templates.  While this is certainly possible, it's probably not 
941 sustainable, because of the template repetition issues mentioned above.
942 If possible, though, I would like to keep the management of dynamic strings
943 as simple as possible.  
944
945 For my experiment, I'm embedding the dynamic strings directly within the
946 TT template.  This lets us manage regular template strings and dynamic
947 strings within the same gettext environment, so there's one less hurdle
948 for managing i18n.  Such string collections could be put within the main
949 template, in an included template, or as a separate t_strings.js.tt2
950 file, loaded as a standalone JS file.  For now, I'm just loading them
951 directly in the body of the index.tt2 file:
952
953 [source,html]
954 -----------------------------------------------------------------------------
955 <script>                                                                       
956 angular.module('egCoreMod').factory('egCheckinStrings', function() {return {   
957
958 /* 
959 Put our strings into an angular service so that our controller can
960 access them.  Store them as a collection of string-key => string-value
961 pairs.  Strings may contain angualr-style variables, which will be
962 interpolated by the dialogs.
963 */
964
965 UNCAT_ALERT_DIALOG :                                                           
966     '[% l('Copy "{{copy_barcode}}" was mis-scanned or is not cataloged') %]',  
967
968 COPY_ALERT_MSG_DIALOG_TITLE :                                                  
969     '[% l('Copy Alert Message for {{copy_barcode}}.  Continue checkin?') %]'                       
970 }});                                                                           
971 </script>
972 -----------------------------------------------------------------------------
973
974 And here's the confirm dialog code:
975
976 [source,js]
977 -----------------------------------------------------------------------------
978 egConfirmDialog.open(                                          
979     egCheckinStrings.COPY_ALERT_MSG_DIALOG_TITLE,              
980     evt.payload,  // copy alert message text             
981     {   copy_barcode : args.copy_barcode,                      
982         ok : function() {                                      
983             // on confirm, redo checkout w/ override           
984             performCheckin(args, true)                         
985         },                                                     
986         cancel : function() {                                  
987             // on cancel, push the event on the list           
988             // to show that it happened                        
989             checkinSvc.checkins.items.push(evt);               
990         }                                                      
991     }                                                          
992 );
993 -----------------------------------------------------------------------------
994
995 You may wonder why I created these as services instead of directives.  
996 http://www.befundoo.com/blog/angularjs-popup-dialog/[These folks], who 
997 developed some similar code, do a pretty good job of explaining why a service 
998 is best in this context.  (See "Why create the AngularJS Popup Dialog 
999 Service?").  More on Angular directives later.
1000
1001 2013-12-13 Unit Tests with AngularJS/Karma/Jasmine
1002 --------------------------------------------------
1003
1004 We now have 4 unit tests in the repository!  For now, the files live 
1005 under Open-ILS/tests/staffweb/.
1006
1007 Running Unit Tests
1008 ~~~~~~~~~~~~~~~~~~~
1009
1010 These instructions replace my earlier instructions at 
1011 link:web-staff-log.html#_2013_11_26_getting_started_with_testing[2013-11-26 Getting Started with Testing].
1012
1013 [source,sh]
1014 -----------------------------------------------------------------------------
1015 # install node.js
1016 % sudo apt-get install nodejs npm
1017
1018 # node.js is installed at 'nodejs' -- npm, etc. assume 'node'
1019 % sudo ln -s /usr/bin/nodejs /usr/bin/node
1020
1021 # install karma test engine node plugin
1022 % sudo npm install -g karma
1023
1024 # fetch a copy of angular / angular-ui-bootstrap 
1025 % sudo apt-get install curl # if needed
1026 % cd Open-ILS/tests/staffweb/
1027 % ./update-angular.sh 1.2.4 0.7.0
1028
1029 # run the tests
1030 % CHROME_BIN=chromium-browser scripts/test.sh 
1031 -----------------------------------------------------------------------------
1032
1033 The output...
1034
1035 [source,sh]
1036 -----------------------------------------------------------------------------
1037 Starting Karma Server (http://karma-runner.github.io)
1038 \-------------------------------------------------------------------
1039 INFO [karma]: Karma v0.10.5 server started at http://localhost:9876/
1040 INFO [launcher]: Starting browser Chrome
1041 INFO [Chromium 30.0.1599 (Ubuntu)]: Connected on socket XmJAj4CwHlta3NuSQwC9
1042 Chromium 30.0.1599 (Ubuntu): Executed 4 of 4 SUCCESS (0.623 secs / 0.073 secs)
1043 \-----------------------------------------------------------------------------
1044 -----------------------------------------------------------------------------
1045
1046 Sample Tests
1047 ~~~~~~~~~~~~
1048
1049 [source,js]
1050 -----------------------------------------------------------------------------
1051 /** patronSvc tests **/                                                      
1052 describe('patronSvcTests', function() {                                      
1053                                                                              
1054   it('patronSvc should start with empty lists', inject(function(patronSvc) { 
1055       expect(patronSvc.patrons.count()).toEqual(0);                          
1056   }));                                                                       
1057                                                                              
1058   it('patronSvc reset should clear data', inject(function(patronSvc) {       
1059       patronSvc.checkout_overrides.a = 1;                                    
1060       expect(Object.keys(patronSvc.checkout_overrides).length).toBe(1);      
1061       patronSvc.resetPatronLists();                                          
1062       expect(Object.keys(patronSvc.checkout_overrides).length).toBe(0);      
1063       expect(patronSvc.holds.items.length).toBe(0);                          
1064   }));                                                                       
1065 })
1066 -----------------------------------------------------------------------------
1067
1068 These tests are very basic.  They ensure that the patronSvc service
1069 (defined in circ/patron/app.js) is properly initialized and that the
1070 resetPatronLists() function behaves as expected.  
1071
1072 Initial Reactions
1073 ~~~~~~~~~~~~~~~~~
1074
1075 These types of tests can be very useful for testing complex, client-side
1076 code.  However, just like with Evergreen Perl/C unit tests, JS unit
1077 tests are not meant to be executed in a live environment.  You can test
1078 code that does not require network IO (as above) and you have the
1079 option of creating mock data which is used in place of network-retrieved
1080 data.  
1081
1082 I believe the best long-term approach, however, will be full coverage 
1083 testing with the live, end-to-end test structure, also supported by Angular.
1084 It requires more setup and I hope to have time
1085 to research it more fully soon.  I say this because Evergreen has fairly
1086 complex data requirements (IDL required, data fetched through opensrf 
1087 instead of bare XHR) and practically all of the prototype code uses
1088 network IO or assumes the presence of a variety of network-fetched data, 
1089 which will be non-trivial to mock up and will only grow over time.
1090 Fingers crossed that it's not a beast to get running.  More on this as 
1091 the story develops....
1092
1093 2013-12-13 #2 - Brief Update
1094 ----------------------------
1095
1096  * Last week the prototype acquired a locale selector.  It uses the
1097    existing locale bits from EGWeb.pm, so it was easy to add.
1098
1099  * The prototype is nearing completion!
1100
1101  * Beware testing on small screens, as the CSS causes the screen to 
1102    break and flow (e.g. for mobile devices) a little too aggressively
1103    right now.  TODO.
1104
1105 2013-12-16 JS Minification
1106 --------------------------
1107
1108 Angular has some built-in magic which forces us to take special care when
1109 preparing for Javascript minification.  For the details, see the "A Note
1110 On Minification" section of the 
1111 http://docs.angularjs.org/tutorial/step_05[Angular Totorial Docs].  Briefly,
1112 there's a short (auto-magic) way and a long way to declare dependencies
1113 for services, controllers, etc.  To use minificaiton, you have to use the 
1114 long way.  To make sure everything is working as expected, I've set up 
1115 a test.
1116
1117 Testing Minification
1118 ~~~~~~~~~~~~~~~~~~~~
1119
1120  * Download jar file from https://github.com/yui/yuicompressor/releases[YUICompressor Releases]
1121  * Create a staff.min.js bundle from commonly loaded files:
1122
1123 [source,sh]
1124 -----------------------------------------------------------------------------
1125 % cd /openils/var/web/js/ui/default/staff                                        
1126
1127 # note the files have to be combined in a specific order
1128 % cat /openils/var/web/js/dojo/opensrf/JSON_v1.js \
1129 /openils/var/web/js/dojo/opensrf/opensrf.js \
1130 /openils/var/web/js/dojo/opensrf/opensrf_xhr.js \
1131 services/core.js \
1132 services/idl.js \
1133 services/event.js \
1134 services/net.js \
1135 services/auth.js \
1136 services/pcrud.js \
1137 services/env.js \
1138 services/org.js \
1139 services/startup.js \
1140 services/ui.js \
1141 navbar.js \
1142 | java -jar yuicompressor-2.4.8.jar --type js -o staff.min.js 
1143 -----------------------------------------------------------------------------
1144
1145  * modify /openils/var/templates/staff/t_base_js.tt2
1146   ** comment out <script> references to the JS files listed above
1147   ** add a new <script> tag for staff.min.js
1148 [source,html]
1149 -----------------------------------------------------------------------------
1150 <script src="[% ctx.media_prefix %]/js/ui/default/staff/staff.min.js"></script>
1151 -----------------------------------------------------------------------------
1152
1153  * Reload the page. 
1154
1155 I'm happy to report all pages load successfully with (mostly) minimized JS.
1156 Next on the minification front would be baking minification into the
1157 release-building process.
1158
1159 Future Topics...
1160 ----------------
1161
1162  * My (currently) preferred parent scope, child scope, service pattern
1163  * Displaying bib records in the prototype
1164
1165
1166 ////
1167 vim: ft=asciidoc
1168 ////