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 2240-2256 Or setter FIXME Link Here
2240
2241
2241
sub extended_attributes {
2242
sub extended_attributes {
2242
    my ( $self, $attributes ) = @_;
2243
    my ( $self, $attributes ) = @_;
2244
2243
    if ($attributes) {    # setter
2245
    if ($attributes) {    # setter
2246
        my %attribute_changes;
2247
        # Stash changes before
2248
        for my $attribute ($self->extended_attributes->as_list) {
2249
            my $repeatable = $attribute->type->repeatable ? 1 : 0;
2250
            $attribute_changes{$repeatable}->{$attribute->code}->{before} //= [];
2251
            push (
2252
                @{$attribute_changes{$repeatable}->{$attribute->code}->{before}},
2253
                $attribute->attribute
2254
            );
2255
        }
2256
        my @new_attributes = map {
2257
            Koha::Patron::Attribute->new({
2258
                    %{$_},
2259
                    ( borrowernumber => $self->borrowernumber ),
2260
                })
2261
        } @{$attributes};
2262
2263
        # Make sure all attribute types are valid
2264
        for my $attribute (@new_attributes) {
2265
            $attribute->validate_type();
2266
        }
2267
2268
        # Sort new attributes by code
2269
        @new_attributes = sort {$a->attribute cmp $b->attribute} @new_attributes;
2270
2271
        # Stash changes after
2272
        for my $attribute (values @new_attributes) {
2273
            my $repeatable = $attribute->type->repeatable ? 1 : 0;
2274
            $attribute_changes{$repeatable}->{$attribute->code}->{after} //= [];
2275
            push (
2276
                @{$attribute_changes{$repeatable}->{$attribute->code}->{after}},
2277
                $attribute->attribute
2278
            );
2279
        }
2280
2281
        my $is_different = sub {
2282
            my ($a, $b) = map { [ sort @{$_} ] } @_;
2283
            return @{$a} != @{$b} || notall { $_->[0] eq $_->[1] } zip6 @{$a}, @{$b};
2284
        };
2285
2244
        my $schema = $self->_result->result_source->schema;
2286
        my $schema = $self->_result->result_source->schema;
2245
        $schema->txn_do(
2287
        $schema->txn_do(
2246
            sub {
2288
            sub {
2247
                # Remove the existing one
2289
                while (my ($repeatable, $changes) = each %attribute_changes) {
2248
                $self->extended_attributes->filter_by_branch_limitations->delete;
2290
                    while (my ($code, $change) = each %{$changes}) {
2249
2291
                        $change->{before} //= [];
2250
                # Insert the new ones
2292
                        $change->{after} //= [];
2293
2294
                        if ($is_different->($change->{before}, $change->{after})) {
2295
                            unless ($repeatable) {
2296
                                $change->{before} = @{$change->{before}} ? $change->{before}->[0] : '';
2297
                                $change->{after} = @{$change->{after}} ? $change->{after}->[0] : '';
2298
                            }
2299
                            # Remove existing
2300
                            $self->extended_attributes->filter_by_branch_limitations->search({
2301
                                    'me.code' => $code,
2302
                                })->delete;
2303
                            # Add possible new attribute values
2304
                            for my $attribute (@new_attributes) {
2305
                                $attribute->store() if ($attribute->code eq $code);
2306
                            }
2307
                            if (C4::Context->preference("BorrowersLog")) {
2308
                                logaction(
2309
                                    "MEMBERS",
2310
                                    "MODIFY",
2311
                                    $self->borrowernumber,
2312
                                    "Patron attribute " . $code . ": " . to_json($change, { pretty => 1, canonical => 1 })
2313
                                );
2314
                            }
2315
                        }
2316
                    }
2317
                }
2251
                my $new_types = {};
2318
                my $new_types = {};
2252
                for my $attribute (@$attributes) {
2319
                for my $attribute (@{$attributes}) {
2253
                    $self->add_extended_attribute($attribute);
2254
                    $new_types->{$attribute->{code}} = 1;
2320
                    $new_types->{$attribute->{code}} = 1;
2255
                }
2321
                }
2256
2322
(-)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 593-642 if ((!$nok) and $nodouble and ($op eq 'cud-insert' or $op eq 'cud-save')){ Link Here
593
        }
593
        }
594
    }
594
    }
595
595
596
    if ($success) {
596
    if ( $success ) {
597
        if ( C4::Context->preference('ExtendedPatronAttributes')
597
        if (
598
            and $input->param('setting_extended_patron_attributes') )
598
            C4::Context->preference('ExtendedPatronAttributes') and
599
        {
599
            $input->param('setting_extended_patron_attributes')
600
            my $existing_attributes = $patron->extended_attributes->filter_by_branch_limitations->unblessed;
600
        ) {
601
601
            $patron->extended_attributes($extended_patron_attributes);
602
            my $needs_update = 1;
603
604
            # If there are an unqueunal number of new and old patron attributes they definitely need updated
605
            if ( scalar @{$existing_attributes} == scalar @{$extended_patron_attributes} ) {
606
                my $seen = 0;
607
                for ( my $i = 0 ; $i <= scalar @{$extended_patron_attributes} ; $i++ ) {
608
                    my $new_attr = $extended_patron_attributes->[$i];
609
                    next unless $new_attr;
610
                    for ( my $j = 0 ; $j <= scalar @{$existing_attributes} ; $j++ ) {
611
                        my $existing_attr = $existing_attributes->[$j];
612
                        next unless $existing_attr;
613
614
                        if (   $new_attr->{attribute} eq $existing_attr->{attribute}
615
                            && $new_attr->{borrowernumber} eq $existing_attr->{borrowernumber}
616
                            && $new_attr->{code} eq $existing_attr->{code} )
617
                        {
618
                            $seen++;
619
620
                            # Remove the match from the "old" attribute
621
                            splice( @{$existing_attributes}, $j, 1 );
622
623
                            # Move on to look at the next "new" attribute
624
                            last;
625
                        }
626
                    }
627
                }
628
629
                # If we found a match for each existing attribute and the number of see attributes matches the number seen
630
                # we don't need to update the attributes
631
                if ( scalar @{$existing_attributes} == 0 && $seen == @{$extended_patron_attributes} ) {
632
                    $needs_update = 0;
633
                }
634
            }
635
636
            if ($needs_update) {
637
                $patron->extended_attributes->filter_by_branch_limitations->delete;
638
                $patron->extended_attributes($extended_patron_attributes);
639
            }
640
        }
602
        }
641
603
642
        if (
604
        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 510-512 subtest 'merge_and_replace_with' => sub { Link Here
510
    $schema->storage->txn_rollback;
513
    $schema->storage->txn_rollback;
511
514
512
};
515
};
513
- 
516
517
subtest 'action log tests' => sub {
518
    plan tests => 11;
519
    my $schema = Koha::Database->new->schema;
520
    $schema->storage->txn_begin;
521
522
    my $get_info = sub {
523
        my ($before, $after, $code, $repeatable) = @_;
524
        my $change = {
525
            before => $before,
526
            after  => $after
527
        };
528
        if ($repeatable) {
529
            while (my ($k, $v) = each %{$change}) {
530
                if (ref $v eq 'ARRAY') {
531
                    $change->{$k} = [sort @{$v}];
532
                }
533
                else {
534
                    $change->{$k} = $v ? [$v] : [];
535
                }
536
            }
537
        }
538
        return  "Patron attribute " . $code . ": " . to_json($change, { pretty => 1, canonical => 1 });
539
    };
540
541
    my $patron = $builder->build_object({ class=> 'Koha::Patrons' });
542
    my $attribute_type = $builder->build_object(
543
        {
544
            class => 'Koha::Patron::Attribute::Types',
545
            value  => { repeatable => 0 }
546
        }
547
    );
548
549
    t::lib::Mocks::mock_preference( 'BorrowersLog', 0 );
550
    my $attributes = [
551
        {
552
            attribute => 'Foo',
553
            code => $attribute_type->code,
554
        }
555
    ];
556
    $patron->extended_attributes($attributes);
557
558
    my $info = $get_info->('', 'Foo', $attribute_type->code);
559
    my $action_logs = Koha::ActionLogs->search(
560
        {
561
            module => "MEMBERS",
562
            action => "MODIFY",
563
            object => $patron->borrowernumber,
564
            info => $info
565
        }
566
    );
567
    is(
568
        $action_logs->count,
569
        0,
570
        'No action log entry has been created when adding patron attribute if BorrowersLog syspref disabled'
571
    );
572
573
    t::lib::Mocks::mock_preference( 'BorrowersLog', 1 );
574
    my $current_action_logs_count;
575
    my $repeatable_text;
576
    for my $repeatable (0, 1) {
577
        $repeatable_text = $repeatable ? ' repeatable' : '';
578
579
        $patron->extended_attributes([]);
580
581
        $attribute_type = $builder->build_object(
582
            {
583
                class => 'Koha::Patron::Attribute::Types',
584
                value  => { repeatable => $repeatable }
585
            }
586
        );
587
        $attributes = [
588
            {
589
                attribute => 'Foo',
590
                code => $attribute_type->code,
591
            }
592
        ];
593
594
        $patron->extended_attributes($attributes);
595
        $info = $get_info->('', 'Foo', $attribute_type->code, $repeatable);
596
        $action_logs = Koha::ActionLogs->search(
597
            {
598
                module => "MEMBERS",
599
                action => "MODIFY",
600
                object => $patron->borrowernumber,
601
                info => $info
602
            }
603
        );
604
        is(
605
            $action_logs->count,
606
            1,
607
            "An action log entry has been created when adding$repeatable_text patron attribute"
608
        );
609
610
        $current_action_logs_count = Koha::ActionLogs->search(
611
            {
612
                module => "MEMBERS",
613
                action => "MODIFY",
614
                object => $patron->borrowernumber
615
            }
616
        )->count;
617
618
        $patron->extended_attributes($attributes);
619
        $action_logs = Koha::ActionLogs->search(
620
            {
621
                module => "MEMBERS",
622
                action => "MODIFY",
623
                object => $patron->borrowernumber
624
            }
625
        );
626
        is(
627
            $action_logs->count,
628
            $current_action_logs_count,
629
            "No additional action log entry has been created when updating$repeatable_text patron attribute with same value"
630
        );
631
632
        $attributes = [
633
            {
634
                attribute => 'Bar',
635
                code => $attribute_type->code,
636
            }
637
        ];
638
        $patron->extended_attributes($attributes);
639
        $info = $get_info->('Foo', 'Bar', $attribute_type->code, $repeatable);
640
        $action_logs = Koha::ActionLogs->search(
641
            {
642
                module => "MEMBERS",
643
                action => "MODIFY",
644
                object => $patron->borrowernumber,
645
                info => $info
646
            }
647
        );
648
        is(
649
            $action_logs->count,
650
            1,
651
            "New action log entry has been created when updating$repeatable_text patron attribute with different value"
652
        );
653
654
        $patron->extended_attributes([]);
655
        $info = $get_info->('Bar', '', $attribute_type->code, $repeatable);
656
        $action_logs = Koha::ActionLogs->search(
657
            {
658
                module => "MEMBERS",
659
                action => "MODIFY",
660
                object => $patron->borrowernumber,
661
                info => $info
662
            }
663
        );
664
        is(
665
            $action_logs->count,
666
            1,
667
            "New action log entry has been created when deleting$repeatable_text patron attribute value"
668
        );
669
    }
670
671
    $attributes = [
672
        {
673
            attribute => 'Foo',
674
            code => $attribute_type->code,
675
        },
676
        {
677
            attribute => 'Bar',
678
            code => $attribute_type->code,
679
        }
680
    ];
681
    $patron->extended_attributes($attributes);
682
683
    $info = $get_info->([], ['Foo', 'Bar'], $attribute_type->code, 1);
684
    use Data::Dumper;
685
    print Dumper($info);
686
    $action_logs = Koha::ActionLogs->search(
687
        {
688
            module => "MEMBERS",
689
            action => "MODIFY",
690
            object => $patron->borrowernumber,
691
            info => $info
692
        }
693
    );
694
    is(
695
        $action_logs->count,
696
        1,
697
        "New action log entry has been created when updating repeatable patron attribute with multiple values"
698
    );
699
700
    $attributes = [
701
        {
702
            attribute => 'Foo',
703
            code => $attribute_type->code,
704
        },
705
        {
706
            attribute => 'Bar',
707
            code => $attribute_type->code,
708
        },
709
        {
710
            attribute => 'Baz',
711
            code => $attribute_type->code,
712
        }
713
    ];
714
    $patron->extended_attributes($attributes);
715
716
    $info = $get_info->(
717
        ['Foo', 'Bar'],
718
        ['Foo', 'Bar', 'Baz'],
719
        $attribute_type->code,
720
        1
721
    );
722
    $action_logs = Koha::ActionLogs->search(
723
        {
724
            module => "MEMBERS",
725
            action => "MODIFY",
726
            object => $patron->borrowernumber,
727
            info => $info
728
        }
729
    );
730
    is(
731
        $action_logs->count,
732
        1,
733
        "New action log entry has been created when updating repeatable patron attribute with existing multiple values with multiple values"
734
    );
735
736
    $schema->storage->txn_rollback;
737
};

Return to bug 26744