<field reporter:label="OPAC/Staff Client Holds Alias" name="alias" reporter:datatype="text"/>
<field reporter:label="Juvenile" name="juvenile" reporter:datatype="bool"/>
<field reporter:label="Record Last Update Time" name="last_update_time" reporter:datatype="timestamp"/>
+ <field reporter:label="Preferred Prefix" name="pref_prefix" reporter:datatype="text"/>
+ <field reporter:label="Preferred First Name" name="pref_first_given_name" reporter:datatype="text"/>
+ <field reporter:label="Preferred Middle Name" name="pref_second_given_name" reporter:datatype="text"/>
+ <field reporter:label="Preferred Last Name" name="pref_family_name" reporter:datatype="text"/>
+ <field reporter:label="Preferred Suffix" name="pref_suffix" reporter:datatype="text"/>
+ <field reporter:label="Name Keywords" name="name_keywords" reporter:datatype="text"/>
<field reporter:label="Additional Permission Groups" name="groups" oils_persist:virtual="true" reporter:datatype="link"/>
<field reporter:label="Is Deleted" name="deleted" reporter:datatype="bool"/>
<field reporter:label="User Notes" name="notes" oils_persist:virtual="true" reporter:datatype="link"/>
<field reporter:label="Date of Birth" name="dob" reporter:datatype="text"/>
<field reporter:label="Complete" name="complete" reporter:datatype="bool"/>
<field reporter:label="Requesting User" name="requesting_usr" reporter:datatype="link"/>
+ <field reporter:label="Preferred First Name" name="pref_first_given_name" reporter:datatype="text"/>
+ <field reporter:label="Preferred Middle Name" name="pref_second_given_name" reporter:datatype="text"/>
+ <field reporter:label="Preferred Last Name" name="pref_family_name" reporter:datatype="text"/>
</fields>
<links>
<link field="requesting_usr" reltype="has_a" key="id" map="" class="au"/>
my $dob;
my @dobv;
- if ($diacritic_insensitive) {
- $usr = join ' AND ', map { "evergreen.unaccent_and_squash(CAST($_ AS text)) ~ ?" } grep { ''.$$search{$_}{group} eq '0' } keys %$search;
- @usrv = map { "^" . _prepare_name_argument($$search{$_}{value}) } grep { ''.$$search{$_}{group} eq '0' } keys %$search;
+ # Compile the WHERE component of the actor.usr fields.
+ # When a name field is encountered, search both the name field and
+ # the alternate version of the name field.
+ my @name_fields = qw/prefix first_given_name second_given_name family_name suffix/;
+ my @usr_where_parts;
- } else {
- $usr = join ' AND ', map { "evergreen.lowercase(CAST($_ AS text)) ~ ?" } grep { ''.$$search{$_}{group} eq '0' } keys %$search;
- @usrv = map { "^" . _clean_regex_chars($$search{$_}{value}) } grep { ''.$$search{$_}{group} eq '0' } keys %$search;
+ my @usr_fields = grep { ''.$$search{$_}{group} eq '0' } keys %$search;
+ for my $usr_field (@usr_fields) {
+
+ # sprintf template
+ my $where_func = $diacritic_insensitive ?
+ "evergreen.unaccent_and_squash(CAST(%s AS text)) ~ ?" :
+ "evergreen.lowercase(CAST(%s AS text)) ~ ?";
+
+ my $val = $diacritic_insensitive ?
+ "^" . _prepare_name_argument($$search{$usr_field}{value}) :
+ "^" . _clean_regex_chars($$search{$usr_field}{value});
+
+ if (grep {$_ eq $usr_field} @name_fields) {
+ # When searching a name field include an OR search
+ # on the alternate version of the same field.
+
+ push(@usr_where_parts, sprintf(
+ "($where_func OR $where_func)", $usr_field, "pref_$usr_field")
+ );
+
+ # search main field and alt name field with same value.
+ push(@usrv, $val);
+ push(@usrv, $val);
+
+ } else {
+
+ push(@usr_where_parts, sprintf($where_func, $usr_field));
+ push(@usrv, $val);
+ }
}
+ $usr = join ' AND ', @usr_where_parts;
+
while (($key, $value) = each (%$search)) {
if($$search{$key}{group} eq '4') {
my $tval = $key;
$ident = '(' . join(' OR ', @is) . ')';
}
+ # name keywords search
my $name = '';
my @ns;
my @namev;
- if (0 && $nv) {
- for my $n ( qw/first_given_name second_given_name family_name/ ) {
- if ($diacritic_insensitive) {
- push @ns, "evergreen.unaccent_and_squash($n) ~ ?";
- } else {
- push @ns, "evergreen.lowercase($n) ~ ?";
- }
- push @namev, "^$nv";
- }
- $name = '(' . join(' OR ', @ns) . ')';
+ if ($nv) {
+ $name = "name_kw_tsvector @@ to_tsquery(?)";
+
+ # Remove characters that to_tsquery might treat as operators.
+ # Note using plainto_tsquery to ignore operators won't let us
+ # also do prefix matching.
+ $nv =~ s/[^\w\s\.\-']//g;
+
+ my @parts = split(' ', $nv);
+
+ # tsquery on multiple names joined w/ '&'
+ # Adding :* gives us prefix matching
+ push @namev, join(' & ', map { "$_:*" } @parts);
}
my $profile = '';
second_given_name TEXT,
family_name TEXT NOT NULL,
suffix TEXT,
+ pref_prefix TEXT,
+ pref_first_given_name TEXT,
+ pref_second_given_name TEXT,
+ pref_family_name TEXT,
+ pref_suffix TEXT,
+ name_keywords TEXT,
+ name_kw_tsvector TSVECTOR,
alias TEXT,
day_phone TEXT,
evening_phone TEXT,
CREATE INDEX actor_usr_family_name_unaccent_idx ON actor.usr (evergreen.unaccent_and_squash(family_name));
CREATE INDEX actor_usr_usrname_unaccent_idx ON actor.usr (evergreen.unaccent_and_squash(usrname));
+CREATE INDEX actor_usr_pref_first_given_name_idx ON actor.usr (evergreen.lowercase(pref_first_given_name));
+CREATE INDEX actor_usr_pref_second_given_name_idx ON actor.usr (evergreen.lowercase(pref_second_given_name));
+CREATE INDEX actor_usr_pref_family_name_idx ON actor.usr (evergreen.lowercase(pref_family_name));
+CREATE INDEX actor_usr_pref_first_given_name_unaccent_idx ON actor.usr (evergreen.unaccent_and_squash(pref_first_given_name));
+CREATE INDEX actor_usr_pref_second_given_name_unaccent_idx ON actor.usr (evergreen.unaccent_and_squash(pref_second_given_name));
+CREATE INDEX actor_usr_pref_family_name_unaccent_idx ON actor.usr (evergreen.unaccent_and_squash(pref_family_name));
+
CREATE INDEX actor_usr_usrname_idx ON actor.usr (evergreen.lowercase(usrname));
CREATE INDEX actor_usr_email_idx ON actor.usr (evergreen.lowercase(email));
CREATE RULE protect_user_delete AS ON DELETE TO actor.usr DO INSTEAD UPDATE actor.usr SET deleted = TRUE WHERE OLD.id = actor.usr.id;
+CREATE OR REPLACE FUNCTION actor.user_ingest_name_keywords()
+ RETURNS TRIGGER AS $func$
+BEGIN
+ NEW.name_kw_tsvector := TO_TSVECTOR(
+ COALESCE(NEW.prefix, '') || ' ' ||
+ COALESCE(NEW.first_given_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.first_given_name), '') || ' ' ||
+ COALESCE(NEW.second_given_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.second_given_name), '') || ' ' ||
+ COALESCE(NEW.family_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.family_name), '') || ' ' ||
+ COALESCE(NEW.suffix, '') || ' ' ||
+ COALESCE(NEW.pref_prefix, '') || ' ' ||
+ COALESCE(NEW.pref_first_given_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.pref_first_given_name), '') || ' ' ||
+ COALESCE(NEW.pref_second_given_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.pref_second_given_name), '') || ' ' ||
+ COALESCE(NEW.pref_family_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.pref_family_name), '') || ' ' ||
+ COALESCE(NEW.pref_suffix, '') || ' ' ||
+ COALESCE(NEW.name_keywords, '')
+ );
+ RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+-- Add after the batch upate above to avoid duplicate updates.
+CREATE TRIGGER user_ingest_name_keywords_tgr
+ BEFORE INSERT OR UPDATE ON actor.usr
+ FOR EACH ROW EXECUTE PROCEDURE actor.user_ingest_name_keywords();
+
CREATE TABLE actor.usr_note (
id BIGSERIAL PRIMARY KEY,
usr BIGINT NOT NULL REFERENCES actor.usr ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
first_given_name TEXT,
second_given_name TEXT,
family_name TEXT,
+ pref_first_given_name TEXT,
+ pref_second_given_name TEXT,
+ pref_family_name TEXT,
day_phone TEXT,
evening_phone TEXT,
home_ou INT DEFAULT 2,
-- do nothing
END;
+ -- propagate preferred name values from the source user to the
+ -- destination user, but only when values are not being replaced.
+ WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr)
+ UPDATE actor.usr SET
+ pref_prefix =
+ COALESCE(pref_prefix, (SELECT pref_prefix FROM susr)),
+ pref_first_given_name =
+ COALESCE(pref_first_given_name, (SELECT pref_first_given_name FROM susr)),
+ pref_second_given_name =
+ COALESCE(pref_second_given_name, (SELECT pref_second_given_name FROM susr)),
+ pref_family_name =
+ COALESCE(pref_family_name, (SELECT pref_family_name FROM susr)),
+ pref_suffix =
+ COALESCE(pref_suffix, (SELECT pref_suffix FROM susr)),
+ name_keywords =
+ COALESCE(name_keywords, '') || ' ' ||
+ COALESCE((SELECT name_keywords FROM susr), '')
+ WHERE id = dest_usr;
+
-- Finally, delete the source user
DELETE FROM actor.usr WHERE id = src_usr;
END;
$$ LANGUAGE plpgsql;
+
+
COMMENT ON FUNCTION actor.usr_merge(INT, INT, BOOLEAN, BOOLEAN, BOOLEAN) IS $$
Merges all user date from src_usr to dest_usr. When collisions occur,
keep dest_usr's data and delete src_usr's data.
--- /dev/null
+
+/**
+update patron merge
+*/
+
+BEGIN;
+
+ALTER TABLE actor.usr
+ ADD COLUMN pref_prefix TEXT,
+ ADD COLUMN pref_first_given_name TEXT,
+ ADD COLUMN pref_second_given_name TEXT,
+ ADD COLUMN pref_family_name TEXT,
+ ADD COLUMN pref_suffix TEXT,
+ ADD COLUMN name_keywords TEXT,
+ ADD COLUMN name_kw_tsvector TSVECTOR;
+
+ALTER TABLE staging.user_stage
+ ADD COLUMN pref_first_given_name TEXT,
+ ADD COLUMN pref_second_given_name TEXT,
+ ADD COLUMN pref_family_name TEXT;
+
+CREATE INDEX actor_usr_pref_first_given_name_idx
+ ON actor.usr (evergreen.lowercase(pref_first_given_name));
+CREATE INDEX actor_usr_pref_second_given_name_idx
+ ON actor.usr (evergreen.lowercase(pref_second_given_name));
+CREATE INDEX actor_usr_pref_family_name_idx
+ ON actor.usr (evergreen.lowercase(pref_family_name));
+CREATE INDEX actor_usr_pref_first_given_name_unaccent_idx
+ ON actor.usr (evergreen.unaccent_and_squash(pref_first_given_name));
+CREATE INDEX actor_usr_pref_second_given_name_unaccent_idx
+ ON actor.usr (evergreen.unaccent_and_squash(pref_second_given_name));
+CREATE INDEX actor_usr_pref_family_name_unaccent_idx
+ ON actor.usr (evergreen.unaccent_and_squash(pref_family_name));
+
+-- Update keyword indexes for existing patrons
+
+UPDATE actor.usr SET name_kw_tsvector =
+ TO_TSVECTOR(
+ COALESCE(prefix, '') || ' ' ||
+ COALESCE(first_given_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(first_given_name), '') || ' ' ||
+ COALESCE(second_given_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(second_given_name), '') || ' ' ||
+ COALESCE(family_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(family_name), '') || ' ' ||
+ COALESCE(suffix, '')
+ );
+
+CREATE OR REPLACE FUNCTION actor.user_ingest_name_keywords()
+ RETURNS TRIGGER AS $func$
+BEGIN
+ NEW.name_kw_tsvector := TO_TSVECTOR(
+ COALESCE(NEW.prefix, '') || ' ' ||
+ COALESCE(NEW.first_given_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.first_given_name), '') || ' ' ||
+ COALESCE(NEW.second_given_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.second_given_name), '') || ' ' ||
+ COALESCE(NEW.family_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.family_name), '') || ' ' ||
+ COALESCE(NEW.suffix, '') || ' ' ||
+ COALESCE(NEW.pref_prefix, '') || ' ' ||
+ COALESCE(NEW.pref_first_given_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.pref_first_given_name), '') || ' ' ||
+ COALESCE(NEW.pref_second_given_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.pref_second_given_name), '') || ' ' ||
+ COALESCE(NEW.pref_family_name, '') || ' ' ||
+ COALESCE(evergreen.unaccent_and_squash(NEW.pref_family_name), '') || ' ' ||
+ COALESCE(NEW.pref_suffix, '') || ' ' ||
+ COALESCE(NEW.name_keywords, '')
+ );
+ RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+-- Add after the batch upate above to avoid duplicate updates.
+CREATE TRIGGER user_ingest_name_keywords_tgr
+ BEFORE INSERT OR UPDATE ON actor.usr
+ FOR EACH ROW EXECUTE PROCEDURE actor.user_ingest_name_keywords();
+
+
+-- merge pref names from source user to target user, except when
+-- clobbering existing pref names.
+CREATE OR REPLACE FUNCTION actor.usr_merge(src_usr INT, dest_usr INT,
+ del_addrs BOOLEAN, del_cards BOOLEAN, deactivate_cards BOOLEAN )
+ RETURNS VOID AS $$
+DECLARE
+ suffix TEXT;
+ bucket_row RECORD;
+ picklist_row RECORD;
+ queue_row RECORD;
+ folder_row RECORD;
+BEGIN
+
+ -- do some initial cleanup
+ UPDATE actor.usr SET card = NULL WHERE id = src_usr;
+ UPDATE actor.usr SET mailing_address = NULL WHERE id = src_usr;
+ UPDATE actor.usr SET billing_address = NULL WHERE id = src_usr;
+
+ -- actor.*
+ IF del_cards THEN
+ DELETE FROM actor.card where usr = src_usr;
+ ELSE
+ IF deactivate_cards THEN
+ UPDATE actor.card SET active = 'f' WHERE usr = src_usr;
+ END IF;
+ UPDATE actor.card SET usr = dest_usr WHERE usr = src_usr;
+ END IF;
+
+
+ IF del_addrs THEN
+ DELETE FROM actor.usr_address WHERE usr = src_usr;
+ ELSE
+ UPDATE actor.usr_address SET usr = dest_usr WHERE usr = src_usr;
+ END IF;
+
+ UPDATE actor.usr_note SET usr = dest_usr WHERE usr = src_usr;
+ -- dupes are technically OK in actor.usr_standing_penalty, should manually delete them...
+ UPDATE actor.usr_standing_penalty SET usr = dest_usr WHERE usr = src_usr;
+ PERFORM actor.usr_merge_rows('actor.usr_org_unit_opt_in', 'usr', src_usr, dest_usr);
+ PERFORM actor.usr_merge_rows('actor.usr_setting', 'usr', src_usr, dest_usr);
+
+ -- permission.*
+ PERFORM actor.usr_merge_rows('permission.usr_perm_map', 'usr', src_usr, dest_usr);
+ PERFORM actor.usr_merge_rows('permission.usr_object_perm_map', 'usr', src_usr, dest_usr);
+ PERFORM actor.usr_merge_rows('permission.usr_grp_map', 'usr', src_usr, dest_usr);
+ PERFORM actor.usr_merge_rows('permission.usr_work_ou_map', 'usr', src_usr, dest_usr);
+
+
+ -- container.*
+
+ -- For each *_bucket table: transfer every bucket belonging to src_usr
+ -- into the custody of dest_usr.
+ --
+ -- In order to avoid colliding with an existing bucket owned by
+ -- the destination user, append the source user's id (in parenthesese)
+ -- to the name. If you still get a collision, add successive
+ -- spaces to the name and keep trying until you succeed.
+ --
+ FOR bucket_row in
+ SELECT id, name
+ FROM container.biblio_record_entry_bucket
+ WHERE owner = src_usr
+ LOOP
+ suffix := ' (' || src_usr || ')';
+ LOOP
+ BEGIN
+ UPDATE container.biblio_record_entry_bucket
+ SET owner = dest_usr, name = name || suffix
+ WHERE id = bucket_row.id;
+ EXCEPTION WHEN unique_violation THEN
+ suffix := suffix || ' ';
+ CONTINUE;
+ END;
+ EXIT;
+ END LOOP;
+ END LOOP;
+
+ FOR bucket_row in
+ SELECT id, name
+ FROM container.call_number_bucket
+ WHERE owner = src_usr
+ LOOP
+ suffix := ' (' || src_usr || ')';
+ LOOP
+ BEGIN
+ UPDATE container.call_number_bucket
+ SET owner = dest_usr, name = name || suffix
+ WHERE id = bucket_row.id;
+ EXCEPTION WHEN unique_violation THEN
+ suffix := suffix || ' ';
+ CONTINUE;
+ END;
+ EXIT;
+ END LOOP;
+ END LOOP;
+
+ FOR bucket_row in
+ SELECT id, name
+ FROM container.copy_bucket
+ WHERE owner = src_usr
+ LOOP
+ suffix := ' (' || src_usr || ')';
+ LOOP
+ BEGIN
+ UPDATE container.copy_bucket
+ SET owner = dest_usr, name = name || suffix
+ WHERE id = bucket_row.id;
+ EXCEPTION WHEN unique_violation THEN
+ suffix := suffix || ' ';
+ CONTINUE;
+ END;
+ EXIT;
+ END LOOP;
+ END LOOP;
+
+ FOR bucket_row in
+ SELECT id, name
+ FROM container.user_bucket
+ WHERE owner = src_usr
+ LOOP
+ suffix := ' (' || src_usr || ')';
+ LOOP
+ BEGIN
+ UPDATE container.user_bucket
+ SET owner = dest_usr, name = name || suffix
+ WHERE id = bucket_row.id;
+ EXCEPTION WHEN unique_violation THEN
+ suffix := suffix || ' ';
+ CONTINUE;
+ END;
+ EXIT;
+ END LOOP;
+ END LOOP;
+
+ UPDATE container.user_bucket_item SET target_user = dest_usr WHERE target_user = src_usr;
+
+ -- vandelay.*
+ -- transfer queues the same way we transfer buckets (see above)
+ FOR queue_row in
+ SELECT id, name
+ FROM vandelay.queue
+ WHERE owner = src_usr
+ LOOP
+ suffix := ' (' || src_usr || ')';
+ LOOP
+ BEGIN
+ UPDATE vandelay.queue
+ SET owner = dest_usr, name = name || suffix
+ WHERE id = queue_row.id;
+ EXCEPTION WHEN unique_violation THEN
+ suffix := suffix || ' ';
+ CONTINUE;
+ END;
+ EXIT;
+ END LOOP;
+ END LOOP;
+
+ -- money.*
+ PERFORM actor.usr_merge_rows('money.collections_tracker', 'usr', src_usr, dest_usr);
+ PERFORM actor.usr_merge_rows('money.collections_tracker', 'collector', src_usr, dest_usr);
+ UPDATE money.billable_xact SET usr = dest_usr WHERE usr = src_usr;
+ UPDATE money.billing SET voider = dest_usr WHERE voider = src_usr;
+ UPDATE money.bnm_payment SET accepting_usr = dest_usr WHERE accepting_usr = src_usr;
+
+ -- action.*
+ UPDATE action.circulation SET usr = dest_usr WHERE usr = src_usr;
+ UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
+ UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
+ UPDATE action.usr_circ_history SET usr = dest_usr WHERE usr = src_usr;
+
+ UPDATE action.hold_request SET usr = dest_usr WHERE usr = src_usr;
+ UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
+ UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
+ UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
+
+ UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
+ UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
+ UPDATE action.non_cataloged_circulation SET patron = dest_usr WHERE patron = src_usr;
+ UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
+ UPDATE action.survey_response SET usr = dest_usr WHERE usr = src_usr;
+
+ -- acq.*
+ UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
+ UPDATE acq.fund_transfer SET transfer_user = dest_usr WHERE transfer_user = src_usr;
+
+ -- transfer picklists the same way we transfer buckets (see above)
+ FOR picklist_row in
+ SELECT id, name
+ FROM acq.picklist
+ WHERE owner = src_usr
+ LOOP
+ suffix := ' (' || src_usr || ')';
+ LOOP
+ BEGIN
+ UPDATE acq.picklist
+ SET owner = dest_usr, name = name || suffix
+ WHERE id = picklist_row.id;
+ EXCEPTION WHEN unique_violation THEN
+ suffix := suffix || ' ';
+ CONTINUE;
+ END;
+ EXIT;
+ END LOOP;
+ END LOOP;
+
+ UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
+ UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
+ UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
+ UPDATE acq.provider_note SET creator = dest_usr WHERE creator = src_usr;
+ UPDATE acq.provider_note SET editor = dest_usr WHERE editor = src_usr;
+ UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
+ UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
+ UPDATE acq.lineitem_usr_attr_definition SET usr = dest_usr WHERE usr = src_usr;
+
+ -- asset.*
+ UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
+ UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
+ UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
+ UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
+ UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
+ UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
+
+ -- serial.*
+ UPDATE serial.record_entry SET creator = dest_usr WHERE creator = src_usr;
+ UPDATE serial.record_entry SET editor = dest_usr WHERE editor = src_usr;
+
+ -- reporter.*
+ -- It's not uncommon to define the reporter schema in a replica
+ -- DB only, so don't assume these tables exist in the write DB.
+ BEGIN
+ UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
+ EXCEPTION WHEN undefined_table THEN
+ -- do nothing
+ END;
+ BEGIN
+ UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
+ EXCEPTION WHEN undefined_table THEN
+ -- do nothing
+ END;
+ BEGIN
+ UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
+ EXCEPTION WHEN undefined_table THEN
+ -- do nothing
+ END;
+ BEGIN
+ -- transfer folders the same way we transfer buckets (see above)
+ FOR folder_row in
+ SELECT id, name
+ FROM reporter.template_folder
+ WHERE owner = src_usr
+ LOOP
+ suffix := ' (' || src_usr || ')';
+ LOOP
+ BEGIN
+ UPDATE reporter.template_folder
+ SET owner = dest_usr, name = name || suffix
+ WHERE id = folder_row.id;
+ EXCEPTION WHEN unique_violation THEN
+ suffix := suffix || ' ';
+ CONTINUE;
+ END;
+ EXIT;
+ END LOOP;
+ END LOOP;
+ EXCEPTION WHEN undefined_table THEN
+ -- do nothing
+ END;
+ BEGIN
+ -- transfer folders the same way we transfer buckets (see above)
+ FOR folder_row in
+ SELECT id, name
+ FROM reporter.report_folder
+ WHERE owner = src_usr
+ LOOP
+ suffix := ' (' || src_usr || ')';
+ LOOP
+ BEGIN
+ UPDATE reporter.report_folder
+ SET owner = dest_usr, name = name || suffix
+ WHERE id = folder_row.id;
+ EXCEPTION WHEN unique_violation THEN
+ suffix := suffix || ' ';
+ CONTINUE;
+ END;
+ EXIT;
+ END LOOP;
+ END LOOP;
+ EXCEPTION WHEN undefined_table THEN
+ -- do nothing
+ END;
+ BEGIN
+ -- transfer folders the same way we transfer buckets (see above)
+ FOR folder_row in
+ SELECT id, name
+ FROM reporter.output_folder
+ WHERE owner = src_usr
+ LOOP
+ suffix := ' (' || src_usr || ')';
+ LOOP
+ BEGIN
+ UPDATE reporter.output_folder
+ SET owner = dest_usr, name = name || suffix
+ WHERE id = folder_row.id;
+ EXCEPTION WHEN unique_violation THEN
+ suffix := suffix || ' ';
+ CONTINUE;
+ END;
+ EXIT;
+ END LOOP;
+ END LOOP;
+ EXCEPTION WHEN undefined_table THEN
+ -- do nothing
+ END;
+
+ -- propagate preferred name values from the source user to the
+ -- destination user, but only when values are not being replaced.
+ WITH susr AS (SELECT * FROM actor.usr WHERE id = src_usr)
+ UPDATE actor.usr SET
+ pref_prefix =
+ COALESCE(pref_prefix, (SELECT pref_prefix FROM susr)),
+ pref_first_given_name =
+ COALESCE(pref_first_given_name, (SELECT pref_first_given_name FROM susr)),
+ pref_second_given_name =
+ COALESCE(pref_second_given_name, (SELECT pref_second_given_name FROM susr)),
+ pref_family_name =
+ COALESCE(pref_family_name, (SELECT pref_family_name FROM susr)),
+ pref_suffix =
+ COALESCE(pref_suffix, (SELECT pref_suffix FROM susr)),
+ name_keywords =
+ COALESCE(name_keywords, '') || ' ' ||
+ COALESCE((SELECT name_keywords FROM susr), '')
+ WHERE id = dest_usr;
+
+ -- Finally, delete the source user
+ DELETE FROM actor.usr WHERE id = src_usr;
+
+END;
+$$ LANGUAGE plpgsql;
+
+
+COMMIT;
+
<td class='light_border'>[% l(
HUMAN_NAME_FORMAT,
- ctx.user.prefix, ctx.user.first_given_name,
- ctx.user.second_given_name, ctx.user.family_name,
- ctx.user.suffix
+ (ctx.user.pref_prefix || ctx.user.prefix),
+ (ctx.user.pref_first_given_name || ctx.user.first_given_name),
+ (ctx.user.pref_second_given_name || ctx.user.second_given_name),
+ (ctx.user.pref_family_name || ctx.user.family_name),
+ (ctx.user.pref_suffix || ctx.user.suffix)
) | html %]</td>
<td></td>
<div id="dash_wrapper">
<div id="dash_identity">
<span id="dash_user">
- [% l('[_1] [_2]', ctx.user.first_given_name, ctx.user.family_name) | html %]
+ [% l('[_1] [_2]',
+ (ctx.user.pref_first_given_name || ctx.user.first_given_name),
+ (ctx.user.pref_family_name || ctx.user.family_name)
+ ) | html %]
</span>
<span class="dash_divider">|</span>
<span class="dash_account_buttons">
{class => 'stgu', name = 'first_given_name', label => l('First Name')},
{class => 'stgu', name = 'second_given_name', label => l('Middle Name')},
{class => 'stgu', name = 'family_name', label => l('Last Name')},
+ {class => 'stgu', name = 'pref_first_given_name', label => l('Preferred First Name')},
+ {class => 'stgu', name = 'pref_second_given_name', label => l('Preferred Middle Name')},
+ {class => 'stgu', name = 'pref_family_name', label => l('Preferred Last Name')},
{class => 'stgma', name = 'street1', label => l('Street Address')},
{class => 'stgma', name = 'street2', label => l('Street Address (2)')},
{class => 'stgma', name = 'city', label => l('City')},
FOR field_def IN register_fields;
fclass = field_def.class;
fname = field_def.name;
+ orig_name = fname;
+
field_path = fclass _ "." _ fname;
+ IF fname.match('^pref_');
+ # Preferred name fields adopt most visibility, etc.
+ # settings from the primary name counterparts.
+ fname = fname.remove('^pref_');
+ END;
+
show = ctx.register.settings.$fclass.$fname.show;
require = ctx.register.settings.$fclass.$fname.require;
example = ctx.register.settings.$fclass.$fname.example;
invalid_require = ctx.register.invalid.$fclass.$fname.require;
invalid_regex = ctx.register.invalid.$fclass.$fname.regex;
+ IF orig_name.match('^pref_');
+ show = show || require;
+ require = 0; # pref name values never required
+ END;
+
NEXT UNLESS require OR show;
%]
<tr>
</div>
</div>
-<!-- PREFIX -->
-
-<div class="row reg-field-row" ng-show="show_field('au.prefix')">
- [% draw_field_label('au', 'prefix') %]
- [% draw_form_input('au', 'prefix'); %]
- <div class="col-md-6 patron-reg-example">
- [% draw_example_text('au', 'prefix') %]
+<div class="row reg-field-row">
+ <div class="col-md-6">
+ <ul class="nav nav-pills nav-pills-like-tabs">
+ <li ng-class="{active : name_tab == 'primary'}">
+ <a ng-click="name_tab='primary'">[% l('Primary Name') %]</a>
+ </li>
+ <li ng-class="{active : name_tab == 'preferred'}">
+ <a ng-click="name_tab='preferred'">[% l('Preferred Name') %]</a>
+ </li>
+ </ul>
</div>
</div>
-<!-- FIRST_GIVEN_NAME -->
+<div ng-show="name_tab == 'primary'">
-<div class="row reg-field-row" ng-show="show_field('au.first_given_name')">
- [% draw_field_label('au', 'first_given_name') %]
- [% draw_form_input('au', 'first_given_name'); %]
- <div class="col-md-6 patron-reg-example">
- [% draw_example_text('au', 'first_given_name') %]
+ <!-- PREFIX -->
+
+ <div class="row reg-field-row" ng-show="show_field('au.prefix')">
+ [% draw_field_label('au', 'prefix') %]
+ [% draw_form_input('au', 'prefix'); %]
+ <div class="col-md-6 patron-reg-example">
+ [% draw_example_text('au', 'prefix') %]
+ </div>
</div>
-</div>
-<!-- SECOND_GIVEN_NAME -->
+ <!-- FIRST_GIVEN_NAME -->
-<div class="row reg-field-row" ng-show="show_field('au.second_given_name')">
- [% draw_field_label('au', 'second_given_name') %]
- [% draw_form_input('au', 'second_given_name'); %]
- <div class="col-md-6 patron-reg-example">
- [% draw_example_text('au', 'second_given_name') %]
+ <div class="row reg-field-row" ng-show="show_field('au.first_given_name')">
+ [% draw_field_label('au', 'first_given_name') %]
+ [% draw_form_input('au', 'first_given_name'); %]
+ <div class="col-md-6 patron-reg-example">
+ [% draw_example_text('au', 'first_given_name') %]
+ </div>
</div>
-</div>
-<!-- FAMILY_NAME -->
+ <!-- SECOND_GIVEN_NAME -->
-<div class="row reg-field-row" ng-show="show_field('au.family_name')">
- [% draw_field_label('au', 'family_name') %]
- [% draw_form_input('au', 'family_name'); %]
- <div class="col-md-6 patron-reg-example">
- [% draw_example_text('au', 'family_name') %]
+ <div class="row reg-field-row" ng-show="show_field('au.second_given_name')">
+ [% draw_field_label('au', 'second_given_name') %]
+ [% draw_form_input('au', 'second_given_name'); %]
+ <div class="col-md-6 patron-reg-example">
+ [% draw_example_text('au', 'second_given_name') %]
+ </div>
+ </div>
+
+ <!-- FAMILY_NAME -->
+
+ <div class="row reg-field-row" ng-show="show_field('au.family_name')">
+ [% draw_field_label('au', 'family_name') %]
+ [% draw_form_input('au', 'family_name'); %]
+ <div class="col-md-6 patron-reg-example">
+ [% draw_example_text('au', 'family_name') %]
+ </div>
+ </div>
+
+ <!-- SUFFIX -->
+
+ <div class="row reg-field-row" ng-show="show_field('au.suffix')">
+ [% draw_field_label('au', 'suffix') %]
+ [% draw_form_input('au', 'suffix'); %]
+ <div class="col-md-6 patron-reg-example">
+ [% draw_example_text('au', 'suffix') %]
+ </div>
+ </div>
+</div> <!-- ng-show == primary -->
+
+<div ng-show="name_tab == 'preferred'" class="patron-reg-pref-names">
+
+ <!-- PREFIX -->
+
+ <div class="row reg-field-row" ng-show="show_field('au.pref_prefix')">
+ [% draw_field_label('au', 'pref_prefix') %]
+ [% draw_form_input('au', 'pref_prefix'); %]
+ <div class="col-md-6 patron-reg-example">
+ [% draw_example_text('au', 'pref_prefix') %]
+ </div>
+ </div>
+
+ <!-- FIRST_GIVEN_NAME -->
+
+ <div class="row reg-field-row" ng-show="show_field('au.pref_first_given_name')">
+ [% draw_field_label('au', 'pref_first_given_name') %]
+ [% draw_form_input('au', 'pref_first_given_name'); %]
+ <div class="col-md-6 patron-reg-example">
+ [% draw_example_text('au', 'pref_first_given_name') %]
+ </div>
+ </div>
+
+ <!-- SECOND_GIVEN_NAME -->
+
+ <div class="row reg-field-row" ng-show="show_field('au.pref_second_given_name')">
+ [% draw_field_label('au', 'pref_second_given_name') %]
+ [% draw_form_input('au', 'pref_second_given_name'); %]
+ <div class="col-md-6 patron-reg-example">
+ [% draw_example_text('au', 'pref_second_given_name') %]
+ </div>
+ </div>
+
+ <!-- FAMILY_NAME -->
+
+ <div class="row reg-field-row" ng-show="show_field('au.pref_family_name')">
+ [% draw_field_label('au', 'pref_family_name') %]
+ [% draw_form_input('au', 'pref_family_name'); %]
+ <div class="col-md-6 patron-reg-example">
+ [% draw_example_text('au', 'pref_family_name') %]
+ </div>
</div>
-</div>
-<!-- SUFFIX -->
+ <!-- SUFFIX -->
+
+ <div class="row reg-field-row" ng-show="show_field('au.pref_suffix')">
+ [% draw_field_label('au', 'pref_suffix') %]
+ [% draw_form_input('au', 'pref_suffix'); %]
+ <div class="col-md-6 patron-reg-example">
+ [% draw_example_text('au', 'pref_suffix') %]
+ </div>
+ </div>
+</div> <!-- ng-show == preferred -->
-<div class="row reg-field-row" ng-show="show_field('au.suffix')">
- [% draw_field_label('au', 'suffix') %]
- [% draw_form_input('au', 'suffix'); %]
+<!-- indicate bottom of name tabs -->
+<div class="row reg-field-row">
+ <div class="col-md-6"><hr class="patron-reg-names-separator"/></div>
+</div>
+
+<div class="row reg-field-row" ng-show="show_field('au.name_keywords')">
+ [% draw_field_label('au', 'name_keywords') %]
+ <div class="col-md-3 reg-field-input">
+ <textarea
+ class="form-control"
+ ng-model="patron.name_keywords"
+ ng-pattern="field_pattern('au', 'name_keywords')"
+ ng-change="field_modified()"
+ ng-blur="handle_field_changed(patron, 'name_keywords')">
+ </textarea>
+ </div>
<div class="col-md-6 patron-reg-example">
- [% draw_example_text('au', 'suffix') %]
+ [% draw_example_text('au', 'name_keywords') %]
</div>
</div>
<div ng-cloak class="patron-summary-grid-wrapper">
<div ng-show="patron()" id="patron-summary-grid">
+ <div class="row patron-summary-pref-name"
+ ng-if="patron().pref_family_name() || patron().pref_first_given_name() || patron().second_given_name()">
+ <div class="col-md-12">
+ [% l('[_1], [_2] [_3] (Preferred)',
+ '{{patron().pref_family_name() || patron().family_name()}}',
+ '{{patron().pref_first_given_name() || patron().first_given_name()}}',
+ '{{patron().pref_second_given_name() || patron().second_given_name()}}')
+ %]
+ </div>
+ </div>
+ <div ng-if="patron().name_keywords()" class="row patron-summary-pref-name">
+ <div class="col-md-12">
+ <a title="{{patron().name_keywords()}}">[% l('Name Keywords') %]</a>
+ </div>
+ </div>
<div class="row"
ng-class="{'patron-summary-divider' : !$index}"
ng-repeat="penalty in alert_penalties()">
<div class="col-md-5">{{map.stat_cat().name()}}</div>
<div class="col-md-7">{{map.stat_cat_entry()}}</div>
</div>
+ <div class="row" ng-if="patron().name_keywords()">
+ <div class="col-md-5">[% l('Name Keywords') %]</div>
+ <div class="col-md-7">{{patron().name_keywords()}}</div>
+ </div>
</div>
<div class="row" ng-repeat="addr in patron().addresses()">
.patron-summary-act-link {font-size: .8em;}
.patron-summary-has-notes:hover, .patron-summary-has-notes:visited{ text-decoration: none; }
+.patron-summary-pref-name {
+ padding-left: 6px;
+ color: #5cb85c;
+}
+
/* FIXME: use .barcode instead */
#patron-checkout-barcode,
#patron-renewal-barcode,
color: red;
}
+.patron-reg-names-separator {
+ margin-top: 3px;
+ margin-bottom: 3px;
+}
+
+.patron-reg-pref-names {
+ background-color: rgb(215, 215, 215);
+}
+
/* -- end patron registration -- */
[%#
* patron - has several fields from the patron object, including a financial summary
+ * prefix
* first_given_name
* second_given_name
* family_name
* suffix
+ * pref_prefix
+ * pref_first_given_name
+ * pref_second_given_name
+ * pref_family_name
+ * pref_suffix
* card.barcode
* money_summary.balance_owed - current balance
* money_summary.total_paid - payments made on outstanding fines/fees
ng-model="searchArgs.second_given_name" placeholder="[% l('Middle Name') %]"/>
</div>
- <div class="col-md-2" ng-mouseover="setLastFormElement()">
- <input type="submit" class="btn btn-primary" value="[% l('Search') %]"/>
+ <div class="col-md-2">
+ <input type="text" class="form-control" ng-model="searchArgs.name"
+ placeholder="[% l('Name Keywords') %]" title="[% l('Name Keywords') %]"/>
</div>
<div class="col-md-2" ng-mouseover="setLastFormElement()">
- <input type="reset" class="btn btn-primary" ng-click="clearForm()"
- value="[% l('Clear Form') %]"/>
+ <input type="submit" class="btn btn-primary" value="[% l('Search') %]"/>
</div>
+
<div class="col-md-2">
<button class="btn btn-default" ng-click="applyShowExtras($event, true)"
ng-mouseover="setLastFormElement()"
<input type="text" class="form-control"
ng-model="searchArgs.email" placeholder="[% l('Email') %]"/>
</div>
- <div class="col-md-2">
- <input type="text" class="form-control"
- ng-model="searchArgs.ident" placeholder="[% l('Identification') %]"/>
+ <div class="col-md-2" ng-mouseover="setLastFormElement()">
+ <input type="reset" class="btn btn-primary" ng-click="clearForm()"
+ value="[% l('Clear Form') %]"/>
</div>
</div>
<div class="form-group" ng-show="showExtras">
<div class="col-md-2">
<input type="text" class="form-control"
- ng-model="searchArgs.id" placeholder="[% l('Database ID') %]"/>
+ ng-model="searchArgs.ident" placeholder="[% l('Identification') %]"/>
</div>
+
<div class="col-md-2">
<input type="text" class="form-control"
ng-model="searchArgs.phone" placeholder="[% l('Phone') %]"/>
<input type="text" class="form-control" ng-model="searchArgs.dob_day"
placeholder="[% l('DOB Day') %]" title="[% l('DOB Day') %]"/>
</div>
+ <div class="col-md-2">
+ <input type="text" class="form-control"
+ ng-model="searchArgs.id" placeholder="[% l('Database ID') %]"/>
+ </div>
</div>
</form>
</div>
second_given_name : 'Martin',
family_name : 'Jones',
suffix : 'III',
+ pref_first_given_name : 'Martin',
+ pref_second_given_name : 'Joe',
+ pref_family_name : 'Smith',
card : {
barcode : '30393830393'
},
second_given_name : cusr.second_given_name(),
family_name : cusr.family_name(),
suffix : cusr.suffix(),
+ pref_prefix : cusr.pref_prefix(),
+ pref_first_given_name : cusr.pref_first_given_name(),
+ pref_secondg_given_name : cusr.second_given_name(),
+ pref_family_name : cusr.pref_family_name(),
+ suffix : cusr.suffix(),
card : { barcode : cusr.card().barcode() },
money_summary : patronSvc.patron_stats.fines,
expire_date : cusr.expire_date(),
// has at the currently selected patron home org unit.
$scope.perms = {};
+ $scope.name_tab = 'primary';
+
if (!$scope.edit_passthru) {
// in edit more, scope.edit_passthru is delivered to us by
// the enclosing controller. In register mode, there is
'au.passwd' : 3,
'au.first_given_name' : 3,
'au.family_name' : 3,
+ 'au.pref_first_given_name' : 2,
+ 'au.pref_family_name' : 2,
'au.ident_type' : 3,
'au.ident_type2' : 2,
'au.home_ou' : 3,
'aua.valid' : 2,
'aua.within_city_limits' : 2,
'stat_cats' : 1,
- 'surveys' : 1
+ 'surveys' : 1,
+ 'au.name_keywords': 1
};
// Returns true if the selected field should be visible
if (field_visibility[field_key] == undefined) {
// compile and cache the visibility for the selected field
- var req_set = 'ui.patron.edit.' + field_key + '.require';
- var sho_set = 'ui.patron.edit.' + field_key + '.show';
- var sug_set = 'ui.patron.edit.' + field_key + '.suggest';
+ // The preferred name fields use the primary name field settings
+ var org_key = field_key;
+ var alt_name = false;
+ if (field_key.match(/^au.alt_/)) {
+ alt_name = true;
+ org_key = field_key.slice(7);
+ }
+
+ var req_set = 'ui.patron.edit.' + org_key + '.require';
+ var sho_set = 'ui.patron.edit.' + org_key + '.show';
+ var sug_set = 'ui.patron.edit.' + org_key + '.suggest';
if ($scope.org_settings[req_set]) {
- field_visibility[field_key] = 3;
+ if (alt_name) {
+ // Avoid requiring alt name fields when primary
+ // name fields are required.
+ field_visibility[field_key] = 2;
+ } else {
+ field_visibility[field_key] = 3;
+ }
} else if ($scope.org_settings[sho_set]) {
field_visibility[field_key] = 2;
search.home_ou = args.home_ou.id(); // passed separately
} else if (key == 'inactive') {
search.inactive = val;
+ } else if (key == 'name') { // name keywords search
+ search.name = {value: val};
} else {
search[key] = {value : val, group : 0};
}