Merge remote-tracking branch 'working/user/kmlussier/lp-1689608-patron_batch_edit_reb...
authorGalen Charlton <gmc@equinoxinitiative.org>
Thu, 24 Aug 2017 21:39:43 +0000 (17:39 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Thu, 24 Aug 2017 21:39:43 +0000 (17:39 -0400)
Conflicts:
Open-ILS/src/templates/staff/circ/patron/index.tt2
Open-ILS/web/js/ui/default/staff/circ/patron/app.js

1  2 
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/sql/Pg/090.schema.action.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/templates/staff/circ/patron/index.tt2
Open-ILS/web/js/ui/default/staff/circ/patron/app.js
Open-ILS/web/js/ui/default/staff/test/karma.conf.js

Simple merge
@@@ -62,7 -62,8 +63,9 @@@ angular.module('egCoreMod').run(['egStr
    s.PATRON_PURGE_OVERRIDE_PROMPT = "[% l('The account has open transactions (circulations and/or unpaid bills). Purge anyway?') %]";
    s.OPT_IN_DIALOG_TITLE = "[% l('Verify Permission to Share Personal Information') %]";
    s.OPT_IN_DIALOG = "[% l('Does patron [_1], [_2] from [_3] ([_4]) consent to having their personal information shared with your library?', '{{family_name}}', '{{first_given_name}}', '{{org_name}}', '{{org_shortname}}') %]";
 +  s.PATRON_EDIT_COLLISION = "[% l('Patron record was modified by another user while you were editing it. Your changes were not saved; please reapply them.') %]";
+   s.BUCKET_ADD_SUCCESS = "[% l('Successfully added [_1] users to bucket [_2].', '{{count}}', '{{name}}') %]";
+   s.BUCKET_ADD_FAIL = "[% l('Failed to add [_1] users to bucket [_2].', '{{count}}', '{{name}}') %]";
  }]);
  </script>
  
@@@ -4,9 -4,8 +4,9 @@@
   * Search, checkout, items out, holds, bills, edit, etc.
   */
  
- angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 
 -angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod', 
 -    'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'ngToast'])
++angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod',
 +    'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'ngToast',
 +    'egPatronSearchMod'])
  
  .config(['ngToastProvider', function(ngToastProvider) {
      ngToastProvider.configure({
@@@ -527,23 -886,153 +527,81 @@@ function($scope , $location , egCore , 
   * Manages patron search
   */
  .controller('PatronSearchCtrl',
-        ['$scope','$q','$routeParams','$timeout','$window','$location','egCore',
-        '$filter','egUser', 'patronSvc','egGridDataProvider','$document',
-        'egPatronMerge','egProgressDialog','$controller',
- function($scope,  $q,  $routeParams,  $timeout,  $window,  $location,  egCore,
-         $filter,  egUser,  patronSvc , egGridDataProvider , $document,
-         egPatronMerge , egProgressDialog,  $controller) {
+        ['$scope','$q','$routeParams','$timeout','$window','$location','egCore','ngToast',
+        '$filter','egUser', 'patronSvc','egGridDataProvider','$document','bucketSvc',
 -       'egPatronMerge','egProgressDialog','$interpolate','$uibModal',
++       'egPatronMerge','egProgressDialog','$interpolate','$uibModal','$controller',
+ function($scope,  $q,  $routeParams,  $timeout,  $window,  $location,  egCore , ngToast,
+          $filter,  egUser,  patronSvc , egGridDataProvider , $document , bucketSvc,
 -        egPatronMerge , egProgressDialog , $interpolate , $uibModal) {
++        egPatronMerge , egProgressDialog , $interpolate , $uibModal , $controller) {
  
 +    angular.extend(this, $controller('BasePatronSearchCtrl', {$scope : $scope}));
      $scope.initTab('search');
 -    $scope.focusMe = true;
 -    $scope.searchArgs = {
 -        // default to searching globally
 -        home_ou : egCore.org.tree()
 -    };
 -
 -    // last used patron search form element
 -    var lastFormElement;
  
      $scope.gridControls = {
          activateItem : function(item) {
              $location.path('/circ/patron/' + item.id() + '/checkout');
          },
-         selectedItems : function() {return []}
+         selectedItems : function() { return [] }
+     }
+     $scope.bucketSvc = bucketSvc;
+     $scope.bucketSvc.fetchUserBuckets();
+     $scope.addToBucket = function(item, data, recs) {
+         if (recs.length == 0) return;
+         var added_count = 0;
+         var failed_count = 0;
+         var p = [];
+         angular.forEach(recs,
+             function(rec) {
+                 var item = new egCore.idl.cubi();
+                 item.bucket(data.id());
+                 item.target_user(rec.id());
+                 p.push(egCore.net.request(
+                     'open-ils.actor',
+                     'open-ils.actor.container.item.create',
+                     egCore.auth.token(), 'user', item
+                 ).then(
+                     function(){ added_count++ },
+                     function(){ failed_count++ }
+                 ));
+             }
+         );
+         $q.all(p).then( function () {
+             if (added_count) ngToast.create($interpolate(egCore.strings.BUCKET_ADD_SUCCESS)({ count: ''+added_count, name: data.name()} ));
+             if (failed_count) ngToast.warning($interpolate(egCore.strings.BUCKET_ADD_FAIL)({ count: ''+failed_count, name: data.name() } ));
+         });
+     }
+     var temp_scope = $scope;
+     $scope.openCreateBucketDialog = function() {
+         $uibModal.open({
+             templateUrl: './circ/patron/bucket/t_bucket_create',
+             controller:
+                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+                 $scope.focusMe = true;
+                 $scope.ok = function(args) { $uibModalInstance.close(args) }
+                 $scope.cancel = function () { $uibModalInstance.dismiss() }
+             }]
+         }).result.then(function (args) {
+             if (!args || !args.name) return;
+             bucketSvc.createBucket(args.name, args.desc).then(
+                 function(id) {
+                     if (id) {
+                         $scope.bucketSvc.fetchBucket(id).then(function (b) {
+                             $scope.addToBucket(
+                                 null,
+                                 b,
+                                 $scope.gridControls.selectedItems()
+                             );
+                             $scope.bucketSvc.fetchUserBuckets(true);
+                         });
+                     }
+                 }
+             );
+         });
      }
  
 -    // Handle URL-encoded searches
 -    if ($location.search().search) {
 -        console.log('URL search = ' + $location.search().search);
 -        patronSvc.urlSearch = {search : JSON2js($location.search().search)};
 -
 -        // why the double-JSON encoded sort?
 -        if (patronSvc.urlSearch.search.search_sort) {
 -            patronSvc.urlSearch.sort = 
 -                JSON2js(patronSvc.urlSearch.search.search_sort);
 -        } else {
 -            patronSvc.urlSearch.sort = [];
 -        }
 -        delete patronSvc.urlSearch.search.search_sort;
 -
 -        // include inactive patrons if "inactive" param
 -        if ($location.search().inactive) {
 -            patronSvc.urlSearch.inactive = $location.search().inactive;
 -        }
 -    }
 -
 -    var propagate;
 -    var propagate_inactive;
 -    if (patronSvc.lastSearch) {
 -        propagate = patronSvc.lastSearch.search;
 -        // home_ou needs to be treated specially
 -        propagate.home_ou = {
 -            value : patronSvc.lastSearch.home_ou,
 -            group : 0
 -        };
 -    } else if (patronSvc.urlSearch) {
 -        propagate = patronSvc.urlSearch.search;
 -        if (patronSvc.urlSearch.inactive) {
 -            propagate_inactive = patronSvc.urlSearch.inactive;
 -        }
 -    }
 -
 -    if (egCore.env.pgt) {
 -        $scope.profiles = egCore.env.pgt.list;
 -    } else {
 -        egCore.pcrud.search('pgt', {parent : null}, 
 -            {flesh : -1, flesh_fields : {pgt : ['children']}}
 -        ).then(
 -            function(tree) {
 -                egCore.env.absorbTree(tree, 'pgt')
 -                $scope.profiles = egCore.env.pgt.list;
 -            }
 -        );
 -    }
 -
 -    if (propagate) {
 -        // populate the search form with our cached / preexisting search info
 -        angular.forEach(propagate, function(val, key) {
 -            if (key == 'profile')
 -                val.value = $scope.profiles.filter(function(p) { return p.id() == val.value })[0];
 -            if (key == 'home_ou')
 -                val.value = egCore.org.get(val.value);
 -            $scope.searchArgs[key] = val.value;
 -        });
 -        if (propagate_inactive) {
 -            $scope.searchArgs.inactive = propagate_inactive;
 -        }
 -    }
 -
 -    var provider = egGridDataProvider.instance({});
 -
      $scope.$watch(
          function() {return $scope.gridControls.selectedItems()},
          function(list) {
          },
          true
      );
 -        
 -    provider.get = function(offset, count) {
 -        var deferred = $q.defer();
 -
 -        var fullSearch;
 -        if (patronSvc.urlSearch) {
 -            fullSearch = patronSvc.urlSearch;
 -            // enusre the urlSearch only runs once.
 -            delete patronSvc.urlSearch;
 -
 -        } else {
 -            patronSvc.search_barcode = $scope.searchArgs.card;
 -            
 -            var search = compileSearch($scope.searchArgs);
 -            if (Object.keys(search) == 0) return $q.when();
 -
 -            var home_ou = search.home_ou;
 -            delete search.home_ou;
 -            var inactive = search.inactive;
 -            delete search.inactive;
 -
 -            fullSearch = {
 -                search : search,
 -                sort : compileSort(),
 -                inactive : inactive,
 -                home_ou : home_ou,
 -            };
 -        }
 -
 -        fullSearch.count = count;
 -        fullSearch.offset = offset;
 -
 -        if (patronSvc.lastSearch) {
 -            // search repeated, return the cached results
 -            if (angular.equals(fullSearch, patronSvc.lastSearch)) {
 -                console.log('patron search returning ' + 
 -                    patronSvc.patrons.length + ' cached results');
 -                
 -                // notify has to happen after returning the promise
 -                $timeout(
 -                    function() {
 -                        angular.forEach(patronSvc.patrons, function(user) {
 -                            deferred.notify(user);
 -                        });
 -                        deferred.resolve();
 -                    }
 -                );
 -                return deferred.promise;
 -            }
 -        }
 -
 -        patronSvc.lastSearch = fullSearch;
 -
 -        if (fullSearch.search.id) {
 -            // search by user id performs a direct ID lookup
 -            var userId = fullSearch.search.id.value;
 -            $timeout(
 -                function() {
 -                    egUser.get(userId).then(function(user) {
 -                        patronSvc.localFlesh(user);
 -                        patronSvc.patrons = [user];
 -                        deferred.notify(user);
 -                        deferred.resolve();
 -                    });
 -                }
 -            );
 -            return deferred.promise;
 -        }
 -
 -        if (!Object.keys(fullSearch.search).length) {
 -            // Empty searches are rejected by the server.  Avoid 
 -            // running the the empty search that runs on page load. 
 -            return $q.when();
 -        }
 -
 -        egProgressDialog.open(); // Indeterminate
 -
 -        patronSvc.patrons = [];
 -        var which_sound = 'success';
 -        egCore.net.request(
 -            'open-ils.actor',
 -            'open-ils.actor.patron.search.advanced.fleshed',
 -            egCore.auth.token(), 
 -            fullSearch.search, 
 -            fullSearch.count,
 -            fullSearch.sort,
 -            fullSearch.inactive,
 -            fullSearch.home_ou,
 -            egUser.defaultFleshFields,
 -            fullSearch.offset
 -
 -        ).then(
 -            function() {
 -                deferred.resolve();
 -            },
 -            function() { // onerror
 -                which_sound = 'error';
 -            },
 -            function(user) {
 -                // hide progress bar as soon as the first result appears.
 -                egProgressDialog.close();
 -                patronSvc.localFlesh(user); // inline
 -                patronSvc.patrons.push(user);
 -                deferred.notify(user);
 -            }
 -        )['finally'](function() { // close on 0-hits or error
 -            if (which_sound == 'success' && patronSvc.patrons.length == 0) {
 -                which_sound = 'warning';
 -            }
 -            egCore.audio.play(which_sound + '.patron.by_search');
 -            egProgressDialog.close();
 -        });
 -
 -        return deferred.promise;
 -    };
 -
 -    $scope.patronSearchGridProvider = provider;
 -
 -    // determine the tree depth of the profile group
 -    $scope.pgt_depth = function(grp) {
 -        var d = 0;
 -        while (grp = egCore.env.pgt.map[grp.parent()]) d++;
 -        return d;
 -    }
 -
 -    $scope.clearForm = function () {
 -        $scope.searchArgs={};
 -        if (lastFormElement) lastFormElement.focus();
 -    }
 -
 -    $scope.applyShowExtras = function($event, bool) {
 -        if (bool) {
 -            $scope.showExtras = true;
 -            egCore.hatch.setItem('eg.circ.patron.search.show_extras', true);
 -        } else {
 -            $scope.showExtras = false;
 -            egCore.hatch.removeItem('eg.circ.patron.search.show_extras');
 -        }
 -        if (lastFormElement) lastFormElement.focus();
 -        $event.preventDefault();
 -    }
 -
 -    egCore.hatch.getItem('eg.circ.patron.search.show_extras')
 -    .then(function(val) {$scope.showExtras = val});
 -
 -    // map form arguments into search params
 -    function compileSearch(args) {
 -        var search = {};
 -        angular.forEach(args, function(val, key) {
 -            if (!val) return;
 -            if (key == 'profile' && args.profile) {
 -                search.profile = {value : args.profile.id(), group : 0};
 -            } else if (key == 'home_ou' && args.home_ou) {
 -                search.home_ou = args.home_ou.id(); // passed separately
 -            } else if (key == 'inactive') {
 -                search.inactive = val;
 -            } else {
 -                search[key] = {value : val, group : 0};
 -            }
 -            if (key.match(/phone|ident/)) {
 -                search[key].group = 2;
 -            } else {
 -                if (key.match(/street|city|state|post_code/)) {
 -                    search[key].group = 1;
 -                } else if (key == 'card') {
 -                    search[key].group = 3
 -                }
 -            }
 -        });
 -
 -        return search;
 -    }
 -
 -    function compileSort() {
 -
 -        if (!provider.sort.length) {
 -            return [ // default
 -                "family_name ASC",
 -                "first_given_name ASC",
 -                "second_given_name ASC",
 -                "dob DESC"
 -            ];
 -        }
 -
 -        var sort = [];
 -        angular.forEach(
 -            provider.sort,
 -            function(sortdef) {
 -                if (angular.isObject(sortdef)) {
 -                    var name = Object.keys(sortdef)[0];
 -                    var dir = sortdef[name];
 -                    sort.push(name + ' ' + dir);
 -                } else {
 -                    sort.push(sortdef);
 -                }
 -            }
 -        );
 -
 -        return sort;
 -    }
 -
 -    $scope.setLastFormElement = function() {
 -        lastFormElement = $document[0].activeElement;
 -    }
 -
 -    // search form submit action; tells the results grid to
 -    // refresh itself.
 -    $scope.search = function(args) { // args === $scope.searchArgs
 -        if (args && Object.keys(args).length) 
 -            $scope.gridControls.refresh();
 -        if (lastFormElement) lastFormElement.focus();
 -    }
 -
 -    // TODO: move this into the (forthcoming) grid row activate action
 -    $scope.onPatronDblClick = function($event, user) {
 -        $location.path('/circ/patron/' + user.id() + '/checkout');
 -    }
 -
 -    if (patronSvc.urlSearch) {
 -        // force the grid to load the url-based search on page load
 -        provider.refresh();
 -    }
  
+     $scope.need_one_selected = function() {
+         var items = $scope.gridControls.selectedItems();
+         return (items.length > 0) ? false : true;
+     }
      $scope.need_two_selected = function() {
          var items = $scope.gridControls.selectedItems();
          return (items.length == 2) ? false : true;
@@@ -44,8 -44,8 +44,9 @@@ module.exports = function(config)
        'services/ui.js',
        'services/grid.js',
        'services/op_change.js',
 +      'services/patron_search.js',
        'services/navbar.js', 'services/date.js',
+       'services/user-bucket.js',
        // load app scripts
        'app.js',
        'circ/**/*.js',