Bug 28633: Add preferred name field to patrons

This patch adds a new field 'preferred_name' to the patron record.

On storage (creation or update) the preferred_name is set to the firstname if no
value is passed. Patron modifications will set the preferred name to the firstname if
the preferred_name field is hidden

With this patchset preferred_name will always be set - either to the firstname, or a specified value.

PatronAutoComplete/ysearch is updated to use 'preferred_name'

To test:
 1 - Apply patches
 2 - Update database and restart all, clear browser cache
 3 - Load a patron in staff module
 4 - Confirm you see and can add a preferred name
 5 - Confirm the preferred name and first name now displays on patron details
 6 - Remove first name from patron record and confirm it no longer shows
 7 - Edit sysprefs BorrowerMandatoryFields and BorrowerUnwantedFields to confirm you can make
     new field required or hidden
 8 - Sign in as patron to opac
 9 - Confirm preferred name shows
10 - Edit account on opac and confirm field is present
11 - Verify DefaultPatronSearchFields contains 'preferredname' if your pref had firstname
12 - Perform checkout and patron search using preferred_name, confirm patron is found
13 - Enable PatronAutoComplete system preference
14 - Type patron's surname into Checkout or patron search but don't hit enter
15 - Confirm patron is displayed with 'preferred_name' in the preview
16 - Set 'preferred_name' in all 'Unwanted' preferences
17 - Confirm editing a patron in staff interface sets both fields when firstname updated
18 - Confirm a patron modification sets both fields when firstname updated
19 - Create a patron / perform self registration and confirm both fields set when preferred_name is hidden
20 - Remove preferred_name from Unwanted prefs and confirm preferred_name is set to firstname if nothing passed

Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
This commit is contained in:
Nick Clemens 2022-01-03 15:24:01 +00:00 committed by Katrin Fischer
parent 131ff0f402
commit 2e83345479
Signed by: kfischer
GPG key ID: 0EF6E2C03357A834
19 changed files with 133 additions and 18 deletions

View file

@ -174,6 +174,7 @@ sub columns {
"password" => __("Password"),
"phone" => __("Primary phone"),
"phonepro" => __("Secondary phone"),
"preferred_name" => __("Preferred_name"),
"primary_contact_method" => __("Primary contact method"),
"privacy_guarantor_checkouts" => __("Show checkouts to guarantor"),
"privacy_guarantor_fines" => __("Show fines to guarantor"),

View file

@ -228,6 +228,11 @@ sub store {
$self->surname( uc( $self->surname ) )
if C4::Context->preference("uppercasesurnames");
# Add preferred name unless specified
unless ( $self->preferred_name ) {
$self->preferred_name( $self->firstname );
}
$self->relationship(undef) # We do not want to store an empty string in this field
if defined $self->relationship
and $self->relationship eq "";

View file

@ -103,6 +103,9 @@ sub approve {
delete $data->{$key};
}
my $unwanted = C4::Context->preference('PatronSelfModificationBorrowerUnwantedField');
$data->{preferred_name} = $data->{firstname} if ( $unwanted =~ 'preferred_name' && defined $data->{firstname} );
$patron->set($data);
# Take care of extended attributes

View file

@ -29,6 +29,11 @@
description: Case insensitive search on firstname
required: false
type: string
- name: preferred_name
in: query
description: Case insensitive search on preferred name
required: false
type: string
- name: title
in: query
description: Case insensitive search on title

View file

@ -20,6 +20,7 @@
var name;
var firstname = escape_str(patron.firstname);
var preferred_name = escape_str(patron.preferred_name);
var surname = escape_str(patron.surname);
if ( patron.middle_name != null && patron.middle_name != '' ) {
@ -30,10 +31,10 @@
firstname += ' (' + escape_str(patron.other_name) + ')';
}
if ( config && config.invert_name ) {
name = surname + ( firstname ? ', ' + firstname : '' );
name = surname + ( preferred_name ? ', ' + preferred_name : '' );
}
else {
name = firstname + ' ' + surname;
name = preferred_name + ' ' + surname;
}
if ( name.replace(' ', '').length == 0 ) {

View file

@ -535,7 +535,7 @@
}
[% CASE 'name-address' %]
{
"data": "me.surname:me.firstname:me.middle_name:me.othernames:me.street_number:me.address:me.address2:me.city:me.state:me.postal_code:me.country",
"data": "me.surname:me.preferred_name:me.firstname:me.middle_name:me.othernames:me.street_number:me.address:me.address2:me.city:me.state:me.postal_code:me.country",
"searchable": true,
"orderable": true,
"render": function( data, type, row, meta ) {
@ -560,7 +560,7 @@
}
[% CASE 'name' %]
{
"data": "me.surname:me.firstname:me.middle_name:me.othernames",
"data": "me.surname:me.preferred_name:me.firstname:me.middle_name:me.othernames",
"searchable": true,
"orderable": true,
"render": function( data, type, row, meta ) {

View file

@ -7,6 +7,7 @@
[%- SET data.surname = patron.surname -%]
[%- SET data.othernames = patron.othernames -%]
[%- SET data.firstname = patron.firstname -%]
[%- SET data.preferred_name = patron.preferred_name -%]
[%- SET data.middle_name = patron.middle_name -%]
[%- SET data.cardnumber = patron.cardnumber -%]
[%- SET data.borrowernumber = patron.borrowernumber -%]
@ -16,6 +17,7 @@
[%- SET data.surname = borrower.surname -%]
[%- SET data.othernames = borrower.othernames -%]
[%- SET data.firstname = borrower.firstname -%]
[%- SET data.preferred_name = borrower.preferred_name -%]
[%- SET data.middle_name = borrower.middle_name -%]
[%- SET data.cardnumber = borrower.cardnumber -%]
[%- SET data.borrowernumber = borrower.borrowernumber -%]
@ -25,6 +27,7 @@
[%- SET data.surname = surname -%]
[%- SET data.othernames = othernames -%]
[%- SET data.firstname = firstname -%]
[%- SET data.preferred_name = preferred_name -%]
[%- SET data.middle_name = middle_name -%]
[%- SET data.cardnumber = cardnumber -%]
[%- SET data.borrowernumber = borrowernumber -%]
@ -66,9 +69,9 @@
[%- IF data.category_type == 'I' -%]
[%- data.surname | html -%] [%- IF data.othernames -%] ([%- data.othernames | html -%])[%- END -%]
[%- ELSIF invert_name -%]
[%- data.title | $raw -%][%- data.surname | html -%][%- IF ( data.firstname ) -%], [% data.firstname | html -%][%- END -%][%- IF data.middle_name -%] [% data.middle_name | html -%][%- END -%][%- IF data.othernames -%] ([%- data.othernames | html -%])[%- END -%]
[%- data.title | $raw -%][%- data.surname | html -%][%- IF ( data.preferred_name ) -%], [% data.preferred_name | html -%][%- END -%][%- IF data.middle_name -%] [% data.middle_name | html -%][%- END -%][%- IF data.othernames -%] ([%- data.othernames | html -%])[%- END -%]
[%- ELSE -%]
[%- data.title | $raw -%][%- data.firstname | html %][%- IF data.middle_name -%] [% data.middle_name | html -%][%- END -%][%- IF data.othernames -%] ([%- data.othernames | html -%]) [%- END -%] [% data.surname | html -%]
[%- data.title | $raw -%][%- data.preferred_name | html %][%- IF data.middle_name -%] [% data.middle_name | html -%][%- END -%][%- IF data.othernames -%] ([%- data.othernames | html -%]) [%- END -%] [% data.surname | html -%]
[%- END -%]
[%- IF display_cardnumber AND data.cardnumber -%] ([%- data.cardnumber | html -%])[%- END -%]
[%- ELSIF display_cardnumber -%]

View file

@ -8,6 +8,7 @@
[%- CASE 'cardnumber' -%][% t("Card number") | html %]
[%- CASE 'surname' -%][% t("Surname") | html %]
[%- CASE 'firstname' -%][% t("First name") | html %]
[%- CASE 'preferred_name' -%][% t("Preferred name") | html %]
[%- CASE 'middle_name' -%][% t("Middle name") | html %]
[%- CASE 'title' -%][% t("Salutation") | html %]
[%- CASE 'othernames' -%][% t("Other name") | html %]
@ -86,7 +87,7 @@
<label for="searchfieldstype_filter">Search field:</label>
<select name="searchfieldstype" class="searchfieldstype_filter">
[% END %]
[% SET standard = Koha.Preference('DefaultPatronSearchFields') || 'firstname|middle_name|surname|othernames|cardnumber|userid' %]
[% SET standard = Koha.Preference('DefaultPatronSearchFields') || 'firstname|preferred_name|middle_name|surname|othernames|cardnumber|userid' %]
[%# Above is needed for adding fields from the DefaultPatronSearchFields preference to the dropdowns %]
[% default_fields = [ 'standard', 'surname', 'cardnumber', 'all_emails', 'borrowernumber', 'userid', 'all_phones', 'full_address', 'dateofbirth', 'sort1', 'sort2' ] %]
[% search_options = default_fields.merge(standard.split('\|')).unique %]

View file

@ -67,7 +67,7 @@ Patrons:
type: modalselect
source: borrowers
exclusions: anonymized|auth_method|autorenew_checkouts|date_renewed|dateenrolled|dateexpiry|lang|lastseen|login_attempts|overdrive_auth_token|password|password_expiration_date|primary_contact_method|gonenoaddress|lost|debarred|debarredcomment|branchcode|categorycode|flags|guarantorid|relationship|privacy|privacy_guarantor_checkouts|privacy_guarantor_fines|pronouns|secret|sms_provider_id|updated_on|checkprevcheckout
- "If empty Koha will default to \"firstname|middle_name|surname|othernames|cardnumber|userid\". Additional fields added to this preference will be added as search options in the dropdown menu on the patron search page."
- "If empty Koha will default to \"firstname|preferred_name|middle_name|surname|othernames|cardnumber|userid\". Additional fields added to this preference will be added as search options in the dropdown menu on the patron search page."
-
- pref: DefaultPatronSearchMethod
choices:

View file

@ -296,7 +296,7 @@ legend.collapsed i.fa.fa-caret-down::before {
[% END %]
[% IF ( step_1 ) %]
[% UNLESS notitle && nosurname && nofirstname && nomiddle_name && nodateofbirth && noinitials && noothernames &&nosex && nopronouns %]
[% UNLESS notitle && nosurname && nofirstname && nopreferred_name && nomiddle_name && nodateofbirth && noinitials && noothernames &&nosex && nopronouns %]
<fieldset class="rows" id="memberentry_identity">
<legend class="expanded" id="identity_lgd">
<i class="fa fa-caret-down" title="Collapse this section"></i>
@ -373,6 +373,21 @@ legend.collapsed i.fa.fa-caret-down::before {
[% END %]
</li>
[% END #/UNLESS nofirstname %]
[% UNLESS nopreferred_name %]
<li>
[% IF ( mandatorypreferred_name ) %]
<label for="preferred_name" class="required">
[% ELSE %]
<label for="preferred_name">
[% END %]
Preferred name:
</label>
<input type="text" id="preferred_name" name="preferred_name" size="20" value="[% borrower_data.preferred_name | html UNLESS opduplicate %]" />
[% IF ( mandatorypreferred_name ) %]
<span class="required">Required</span>
[% END %]
</li>
[% END #/UNLESS nopreferred_name %]
[% UNLESS nomiddle_name %]
<li>
[% IF ( mandatorymiddle_name ) %]

View file

@ -25,6 +25,7 @@
[% CASE 'branchcode' %]<span>Home library (branchcode)</span>
[% CASE 'surname' %]<span>Surname</span>
[% CASE 'firstname' %]<span>First name</span>
[% CASE 'preferred_name' %]<span>Preferred name</span>
[% CASE 'middle_name' %]<span>Middle name</span>
[% CASE 'title' %]<span>Title</span>
[% CASE 'othernames' %]<span>Other names</span>

View file

@ -135,6 +135,12 @@
<div class="rows">
<ol>
[% IF ( patron.preferred_name && patron.firstname ) %]
<li id="patron_first_name">
<span class="label patron_first_name">First name: </span>
[% patron.firstname | html %]
</li>
[% END %]
[% IF ( patron.phone ) %]
<li>
<span class="label">Primary phone: </span>
@ -225,7 +231,7 @@
[% IF logged_in_user.can_see_patron_infos( guarantee ) %]
<li><a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% guarantee.borrowernumber | uri %]">[% guarantee.firstname | html %] [% guarantee.surname | html %]</a></li>
[% ELSE %]
<li>[% guarantee.firstname | html %] [% guarantee.surname | html %]</li>
<li>[% guarantee.preferred_name | html %] [% guarantee.surname | html %]</li>
[% END %]
[% END %]
</ul>
@ -241,7 +247,7 @@
[% FOREACH gr IN guarantor_relationships %]
[% SET guarantor = gr.guarantor %]
[% IF logged_in_user.can_see_patron_infos( guarantor ) %]
<li><a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% guarantor.id | uri %]">[% guarantor.firstname | html %] [% guarantor.surname | html %][% IF gr.relationship %] ([% gr.relationship | html %])[% END %]</a></li>
<li><a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% guarantor.id | uri %]">[% guarantor.preferred_name | html %] [% guarantor.surname | html %][% IF gr.relationship %] ([% gr.relationship | html %])[% END %]</a></li>
[% END %]
[% END %]
[% IF patron.contactfirstname OR patron.contactname %]

View file

@ -709,7 +709,7 @@ function patron_autocomplete(node, options) {
(item.link ? '<a href="' + item.link + '">' : "<a>") +
(item.surname ? item.surname.escapeHtml() : "") +
", " +
(item.firstname ? item.firstname.escapeHtml() : "") +
(item.preferred_name ? item.preferred_name.escapeHtml() : item.firstname ? item.firstname.escapeHtml() : "") +
" " +
(item.middle_name ? item.middle_name.escapeHtml() : "") +
" " +

View file

@ -4,5 +4,5 @@
[%- IF patron.title -%]
<span class="patron-title">[% patron.title | html %]</span>
[%- END -%]
[% patron.firstname | html %] [% patron.middle_name | html %] [% patron.surname | html %]
[% patron.preferred_name | html %] [% patron.middle_name | html %] [% patron.surname | html %]
[%- END -%]

View file

@ -194,7 +194,7 @@
Guaranteed by
[% FOREACH gr IN patron.guarantor_relationships %]
[% SET g = gr.guarantor %]
[% g.firstname | html %] [% g.middle_name | html %] [% g.surname | html %]
[% g.preferred_name | html %] [% g.middle_name | html %] [% g.surname | html %]
[%- IF ! loop.last %], [% END %]
[% END %]
</span>
@ -212,7 +212,7 @@
<form method="post" action="/cgi-bin/koha/opac-memberentry.pl" id="memberentry-form" autocomplete="off">
[% INCLUDE 'csrf-token.inc' %]
[% FOREACH field = ['streetnumber' 'streettype' 'cardnumber' 'branchcode' 'categorycode' 'title' 'surname' 'firstname' 'middle_name' 'dateofbirth' 'initials' 'pronouns' 'othernames' 'address' 'address2' 'city' 'state' 'zipcode' 'country' 'phone' 'phonepro' 'mobile' 'email' 'emailpro' 'fax' 'B_streettype' 'B_streetnumber' 'B_address' 'B_address2' 'B_city' 'B_state' 'B_zipcode' 'B_country' 'B_phone' 'B_email' 'contactnote' 'altcontactsurname' 'altcontactfirstname' 'altcontactaddress1' 'altcontactaddress2' 'altcontactaddress3' 'altcontactstate' 'altcontactzipcode' 'altcontactcountry' 'altcontactphone' 'password' 'lang' ] %]
[% FOREACH field = ['streetnumber' 'streettype' 'cardnumber' 'branchcode' 'categorycode' 'title' 'surname' 'firstname' 'preferred_name' 'middle_name' 'dateofbirth' 'initials' 'pronouns' 'othernames' 'address' 'address2' 'city' 'state' 'zipcode' 'country' 'phone' 'phonepro' 'mobile' 'email' 'emailpro' 'fax' 'B_streettype' 'B_streetnumber' 'B_address' 'B_address2' 'B_city' 'B_state' 'B_zipcode' 'B_country' 'B_phone' 'B_email' 'contactnote' 'altcontactsurname' 'altcontactfirstname' 'altcontactaddress1' 'altcontactaddress2' 'altcontactaddress3' 'altcontactstate' 'altcontactzipcode' 'altcontactcountry' 'altcontactphone' 'password' 'lang' ] %]
[% IF mandatory.defined( field ) %]
[% SET required.$field = 'required' %]
[% END %]
@ -328,7 +328,7 @@
[% END # / defined 'branchcode' %]
[%# Following on one line for translatability %]
[% UNLESS hidden.defined('title') && hidden.defined('surname') && hidden.defined('firstname') && hidden.defined('middle_name') && hidden.defined('dateofbirth') && hidden.defined('initials') && hidden.defined('pronouns') && hidden.defined('othernames') && hidden.defined('sex') %]
[% UNLESS hidden.defined('title') && hidden.defined('surname') && hidden.defined('firstname') && hidden.defined('preferred_name') && hidden.defined('middle_name') && hidden.defined('dateofbirth') && hidden.defined('initials') && hidden.defined('pronouns') && hidden.defined('othernames') && hidden.defined('sex') %]
<div class="row">
<div class="col">
<fieldset class="rows" id="memberentry_identity">
@ -380,6 +380,15 @@
</li>
[% END %]
[% UNLESS hidden.defined('preferred_name') %]
<li>
<label for="borrower_preferred_name" class="[% required.preferred_name | html %]">Preferred name:</label>
<input type="text" id="borrower_preferred_name" name="borrower_preferred_name" value="[% borrower.preferred_name | html %]" class="[% required.preferred_name | html %]" />
<div class="required_label [% required.preferred_name | html %]">Required</div>
</li>
[% END %]
[% UNLESS hidden.defined('dateofbirth') %]
<li>
<label for="borrower_dateofbirth" class="[% required.dateofbirth | html %]">Date of birth:</label>

View file

@ -525,6 +525,9 @@ if ((!$nok) and $nodouble and ($op eq 'cud-insert' or $op eq 'cud-save')){
$patron = Koha::Patrons->find( $borrowernumber );
# Ensure preferred name is set even if not passed because of BorrowerUnwantedFields
$newdata{preferred_name} = undef unless defined $newdata{preferred_name};
if ($NoUpdateEmail) {
delete $newdata{'email'};
delete $newdata{'emailpro'};

View file

@ -330,6 +330,9 @@ elsif ( $op eq 'cud-update' ) {
$template->param( op => 'edit' );
}
else {
# If preferred name is not included but firstname is then set preferred_name to firstname
$borrower{preferred_name} = $borrower{firstname}
if defined $borrower{firstname} && !defined $borrower{preferred_name};
my %borrower_changes = DelUnchangedFields( $borrowernumber, %borrower );
$borrower_changes{'changed_fields'} = join ',', keys %borrower_changes;
my $extended_attributes_changes = FilterUnchangedAttributes( $borrowernumber, $attributes );

View file

@ -2688,3 +2688,61 @@ subtest 'Scrub the note fields' => sub {
$schema->storage->txn_rollback;
};
subtest 'preferred_name' => sub {
plan tests => 6;
$schema->storage->txn_begin;
my $tmp_patron = $builder->build_object( { class => 'Koha::Patrons' } );
my $patron_data = $tmp_patron->unblessed;
$tmp_patron->delete;
delete $patron_data->{borrowernumber};
delete $patron_data->{preferred_name};
my $patron = Koha::Patron->new(
{
%$patron_data,
}
)->store;
is( $patron->preferred_name, $patron->firstname, "Preferred name set to first name on creation when not defined");
$patron->delete;
$patron_data->{preferred_name} = "";
$patron = Koha::Patron->new(
{
%$patron_data,
}
)->store;
is( $patron->preferred_name, $patron->firstname, "Preferred name set to first name on creation when empty string");
$patron->delete;
$patron_data->{preferred_name} = "Preferred";
$patron_data->{firstname} = "Not Preferred";
$patron = Koha::Patron->new(
{
%$patron_data,
}
)->store;
is( $patron->preferred_name, "Preferred", "Preferred name set when passed on creation");
$patron->preferred_name("")->store;
is( $patron->preferred_name, $patron->firstname, "Preferred name set to first name on update when empty string");
$patron->preferred_name(undef)->store();
is( $patron->preferred_name, $patron->firstname, "Preferred name set to first name on update when undef");
$patron->preferred_name("Preferred again")->store();
is( $patron->preferred_name, "Preferred again", "Preferred name set on update when passed");
};

View file

@ -81,7 +81,7 @@ subtest 'list() tests' => sub {
subtest 'librarian access tests' => sub {
plan tests => 21;
plan tests => 22;
$schema->storage->txn_begin;
@ -264,7 +264,8 @@ subtest 'get() tests' => sub {
$schema->storage->txn_rollback;
subtest 'librarian access tests' => sub {
plan tests => 8;
plan tests => 9;
$schema->storage->txn_begin;