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

(-)a/Koha/Patron.pm (-32 / +94 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 2241-2282 Or setter FIXME Link Here
2241
sub extended_attributes {
2242
sub extended_attributes {
2242
    my ( $self, $attributes ) = @_;
2243
    my ( $self, $attributes ) = @_;
2243
    if ($attributes) {    # setter
2244
    if ($attributes) {    # setter
2244
        my $schema = $self->_result->result_source->schema;
2245
        $schema->txn_do(
2246
            sub {
2247
                # Remove the existing one
2248
                $self->extended_attributes->filter_by_branch_limitations->delete;
2249
2250
                # Insert the new ones
2251
                my $new_types = {};
2252
                for my $attribute (@$attributes) {
2253
                    $self->add_extended_attribute($attribute);
2254
                    $new_types->{$attribute->{code}} = 1;
2255
                }
2256
2245
2257
                # Check globally mandatory types
2246
        my $new_types = {};
2258
                my $interface = C4::Context->interface;
2247
        for my $attribute (@{$attributes}) {
2259
                my $params = {
2248
            $new_types->{$attribute->{code}} = 1;
2260
                    mandatory                                        => 1,
2249
        }
2261
                    category_code                                    => [ undef, $self->categorycode ],
2262
                    'borrower_attribute_types_branches.b_branchcode' => undef,
2263
                };
2264
2250
2265
                if ( $interface eq 'opac' ) {
2251
        # Check globally mandatory types
2266
                    $params->{opac_editable} = 1;
2252
        my $interface = C4::Context->interface;
2267
                }
2253
        my $params = {
2254
            mandatory                                        => 1,
2255
            category_code                                    => [ undef, $self->categorycode ],
2256
            'borrower_attribute_types_branches.b_branchcode' => undef,
2257
        };
2258
2259
        if ( $interface eq 'opac' ) {
2260
            $params->{opac_editable} = 1;
2261
        }
2262
2263
        my @required_attribute_types = Koha::Patron::Attribute::Types->search(
2264
            $params,
2265
            { join => 'borrower_attribute_types_branches' }
2266
        )->get_column('code');
2267
        for my $type (@required_attribute_types) {
2268
            Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
2269
                type => $type,
2270
            ) if !$new_types->{$type};
2271
        }
2272
2273
        my %attribute_changes;
2268
2274
2269
                my @required_attribute_types = Koha::Patron::Attribute::Types->search(
2275
        # Stash changes before
2270
                    $params,
2276
        for my $attribute ($self->extended_attributes->as_list) {
2271
                    { join => 'borrower_attribute_types_branches' }
2277
            my $repeatable = $attribute->type->repeatable ? 1 : 0;
2272
                )->get_column('code');
2278
            $attribute_changes{$repeatable}->{$attribute->code}->{before} //= [];
2273
                for my $type (@required_attribute_types) {
2279
            push (
2274
                    Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
2280
                @{$attribute_changes{$repeatable}->{$attribute->code}->{before}},
2275
                        type => $type,
2281
                $attribute->attribute
2276
                    ) if !$new_types->{$type};
2282
            );
2283
        }
2284
        my @new_attributes = map {
2285
            Koha::Patron::Attribute->new({
2286
                    %{$_},
2287
                    ( borrowernumber => $self->borrowernumber ),
2288
                })
2289
        } @{$attributes};
2290
2291
        # Sort new attributes by code
2292
        @new_attributes = sort {$a->attribute cmp $b->attribute} @new_attributes;
2293
2294
        # Stash changes after
2295
        for my $attribute (values @new_attributes) {
2296
            my $repeatable = $attribute->type->repeatable ? 1 : 0;
2297
            $attribute_changes{$repeatable}->{$attribute->code}->{after} //= [];
2298
            push (
2299
                @{$attribute_changes{$repeatable}->{$attribute->code}->{after}},
2300
                $attribute->attribute
2301
            );
2302
        }
2303
2304
        my $schema = $self->_result->result_source->schema;
2305
        my $is_different = sub {
2306
            my ($a, $b) = map { [ sort @{$_} ] } @_;
2307
            return @{$a} != @{$b} || notall { $_->[0] eq $_->[1] } zip6 @{$a}, @{$b};
2308
        };
2309
        while (my ($repeatable, $changes) = each %attribute_changes) {
2310
            while (my ($code, $change) = each %{$changes}) {
2311
                $change->{before} //= [];
2312
                $change->{after} //= [];
2313
2314
                if ($is_different->($change->{before}, $change->{after})) {
2315
                    unless ($repeatable) {
2316
                        $change->{before} = @{$change->{before}} ? $change->{before}->[0] : '';
2317
                        $change->{after} = @{$change->{after}} ? $change->{after}->[0] : '';
2318
                    }
2319
                    $schema->txn_do(
2320
                        sub {
2321
                            # Remove existing
2322
                            $self->extended_attributes->filter_by_branch_limitations->search({
2323
                                    code => $code,
2324
                                })->delete;
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 " . $code . ": " . to_json($change, { pretty => 1, canonical => 1 })
2335
                                );
2336
                            }
2337
                        }
2338
                    ); 
2277
                }
2339
                }
2278
            }
2340
            }
2279
        );
2341
        }
2280
    }
2342
    }
2281
2343
2282
    my $rs = $self->_result->borrower_attributes;
2344
    my $rs = $self->_result->borrower_attributes;
(-)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