[open-ils-commits] [GIT] Evergreen ILS branch master updated. cb6050ed4bc723e571fc6013b48b5b3e7ce0011f

Evergreen Git git at git.evergreen-ils.org
Mon Feb 24 11:33:24 EST 2020


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, master has been updated
       via  cb6050ed4bc723e571fc6013b48b5b3e7ce0011f (commit)
       via  7e9229353092302402c1ca0cbc2efcd5f8a54d90 (commit)
      from  ed4b678f8d393674f4b82eee1b279dbba556a9b5 (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 cb6050ed4bc723e571fc6013b48b5b3e7ce0011f
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Aug 7 20:08:25 2019 -0400

    LP#1570072: add release notes
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Terran McCanna <tmccanna at georgialibraries.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/update-hold-notifications.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/update-hold-notifications.adoc
new file mode 100644
index 0000000000..c30ab1d73f
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Circulation/update-hold-notifications.adoc
@@ -0,0 +1,7 @@
+Update Hold Notification Information
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The public catalog and staff client now have the ability to update existing
+holds if a patron or a staff member changes certain notification preferences or
+contact information.  Evergreen will detect these changes and prompt the staff
+user or patron user and ask if they want to update existing holds with the new
+contact information and/or notification preferences.

commit 7e9229353092302402c1ca0cbc2efcd5f8a54d90
Author: Cesar Velez <cesar.velez at equinoxinitiative.org>
Date:   Tue Jan 22 12:16:54 2019 -0500

    LP#1570072: update hold notification methods upon preference changes
    
    This patch adds a feature where if a patron's hold notification
    preferences are changed, they are given an opportunity to have
    notfication methods for their pending hold requests updated to
    reflect their new prefernces.
    
    This feature affects both the public catalog My Account interface
    and the staff patron registration form. In both cases, the user
    is presented with a modal (staff-side) or interstitial page (public
    catalog) asking them whether to update current hold requests.
    
    Sponsored-by: MassLNC
    
    Additional-work-by: Mike Rylander <mrylander at gmail.com>
    Additional-work-by: Galen Charlton <gmc at equinoxinitiative.org>
    
    Signed-off-by: Cesar Velez <cesar.velez at equinoxinitiative.org>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Terran McCanna <tmccanna at georgialibraries.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
index e19915e75c..11c29f8914 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
@@ -4549,6 +4549,282 @@ sub copy_has_holds_count {
 }
 
 __PACKAGE__->register_method(
+    method    => "retrieve_holds_by_usr_notify_value_staff",
+    api_name  => "open-ils.circ.holds.retrieve_by_notify_staff",
+    signature => {
+        desc   => "Retrieve the hold, for the specified user using the notify value.  $ses_is_req_note",
+        params => [
+            { desc => 'Authentication token', type => 'string' },
+            { desc => 'User ID',              type => 'number' },
+            { desc => 'notify value',         type => 'string' },
+            { desc => 'notify_type',          type => 'string' }
+        ],
+        return => {
+            desc => 'Hold objects with transits attached, event on error',
+        }
+    }
+);
+
+sub retrieve_holds_by_usr_notify_value_staff {
+    
+    my($self, $conn, $auth, $usr_id, $contact, $cType) = @_;
+
+    my $e = new_editor(authtoken=>$auth);
+    $e->checkauth or return $e->event;
+
+    if ($e->requestor->id != $usr_id){
+        $e->allowed('VIEW_HOLD') or return $e->event;
+    }
+
+    my $q = {
+        "select" => { "ahr" => ["id", "sms_notify", "phone_notify", "email_notify", "sms_carrier"]},
+        "from" => "ahr",
+        "where" => {
+            "usr"          =>      $usr_id,
+            "capture_time" =>      undef,
+            "cancel_time"  =>      undef,
+            "fulfillment_time" =>  undef,
+        }
+    };
+
+    if ($cType eq "day_phone" or $cType eq "evening_phone" or
+        $cType eq "other_phone" or $cType eq "default_phone"){
+            $q->{where}->{"-not"} = [
+                { "phone_notify" => { "=" => $contact} },
+                { "phone_notify" => { "<>" => undef } }
+            ];
+    }
+
+
+    if ($cType eq "default_sms") {
+        $q->{where}->{"-not"} = [
+            { "sms_notify" => { "=" => $contact} },
+            { "sms_notify" => { "<>" => undef } }
+        ];
+    }
+
+    if ($cType eq "default_sms_carrier_id") {
+        $q->{where}->{"-not"} = [
+            { "sms_carrier" => { "=" => int($contact)} },
+            { "sms_carrier" => { "<>" => undef } }
+        ];
+    }
+
+    if ($cType =~ /notify/){
+        # this is was notification pref change
+        # we find all unfulfilled holds that match have that pref
+        my $optr = $contact == 1 ? "<>" : "="; # unless it's email, true val means we want to query for not null
+        my $conj = $optr eq '=' ? '-or' : '-and';
+        if ($cType =~ /sms/) {
+            $q->{where}->{$conj} = [ { sms_notify => { $optr => undef } }, { sms_notify => { $optr => '' } } ];
+        }
+        if ($cType =~ /phone/) {
+            $q->{where}->{$conj} = [ { phone_notify => { $optr => undef } }, { phone_notify => { $optr => '' } } ];
+        }
+        if ($cType =~ /email/) {
+            if ($contact) {
+                $q->{where}->{'+ahr'} = 'email_notify';
+            } else {
+                $q->{where}->{'-not'} = {'+ahr' => 'email_notify'};
+            }
+        }
+    }
+
+    my $holds = $e->json_query($q);
+    #$hold_ids = [ map { $_->{id} } @$hold_ids ];
+
+    return $holds;
+}
+
+__PACKAGE__->register_method(
+    method    => "batch_update_holds_by_value_staff",
+    api_name  => "open-ils.circ.holds.batch_update_holds_by_notify_staff",
+    signature => {
+        desc   => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
+        params => [
+            { desc => 'Authentication token', type => 'string' },
+            { desc => 'User ID',              type => 'number' },
+            { desc => 'Hold IDs',             type => 'array'  },
+            { desc => 'old notify value',     type => 'string' },
+            { desc => 'new notify value',     type => 'string' },
+            { desc => 'field name',           type => 'string' },
+            { desc => 'SMS carrier ID',       type => 'number' }
+
+        ],
+        return => {
+            desc => 'Hold objects with transits attached, event on error',
+        }
+    }
+);
+
+sub batch_update_holds_by_value_staff {
+    my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType, $carrierId) = @_;
+
+    my $e = new_editor(authtoken=>$auth, xact=>1);
+    $e->checkauth or return $e->event;
+    if ($e->requestor->id != $usr_id){
+        $e->allowed('UPDATE_HOLD') or return $e->event;
+    }
+
+    my @success;
+    for my $id (@$hold_ids) {
+        
+        my $hold = $e->retrieve_action_hold_request($id);
+
+        if ($cType eq "day_phone" or $cType eq "evening_phone" or
+            $cType eq "other_phone" or $cType eq "default_phone") {
+
+            if ($newval eq '') {
+                $hold->clear_phone_notify();
+            }
+            else {
+                $hold->phone_notify($newval);
+            }
+        }
+        
+        if ($cType eq "default_sms"){
+            if ($newval eq '') {
+                $hold->clear_sms_notify();
+                $hold->clear_sms_carrier(); # TODO: prevent orphan sms_carrier, via db trigger
+            }
+            else {
+                $hold->sms_notify($newval);
+                $hold->sms_carrier($carrierId);
+            }
+
+        }
+
+        if ($cType eq "default_sms_carrier_id") {
+            $hold->sms_carrier($newval);
+        }
+
+        if ($cType =~ /notify/){
+            # this is a notification pref change
+            if ($cType =~ /email/) { $hold->email_notify($newval); }
+            if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
+            if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
+            # the other case, where x_notify is changed to true,
+            # is covered by an actual value being assigned
+        }
+
+        $e->update_action_hold_request($hold) or return $e->die_event;
+        push @success, $id;
+    }
+
+    #$e->disconnect;
+    $e->commit; #unless $U->event_code($res);
+    return \@success;
+
+}
+
+
+__PACKAGE__->register_method(
+    method    => "retrieve_holds_by_usr_with_notify",
+    api_name  => "open-ils.circ.holds.retrieve.by_usr.with_notify",
+    signature => {
+        desc   => "Retrieve the hold, for the specified user using the notify value.  $ses_is_req_note",
+        params => [
+            { desc => 'Authentication token', type => 'string' },
+            { desc => 'User ID',              type => 'number' },
+        ],
+        return => {
+            desc => 'Lists of holds with notification values, event on error',
+        }
+    }
+);
+
+sub retrieve_holds_by_usr_with_notify {
+    
+    my($self, $conn, $auth, $usr_id) = @_;
+
+    my $e = new_editor(authtoken=>$auth);
+    $e->checkauth or return $e->event;
+
+    if ($e->requestor->id != $usr_id){
+        $e->allowed('VIEW_HOLD') or return $e->event;
+    }
+
+    my $q = {
+        "select" => { "ahr" => ["id", "phone_notify", "email_notify", "sms_carrier", "sms_notify"]},
+        "from" => "ahr",
+        "where" => {
+            "usr"          =>      $usr_id,
+            "capture_time" =>      undef,
+            "cancel_time"  =>      undef,
+            "fulfillment_time" =>  undef,
+        }
+    };
+
+    my $holds = $e->json_query($q);
+    return $holds;
+}
+
+__PACKAGE__->register_method(
+    method    => "batch_update_holds_by_value",
+    api_name  => "open-ils.circ.holds.batch_update_holds_by_notify",
+    signature => {
+        desc   => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
+        params => [
+            { desc => 'Authentication token', type => 'string' },
+            { desc => 'User ID',              type => 'number' },
+            { desc => 'Hold IDs',             type => 'array'  },
+            { desc => 'old notify value',     type => 'string' },
+            { desc => 'new notify value',     type => 'string' },
+            { desc => 'notify_type',          type => 'string' }
+        ],
+        return => {
+            desc => 'Hold objects with transits attached, event on error',
+        }
+    }
+);
+
+sub batch_update_holds_by_value {
+    my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType) = @_;
+
+    my $e = new_editor(authtoken=>$auth, xact=>1);
+    $e->checkauth or return $e->event;
+    if ($e->requestor->id != $usr_id){
+        $e->allowed('UPDATE_HOLD') or return $e->event;
+    }
+
+    my @success;
+    for my $id (@$hold_ids) {
+        
+        my $hold = $e->retrieve_action_hold_request(int($id));
+
+        if ($cType eq "day_phone" or $cType eq "evening_phone" or
+            $cType eq "other_phone" or $cType eq "default_phone") {
+            # change phone number value on hold
+            $hold->phone_notify($newval);
+        }
+        if ($cType eq "default_sms") {
+            # change SMS number value on hold
+            $hold->sms_notify($newval);
+        }
+
+        if ($cType eq "default_sms_carrier_id") {
+            $hold->sms_carrier(int($newval));
+        }
+
+        if ($cType =~ /notify/){
+            # this is a notification pref change
+            if ($cType =~ /email/) { $hold->email_notify($newval); }
+            if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
+            if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
+            # the other case, where x_notify is changed to true,
+            # is covered by an actual value being assigned
+        }
+
+        $e->update_action_hold_request($hold) or return $e->die_event;
+        push @success, $id;
+    }
+
+    #$e->disconnect;
+    $e->commit; #unless $U->event_code($res);
+    return \@success;
+}
+
+__PACKAGE__->register_method(
     method        => "hold_metadata",
     api_name      => "open-ils.circ.hold.get_metadata",
     authoritative => 1,
@@ -4573,6 +4849,7 @@ __PACKAGE__->register_method(
     }
 );
 
+
 sub hold_metadata {
     my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
index 105c3bd1af..7388d79706 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
@@ -225,6 +225,7 @@ sub load {
     return $self->load_myopac_circ_history_export if $path =~ m|opac/myopac/circ_history/export|;
     return $self->load_myopac_circ_history if $path =~ m|opac/myopac/circ_history|;
     return $self->load_myopac_hold_history if $path =~ m|opac/myopac/hold_history|;
+    return $self->load_myopac_prefs_notify_changed_holds if $path =~ m|opac/myopac/prefs_notify_changed_holds|;
     return $self->load_myopac_prefs_notify if $path =~ m|opac/myopac/prefs_notify|;
     return $self->load_myopac_prefs_settings if $path =~ m|opac/myopac/prefs_settings|;
     return $self->load_myopac_prefs_my_lists if $path =~ m|opac/myopac/prefs_my_lists|;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
index 5bc2f8b98c..e246bd591c 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
@@ -235,9 +235,272 @@ sub load_myopac_prefs_notify {
 
     # re-fetch user prefs
     $self->ctx->{updated_user_settings} = \%settings;
+
+    # update holds: check if any changes affect any holds
+    my @llchgs = $self->_parse_prefs_notify_hold_related();
+    my @ffectedChgs;
+
+    if ( $self->cgi->param('hasHoldsChanges') ) {
+        # propagate pref_notify changes to holds
+        for my $chset (@llchgs){
+            # FIXME is this still needed?
+        }
+    
+    }
+    else {
+        my $holds = $U->simplereq('open-ils.circ', 'open-ils.circ.holds.retrieve.by_usr.with_notify',
+            $e->authtoken, $e->requestor->id);
+
+        if (@$holds > 0) {
+
+            my $default_phone_changes = {};
+            my $sms_changes           = {};
+            my $new_phone;
+            my $new_carrier;
+            my $new_sms;
+            for my $chset (@llchgs) {
+                next if scalar(@$chset) < 3;
+                my ($old, $new, $field) = @$chset;
+
+                my $bool = $field =~ /_notify/ ? 1 : 0;
+
+                # find holds that would change
+                my $affected = [];
+                foreach my $hold (@$holds) {
+                    if ($field eq 'email_notify') {
+                        my $curr = $hold->{$field} eq 't' ? 'true' : 'false';
+                        push @$affected, $hold if $curr ne $new;
+                    } elsif ($field eq 'default_phone') {
+                        my $old_phone = $hold->{phone_notify} // '';
+                        $new_phone = $new // '';
+                        push @{ $default_phone_changes->{ $old_phone } }, $hold->{id}
+                            if $old_phone ne $new_phone;
+                    } elsif ($field eq 'phone_notify') {
+                        my $curr = ($hold->{$field} // '' ne '') ? 'true' : 'false';
+                        push @$affected, $hold if $curr ne $new;
+                    } elsif ($field eq 'sms_notify') {
+                        my $curr = ($hold->{$field} // '' ne '') ? 'true' : 'false';
+                        push @$affected, $hold if $curr ne $new;
+                    } elsif ($field eq 'sms_info') {
+                        my $old_carrier = $hold->{'sms_carrier'} // '';
+                        my $old_sms = $hold->{'sms_notify'} // '';
+                        $new_carrier = $new->{carrier} // '';
+                        $new_sms = $new->{sms} // '';
+                        if (!($old_carrier eq $new_carrier && $old_sms eq $new_sms)) {
+                            push @{ $sms_changes->{ join("\t", $old_carrier, $old_sms) } }, $hold->{id};
+                        }
+                    }
+                }
+
+                # append affected array to chset
+                if (scalar(@$affected) > 0){
+                    push(@$chset, [ map { $_->{id} } @$affected ]);
+                    push(@ffectedChgs, $chset);
+                }
+            }
+
+
+            foreach my $old_phone (keys %$default_phone_changes) {
+                push(@ffectedChgs, [ $old_phone, $new_phone, 'default_phone', $default_phone_changes->{$old_phone} ]);
+            }
+            foreach my $old_sms_info (keys %$sms_changes) {
+                my ($old_carrier, $old_sms) = split /\t/, $old_sms_info;
+                push(@ffectedChgs, [
+                                        { carrier => $old_carrier, sms => $old_sms },
+                                        { carrier => $new_carrier, sms => $new_sms },
+                                        'sms_info',
+                                        $sms_changes->{$old_sms_info}
+                                   ]);
+            }
+
+            if ( scalar(@ffectedChgs) ){
+                $self->ctx->{affectedChgs} = \@ffectedChgs;
+            }
+        }
+    }
+
     return $self->_load_user_with_prefs || Apache2::Const::OK;
 }
 
+sub _parse_prefs_notify_hold_related {
+
+    my $self = shift;
+    my $for_update = shift;
+
+    # create an array of change arrays
+    my @chgs;
+
+    my @phone_notify = $self->cgi->multi_param('phone_notify[]');
+    push(@chgs, \@phone_notify) if scalar(@phone_notify);
+
+    my $turning_on_phone_notify  = !$for_update &&
+                                   scalar(@phone_notify) &&
+                                   $phone_notify[1] eq 'true';
+    my $turning_off_phone_notify = !$for_update &&
+                                   scalar(@phone_notify) &&
+                                   $phone_notify[1] eq 'false';
+
+    my $changing_default_phone = 0;
+    if (!$turning_off_phone_notify) {
+        my @default_phone = $self->cgi->multi_param('default_phone[]');
+        if ($for_update) {
+            while (scalar(@default_phone) > 0) {
+                my $chg = [ splice(@default_phone, 0, 4) ];
+                if (scalar(@default_phone) > 0 && $default_phone[0] eq 'on') {
+                    push @$chg, shift(@default_phone);
+                    push(@chgs, $chg);
+                    $changing_default_phone = 1;
+                }
+            }
+        } else {
+            if (scalar(@default_phone)) {
+                push @chgs, \@default_phone;
+                $changing_default_phone = 1;
+            }
+        }
+    }
+
+    if ($turning_on_phone_notify && $changing_default_phone) {
+        # we don't need to have both the phone_notify and default_phone
+        # changes; the latter will suffice
+        @chgs = grep { $_->[2] ne 'phone_notify' } @chgs;
+    } elsif ($turning_on_phone_notify && !$changing_default_phone) {
+        # replace the phone_notify change with a default_phone change
+        @chgs = grep { $_->[2] ne 'phone_notify' } @chgs;
+        my $default_phone = $self->cgi->param('opac.default_phone'); # we assume this is set
+        push @chgs, [ '', $default_phone, 'default_phone' ];
+    }
+
+    # on to SMS
+    # ... since both carrier and number are needed to send an SMS notifcation,
+    # we need to treat the pair as a unit
+    my @sms_notify = $self->cgi->multi_param('sms_notify[]');
+    push(@chgs, \@sms_notify) if scalar(@sms_notify);
+
+    my $turning_on_sms_notify  = !$for_update &&
+                                   scalar(@sms_notify) &&
+                                   $sms_notify[1] eq 'true';
+    my $turning_off_sms_notify = !$for_update &&
+                                   scalar(@sms_notify) &&
+                                   $sms_notify[1] eq 'false';
+
+    my $changing_sms_info = 0;
+    if (!$turning_off_sms_notify) {
+        my @sms_carrier = $self->cgi->multi_param('default_sms_carrier_id[]');
+        my @sms = $self->cgi->multi_param('default_sms[]');
+
+        if (scalar(@sms) || scalar(@sms_carrier)) {
+            my $new_carrier = scalar(@sms_carrier) ? $sms_carrier[1] : $self->cgi->param('sms_carrier');
+            my $new_sms = scalar(@sms) ? $sms[1] : $self->cgi->param('opac.default_sms_notify');
+            push @chgs, [
+                            { carrier => '', sms => '' },
+                            { carrier => $new_carrier, sms => $new_sms },
+                            'sms_info'
+                        ];
+           $changing_sms_info = 1;
+        }
+    }
+
+    my @sms_info = $self->cgi->multi_param('sms_info[]'); # only sent by confirmation page
+    if (scalar(@sms_info)) {
+        while (scalar(@sms_info) > 0) {
+            my $chg = [ splice(@sms_info, 0, 4) ];
+            if (scalar(@sms_info) > 0 && $sms_info[0] eq 'on') {
+                push @$chg, shift(@sms_info);
+                my ($carrier, $sms) = split /,/, $chg->[0], -1;
+                $chg->[0] = { carrier => $carrier, sms => $sms };
+                ($carrier, $sms) = split /,/, $chg->[1], -1;
+                $chg->[1] = { carrier => $carrier, sms => $sms };
+                push(@chgs, $chg);
+                $changing_sms_info = 1;
+            }
+        }
+    }
+
+    if ($turning_on_sms_notify && $changing_sms_info) {
+        # we don't need to have both the sms_notify and sms_info
+        # changes; the latter will suffice
+        @chgs = grep { $_->[2] ne 'sms_notify' } @chgs;
+    } elsif ($turning_on_sms_notify && !$changing_sms_info) {
+        # replace the sms_notify change with a sms_info change
+        @chgs = grep { $_->[2] ne 'sms_notify' } @chgs;
+        my $sms_info = {
+            carrier => $self->cgi->param('sms_carrier'),
+            sms     => $self->cgi->param('opac.default_sms_notify'),
+        };
+        push @chgs, [ { carrier => '', sms => ''}, $sms_info, 'sms_info' ];
+    }
+
+    my @email_notify = $self->cgi->multi_param('email_notify[]');
+    push(@chgs, \@email_notify) if scalar(@email_notify);
+
+    if ($for_update) {
+        # if we're updating, keep only the ones that have been
+        # explicitly checked by the user
+        @chgs = grep { scalar(@$_) == 5 && $_->[4] eq 'on' } @chgs;
+    }
+    return @chgs;
+}
+
+sub load_myopac_prefs_notify_changed_holds {
+    my $self = shift;
+    my $e = $self->editor;
+
+    my $hasChanges = $self->cgi->param('hasHoldsChanges');
+    
+    return $self->_load_user_with_prefs || Apache2::Const::OK unless $hasChanges;
+
+    my @ll = $self->_parse_prefs_notify_hold_related(1);
+
+    my @updates;
+    for my $chset (@ll){
+        my ($old, $new, $type, $holdids, $doit) = @$chset;
+        next if $doit ne 'on';
+        
+        # parse string list into array list
+        my @holdids = split(',', $holdids);
+        
+        if ($type =~ /_notify/){
+            # translate true/false string into 1/0
+            $old = $old eq 'true' ? 1 : 0;
+            $new = $new eq 'true' ? 1 : 0;
+        }
+
+        my $update;
+        if ($type eq 'sms_info') {
+            if ($new->{carrier} eq '' && $new->{sms} eq '') {
+                # clear SMS number first to avoid check contrainst issue
+                $update = $U->simplereq('open-ils.circ', "open-ils.circ.holds.batch_update_holds_by_notify",
+                    $e->authtoken, $e->requestor->id, [@holdids], $old->{sms}, $new->{sms}, 'default_sms');
+                push (@updates, $update) if (scalar(@$update) > 0);
+                $update = $U->simplereq('open-ils.circ', "open-ils.circ.holds.batch_update_holds_by_notify",
+                    $e->authtoken, $e->requestor->id, [@holdids], $old->{carrier}, $new->{carrier}, 'default_sms_carrier_id');
+                push (@updates, $update) if (scalar(@$update) > 0);
+            } else {
+                $update = $U->simplereq('open-ils.circ', "open-ils.circ.holds.batch_update_holds_by_notify",
+                    $e->authtoken, $e->requestor->id, [@holdids], $old->{carrier}, $new->{carrier}, 'default_sms_carrier_id');
+                push (@updates, $update) if (scalar(@$update) > 0);
+                $update = $U->simplereq('open-ils.circ', "open-ils.circ.holds.batch_update_holds_by_notify",
+                    $e->authtoken, $e->requestor->id, [@holdids], $old->{sms}, $new->{sms}, 'default_sms');
+                push (@updates, $update) if (scalar(@$update) > 0);
+            }
+        } else {
+            $update = $U->simplereq('open-ils.circ', "open-ils.circ.holds.batch_update_holds_by_notify",
+                $e->authtoken, $e->requestor->id, [@holdids], $old, $new, $type);
+
+            # append affected array to chset
+            if (scalar(@$update) > 0){
+                push(@updates, $update);
+            }
+        }
+    }
+
+    $self->ctx->{'updated'} = \@updates;
+
+    return $self->_load_user_with_prefs || Apache2::Const::OK;
+
+}
+
 sub fetch_optin_prefs {
     my $self = shift;
     my $e = $self->editor;
@@ -959,6 +1222,10 @@ sub handle_hold_update {
             my $val = {"id" => $_};
             $val->{"frozen"} = $self->cgi->param("frozen");
             $val->{"pickup_lib"} = $self->cgi->param("pickup_lib");
+            $val->{"email_notify"} = $self->cgi->param("email_notify") ? 1 : 0;
+            $val->{"phone_notify"} = $self->cgi->param("phone_notify");
+            $val->{"sms_notify"} = $self->cgi->param("sms_notify");
+            $val->{"sms_carrier"} = int($self->cgi->param("sms_carrier")) if $val->{"sms_notify"};
 
             for my $field (qw/expire_time thaw_date/) {
                 # XXX TODO make this support other date formats, not just
diff --git a/Open-ILS/src/templates/opac/myopac/holds.tt2 b/Open-ILS/src/templates/opac/myopac/holds.tt2
index d881d9f3a3..9aa0b2a658 100644
--- a/Open-ILS/src/templates/opac/myopac/holds.tt2
+++ b/Open-ILS/src/templates/opac/myopac/holds.tt2
@@ -1,6 +1,7 @@
 [%  PROCESS "opac/parts/header.tt2";
     PROCESS "opac/parts/misc_util.tt2";
     PROCESS "opac/parts/hold_status.tt2";
+    PROCESS "opac/parts/hold_notify.tt2";
     PROCESS "opac/parts/myopac/column_sort_support.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
     myopac_page = "holds";
@@ -119,6 +120,7 @@
                 <th>[% l('Pickup Location') %]</th>
                 <th>[% l('Cancel if not filled by') %]</th>
                 <th>[% l('Status') %]</th>
+                <th>[% l('Notify Method') %]</th>
                 <th>[% l('Notes') %]</th>
             </tr>
             </thead>
@@ -240,6 +242,11 @@
                             [% PROCESS get_hold_status hold=hold; %]
                         </div>
                     </td>
+                    <td>
+                        <div name="acct_holds_notify">
+                            [% PROCESS get_hold_notify h=ahr; %]
+                        </div>
+                    </td>
                     <td class="hold_notes">
                     [%- FOREACH pubnote IN ahr.notes;
                         IF pubnote.pub == 't';
diff --git a/Open-ILS/src/templates/opac/myopac/holds/edit.tt2 b/Open-ILS/src/templates/opac/myopac/holds/edit.tt2
index 6b567a65f1..ab8737339e 100644
--- a/Open-ILS/src/templates/opac/myopac/holds/edit.tt2
+++ b/Open-ILS/src/templates/opac/myopac/holds/edit.tt2
@@ -100,6 +100,26 @@
                             <em>[% l('Enter date in MM/DD/YYYY format') %]</em>
                         </td>
                     </tr>
+                    <tr>
+                        <td>[% l('Email Notification') %]</td>
+                        <td><input type="checkbox" name="email_notify"
+                                [% IF ahr.email_notify == 't' %] checked [% END %] />
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>[% l('Phone Notification') %]</td>
+                        <td><input type="text" name="phone_notify"
+                                value="[% ahr.phone_notify | html %]" /></td>
+                    </tr>
+                    <tr>
+                        <td>[% l('SMS Notification') %]</td>
+                        <td><input onblur="check_sms_carrier(event)" type="text" name="sms_notify"
+                                value="[% ahr.sms_notify | html %]" /></td>
+                    </tr>
+                    <tr>
+                        <td>[% l('Default Mobile Carrier') %]</td>
+                        <td>[% INCLUDE "opac/parts/sms_carrier_selector.tt2" ahr, sms_carrier_hide_warning="true", sms_carrier_hide_label="true" %]</td>
+                    </tr>
                     [% END %]
                      
                     <tr><td colspan='4'>
diff --git a/Open-ILS/src/templates/opac/myopac/prefs_notify.tt2 b/Open-ILS/src/templates/opac/myopac/prefs_notify.tt2
index 47b7f72677..7b17ada0f9 100644
--- a/Open-ILS/src/templates/opac/myopac/prefs_notify.tt2
+++ b/Open-ILS/src/templates/opac/myopac/prefs_notify.tt2
@@ -4,103 +4,153 @@
     prefs_page = 'prefs_notify' %]
 
 <h3 class="sr-only">[% l('Notification Preferences') %]</h3>
-<form method='post'>
-    [% setting = 'opac.hold_notify' %]
-    <input name='[% setting %]' type="hidden"
-        [% IF ctx.user_setting_map.$setting; %] value='[% ctx.user_setting_map.$setting | html %]' [% END %]/>
+    [% IF ctx.affectedChgs %]
 
-    <table class="full-width data_grid" id="acct_search_main" 
-        title="[% l('Notification Preferences') %]">
-        <tbody>
-
-            [% IF ctx.updated_user_settings %]
-            <tr><td colspan='2'>
-                <div class='renew-summary'>
-                    [% l('Account Successfully Updated') %]
-                </div>
-            </td></tr>
-            [% END %]
+        [% # get hash of sms carriers keyed by id:
+            temp = ctx.search_csc('active','t');
+            tcos = { '0' => 'None' };
+            FOR o IN temp;
+                id = o.id;
+                tcos.$id = o;
+            END;
+        %]
+  <h4 class="">[% l('You have updated notification preferences. Those changes only affect future holds. Would you like to update existing holds to use the new information?') %]</h4>
+    <form id="hold_updates_form" name="hold_updates_form" method='post' action="./prefs_notify_changed_holds" onsubmit='return updateHoldsCheck()'>
+        <table class="full-width data_grid" id="acct_search_main" 
+            title="[% l('Notification Preferences') %]">
+            <tbody>
+        [% SET blnk = l('Blank') %]
+        [% FOR c IN ctx.affectedChgs %]
+            <tr>
+              <td>
+                [% IF c.2 == 'sms_info' %]
+                <input type='hidden' name="[% c.2 %][]" value="[% c.0.carrier _ ',' _ c.0.sms | html %]" />
+                <input type='hidden' name="[% c.2 %][]" value="[% c.1.carrier _ ',' _ c.1.sms | html %]" />
+                [% ELSE %]
+                <input type='hidden' name="[% c.2 %][]" value="[% c.0 %]" />
+                <input type='hidden' name="[% c.2 %][]" value="[% c.1 %]" />
+                [% END %]
+                <input type='hidden' name="[% c.2 %][]" value="[% c.2 %]" />
+                <input type='hidden' name="[% c.2 %][]" value="[% FOREACH i IN c.3 %][% i %],[% END %]" />
+                <input id="[% c %]" type="checkbox" onchange="canSubmit(event)" name="[% c.2 %][]"/>
+                [% IF c.2 == 'sms_info' %]
+                    [% SET disp_name = l('SMS carrier/number') %]
+                    <label for="[% c %]">[% c.3.size %] hold(s) currently with [% disp_name %] set to '[% old = c.0.carrier; tcos.$old.name() ? tcos.$old.name() : blnk | html %]/[% c.0.sms ? c.0.sms : blnk | html %]'. Update to '[% new = c.1.carrier; tcos.$new.name()  ? tcos.$new.name() : blnk | html %]/[% c.1.sms ? c.1.sms : blnk | html %]'?</label>
+                [% ELSIF c.2.match('_notify') %]
+                    [% SET f_name = c.2.replace("_", " "); Y = l('YES'); N = l('NO') %]
+                    <label for="[% c %]">[% c.3.size %] hold(s) currently with [% f_name %] set to [% c.0 == 'false' ? N : Y %]. Update to [% c.1 == 'false' ? N : Y %]?</label>
+                [% ELSE %]
+                    [% SET f_name = c.2.replace("_", " ") %]
+                    <label for="[% c %]">[% c.3.size %] hold(s) currently with [% f_name %] set to '[% c.0 ? c.0 : blnk %]'. Update to '[% c.1 ? c.1 : blnk %]'?</label>
+                [% END %]
+              </td>
+            </tr>
+        [% END %]
+            </tbody>
+        </table>
+        <input type='submit' disabled value="[% l('Update') %]" class="opac-button" />
+        <a href='/eg/opac/myopac/prefs_notify'>[% l('Continue without updating') %]</a>
+    </form>
 
+    [% ELSE %]
+        <form id="hold_notify_form" name="hold_notify_form" method='post'>
             [% setting = 'opac.hold_notify' %]
-            <tr>
-                [%# WCAG insists that labels for checkboxes contain the input
-                    or directly follow the input, which would not look right
-                    with the rest of the table.  As an alternative, we can
-                    repeat the label as a title attr.
-                    http://www.w3.org/TR/WCAG20-TECHS/H44.html %]
-                [% email_label = l('Notify by Email by default when a hold is ready for pickup?') %]
+            <input name='[% setting %]' type="hidden"
+                [% IF ctx.user_setting_map.$setting; %] value='[% ctx.user_setting_map.$setting | html %]' [% END %]/>
 
-                <td><label for='[% setting %].email'>[% email_label %]</label></td>
-                <td>
-                    <input id='[% setting %].email' name='[% setting %].email' 
-                        type="checkbox" title="[% email_label %]"
-                        [% IF (matches = ctx.user_setting_map.$setting.match('email')); %] checked='checked' [% END %]/>
-                </td>
-            </tr>
-            [%- IF allow_phone_notifications == 'true';
-                setting = 'opac.hold_notify'; 
-            -%]
-            <tr>
-                [% phone_label = l('Notify by Phone by default when a hold is ready for pickup?') %]
-                <td><label for='[% setting %].phone'>[% phone_label %]</label></td>
-                <td>
-                    <input id='[% setting %].phone' name='[% setting %].phone' 
-                        type="checkbox" title="[% phone_label %]"
-                        [% IF (matches = ctx.user_setting_map.$setting.match('phone')); %] checked='checked' [% END %]/>
-                </td>
-            </tr>
-            [% setting = 'opac.default_phone' %]
-            <tr>
-                <td><label for='[% setting %]'>[% l('Default Phone Number') %]</label></td>
-                <td>
-                    <input id='[% setting %]' name='[% setting %]' type="text"
-                        [% IF ctx.user_setting_map.$setting; %] value='[% ctx.user_setting_map.$setting | html %]' [% END %]/>
-                </td>
-            </tr>
-            [%- END %]
-            [%- IF ctx.get_org_setting(ctx.search_ou, 'sms.enable') == 1;
-               setting = 'opac.hold_notify';
-            -%]
-            <tr>
-                [% sms_label = l('Notify by Text by default when a hold is ready for pickup?') %]
-                <td><label for='[% setting %].sms'>[% sms_label %]</label></td>
-                <td>
-                    <input id='[% setting %].sms' name='[% setting %].sms' 
-                        type="checkbox" title="[% sms_label %]"
-                        [% IF (matches = ctx.user_setting_map.$setting.match('sms')); %] checked='checked' [% END %]/>
-                </td>
-            </tr>
-            <tr>
-                <td>[% l('Default Mobile Carrier') %]</td>
-                <td>[% INCLUDE "opac/parts/sms_carrier_selector.tt2" sms_carrier_hide_label="true" %]</td>
-            </tr>
-            [% setting = 'opac.default_sms_notify' %]
-            <tr>
-                <td><label for='[% setting %]'>[% l('Default Mobile Number') %]</label></td>
-                <td>
-                    <input id='[% setting %]' name='[% setting %]' type="text"
-                        [% IF ctx.user_setting_map.$setting; %] value='[% ctx.user_setting_map.$setting | html %]' [% END %]/>
-                    [% l('Hint: use the full 10 digits of your phone #, no spaces, no dashes'); %]
-                </td>
-            </tr>
-            [% END %]
-            [% FOR optin IN ctx.opt_in_settings %]
-            <tr>
-                <td><label for='[% optin.cust.name | uri %]'>[% optin.cust.label | html %]</label></td>
-                <td>
-                    <input type='checkbox' name='setting' 
-                        value='[% optin.cust.name | uri %]' 
-                        id='[% optin.cust.name | uri %]' 
-                        title="[% optin.cust.label | html %]"
-                        [% IF optin.value %] checked='checked' [% END %]/>
-                </td>
-            </tr>
-            [% END %]
-        </tbody>
-    </table>
+            <table class="full-width data_grid" id="acct_search_main" 
+                title="[% l('Notification Preferences') %]">
+                <tbody>
+
+                    [% IF ctx.updated_user_settings %]
+                    <tr><td colspan='2'>
+                        <div class='renew-summary'>
+                            [% l('Account Successfully Updated') %]
+                        </div>
+                    </td></tr>
+                    [% END %]
+
+                    [% setting = 'opac.hold_notify' %]
+                    <tr>
+                        [%# WCAG insists that labels for checkboxes contain the input
+                            or directly follow the input, which would not look right
+                            with the rest of the table.  As an alternative, we can
+                            repeat the label as a title attr.
+                            http://www.w3.org/TR/WCAG20-TECHS/H44.html %]
+                        [% email_label = l('Notify by Email by default when a hold is ready for pickup?') %]
+
+                        <td><label for='[% setting %].email'>[% email_label %]</label></td>
+                        <td>
+                            <input onchange="record_change(event)" id='[% setting %].email' name='[% setting %].email'
+                                type="checkbox" title="[% email_label %]"
+                                [% IF (matches = ctx.user_setting_map.$setting.match('email')); %] checked='checked' [% END %]/>
+                        </td>
+                    </tr>
+                    [%- IF allow_phone_notifications == 'true';
+                        setting = 'opac.hold_notify'; 
+                    -%]
+                    <tr>
+                        [% phone_label = l('Notify by Phone by default when a hold is ready for pickup?') %]
+                        <td><label for='[% setting %].phone'>[% phone_label %]</label></td>
+                        <td>
+                            <input onchange="record_change(event)" id='[% setting %].phone' name='[% setting %].phone'
+                                type="checkbox" title="[% phone_label %]"
+                                [% IF (matches = ctx.user_setting_map.$setting.match('phone')); %] checked='checked' [% END %]/>
+                        </td>
+                    </tr>
+                    [% setting = 'opac.default_phone' %]
+                    <tr>
+                        <td><label for='[% setting %]'>[% l('Default Phone Number') %]</label></td>
+                        <td>
+                            <input onchange="record_change(event)" id='[% setting %]' name='[% setting %]' type="text"
+                                [% IF ctx.user_setting_map.$setting; %] value='[% ctx.user_setting_map.$setting | html %]' [% END %]/>
+                        </td>
+                    </tr>
+                    [%- END %]
+                    [%- IF ctx.get_org_setting(ctx.search_ou, 'sms.enable') == 1;
+                       setting = 'opac.hold_notify';
+                    -%]
+                    <tr>
+                        [% sms_label = l('Notify by Text by default when a hold is ready for pickup?') %]
+                        <td><label for='[% setting %].sms'>[% sms_label %]</label></td>
+                        <td>
+                            <input onchange="record_change(event)" id='[% setting %].sms' name='[% setting %].sms'
+                                type="checkbox" title="[% sms_label %]"
+                                [% IF (matches = ctx.user_setting_map.$setting.match('sms')); %] checked='checked' [% END %]/>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>[% l('Default Mobile Carrier') %]</td>
+                        <td>[% INCLUDE "opac/parts/sms_carrier_selector.tt2" sms_carrier_hide_label="true" %]</td>
+                    </tr>
+                    [% setting = 'opac.default_sms_notify' %]
+                    <tr>
+                        <td><label for='[% setting %]'>[% l('Default Mobile Number') %]</label></td>
+                        <td>
+                            <input onchange="record_change(event)" id='[% setting %]' name='[% setting %]' type="text"
+                                [% IF ctx.user_setting_map.$setting; %] value='[% ctx.user_setting_map.$setting | html %]' [% END %]/>
+                            [% l('Hint: use the full 10 digits of your phone #, no spaces, no dashes'); %]
+                        </td>
+                    </tr>
+                    [% END %]
+                    [% FOR optin IN ctx.opt_in_settings %]
+                    <tr>
+                        <td><label for='[% optin.cust.name | uri %]'>[% optin.cust.label | html %]</label></td>
+                        <td>
+                            <input type='checkbox' name='setting' 
+                                value='[% optin.cust.name | uri %]' 
+                                id='[% optin.cust.name | uri %]' 
+                                title="[% optin.cust.label | html %]"
+                                [% IF optin.value %] checked='checked' [% END %]/>
+                        </td>
+                    </tr>
+                    [% END %]
+                </tbody>
+            </table>
 
-    <input type='submit' value="[% l('Save') %]" class="opac-button" />
-</form>
+            <input type='submit' value="[% l('Save') %]" class="opac-button" />
+        </form>
+    [% END %]
 [% END %]
 
 
diff --git a/Open-ILS/src/templates/opac/myopac/prefs_notify_changed_holds.tt2 b/Open-ILS/src/templates/opac/myopac/prefs_notify_changed_holds.tt2
new file mode 100644
index 0000000000..0a33c3f2af
--- /dev/null
+++ b/Open-ILS/src/templates/opac/myopac/prefs_notify_changed_holds.tt2
@@ -0,0 +1,29 @@
+[%  PROCESS "opac/parts/header.tt2";
+    WRAPPER "opac/parts/myopac/prefs_base.tt2";
+    myopac_page = "prefs";
+    prefs_page = 'prefs_notify' %]
+
+<h3 class="sr-only">[% l('Affected Holds') %]</h3>
+<div id="update_hold_notify_confirm" >
+    [% IF ctx.updated %]
+        <p>[% l('Hold Notification Information Updated.') %]</p>
+<!--
+        <ul>
+        [% FOREACH c IN ctx.updated %]
+            <li>
+            [% l('Holds updated: ') %]
+                [% FOREACH i IN c %]
+                    [% i %]
+                [% END %]
+            </li>
+        [% END %]
+        </ul>
+-->
+    [% ELSE %]
+    <p>[% l('No changes') %].</p>
+    [% END %]
+    <a href='/eg/opac/myopac/prefs_notify'>[% l('Continue.') %]</a>
+</form>
+[% END %]
+
+
diff --git a/Open-ILS/src/templates/opac/parts/hold_notify.tt2 b/Open-ILS/src/templates/opac/parts/hold_notify.tt2
new file mode 100644
index 0000000000..4d755f1e8a
--- /dev/null
+++ b/Open-ILS/src/templates/opac/parts/hold_notify.tt2
@@ -0,0 +1,28 @@
+[% BLOCK get_hold_notify %]
+    [% # get hash of sms carriers keyed by id:
+        temp = ctx.search_csc('active','t');
+        tcos = { '0' => 'None' };
+        FOR o IN temp;
+            id = o.id;
+            tcos.$id = o;
+        END;
+    %]
+    [% SET any_notify = 0 %]
+    <div>
+    [% IF h.email_notify == 't' %]
+        [% any_notify = 1 %]
+        <strong>[% l("Email") %]</strong>: [% l("Yes") %]<br/>
+    [% END %]
+    [% IF h.phone_notify %]
+        [% any_notify = 1 %]
+        <strong>[% l("Phone") %]</strong>: [% h.phone_notify | html %]<br/>
+    [% END %]
+    [% IF h.sms_notify %]
+        [% any_notify = 1, cid = h.sms_carrier; %]
+        <strong>[% l("SMS") %]</strong>: [% h.sms_notify | html %] ([% tcos.$cid.name() | html %])<br/>
+    [% END %]
+    [% UNLESS any_notify %]
+        <span style="color:red">[% l("None") %]</span>
+    [% END %]
+    </div>
+[% END %]
diff --git a/Open-ILS/src/templates/opac/parts/sms_carrier_selector.tt2 b/Open-ILS/src/templates/opac/parts/sms_carrier_selector.tt2
index 95ba8ffc43..3515bc7d23 100644
--- a/Open-ILS/src/templates/opac/parts/sms_carrier_selector.tt2
+++ b/Open-ILS/src/templates/opac/parts/sms_carrier_selector.tt2
@@ -18,11 +18,11 @@
     END;
 %]
 [% IF NOT sms_carrier_hide_label; '<label for="sms_carrier">' _ l('Mobile carrier:') _ '</label>'; END; %]
-<select name="sms_carrier" id="sms_carrier" [% IF sms_carrier_hide_label; 'aria-label="' _ l('Mobile carrier') _ '"'; END; %]>
+<select onchange="record_change(event)" id="sms_carrier" name="sms_carrier" id="sms_carrier" [% IF sms_carrier_hide_label; 'aria-label="' _ l('Mobile carrier') _ '"'; END; %]>
     <option value="">[% l('Please select your mobile carrier') %]</option>
     [% FOR carrier IN carriers.sort('name','region') -%]
     <option value='[% carrier.id | html %]'[%
-        default_carrier == carrier.id ? ' selected="selected"' : ''
+        default_carrier == carrier.id || ahr.sms_carrier == carrier.id ? ' selected="selected"' : ''
     %]>[% carrier.name | html %] ([% carrier.region | html %])</option>
     [% END -%]
 </select>
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 f61f5eea07..1c66575c1b 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -113,7 +113,7 @@ MACRO draw_form_input(cls, field, path, type, disable) BLOCK;
 %]
   <div class="col-md-3 reg-field-input">
     <input 
-      [% IF type == "email" %]type="text"
+      [% IF type == "email" %]type="text" ng-required="hold_notify_type.email"
       [% ELSE %]type="[% type %]"
       [% END %]
       class="form-control" 
@@ -696,8 +696,9 @@ within the "form" by name for validation.
     <label>{{user_setting_types['opac.default_phone'].label()}}</label>
   </div>
   <div class="col-md-3 reg-field-input">
-    <input 
+    <input ng-required="hold_notify_type.phone" 
       ng-change="field_modified()" 
+      ng-blur="handle_field_changed(user_settings, 'opac.default_phone')"
       type='text' ng-model="user_settings['opac.default_phone']"/>
   </div>
 </div>
@@ -773,8 +774,9 @@ within the "form" by name for validation.
     <label>[% l('Default SMS/Text Number') %]</label>
   </div>
   <div class="col-md-3 reg-field-input">
-    <input 
+    <input ng-required="hold_notify_type.sms" 
       ng-change="field_modified()" ng-model="user_settings['opac.default_sms_notify']"
+      ng-blur="handle_field_changed(user_settings, 'opac.default_sms_notify')"
       type='text'/>
   </div>
 </div>
@@ -785,7 +787,8 @@ within the "form" by name for validation.
   </div>
   <div class="col-md-3 reg-field-input">
     <span class="nullable">
-      <select str-to-int class="form-control" ng-model="user_settings['opac.default_sms_carrier']" ng-options="c.id() as c.name() for c in sms_carriers">
+      <select str-to-int ng-required="user_settings['opac.default_sms_notify']" class="form-control" ng-model="user_settings['opac.default_sms_carrier']" ng-options="c.id() as c.name() for c in sms_carriers"
+      ng-blur="handle_field_changed(user_settings, 'opac.default_sms_carrier')">
         <option value="">Select a Carrier</option>
       </select>
     </span>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_hold_notify_update.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_hold_notify_update.tt2
new file mode 100644
index 0000000000..b80891f676
--- /dev/null
+++ b/Open-ILS/src/templates/staff/circ/patron/t_hold_notify_update.tt2
@@ -0,0 +1,42 @@
+<div class="modal-header">
+  <button type="button" class="close" ng-click="ok('no-update')"
+    aria-hidden="true">×</button>
+  <h4 class="modal-title">[% l('Update Hold Notification Info?') %]</h4>
+</div>
+<div class="modal-body">
+<form name="updateHoldsForm">
+  <div class="row" ng-repeat="f in ch_fields">
+    <div class="col-md-11">
+      <span ng-switch="f.name">
+        <span ng-switch-when="phone_notify">[% l("You have set Notify by Phone to '[_1]'", '{{prettyBool(f.newval)}}') %]</span>
+        <span ng-switch-when="sms_notify">[% l("You have set Notify by SMS to '[_1]'", '{{prettyBool(f.newval)}}') %]</span>
+        <span ng-switch-when="email_notify">[% l("You have set Notify by Email to '[_1]'", '{{prettyBool(f.newval)}}') %]</span>
+        <span ng-switch-when="default_phone">[% l("You have set Default Phone Number to '[_1]'", '{{f.newval}}') %]</span>
+        <span ng-switch-when="default_sms">[% l("You have set Default SMS/Text Number to '[_1]'", '{{f.newval}}') %]</span>
+        <span ng-switch-when="default_sms_carrier_id">[% l("You have set Default SMS Carrier to '[_1]'", '{{prettyCarrier(f.newval)}}') %]</span>
+      </span>
+      <ul style="padding-left:0" ng-if="isNumberCh(f)" class="list-unstyled">
+        <li ng-repeat="(k, h) in f.groups" style="margin-left: 20px">
+          <input id="{{f.name + h[0].id}}" type="checkbox" ng-model="h.isChecked" ng-change="groupChanged(f, k)" style="position: absolute" />
+          <label ng-if="f.newval" for="{{f.name + h[0].id}}" style="padding-left: 1.5em">[% l("'[_1]' is currently used for [_2] hold(s). Update to '[_3]'?", "{{k}}", "{{f.groups[k].length}}", "{{f.newval ? f.newval : '(null)'}}") %]</label>
+          <label ng-if="!f.newval" for="{{f.name + h[0].id}}" style="padding-left: 1.5em">[% l("'[_1]' is currently used for [_2] hold(s). Remove that from the hold(s)?", "{{k}}", "{{f.groups[k].length}}") %]</label>
+        </li>
+      </ul>
+      <ul style="padding-left:0" ng-if="!isNumberCh(f)" class="list-unstyled">
+        <li style="margin-left: 20px">
+          <input id="{{f.name}}" type="checkbox" ng-model="f.isChecked" ng-change="nonGrpChanged(f)" style="position: absolute" />
+          <label ng-if="f.name.includes('sms_carrier')" for="{{f.name}}" style="padding-left: 1.5em">[% l("A carrier other than '[_1]' is currently used in [_2] hold(s). Update to '[_3]'?", "{{prettyCarrier(f.newval)}}", "{{f.affects.length}}", "{{prettyCarrier(f.newval)}}") %]</label>
+          <label ng-if="!f.name.includes('sms_carrier')" for="{{f.name}}" style="padding-left: 1.5em">[% l("[_1] hold(s) have it set to [_2]. Update to [_3]?", "{{f.affects.length}}", "{{prettyBool(f.old)}}", "{{prettyBool(f.newval)}}") %]</label>
+        </li>
+      </ul>
+    </div>
+  </div>
+<div class="modal-footer">
+  <div class="row">
+    <div class="col-md-10 pull-right">
+      <input type="submit" class="btn btn-primary" ng-disabled="chgCt < 1" ng-click="ok()" value="[% l('Update Holds') %]"/>
+      <input type="submit" class="btn btn-warning" ng-click="ok('no-update')" value="[% l('Do Not Update Holds') %]"/>
+    </div>
+  </div>
+
+</div>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_holds_list.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_holds_list.tt2
index 48700fcc22..961ad03fc6 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_holds_list.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_holds_list.tt2
@@ -87,6 +87,7 @@
   <eg-grid-field path='hold.requestor.id' parent-idl-class="ahr" label="[% l('Requestor ID') %]" hidden></eg-grid-field>
   <eg-grid-field path='hold.requestor.usrname' parent-idl-class="ahr" label="[% l('Requestor Username') %]" hidden></eg-grid-field>
   <eg-grid-field path='hold.sms_carrier.name' parent-idl-class="ahr" label="[% l('Notifications SMS Carrier') %]" hidden></eg-grid-field>
+  <eg-grid-field path='hold.sms_carrier' label="[% l('SMS Carrier') %]" hidden></eg-grid-field>
 
   <eg-grid-field path='part.label' parent-idl-class="bmp" label="[% l('Part') %]" hidden></eg-grid-field>
   <eg-grid-field path='volume.*' parent-idl-class="acn" hidden></eg-grid-field>
diff --git a/Open-ILS/web/js/ui/default/opac/simple.js b/Open-ILS/web/js/ui/default/opac/simple.js
index c1138147bc..e121fd188f 100644
--- a/Open-ILS/web/js/ui/default/opac/simple.js
+++ b/Open-ILS/web/js/ui/default/opac/simple.js
@@ -119,3 +119,198 @@ function exclude_onchange(checkbox) {
 
     checkbox.form.submit();
 }
+
+// prefs notify update holds-related code
+var hold_notify_prefs = [];
+document.addEventListener("DOMContentLoaded", function() {
+    var form = document.getElementById('hold_notify_form');
+    if (!form) return;
+    var els = form.elements;
+    for (i = 0; i < els.length; i++){
+        var e = els[i];
+        if (e.id.startsWith("opac") || e.id == 'sms_carrier'){
+            hold_notify_prefs.push({
+                name : e.id,
+                oldval : e.type == 'checkbox' ? e.checked : e.value,
+                newval : null
+            });
+            // set required attribute input fields that need it
+            if (e.id.includes('hold_notify') && !e.id.includes('email')){
+                var fieldToReq = e.id.includes('sms') ? 'opac.default_sms_notify' : 'opac.default_phone';
+                toggle_related_required(fieldToReq, e.checked);
+            }
+
+        }
+    }
+    form.addEventListener('submit', addHoldUpdates);
+});
+
+function appendChgInputs(chg){
+    // server-side we'll parse the param as an array where:
+    // [ #oldval, #newval, #name, [#arr of affected holds], #propagateBool ]
+    // this first POST will set the first three, and the confirmation interstitial
+    // the rest.
+    var form = document.getElementById('hold_notify_form');
+
+    var inputold = document.createElement('input');
+    inputold.setAttribute('type', 'hidden');
+    inputold.setAttribute('name', chg.name + '[]');
+    inputold.setAttribute('value', chg.oldval);
+    form.appendChild(inputold);
+
+    var inputnew = document.createElement('input');
+    inputnew.setAttribute('type', 'hidden');
+    inputnew.setAttribute('name', chg.name + '[]');
+    inputnew.setAttribute('value', chg.newval);
+    form.appendChild(inputnew);
+
+    var inputname = document.createElement('input');
+    inputname.setAttribute('type', 'hidden');
+    inputname.setAttribute('name', chg.name + '[]');
+    inputname.setAttribute('value', chg.name);
+    form.appendChild(inputname);
+}
+
+function addHoldUpdates(){
+    paramTranslate(hold_notify_prefs).forEach(function(chg){
+        // only append a change if it actually changed from
+        // what we had server-side originally
+        if (chg.newval != null && chg.oldval != chg.newval) appendChgInputs(chg);
+    });
+    return true;
+}
+
+function chkPh(number){
+    // normalize phone # for comparison, only digits
+    if (number == null || number == undefined) return '';
+    var regex = /[^\d]/g;
+    return number.replace(regex, '');
+}
+
+function idxOfName(n){
+    return hold_notify_prefs.findIndex(function(e){ return e.name === n});
+}
+
+function record_change(evt){
+    var field = evt.target;
+    switch(field.id){
+        case "opac.hold_notify.email":
+            var chg = hold_notify_prefs[idxOfName(field.id)]
+            chg.newval = field.checked;
+            break;
+        case "opac.hold_notify.phone":
+            var chg = hold_notify_prefs[idxOfName(field.id)]
+            chg.newval = field.checked;
+            toggle_related_required('opac.default_phone', chg.newval);
+            break;
+        case "opac.hold_notify.sms":
+            var chg = hold_notify_prefs[idxOfName(field.id)]
+            chg.newval = field.checked;
+            toggle_related_required('opac.default_sms_notify', chg.newval);
+            break;
+        case "sms_carrier": // carrier id string
+            var chg = hold_notify_prefs[idxOfName(field.id)]
+            chg.newval = field.value;
+            break;
+        case "opac.default_phone":
+            var chg = hold_notify_prefs[idxOfName(field.id)]
+            if (chkPh(field.value) != chkPh(chg.oldval)){
+                chg.newval = field.value;
+            }
+            break;
+        case "opac.default_sms_notify":
+            var chg = hold_notify_prefs[idxOfName(field.id)]
+            if (chkPh(field.value) != chkPh(chg.oldval)){
+                chg.newval = field.value;
+                toggle_related_required('sms_carrier', chg.newval ? true : false);
+            }
+            break;
+    }
+}
+
+// there are the param values for the changed fields we expect server-side
+function paramTranslate(chArr){
+    return chArr.map(function(ch){
+        var n = "";
+        switch(ch.name){
+            case "opac.hold_notify.email":
+                n = "email_notify";
+                break;
+            case "opac.hold_notify.phone":
+                n = "phone_notify";
+                break;
+            case "opac.hold_notify.sms":
+                n = "sms_notify";
+                break;
+            case "sms_carrier": // carrier id string
+                n = "default_sms_carrier_id";
+                break;
+            case "opac.default_phone":
+                n = "default_phone";
+                break;
+            case "opac.default_sms_notify":
+                n = "default_sms";
+                break;
+        }
+        return { name : n, oldval : ch.oldval, newval : ch.newval };
+    });
+}
+
+function updateHoldsCheck() {
+    // just dynamically add an input that flags that we have
+    // holds-related updates
+    var form = document.getElementById('hold_updates_form');
+    if (!form) return;
+    var els = form.elements;
+    var isValid = false;
+    for (i = 0; i < els.length; i++){
+        var e = els[i];
+        if (e.type == "checkbox" && e.checked){
+            var flag = document.createElement('input');
+            flag.setAttribute('name', 'hasHoldsChanges');
+            flag.setAttribute('type', 'hidden');
+            flag.setAttribute('value', 1);
+            form.appendChild(flag);
+            isValid = true;
+            return isValid;
+        }
+    }
+
+    alert("No option selected.");
+    return isValid;
+}
+
+function check_sms_carrier(e){
+    var sms_num = e.target;
+    // if sms number has anything in it that's not just whitespace, then require a carrier
+    if (!sms_num.value.match(/\S+/)) return;
+
+    var carrierSelect = document.getElementById('sms_carrier');
+    if (carrierSelect.selectedIndex == 0){
+        carrierSelect.setAttribute("required", "");
+    }
+
+}
+
+function canSubmit(evt){
+   // check hold updates form to see if we have any selected
+   // enable the submit button if we do
+    var form = document.getElementById('hold_updates_form');
+    var submit = form.querySelector('input[type="submit"]');
+    if (!form || !submit) return;
+    var els = form.elements;
+    for (i = 0; i < els.length; i++){
+        var e = els[i];
+        if (e.type == "checkbox" && !e.hidden && e.checked){
+            submit.removeAttribute("disabled");
+            return;
+        }
+    }
+
+    submit.setAttribute("disabled","");
+}
+
+function toggle_related_required(id, isRequired){
+    var input = document.getElementById(id);
+    input.required = isRequired;
+}
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 5949658912..72a9ebd863 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
@@ -1258,6 +1258,8 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
 
     $scope.page_data_loaded = false;
     $scope.hold_notify_type = { phone : null, email : null, sms : null };
+    $scope.hold_notify_observer = {};
+    $scope.hold_rel_contacts = {};
     $scope.clone_id = patronRegSvc.clone_id = $routeParams.clone_id;
     $scope.stage_username = 
         patronRegSvc.stage_username = $routeParams.stage_username;
@@ -1375,6 +1377,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         }
 
         extract_hold_notify();
+
         if ($scope.patron.isnew)
             set_new_patron_defaults(prs);
 
@@ -1424,6 +1427,28 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
             }
         });
     }
+    
+    // add watchers for hold notify method prefs
+    $scope.$watch('hold_notify_type.phone', function(newVal, oldVal) {
+        var notifyOpt = $scope.hold_notify_observer['phone'];
+        if (newVal !== null) {
+            notifyOpt.newval = newVal;
+        }
+    });
+
+    $scope.$watch('hold_notify_type.sms', function(newVal, oldVal) {
+        var notifyOpt = $scope.hold_notify_observer['sms'];
+        if (newVal !== null) {
+            notifyOpt.newval = newVal;
+        }
+    });
+    
+    $scope.$watch('hold_notify_type.email', function(newVal, oldVal) {
+        var notifyOpt = $scope.hold_notify_observer['email'];
+        if (newVal !== null) {
+            notifyOpt.newval = newVal;
+        }
+    });
 
     // update the currently displayed field documentation
     $scope.set_selected_field_doc = function(cls, field) {
@@ -1870,15 +1895,45 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
     }
 
     function extract_hold_notify() {
-        var notify = $scope.user_settings['opac.hold_notify'];
-        if (!notify) return;
+        var p = $scope.patron;
+        var notify = $scope.user_settings['opac.hold_notify'] || '';
+
         $scope.hold_notify_type.phone = Boolean(notify.match(/phone/));
         $scope.hold_notify_type.email = Boolean(notify.match(/email/));
         $scope.hold_notify_type.sms = Boolean(notify.match(/sms/));
+
+        // stores original loaded values for comparison later
+        for (var k in $scope.hold_notify_type){
+            var val = $scope.hold_notify_type[k];
+
+            if ($scope.hold_notify_type.hasOwnProperty(k)){
+                $scope.hold_notify_observer[k] = {old : val, newval: null};
+            }
+        }
+
+        // actual value from user
+        $scope.hold_rel_contacts.day_phone = { old: p.day_phone, newval : null };
+        $scope.hold_rel_contacts.other_phone = { old: p.other_phone, newval : null };
+        $scope.hold_rel_contacts.evening_phone = { old: p.evening_phone, newval : null };
+        // from user_settings
+        $scope.hold_rel_contacts.default_phone = { old: $scope.user_settings['opac.default_phone'], newval : null };
+        $scope.hold_rel_contacts.default_sms = { old: $scope.user_settings['opac.default_sms_notify'], newval : null };
+        $scope.hold_rel_contacts.default_sms_carrier_id = { old: $scope.user_settings['opac.default_sms_carrier'], newval : null };
+
+    }
+
+    function normalizePhone(number){
+        // normalize phone # for comparison, only digits
+        if (number == null || number == undefined) return '';
+        
+        var regex = /[^\d]/g;
+        return number.replace(regex, '');
     }
 
     $scope.invalidate_field = function(field) {
-        patronRegSvc.invalidate_field($scope.patron, field);
+        patronRegSvc.invalidate_field($scope.patron, field).then(function() {
+            $scope.handle_field_changed($scope.patron, field);
+        });
     }
 
     address_alert = function(addr) {
@@ -1983,17 +2038,29 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         console.debug('changing field ' + field_name + ' to ' + value);
 
         switch (field_name) {
-            case 'day_phone' : 
+            case 'day_phone' :
+                if (normalizePhone(value) !== normalizePhone($scope.hold_rel_contacts.day_phone.old)){
+                    $scope.hold_rel_contacts.day_phone.newval = value;
+                }
                 if ($scope.patron.day_phone && 
                     $scope.patron.isnew && 
                     $scope.org_settings['patron.password.use_phone']) {
                     $scope.patron.passwd = $scope.patron.day_phone.substr(-4);
                 }
-            case 'evening_phone' : 
+                $scope.dupe_value_changed(field_name, value);
+                break;
+            case 'evening_phone' :
+                if (normalizePhone(value) !== normalizePhone($scope.hold_rel_contacts.evening_phone.old)){
+                    $scope.hold_rel_contacts.evening_phone.newval = value;
+                }
+                $scope.dupe_value_changed(field_name, value);
+                break;
             case 'other_phone' : 
+                if (normalizePhone(value) !== normalizePhone($scope.hold_rel_contacts.other_phone.old)){
+                    $scope.hold_rel_contacts.other_phone.newval = value;
+                }
                 $scope.dupe_value_changed(field_name, value);
                 break;
-
             case 'ident_value':
             case 'ident_value2':
                 $scope.dupe_value_changed('ident', value);
@@ -2030,6 +2097,21 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
                 $scope.barcode_changed(value);
                 apply_username_regex();
                 break;
+            case 'opac.default_phone':
+                if (normalizePhone(value) !== normalizePhone($scope.hold_rel_contacts.default_phone.old)){
+                    $scope.hold_rel_contacts.default_phone.newval = value;
+                }
+                break;
+            case 'opac.default_sms_notify':
+                if (normalizePhone(value) !== normalizePhone($scope.hold_rel_contacts.default_sms.old)){
+                    $scope.hold_rel_contacts.default_sms.newval = value;
+                }
+                break;
+            case 'opac.default_sms_carrier':
+                if (value !== $scope.hold_rel_contacts.default_sms_carrier_id.old){
+                    $scope.hold_rel_contacts.default_sms_carrier_id.newval = value;
+                }
+                break;
         }
     }
 
@@ -2171,11 +2253,15 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
                 var ids = $scope.patron.groups.map(function(g) {return g.id()});
                 return patronRegSvc.apply_secondary_groups(updated_user.id(), ids)
             }
-
             return $q.when();
 
+        }).then(findChangedFieldsAffectedHolds)
+        .then(function(changed_fields_plus_holds) {
+            var needModal = changed_fields_plus_holds[0] && changed_fields_plus_holds[0].length > 0;
+            return needModal
+                ? $scope.update_holds_notify_modal(changed_fields_plus_holds[0])
+                : $q.when(); // nothing changed, continue
         }).then(function() {
-
             if (updated_user) {
                 egWorkLog.record(
                     $scope.patron.isnew
@@ -2219,7 +2305,254 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
             }
         });
     }
+    
+    var phone_inputs = ["day_phone", "evening_phone","other_phone", "default_phone"];
+    var sms_inputs = ["default_sms", "default_sms_carrier_id"];
+    var method_prefs = ["sms_notify", "phone_notify", "email_notify"];
+    var groupBy = function(xs, key){
+        return xs.reduce(function(rv, x){
+            (rv[x[key]] = rv[x[key]] || []).push(x);
+            return rv;
+        }, {});
+    };
+
+    function findChangedFieldsAffectedHolds(){
+    
+        var changed_hold_fields = [];
+
+        var default_phone_changed = false;
+        var default_sms_carrier_changed = false;
+        var default_sms_changed = false;
+        for (var c in $scope.hold_rel_contacts){
+            var newinput = $scope.hold_rel_contacts[c].newval;
+            if ($scope.hold_rel_contacts.hasOwnProperty(c)
+                && newinput !== null // null means user has not provided a value in this session
+                && newinput != $scope.hold_rel_contacts[c].old){
+                var changed = $scope.hold_rel_contacts[c];
+                changed.name = c;
+                changed.isChecked = false;
+                changed_hold_fields.push(changed);
+                if (c === 'default_phone') default_phone_changed = true;
+                if (c === 'default_sms_carrier_id') default_sms_carrier_changed = true;
+                if (c === 'default_sms') default_sms_changed = true;
+            }
+        }
 
+        for (var c in $scope.hold_notify_observer){
+            var newinput = $scope.hold_notify_observer[c].newval;
+            if ($scope.hold_notify_observer.hasOwnProperty(c)
+                && newinput !== null // null means user has not provided a value in this session
+                && newinput != $scope.hold_notify_observer[c].old){
+                var changed = $scope.hold_notify_observer[c];
+                changed.name = c + "_notify";
+                changed.isChecked = false;
+                changed_hold_fields.push(changed);
+
+                // if we're turning on phone notifications, offer to update to the
+                // current default number
+                if (c === 'phone' && $scope.user_settings['opac.default_phone'] && newinput && !default_phone_changed) {
+                    changed_hold_fields.push({
+                        name: 'default_phone',
+                        old: 'nosuch',
+                        newval: $scope.user_settings['opac.default_phone'],
+                        isChecked: false
+                    });
+                }
+                // and similarly for SMS
+                if (c === 'sms' && $scope.user_settings['opac.default_sms_carrier'] && newinput && !default_sms_carrier_changed) {
+                    changed_hold_fields.push({
+                        name: 'default_sms_carrier_id',
+                        old: -1,
+                        newval: $scope.user_settings['opac.default_sms_carrier'],
+                        isChecked: false
+                    });
+                }
+                if (c === 'sms' && $scope.user_settings['opac.default_sms_notify'] && newinput && !default_sms_changed) {
+                    changed_hold_fields.push({
+                        name: 'default_sms',
+                        old: 'nosuch',
+                        newval: $scope.user_settings['opac.default_sms_notify'],
+                        isChecked: false
+                    });
+                }
+            }
+        }
+
+        var promises = [];
+        angular.forEach(changed_hold_fields, function(c){
+            promises.push(egCore.net.request('open-ils.circ',
+            'open-ils.circ.holds.retrieve_by_notify_staff',
+            egCore.auth.token(),
+            $scope.patron.id,
+            c.name.includes('notify') || c.name.includes('carrier') ? c.old : c.newval,
+            c.name)
+                .then(function(affected_holds){
+                    if(!affected_holds || affected_holds.length < 1){
+                        // no holds affected - remove change from list
+                        var p = changed_hold_fields.indexOf(c);
+                        changed_hold_fields.splice(p, 1);
+                    } else {
+                        c.affects = affected_holds;
+                        //c.groups = {};
+                        //angular.forEach(c.affects, function(h){
+                        //    c.groups[]
+                        //});
+                        if (!c.name.includes("notify")){
+                            if (c.name === "default_sms_carrier_id") {
+                                c.groups = groupBy(c.affects,'sms_carrier');
+                            } else {
+                                c.groups = groupBy(c.affects, c.name.includes('_phone') ? 'phone_notify':'sms_notify');
+                            }
+                        }
+                    }
+                    return $q.when(changed_hold_fields);
+                })
+            );
+        });
+
+        return $q.all(promises);
+    }
+
+    $scope.update_holds_notify_modal = function(changed_hold_fields){
+        // open modal after-save, pre-reload modal to deal with updated hold notification stuff
+        if ($scope.patron.isnew || changed_hold_fields.length < 1){
+            return $q.when();
+        }
+
+        return $uibModal.open({
+            templateUrl: './circ/patron/t_hold_notify_update',
+            backdrop: 'static',
+            controller:
+                       ['$scope','$uibModalInstance','changed_fields','patron','carriers','def_carrier_id','default_phone','default_sms',
+                function($scope , $uibModalInstance , changed_fields , patron,  carriers,  def_carrier_id , default_phone , default_sms) {
+                // local modal scope
+                $scope.ch_fields = changed_fields;
+                $scope.focusMe = true;
+                $scope.ok = function(msg) {
+
+                    // Need to do this so the page will reload automatically
+                    if (msg == 'no-update') return $uibModalInstance.close();
+
+                    //var selectedChanges = $scope.changed_fields.filter(function(c) {
+                    //    return c.isChecked;
+                    //});
+                    var selectedChanges = [];
+                    angular.forEach($scope.ch_fields, function(f){
+                        if (f.name == 'phone_notify' && f.newval && f.isChecked) {
+                            // convert to default_phone change
+                            f.sel_hids = f.affects.map(function(h){ return h.id});
+                            f.newval = default_phone;
+                            f.name = 'default_phone';
+                            selectedChanges.push(f);
+                        } else if (f.name == 'sms_notify' && f.newval && f.isChecked) {
+                            // convert to default_sms change
+                            f.sel_hids = f.affects.map(function(h){ return h.id});
+                            f.newval = default_sms;
+                            f.name = 'default_sms';
+                            selectedChanges.push(f);
+                        } else if (f.name.includes('notify') || f.name.includes('carrier')){
+                            if (f.isChecked){
+                                f.sel_hids = f.affects.map(function(h){ return h.id});
+                                selectedChanges.push(f);
+                            }
+                        } else {
+                            // this is the sms or phone, so look in the groups obj
+                            f.sel_hids = [];
+                            for (var k in f.groups){
+                                if (f.groups.hasOwnProperty(k)){
+                                    var sel_holds = f.groups[k].filter(function(h){
+                                        return h.isChecked;
+                                    });
+                                    
+                                    var hids = sel_holds.map(function(h){ return h.id});
+                                    f.sel_hids.push.apply(f.sel_hids, hids);
+                                }
+                            }
+
+                            if (f.sel_hids.length > 0) selectedChanges.push(f);
+                        }
+                    });
+
+
+                    // call method to update holds for each change
+                    var chain = $q.when();
+                    angular.forEach(selectedChanges, function(c){
+                        var carrierId = c.name.includes('default_sms') ? Number(def_carrier_id) : null;
+                        chain = chain.then(function() {
+                            return egCore.net.request('open-ils.circ',
+                                    'open-ils.circ.holds.batch_update_holds_by_notify_staff', egCore.auth.token(),
+                                    patron.id,
+                                    c.sel_hids,
+                                    c.old, // TODO: for number changes, old val is effectively moot
+                                    c.newval,
+                                    c.name,
+                                    carrierId).then(function(okList){ console.log(okList) });
+                        });
+                    });
+
+                    // carry out the updates and close modal
+                    chain.finally(function(){ $uibModalInstance.close() });
+                }
+
+                $scope.cancel = function () { $uibModalInstance.dismiss() }
+
+                $scope.isNumberCh = function(c){
+                    return !(c.name.includes('notify') || c.name.includes('carrier'));
+                }
+
+                $scope.chgCt = 0;
+                $scope.groupChanged = function(ch_f, grpK){
+                    var holdArr = ch_f.groups[grpK];
+                    if (holdArr && holdArr.length > 0){
+                        angular.forEach(holdArr, function(h){
+                            if (h.isChecked) { h.isChecked = !h.isChecked; $scope.chgCt-- }
+                            else { h.isChecked = true; $scope.chgCt++ }
+                        });
+                    }
+                }
+                
+                $scope.nonGrpChanged = function(field_ch){
+                    if (field_ch.isChecked) $scope.chgCt++;
+                    else $scope.chgCt--;
+                };
+
+                // use this function as a keydown handler on form
+                // elements that should not submit the form on enter.
+                $scope.preventSubmit = function($event) {
+                    if ($event.keyCode == 13)
+                        $event.preventDefault();
+                }
+
+                $scope.prettyCarrier = function(carrierId){
+                    var sms_carrierObj = carriers.find(function(c){ return c.id == carrierId});
+                    return sms_carrierObj.name;
+                };
+                $scope.prettyBool = function(v){
+                    return v ? 'YES' : 'NO';
+                };
+            }],
+            resolve : {
+                    changed_fields : function(){ return changed_hold_fields },
+                    patron : function(){ return $scope.patron },
+                    def_carrier_id : function(){
+                                       var d = $scope.hold_rel_contacts.default_sms_carrier_id;
+                                       return d.newval ? d.newval : d.old;
+                                    },
+                    default_phone : function() {
+                                        return ($scope.hold_rel_contacts.default_phone.newval) ?
+                                                    $scope.hold_rel_contacts.default_phone.newval :
+                                                    $scope.hold_rel_contacts.default_phone.old;
+                                    },
+                    default_sms : function() {
+                                        return ($scope.hold_rel_contacts.default_sms.newval) ?
+                                                    $scope.hold_rel_contacts.default_sms.newval :
+                                                    $scope.hold_rel_contacts.default_sms.old;
+                                    },
+                    carriers : function(){ return $scope.sms_carriers.map(function(c){ return egCore.idl.toHash(c) }) }
+                }
+        }).result;
+    }
+    
     $scope.edit_passthru.print = function() {
         var print_data = {patron : $scope.patron}
 

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

Summary of changes:
 .../perlmods/lib/OpenILS/Application/Circ/Holds.pm | 277 ++++++++++++++++
 .../src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm    |   1 +
 .../lib/OpenILS/WWW/EGCatLoader/Account.pm         | 267 ++++++++++++++++
 Open-ILS/src/templates/opac/myopac/holds.tt2       |   7 +
 Open-ILS/src/templates/opac/myopac/holds/edit.tt2  |  20 ++
 .../src/templates/opac/myopac/prefs_notify.tt2     | 234 ++++++++------
 .../opac/myopac/prefs_notify_changed_holds.tt2     |  29 ++
 Open-ILS/src/templates/opac/parts/hold_notify.tt2  |  28 ++
 .../templates/opac/parts/sms_carrier_selector.tt2  |   4 +-
 .../src/templates/staff/circ/patron/t_edit.tt2     |  11 +-
 .../staff/circ/patron/t_hold_notify_update.tt2     |  42 +++
 .../templates/staff/circ/patron/t_holds_list.tt2   |   1 +
 Open-ILS/web/js/ui/default/opac/simple.js          | 195 ++++++++++++
 .../web/js/ui/default/staff/circ/patron/regctl.js  | 349 ++++++++++++++++++++-
 .../Circulation/update-hold-notifications.adoc     |   7 +
 15 files changed, 1366 insertions(+), 106 deletions(-)
 create mode 100644 Open-ILS/src/templates/opac/myopac/prefs_notify_changed_holds.tt2
 create mode 100644 Open-ILS/src/templates/opac/parts/hold_notify.tt2
 create mode 100644 Open-ILS/src/templates/staff/circ/patron/t_hold_notify_update.tt2
 create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/update-hold-notifications.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list