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

(-)a/Koha/Patron.pm (-7 / +73 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
        # Stash changes before
2263
        for my $attribute ($self->extended_attributes->as_list) {
2264
            my $repeatable = $attribute->type->repeatable ? 1 : 0;
2265
            $attribute_changes{$repeatable}->{$attribute->code}->{before} //= [];
2266
            push (
2267
                @{$attribute_changes{$repeatable}->{$attribute->code}->{before}},
2268
                $attribute->attribute
2269
            );
2270
        }
2271
        my @new_attributes = map {
2272
            Koha::Patron::Attribute->new({
2273
                    %{$_},
2274
                    ( borrowernumber => $self->borrowernumber ),
2275
                })
2276
        } @{$attributes};
2277
2278
        # Make sure all attribute types are valid
2279
        for my $attribute (@new_attributes) {
2280
            $attribute->validate_type();
2281
        }
2282
2283
        # Sort new attributes by code
2284
        @new_attributes = sort {$a->attribute cmp $b->attribute} @new_attributes;
2285
2286
        # Stash changes after
2287
        for my $attribute (values @new_attributes) {
2288
            my $repeatable = $attribute->type->repeatable ? 1 : 0;
2289
            $attribute_changes{$repeatable}->{$attribute->code}->{after} //= [];
2290
            push (
2291
                @{$attribute_changes{$repeatable}->{$attribute->code}->{after}},
2292
                $attribute->attribute
2293
            );
2294
        }
2295
2296
        my $is_different = sub {
2297
            my ($a, $b) = map { [ sort @{$_} ] } @_;
2298
            return @{$a} != @{$b} || notall { $_->[0] eq $_->[1] } zip6 @{$a}, @{$b};
2299
        };
2300
2259
        my $schema = $self->_result->result_source->schema;
2301
        my $schema = $self->_result->result_source->schema;
2260
        $schema->txn_do(
2302
        $schema->txn_do(
2261
            sub {
2303
            sub {
2262
                # Remove the existing one
2304
                while (my ($repeatable, $changes) = each %attribute_changes) {
2263
                $self->extended_attributes->filter_by_branch_limitations->delete;
2305
                    while (my ($code, $change) = each %{$changes}) {
2264
2306
                        $change->{before} //= [];
2265
                # Insert the new ones
2307
                        $change->{after} //= [];
2308
2309
                        if ($is_different->($change->{before}, $change->{after})) {
2310
                            unless ($repeatable) {
2311
                                $change->{before} = @{$change->{before}} ? $change->{before}->[0] : '';
2312
                                $change->{after} = @{$change->{after}} ? $change->{after}->[0] : '';
2313
                            }
2314
                            # Remove existing
2315
                            $self->extended_attributes->filter_by_branch_limitations->search({
2316
                                    'me.code' => $code,
2317
                                })->delete;
2318
                            # Add possible new attribute values
2319
                            for my $attribute (@new_attributes) {
2320
                                $attribute->store() if ($attribute->code eq $code);
2321
                            }
2322
                            if (C4::Context->preference("BorrowersLog")) {
2323
                                logaction(
2324
                                    "MEMBERS",
2325
                                    "MODIFY",
2326
                                    $self->borrowernumber,
2327
                                    "Patron attribute " . $code . ": " . to_json($change, { pretty => 1, canonical => 1 })
2328
                                );
2329
                            }
2330
                        }
2331
                    }
2332
                }
2266
                my $new_types = {};
2333
                my $new_types = {};
2267
                for my $attribute (@$attributes) {
2334
                for my $attribute (@{$attributes}) {
2268
                    $self->add_extended_attribute($attribute);
2269
                    $new_types->{ $attribute->{code} } = 1;
2335
                    $new_types->{ $attribute->{code} } = 1;
2270
                }
2336
                }
2271
2337
(-)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 (-44 / +6 lines)
Lines 622-671 if ( ( !$nok ) and $nodouble and ( $op eq 'cud-insert' or $op eq 'cud-save' ) ) Link Here
622
        }
622
        }
623
    }
623
    }
624
624
625
    if ($success) {
625
    if ( $success ) {
626
        if ( C4::Context->preference('ExtendedPatronAttributes')
626
        if (
627
            and $input->param('setting_extended_patron_attributes') )
627
            C4::Context->preference('ExtendedPatronAttributes') and
628
        {
628
            $input->param('setting_extended_patron_attributes')
629
            my $existing_attributes = $patron->extended_attributes->filter_by_branch_limitations->unblessed;
629
        ) {
630
630
            $patron->extended_attributes($extended_patron_attributes);
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
        }
631
        }
670
632
671
        if (
633
        if (
(-)a/t/db_dependent/Koha/Patron/Attribute.t (-2 / +226 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
                }
542
                else {
543
                    $change->{$k} = $v ? [$v] : [];
544
                }
545
            }
546
        }
547
        return  "Patron attribute " . $code . ": " . to_json($change, { pretty => 1, canonical => 1 });
548
    };
549
550
    my $patron = $builder->build_object({ class=> 'Koha::Patrons' });
551
    my $attribute_type = $builder->build_object(
552
        {
553
            class => 'Koha::Patron::Attribute::Types',
554
            value  => { repeatable => 0 }
555
        }
556
    );
557
558
    t::lib::Mocks::mock_preference( 'BorrowersLog', 0 );
559
    my $attributes = [
560
        {
561
            attribute => 'Foo',
562
            code => $attribute_type->code,
563
        }
564
    ];
565
    $patron->extended_attributes($attributes);
566
567
    my $info = $get_info->('', 'Foo', $attribute_type->code);
568
    my $action_logs = Koha::ActionLogs->search(
569
        {
570
            module => "MEMBERS",
571
            action => "MODIFY",
572
            object => $patron->borrowernumber,
573
            info => $info
574
        }
575
    );
576
    is(
577
        $action_logs->count,
578
        0,
579
        'No action log entry has been created when adding patron attribute if BorrowersLog syspref disabled'
580
    );
581
582
    t::lib::Mocks::mock_preference( 'BorrowersLog', 1 );
583
    my $current_action_logs_count;
584
    my $repeatable_text;
585
    for my $repeatable (0, 1) {
586
        $repeatable_text = $repeatable ? ' repeatable' : '';
587
588
        $patron->extended_attributes([]);
589
590
        $attribute_type = $builder->build_object(
591
            {
592
                class => 'Koha::Patron::Attribute::Types',
593
                value  => { repeatable => $repeatable }
594
            }
595
        );
596
        $attributes = [
597
            {
598
                attribute => 'Foo',
599
                code => $attribute_type->code,
600
            }
601
        ];
602
603
        $patron->extended_attributes($attributes);
604
        $info = $get_info->('', 'Foo', $attribute_type->code, $repeatable);
605
        $action_logs = Koha::ActionLogs->search(
606
            {
607
                module => "MEMBERS",
608
                action => "MODIFY",
609
                object => $patron->borrowernumber,
610
                info => $info
611
            }
612
        );
613
        is(
614
            $action_logs->count,
615
            1,
616
            "An action log entry has been created when adding$repeatable_text patron attribute"
617
        );
618
619
        $current_action_logs_count = Koha::ActionLogs->search(
620
            {
621
                module => "MEMBERS",
622
                action => "MODIFY",
623
                object => $patron->borrowernumber
624
            }
625
        )->count;
626
627
        $patron->extended_attributes($attributes);
628
        $action_logs = Koha::ActionLogs->search(
629
            {
630
                module => "MEMBERS",
631
                action => "MODIFY",
632
                object => $patron->borrowernumber
633
            }
634
        );
635
        is(
636
            $action_logs->count,
637
            $current_action_logs_count,
638
            "No additional action log entry has been created when updating$repeatable_text patron attribute with same value"
639
        );
640
641
        $attributes = [
642
            {
643
                attribute => 'Bar',
644
                code => $attribute_type->code,
645
            }
646
        ];
647
        $patron->extended_attributes($attributes);
648
        $info = $get_info->('Foo', 'Bar', $attribute_type->code, $repeatable);
649
        $action_logs = Koha::ActionLogs->search(
650
            {
651
                module => "MEMBERS",
652
                action => "MODIFY",
653
                object => $patron->borrowernumber,
654
                info => $info
655
            }
656
        );
657
        is(
658
            $action_logs->count,
659
            1,
660
            "New action log entry has been created when updating$repeatable_text patron attribute with different value"
661
        );
662
663
        $patron->extended_attributes([]);
664
        $info = $get_info->('Bar', '', $attribute_type->code, $repeatable);
665
        $action_logs = Koha::ActionLogs->search(
666
            {
667
                module => "MEMBERS",
668
                action => "MODIFY",
669
                object => $patron->borrowernumber,
670
                info => $info
671
            }
672
        );
673
        is(
674
            $action_logs->count,
675
            1,
676
            "New action log entry has been created when deleting$repeatable_text patron attribute value"
677
        );
678
    }
679
680
    $attributes = [
681
        {
682
            attribute => 'Foo',
683
            code => $attribute_type->code,
684
        },
685
        {
686
            attribute => 'Bar',
687
            code => $attribute_type->code,
688
        }
689
    ];
690
    $patron->extended_attributes($attributes);
691
692
    $info = $get_info->([], ['Foo', 'Bar'], $attribute_type->code, 1);
693
    use Data::Dumper;
694
    print Dumper($info);
695
    $action_logs = Koha::ActionLogs->search(
696
        {
697
            module => "MEMBERS",
698
            action => "MODIFY",
699
            object => $patron->borrowernumber,
700
            info => $info
701
        }
702
    );
703
    is(
704
        $action_logs->count,
705
        1,
706
        "New action log entry has been created when updating repeatable patron attribute with multiple values"
707
    );
708
709
    $attributes = [
710
        {
711
            attribute => 'Foo',
712
            code => $attribute_type->code,
713
        },
714
        {
715
            attribute => 'Bar',
716
            code => $attribute_type->code,
717
        },
718
        {
719
            attribute => 'Baz',
720
            code => $attribute_type->code,
721
        }
722
    ];
723
    $patron->extended_attributes($attributes);
724
725
    $info = $get_info->(
726
        ['Foo', 'Bar'],
727
        ['Foo', 'Bar', 'Baz'],
728
        $attribute_type->code,
729
        1
730
    );
731
    $action_logs = Koha::ActionLogs->search(
732
        {
733
            module => "MEMBERS",
734
            action => "MODIFY",
735
            object => $patron->borrowernumber,
736
            info => $info
737
        }
738
    );
739
    is(
740
        $action_logs->count,
741
        1,
742
        "New action log entry has been created when updating repeatable patron attribute with existing multiple values with multiple values"
743
    );
744
745
    $schema->storage->txn_rollback;
746
};

Return to bug 26744