lp1908439 Auto-override enhancment
authorJason Etheridge <jason@EquinoxInitiative.org>
Tue, 9 Feb 2021 14:42:06 +0000 (09:42 -0500)
committerMike Rylander <mrylander@gmail.com>
Wed, 18 Aug 2021 13:51:48 +0000 (09:51 -0400)
This reworks the override action dialogs in the patron display for Check
Out and Items Out, and in the Circulation -> Renew Items interface.  It
exposes the auto-override behavior as checkboxes giving staff more fine
grained control over which events are auto-forced or skipped upon
subsequent encounters.  It also changes the Cancel action for batch
renewals to abort the remaining renewals in the batch, and makes it so
that new authorization credentials provided during such a batch will be
treated as an operator change for the entire batch.  We also fix an
existing bug where events marked as already encountered for
auto-override could leak into other patron contexts via Patron Search.

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
Signed-off-by: Terran McCanna <tmccanna@georgialibraries.org>
Signed-off-by: Mike Rylander <mrylander@gmail.com>

Open-ILS/src/templates/staff/base_js.tt2
Open-ILS/src/templates/staff/circ/share/t_event_override_dialog.tt2
Open-ILS/web/js/ui/default/staff/circ/patron/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/items_out.js
Open-ILS/web/js/ui/default/staff/circ/services/circ.js
Open-ILS/web/js/ui/default/staff/services/op_change.js
docs/RELEASE_NOTES_NEXT/Circulation/override-dialogs.adoc [new file with mode: 0644]

index 53e6f5f..f0df81e 100644 (file)
@@ -136,6 +136,7 @@ UpUp.start({
     s.OP_CHANGE_PERM_MESSAGE = "[% l('Another staff member with the above permission may authorize this specific action.  Please notify your library administrator if you need this permission.  If you feel you have received this exception in error, please inform your friendly Evergreen developers or helpdesk staff of the above permission.') %]";
     s.PERM_OP_CHANGE_SUCCESS = "[% l('Permission Override Login Succeeded') %]";
     s.PERM_OP_CHANGE_FAILURE = "[% l('Permission Override Login Failed') %]";
+    s.PERM_OP_CHANGE_PERSIST = "[% l('Re-Using Permission Override Login in Batch') %]";
     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.OPT_IN_RESTRICTED = "[% l("This patron's record is not viewable at your library.") %]";
index 398796f..f3f1317 100644 (file)
@@ -2,35 +2,45 @@
   <div class="modal-header">
     <button type="button" class="close" 
       ng-click="cancel()" aria-hidden="true">&times;</button>
-    <h4 class="modal-title">
+    <h4 ng-if="action == 'checkout'" class="modal-title">
       [% l('Exceptions occurred during checkout.') %]
     </h4>
+    <h4 ng-if="action == 'renew'" class="modal-title">
+      [% l('Exceptions occurred during renewal.') %]
+    </h4>
   </div>
   <div class="modal-body">
     <div ng-repeat="evt in events">
-      <div class="panel panel-danger">
+      <div ng-class="{ 'panel': true, 'panel-danger': !formdata.event_ui_data[evt.ilsevent].overridable, 'panel-warning': formdata.event_ui_data[evt.ilsevent].overridable }">
         <div class="panel-heading">{{evt.textcode}}</div>
         <div class="panel-body">
           <div ng-if="copy_barcode" class="strong-text-2">{{copy_barcode}}</div>
           {{evt.desc}}
-          <div ng-if="evt.textcode == 'ITEM_ON_HOLDS_SHELF'"> 
-              <a target="_blank" href="[% ctx.base_path %]/staff/circ/patron/{{patronID}}/checkout">{{patronName}}</a>.
+          <div ng-if="evt.textcode == 'ITEM_ON_HOLDS_SHELF'">
+            <a target="_blank" href="[% ctx.base_path %]/staff/circ/patron/{{patronID}}/checkout">{{patronName}}</a>.
             <div>
-               <label><input type="checkbox" ng-model="formdata.clearHold"/> 
-               [% l('Cancel this hold upon checkout?') %]</label>
-           </div>
-
-         </div>
+              <label><input type="checkbox" ng-model="formdata.clearHold"/>
+                [% l('Cancel this hold upon checkout?') %]</label>
+            </div>
+          </div>
+          <div ng-if="formdata.event_ui_data[evt.ilsevent].overridable">
+              <label ng-class="{ 'acknowledged': formdata.nonoverridable }"><input type="checkbox" ng-disabled="formdata.nonoverridable" ng-model="formdata.event_ui_data[evt.ilsevent].checkbox"/>
+                [% l('Automatically override for subsequent items?') %]</label>
+          </div>
+          <div ng-if="!formdata.event_ui_data[evt.ilsevent].overridable">
+              <label><input type="checkbox" ng-model="formdata.event_ui_data[evt.ilsevent].checkbox"/>
+                [% l('Automatically skip items with this error?') %]</label>
+          </div>
         </div>
       </div>
     </div>
   </div>
   <div class="modal-footer">
-    <i ng-if="auto_override">[% |l %]If overridden, subsequent checkouts during this patron's 
- session will auto-override this event[% END %]</i>
     <br/><br/>
-    <input type="submit" class="btn btn-primary" 
+    <input type="submit" class="btn btn-primary" ng-disabled="formdata.nonoverridable"
         value="[% l('Force Action?') %]"/>
+    <button class="btn" 
+      ng-click="skip($event)">[% l('Skip?') %]</button>
     <button class="btn btn-warning" 
       ng-click="cancel($event)">[% l('Cancel') %]</button>
   </div>
index 736c938..1cfcba6 100644 (file)
@@ -225,8 +225,8 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod',
  *
  * */
 .controller('PatronCtrl',
-       ['$scope','$q','$location','$filter','egCore','egNet','egUser','egAlertDialog','egConfirmDialog','egPromptDialog','patronSvc',
-function($scope,  $q , $location , $filter , egCore , egNet , egUser , egAlertDialog , egConfirmDialog , egPromptDialog , patronSvc) {
+       ['$scope','$q','$location','$filter','egCore','egNet','egUser','egAlertDialog','egConfirmDialog','egPromptDialog','patronSvc','egCirc',
+function($scope,  $q , $location , $filter , egCore , egNet , egUser , egAlertDialog , egConfirmDialog , egPromptDialog , patronSvc , egCirc) {
 
     $scope.is_patron_edit = function() {
         return Boolean($location.path().match(/patron\/\d+\/edit$/));
@@ -283,6 +283,10 @@ function($scope,  $q , $location , $filter , egCore , egNet , egUser , egAlertDi
         $scope.aous = egCore.env.aous;
         $scope.auth_user_id = egCore.auth.user().id();
 
+        if (tab == 'search') {
+            egCirc.reset(); // clear out auto-override and auto-skip selections when switching patrons
+        }
+
         if (patron_id) {
             $scope.patron_id = patron_id;
             return patronSvc.setPrimary($scope.patron_id)
index d8cc9da..16c04c9 100644 (file)
@@ -493,11 +493,35 @@ function($scope , $q , $routeParams , $timeout , egCore , egUser , patronSvc ,
 
         return egConfirmDialog.open(msg, barcodes.join(' '), {}).result
         .then(function() {
+            window.oils_cancel_batch = false;
+            window.oils_inside_batch = true;
+            function batch_cleanup() {
+                if (window.oils_inside_batch && window.oils_op_change_within_batch) {
+                    window.oils_op_change_undo_func();
+                }
+                window.oils_inside_batch = false;
+                window.oils_op_change_within_batch = false;
+                reset_page();
+            }
             function do_one() {
                 var bc = barcodes.pop();
-                if (!bc) { reset_page(); return }
+                if (!bc) {
+                    batch_cleanup();
+                    return;
+                }
+                if (window.oils_op_change_within_batch) {
+                    window.oils_op_change_toast_func();
+                }
                 // finally -> continue even when one fails
-                egCirc.renew({copy_barcode : bc}).finally(do_one);
+                egCirc.renew({copy_barcode : bc}).finally(function() {
+                    if (!window.oils_cancel_batch) {
+                        do_one();
+                    } else {
+                        console.log('batch cancelled');
+                        batch_cleanup();
+                        return;
+                    }
+                });
             }
             do_one();
         });
index 6f22aa8..975e3ce 100644 (file)
@@ -13,7 +13,9 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
 
     var service = {
         // auto-override these events after the first override
-        auto_override_checkout_events : {},
+        auto_override_circ_events : {},
+        // auto-skip these events after the first skip
+        auto_skip_circ_events : {},
         require_initials : false,
         never_auto_print : {
             hold_shelf_slip : false,
@@ -46,37 +48,23 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
     });
 
     service.reset = function() {
-        service.auto_override_checkout_events = {};
+        service.auto_override_circ_events = {};
+        service.auto_skip_circ_events = {};
     }
 
-    // these events can be overridden by staff during checkout
-    service.checkout_overridable_events = [
-        'PATRON_EXCEEDS_OVERDUE_COUNT',
-        'PATRON_EXCEEDS_CHECKOUT_COUNT',
-        'PATRON_EXCEEDS_FINES',
-        'PATRON_EXCEEDS_LONGOVERDUE_COUNT',
-        'PATRON_BARRED',
-        'CIRC_EXCEEDS_COPY_RANGE',
-        'ITEM_DEPOSIT_REQUIRED',
-        'ITEM_RENTAL_FEE_REQUIRED',
-        'PATRON_EXCEEDS_LOST_COUNT',
-        'COPY_CIRC_NOT_ALLOWED',
-        'COPY_NOT_AVAILABLE',
-        'COPY_IS_REFERENCE',
-        'COPY_ALERT_MESSAGE',
-        'ITEM_ON_HOLDS_SHELF',
-        'STAFF_C',
-        'STAFF_CH',
-        'STAFF_CHR',
-        'STAFF_CR',
-        'STAFF_H',
-        'STAFF_HR',
-        'STAFF_R'
+    // these events cannot be overriden
+    service.nonoverridable_events = [
+        'ACTION_CIRCULATION_NOT_FOUND',
+        'ACTOR_USER_NOT_FOUND',
+        'ASSET_COPY_NOT_FOUND',
+        'PATRON_INACTIVE',
+        'PATRON_CARD_INACTIVE',
+        'PATRON_ACCOUNT_EXPIRED',
+        'PERM_FAILURE' // should be handled elsewhere
     ]
 
-    // after the first override of any of these events, 
-    // auto-override them in subsequent calls.
-    service.checkout_auto_override_after_first = [
+    // Default to checked for "Automatically override for subsequent items?"
+    service.default_auto_override = [
         'PATRON_EXCEEDS_OVERDUE_COUNT',
         'PATRON_BARRED',
         'PATRON_EXCEEDS_LOST_COUNT',
@@ -85,34 +73,6 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
         'PATRON_EXCEEDS_LONGOVERDUE_COUNT'
     ]
 
-
-    // overridable during renewal
-    service.renew_overridable_events = [
-        'PATRON_EXCEEDS_OVERDUE_COUNT',
-        'PATRON_EXCEEDS_LOST_COUNT',
-        'PATRON_EXCEEDS_CHECKOUT_COUNT',
-        'PATRON_EXCEEDS_FINES',
-        'PATRON_EXCEEDS_LONGOVERDUE_COUNT',
-        'CIRC_EXCEEDS_COPY_RANGE',
-        'ITEM_DEPOSIT_REQUIRED',
-        'ITEM_RENTAL_FEE_REQUIRED',
-        'ITEM_DEPOSIT_PAID',
-        'COPY_CIRC_NOT_ALLOWED',
-        'COPY_NOT_AVAILABLE',
-        'COPY_IS_REFERENCE',
-        'COPY_ALERT_MESSAGE',
-        'COPY_NEEDED_FOR_HOLD',
-        'MAX_RENEWALS_REACHED',
-        'CIRC_CLAIMS_RETURNED',
-        'STAFF_C',
-        'STAFF_CH',
-        'STAFF_CHR',
-        'STAFF_CR',
-        'STAFF_H',
-        'STAFF_HR',
-        'STAFF_R'
-    ];
-
     // these checkin events do not produce alerts when 
     // options.suppress_alerts is in effect.
     service.checkin_suppress_overrides = [
@@ -399,7 +359,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
 
         } 
 
-        if (evt.filter(function(e){return !service.auto_override_checkout_events[e.textcode];}).length == 0) {
+        if (evt.filter(function(e){return !service.auto_override_circ_events[e.textcode];}).length == 0) {
             // user has already opted to override these type
             // of events.  Re-run the checkout w/ override.
             options.override = true;
@@ -432,7 +392,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
         } 
 
         // renewal auto-overrides are the same as checkout
-        if (evt.filter(function(e){return !service.auto_override_checkout_events[e.textcode];}).length == 0) {
+        if (evt.filter(function(e){return !service.auto_override_circ_events[e.textcode];}).length == 0) {
             // user has already opted to override these type
             // of events.  Re-run the renew w/ override.
             options.override = true;
@@ -492,26 +452,30 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
         // track the barcode regardless of whether it refers to a copy
         angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
 
-        // Overridable Events
-        if (evt.filter(function(e){return service.renew_overridable_events.indexOf(e.textcode) > -1;}).length > 0)
+        // test for success first to simplify things
+        if (evt[0].textcode == 'SUCCESS') {
+            egCore.audio.play('info.renew');
+            return $q.when(final_resp);
+        }
+
+        // handle Overridable and Non-Overridable Events, but only if no skipped non-overridable events
+        if (evt.filter(function(e){return service.auto_skip_circ_events[e.textcode];}).length == 0) {
             return service.handle_overridable_renew_event(evt, params, options);
+        }
 
         // Other events
         switch (evt[0].textcode) {
-            case 'SUCCESS':
-                egCore.audio.play('info.renew');
-                return $q.when(final_resp);
-
             case 'COPY_IN_TRANSIT':
             case 'PATRON_CARD_INACTIVE':
             case 'PATRON_INACTIVE':
             case 'PATRON_ACCOUNT_EXPIRED':
             case 'CIRC_CLAIMS_RETURNED':
+            case 'ITEM_NOT_CATALOGED':
+            case 'ASSET_COPY_NOT_FOUND':
+                // since handle_overridable_renew_event essentially advertises these events at some point,
+                // we no longer need the original alerts; however, the sound effects are still nice.
                 egCore.audio.play('warning.renew');
-                return service.exit_alert(
-                    egCore.strings[evt[0].textcode],
-                    {barcode : params.copy_barcode}
-                );
+                return $q.reject();
 
             default:
                 egCore.audio.play('warning.renew.unknown');
@@ -533,16 +497,14 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
         // track the barcode regardless of whether it refers to a copy
         angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
 
-        // Overridable Events
-        if (evt.filter(function(e){return service.checkout_overridable_events.indexOf(e.textcode) > -1;}).length > 0)
-            return service.handle_overridable_checkout_event(evt, params, options);
+        // test for success first to simplify things
+        if (evt[0].textcode == 'SUCCESS') {
+            egCore.audio.play('success.checkout');
+            return $q.when(final_resp);
+        }
 
-        // Other events
+        // other events that should precede generic overridable/non-overridable handling
         switch (evt[0].textcode) {
-            case 'SUCCESS':
-                egCore.audio.play('success.checkout');
-                return $q.when(final_resp);
-
             case 'ITEM_NOT_CATALOGED':
                 egCore.audio.play('error.checkout.no_cataloged');
                 return service.precat_dialog(params, options);
@@ -555,16 +517,25 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
             case 'COPY_IN_TRANSIT':
                 egCore.audio.play('warning.checkout.in_transit');
                 return service.copy_in_transit_dialog(evt, params, options);
+        }
+
+        // handle Overridable and Non-Overridable Events, but only if no skipped non-overridable events
+        if (evt.filter(function(e){return service.auto_skip_circ_events[e.textcode];}).length == 0) {
+            return service.handle_overridable_checkout_event(evt, params, options);
+        }
 
+        // Other events
+        switch (evt[0].textcode) {
             case 'PATRON_CARD_INACTIVE':
             case 'PATRON_INACTIVE':
             case 'PATRON_ACCOUNT_EXPIRED':
             case 'CIRC_CLAIMS_RETURNED':
+            case 'ITEM_NOT_CATALOGED':
+            case 'ASSET_COPY_NOT_FOUND':
+                // since handle_overridable_checkout_event essentially advertises these events at some point,
+                // we no longer need the original alerts; however, the sound effects are still nice.
                 egCore.audio.play('warning.checkout');
-                return service.exit_alert(
-                    egCore.strings[evt[0].textcode],
-                    {barcode : params.copy_barcode}
-                );
+                return $q.reject();
 
             default:
                 egCore.audio.play('error.checkout.unknown');
@@ -779,6 +750,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
                 ['$scope', '$uibModalInstance', 
                 function($scope, $uibModalInstance) {
                 $scope.events = evt;
+                $scope.action = action;
 
                 // Find the event, if any, that is for ITEM_ON_HOLDS_SHELF
                 //  and grab the patron name of the owner. 
@@ -797,18 +769,77 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
                     $scope.patronID = $scope.holdEvent.payload.patron_id;
                 }
 
-                $scope.auto_override =
-                    evt.filter(function(e){
-                        return service.checkout_auto_override_after_first.indexOf(evt.textcode) > -1;
-                    }).length > 0;
                 $scope.copy_barcode = params.copy_barcode; // may be null
 
                 // Implementation note: Why not use a primitive here? It
                 // doesn't work.  See: 
                 // http://stackoverflow.com/questions/18642371/checkbox-not-binding-to-scope-in-angularjs
-                $scope.formdata = {clearHold : service.clearHold};
+                $scope.formdata = {
+                    clearHold : service.clearHold,
+                    nonoverridable: evt.filter(function(e){
+                        return service.nonoverridable_events.indexOf(e.textcode) > -1;}).length > 0,
+                    event_ui_data : Object.fromEntries(
+                        evt.map( e => [ e.ilsevent, {
+                            // non-overridable events will be rare, but they are skippable.  We use
+                            // the same checkbox variable to track desired skip and auto-override
+                            // selections.
+                            overridable: service.nonoverridable_events.indexOf(e.textcode) == -1,
+                            // for non-overridable events, we'll default the checkbox to any previous
+                            // choice made for the current patron, though normally the UI will be
+                            // suppressed unless some previously unencountered events are in the set
+                            checkbox: service.nonoverridable_events.indexOf(e.textcode) > -1
+                            ? (service.auto_skip_circ_events[e.textcode] == undefined
+                                ? false
+                                : service.auto_skip_circ_events[e.textcode]
+                            )
+                            // if a given event is overridable, said checkbox will default to any previous
+                            // choice made for the current patron, as long as there are no non-overridable
+                            // events in the set (because we'll disable the checkbox in that case and don't
+                            // want to imply that we're going to set an auto-override)
+                            : (service.auto_override_circ_events[e.textcode] == undefined
+                                ? (
+                                    service.nonoverridable_events.indexOf(e.textcode) > -1
+                                    ? false
+                                    : service.default_auto_override.indexOf(e.textcode) > -1
+                                )
+                                : service.auto_override_circ_events[e.textcode]
+                            )
+                        }])
+                    ) 
+                };
+
+                function update_auto_override_and_skip_lists() {
+                    angular.forEach(evt, function(e){
+                        if ($scope.formdata.nonoverridable) {
+                            // the action had at least one non-overridable event, so let's only
+                            // record skip choices for those
+                            if (!$scope.formdata.event_ui_data[e.ilsevent].overridable) {
+                                if ($scope.formdata.event_ui_data[e.ilsevent].checkbox) {
+                                    // grow the skip list
+                                    service.auto_skip_circ_events[e.textcode] = true;
+                                } else {
+                                    // shrink the skip list
+                                    service.auto_skip_circ_events[e.textcode] = false;
+                                }
+                            }
+                        } else {
+                            // record all auto-override choices
+                            if ($scope.formdata.event_ui_data[e.ilsevent].checkbox) {
+                                // grow the auto-override list
+                                service.auto_override_circ_events[e.textcode] = true;
+                            } else {
+                                // shrink the auto-override list
+                                service.auto_override_circ_events[e.textcode] = false;
+                            }
+                        }
+                    });
+                    // for debugging
+                    window.oils_auto_skip_circ_events = service.auto_skip_circ_events;
+                    window.oils_auto_override_circ_events = service.auto_override_circ_events;
+                }
 
                 $scope.ok = function() { 
+                    update_auto_override_and_skip_lists();
                     // Handle the cancellation of the assciated hold here
                     if ($scope.formdata.clearHold && $scope.holdID) {
                         egCore.net.request(
@@ -822,7 +853,14 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
                     $uibModalInstance.close();
                 }
 
+                $scope.skip = function($event) {
+                    update_auto_override_and_skip_lists();
+                    $uibModalInstance.dismiss();
+                    $event.preventDefault();
+                }
+
                 $scope.cancel = function ($event) { 
+                    window.oils_cancel_batch = true;
                     $uibModalInstance.dismiss();
                     $event.preventDefault();
                 }
@@ -839,12 +877,6 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
                     return service.checkin(params, options);
                 }
 
-                // checkout/renew support override-after-first
-                angular.forEach(evt, function(e){
-                    if (service.checkout_auto_override_after_first.indexOf(e.textcode) > -1)
-                        service.auto_override_checkout_events[e.textcode] = true;
-                });
-
                 return service[action](params, options);
             }
         );
index 5d06d71..172e81d 100644 (file)
@@ -97,8 +97,24 @@ function($uibModal, $interpolate, $rootScope, $q, egAuth, egStrings, egNet, ngTo
 
             )['finally'](function() {
                 // always undo the operator change after a perm override.
-                console.debug("clearing op-change after perm override redo");
-                service.changeOperatorUndo(true);
+                // well, unless inside a UI "batch" like Renew All
+                if (!window.oils_inside_batch) {
+                    console.debug("clearing op-change after perm override redo");
+                    window.oils_op_change_within_batch = false;
+                    service.changeOperatorUndo(true);
+                } else {
+                    // up to the batch caller to call changeOperatorUndo
+                    console.debug("persisting op-change after perm override redo");
+                    window.oils_op_change_within_batch = true;
+                    // this is an even kludgier use of window-scoped variables
+                    window.oils_op_change_undo_func = function() {
+                        console.debug("clearing op-change after perm override redo");
+                        service.changeOperatorUndo(true);
+                    }
+                    window.oils_op_change_toast_func = function() {
+                        ngToast.create(egStrings.PERM_OP_CHANGE_PERSIST);
+                    }
+                }
             });
         });
     }
diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/override-dialogs.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/override-dialogs.adoc
new file mode 100644 (file)
index 0000000..0e13967
--- /dev/null
@@ -0,0 +1,3 @@
+== Override Dialogs  ==
+
+This reworks the override action dialogs in the patron display for Check Out and Items Out, and in the Circulation -> Renew Items interface.  It exposes the auto-override behavior as checkboxes giving staff more fine grained control over which events are auto-forced or skipped upon subsequent encounters.  It also changes the Cancel action for batch renewals to abort the remaining renewals in the batch, and makes it so that new authorization credentials provided during such a batch will be treated as an operator change for the entire batch.  We also fix an existing bug where events marked as already encountered for auto-override could leak into other patron contexts via Patron Search.