use OpenILS::Utils::CStoreEditor qw/:funcs/;
use OpenILS::Utils::Penalty;
+use OpenILS::Utils::BadContact;
use List::Util qw/max reduce/;
use UUID::Tiny qw/:std/;
if(ref($patron->mailing_address));
# create/update the patron first so we can use his id
+
+ # $patron is the obj from the client (new data) and $new_patron is the
+ # patron object properly built for db insertion, so we need a third variable
+ # if we want to represent the old patron.
+
+ my $old_patron;
+
if($patron->isnew()) {
( $new_patron, $evt ) = _add_patron($session, _clone_patron($patron), $user_obj);
return $evt if $evt;
- } else { $new_patron = $patron; }
+ } else {
+ $new_patron = $patron;
+
+ # Did auth checking above already.
+ my $e = new_editor;
+ $old_patron = $e->retrieve_actor_user($patron->id) or
+ return $e->die_event;
+ $e->disconnect;
+ }
( $new_patron, $evt ) = _add_update_addresses($session, $patron, $new_patron, $user_obj);
return $evt if $evt;
return $evt if $evt;
}
+ ( $new_patron, $evt ) = _clear_badcontact_penalties($session, $old_patron, $new_patron, $user_obj);
+ return $evt if $evt;
+
($new_patron, $evt) = _create_stat_maps($session, $user_session, $patron, $new_patron, $user_obj);
return $evt if $evt;
return ( $new_patron, undef );
}
+sub _clear_badcontact_penalties {
+ my ($session, $old_patron, $new_patron, $user_obj) = @_;
+
+ return ($new_patron, undef) unless $old_patron;
+
+ my $PNM = $OpenILS::Utils::BadContact::PENALTY_NAME_MAP;
+ my $e = new_editor(xact => 1);
+
+ # This ignores whether the caller of update_patron has any permission
+ # to remove penalties, but these penalties no longer make sense
+ # if an email address field (for example) is changed (and the caller must
+ # have perms to do *that*) so there's no reason not to clear the penalties.
+
+ my $bad_contact_penalties = $e->search_actor_user_standing_penalty([
+ {
+ "+csp" => {"name" => [values(%$PNM)]},
+ "+ausp" => {"stop_date" => undef, "usr" => $new_patron->id}
+ }, {
+ "join" => {"csp" => {}},
+ "flesh" => 1,
+ "flesh_fields" => {"ausp" => ["standing_penalty"]}
+ }
+ ]) or return (undef, $e->die_event);
+
+ return ($new_patron, undef) unless @$bad_contact_penalties;
+
+ my @penalties_to_clear;
+ my ($field, $penalty_name);
+
+ # For each field that might have an associated bad contact penalty,
+ # check for such penalties and add them to the to-clear list if that
+ # field has changed.
+ while (($field, $penalty_name) = each(%$PNM)) {
+ if ($old_patron->$field ne $new_patron->$field) {
+ push @penalties_to_clear, grep {
+ $_->standing_penalty->name eq $penalty_name
+ } @$bad_contact_penalties;
+ }
+ }
+
+ foreach (@penalties_to_clear) {
+ # Note that this "archives" penalties, in the terminology of the staff
+ # client, instead of just deleting them. This may assist reporting,
+ # or preserving old contact information when it is still potentially
+ # of interest.
+ $_->standing_penalty($_->standing_penalty->id); # deflesh
+ $_->stop_date('now');
+ $e->update_actor_user_standing_penalty($_) or return (undef, $e->die_event);
+ }
+
+ $e->commit;
+ return ($new_patron, undef);
+}
+
sub _create_stat_maps {
];
}
+__PACKAGE__->register_method(
+ method => "mark_users_contact_invalid",
+ api_name => "open-ils.actor.invalidate.email",
+ signature => {
+ desc => "Given a patron, clear the email field and put the old email address into a note and/or create a standing penalty, depending on OU settings",
+ params => [
+ {desc => "Authentication token", type => "string"},
+ {desc => "Patron ID", type => "number"},
+ {desc => "Additional note text (optional)", type => "string"},
+ {desc => "penalty org unit ID (optional)", type => "number"}
+ ],
+ return => {desc => "Event describing success or failure", type => "object"}
+ }
+);
+
+__PACKAGE__->register_method(
+ method => "mark_users_contact_invalid",
+ api_name => "open-ils.actor.invalidate.day_phone",
+ signature => {
+ desc => "Given a patron, clear the day_phone field and put the old day_phone into a note and/or create a standing penalty, depending on OU settings",
+ params => [
+ {desc => "Authentication token", type => "string"},
+ {desc => "Patron ID", type => "number"},
+ {desc => "Additional note text (optional)", type => "string"},
+ {desc => "penalty org unit ID (optional)", type => "number"}
+ ],
+ return => {desc => "Event describing success or failure", type => "object"}
+ }
+);
+
+__PACKAGE__->register_method(
+ method => "mark_users_contact_invalid",
+ api_name => "open-ils.actor.invalidate.evening_phone",
+ signature => {
+ desc => "Given a patron, clear the evening_phone field and put the old evening_phone into a note and/or create a standing penalty, depending on OU settings",
+ params => [
+ {desc => "Authentication token", type => "string"},
+ {desc => "Patron ID", type => "number"},
+ {desc => "Additional note text (optional)", type => "string"},
+ {desc => "penalty org unit ID (optional)", type => "number"}
+ ],
+ return => {desc => "Event describing success or failure", type => "object"}
+ }
+);
+
+__PACKAGE__->register_method(
+ method => "mark_users_contact_invalid",
+ api_name => "open-ils.actor.invalidate.other_phone",
+ signature => {
+ desc => "Given a patron, clear the other_phone field and put the old other_phone into a note and/or create a standing penalty, depending on OU settings",
+ params => [
+ {desc => "Authentication token", type => "string"},
+ {desc => "Patron ID", type => "number"},
+ {desc => "Additional note text (optional)", type => "string"},
+ {desc => "penalty org unit ID (optional, default to top of org tree)",
+ type => "number"}
+ ],
+ return => {desc => "Event describing success or failure", type => "object"}
+ }
+);
+
+sub mark_users_contact_invalid {
+ my ($self, $conn, $auth, $patron_id, $addl_note, $penalty_ou) = @_;
+
+ # This method invalidates an email address or a phone_number which
+ # removes the bad email address or phone number, copying its contents
+ # to a patron note, and institutes a standing penalty for "bad email"
+ # or "bad phone number" which is cleared when the user is saved or
+ # optionally only when the user is saved with an email address or
+ # phone number (or staff manually delete the penalty).
+
+ my $contact_type = ($self->api_name =~ /invalidate.(\w+)(\.|$)/)[0];
+
+ my $e = new_editor(authtoken => $auth, xact => 1);
+ return $e->die_event unless $e->checkauth;
+
+ return OpenILS::Utils::BadContact->mark_users_contact_invalid(
+ $e, $contact_type, {usr => $patron_id},
+ $addl_note, $penalty_ou, $e->requestor->id
+ );
+}
+
1;
--- /dev/null
+package OpenILS::Utils::BadContact;
+
+use warnings;
+use strict;
+
+use OpenILS::Event;
+use OpenILS::Utils::CStoreEditor;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+
+my $U = "OpenILS::Application::AppUtils";
+
+our $PENALTY_NAME_MAP = {
+ email => "INVALID_PATRON_EMAIL_ADDRESS",
+ day_phone => "INVALID_PATRON_DAY_PHONE",
+ evening_phone => "INVALID_PATRON_EVENING_PHONE",
+ other_phone => "INVALID_PATRON_OTHER_PHONE"
+};
+
+sub mark_users_contact_invalid {
+ my (
+ $class, $editor, $contact_type, $howfind,
+ $addl_note, $penalty_ou, $staff_id
+ ) = @_;
+
+ if (not ref $howfind eq "HASH") {
+ return new OpenILS::Event(
+ "BAD_PARAMS", note => "howfind argument must be hash"
+ );
+ }
+
+ if (not exists $PENALTY_NAME_MAP->{$contact_type}) {
+ return new OpenILS::Event(
+ "BAD_PARAMS", note => "contact_type argument invalid"
+ );
+ }
+
+ my $penalty_name = $PENALTY_NAME_MAP->{$contact_type};
+
+ # we can find either user-by-id, or user(s)-by-contact-info
+ my $users;
+
+ if (exists $howfind->{usr}) {
+ # just the specified patron
+
+ $users = $editor->search_actor_user({
+ id => $howfind->{usr}, deleted => "f"
+ }) or return $editor->die_event;
+
+ } elsif (exists $howfind->{$contact_type}) {
+ # all users with matching contact data
+
+ $users = $editor->search_actor_user({
+ $contact_type => $howfind->{$contact_type}, deleted => "f"
+ }) or return $editor->die_event;
+ }
+
+ # waste no more time if no users collected
+ return $editor->die_event(new OpenILS::Event("ACTOR_USER_NOT_FOUND"))
+ unless $users and @$users;
+
+ # we'll need this to apply user penalties
+ my $penalty = $editor->search_config_standing_penalty({
+ name => $penalty_name
+ });
+
+ if (not $penalty_ou) {
+ # Fallback to using top of org tree if no penalty_ou provided. This
+ # possibly makes sense in most cases anyway.
+
+ my $results = $editor->json_query({
+ "select" => {"aou" => ["id"]},
+ "from" => {"aou" => {}},
+ "where" => {"parent_ou" => undef}
+ }) or return $editor->die_event;
+
+ $penalty_ou = $results->[0]->{"id"};
+ }
+
+ return $editor->die_event unless $penalty and @$penalty;
+ $penalty = $penalty->[0];
+
+ my $last_xact_id_map = {};
+ my $clear_meth = "clear_$contact_type";
+
+ foreach (@$users) {
+ if ($editor->requestor) {
+ next unless $editor->allowed("UPDATE_USER", $_->home_ou);
+ }
+
+ my $usr_penalty = new Fieldmapper::actor::user_standing_penalty;
+ $usr_penalty->usr($_->id);
+ $usr_penalty->org_unit($penalty_ou);
+ $usr_penalty->standing_penalty($penalty->id);
+ $usr_penalty->staff($staff_id);
+ $usr_penalty->note($_->$contact_type);
+
+ $editor->create_actor_user_standing_penalty($usr_penalty) or
+ return $editor->die_event;
+
+ $_->$clear_meth;
+ $editor->update_actor_user($_) or return $editor->die_event;
+
+ my $updated = $editor->retrieve_actor_user($editor->data);
+ $last_xact_id_map->{$_->id} = $updated->last_xact_id;
+ }
+
+ $editor->commit or return $editor->die_event;
+
+ return new OpenILS::Event(
+ "SUCCESS", payload => {last_xact_id => $last_xact_id_map}
+ );
+}
+
+1;
INSERT INTO config.standing_penalty (id,name,label,block_list,staff_alert) VALUES (28,'STAFF_R','Alerting block on Renew','RENEW', TRUE);
INSERT INTO config.standing_penalty (id,name,label) VALUES (29,'INVALID_PATRON_ADDRESS','Patron has an invalid address');
INSERT INTO config.standing_penalty (id,name,label) VALUES (30,'PATRON_IN_COLLECTIONS','Patron has been referred to a collections agency');
+INSERT INTO config.standing_penalty (id, name, label, staff_alert) VALUES
+ (
+ 31,
+ 'INVALID_PATRON_EMAIL_ADDRESS',
+ oils_i18n_gettext(
+ 31,
+ 'Patron had an invalid email address',
+ 'csp',
+ 'label'
+ ),
+ TRUE
+ ),
+ (
+ 32,
+ 'INVALID_PATRON_DAY_PHONE',
+ oils_i18n_gettext(
+ 32,
+ 'Patron had an invalid daytime phone number',
+ 'csp',
+ 'label'
+ ),
+ TRUE
+ ),
+ (
+ 33,
+ 'INVALID_PATRON_EVENING_PHONE',
+ oils_i18n_gettext(
+ 33,
+ 'Patron had an invalid evening phone number',
+ 'csp',
+ 'label'
+ ),
+ TRUE
+ ),
+ (
+ 34,
+ 'INVALID_PATRON_OTHER_PHONE',
+ oils_i18n_gettext(
+ 34,
+ 'Patron had an invalid other phone number',
+ 'csp',
+ 'label'
+ ),
+ TRUE
+ );
+
SELECT SETVAL('config.standing_penalty_id_seq', 100);
--- /dev/null
+-- Evergreen DB patch XXXX.data.mark-email-and-phone-invalid.sql
+--
+-- Add org unit settings and standing penalty types to support
+-- the mark email/phone invalid features.
+--
+BEGIN;
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+
+INSERT INTO config.standing_penalty (id, name, label, staff_alert) VALUES
+ (
+ 31,
+ 'INVALID_PATRON_EMAIL_ADDRESS',
+ oils_i18n_gettext(
+ 31,
+ 'Patron had an invalid email address',
+ 'csp',
+ 'label'
+ ),
+ TRUE
+ ),
+ (
+ 32,
+ 'INVALID_PATRON_DAY_PHONE',
+ oils_i18n_gettext(
+ 32,
+ 'Patron had an invalid daytime phone number',
+ 'csp',
+ 'label'
+ ),
+ TRUE
+ ),
+ (
+ 33,
+ 'INVALID_PATRON_EVENING_PHONE',
+ oils_i18n_gettext(
+ 33,
+ 'Patron had an invalid evening phone number',
+ 'csp',
+ 'label'
+ ),
+ TRUE
+ ),
+ (
+ 34,
+ 'INVALID_PATRON_OTHER_PHONE',
+ oils_i18n_gettext(
+ 34,
+ 'Patron had an invalid other phone number',
+ 'csp',
+ 'label'
+ ),
+ TRUE
+ );
+
+
+COMMIT;
</div>
<div class='hidden'>
+ <div jsId="progressDialog" dojoType="openils.widget.ProgressDialog"></div>
<span id='true' style='color:green;'>✓</span>
<span id='false' style='color:red;'>✗</span>
<div dojoType='dijit.Dialog' jsId='allCardsDialog'>
if(isList && dojo.isArray(val))
testval = val[0];
if(e = openils.Event.parse(testval)) {
- if(eventOk) return e;
+ if(eventOk || e.textcode == 'SUCCESS') return e;
console.log(e.toString());
// session timed out. Stop propagation of requests queued by Util.onload
"ALL_CARDS_ACTIVE" : "Active",
"ALL_CARDS_PRIMARY" : "Primary",
"ALL_CARDS_CLOSE" : "Close",
- "ALL_CARDS_APPLY" : "Apply Changes"
+ "ALL_CARDS_APPLY" : "Apply Changes",
+ "INVALIDATE": "Invalidate"
}
dojo.require('openils.PermaCrud');
dojo.require('openils.widget.AutoGrid');
dojo.require('openils.widget.AutoFieldWidget');
+dojo.require('openils.widget.ProgressDialog');
dojo.require('dijit.form.CheckBox');
dojo.require('dijit.form.Button');
dojo.require('dojo.date');
saveCloneButton.attr('disabled', true);
}
+ uUpdateContactInvalidators();
lock_ready = true;
}
else if(fieldIdl.datatype == 'timestamp') {
ftd.appendChild(document.createTextNode(localeStrings.EXAMPLE + dojo.date.locale.format(new Date(1970,0,31),{selector: "date", fullYear: true, datePattern: (orgSettings['format.date'] ? orgSettings['format.date'] : null)})));
}
+
+ if (fmcls == "au" && (isphone || fmfield == "email")) {
+ var span = dojo.create(
+ "span", {
+ "className": "hidden",
+ "id": "wrap_invalidate_" + fmfield
+ }
+ );
+ uGenerateInvalidatorWidget(span, fmfield);
+ ftd.appendChild(span);
+ }
}
var span = document.createElement('span');
xulG.unlock_tab();
already_locked = false;
}
+ /* There's something that seems to just make the form reload
+ * on all saves, so this uUpdate... isn't needed here after
+ * all. */
+ //uUpdateContactInvalidators();
+
newPatron = openils.Util.readResponse(r);
if(newPatron) {
uEditUpdateUserSettings(newPatron.id());
);
}
+function uUpdateContactInvalidators() {
+ /* show invalidator buttons for fields that having anything in them */
+ ["email", "day_phone", "evening_phone", "other_phone"].forEach(
+ function(f) {
+ openils.Util[patron[f]() ? "show" : "hide"]("wrap_invalidate_" + f);
+ }
+ );
+}
+
+function uGenerateInvalidatorWidget(container_node, field) {
+ new dijit.form.Button(
+ {
+ "label": localeStrings.INVALIDATE,
+ "scrollOnFocus": false,
+ "onClick": function() {
+ progressDialog.show(true);
+ fieldmapper.standardRequest(
+ ["open-ils.actor", "open-ils.actor.invalidate." + field], {
+ "async": true,
+ "params": [openils.User.authtoken, patron.id()],
+ "oncomplete": function(r) {
+ progressDialog.hide();
+ // alerts on non-success event
+ var res = openils.Util.readResponse(r);
+
+ if (res.payload.last_xact_id) {
+ for (var id in res.payload.last_xact_id) {
+ if (patron.id() == id)
+ patron.last_xact_id(
+ res.payload.last_xact_id[id]
+ );
+ }
+
+ findWidget("au",field).widget.attr("value","");
+ openils.Util.hide(container_node);
+ }
+ }
+ }
+ );
+ }
+ }, dojo.create("span", null, container_node, "only")
+ );
+}
+
function uEditRemoveStage() {
var resp = fieldmapper.standardRequest(
['open-ils.actor', 'open-ils.actor.user.stage.delete'],
<!ENTITY staff.patron.display_overlay.has_overdues.value "(Has Overdues)">
<!ENTITY staff.patron.display_overlay.invalid_dob.value "(Invalid Date of Birth)">
<!ENTITY staff.patron.display_overlay.invalid_address.value "(Invalid Address)">
+<!ENTITY staff.patron.display_overlay.invalid_email.value "(Invalid Email - See Messages)">
+<!ENTITY staff.patron.display_overlay.invalid_phone.value "(Invalid Phone - See Messages)">
<!ENTITY staff.patron.display_overlay.exit.label "Exit">
<!ENTITY staff.patron.display_overlay.exit.accesskey "x">
<!ENTITY staff.patron.display_overlay.search_form.label "Search Form">
<label class="hideme overdues_indicator" value="&staff.patron.display_overlay.has_overdues.value;"/>
<label class="hideme invalid_dob_indicator" value="&staff.patron.display_overlay.invalid_dob.value;"/>
<label class="hideme invalid_address_indicator" value="&staff.patron.display_overlay.invalid_address.value;"/>
+ <label class="hideme invalid_email_indicator" value="&staff.patron.display_overlay.invalid_email.value;"/>
+ <label class="hideme invalid_phone_indicator" value="&staff.patron.display_overlay.invalid_phone.value;"/>
</hbox>
<vbox id="PatronNotNavBar" flex="1" class="my_bg">
<hbox id="left_deck_vbox" flex="1" oils_persist="height">
<label class="hideme overdues_indicator" value="&staff.patron.display_overlay.has_overdues.value;"/>
<label class="hideme invalid_dob_indicator" value="&staff.patron.display_overlay.invalid_dob.value;"/>
<label class="hideme invalid_address_indicator" value="&staff.patron.display_overlay.invalid_address.value;"/>
+ <label class="hideme invalid_email_indicator" value="&staff.patron.display_overlay.invalid_email.value;"/>
+ <label class="hideme invalid_phone_indicator" value="&staff.patron.display_overlay.invalid_phone.value;"/>
</hbox>
<hbox id="PatronNotNavBar" flex="1" class="my_bg">
<vbox id="left_deck_vbox" flex="1" oils_persist="width">
}
*/
document.getElementById('progress').hidden = true;
+
+ patron.util.set_penalty_css(xulG.patron, patron.display.w.document.documentElement);
}
);
document.getElementById('progress').hidden = false;
row_params.row.my.ausp = p;
row_params.row.my.csp = p.standing_penalty();
list.refresh_row( row_params );
+
+ patron.util.set_penalty_css(xulG.patron, patron.display.w.document.documentElement);
document.getElementById('progress').hidden = true;
} catch(E) {
alert(E);
}
if (--outstanding_requests==0) {
document.getElementById('progress').hidden = true;
+
+ patron.util.set_penalty_css(xulG.patron, patron.display.w.document.documentElement);
}
}
}(ids[i])
removeCSSClass(document.documentElement,'NO_PENALTIES');
removeCSSClass(document.documentElement,'ONE_PENALTY');
removeCSSClass(document.documentElement,'MULTIPLE_PENALTIES');
+ removeCSSClass(document.documentElement,'INVALID_PATRON_EMAIL_ADDRESS');
+ removeCSSClass(document.documentElement,'INVALID_PATRON_DAY_PHONE');
+ removeCSSClass(document.documentElement,'INVALID_PATRON_EVENING_PHONE');
+ removeCSSClass(document.documentElement,'INVALID_PATRON_OTHER_PHONE');
removeCSSClass(document.documentElement,'PATRON_HAS_ALERT');
removeCSSClass(document.documentElement,'PATRON_BARRED');
removeCSSClass(document.documentElement,'PATRON_INACTIVE');
data.last_patron = patron.id(); data.stash('last_patron');
*/
- var penalties = patron.standing_penalties();
- if (!penalties) { penalties = []; }
+ var penalties = patron.standing_penalties() || [];
+ penalties = penalties.filter(
+ function(p) {
+ return (!(p.isdeleted() || p.stop_date()));
+ }
+ );
for (var i = 0; i < penalties.length; i++) {
/* this comes from /opac/common/js/utils.js */
addCSSClass(document.documentElement,penalties[i].standing_penalty().name());
.PATRON_HAS_INVALID_ADDRESS label.invalid_address_indicator { display: inline; color: #CC3300 }
+.INVALID_PATRON_EMAIL_ADDRESS label.invalid_email_indicator { display: inline; color: #003399 }
+.INVALID_PATRON_DAY_PHONE label.invalid_phone_indicator { display: inline; color: #003399 }
+.INVALID_PATRON_EVENING_PHONE label.invalid_phone_indicator { display: inline; color: #003399 }
+.INVALID_PATRON_OTHER_PHONE label.invalid_phone_indicator { display: inline; color: #003399 }
+
.PATRON_HAS_ALERT .patronNameLarge { border: solid thick yellow; padding: 10px }
.PATRON_HAS_ALERT groupbox.alert { background-color: yellow; color: black; }
.PATRON_HAS_ALERT [id^=patron_alert] { background: yellow; color: black; }