Bug 33036: Use Koha::Objects
[koha.git] / opac / opac-memberentry.pl
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19
20 use CGI qw ( -utf8 );
21 use Digest::MD5 qw( md5_base64 md5_hex );
22 use JSON qw( to_json );
23 use List::MoreUtils qw( any each_array uniq );
24 use String::Random qw( random_string );
25 use Try::Tiny;
26
27 use C4::Auth qw( get_template_and_user );
28 use C4::Output qw( output_html_with_http_headers );
29 use C4::Context;
30 use C4::Letters qw( GetPreparedLetter EnqueueLetter SendQueuedMessages );
31 use C4::Form::MessagingPreferences;
32 use Koha::AuthUtils;
33 use Koha::Patrons;
34 use Koha::Patron::Consent;
35 use Koha::Patron::Modification;
36 use Koha::Patron::Modifications;
37 use C4::Scrubber;
38 use Koha::DateUtils qw( dt_from_string );
39 use Koha::Email;
40 use Koha::Libraries;
41 use Koha::Patron::Attribute::Types;
42 use Koha::Patron::Attributes;
43 use Koha::Patron::Images;
44 use Koha::Patron::Categories;
45 use Koha::Policy::Patrons::Cardnumber;
46 use Koha::AuthorisedValues;
47 my $cgi = CGI->new;
48 my $dbh = C4::Context->dbh;
49
50 my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
51     {
52         template_name   => "opac-memberentry.tt",
53         type            => "opac",
54         query           => $cgi,
55         authnotrequired => 1,
56     }
57 );
58
59 my $op = $cgi->param('op') || q{};
60 if ( $borrowernumber && ( $op eq 'cud-create' || $op eq 'new' ) ) {
61     print $cgi->redirect("/cgi-bin/koha/opac-main.pl");
62     exit;
63 }
64
65 if ( $op eq q{} ) {
66     if ($borrowernumber) {
67         $op = 'edit';
68     }
69     else {
70         $op = 'new';
71     }
72 }
73
74 my $PatronSelfRegistrationDefaultCategory = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
75 my $defaultCategory = Koha::Patron::Categories->find($PatronSelfRegistrationDefaultCategory);
76 # Having a valid PatronSelfRegistrationDefaultCategory is mandatory
77 if ( !C4::Context->preference('PatronSelfRegistration') && !$borrowernumber
78     || ( ( $op eq 'new' || $op eq 'cud-create' ) && !$defaultCategory ) )
79 {
80     print $cgi->redirect("/cgi-bin/koha/opac-main.pl");
81     exit;
82 }
83
84 my $mandatory = GetMandatoryFields($op);
85
86 my $params = {};
87 if ( $op eq 'cud-create' || $op eq 'new' ) {
88     my @PatronSelfRegistrationLibraryList = split '\|', C4::Context->preference('PatronSelfRegistrationLibraryList');
89     $params = { branchcode => { -in => \@PatronSelfRegistrationLibraryList } }
90       if @PatronSelfRegistrationLibraryList;
91 }
92 my $libraries = Koha::Libraries->search( $params, { order_by => ['branchname'] } );
93
94 my ( $min, $max ) = Koha::Policy::Patrons::Cardnumber->get_valid_length();
95 if ( defined $min ) {
96      $template->param(
97          minlength_cardnumber => $min,
98          maxlength_cardnumber => $max
99      );
100  }
101
102 my $translated_languages = C4::Languages::getTranslatedLanguages( 'opac', C4::Context->preference('template') );
103
104 $template->param(
105     op                => $op,
106     hidden            => GetHiddenFields( $mandatory, $op ),
107     mandatory         => $mandatory,
108     libraries         => $libraries,
109     OPACPatronDetails => C4::Context->preference('OPACPatronDetails'),
110     defaultCategory   => $defaultCategory,
111     languages         => $translated_languages,
112 );
113
114 my $attributes = ParsePatronAttributes($borrowernumber,$cgi);
115 my $conflicting_attribute = 0;
116
117 foreach my $attr (@$attributes) {
118     my $attribute = Koha::Patron::Attribute->new($attr);
119     if ( !$attribute->unique_ok ) {
120         my $attr_type = Koha::Patron::Attribute::Types->find($attr->{code});
121         $template->param(
122             extended_unique_id_failed_code => $attr->{code},
123             extended_unique_id_failed_value => $attr->{attribute},
124             extended_unique_id_failed_description => $attr_type->description,
125         );
126         $conflicting_attribute = 1;
127     }
128 }
129
130 if ( $op eq 'cud-create' ) {
131
132     my %borrower = ParseCgiForBorrower($cgi);
133
134     %borrower = DelEmptyFields(%borrower);
135     $borrower{categorycode} ||= $PatronSelfRegistrationDefaultCategory;
136
137     my @empty_mandatory_fields = (CheckMandatoryFields( \%borrower, $op ), CheckMandatoryAttributes( \%borrower, $attributes ) );
138     my $invalidformfields = CheckForInvalidFields(\%borrower);
139     delete $borrower{'password2'};
140     my $is_cardnumber_valid;
141     if ( !grep { $_ eq 'cardnumber' } @empty_mandatory_fields ) {
142         # No point in checking the cardnumber if it's missing and mandatory, it'll just generate a
143         # spurious length warning.
144         my $patron = Koha::Patrons->find($borrower{borrowernumber});
145         $is_cardnumber_valid = Koha::Policy::Patrons::Cardnumber->is_valid($borrower{cardnumber}, $patron);
146         unless ($is_cardnumber_valid) {
147             for my $m ( @{ $is_cardnumber_valid->messages } ) {
148                 my $message = $m->message;
149                 if ( $message eq 'already_exists' ) {
150                     $template->param( cardnumber_already_exists => 1 );
151                 } elsif ( $message eq 'invalid_length' ) {
152                     $template->param( cardnumber_wrong_length => 1 );
153                 }
154             }
155         }
156     }
157
158     if ( @empty_mandatory_fields || @$invalidformfields || !$is_cardnumber_valid || $conflicting_attribute ) {
159
160         $template->param(
161             empty_mandatory_fields => \@empty_mandatory_fields,
162             invalid_form_fields    => $invalidformfields,
163             borrower               => \%borrower
164         );
165         $template->param( patron_attribute_classes => GeneratePatronAttributesForm( undef, $attributes ) );
166     }
167     elsif (
168         md5_base64( uc( $cgi->param('captcha') ) ) ne $cgi->param('captcha_digest') )
169     {
170         $template->param(
171             failed_captcha => 1,
172             borrower       => \%borrower
173         );
174         $template->param( patron_attribute_classes => GeneratePatronAttributesForm( undef, $attributes ) );
175     } elsif ( !$libraries->find($borrower{branchcode}) ) {
176         die "Branchcode not allowed"; # They hack the form
177     }
178     else {
179         if (
180             C4::Context->preference(
181                 'PatronSelfRegistrationVerifyByEmail')
182           )
183         {
184             ( $template, $borrowernumber, $cookie ) = get_template_and_user(
185                 {
186                     template_name   => "opac-registration-email-sent.tt",
187                     type            => "opac",
188                     query           => $cgi,
189                     authnotrequired => 1,
190                 }
191             );
192             $template->param( 'email' => $borrower{'email'} );
193
194             my $verification_token = md5_hex( time().{}.rand().{}.$$ );
195             while ( Koha::Patron::Modifications->search( { verification_token => $verification_token } )->count() ) {
196                 $verification_token = md5_hex( time().{}.rand().{}.$$ );
197             }
198
199             $borrower{password}          = Koha::AuthUtils::generate_password(Koha::Patron::Categories->find($borrower{categorycode})) unless $borrower{password};
200             $borrower{verification_token} = $verification_token;
201
202             $borrower{extended_attributes} = to_json($attributes);
203             Koha::Patron::Modification->new( \%borrower )->store();
204
205             #Send verification email
206             my $letter = C4::Letters::GetPreparedLetter(
207                 module      => 'members',
208                 letter_code => 'OPAC_REG_VERIFY',
209                 lang        => 'default', # Patron does not have a preferred language defined yet
210                 tables      => {
211                     borrower_modifications => $verification_token,
212                 },
213             );
214
215             my $message_id = C4::Letters::EnqueueLetter(
216                 {
217                     letter                 => $letter,
218                     message_transport_type => 'email',
219                     to_address             => $borrower{'email'},
220                     from_address =>
221                       C4::Context->preference('KohaAdminEmailAddress'),
222                 }
223             );
224             C4::Letters::SendQueuedMessages( { message_id => $message_id } ) if $message_id;
225         }
226         else {
227             $borrower{password}         ||= Koha::AuthUtils::generate_password(Koha::Patron::Categories->find($borrower{categorycode}));
228             my $consent_dt = delete $borrower{gdpr_proc_consent};
229             my $patron;
230             try {
231                 $patron = Koha::Patron->new( \%borrower )->store;
232                 Koha::Patron::Consent->new({ borrowernumber => $patron->borrowernumber, type => 'GDPR_PROCESSING', given_on => $consent_dt })->store if $patron && $consent_dt;
233             } catch {
234                 my $type = ref($_);
235                 my $info = "$_";
236                 $template->param( error_type => $type, error_info => $info );
237                 $template->param( borrower => \%borrower );
238             };
239
240             ( $template, $borrowernumber, $cookie ) = get_template_and_user(
241                 {
242                     template_name   => "opac-registration-confirmation.tt",
243                     type            => "opac",
244                     query           => $cgi,
245                     authnotrequired => 1,
246                 }
247             ) if $patron;
248
249             if ( $patron ) {
250                 $patron->extended_attributes->filter_by_branch_limitations->delete;
251                 $patron->extended_attributes($attributes);
252                 if ( C4::Context->preference('EnhancedMessagingPreferences') ) {
253                     C4::Form::MessagingPreferences::handle_form_action(
254                         $cgi,
255                         { borrowernumber => $patron->borrowernumber },
256                         $template,
257                         1,
258                         $PatronSelfRegistrationDefaultCategory
259                     );
260                 }
261
262                 $template->param( password_cleartext => $patron->plain_text_password );
263                 $template->param( borrower => $patron->unblessed );
264
265                 $template->param( confirmed => 1 );
266
267                 # If 'AutoEmailNewUser' syspref is on, email user their account details from the 'notice' that matches the user's branchcode.
268                 if ( C4::Context->preference("AutoEmailNewUser") ) {
269                     #look for defined primary email address, if blank - attempt to use borr.email and borr.emailpro instead
270                     my $emailaddr = $patron->notice_email_address;
271                     # if we manage to find a valid email address, send notice
272                     if ($emailaddr) {
273                         eval {
274                             my $letter = GetPreparedLetter(
275                                 module      => 'members',
276                                 letter_code => 'WELCOME',
277                                 branchcode  => $patron->branchcode,,
278                                 lang        => $patron->lang || 'default',
279                                 tables      => {
280                                     'branches'  => $patron->branchcode,
281                                     'borrowers' => $patron->borrowernumber,
282                                 },
283                                 want_librarian => 1,
284                             ) or return;
285
286                             my $message_id = EnqueueLetter(
287                                 {
288                                     letter                 => $letter,
289                                     borrowernumber         => $patron->id,
290                                     to_address             => $emailaddr,
291                                     message_transport_type => 'email'
292                                 }
293                             );
294                             SendQueuedMessages( { message_id => $message_id } ) if $message_id;
295                         };
296                     }
297                 }
298
299                 # Notify library of new patron registration
300                 my $notify_library = C4::Context->preference('EmailPatronRegistrations');
301                 if ($notify_library) {
302                     $patron->notify_library_of_registration($notify_library);
303                 }
304
305             }
306             $template->param(
307                 PatronSelfRegistrationAdditionalInstructions =>
308                   C4::Context->preference(
309                     'PatronSelfRegistrationAdditionalInstructions')
310             );
311         }
312     }
313 }
314 elsif ( $op eq 'cud-update' ) {
315
316     my $borrower = Koha::Patrons->find( $borrowernumber )->unblessed;
317
318     my %borrower = ParseCgiForBorrower($cgi);
319     $borrower{borrowernumber} = $borrowernumber;
320
321     my @empty_mandatory_fields = grep { $_ ne 'password' } # password is not required when editing personal details
322       ( CheckMandatoryFields( \%borrower, $op ), CheckMandatoryAttributes( \%borrower, $attributes ) );
323     my $invalidformfields = CheckForInvalidFields(\%borrower);
324
325     # Send back the data to the template
326     %borrower = ( %$borrower, %borrower );
327
328     if (@empty_mandatory_fields || @$invalidformfields) {
329         $template->param(
330             empty_mandatory_fields => \@empty_mandatory_fields,
331             invalid_form_fields    => $invalidformfields,
332             borrower               => \%borrower,
333         );
334         $template->param( patron_attribute_classes => GeneratePatronAttributesForm( $borrowernumber, $attributes ) );
335
336         $template->param( op => 'edit' );
337     }
338     else {
339         my %borrower_changes = DelUnchangedFields( $borrowernumber, %borrower );
340         $borrower_changes{'changed_fields'} = join ',', keys %borrower_changes;
341         my $extended_attributes_changes = FilterUnchangedAttributes( $borrowernumber, $attributes );
342
343         if ( %borrower_changes || scalar @{$extended_attributes_changes} > 0 ) {
344             ( $template, $borrowernumber, $cookie ) = get_template_and_user(
345                 {
346                     template_name   => "opac-memberentry-update-submitted.tt",
347                     type            => "opac",
348                     query           => $cgi,
349                     authnotrequired => 1,
350                 }
351             );
352
353             $borrower_changes{borrowernumber} = $borrowernumber;
354             $borrower_changes{extended_attributes} = to_json($extended_attributes_changes);
355
356             Koha::Patron::Modifications->search({ borrowernumber => $borrowernumber })->delete;
357
358             my $m = Koha::Patron::Modification->new( \%borrower_changes )->store();
359             #Automatically approve patron profile changes if set in syspref
360
361             if (C4::Context->preference('AutoApprovePatronProfileSettings')) {
362                 # Need to get the object from database, otherwise it is not complete enough to allow deletion
363                 # when approval has been performed.
364                 my $tmp_m = Koha::Patron::Modifications->find({borrowernumber => $borrowernumber});
365                 $tmp_m->approve() if $tmp_m;
366             }
367
368             my $patron = Koha::Patrons->find( $borrowernumber );
369             $template->param( borrower => $patron->unblessed );
370         }
371         else {
372             my $patron = Koha::Patrons->find( $borrowernumber );
373             $template->param(
374                 op => 'edit',
375                 nochanges => 1,
376                 borrower => $patron->unblessed,
377                 patron_attribute_classes => GeneratePatronAttributesForm( $borrowernumber, $attributes ),
378             );
379         }
380     }
381 }
382 elsif ( $op eq 'edit' ) {    #Display logged in borrower's data
383     my $patron = Koha::Patrons->find( $borrowernumber );
384     my $borrower = $patron->unblessed;
385
386     $template->param(
387         borrower  => $borrower,
388         hidden => GetHiddenFields( $mandatory, 'edit' ),
389     );
390
391     if (C4::Context->preference('OPACpatronimages')) {
392         $template->param( display_patron_image => 1 ) if $patron->image;
393     }
394
395     $template->param( patron_attribute_classes => GeneratePatronAttributesForm( $borrowernumber ) );
396 } else {
397     # Render self-registration page
398     $template->param( patron_attribute_classes => GeneratePatronAttributesForm() );
399 }
400
401 my $captcha = random_string("CCCCC");
402 my $patron_param = Koha::Patrons->find( $borrowernumber );
403 $template->param(
404     has_guarantor_flag => $patron_param->guarantor_relationships->guarantors->_resultset->count
405 ) if $patron_param;
406
407 $template->param(
408     captcha        => $captcha,
409     captcha_digest => md5_base64($captcha),
410     patron         => $patron_param
411 );
412
413 output_html_with_http_headers $cgi, $cookie, $template->output, undef, { force_no_caching => 1 };
414
415 sub GetHiddenFields {
416     my ( $mandatory, $op ) = @_;
417     my %hidden_fields;
418
419     my $BorrowerUnwantedField = $op eq 'edit' || $op eq 'cud-update' ?
420       C4::Context->preference( "PatronSelfModificationBorrowerUnwantedField" ) :
421       C4::Context->preference( "PatronSelfRegistrationBorrowerUnwantedField" );
422
423     my @fields = split( /\|/, $BorrowerUnwantedField || q|| );
424     foreach (@fields) {
425         next unless m/\w/o;
426         #Don't hide mandatory fields
427         next if $mandatory->{$_};
428         $hidden_fields{$_} = 1;
429     }
430
431     return \%hidden_fields;
432 }
433
434 sub GetMandatoryFields {
435     my ($op) = @_;
436
437     my %mandatory_fields;
438
439     my $BorrowerMandatoryField = $op eq 'edit' || $op eq 'cud-update' ?
440       C4::Context->preference("PatronSelfModificationMandatoryField") :
441       C4::Context->preference("PatronSelfRegistrationBorrowerMandatoryField");
442
443     my @fields = split( /\|/, $BorrowerMandatoryField );
444     push @fields, 'gdpr_proc_consent' if C4::Context->preference('PrivacyPolicyConsent') && $op eq 'cud-create';
445
446     foreach (@fields) {
447         $mandatory_fields{$_} = 1;
448     }
449
450     if ( $op eq 'cud-create' || $op eq 'new' ) {
451         $mandatory_fields{'email'} = 1
452           if C4::Context->preference(
453             'PatronSelfRegistrationVerifyByEmail');
454     }
455
456     return \%mandatory_fields;
457 }
458
459 sub CheckMandatoryFields {
460     my ( $borrower, $op ) = @_;
461
462     my @empty_mandatory_fields;
463
464     my $mandatory_fields = GetMandatoryFields($op);
465     delete $mandatory_fields->{'cardnumber'};
466
467     foreach my $key ( keys %$mandatory_fields ) {
468         push( @empty_mandatory_fields, $key )
469           unless ( defined( $borrower->{$key} ) && $borrower->{$key} );
470     }
471
472     return @empty_mandatory_fields;
473 }
474
475 sub CheckMandatoryAttributes{
476     my ( $borrower, $attributes ) = @_;
477
478     my @empty_mandatory_fields;
479
480     for my $attribute (@$attributes ) {
481         my $attr = Koha::Patron::Attribute::Types->find($attribute->{code});
482         push @empty_mandatory_fields, $attribute->{code}
483             if $attr && $attr->mandatory && $attribute->{attribute} =~ m|^\s*$|;
484     }
485
486     return @empty_mandatory_fields;
487 }
488
489 sub CheckForInvalidFields {
490     my $borrower = shift;
491     my @invalidFields;
492     if ($borrower->{'email'}) {
493         unless ( Koha::Email->is_valid($borrower->{email}) ) {
494             push(@invalidFields, "email");
495         } elsif ( C4::Context->preference("PatronSelfRegistrationEmailMustBeUnique") ) {
496             my $patrons_with_same_email = Koha::Patrons->search( # FIXME Should be search_limited?
497                 {
498                     email => $borrower->{email},
499                     (
500                         exists $borrower->{borrowernumber}
501                         ? ( borrowernumber =>
502                               { '!=' => $borrower->{borrowernumber} } )
503                         : ()
504                     )
505                 }
506             )->count;
507             if ( $patrons_with_same_email ) {
508                 push @invalidFields, "duplicate_email";
509             }
510         } elsif ( C4::Context->preference("PatronSelfRegistrationConfirmEmail")
511             && $borrower->{'email'} ne $borrower->{'repeat_email'}
512             && !defined $borrower->{borrowernumber} ) {
513             push @invalidFields, "email_match";
514         }
515         # email passed all tests, so prevent attempting to store repeat_email
516         delete $borrower->{'repeat_email'};
517     }
518     if ($borrower->{'emailpro'}) {
519         push(@invalidFields, "emailpro") unless Koha::Email->is_valid($borrower->{'emailpro'});
520     }
521     if ($borrower->{'B_email'}) {
522         push(@invalidFields, "B_email") unless Koha::Email->is_valid($borrower->{'B_email'});
523     }
524     if ( defined $borrower->{'password'}
525         and $borrower->{'password'} ne $borrower->{'password2'} )
526     {
527         push( @invalidFields, "password_match" );
528     }
529     if ( $borrower->{'password'} ) {
530         my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $borrower->{password}, Koha::Patron::Categories->find($borrower->{categorycode}||C4::Context->preference('PatronSelfRegistrationDefaultCategory')) );
531           unless ( $is_valid ) {
532               push @invalidFields, 'password_too_short' if $error eq 'too_short';
533               push @invalidFields, 'password_too_weak' if $error eq 'too_weak';
534               push @invalidFields, 'password_has_whitespaces' if $error eq 'has_whitespaces';
535           }
536     }
537
538     return \@invalidFields;
539 }
540
541 sub ParseCgiForBorrower {
542     my ($cgi) = @_;
543
544     my $scrubber = C4::Scrubber->new();
545     my %borrower;
546
547     foreach my $field ( $cgi->param ) {
548         if ( $field =~ '^borrower_' ) {
549             my ($key) = substr( $field, 9 );
550             if ( $field !~ '^borrower_password' ) {
551                 $borrower{$key} = $scrubber->scrub( scalar $cgi->param($field) );
552             } else {
553                 # Allow html characters for passwords
554                 $borrower{$key} = $cgi->param($field);
555             }
556         }
557     }
558
559     # Replace checkbox 'agreed' by datetime in gdpr_proc_consent
560     $borrower{gdpr_proc_consent} = dt_from_string if  $borrower{gdpr_proc_consent} && $borrower{gdpr_proc_consent} eq 'agreed';
561
562     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
563     delete $borrower{$_} for qw/dateenrolled dateexpiry borrowernotes opacnote sort1 sort2 sms_provider_id autorenew_checkouts gonenoaddress lost relationship/; # On OPAC only
564     delete $borrower{$_} for split( /\s*\|\s*/, C4::Context->preference('PatronSelfRegistrationBorrowerUnwantedField') || q{} );
565
566     return %borrower;
567 }
568
569 sub DelUnchangedFields {
570     my ( $borrowernumber, %new_data ) = @_;
571     # get the mandatory fields so we can get the hidden fields
572     my $mandatory = GetMandatoryFields('edit');
573     my $patron = Koha::Patrons->find( $borrowernumber );
574     my $current_data = $patron->unblessed;
575     # get the hidden fields so we don't obliterate them should they have data patrons aren't allowed to modify
576     my $hidden_fields = GetHiddenFields($mandatory, 'edit');
577
578
579     foreach my $key ( keys %new_data ) {
580         next if defined($new_data{$key}) xor defined($current_data->{$key});
581         if ( !defined($new_data{$key}) || $current_data->{$key} eq $new_data{$key} || $hidden_fields->{$key} ) {
582            delete $new_data{$key};
583         }
584     }
585
586     return %new_data;
587 }
588
589 sub DelEmptyFields {
590     my (%borrower) = @_;
591
592     foreach my $key ( keys %borrower ) {
593         delete $borrower{$key} unless $borrower{$key};
594     }
595
596     return %borrower;
597 }
598
599 sub FilterUnchangedAttributes {
600     my ( $borrowernumber, $entered_attributes ) = @_;
601
602     my @patron_attributes = grep {$_->type->opac_editable ? $_ : ()} Koha::Patron::Attributes->search({ borrowernumber => $borrowernumber })->as_list;
603
604     my $patron_attribute_types;
605     foreach my $attr (@patron_attributes) {
606         $patron_attribute_types->{ $attr->code } += 1;
607     }
608
609     my $passed_attribute_types;
610     foreach my $attr (@{ $entered_attributes }) {
611         $passed_attribute_types->{ $attr->{ code } } += 1;
612     }
613
614     my @changed_attributes;
615
616     # Loop through the current patron attributes
617     foreach my $attribute_type ( keys %{ $patron_attribute_types } ) {
618         if ( $patron_attribute_types->{ $attribute_type } !=  $passed_attribute_types->{ $attribute_type } ) {
619             # count differs, overwrite all attributes for given type
620             foreach my $attr (@{ $entered_attributes }) {
621                 push @changed_attributes, $attr
622                     if $attr->{ code } eq $attribute_type;
623             }
624         } else {
625             # count matches, check values
626             my $changes = 0;
627             foreach my $attr (grep { $_->code eq $attribute_type } @patron_attributes) {
628                 $changes = 1
629                     unless any { $_->{ value } eq $attr->attribute } @{ $entered_attributes };
630                 last if $changes;
631             }
632
633             if ( $changes ) {
634                 foreach my $attr (@{ $entered_attributes }) {
635                     push @changed_attributes, $attr
636                         if $attr->{ code } eq $attribute_type;
637                 }
638             }
639         }
640     }
641
642     # Loop through passed attributes, looking for new ones
643     foreach my $attribute_type ( keys %{ $passed_attribute_types } ) {
644         if ( !defined $patron_attribute_types->{ $attribute_type } ) {
645             # YAY, new stuff
646             foreach my $attr (grep { $_->{code} eq $attribute_type } @{ $entered_attributes }) {
647                 push @changed_attributes, $attr;
648             }
649         }
650     }
651
652     return \@changed_attributes;
653 }
654
655 sub GeneratePatronAttributesForm {
656     my ( $borrowernumber, $entered_attributes ) = @_;
657
658     # Get all attribute types and the values for this patron (if applicable)
659     my @types = grep { $_->opac_editable() or $_->opac_display } # FIXME filter using DBIC
660         Koha::Patron::Attribute::Types->search()->as_list();
661     if ( scalar(@types) == 0 ) {
662         return [];
663     }
664
665     my @displayable_attributes = grep { $_->type->opac_display ? $_ : () }
666         Koha::Patron::Attributes->search({ borrowernumber => $borrowernumber })->as_list;
667
668     my %attr_values = ();
669
670     # Build the attribute values list either from the passed values
671     # or taken from the patron itself
672     if ( defined $entered_attributes ) {
673         foreach my $attr (@$entered_attributes) {
674             push @{ $attr_values{ $attr->{code} } }, $attr->{value};
675         }
676     }
677     elsif ( defined $borrowernumber ) {
678         my @editable_attributes = grep { $_->type->opac_editable ? $_ : () } @displayable_attributes;
679         foreach my $attr (@editable_attributes) {
680             push @{ $attr_values{ $attr->code } }, $attr->attribute;
681         }
682     }
683
684     # Add the non-editable attributes (that don't come from the form)
685     foreach my $attr ( grep { !$_->type->opac_editable } @displayable_attributes ) {
686         push @{ $attr_values{ $attr->code } }, $attr->attribute;
687     }
688
689     # Find all existing classes
690     my @classes = sort( uniq( map { $_->class } @types ) );
691     my %items_by_class;
692
693     foreach my $attr_type (@types) {
694         push @{ $items_by_class{ $attr_type->class() } }, {
695             type => $attr_type,
696             # If editable, make sure there's at least one empty entry,
697             # to make the template's job easier
698             values => $attr_values{ $attr_type->code() } || ['']
699         }
700             unless !defined $attr_values{ $attr_type->code() }
701                     and !$attr_type->opac_editable;
702     }
703
704     # Finally, build a list of containing classes
705     my @class_loop;
706     foreach my $class (@classes) {
707         next unless ( $items_by_class{$class} );
708
709         my $av = Koha::AuthorisedValues->search(
710             { category => 'PA_CLASS', authorised_value => $class } );
711
712         my $lib = $av->count ? $av->next->opac_description : $class;
713
714         push @class_loop,
715             {
716             class => $class,
717             items => $items_by_class{$class},
718             lib   => $lib,
719             };
720     }
721
722     return \@class_loop;
723 }
724
725 sub ParsePatronAttributes {
726     my ( $borrowernumber, $cgi ) = @_;
727
728     my @codes  = $cgi->multi_param('patron_attribute_code');
729     my @values = $cgi->multi_param('patron_attribute_value');
730
731     my @editable_attribute_types
732         = map { $_->code } Koha::Patron::Attribute::Types->search({ opac_editable => 1 })->as_list;
733
734     my $ea = each_array( @codes, @values );
735     my @attributes;
736
737     my $delete_candidates = {};
738
739     my $scrubber = C4::Scrubber->new();
740     while ( my ( $code, $value ) = $ea->() ) {
741         if ( any { $_ eq $code } @editable_attribute_types ) {
742             # It is an editable attribute
743             if ( !defined($value) or $value eq '' ) {
744                 $delete_candidates->{$code} = 1
745                     unless $delete_candidates->{$code};
746             }
747             else {
748                 # we've got a value
749                 push @attributes, { code => $code, attribute => $scrubber->scrub( $value ) };
750
751                 # 'code' is no longer a delete candidate
752                 delete $delete_candidates->{$code}
753                     if defined $delete_candidates->{$code};
754             }
755         }
756     }
757
758     foreach my $code ( keys %{$delete_candidates} ) {
759         if ( not $borrowernumber # self-registration
760             || Koha::Patron::Attributes->search({
761                 borrowernumber => $borrowernumber, code => $code })->count > 0 )
762         {
763             push @attributes, { code => $code, attribute => '' }
764                 unless any { $_->{code} eq $code } @attributes;
765         }
766     }
767
768     return \@attributes;
769 }
770
771
772 1;