View | Details | Raw Unified | Return to bug 26744
Collapse All | Expand All

(-)a/Koha/Patron.pm (-6 / +81 lines)
Lines 20-30 package Koha::Patron; Link Here
20
20
21
use Modern::Perl;
21
use Modern::Perl;
22
22
23
use List::MoreUtils    qw( any none uniq );
23
use List::MoreUtils    qw( any none uniq notall zip6);
24
use JSON               qw( to_json );
24
use JSON               qw( to_json );
25
use Unicode::Normalize qw( NFKD );
25
use Unicode::Normalize qw( NFKD );
26
use Try::Tiny;
26
use Try::Tiny;
27
use DateTime ();
27
use DateTime ();
28
use C4::Log  qw( logaction );
28
29
29
use C4::Auth qw( checkpw_hash );
30
use C4::Auth qw( checkpw_hash );
30
use C4::Context;
31
use C4::Context;
Lines 2255-2271 Or setter FIXME Link Here
2255
2256
2256
sub extended_attributes {
2257
sub extended_attributes {
2257
    my ( $self, $attributes ) = @_;
2258
    my ( $self, $attributes ) = @_;
2259
2258
    if ($attributes) {    # setter
2260
    if ($attributes) {    # setter
2261
        my %attribute_changes;
2262
2263
        # Stash changes before
2264
        for my $attribute ( $self->extended_attributes->as_list ) {
2265
            my $repeatable = $attribute->type->repeatable ? 1 : 0;
2266
            $attribute_changes{$repeatable}->{ $attribute->code }->{before} //= [];
2267
            push(
2268
                @{ $attribute_changes{$repeatable}->{ $attribute->code }->{before} },
2269
                $attribute->attribute
2270
            );
2271
        }
2272
        my @new_attributes = map {
2273
            Koha::Patron::Attribute->new(
2274
                {
2275
                    %{$_},
2276
                    ( borrowernumber => $self->borrowernumber ),
2277
                }
2278
            )
2279
        } @{$attributes};
2280
2281
        # Make sure all attribute types are valid
2282
        for my $attribute (@new_attributes) {
2283
            $attribute->validate_type();
2284
        }
2285
2286
        # Sort new attributes by code
2287
        @new_attributes = sort { $a->attribute cmp $b->attribute } @new_attributes;
2288
2289
        # Stash changes after
2290
        for my $attribute ( values @new_attributes ) {
2291
            my $repeatable = $attribute->type->repeatable ? 1 : 0;
2292
            $attribute_changes{$repeatable}->{ $attribute->code }->{after} //= [];
2293
            push(
2294
                @{ $attribute_changes{$repeatable}->{ $attribute->code }->{after} },
2295
                $attribute->attribute
2296
            );
2297
        }
2298
2299
        my $is_different = sub {
2300
            my ( $a, $b ) = map { [ sort @{$_} ] } @_;
2301
            return @{$a} != @{$b} || notall { $_->[0] eq $_->[1] } zip6 @{$a}, @{$b};
2302
        };
2303
2259
        my $schema = $self->_result->result_source->schema;
2304
        my $schema = $self->_result->result_source->schema;
2260
        $schema->txn_do(
2305
        $schema->txn_do(
2261
            sub {
2306
            sub {
2262
                # Remove the existing one
2307
                while ( my ( $repeatable, $changes ) = each %attribute_changes ) {
2263
                $self->extended_attributes->filter_by_branch_limitations->delete;
2308
                    while ( my ( $code, $change ) = each %{$changes} ) {
2309
                        $change->{before} //= [];
2310
                        $change->{after}  //= [];
2311
2312
                        if ( $is_different->( $change->{before}, $change->{after} ) ) {
2313
                            unless ($repeatable) {
2314
                                $change->{before} = @{ $change->{before} } ? $change->{before}->[0] : '';
2315
                                $change->{after}  = @{ $change->{after} }  ? $change->{after}->[0]  : '';
2316
                            }
2317
2318
                            # Remove existing
2319
                            $self->extended_attributes->filter_by_branch_limitations->search(
2320
                                {
2321
                                    'me.code' => $code,
2322
                                }
2323
                            )->delete;
2264
2324
2265
                # Insert the new ones
2325
                            # Add possible new attribute values
2326
                            for my $attribute (@new_attributes) {
2327
                                $attribute->store() if ( $attribute->code eq $code );
2328
                            }
2329
                            if ( C4::Context->preference("BorrowersLog") ) {
2330
                                logaction(
2331
                                    "MEMBERS",
2332
                                    "MODIFY",
2333
                                    $self->borrowernumber,
2334
                                    "Patron attribute "
2335
                                        . $code . ": "
2336
                                        . to_json( $change, { pretty => 1, canonical => 1 } )
2337
                                );
2338
                            }
2339
                        }
2340
                    }
2341
                }
2266
                my $new_types = {};
2342
                my $new_types = {};
2267
                for my $attribute (@$attributes) {
2343
                for my $attribute ( @{$attributes} ) {
2268
                    $self->add_extended_attribute($attribute);
2269
                    $new_types->{ $attribute->{code} } = 1;
2344
                    $new_types->{ $attribute->{code} } = 1;
2270
                }
2345
                }
2271
2346
(-)a/Koha/Patron/Attribute.pm (-2 / +20 lines)
Lines 35-40 Koha::Patron::Attribute - Koha Patron Attribute Object class Link Here
35
35
36
=cut
36
=cut
37
37
38
=head3 validate_type
39
40
    my $attribute = Koha::Patron::Attribute->new({ code => 'a_code', ... });
41
    try { $attribute->validate_type }
42
    catch { handle_exception };
43
44
Validate type of C<Koha::Patron::Attribute> object, used in extended_attributes
45
to validate type when attribute has not yet been stored.
46
47
=cut
48
49
sub validate_type {
50
51
    my $self = shift;
52
53
    Koha::Exceptions::Patron::Attribute::InvalidType->throw( type => $self->code )
54
        unless $self->type;
55
}
56
38
=head3 store
57
=head3 store
39
58
40
    my $attribute = Koha::Patron::Attribute->new({ code => 'a_code', ... });
59
    my $attribute = Koha::Patron::Attribute->new({ code => 'a_code', ... });
Lines 47-54 sub store { Link Here
47
66
48
    my $self = shift;
67
    my $self = shift;
49
68
50
    Koha::Exceptions::Patron::Attribute::InvalidType->throw( type => $self->code )
69
    $self->validate_type();
51
        unless $self->type;
52
70
53
    Koha::Exceptions::Patron::Attribute::NonRepeatable->throw( attribute => $self )
71
    Koha::Exceptions::Patron::Attribute::NonRepeatable->throw( attribute => $self )
54
        unless $self->repeatable_ok();
72
        unless $self->repeatable_ok();
(-)a/members/memberentry.pl (-42 / +2 lines)
Lines 297-304 foreach my $guarantor (@guarantors) { Link Here
297
    if (   ( $op eq 'cud-save' || $op eq 'cud-insert' )
297
    if (   ( $op eq 'cud-save' || $op eq 'cud-insert' )
298
        && ( $guarantor->is_child || $guarantor->is_guarantee || ( $patron && $patron->is_guarantor ) ) )
298
        && ( $guarantor->is_child || $guarantor->is_guarantee || ( $patron && $patron->is_guarantor ) ) )
299
    {
299
    {
300
        push @errors, 'ERROR_child_is_guarantor'     if ( $guarantor->is_child );
300
        push @errors, 'ERROR_guarantor_is_guarantee';
301
        push @errors, 'ERROR_guarantor_is_guarantee' if ( !$guarantor->is_child );
302
    }
301
    }
303
}
302
}
304
303
Lines 626-671 if ( ( !$nok ) and $nodouble and ( $op eq 'cud-insert' or $op eq 'cud-save' ) ) Link Here
626
        if ( C4::Context->preference('ExtendedPatronAttributes')
625
        if ( C4::Context->preference('ExtendedPatronAttributes')
627
            and $input->param('setting_extended_patron_attributes') )
626
            and $input->param('setting_extended_patron_attributes') )
628
        {
627
        {
629
            my $existing_attributes = $patron->extended_attributes->filter_by_branch_limitations->unblessed;
628
            $patron->extended_attributes($extended_patron_attributes);
630
631
            my $needs_update = 1;
632
633
            # If there are an unqueunal number of new and old patron attributes they definitely need updated
634
            if ( scalar @{$existing_attributes} == scalar @{$extended_patron_attributes} ) {
635
                my $seen = 0;
636
                for ( my $i = 0 ; $i <= scalar @{$extended_patron_attributes} ; $i++ ) {
637
                    my $new_attr = $extended_patron_attributes->[$i];
638
                    next unless $new_attr;
639
                    for ( my $j = 0 ; $j <= scalar @{$existing_attributes} ; $j++ ) {
640
                        my $existing_attr = $existing_attributes->[$j];
641
                        next unless $existing_attr;
642
643
                        if (   $new_attr->{attribute} eq $existing_attr->{attribute}
644
                            && $new_attr->{borrowernumber} eq $existing_attr->{borrowernumber}
645
                            && $new_attr->{code} eq $existing_attr->{code} )
646
                        {
647
                            $seen++;
648
649
                            # Remove the match from the "old" attribute
650
                            splice( @{$existing_attributes}, $j, 1 );
651
652
                            # Move on to look at the next "new" attribute
653
                            last;
654
                        }
655
                    }
656
                }
657
658
                # If we found a match for each existing attribute and the number of see attributes matches the number seen
659
                # we don't need to update the attributes
660
                if ( scalar @{$existing_attributes} == 0 && $seen == @{$extended_patron_attributes} ) {
661
                    $needs_update = 0;
662
                }
663
            }
664
665
            if ($needs_update) {
666
                $patron->extended_attributes->filter_by_branch_limitations->delete;
667
                $patron->extended_attributes($extended_patron_attributes);
668
            }
669
        }
629
        }
670
630
671
        if (
631
        if (
(-)a/t/db_dependent/Koha/Patron/Attribute.t (-2 / +225 lines)
Lines 18-32 Link Here
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
use JSON qw( to_json );
21
22
22
use Test::More tests => 3;
23
use Test::More tests => 4;
23
24
24
use t::lib::TestBuilder;
25
use t::lib::TestBuilder;
26
use t::lib::Mocks;
25
use Test::Exception;
27
use Test::Exception;
26
28
27
use Koha::Database;
29
use Koha::Database;
28
use Koha::Patron::Attribute;
30
use Koha::Patron::Attribute;
29
use Koha::Patron::Attributes;
31
use Koha::Patron::Attributes;
32
use Koha::ActionLogs;
30
33
31
my $schema  = Koha::Database->new->schema;
34
my $schema  = Koha::Database->new->schema;
32
my $builder = t::lib::TestBuilder->new;
35
my $builder = t::lib::TestBuilder->new;
Lines 519-521 subtest 'merge_and_replace_with' => sub { Link Here
519
    $schema->storage->txn_rollback;
522
    $schema->storage->txn_rollback;
520
523
521
};
524
};
522
- 
525
526
subtest 'action log tests' => sub {
527
    plan tests => 11;
528
    my $schema = Koha::Database->new->schema;
529
    $schema->storage->txn_begin;
530
531
    my $get_info = sub {
532
        my ( $before, $after, $code, $repeatable ) = @_;
533
        my $change = {
534
            before => $before,
535
            after  => $after
536
        };
537
        if ($repeatable) {
538
            while ( my ( $k, $v ) = each %{$change} ) {
539
                if ( ref $v eq 'ARRAY' ) {
540
                    $change->{$k} = [ sort @{$v} ];
541
                } else {
542
                    $change->{$k} = $v ? [$v] : [];
543
                }
544
            }
545
        }
546
        return "Patron attribute " . $code . ": " . to_json( $change, { pretty => 1, canonical => 1 } );
547
    };
548
549
    my $patron         = $builder->build_object( { class => 'Koha::Patrons' } );
550
    my $attribute_type = $builder->build_object(
551
        {
552
            class => 'Koha::Patron::Attribute::Types',
553
            value => { repeatable => 0 }
554
        }
555
    );
556
557
    t::lib::Mocks::mock_preference( 'BorrowersLog', 0 );
558
    my $attributes = [
559
        {
560
            attribute => 'Foo',
561
            code      => $attribute_type->code,
562
        }
563
    ];
564
    $patron->extended_attributes($attributes);
565
566
    my $info        = $get_info->( '', 'Foo', $attribute_type->code );
567
    my $action_logs = Koha::ActionLogs->search(
568
        {
569
            module => "MEMBERS",
570
            action => "MODIFY",
571
            object => $patron->borrowernumber,
572
            info   => $info
573
        }
574
    );
575
    is(
576
        $action_logs->count,
577
        0,
578
        'No action log entry has been created when adding patron attribute if BorrowersLog syspref disabled'
579
    );
580
581
    t::lib::Mocks::mock_preference( 'BorrowersLog', 1 );
582
    my $current_action_logs_count;
583
    my $repeatable_text;
584
    for my $repeatable ( 0, 1 ) {
585
        $repeatable_text = $repeatable ? ' repeatable' : '';
586
587
        $patron->extended_attributes( [] );
588
589
        $attribute_type = $builder->build_object(
590
            {
591
                class => 'Koha::Patron::Attribute::Types',
592
                value => { repeatable => $repeatable }
593
            }
594
        );
595
        $attributes = [
596
            {
597
                attribute => 'Foo',
598
                code      => $attribute_type->code,
599
            }
600
        ];
601
602
        $patron->extended_attributes($attributes);
603
        $info        = $get_info->( '', 'Foo', $attribute_type->code, $repeatable );
604
        $action_logs = Koha::ActionLogs->search(
605
            {
606
                module => "MEMBERS",
607
                action => "MODIFY",
608
                object => $patron->borrowernumber,
609
                info   => $info
610
            }
611
        );
612
        is(
613
            $action_logs->count,
614
            1,
615
            "An action log entry has been created when adding$repeatable_text patron attribute"
616
        );
617
618
        $current_action_logs_count = Koha::ActionLogs->search(
619
            {
620
                module => "MEMBERS",
621
                action => "MODIFY",
622
                object => $patron->borrowernumber
623
            }
624
        )->count;
625
626
        $patron->extended_attributes($attributes);
627
        $action_logs = Koha::ActionLogs->search(
628
            {
629
                module => "MEMBERS",
630
                action => "MODIFY",
631
                object => $patron->borrowernumber
632
            }
633
        );
634
        is(
635
            $action_logs->count,
636
            $current_action_logs_count,
637
            "No additional action log entry has been created when updating$repeatable_text patron attribute with same value"
638
        );
639
640
        $attributes = [
641
            {
642
                attribute => 'Bar',
643
                code      => $attribute_type->code,
644
            }
645
        ];
646
        $patron->extended_attributes($attributes);
647
        $info        = $get_info->( 'Foo', 'Bar', $attribute_type->code, $repeatable );
648
        $action_logs = Koha::ActionLogs->search(
649
            {
650
                module => "MEMBERS",
651
                action => "MODIFY",
652
                object => $patron->borrowernumber,
653
                info   => $info
654
            }
655
        );
656
        is(
657
            $action_logs->count,
658
            1,
659
            "New action log entry has been created when updating$repeatable_text patron attribute with different value"
660
        );
661
662
        $patron->extended_attributes( [] );
663
        $info        = $get_info->( 'Bar', '', $attribute_type->code, $repeatable );
664
        $action_logs = Koha::ActionLogs->search(
665
            {
666
                module => "MEMBERS",
667
                action => "MODIFY",
668
                object => $patron->borrowernumber,
669
                info   => $info
670
            }
671
        );
672
        is(
673
            $action_logs->count,
674
            1,
675
            "New action log entry has been created when deleting$repeatable_text patron attribute value"
676
        );
677
    }
678
679
    $attributes = [
680
        {
681
            attribute => 'Foo',
682
            code      => $attribute_type->code,
683
        },
684
        {
685
            attribute => 'Bar',
686
            code      => $attribute_type->code,
687
        }
688
    ];
689
    $patron->extended_attributes($attributes);
690
691
    $info = $get_info->( [], [ 'Foo', 'Bar' ], $attribute_type->code, 1 );
692
    use Data::Dumper;
693
    print Dumper($info);
694
    $action_logs = Koha::ActionLogs->search(
695
        {
696
            module => "MEMBERS",
697
            action => "MODIFY",
698
            object => $patron->borrowernumber,
699
            info   => $info
700
        }
701
    );
702
    is(
703
        $action_logs->count,
704
        1,
705
        "New action log entry has been created when updating repeatable patron attribute with multiple values"
706
    );
707
708
    $attributes = [
709
        {
710
            attribute => 'Foo',
711
            code      => $attribute_type->code,
712
        },
713
        {
714
            attribute => 'Bar',
715
            code      => $attribute_type->code,
716
        },
717
        {
718
            attribute => 'Baz',
719
            code      => $attribute_type->code,
720
        }
721
    ];
722
    $patron->extended_attributes($attributes);
723
724
    $info = $get_info->(
725
        [ 'Foo', 'Bar' ],
726
        [ 'Foo', 'Bar', 'Baz' ],
727
        $attribute_type->code,
728
        1
729
    );
730
    $action_logs = Koha::ActionLogs->search(
731
        {
732
            module => "MEMBERS",
733
            action => "MODIFY",
734
            object => $patron->borrowernumber,
735
            info   => $info
736
        }
737
    );
738
    is(
739
        $action_logs->count,
740
        1,
741
        "New action log entry has been created when updating repeatable patron attribute with existing multiple values with multiple values"
742
    );
743
744
    $schema->storage->txn_rollback;
745
};

Return to bug 26744