[open-ils-commits] [GIT] Evergreen ILS branch rel_2_10 updated. 9f4dfb130d4ca941519d534c81dabf08a5d9c271

Evergreen Git git at git.evergreen-ils.org
Mon Mar 14 17:55:00 EDT 2016


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, rel_2_10 has been updated
       via  9f4dfb130d4ca941519d534c81dabf08a5d9c271 (commit)
       via  ed185fce50d3aced2200bc37ad96595db4e088d8 (commit)
       via  238b96537dadb1e4d3ef4c97549d792f49d03544 (commit)
      from  4effe3a41071afd893fa5103c0ea555091ce41cb (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 9f4dfb130d4ca941519d534c81dabf08a5d9c271
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Mar 6 18:09:30 2016 -0500

    LP1550035 Address deletion / avoid xact collisions
    
    Avoid transaction collision errors when updating patrons, in cases where
    the patron object has to be updated more than once, by refreshing the
    last_xact_id value on the in-progress patron object.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
index 56205f2..87fb870 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
@@ -667,6 +667,10 @@ sub _update_patron {
 
     $e->update_actor_user($patron) or return (undef, $e->die_event);
 
+    # re-fetch the user to pick up the latest last_xact_id value
+    # to avoid collisions.
+    $patron = $e->retrieve_actor_user($patron->id);
+
     return ($patron);
 }
 

commit ed185fce50d3aced2200bc37ad96595db4e088d8
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Mar 6 17:50:30 2016 -0500

    LP#1550036 Linked patrons for new users
    
    Support adding auzillary (linked) groups for new patrons in the browser
    client editor.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js b/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
index 1749f32..ae7838e 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
@@ -67,6 +67,21 @@ angular.module('egCoreMod')
         });
     }
 
+    service.apply_secondary_groups = function(user_id, group_ids) {
+        return egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.set_groups',
+            egCore.auth.token(), user_id, group_ids)
+        .then(function(resp) {
+            if (resp == 1) {
+                return true;
+            } else {
+                // debugging -- should be no events
+                alert('linked groups failure ' + egCore.evt.parse(resp));
+            }
+        });
+    }
+
     service.get_stage_user = function() {
         if (!service.stage_username) return $q.when();
 
@@ -619,6 +634,7 @@ angular.module('egCoreMod')
             cards : [card],
             home_ou : egCore.org.get(egCore.auth.user().ws_ou()),
             stat_cat_entries : [],
+            groups : [],
             addresses : [addr]
         };
 
@@ -1463,19 +1479,20 @@ function PatronRegCtrl($scope, $routeParams,
             }
         }).result.then(
             function(args) {
+
+                if ($scope.patron.isnew) {
+                    // groups must be linked for new patrons after the
+                    // patron is created.
+                    $scope.patron.groups = args.linked_groups;
+                    return;
+                }
+
+                // update links groups for existing users in real time.
                 var ids = args.linked_groups.map(function(g) {return g.id()});
-                console.log('linking permission groups ' + ids);
-                return egCore.net.request(
-                    'open-ils.actor',
-                    'open-ils.actor.user.set_groups',
-                    egCore.auth.token(), $scope.patron.id, ids)
-                .then(function(resp) {
-                    if (resp == 1) {
+                patronRegSvc.apply_secondary_groups($scope.patron.id, ids)
+                .then(function(success) {
+                    if (success)
                         $scope.patron.groups = args.linked_groups;
-                    } else {
-                        // debugging -- should be no events
-                        alert('linked groups failure ' + egCore.evt.parse(resp));
-                    }
                 });
             }
         );
@@ -1666,6 +1683,20 @@ function PatronRegCtrl($scope, $routeParams,
             if (updated_user) 
                 return patronRegSvc.remove_staged_user();
 
+            return $q.when();
+
+        }).then(function() {
+
+            // linked groups for new users must be created after the new
+            // user is created.
+            if ($scope.patron.isnew && 
+                $scope.patron.groups && $scope.patron.groups.length) {
+                var ids = $scope.patron.groups.map(function(g) {return g.id()});
+                return patronRegSvc.apply_secondary_groups(updated_user.id(), ids)
+            }
+
+            return $q.when();
+
         }).then(function() {
 
             // reloading the page means potentially losing some information

commit 238b96537dadb1e4d3ef4c97549d792f49d03544
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Feb 24 23:07:54 2016 -0500

    LP#1553813 Patron editor validation / perm checks.
    
    1. Adds support for enforcing ui.patron.edit.*.require and
    ui.patron.edit.*.regex org unit settings via Angular's ng-pattern and
    ng-required attributes.
    
    2. Supports selecting only valid profile groups and home org units.
    
    3. Warns the user when a duplicate barcode or username is encountered.
    
    When any fields in the form are invalid, the save options are disabled.
    
    ==
    
    Adds support for enforcing the following permissions:
    
    UPDATE_USER
    CREATE_USER
    CREATE_USER_GROUP_LINK
    UPDATE_PATRON_COLLECTIONS_EXEMPT
    UPDATE_PATRON_CLAIM_RETURN_COUNT
    UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT
    UPDATE_PATRON_ACTIVE_CARD
    UPDATE_PATRON_PRIMARY_CARD
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2 b/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
index a533ba8..1508018 100644
--- a/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
@@ -24,10 +24,12 @@
 <div>
   <span class="pad-all-min">
     <button type="button" class="btn btn-default" 
+      ng-disabled="edit_passthru.hide_save_actions()"
       ng-click="edit_passthru.save()">[% l('Save') %]</button>
   </span>
   <span class="pad-all-min">
     <button type="button" class="btn btn-default"
+      ng-disabled="edit_passthru.hide_save_actions()"
       ng_click="edit_passthru.save({clone:true})">[% l('Save & Clone') %]</button>
   </span>
 </div>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index c1e769e..6a5fcc2 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -19,7 +19,6 @@
 <div ng-if="patron_id"
     class="strong-text-2">[% l('Patron Edit') %]</div>
 
-
 <div id="reg-alert-pane">
 
   <div id="reg-dupe-links">
@@ -74,259 +73,345 @@
   </div>
 </div>
 
-
-[% MACRO formfield(cls, field, path, input_type) BLOCK;
-
-  # input field generator for common text/number/checkbox fields
-
-  IF NOT input_type; input_type = 'text'; END %] 
-
-<div class="row reg-field-row" 
-  ng-show="show_field('[% cls _ '.' _ field %]')">
-
+[% MACRO draw_field_label (cls, field) BLOCK %]
   <div class="col-md-3 reg-field-label"> <!-- field label -->
-
     <label>{{idl_fields.[% cls %].[% field %].label}}</label>
-
     <!-- field documentation img/link -->
     <img ng-show="field_doc.[% cls %].[% field %]" 
       ng-click="set_selected_field_doc('[% cls %]','[% field %]')"
       src='[% DOC_IMG %]'></img>
   </div>
+[% END %]
 
-  <div class="col-md-3 reg-field-input"> <!-- field form input -->
 
-  [% model = path ? 'patron.' _ path _ '.' _ field : 'patron.' _ field %]
+[% 
+# draws a vanilla form input field for inputs that require no 
+# special additions.
+MACRO draw_form_input(cls, field, path, type, disable) BLOCK;
+  IF !type; type = 'text'; END;
+  base_obj = path ? 'patron.' _ path : 'patron';
+  model = base_obj _ '.' _ field;
+%]
+  <div class="col-md-3 reg-field-input">
+    <input 
+      type="[% type %]" 
+      class="form-control" 
+      name="[% model %]"
+      ng-change="field_modified()" 
+      ng-required="field_required('[% cls %]', '[% field %]')"
+      ng-blur="handle_field_changed([% base_obj %], '[% field %]')"
+      ng-pattern="field_pattern('[% cls %]', '[% field %]')"
+      [% IF disable %]ng-disabled="[% disable %]"[% END %]
+      ng-model="[% model %]"/>
+  </div>
+[% END %]
 
-  [% IF input_type == 'checkbox' %]
+[% MACRO draw_example_text(cls, field) BLOCK;
+  set_str = "org_settings['ui.patron.edit." _ cls _ "." _ field _ ".example']";
+%]
+  <span ng-if="[% set_str %]">
+    [% l('Example: [_1]', '{{' _ set_str _ '}}') %]
+  </span>
+[% END %]
 
-    <div class='checkbox'>
-      <input type='checkbox' ng-model='[% model %]'/>
+<!-- progress dialog displayed as we await all data to finish loading -->
+<div class="row" ng-show="!page_data_loaded">
+  <div class="col-md-6 pad-vert">
+    <div class="progress progress-striped active">
+        <div class="progress-bar"  role="progressbar" aria-valuenow="100" 
+              aria-valuemin="0" aria-valuemax="100" style="width: 100%">
+            <span class="sr-only">[% l('Loading...') %]</span>
+        </div>
     </div>
+  </div>
+</div>
 
-  [% ELSE %]
-    <!-- text / number input -->
-
-    [% IF field == 'alert_message' %]
-      <textarea ng-change="field_modified()" 
-        class="form-control" ng-model="[% model %]"/>
-    [% ELSIF field == 'post_code' %]
-      <input type="text" ng-change="field_modified()" 
-        ng-blur="post_code_changed(patron.[% path %])"
-        class="form-control" ng-model="[% model %]"/>
-    [% ELSIF field == 'barcode' %]
+<!--  
+MAIN FORM
+This div wraps the entire form so we can hide it until all needed data
+has been loaded.  Setting ng-form and a name lets us refer to fields
+within the "form" by name for validation.
+-->
+<div ng-form id="patron-reg-container" 
+  name="reg_form" ng-show="page_data_loaded">
+
+<!-- BARCODE -->
+
+<div class="row reg-field-row" ng-show="show_field('ac.barcode')">
+  [% draw_field_label('ac', 'barcode') %]
+  <div class="col-md-3 reg-field-input"> <!-- field form input -->
       <input type="text" 
+        name="barcode"
+        ng-model="patron.card.barcode"
+        ng-pattern="field_pattern('ac', 'barcode')"
+        ng-required="field_required('ac', 'barcode')"
         focus-me="focus_bc"
         ng-change="field_modified()" 
         ng-disabled="disable_bc"
-        ng-blur="barcode_changed(patron.card.barcode)"
-        class="form-control" ng-model="[% model %]"/>
-    [% ELSIF field == 'usrname' %]
-      <input type="text" 
-        focus-me="focus_usrname"
-        ng-change="field_modified()" 
-        ng-blur="usrname_changed(patron.usrname)"
-        class="form-control" ng-model="[% model %]"/>
-    [% ELSIF field == 'day_phone' %]
-      <input type="text" 
-        ng-blur="day_phone_changed(patron.day_phone)"
-        ng-change="field_modified()" 
-        class="form-control" ng-model="[% model %]"/>
-    [% ELSIF field.match('phone') %]
-      <input type="text" 
-        ng-change="field_modified()" 
-        ng-blur="dupe_value_changed('phone', patron.[% field %])"
-        class="form-control" ng-model="[% model %]"/>
-    [% ELSIF field.match('ident_value') %]
-      <input type="text" 
-        ng-change="field_modified()" 
-        ng-blur="dupe_value_changed('ident', patron.[% field %])"
-        class="form-control" ng-model="[% model %]"/>
-    [% ELSIF field == 'first_given_name' OR field == 'family_name' %]
-      <input type="text" 
-        ng-change="field_modified()" 
-        ng-blur="dupe_value_changed('name', patron.[% field %])"
-        class="form-control" ng-model="[% model %]"/>
-    [% ELSIF field == 'email' %]
-      <input type="[% input_type %]" 
-        ng-change="field_modified()" 
-        ng-blur="dupe_value_changed('email', patron.email)"
-        class="form-control" ng-model="[% model %]"/>
-    [% ELSIF field.match('street') OR field == 'city' %]
-      <!-- note: passing address object to dupe_value_changed -->
-      <input type="[% input_type %]" 
-        ng-change="field_modified()" 
-        ng-blur="dupe_value_changed('address', patron.[% path %])"
-        class="form-control" ng-model="[% model %]"/>
-    [% ELSE %]
-      <input type="[% input_type %]" 
-        ng-change="field_modified()" 
-        class="form-control" ng-model="[% model %]"/>
-    [% END %]
-  [% END %]
-
+        class="form-control" 
+        ng-blur="handle_field_changed(patron.card, 'barcode')"/>
   </div>
-
-  <!-- supplemental actions and example text -->
   <div class="col-md-6 patron-reg-example">
-
-    [% IF field == 'barcode' %]
-
       <button class="btn btn-default" ng-show="!patron.isnew"
         ng-click="replace_card()">[% l('Replace Barcode') %]</button>
       <button class="btn btn-default" 
         ng-click="cards_dialog()">[% l('See All') %]</button>
+      <div ng-show="dupe_barcode" class="patron-reg-validation-alert">
+        <span>[% l('Barcode is already in use') %]</span>
+      </div>
+  </div>
+</div>
 
-    [% ELSIF field == 'passwd' %]
+<!-- USRNAME -->
 
-      <button class="btn btn-default" ng-click="generate_password()">
-        [% l('Generate Password') %]</button>
+<div class="row reg-field-row" ng-show="show_field('au.usrname')">
+  [% draw_field_label('au', 'usrname') %]
+  <div class="col-md-3 reg-field-input">
+    <input type="text" 
+      name='usrname'
+      ng-required="field_required('au', 'usrname')"
+      focus-me="focus_usrname"
+      ng-change="field_modified()" 
+      ng-pattern="field_pattern('au', 'usrname')"
+      ng-blur="handle_field_changed(patron, 'usrname')"
+      class="form-control" 
+      ng-model="patron.usrname"/>
+  </div>
+  <div class="col-md-6 patron-reg-example">
+    <div ng-show="dupe_username" class="patron-reg-validation-alert">
+      <span>[% l('Username is already in use') %]</span>
+    </div>
+  </div>
+</div>
 
-    [% ELSE %]
+<!-- PASSWD -->
 
-      <!-- invalidate buttons -->
+<div class="row reg-field-row" ng-show="show_field('au.passwd')">
+  [% draw_field_label('au', 'passwd') %]
+  [% draw_form_input('au', 'passwd'); %]
+  <div class="col-md-6 patron-reg-example">
+    <button class="btn btn-default" ng-click="generate_password()">
+      [% l('Generate Password') %]</button>
+  </div>
+</div>
 
-      [% IF field.match('phone') OR field.match('email') %]
-        <button ng-show="patron.[% field %] && !patron.isnew" 
-            class="btn btn-default" 
-            ng-click="invalidate_field('[% field %]')">
-            [% l('Invalidate') %]
-        </button>
-      [% END %]
+<!-- PREFIX -->
 
-      <!-- example strings -->
+<div class="row reg-field-row" ng-show="show_field('au.prefix')">
+  [% draw_field_label('au', 'prefix') %]
+  [% draw_form_input('au', 'prefix'); %]
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'prefix') %]
+  </div>
+</div>
 
-      [% set_str = "org_settings['ui.patron.edit." _ 
-          cls _ "." _ field _ ".example']"; %]
+<!-- FIRST_GIVEN_NAME -->
 
-      <span ng-if="[% set_str %]">
-        [% l('Example: [_1]', "{{" _ set_str _ "}}") %]
-      </span>
+<div class="row reg-field-row" ng-show="show_field('au.first_given_name')">
+  [% draw_field_label('au', 'first_given_name') %]
+  [% draw_form_input('au', 'first_given_name'); %]
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'first_given_name') %]
+  </div>
+</div>
 
-      <!-- phones have a fall-through example strings -->
-      [% IF field.match('phone') %]
-        <span ng-if="![% set_str %] && org_settings['ui.patron.edit.phone.example']">
-          [% l('Example: [_1]', 
-          "{{org_settings['ui.patron.edit.phone.example']}}") %]
-        </span>
-      [% END %]
-    [% END %]
+<!-- SECOND_GIVEN_NAME -->
+
+<div class="row reg-field-row" ng-show="show_field('au.second_given_name')">
+  [% draw_field_label('au', 'second_given_name') %]
+  [% draw_form_input('au', 'second_given_name'); %]
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'second_given_name') %]
   </div>
 </div>
-[% END %]
 
-<!-- progress dialog displayed as we await all data to finish loading -->
-<div class="row" ng-show="!page_data_loaded">
-  <div class="col-md-6 pad-vert">
-    <div class="progress progress-striped active">
-        <div class="progress-bar"  role="progressbar" aria-valuenow="100" 
-              aria-valuemin="0" aria-valuemax="100" style="width: 100%">
-            <span class="sr-only">[% l('Loading...') %]</span>
-        </div>
-    </div>
+<!-- FAMILY_NAME -->
+
+<div class="row reg-field-row" ng-show="show_field('au.family_name')">
+  [% draw_field_label('au', 'family_name') %]
+  [% draw_form_input('au', 'family_name'); %]
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'family_name') %]
   </div>
 </div>
 
-<!-- this div wraps the entire form so we can hide it 
-     until all needed data has been loaded -->
-<div ng-show="page_data_loaded"><!-- form wrapper -->
+<!-- SUFFIX -->
 
-[% formfield('ac', 'barcode', 'card') %]
-[% formfield('au', 'usrname') %]
-[% formfield('au', 'passwd') %]
-[% formfield('au', 'prefix') %]
-[% formfield('au', 'first_given_name') %]
-[% formfield('au', 'second_given_name') %]
-[% formfield('au', 'family_name') %]
-[% formfield('au', 'suffix') %]
-[% formfield('au', 'alias') %]
+<div class="row reg-field-row" ng-show="show_field('au.suffix')">
+  [% draw_field_label('au', 'suffix') %]
+  [% draw_form_input('au', 'suffix'); %]
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'suffix') %]
+  </div>
+</div>
 
-<div class="row reg-field-row" ng-show="show_field('au.dob')">
-  <div class="col-md-3 reg-field-label">
-    <label>{{idl_fields.au.dob.label}}</label>
-    <img ng-show="field_doc.au.dob" 
-      ng-click="selected_field_doc=field_doc.au.dob"
-      src='[% DOC_IMG %]'></img>
+<!-- ALIAS -->
+
+<div class="row reg-field-row" ng-show="show_field('au.alias')">
+  [% draw_field_label('au', 'alias') %]
+  [% draw_form_input('au', 'alias'); %]
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'alias') %]
   </div>
+</div>
+
+<!-- DOB -->
+
+<div class="row reg-field-row" ng-show="show_field('au.dob')">
+  [% draw_field_label('au', 'dob') %]
   <div class="col-md-3 reg-field-input">
     <input eg-date-input 
+      name="dob"
       ng-change="field_modified()" 
+      ng-required="field_required('au', 'dob')"
+      ng-blur="handle_field_changed(patron, 'dob')"
       class="form-control" ng-model="patron.dob"/>
   </div>
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'dob') %]
+  </div>
 </div>
 
-[% formfield('au', 'juvenile', '', 'checkbox') %]
+<!-- JUVENILE -->
+
+<div class="row reg-field-row" ng-show="show_field('au.juvenile')">
+  [% draw_field_label('au', 'juvenile') %]
+  [% draw_form_input('au', 'juvenile', '', 'checkbox'); %]
+</div>
 
 <!-- ident_type -->
 
 <div class="row reg-field-row" ng-show="show_field('au.ident_type')">
-  <div class="col-md-3 reg-field-label">
-    <label>{{idl_fields.au.ident_type.label}}</label>
-    <img ng-show="field_doc.au.ident_type" 
-      ng-click="selected_field_doc=field_doc.au.ident_type"
-      src='[% DOC_IMG %]'></img>
-  </div>
+  [% draw_field_label('au', 'ident_type') %]
   <div class="col-md-3 reg-field-input">
-    <div class="btn-group" dropdown>
-      <button type="button" class="btn btn-default dropdown-toggle">
-        <span style="padding-right: 5px;">
-          {{patron.ident_type.name() || "[% l('Primary Ident Type') %]"}}
-        </span>
-        <span class="caret"></span>
-      </button>
-      <ul class="dropdown-menu">
-        <li ng-repeat="type in ident_types">
-          <a href ng-click="patron.ident_type = type; field_modified()">
-            {{type.name()}}
-          </a>
-        </li>
-      </ul>
-    </div>
+    <select 
+      class="form-control" 
+      ng-model="patron.ident_type"
+      ng-required="field_required('au', 'ident_type')"
+      ng-blur="handle_field_changed(patron, 'ident_type')"
+      ng-options="type.name() for type in ident_types track by type.id()">
+    </select>
   </div>
 </div>
 
+<!-- IDENT_VALUE -->
+
+<div class="row reg-field-row" ng-show="show_field('au.ident_value')">
+  [% draw_field_label('au', 'ident_value') %]
+  [% draw_form_input('au', 'ident_value') %]
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'ident_value') %]
+  </div>
+</div>
+
+<!-- IDENT_VALUE2 -->
+<div class="row reg-field-row" ng-show="show_field('au.ident_value2')">
+  [% draw_field_label('au', 'ident_value2') %]
+  [% draw_form_input('au', 'ident_value2') %]
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'ident_value2') %]
+  </div>
+</div>
 
-[% formfield('au', 'ident_value') %]
-[% formfield('au', 'ident_value2') %]
-[% formfield('au', 'email', '', 'email') %]
-[% formfield('au', 'day_phone') %]
-[% formfield('au', 'evening_phone') %]
-[% formfield('au', 'other_phone') %]
+
+<!-- EMAIL -->
+<div class="row reg-field-row" ng-show="show_field('au.email')">
+  [% draw_field_label('au', 'email') %]
+  [% draw_form_input('au', 'email', '', 'email') %]
+  <div class="col-md-6 patron-reg-example">
+    <button ng-show="patron.email && !patron.isnew" 
+      class="btn btn-default" 
+      ng-click="invalidate_field('email')">[% l('Invalidate') %]</button>
+    <span ng-if="org_settings['ui.patron.edit.au.email.example']">
+      [% l('Example: [_1]',
+        "{{org_settings['ui.patron.edit.au.email.example']}}") %]
+    </span>
+  </div>
+</div>
+
+<!-- DAY_PHONE -->
+
+<div class="row reg-field-row" ng-show="show_field('au.day_phone')">
+  [% draw_field_label('au', 'day_phone') %]
+  [% draw_form_input('au', 'day_phone') %]
+  <div class="col-md-6 patron-reg-example">
+    <button ng-show="patron.day_phone && !patron.isnew" 
+        class="btn btn-default" 
+        ng-click="invalidate_field('day_phone')">[% l('Invalidate') %]</button>
+    [% draw_example_text('au', 'day_phone') %]
+    <!-- phones have a fall-through example strings -->
+    <span ng-if="!org_settings['ui.patron.edit.au.day_phone.example'] && org_settings['ui.patron.edit.phone.example']">
+      [% l('Example: [_1]', 
+        "{{org_settings['ui.patron.edit.phone.example']}}") %]
+    </span>
+  </div>
+</div>
+
+<!-- EVENING_PHONE -->
+
+<div class="row reg-field-row" ng-show="show_field('au.evening_phone')">
+  [% draw_field_label('au', 'evening_phone') %]
+  [% draw_form_input('au', 'evening_phone') %]
+  <div class="col-md-6 patron-reg-example">
+    <button ng-show="patron.evening_phone && !patron.isnew" 
+        class="btn btn-default" 
+        ng-click="invalidate_field('evening_phone')">[% l('Invalidate') %]</button>
+    [% draw_example_text('au', 'evening_phone') %]
+    <!-- phones have a fall-through example strings -->
+    <span ng-if="!org_settings['ui.patron.edit.au.evening_phone.example'] && org_settings['ui.patron.edit.phone.example']">
+      [% l('Example: [_1]', 
+        "{{org_settings['ui.patron.edit.phone.example']}}") %]
+    </span>
+  </div>
+</div>
+
+<!-- OTHER_PHONE -->
+
+<div class="row reg-field-row" ng-show="show_field('au.other_phone')">
+  [% draw_field_label('au', 'other_phone') %]
+  [% draw_form_input('au', 'other_phone') %]
+  <div class="col-md-6 patron-reg-example">
+    <button ng-show="patron.other_phone && !patron.isnew" 
+        class="btn btn-default" 
+        ng-click="invalidate_field('other_phone')">[% l('Invalidate') %]</button>
+    [% draw_example_text('au', 'other_phone') %]
+    <!-- phones have a fall-through example strings -->
+    <span ng-if="!org_settings['ui.patron.edit.au.other_phone.example'] && org_settings['ui.patron.edit.phone.example']">
+      [% l('Example: [_1]', 
+        "{{org_settings['ui.patron.edit.phone.example']}}") %]
+    </span>
+  </div>
+</div>
 
 <!-- home org unit selector -->
 
 <div class="row reg-field-row" ng-show="show_field('au.home_ou')">
-  <div class="col-md-3 reg-field-label">
-    <label>{{idl_fields.au.home_ou.label}}</label>
-    <img ng-show="field_doc.au.home_ou" 
-      ng-click="selected_field_doc=field_doc.au.home_ou"
-      src='[% DOC_IMG %]'></img>
-    </div>
-    <div class="col-md-3 reg-field-input">
-      <eg-org-selector selected="patron.home_ou" onchange="field_modified">
-      </eg-org-selector>
+  [% draw_field_label('au', 'home_ou') %]
+  <div class="col-md-3 reg-field-input">
+    <eg-org-selector 
+      selected="patron.home_ou" 
+      onchange="handle_home_org_changed"
+      disable-test="disable_home_org">
+    </eg-org-selector>
   </div>
 </div>
 
 <!-- profile selector -->
 
 <div class="row reg-field-row" ng-show="show_field('au.profile')">
-  <div class="col-md-3 reg-field-label">
-    <label>{{idl_fields.au.profile.label}}</label>
-    <img ng-show="field_doc.au.profile" 
-      ng-click="selected_field_doc=field_doc.au.profile"
-      src='[% DOC_IMG %]'></img>
-  </div>
+  [% draw_field_label('au', 'profile') %]
   <div class="col-md-3 reg-field-input">
     <div class="btn-group" dropdown>
-      <button type="button" class="btn btn-default dropdown-toggle">
+      <button type="button" class="btn btn-default dropdown-toggle"
+          ng-class="{'ng-invalid' : invalid_profile()}">
         <span style="padding-right: 5px;">
           {{patron.profile.name() || "[% l('Profile Group') %]"}}
         </span>
         <span class="caret"></span>
       </button>
       <ul class="dropdown-menu">
-        <li ng-repeat="grp in edit_profiles">
+        <li ng-repeat="grp in edit_profiles" 
+          ng-class="{disabled : grp.usergroup() == 'f'}">
           <a href 
             style="padding-left: {{pgt_depth(grp) * 10 + 5}}px"
             ng-click="set_profile(grp)">{{grp.name()}}</a>
@@ -335,21 +420,16 @@
     </div>
   </div>
   <div class="col-md-3">
-    <button class="btn btn-default" ng-disabled="!has_group_link_perm"
+    <button class="btn btn-default" ng-disabled="!perms.CREATE_USER_GROUP_LINK"
       ng-click="secondary_groups_dialog()">[% l('Secondary Groups') %]</button>
   </div> 
 </div>
 
 <div class="row reg-field-row" ng-show="show_field('au.expire_date')">
-  <div class="col-md-3 reg-field-label">
-  <label>{{idl_fields.au.expire_date.label}}</label>
-    <img ng-show="field_doc.au.expire_date" 
-    ng-click="selected_field_doc=field_doc.au.expire_date"
-    src='[% DOC_IMG %]'></img>
-  </div>
+  [% draw_field_label('au', 'expire_date') %]
   <div class="col-md-3 reg-field-input">
     <input eg-date-input 
-      ng-change="field_modified()" 
+      ng-blur="handle_field_changed(patron, 'expire_date')"
       class="form-control" ng-model="patron.expire_date"/>
   </div>
   <div class="col-md-3">
@@ -361,36 +441,78 @@
 <!-- net_access_level -->
 
 <div class="row reg-field-row" ng-show="show_field('au.net_access_level')">
-  <div class="col-md-3 reg-field-label">
-    <label>{{idl_fields.au.net_access_level.label}}</label>
-    <img ng-show="field_doc.au.net_access_level" 
-      ng-click="selected_field_doc=field_doc.au.net_access_level"
-      src='[% DOC_IMG %]'></img>
-  </div>
+  [% draw_field_label('au', 'net_access_level') %]
   <div class="col-md-3 reg-field-input">
-    <div class="btn-group" dropdown>
-      <button type="button" class="btn btn-default dropdown-toggle">
-        <span style="padding-right: 5px;">
-          {{patron.net_access_level.name() || "[% l('Net Access Level') %]"}}
-        </span>
-        <span class="caret"></span>
-      </button>
-      <ul class="dropdown-menu">
-        <li ng-repeat="level in net_access_levels">
-          <a href 
-            ng-click="patron.net_access_level = level">{{level.name()}}</a>
-        </li>
-      </ul>
-    </div>
+    <select 
+      class="form-control" 
+      ng-model="patron.net_access_level"
+      ng-required="field_required('au', 'net_access_level')"
+      ng-blur="handle_field_changed(patron, 'net_access_level')"
+      ng-options="level.name() for level in net_access_levels track by level.id()">
+    </select>
+  </div>
+</div>
+
+<!-- ACTIVE -->
+
+<div class="row reg-field-row" ng-show="show_field('au.active')">
+  [% draw_field_label('au', 'active') %]
+  [% draw_form_input('au', 'active', '', 'checkbox') %]
+</div>
+
+<!-- BARRED -->
+
+<div class="row reg-field-row" ng-show="show_field('au.barred')">
+  [% draw_field_label('au', 'barred') %]
+  [% draw_form_input('au', 'barred', '', 'checkbox') %]
+</div>
+
+<!-- MASTER_ACCOUNT -->
+
+<div class="row reg-field-row" ng-show="show_field('au.master_account')">
+  [% draw_field_label('au', 'master_account') %]
+  [% draw_form_input('au', 'master_account', '', 'checkbox') %]
+</div>
+
+<!-- CLAIMS_RETURNED_COUNT -->
+
+<div class="row reg-field-row" ng-show="show_field('au.claims_returned_count')">
+  [% draw_field_label('au', 'claims_returned_count') %]
+  [% draw_form_input('au', 'claims_returned_count', 
+    '', 'number', '!perms.UPDATE_PATRON_CLAIM_RETURN_COUNT') %]
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'claims_returned_count') %]
+  </div>
+</div>
+
+<!-- CLAIMS_NEVER_CHECKED_OUT_COUNT -->
+
+<div class="row reg-field-row" ng-show="show_field('au.claims_never_checked_out_count')">
+  [% draw_field_label('au', 'claims_never_checked_out_count') %]
+  [% draw_form_input('au', 'claims_never_checked_out_count',
+    '', 'number', '!perms.UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT') %]
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'claims_never_checked_out_count') %]
   </div>
 </div>
 
-[% formfield('au', 'active', '', 'checkbox') %]
-[% formfield('au', 'barred', '', 'checkbox') %]
-[% formfield('au', 'master_account', '', 'checkbox') %]
-[% formfield('au', 'claims_returned_count', '', 'number') %]
-[% formfield('au', 'claims_never_checked_out_count', '', 'number') %]
-[% formfield('au', 'alert_message') %]
+<!-- ALERT_MESSAGE -->
+
+<div class="row reg-field-row" ng-show="show_field('au.alert_message')">
+  [% draw_field_label('au', 'alert_message') %]
+  <div class="col-md-3 reg-field-input">
+    <textarea 
+      class="form-control" 
+      ng-model="patron.alert_message"
+      ng-pattern="field_pattern('au', 'alert_message')"
+      ng-change="field_modified()" 
+      ng-blur="handle_field_changed(patron, 'alert_message')">
+    </textarea>
+  </div>
+  <div class="col-md-6 patron-reg-example">
+    [% draw_example_text('au', 'alert_message') %]
+  </div>
+</div>
 
 <div class="alert alert-success row" role="alert">
   <div class="col-md-6">[% l('User Settings') %]</div>
@@ -431,6 +553,23 @@
   </div>
 </div>
 
+<!-- TODO: Add circ.collections.exempt to master SQL seed data -->
+<div class="row reg-field-row" 
+  ng-if="user_setting_types['circ.collections.exempt']">
+  <div class="col-md-3 reg-field-label">
+    <label>{{user_setting_types['circ.collections.exempt'].label()}}</label>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <div class='checkbox'>
+      <input 
+        type='checkbox' 
+        ng-change="field_modified()" 
+        ng-disabled="!perms.UPDATE_PATRON_COLLECTIONS_EXEMPT"
+        ng-model="user_settings['circ.collections.exempt']"/>
+    </div>
+  </div>
+</div>
+
 <div class="row reg-field-row">
   <div class="col-md-3 reg-field-label">
     <label>[% l('Holds Notices') %]</label>
@@ -533,16 +672,104 @@
       </div>
   </div>
 
-  [% formfield('aua', 'address_type', 'addresses[$index]') %]
-  [% formfield('aua', 'post_code', 'addresses[$index]') %]
-  [% formfield('aua', 'street1', 'addresses[$index]') %]
-  [% formfield('aua', 'street2', 'addresses[$index]') %]
-  [% formfield('aua', 'city', 'addresses[$index]') %]
-  [% formfield('aua', 'county', 'addresses[$index]') %]
-  [% formfield('aua', 'state', 'addresses[$index]') %]
-  [% formfield('aua', 'country', 'addresses[$index]') %]
-  [% formfield('aua', 'valid', 'addresses[$index]', 'checkbox') %]
-  [% formfield('aua', 'within_city_limits', 'addresses[$index]', 'checkbox') %]
+  <!-- ADDRESS_TYPE -->
+  <div class="row reg-field-row" ng-show="show_field('aua.address_type')">
+    [% draw_field_label('aua', 'address_type') %]
+    [% draw_form_input('aua', 'address_type', 'addresses[$index]') %]
+    <div class="col-md-6 patron-reg-example">
+      [% draw_example_text('aua', 'address_type') %]
+    </div>
+  </div>
+
+  <!-- POST_CODE -->
+
+  <div class="row reg-field-row" ng-show="show_field('aua.post_code')">
+    [% draw_field_label('aua', 'post_code') %]
+    [% draw_form_input('aua', 'post_code', 'addresses[$index]') %]
+    <div class="col-md-6 patron-reg-example">
+      [% draw_example_text('aua', 'post_code') %]
+    </div>
+  </div>
+
+  <!-- STREET1 -->
+
+  <div class="row reg-field-row" ng-show="show_field('aua.street1')">
+    [% draw_field_label('aua', 'street1') %]
+    [% draw_form_input('aua', 'street1', 'addresses[$index]') %]
+    <div class="col-md-6 patron-reg-example">
+      [% draw_example_text('aua', 'street1') %]
+    </div>
+  </div>
+
+  <!-- STREET2 -->
+
+  <div class="row reg-field-row" ng-show="show_field('aua.street2')">
+    [% draw_field_label('aua', 'street2') %]
+    [% draw_form_input('aua', 'street2', 'addresses[$index]') %]
+    <div class="col-md-6 patron-reg-example">
+      [% draw_example_text('aua', 'street2') %]
+    </div>
+  </div>
+
+  <!-- CITY -->
+
+  <div class="row reg-field-row" ng-show="show_field('aua.city')">
+    [% draw_field_label('aua', 'city') %]
+    [% draw_form_input('aua', 'city', 'addresses[$index]') %]
+    <div class="col-md-6 patron-reg-example">
+      [% draw_example_text('aua', 'city') %]
+    </div>
+  </div>
+
+  <!-- COUNTY -->
+
+  <div class="row reg-field-row" ng-show="show_field('aua.county')">
+    [% draw_field_label('aua', 'county') %]
+    [% draw_form_input('aua', 'county', 'addresses[$index]') %]
+    <div class="col-md-6 patron-reg-example">
+      [% draw_example_text('aua', 'county') %]
+    </div>
+  </div>
+
+  <!-- STATE -->
+
+  <div class="row reg-field-row" ng-show="show_field('aua.state')">
+    [% draw_field_label('aua', 'state') %]
+    [% draw_form_input('aua', 'state', 'addresses[$index]') %]
+    <div class="col-md-6 patron-reg-example">
+      [% draw_example_text('aua', 'state') %]
+    </div>
+  </div>
+
+  <!-- COUNTRY -->
+
+  <div class="row reg-field-row" ng-show="show_field('aua.country')">
+    [% draw_field_label('aua', 'country') %]
+    [% draw_form_input('aua', 'country', 'addresses[$index]') %]
+    <div class="col-md-6 patron-reg-example">
+      [% draw_example_text('aua', 'country') %]
+    </div>
+  </div>
+
+  <!-- VALID -->
+
+  <div class="row reg-field-row" ng-show="show_field('aua.valid')">
+    [% draw_field_label('aua', 'valid') %]
+    [% draw_form_input('aua', 'valid', 'addresses[$index]', 'checkbox') %]
+    <div class="col-md-6 patron-reg-example">
+      [% draw_example_text('aua', 'valid') %]
+    </div>
+  </div>
+
+  <!-- WITHIN_CITY_LIMITS -->
+
+  <div class="row reg-field-row" ng-show="show_field('aua.within_city_limits')">
+    [% draw_field_label('aua', 'within_city_limits') %]
+    [% draw_form_input('aua', 'within_city_limits', 'addresses[$index]', 'checkbox') %]
+    <div class="col-md-6 patron-reg-example">
+      [% draw_example_text('aua', 'within_city_limits') %]
+    </div>
+  </div>
 
   <div class="row" ng-if="$last">
     <button type="button" ng-click="new_address()" 
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_patron_cards_dialog.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_patron_cards_dialog.tt2
index 4ef6b32..5ba4652 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_patron_cards_dialog.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_patron_cards_dialog.tt2
@@ -19,15 +19,19 @@
       <div class="row" ng-repeat="card in args.cards">
         <div class="col-md-4">{{card.barcode}}</div>
         <div class="col-md-4">
-          <input type='checkbox' ng-model='card.active'/>
+          <input type='checkbox' ng-model='card.active' 
+            ng-disabled="!perms.UPDATE_PATRON_ACTIVE_CARD"/>
         </div>
         <div class="col-md-4">
-          <input type='radio' name='primary' value='on' ng-model='card._primary'/>
+          <input type='radio' name='primary' value='on' 
+            ng-model='card._primary'
+            ng-disabled="!perms.UPDATE_PATRON_PRIMARY_CARD"/>
         </div>
       </div>
     </div>
     <div class="modal-footer">
-      <input type="submit" class="btn btn-primary" value="[% l('Apply Changes') %]"/>
+      <input type="submit" class="btn btn-primary" value="[% l('Apply Changes') %]"
+        ng-disabled="!perms.UPDATE_PATRON_PRIMARY_CARD && !perms.UPDATE_PATRON_ACTIVE_CARD"/>
       <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
     </div>
   </div> <!-- modal-content -->
diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index e381543..c676744 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -151,6 +151,21 @@ but the ones I'm finding aren't quite cutting it..*/
   font-weight: bold;
 }
 
+/* Bootstrap alert panes are too stylized/padded/etc. in this case,
+ * but consider revisiting. */
+.patron-reg-validation-alert {
+  font-weight: bold;
+  color: red;
+}
+
+/* Angular applies these classes based on the field's 
+ * required and pattern settings */
+#patron-reg-container .ng-invalid,
+#patron-reg-container .ng-invalid-required {
+  background-color: yellow;
+  color: red;
+}
+
 /* -- end patron registration -- */
 
 [%# 
diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js b/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
index 1e78455..1749f32 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
@@ -109,6 +109,13 @@ angular.module('egCoreMod')
         return last + ', ' + first + (middle ? ' ' + middle : '');
     }
 
+    service.check_dupe_username = function(usrname) {
+        return egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.username.exists',
+            egCore.auth.token(), usrname);
+    }
+
     //service.check_grp_app_perm = function(grp_id) {
 
     // determine which user groups our user is not allowed to modify
@@ -148,9 +155,31 @@ angular.module('egCoreMod')
         );
     }
 
-    service.has_group_link_perms = function(org_id) {
-        return egCore.perm.hasPermAt('CREATE_USER_GROUP_LINK', true)
-        .then(function(p) { return p.indexOf(org_id) > -1; });
+    // resolves to a hash of perm-name => boolean value indicating
+    // wether the user has the permission at org_id.
+    service.has_perms_for_org = function(org_id) {
+
+        var perms_needed = [
+            'UPDATE_USER',
+            'CREATE_USER',
+            'CREATE_USER_GROUP_LINK', 
+            'UPDATE_PATRON_COLLECTIONS_EXEMPT',
+            'UPDATE_PATRON_CLAIM_RETURN_COUNT',
+            'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
+            'UPDATE_PATRON_ACTIVE_CARD',
+            'UPDATE_PATRON_PRIMARY_CARD'
+        ];
+
+        return egCore.perm.hasPermAt(perms_needed, true)
+        .then(function(perm_map) {
+
+            angular.forEach(perms_needed, function(perm) {
+                perm_map[perm] = 
+                    Boolean(perm_map[perm].indexOf(org_id) > -1);
+            });
+
+            return perm_map;
+        });
     }
 
     service.get_surveys = function() {
@@ -451,15 +480,14 @@ angular.module('egCoreMod')
     service.dupe_patron_search = function(patron, type, value) {
         var search;
 
-        console.log('Dupe search called with "' + 
-            type +"' and value " + value);
+        console.log('Dupe search called with "'+ type +'" and value '+ value);
 
         switch (type) {
 
             case 'name':
                 var fname = patron.first_given_name;   
                 var lname = patron.family_name;   
-                if (!(fname && lname)) return;
+                if (!(fname && lname)) return $q.when({count:0});
                 search = {
                     first_given_name : {value : fname, group : 0},
                     family_name : {value : lname, group : 0}
@@ -574,7 +602,7 @@ angular.module('egCoreMod')
             _is_mailing : true,
             _is_billing : true,
             within_city_limits : false,
-            stat_cat_entries : []
+            country : service.org_settings['ui.patron.default_country'],
         };
 
         var card = {
@@ -590,6 +618,7 @@ angular.module('egCoreMod')
             card : card,
             cards : [card],
             home_ou : egCore.org.get(egCore.auth.user().ws_ou()),
+            stat_cat_entries : [],
             addresses : [addr]
         };
 
@@ -943,11 +972,49 @@ angular.module('egCoreMod')
             'open-ils.actor.patron.settings.update',
             egCore.auth.token(), new_user.id(), settings
         ).then(function(resp) {
-            console.log('settings returned ' + resp);
             return resp;
         });
     }
 
+    // Applies field-specific validation regex's from org settings 
+    // to form fields.  Be careful not remove any pattern data we
+    // are not explicitly over-writing in the provided patterns obj.
+    service.set_field_patterns = function(patterns) {
+        if (service.org_settings['opac.username_regex']) {
+            patterns.au.usrname = 
+                new RegExp(service.org_settings['opac.username_regex']);
+        }
+
+        if (service.org_settings['opac.barcode_regex']) {
+            patterns.ac.barcode = 
+                new RegExp(service.org_settings['opac.barcode_regex']);
+        }
+
+        if (service.org_settings['global.password_regex']) {
+            patterns.au.passwd = 
+                new RegExp(service.org_settings['global.password_regex']);
+        }
+
+        var phone_reg = service.org_settings['ui.patron.edit.phone.regex'];
+        if (phone_reg) {
+            // apply generic phone regex first, replace below as needed.
+            patterns.au.day_phone = new RegExp(phone_reg);
+            patterns.au.evening_phone = new RegExp(phone_reg);
+            patterns.au.other_phone = new RegExp(phone_reg);
+        }
+
+        // the remaining patterns fit a well-known key name pattern
+
+        angular.forEach(service.org_settings, function(val, key) {
+            if (!val) return;
+            var parts = key.match(/ui.patron.edit\.(\w+)\.(\w+)\.regex/);
+            if (!parts) return;
+            var cls = parts[1];
+            var name = parts[2];
+            patterns[cls][name] = new RegExp(val);
+        });
+    }
+
     return service;
 }]);
 
@@ -967,6 +1034,10 @@ function PatronRegCtrl($scope, $routeParams,
     $scope.focus_bc = !Boolean($scope.patron_id);
     $scope.dupe_counts = {};
 
+    // map of perm name to true/false for perms the logged in user
+    // has at the currently selected patron home org unit.
+    $scope.perms = {};
+
     if (!$scope.edit_passthru) {
         // in edit more, scope.edit_passthru is delivered to us by
         // the enclosing controller.  In register mode, there is 
@@ -976,15 +1047,6 @@ function PatronRegCtrl($scope, $routeParams,
 
     // 0=all, 1=suggested, 2=all
     $scope.edit_passthru.vis_level = 0; 
-    // TODO: add save/clone handlers here
-
-    $scope.field_modified = function() {
-        // Call attach with every field change, regardless of whether
-        // it's been called before.  This will allow for re-attach after
-        // the user clicks through the unload warning. egUnloadPrompt
-        // will ensure we only attach once.
-        egUnloadPrompt.attach($scope);
-    }
 
     // Apply default values for new patrons during initial registration
     // prs is shorthand for patronSvc
@@ -1019,13 +1081,21 @@ function PatronRegCtrl($scope, $routeParams,
         }
     }
 
-    function handle_home_org_changed() {
-        org_id = $scope.patron.home_ou.id();
-
-        patronRegSvc.has_group_link_perms(org_id)
-        .then(function(bool) {$scope.has_group_link_perm = bool});
+    // A null or undefined pattern leads to exceptions.  Before the
+    // patterns are loaded from the server, default all patterns
+    // to an innocuous regex.  To avoid re-creating numerous
+    // RegExp objects, cache the stub RegExp after initial creation.
+    // note: angular docs say ng-pattern accepts a regexp or string,
+    // but as of writing, it only works with a regexp object.
+    // (Likely an angular 1.2 vs. 1.4 issue).
+    var field_patterns = {au : {}, ac : {}, aua : {}};
+    $scope.field_pattern = function(cls, field) { 
+        if (!field_patterns[cls][field])
+            field_patterns[cls][field] = new RegExp('.*');
+        return field_patterns[cls][field];
     }
 
+    // Main page load function.  Kicks off tab init and data loading.
     $q.all([
 
         $scope.initTab ? // initTab comes from patron app
@@ -1063,7 +1133,7 @@ function PatronRegCtrl($scope, $routeParams,
         });
 
         extract_hold_notify();
-        handle_home_org_changed();
+        $scope.handle_home_org_changed();
 
         if ($scope.org_settings['ui.patron.edit.default_suggested'])
             $scope.edit_passthru.vis_level = 1;
@@ -1073,6 +1143,7 @@ function PatronRegCtrl($scope, $routeParams,
 
         $scope.page_data_loaded = true;
 
+        prs.set_field_patterns(field_patterns);
     });
 
     // update the currently displayed field documentation
@@ -1095,26 +1166,28 @@ function PatronRegCtrl($scope, $routeParams,
     };
 
     // field visibility cache.  Some fields are universally required.
+    // 3 == value universally required
+    // 2 == field is visible by default
+    // 1 == field is suggested by default
     var field_visibility = {
-        'ac.barcode' : 2,
-        'au.usrname' : 2,
-        'au.passwd' :  2,
-        // TODO passwd2 2,
-        'au.first_given_name' : 2,
-        'au.family_name' : 2,
-        'au.ident_type' : 2,
-        'au.home_ou' : 2,
-        'au.profile' : 2,
-        'au.expire_date' : 2,
-        'au.net_access_level' : 2,
-        'aua.address_type' : 2,
-        'aua.post_code' : 2,
-        'aua.street1' : 2,
+        'ac.barcode' : 3,
+        'au.usrname' : 3,
+        'au.passwd' :  3,
+        'au.first_given_name' : 3,
+        'au.family_name' : 3,
+        'au.ident_type' : 3,
+        'au.home_ou' : 3,
+        'au.profile' : 3,
+        'au.expire_date' : 3,
+        'au.net_access_level' : 3,
+        'aua.address_type' : 3,
+        'aua.post_code' : 3,
+        'aua.street1' : 3,
         'aua.street2' : 2,
-        'aua.city' : 2,
+        'aua.city' : 3,
         'aua.county' : 2,
         'aua.state' : 2,
-        'aua.country' : 2,
+        'aua.country' : 3,
         'aua.valid' : 2,
         'aua.within_city_limits' : 2,
         'stat_cats' : 1,
@@ -1136,7 +1209,7 @@ function PatronRegCtrl($scope, $routeParams,
             var sug_set = 'ui.patron.edit.' + field_key + '.suggest';
 
             if ($scope.org_settings[req_set]) {
-                field_visibility[field_key] = 2;
+                field_visibility[field_key] = 3;
             } else if ($scope.org_settings[sho_set]) {
                 field_visibility[field_key] = 2;
             } else if ($scope.org_settings[sug_set]) {
@@ -1149,6 +1222,18 @@ function PatronRegCtrl($scope, $routeParams,
         return field_visibility[field_key] >= $scope.edit_passthru.vis_level;
     }
 
+    // See $scope.show_field().
+    // A field with visbility level 3 means it's required.
+    $scope.field_required = function(cls, field) {
+
+        // Value in the password field is not required
+        // for existing patrons.
+        if (field == 'passwd' && $scope.patron && !$scope.patron.isnew) 
+          return false;
+
+        return (field_visibility[cls + '.' + field] == 3);
+    }
+
     // generates a random 4-digit password
     $scope.generate_password = function() {
         $scope.patron.passwd = Math.floor(Math.random()*9000) + 1000;
@@ -1170,6 +1255,14 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.field_modified();
     }
 
+    $scope.invalid_profile = function() {
+        return !(
+            $scope.patron && 
+            $scope.patron.profile && 
+            $scope.patron.profile.usergroup() == 't'
+        );
+    }
+
     $scope.new_address = function() {
         var addr = egCore.idl.toHash(new egCore.idl.aua());
         patronRegSvc.ingest_address($scope.patron, addr);
@@ -1177,6 +1270,7 @@ function PatronRegCtrl($scope, $routeParams,
         addr.isnew = true;
         addr.valid = true;
         addr.within_city_limits = true;
+        addr.country = $scope.org_settings['ui.patron.default_country'];
         $scope.patron.addresses.push(addr);
     }
 
@@ -1234,12 +1328,14 @@ function PatronRegCtrl($scope, $routeParams,
 
     $scope.barcode_changed = function(bc) {
         if (!bc) return;
+        $scope.dupe_barcode = false;
         egCore.net.request(
             'open-ils.actor',
             'open-ils.actor.barcode.exists',
             egCore.auth.token(), bc
         ).then(function(resp) {
             if (resp == '1') {
+                $scope.dupe_barcode = true;
                 console.log('duplicate barcode detected: ' + bc);
                 // DUPLICATE CARD
             } else {
@@ -1254,10 +1350,11 @@ function PatronRegCtrl($scope, $routeParams,
         $modal.open({
             templateUrl: './circ/patron/t_patron_cards_dialog',
             controller: 
-                   ['$scope','$modalInstance','cards',
-            function($scope , $modalInstance , cards) {
+                   ['$scope','$modalInstance','cards', 'perms',
+            function($scope , $modalInstance , cards, perms) {
                 // scope here is the modal-level scope
                 $scope.args = {cards : cards};
+                $scope.perms = perms;
                 $scope.ok = function() { $modalInstance.close($scope.args) }
                 $scope.cancel = function () { $modalInstance.dismiss() }
             }],
@@ -1265,6 +1362,9 @@ function PatronRegCtrl($scope, $routeParams,
                 cards : function() {
                     // scope here is the controller-level scope
                     return $scope.patron.cards;
+                },
+                perms : function() {
+                    return $scope.perms;
                 }
             }
         }).result.then(
@@ -1393,16 +1493,148 @@ function PatronRegCtrl($scope, $routeParams,
         patronRegSvc.invalidate_field($scope.patron, field);
     }
 
+
     $scope.dupe_value_changed = function(type, value) {
         $scope.dupe_counts[type] = 0;
         patronRegSvc.dupe_patron_search($scope.patron, type, value)
         .then(function(res) {
             $scope.dupe_counts[type] = res.count;
-            $scope.dupe_search_encoded = 
-                encodeURIComponent(js2JSON(res.search));
+            if (res.count) {
+                $scope.dupe_search_encoded = 
+                    encodeURIComponent(js2JSON(res.search));
+            } else {
+                $scope.dupe_search_encoded = '';
+            }
+        });
+    }
+
+    $scope.handle_home_org_changed = function() {
+        org_id = $scope.patron.home_ou.id();
+        patronRegSvc.has_perms_for_org(org_id).then(function(map) {
+            angular.forEach(map, function(v, k) { $scope.perms[k] = v });
         });
     }
 
+    // This is called with every character typed in a form field,
+    // since that's the only way to gaurantee something has changed.
+    // See handle_field_changed for ng-change vs. ng-blur.
+    $scope.field_modified = function() {
+        // Call attach with every field change, regardless of whether
+        // it's been called before.  This will allow for re-attach after
+        // the user clicks through the unload warning. egUnloadPrompt
+        // will ensure we only attach once.
+        egUnloadPrompt.attach($scope);
+    }
+
+    // obj could be the patron, an address, etc.
+    // This is called any time a form field achieves then loses focus.
+    // It does not necessarily mean the field has changed.
+    // The alternative is ng-change, but it's called with each character
+    // typed, which would be overkill for many of the actions called here.
+    $scope.handle_field_changed = function(obj, field_name) {
+        var cls = obj.classname; // set by egIdl
+        var value = obj[field_name];
+
+        console.log('changing field ' + field_name + ' to ' + value);
+
+        switch (field_name) {
+            case 'day_phone' : 
+                if ($scope.patron.day_phone && 
+                    $scope.patron.isnew && 
+                    $scope.org_settings['patron.password.use_phone']) {
+                    $scope.patron.passwd = phone.substr(-4);
+                }
+            case 'evening_phone' : 
+            case 'other_phone' : 
+                $scope.dupe_value_changed('phone', value);
+                break;
+
+            case 'ident_value':
+            case 'ident_value2':
+                $scope.dupe_value_changed('ident', value);
+                break;
+
+            case 'first_given_name':
+            case 'family_name':
+                $scope.dupe_value_changed('name', value);
+                break;
+
+            case 'email':
+                $scope.dupe_value_changed('email', value);
+                break;
+
+            case 'street1':
+            case 'street2':
+            case 'city':
+                // dupe search on address wants the address object as the value.
+                $scope.dupe_value_changed('address', obj);
+                break;
+
+            case 'post_code':
+                $scope.post_code_changed(obj);
+                break;
+
+            case 'usrname':
+                patronRegSvc.check_dupe_username(value)
+                .then(function(yes) {$scope.dupe_username = Boolean(yes)});
+                break;
+
+            case 'barcode':
+                // TODO: finish barcode_changed handler.
+                $scope.barcode_changed(value);
+                break;
+
+            case 'dob':
+                maintain_juvenile_flag();
+                break;
+        }
+    }
+
+    // patron.juvenile is set to true if the user was born after
+    function maintain_juvenile_flag() {
+        if ( !($scope.patron && $scope.patron.dob) ) return;
+
+        var juv_interval = 
+            $scope.org_settings['global.juvenile_age_threshold'] 
+            || '18 years';
+
+        var base = new Date();
+
+        base.setTime(base.getTime() - 
+            Number(egCore.date.intervalToSeconds(juv_interval) + '000'));
+
+        $scope.patron.juvenile = ($scope.patron.dob > base);
+    }
+
+    // returns true (disable) for orgs that cannot have users.
+    $scope.disable_home_org = function(org_id) {
+        if (!org_id) return;
+        var org = egCore.org.get(org_id);
+        return (
+            org &&
+            org.ou_type() &&
+            org.ou_type().can_have_users() == 'f'
+        );
+    }
+
+    // Returns true if any input elements are tagged as invalid
+    $scope.edit_passthru.has_invalid_fields = function() {
+        return $('#patron-reg-container .ng-invalid').length > 0;
+    }
+
+    // Returns true if the Save and Save & Clone buttons should be disabled.
+    $scope.edit_passthru.hide_save_actions = function() {
+        var can_save = $scope.patron.isnew ?
+            $scope.perms.CREATE_USER : $scope.perms.UPDATE_USER;
+
+        return (
+            !can_save ||
+            $scope.dupe_username ||
+            $scope.dupe_barcode ||
+            $scope.edit_passthru.has_invalid_fields()
+        );
+    }
+
     $scope.edit_passthru.save = function(save_args) {
         if (!save_args) save_args = {};
 

-----------------------------------------------------------------------

Summary of changes:
 .../src/perlmods/lib/OpenILS/Application/Actor.pm  |    4 +
 .../templates/staff/circ/patron/reg_actions.tt2    |    2 +
 .../src/templates/staff/circ/patron/t_edit.tt2     |  673 +++++++++++++-------
 .../staff/circ/patron/t_patron_cards_dialog.tt2    |   10 +-
 Open-ILS/src/templates/staff/css/circ.css.tt2      |   15 +
 .../web/js/ui/default/staff/circ/patron/regctl.js  |  373 +++++++++--
 6 files changed, 796 insertions(+), 281 deletions(-)


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list