Koha-clms/opac/opac-memberentry.pl

759 lines
28 KiB
Perl
Executable File

#!/usr/bin/perl
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <https://www.gnu.org/licenses>.
use Modern::Perl;
use CGI qw ( -utf8 );
use Digest::MD5 qw( md5_base64 md5_hex );
use JSON qw( to_json );
use List::MoreUtils qw( any each_array uniq );
use String::Random qw( random_string );
use Try::Tiny;
use C4::Auth qw( get_template_and_user );
use C4::Output qw( output_html_with_http_headers );
use C4::Context;
use C4::Letters qw( GetPreparedLetter EnqueueLetter SendQueuedMessages );
use C4::Form::MessagingPreferences;
use C4::Members::Messaging qw( SetMessagingPreferencesFromDefaults );
use Koha::AuthUtils;
use Koha::Patrons;
use Koha::Patron::Consent;
use Koha::Patron::Modification;
use Koha::Patron::Modifications;
use C4::Scrubber;
use Koha::DateUtils qw( dt_from_string );
use Koha::Email;
use Koha::Libraries;
use Koha::Patron::Attribute::Types;
use Koha::Patron::Attributes;
use Koha::Patron::Images;
use Koha::Patron::Categories;
use Koha::Policy::Patrons::Cardnumber;
use Koha::AuthorisedValues;
my $cgi = CGI->new;
my $dbh = C4::Context->dbh;
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
{
template_name => "opac-memberentry.tt",
type => "opac",
query => $cgi,
authnotrequired => 1,
}
);
my $op = $cgi->param('op') || q{};
if ( $borrowernumber && ( $op eq 'cud-create' || $op eq 'new' ) ) {
print $cgi->redirect("/cgi-bin/koha/opac-main.pl");
exit;
}
if ( $op eq q{} ) {
if ($borrowernumber) {
$op = 'edit';
} else {
$op = 'new';
}
}
my $PatronSelfRegistrationDefaultCategory = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
my $defaultCategory = Koha::Patron::Categories->find($PatronSelfRegistrationDefaultCategory);
# Having a valid PatronSelfRegistrationDefaultCategory is mandatory
if ( !C4::Context->preference('PatronSelfRegistration') && !$borrowernumber
|| ( ( $op eq 'new' || $op eq 'cud-create' ) && !$defaultCategory ) )
{
print $cgi->redirect("/cgi-bin/koha/opac-main.pl");
exit;
}
my $mandatory = GetMandatoryFields($op);
my $params = {};
if ( $op eq 'cud-create' || $op eq 'new' ) {
my @PatronSelfRegistrationLibraryList = split '\|', C4::Context->preference('PatronSelfRegistrationLibraryList');
$params = { branchcode => { -in => \@PatronSelfRegistrationLibraryList } }
if @PatronSelfRegistrationLibraryList;
}
my $libraries = Koha::Libraries->search( $params, { order_by => ['branchname'] } );
my ( $min, $max ) = Koha::Policy::Patrons::Cardnumber->get_valid_length();
if ( defined $min ) {
$template->param(
minlength_cardnumber => $min,
maxlength_cardnumber => $max
);
}
my $translated_languages = C4::Languages::getTranslatedLanguages( 'opac', C4::Context->preference('opacthemes') );
$template->param(
op => $op,
hidden => GetHiddenFields( $mandatory, $op ),
mandatory => $mandatory,
libraries => $libraries,
OPACPatronDetails => C4::Context->preference('OPACPatronDetails'),
defaultCategory => $defaultCategory,
languages => $translated_languages,
);
my $attributes = ParsePatronAttributes( $borrowernumber, $cgi );
my $conflicting_attribute = 0;
foreach my $attr (@$attributes) {
my $attribute = Koha::Patron::Attribute->new($attr);
if ( !$attribute->unique_ok ) {
my $attr_type = Koha::Patron::Attribute::Types->find( $attr->{code} );
$template->param(
extended_unique_id_failed_code => $attr->{code},
extended_unique_id_failed_value => $attr->{attribute},
extended_unique_id_failed_description => $attr_type->description,
);
$conflicting_attribute = 1;
}
}
if ( $op eq 'cud-create' ) {
my %borrower = ParseCgiForBorrower( { cgi => $cgi, op => $op } );
%borrower = DelEmptyFields(%borrower);
$borrower{categorycode} ||= $PatronSelfRegistrationDefaultCategory;
my @empty_mandatory_fields =
( CheckMandatoryFields( \%borrower, $op ), CheckMandatoryAttributes( \%borrower, $attributes ) );
my $invalidformfields = CheckForInvalidFields( { borrower => \%borrower, context => 'create' } );
my $match_result = Koha::Patrons->check_for_existing_matches( \%borrower );
delete $borrower{'password2'};
my $is_cardnumber_valid;
if ( !grep { $_ eq 'cardnumber' } @empty_mandatory_fields ) {
# No point in checking the cardnumber if it's missing and mandatory, it'll just generate a
# spurious length warning.
my $patron = Koha::Patrons->find( $borrower{borrowernumber} );
$is_cardnumber_valid = Koha::Policy::Patrons::Cardnumber->is_valid( $borrower{cardnumber}, $patron );
unless ($is_cardnumber_valid) {
for my $m ( @{ $is_cardnumber_valid->messages } ) {
my $message = $m->message;
if ( $message eq 'already_exists' ) {
$template->param( cardnumber_already_exists => 1 );
} elsif ( $message eq 'invalid_length' ) {
$template->param( cardnumber_wrong_length => 1 );
}
}
}
}
if ( @empty_mandatory_fields
|| @$invalidformfields
|| !$is_cardnumber_valid
|| $conflicting_attribute
|| $match_result->{duplicate_found} )
{
$template->param(
empty_mandatory_fields => \@empty_mandatory_fields,
invalid_form_fields => $invalidformfields,
borrower => \%borrower,
duplicate_found => $match_result->{duplicate_found}
);
$template->param( patron_attribute_classes => GeneratePatronAttributesForm( undef, $attributes ) );
} elsif ( md5_base64( uc( $cgi->param('captcha') ) ) ne $cgi->param('captcha_digest') ) {
$template->param(
failed_captcha => 1,
borrower => \%borrower
);
$template->param( patron_attribute_classes => GeneratePatronAttributesForm( undef, $attributes ) );
} elsif ( !$libraries->find( $borrower{branchcode} ) ) {
die "Branchcode not allowed"; # They hack the form
} else {
if ( C4::Context->preference('PatronSelfRegistrationVerifyByEmail') ) {
( $template, $borrowernumber, $cookie ) = get_template_and_user(
{
template_name => "opac-registration-email-sent.tt",
type => "opac",
query => $cgi,
authnotrequired => 1,
}
);
$template->param( 'email' => $borrower{'email'} );
my $verification_token = md5_hex( time() . {} . rand() . {} . $$ );
while ( Koha::Patron::Modifications->search( { verification_token => $verification_token } )->count() ) {
$verification_token = md5_hex( time() . {} . rand() . {} . $$ );
}
$borrower{password} =
Koha::AuthUtils::generate_password( Koha::Patron::Categories->find( $borrower{categorycode} ) )
unless $borrower{password};
$borrower{verification_token} = $verification_token;
$borrower{extended_attributes} = to_json($attributes);
$borrower{borrowernumber} = 0; # prevent warn Missing value for PK column
Koha::Patron::Modification->new( \%borrower )->store();
#Send verification email
my $letter = C4::Letters::GetPreparedLetter(
module => 'members',
letter_code => 'OPAC_REG_VERIFY',
lang => 'default', # Patron does not have a preferred language defined yet
tables => {
borrower_modifications => $verification_token,
},
);
my $message_id = C4::Letters::EnqueueLetter(
{
letter => $letter,
message_transport_type => 'email',
to_address => $borrower{'email'},
from_address => C4::Context->preference('KohaAdminEmailAddress'),
}
);
C4::Letters::SendQueuedMessages( { message_id => $message_id } ) if $message_id;
} else {
$borrower{password} ||=
Koha::AuthUtils::generate_password( Koha::Patron::Categories->find( $borrower{categorycode} ) );
my $consent_dt = delete $borrower{gdpr_proc_consent};
my $patron;
try {
$patron = Koha::Patron->new( \%borrower )->store;
Koha::Patron::Consent->new(
{ borrowernumber => $patron->borrowernumber, type => 'GDPR_PROCESSING', given_on => $consent_dt } )
->store
if $patron && $consent_dt;
C4::Members::Messaging::SetMessagingPreferencesFromDefaults(
{ borrowernumber => $patron->borrowernumber, categorycode => $patron->categorycode } );
} catch {
my $type = ref($_);
my $info = "$_";
$template->param( error_type => $type, error_info => $info );
$template->param( borrower => \%borrower );
};
( $template, $borrowernumber, $cookie ) = get_template_and_user(
{
template_name => "opac-registration-confirmation.tt",
type => "opac",
query => $cgi,
authnotrequired => 1,
}
) if $patron;
if ($patron) {
$patron->extended_attributes->filter_by_branch_limitations->delete;
$patron->extended_attributes($attributes);
$template->param( password_cleartext => $patron->plain_text_password );
$template->param( borrower => $patron->unblessed );
$template->param( confirmed => 1 );
# If 'AutoEmailNewUser' syspref is on, email user their account details from the 'notice' that matches the user's branchcode.
if ( C4::Context->preference("AutoEmailNewUser") ) {
#look for defined primary email address, if blank - attempt to use borr.email and borr.emailpro instead
my $emailaddr = $patron->notice_email_address;
# if we manage to find a valid email address, send notice
if ($emailaddr) {
eval {
my $letter = GetPreparedLetter(
module => 'members',
letter_code => 'WELCOME',
branchcode => $patron->branchcode,
lang => $patron->lang || 'default',
tables => {
'branches' => $patron->branchcode,
'borrowers' => $patron->borrowernumber,
},
want_librarian => 1,
) or return;
my $message_id = EnqueueLetter(
{
letter => $letter,
borrowernumber => $patron->id,
to_address => $emailaddr,
message_transport_type => 'email'
}
);
SendQueuedMessages( { message_id => $message_id } ) if $message_id;
};
}
}
# Notify library of new patron registration
my $notify_library = C4::Context->preference('EmailPatronRegistrations');
if ($notify_library) {
$patron->notify_library_of_registration($notify_library);
}
}
}
}
} elsif ( $op eq 'cud-update' ) {
my $borrower = Koha::Patrons->find($borrowernumber)->unblessed;
my %borrower = ParseCgiForBorrower( { cgi => $cgi, op => $op } );
$borrower{borrowernumber} = $borrowernumber;
$borrower{categorycode} = $borrower->{categorycode};
my @empty_mandatory_fields = grep { $_ ne 'password' } # password is not required when editing personal details
( CheckMandatoryFields( \%borrower, $op ), CheckMandatoryAttributes( \%borrower, $attributes ) );
my $invalidformfields = CheckForInvalidFields( { borrower => \%borrower, context => 'update' } );
# Send back the data to the template
%borrower = ( %$borrower, %borrower );
if ( @empty_mandatory_fields || @$invalidformfields ) {
$template->param(
empty_mandatory_fields => \@empty_mandatory_fields,
invalid_form_fields => $invalidformfields,
borrower => \%borrower,
);
$template->param( patron_attribute_classes => GeneratePatronAttributesForm( $borrowernumber, $attributes ) );
$template->param( op => 'edit' );
} else {
my $patron = Koha::Patrons->find($borrowernumber);
# 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 = $patron->identify_updated_extended_attributes($attributes);
if ( $borrower_changes{'changed_fields'} || scalar @{$extended_attributes_changes} > 0 ) {
( $template, $borrowernumber, $cookie ) = get_template_and_user(
{
template_name => "opac-memberentry-update-submitted.tt",
type => "opac",
query => $cgi,
authnotrequired => 1,
}
);
$borrower_changes{borrowernumber} = $borrowernumber;
$borrower_changes{extended_attributes} = to_json($extended_attributes_changes);
$patron->request_modification( \%borrower_changes );
$template->param( borrower => $patron->unblessed );
} else {
my $patron = Koha::Patrons->find($borrowernumber);
$template->param(
op => 'edit',
nochanges => 1,
borrower => $patron->unblessed,
patron_attribute_classes => GeneratePatronAttributesForm( $borrowernumber, $attributes ),
);
}
}
} elsif ( $op eq 'edit' ) { #Display logged in borrower's data
my $patron = Koha::Patrons->find($borrowernumber);
my $borrower = $patron->unblessed;
my $self_renewal = $cgi->param('self_renewal') || 0;
$template->param(
borrower => $borrower,
hidden => GetHiddenFields( $mandatory, 'edit' ),
self_renewal => $self_renewal
);
if ( C4::Context->preference('OPACpatronimages') ) {
$template->param( display_patron_image => 1 ) if $patron->image;
}
$template->param( patron_attribute_classes => GeneratePatronAttributesForm($borrowernumber) );
my $patron_pending_modification =
Koha::Patron::Modifications->search( { borrowernumber => $patron->borrowernumber } )->next;
if ($patron_pending_modification) {
$template->param( patron_pending_modification => $patron_pending_modification );
}
} else {
# Render self-registration page
$template->param( patron_attribute_classes => GeneratePatronAttributesForm() );
}
my $captcha = random_string("CCCCC");
my $patron_param = Koha::Patrons->find($borrowernumber);
$template->param( has_guarantor_flag => $patron_param->guarantor_relationships->guarantors->_resultset->count )
if $patron_param;
$template->param(
captcha => $captcha,
captcha_digest => md5_base64($captcha),
patron => $patron_param
);
output_html_with_http_headers $cgi, $cookie, $template->output, undef, { force_no_caching => 1 };
sub GetHiddenFields {
my ( $mandatory, $op ) = @_;
my %hidden_fields;
my $BorrowerUnwantedField =
$op eq 'edit' || $op eq 'cud-update'
? C4::Context->preference("PatronSelfModificationBorrowerUnwantedField")
: C4::Context->preference("PatronSelfRegistrationBorrowerUnwantedField");
my @fields = split( /\|/, $BorrowerUnwantedField || q|| );
foreach (@fields) {
next unless m/\w/o;
#Don't hide mandatory fields
next if $mandatory->{$_};
$hidden_fields{$_} = 1;
}
return \%hidden_fields;
}
sub GetMandatoryFields {
my ($op) = @_;
my %mandatory_fields;
my $BorrowerMandatoryField =
$op eq 'edit' || $op eq 'cud-update'
? C4::Context->preference("PatronSelfModificationMandatoryField")
: C4::Context->preference("PatronSelfRegistrationBorrowerMandatoryField");
my @fields = split( /\|/, $BorrowerMandatoryField );
push @fields, 'gdpr_proc_consent' if C4::Context->preference('PrivacyPolicyConsent') && $op eq 'cud-create';
foreach (@fields) {
$mandatory_fields{$_} = 1;
}
if ( $op eq 'cud-create' || $op eq 'new' ) {
$mandatory_fields{'email'} = 1
if C4::Context->preference('PatronSelfRegistrationVerifyByEmail');
}
return \%mandatory_fields;
}
sub CheckMandatoryFields {
my ( $borrower, $op ) = @_;
my @empty_mandatory_fields;
my $mandatory_fields = GetMandatoryFields($op);
delete $mandatory_fields->{'cardnumber'};
foreach my $key ( keys %$mandatory_fields ) {
push( @empty_mandatory_fields, $key )
unless ( defined( $borrower->{$key} ) && $borrower->{$key} && $borrower->{$key} !~ /^\s*$/ );
}
return @empty_mandatory_fields;
}
sub CheckMandatoryAttributes {
my ( $borrower, $attributes ) = @_;
my @empty_mandatory_fields;
for my $attribute (@$attributes) {
my $attr = Koha::Patron::Attribute::Types->find( $attribute->{code} );
push @empty_mandatory_fields, $attribute->{code}
if $attr && $attr->opac_mandatory && $attribute->{attribute} =~ m|^\s*$|;
}
return @empty_mandatory_fields;
}
sub CheckForInvalidFields {
my $params = shift;
my $borrower = $params->{borrower};
my $context = $params->{context};
my @invalidFields;
if ( $borrower->{'email'} ) {
unless ( Koha::Email->is_valid( $borrower->{email} ) ) {
push( @invalidFields, "email" );
} elsif ( C4::Context->preference("PatronSelfRegistrationEmailMustBeUnique") && $context eq 'create' ) {
my $patrons_with_same_email = Koha::Patrons->search( # FIXME Should be search_limited?
{
email => $borrower->{email},
(
exists $borrower->{borrowernumber}
? ( borrowernumber => { '!=' => $borrower->{borrowernumber} } )
: ()
)
}
)->count;
if ($patrons_with_same_email) {
push @invalidFields, "duplicate_email";
}
} elsif ( C4::Context->preference("PatronSelfRegistrationConfirmEmail")
&& $borrower->{'email'} ne $borrower->{'repeat_email'}
&& !defined $borrower->{borrowernumber} )
{
push @invalidFields, "email_match";
}
# email passed all tests, so prevent attempting to store repeat_email
delete $borrower->{'repeat_email'};
}
if ( $borrower->{'emailpro'} ) {
push( @invalidFields, "emailpro" ) unless Koha::Email->is_valid( $borrower->{'emailpro'} );
}
if ( $borrower->{'B_email'} ) {
push( @invalidFields, "B_email" ) unless Koha::Email->is_valid( $borrower->{'B_email'} );
}
if ( defined $borrower->{'password'}
and $borrower->{'password'} ne $borrower->{'password2'} )
{
push( @invalidFields, "password_match" );
}
if ( $borrower->{'password'} ) {
my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid(
$borrower->{password},
Koha::Patron::Categories->find(
$borrower->{categorycode} || C4::Context->preference('PatronSelfRegistrationDefaultCategory')
)
);
unless ($is_valid) {
push @invalidFields, 'password_too_short' if $error eq 'too_short';
push @invalidFields, 'password_too_weak' if $error eq 'too_weak';
push @invalidFields, 'password_has_whitespaces' if $error eq 'has_whitespaces';
}
}
if ( $borrower->{'dateofbirth'} ) {
my $patron = Koha::Patron->new( { dateofbirth => $borrower->{'dateofbirth'} } );
my $age = $patron->get_age;
my $borrowercategory = Koha::Patron::Categories->find( $borrower->{'categorycode'} );
my ( $low, $high ) = ( $borrowercategory->dateofbirthrequired, $borrowercategory->upperagelimit );
my $age_restriction =
C4::Context->preference("PatronSelfRegistrationAgeRestriction")
|| C4::Context->preference("PatronAgeRestriction")
|| undef;
if ( ( $high && ( $age > $high ) ) or ( $age < $low ) ) {
push @invalidFields, 'ERROR_age_limitations';
}
if ( $age_restriction && $age > $age_restriction ) {
push @invalidFields, 'ERROR_age_out_of_range';
}
}
return \@invalidFields;
}
sub ParseCgiForBorrower {
my ($params) = @_;
my $cgi = $params->{cgi};
my $op = $params->{op};
my $scrubber = C4::Scrubber->new();
my %borrower;
foreach my $field ( $cgi->param ) {
if ( $field =~ '^borrower_' ) {
my ($key) = substr( $field, 9 );
if ( $field !~ '^borrower_password' ) {
$borrower{$key} = $scrubber->scrub( scalar $cgi->param($field) );
} else {
# Allow html characters for passwords
$borrower{$key} = $cgi->param($field);
}
}
}
# Replace checkbox 'agreed' by datetime in gdpr_proc_consent
$borrower{gdpr_proc_consent} = dt_from_string
if $borrower{gdpr_proc_consent} && $borrower{gdpr_proc_consent} eq 'agreed';
delete $borrower{$_}
for
qw/borrowernumber date_renewed debarred debarredcomment flags privacy privacy_guarantor_fines privacy_guarantor_checkouts checkprevcheckout updated_on lastseen login_attempts overdrive_auth_token anonymized/
; # See also members/memberentry.pl
delete $borrower{$_}
for
qw/dateenrolled dateexpiry borrowernotes opacnote sort1 sort2 sms_provider_id autorenew_checkouts gonenoaddress lost relationship/
; # On OPAC only
# Delete fields hidden from this operation
my $mandatory = GetMandatoryFields($op);
my $hidden = GetHiddenFields( $mandatory, $op );
delete $borrower{$_} for keys %$hidden;
return %borrower;
}
sub DelUnchangedFields {
my ( $borrowernumber, %new_data ) = @_;
# get the mandatory fields so we can get the hidden fields
my $mandatory = GetMandatoryFields('edit');
my $patron = Koha::Patrons->find($borrowernumber);
my $current_data = $patron->unblessed;
# get the hidden fields so we don't obliterate them should they have data patrons aren't allowed to modify
my $hidden_fields = GetHiddenFields( $mandatory, 'edit' );
foreach my $key ( keys %new_data ) {
next
if ( $new_data{$key} || $current_data->{$key} )
&& $new_data{$key} ne $current_data->{$key}
&& !$hidden_fields->{$key};
delete $new_data{$key};
}
return %new_data;
}
sub DelEmptyFields {
my (%borrower) = @_;
foreach my $key ( keys %borrower ) {
delete $borrower{$key} unless $borrower{$key};
}
return %borrower;
}
sub GeneratePatronAttributesForm {
my ( $borrowernumber, $entered_attributes ) = @_;
# Get all attribute types and the values for this patron (if applicable)
my @types = grep { $_->opac_editable() or $_->opac_display } # FIXME filter using DBIC
Koha::Patron::Attribute::Types->search()->as_list();
if ( scalar(@types) == 0 ) {
return [];
}
my @displayable_attributes = grep { $_->type->opac_display ? $_ : () }
Koha::Patron::Attributes->search( { borrowernumber => $borrowernumber } )->as_list;
my %attr_values = ();
# Build the attribute values list either from the passed values
# or taken from the patron itself
if ( defined $entered_attributes ) {
foreach my $attr (@$entered_attributes) {
push @{ $attr_values{ $attr->{code} } }, $attr->{attribute};
}
} elsif ( defined $borrowernumber ) {
my @editable_attributes = grep { $_->type->opac_editable ? $_ : () } @displayable_attributes;
foreach my $attr (@editable_attributes) {
push @{ $attr_values{ $attr->code } }, $attr->attribute;
}
}
# Add the non-editable attributes (that don't come from the form)
foreach my $attr ( grep { !$_->type->opac_editable } @displayable_attributes ) {
push @{ $attr_values{ $attr->code } }, $attr->attribute;
}
# Find all existing classes
my @classes = sort( uniq( map { $_->class } @types ) );
my %items_by_class;
foreach my $attr_type (@types) {
push @{ $items_by_class{ $attr_type->class() } }, {
type => $attr_type,
# If editable, make sure there's at least one empty entry,
# to make the template's job easier
values => $attr_values{ $attr_type->code() } || ['']
}
unless !defined $attr_values{ $attr_type->code() }
and !$attr_type->opac_editable;
}
# Finally, build a list of containing classes
my @class_loop;
foreach my $class (@classes) {
next unless ( $items_by_class{$class} );
my $av = Koha::AuthorisedValues->search( { category => 'PA_CLASS', authorised_value => $class } );
my $lib = $av->count ? $av->next->opac_description : $class;
push @class_loop,
{
class => $class,
items => $items_by_class{$class},
lib => $lib,
};
}
return \@class_loop;
}
sub ParsePatronAttributes {
my ( $borrowernumber, $cgi ) = @_;
my @codes = $cgi->multi_param('patron_attribute_code');
my @values = $cgi->multi_param('patron_attribute_value');
my @editable_attribute_types =
map { $_->code } Koha::Patron::Attribute::Types->search( { opac_editable => 1 } )->as_list;
my $ea = each_array( @codes, @values );
my @attributes;
my $delete_candidates = {};
my $scrubber = C4::Scrubber->new();
while ( my ( $code, $value ) = $ea->() ) {
if ( any { $_ eq $code } @editable_attribute_types ) {
# It is an editable attribute
if ( !defined($value) or $value eq '' ) {
$delete_candidates->{$code} = 1
unless $delete_candidates->{$code};
} else {
# we've got a value
push @attributes, { code => $code, attribute => $scrubber->scrub($value) };
# 'code' is no longer a delete candidate
delete $delete_candidates->{$code}
if defined $delete_candidates->{$code};
}
}
}
foreach my $code ( keys %{$delete_candidates} ) {
if (
!$borrowernumber # self-registration
|| Koha::Patron::Attributes->search( { borrowernumber => $borrowernumber, code => $code } )->count > 0
)
{
push @attributes, { code => $code, attribute => '' }
unless any { $_->{code} eq $code } @attributes;
}
}
return \@attributes;
}
1;