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

(-)a/C4/Auth.pm (-5 / +1 lines)
Lines 1492-1501 sub checkauth { Link Here
1492
    my $auth_template_name = ( $type eq 'opac' ) ? 'opac-auth.tt' : 'auth.tt';
1492
    my $auth_template_name = ( $type eq 'opac' ) ? 'opac-auth.tt' : 'auth.tt';
1493
    my $template           = C4::Templates::gettemplate( $auth_template_name, $type, $query );
1493
    my $template           = C4::Templates::gettemplate( $auth_template_name, $type, $query );
1494
1494
1495
    my $borrowernumber      = $patron and $patron->borrowernumber;
1496
    my $anonymous_patron    = C4::Context->preference('AnonymousPatron');
1497
    my $is_anonymous_patron = $patron && ( $patron->borrowernumber eq $anonymous_patron );
1498
1499
    $template->param(
1495
    $template->param(
1500
        login                                 => 1,
1496
        login                                 => 1,
1501
        INPUTS                                => \@inputs,
1497
        INPUTS                                => \@inputs,
Lines 1530-1536 sub checkauth { Link Here
1530
        opac_css_override                     => $ENV{'OPAC_CSS_OVERRIDE'},
1526
        opac_css_override                     => $ENV{'OPAC_CSS_OVERRIDE'},
1531
        too_many_login_attempts               => ( $patron and $patron->account_locked ),
1527
        too_many_login_attempts               => ( $patron and $patron->account_locked ),
1532
        password_has_expired                  => ( $patron and $patron->password_expired ),
1528
        password_has_expired                  => ( $patron and $patron->password_expired ),
1533
        is_anonymous_patron                   => ($is_anonymous_patron),
1529
        is_anonymous_patron                   => ( $patron and $patron->is_anonymous ),
1534
        password_expiration_date              => ( $patron and $patron->password_expiration_date ),
1530
        password_expiration_date              => ( $patron and $patron->password_expiration_date ),
1535
        date_enrolled                         => ( $patron and $patron->dateenrolled ),
1531
        date_enrolled                         => ( $patron and $patron->dateenrolled ),
1536
        auth_error                            => $auth_error,
1532
        auth_error                            => $auth_error,
(-)a/C4/Biblio.pm (-137 / +1 lines)
Lines 62-68 BEGIN { Link Here
62
        TransformMarcToKoha
62
        TransformMarcToKoha
63
        TransformHtmlToMarc
63
        TransformHtmlToMarc
64
        TransformHtmlToXml
64
        TransformHtmlToXml
65
        prepare_host_field
66
    );
65
    );
67
66
68
    # Internal functions
67
    # Internal functions
Lines 2998-3138 sub ModBiblioMarc { Link Here
2998
    return $biblionumber;
2997
    return $biblionumber;
2999
}
2998
}
3000
2999
3001
=head2 prepare_host_field
3002
3003
$marcfield = prepare_host_field( $hostbiblioitem, $marcflavour );
3004
Generate the host item entry for an analytic child entry
3005
3006
=cut
3007
3008
sub prepare_host_field {
3009
    my ( $hostbiblio, $marcflavour ) = @_;
3010
    $marcflavour ||= C4::Context->preference('marcflavour');
3011
3012
    my $biblio = Koha::Biblios->find($hostbiblio);
3013
    my $host   = $biblio->metadata->record;
3014
3015
    # unfortunately as_string does not 'do the right thing'
3016
    # if field returns undef
3017
    my %sfd;
3018
    my $field;
3019
    my $host_field;
3020
    if ( $marcflavour eq 'MARC21' ) {
3021
        if ( $field = $host->field('100') || $host->field('110') || $host->field('11') ) {
3022
            my $s = $field->as_string('ab');
3023
            if ($s) {
3024
                $sfd{a} = $s;
3025
            }
3026
        }
3027
        if ( $field = $host->field('245') ) {
3028
            my $s = $field->as_string('a');
3029
            if ($s) {
3030
                $sfd{t} = $s;
3031
            }
3032
        }
3033
        if ( $field = $host->field('260') ) {
3034
            my $s = $field->as_string('abc');
3035
            if ($s) {
3036
                $sfd{d} = $s;
3037
            }
3038
        }
3039
        if ( $field = $host->field('240') ) {
3040
            my $s = $field->as_string();
3041
            if ($s) {
3042
                $sfd{b} = $s;
3043
            }
3044
        }
3045
        if ( $field = $host->field('022') ) {
3046
            my $s = $field->as_string('a');
3047
            if ($s) {
3048
                $sfd{x} = $s;
3049
            }
3050
        }
3051
        if ( $field = $host->field('020') ) {
3052
            my $s = $field->as_string('a');
3053
            if ($s) {
3054
                $sfd{z} = $s;
3055
            }
3056
        }
3057
        if ( $field = $host->field('001') ) {
3058
            $sfd{w} = $field->data(),;
3059
        }
3060
        $host_field = MARC::Field->new( 773, '0', ' ', %sfd );
3061
        return $host_field;
3062
    } elsif ( $marcflavour eq 'UNIMARC' ) {
3063
3064
        #author
3065
        if ( $field = $host->field('700') || $host->field('710') || $host->field('720') ) {
3066
            my $s = $field->as_string('ab');
3067
            if ($s) {
3068
                $sfd{a} = $s;
3069
            }
3070
        }
3071
3072
        #title
3073
        if ( $field = $host->field('200') ) {
3074
            my $s = $field->as_string('a');
3075
            if ($s) {
3076
                $sfd{t} = $s;
3077
            }
3078
        }
3079
3080
        #place of publicaton
3081
        if ( $field = $host->field('210') ) {
3082
            my $s = $field->as_string('a');
3083
            if ($s) {
3084
                $sfd{c} = $s;
3085
            }
3086
        }
3087
3088
        #date of publication
3089
        if ( $field = $host->field('210') ) {
3090
            my $s = $field->as_string('d');
3091
            if ($s) {
3092
                $sfd{d} = $s;
3093
            }
3094
        }
3095
3096
        #edition statement
3097
        if ( $field = $host->field('205') ) {
3098
            my $s = $field->as_string();
3099
            if ($s) {
3100
                $sfd{e} = $s;
3101
            }
3102
        }
3103
3104
        #URL
3105
        if ( $field = $host->field('856') ) {
3106
            my $s = $field->as_string('u');
3107
            if ($s) {
3108
                $sfd{u} = $s;
3109
            }
3110
        }
3111
3112
        #ISSN
3113
        if ( $field = $host->field('011') ) {
3114
            my $s = $field->as_string('a');
3115
            if ($s) {
3116
                $sfd{x} = $s;
3117
            }
3118
        }
3119
3120
        #ISBN
3121
        if ( $field = $host->field('010') ) {
3122
            my $s = $field->as_string('a');
3123
            if ($s) {
3124
                $sfd{y} = $s;
3125
            }
3126
        }
3127
        if ( $field = $host->field('001') ) {
3128
            $sfd{0} = $field->data(),;
3129
        }
3130
        $host_field = MARC::Field->new( 461, '0', ' ', %sfd );
3131
        return $host_field;
3132
    }
3133
    return;
3134
}
3135
3136
=head2 UpdateTotalIssues
3000
=head2 UpdateTotalIssues
3137
3001
3138
  UpdateTotalIssues($biblionumber, $increase, [$value])
3002
  UpdateTotalIssues($biblionumber, $increase, [$value])
Lines 3182-3188 sub UpdateTotalIssues { Link Here
3182
    if ( defined $value ) {
3046
    if ( defined $value ) {
3183
        $totalissues = $value;
3047
        $totalissues = $value;
3184
    } else {
3048
    } else {
3185
        $totalissues = $biblioitem->totalissues + $increase;
3049
        $totalissues = $current_issues + $increase;
3186
    }
3050
    }
3187
    return 0 if $current_issues == $totalissues;    # No need to update if no changes
3051
    return 0 if $current_issues == $totalissues;    # No need to update if no changes
3188
3052
(-)a/C4/Circulation.pm (-5 / +5 lines)
Lines 4442-4448 sub ProcessOfflineOperation { Link Here
4442
    if ( $operation->{action} eq 'return' ) {
4442
    if ( $operation->{action} eq 'return' ) {
4443
        $report = ProcessOfflineReturn($operation);
4443
        $report = ProcessOfflineReturn($operation);
4444
    } elsif ( $operation->{action} eq 'issue' ) {
4444
    } elsif ( $operation->{action} eq 'issue' ) {
4445
        $report = ProcessOfflineIssue($operation);
4445
        ($report) = ProcessOfflineIssue($operation);
4446
    } elsif ( $operation->{action} eq 'payment' ) {
4446
    } elsif ( $operation->{action} eq 'payment' ) {
4447
        $report = ProcessOfflinePayment($operation);
4447
        $report = ProcessOfflinePayment($operation);
4448
    }
4448
    }
Lines 4513-4527 sub ProcessOfflineIssue { Link Here
4513
                $operation->{timestamp},
4513
                $operation->{timestamp},
4514
            );
4514
            );
4515
        }
4515
        }
4516
        AddIssue(
4516
        my $checkout = AddIssue(
4517
            $patron,
4517
            $patron,
4518
            $operation->{'barcode'},
4518
            $operation->{barcode},
4519
            undef,
4519
            $operation->{due_date},
4520
            undef,
4520
            undef,
4521
            $operation->{timestamp},
4521
            $operation->{timestamp},
4522
            undef,
4522
            undef,
4523
        );
4523
        );
4524
        return "Success.";
4524
        return ( "Success.", $checkout );
4525
    } else {
4525
    } else {
4526
        return "Borrower not found.";
4526
        return "Borrower not found.";
4527
    }
4527
    }
(-)a/C4/ClassSplitRoutine/LCC.pm (-1 / +1 lines)
Lines 46-52 sub split_callnumber { Link Here
46
    # lccn examples: 'HE8700.7 .P6T44 1983', 'BS2545.E8 H39 1996';
46
    # lccn examples: 'HE8700.7 .P6T44 1983', 'BS2545.E8 H39 1996';
47
    my @lines = Library::CallNumber::LC->new($cn_item)->components();
47
    my @lines = Library::CallNumber::LC->new($cn_item)->components();
48
    unless ( scalar @lines && defined $lines[0] ) {
48
    unless ( scalar @lines && defined $lines[0] ) {
49
        Koha::Logger->get->debug( sprintf( 'regexp failed to match string: %s', $cn_item ) );
49
        Koha::Logger->get->debug( sprintf( 'regexp failed to match string: %s', $cn_item // q{} ) );
50
        @lines = $cn_item;    # if no match, just use the whole string.
50
        @lines = $cn_item;    # if no match, just use the whole string.
51
    }
51
    }
52
    my $LastPiece = pop @lines;
52
    my $LastPiece = pop @lines;
(-)a/C4/Form/MessagingPreferences.pm (+45 lines)
Lines 148-153 PREF: foreach my $option (@$messaging_options) { Link Here
148
    $template->param( messaging_preferences => $messaging_options );
148
    $template->param( messaging_preferences => $messaging_options );
149
}
149
}
150
150
151
=head2 restore_form_values
152
153
    C4::Form::MessagingPreferences::restore_form_values( $input, $template );
154
155
Restores patron message preferences if error occurs while creating a patron.
156
157
C<$input> is the CGI query object.
158
159
C<$template> is the Template::Toolkit object for the response.
160
161
=cut
162
163
sub restore_form_values {
164
    my ( $input, $template ) = @_;
165
    my $messaging_options = C4::Members::Messaging::GetMessagingOptions();
166
    foreach my $option (@$messaging_options) {
167
        $option->{ $option->{'message_name'} } = 1;
168
169
        my $message_attribute_id = $option->{'message_attribute_id'};
170
        if ( $option->{'takes_days'} ) {
171
            my $selected_value  = $input->param( $message_attribute_id . '-DAYS' );
172
            my $days_in_advance = $selected_value ? $selected_value : 0;
173
            $option->{days_in_advance} = $days_in_advance;
174
            @{ $option->{'select_days'} } = map {
175
                {
176
                    day      => $_,
177
                    selected => $_ == $days_in_advance
178
                }
179
            } ( 0 .. MAX_DAYS_IN_ADVANCE );
180
        }
181
182
        my @transport_types = $input->multi_param($message_attribute_id);
183
        foreach my $transport_type (@transport_types) {
184
            $option->{ 'transports_' . $transport_type } = 1;
185
        }
186
187
        if ( $option->{'has_digest'} ) {
188
            if ( List::Util::first { $_ == $message_attribute_id } $input->multi_param('digest') ) {
189
                $option->{'digest'} = 1;
190
            }
191
        }
192
    }
193
    $template->param( messaging_preferences => $messaging_options );
194
}
195
151
=head1 TODO
196
=head1 TODO
152
197
153
=over 4
198
=over 4
(-)a/C4/Items.pm (-1 / +11 lines)
Lines 1732-1738 sub ToggleNewStatus { Link Here
1732
                $item->$field($value);
1732
                $item->$field($value);
1733
                push @{ $report->{$itemnumber} }, $substitution;
1733
                push @{ $report->{$itemnumber} }, $substitution;
1734
            }
1734
            }
1735
            $item->store unless $report_only;
1735
            unless ($report_only) {
1736
                try {
1737
                    $item->store;
1738
                } catch {
1739
                    push @{ $report->{$itemnumber} }, {
1740
                        field => 'ERROR',
1741
                        error => 1,
1742
                        value => $_->error,
1743
                    }
1744
                }
1745
            }
1736
        }
1746
        }
1737
    }
1747
    }
1738
1748
(-)a/C4/Koha.pm (-1 / +1 lines)
Lines 513-519 sub GetAuthorisedValues { Link Here
513
    my $opac     = shift ? 1 : 0;    # normalise to be safe
513
    my $opac     = shift ? 1 : 0;    # normalise to be safe
514
514
515
    # Is this cached already?
515
    # Is this cached already?
516
    my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
516
    my $branch_limit = C4::Context::mybranch();
517
    my $cache_key    = "AuthorisedValues-$category-$opac-$branch_limit";
517
    my $cache_key    = "AuthorisedValues-$category-$opac-$branch_limit";
518
    my $cache        = Koha::Caches->get_instance();
518
    my $cache        = Koha::Caches->get_instance();
519
    my $result       = $cache->get_from_cache($cache_key);
519
    my $result       = $cache->get_from_cache($cache_key);
(-)a/C4/Labels/Label.pm (-2 / +6 lines)
Lines 385-392 sub draw_label_text { Link Here
385
    my $font         = $self->{'font'};
385
    my $font         = $self->{'font'};
386
    my $item         = _get_label_item( $self->{'item_number'} );
386
    my $item         = _get_label_item( $self->{'item_number'} );
387
    my $label_fields = _get_text_fields( $self->{'format_string'} );
387
    my $label_fields = _get_text_fields( $self->{'format_string'} );
388
    my $biblio       = Koha::Biblios->find( $item->{biblionumber} );
388
    my $biblio       = Koha::Biblios->find( $item->{'biblionumber'} );
389
    my $record       = $biblio->metadata->record;
389
    my $record;
390
391
    if ( defined $biblio ) {
392
        $record = $biblio->metadata->record;
393
    }
390
394
391
    # FIXME - returns all items, so you can't get data from an embedded holdings field.
395
    # FIXME - returns all items, so you can't get data from an embedded holdings field.
392
    # TODO - add a GetMarcBiblio1item(bibnum,itemnum) or a GetMarcItem(itemnum).
396
    # TODO - add a GetMarcBiblio1item(bibnum,itemnum) or a GetMarcItem(itemnum).
(-)a/C4/SIP/ILS/Transaction/Checkout.pm (-2 / +4 lines)
Lines 156-168 sub do_checkout { Link Here
156
156
157
    if ($no_block_due_date) {
157
    if ($no_block_due_date) {
158
        $overridden_duedate = $no_block_due_date;
158
        $overridden_duedate = $no_block_due_date;
159
        ProcessOfflineIssue(
159
        my ( $msg, $checkout ) = ProcessOfflineIssue(
160
            {
160
            {
161
                cardnumber => $patron->cardnumber,
161
                cardnumber => $patron->cardnumber,
162
                barcode    => $barcode,
162
                barcode    => $barcode,
163
                timestamp  => $no_block_due_date,
163
                due_date   => $no_block_due_date,
164
                timestamp  => dt_from_string,
164
            }
165
            }
165
        );
166
        );
167
        $self->{due} = $self->duedatefromissue( $checkout, $itemnumber );
166
    } else {
168
    } else {
167
169
168
        # can issue
170
        # can issue
(-)a/C4/Scrubber.pm (-2 / +2 lines)
Lines 29-40 use C4::Context; Link Here
29
my %scrubbertypes = (
29
my %scrubbertypes = (
30
    default => {},    # place holder, default settings are below as fallbacks in call to constructor
30
    default => {},    # place holder, default settings are below as fallbacks in call to constructor
31
    comment => { allow => [qw( br b i em big small strong )], },
31
    comment => { allow => [qw( br b i em big small strong )], },
32
    note    => { allow => [qw[ br b i em big small strong u hr span div p ]] },
32
    note    => { allow => [qw[ br b i em big small strong u hr span div p ol ul li dl dt dd ]] },
33
);
33
);
34
34
35
sub new {
35
sub new {
36
    shift;            # ignore our class we are wrapper
36
    shift;            # ignore our class we are wrapper
37
    my $type = (@_) ? shift : 'default';
37
    my $type = (@_) ? shift : 'default';
38
    $type = 'default' if !defined $type;
38
    if ( !exists $scrubbertypes{$type} ) {
39
    if ( !exists $scrubbertypes{$type} ) {
39
        croak "New called with unrecognized type '$type'";
40
        croak "New called with unrecognized type '$type'";
40
    }
41
    }
Lines 44-50 sub new { Link Here
44
        rules   => exists $settings->{rules}   ? $settings->{rules}   : [],
45
        rules   => exists $settings->{rules}   ? $settings->{rules}   : [],
45
        default => exists $settings->{default} ? $settings->{default} : [ 0 => { '*' => 0 } ],
46
        default => exists $settings->{default} ? $settings->{default} : [ 0 => { '*' => 0 } ],
46
        comment => exists $settings->{comment} ? $settings->{comment} : 0,
47
        comment => exists $settings->{comment} ? $settings->{comment} : 0,
47
        note    => exists $settings->{note}    ? $settings->{note}    : 0,
48
        process => 0,
48
        process => 0,
49
    );
49
    );
50
    return $scrubber;
50
    return $scrubber;
(-)a/C4/Search.pm (-2 / +40 lines)
Lines 40-45 use URI::Escape; Link Here
40
use Business::ISBN;
40
use Business::ISBN;
41
use MARC::Record;
41
use MARC::Record;
42
use MARC::Field;
42
use MARC::Field;
43
use POSIX qw(setlocale LC_COLLATE);
44
use Unicode::Collate::Locale;
43
45
44
our ( @ISA, @EXPORT_OK );
46
our ( @ISA, @EXPORT_OK );
45
47
Lines 291-296 See verbose embedded documentation. Link Here
291
293
292
=cut
294
=cut
293
295
296
=head2 _sort_facets_zebra
297
298
    my $sorted_facets = _sort_facets_zebra($facets, $locale);
299
300
Sorts facets using a configurable locale for Zebra search engine.
301
302
=cut
303
304
sub _sort_facets_zebra {
305
    my ( $facets, $locale ) = @_;
306
307
    if ( !$locale ) {
308
309
        # Get locale from system preference, falling back to system LC_COLLATE
310
        $locale = C4::Context->preference('FacetSortingLocale') || 'default';
311
        if ( $locale eq 'default' || !$locale ) {
312
313
            #NOTE: When setlocale is run with only the 1st parameter, it is a "get" not a "set" function.
314
            $locale = setlocale(LC_COLLATE) || 'default';
315
        }
316
    }
317
318
    my $collator = Unicode::Collate::Locale->new( locale => $locale );
319
    if ( $collator && $facets ) {
320
        my @sorted_facets = sort { $collator->cmp( $a->{facet_label_value}, $b->{facet_label_value} ) } @{$facets};
321
        if (@sorted_facets) {
322
            return \@sorted_facets;
323
        }
324
    }
325
326
    #NOTE: If there was a problem, at least return the not sorted facets
327
    return $facets;
328
}
329
294
sub getRecords {
330
sub getRecords {
295
    my (
331
    my (
296
        $koha_query,       $simple_query, $sort_by_ref, $servers_ref,
332
        $koha_query,       $simple_query, $sort_by_ref, $servers_ref,
Lines 554-561 sub getRecords { Link Here
554
    if (@facets_loop) {
590
    if (@facets_loop) {
555
        foreach my $f (@facets_loop) {
591
        foreach my $f (@facets_loop) {
556
            if ( C4::Context->preference('FacetOrder') eq 'Alphabetical' ) {
592
            if ( C4::Context->preference('FacetOrder') eq 'Alphabetical' ) {
557
                $f->{facets} =
593
                my $sorted_facets = _sort_facets_zebra( $f->{facets} );
558
                    [ sort { uc( $a->{facet_label_value} ) cmp uc( $b->{facet_label_value} ) } @{ $f->{facets} } ];
594
                if ($sorted_facets) {
595
                    $f->{facets} = $sorted_facets;
596
                }
559
            }
597
            }
560
        }
598
        }
561
    }
599
    }
(-)a/Koha.pm (-1 / +1 lines)
Lines 29-35 use vars qw{ $VERSION }; Link Here
29
# - #4 : the developer version. The 4th number is the database subversion.
29
# - #4 : the developer version. The 4th number is the database subversion.
30
#        used by developers when the database changes. updatedatabase take care of the changes itself
30
#        used by developers when the database changes. updatedatabase take care of the changes itself
31
#        and is automatically called by Auth.pm when needed.
31
#        and is automatically called by Auth.pm when needed.
32
$VERSION = "25.06.00.000";
32
$VERSION = "25.06.00.005";
33
33
34
sub version {
34
sub version {
35
    return $VERSION;
35
    return $VERSION;
(-)a/Koha/Acquisition/Order/Claims.pm (-1 / +1 lines)
Lines 25-31 use base qw(Koha::Objects); Link Here
25
25
26
=head1 NAME
26
=head1 NAME
27
27
28
Koha::Cities - Koha Claim Object set class
28
Koha::Acquisition::Order::Claims - Koha Claims Object set class
29
29
30
=head1 API
30
=head1 API
31
31
(-)a/Koha/BackgroundJob/BatchUpdateItem.pm (-1 / +1 lines)
Lines 113-118 sub process { Link Here
113
                callback                          => sub { $self->step; },
113
                callback                          => sub { $self->step; },
114
            }
114
            }
115
        );
115
        );
116
        $report->{errors}               = $results->{errors};
116
        $report->{modified_itemnumbers} = $results->{modified_itemnumbers};
117
        $report->{modified_itemnumbers} = $results->{modified_itemnumbers};
117
        $report->{modified_fields}      = $results->{modified_fields};
118
        $report->{modified_fields}      = $results->{modified_fields};
118
    } catch {
119
    } catch {
Lines 123-129 sub process { Link Here
123
124
124
    my $data = $self->decoded_data;
125
    my $data = $self->decoded_data;
125
    $data->{report} = $report;
126
    $data->{report} = $report;
126
127
    $self->finish($data);
127
    $self->finish($data);
128
}
128
}
129
129
(-)a/Koha/Biblio.pm (-91 / +122 lines)
Lines 1921-2084 sub generate_marc_host_field { Link Here
1921
1921
1922
    my $marcflavour = C4::Context->preference('marcflavour');
1922
    my $marcflavour = C4::Context->preference('marcflavour');
1923
    my $marc_host   = $self->metadata->record;
1923
    my $marc_host   = $self->metadata->record;
1924
    my %sfd;
1924
1925
    my @sfd;
1925
    my $host_field;
1926
    my $host_field;
1926
    my $link_field;
1927
    my $link_field;
1927
1928
1928
    if ( $marcflavour eq 'MARC21' ) {
1929
    if ( $marcflavour eq 'MARC21' ) {
1929
1930
1930
        # Author
1931
        # Attempt to clone the main entry (100, 110, or 111)
1931
        if ( $host_field = $marc_host->field('100') || $marc_host->field('110') || $marc_host->field('111') ) {
1932
        my $main_entry = $marc_host->field('100') || $marc_host->field('110') || $marc_host->field('111');
1932
            my $s = $host_field->as_string('ab');
1933
        $main_entry = $main_entry ? $main_entry->clone : undef;
1933
            if ($s) {
1934
1934
                $sfd{a} = $s;
1935
        # Clean unwanted subfields based on tag type
1936
        if ($main_entry) {
1937
            if ( $main_entry->tag eq '111' ) {
1938
                $main_entry->delete_subfield( code => qr/[94j]/ );
1939
            } else {
1940
                $main_entry->delete_subfield( code => qr/[94e]/ );
1935
            }
1941
            }
1936
        }
1942
        }
1937
1943
1938
        # Edition
1944
        # Construct subfield 7 from leader and main entry tag
1939
        if ( $host_field = $marc_host->field('250') ) {
1945
        my $s7 = "nn" . substr( $marc_host->leader, 6, 2 );
1940
            my $s = $host_field->as_string('ab');
1946
        if ($main_entry) {
1947
            my $c1 = 'n';
1948
            if ( $main_entry->tag =~ /^1[01]/ ) {
1949
                $c1 = $main_entry->indicator('1');
1950
                $c1 = $main_entry->tag eq '100' ? 1 : 2 unless $c1 =~ /\d/;
1951
            }
1952
            my $c0 =
1953
                  ( $main_entry->tag eq '100' ) ? 'p'
1954
                : ( $main_entry->tag eq '110' ) ? 'c'
1955
                : ( $main_entry->tag eq '111' ) ? 'm'
1956
                :                                 'u';
1957
            substr( $s7, 0, 2, $c0 . $c1 );
1958
        }
1959
        push @sfd, ( '7' => $s7 );
1960
1961
        # Subfield a - cleaned main entry string
1962
        if ($main_entry) {
1963
            my $a = $main_entry->as_string;
1964
            $a =~ s/\.$// unless $a =~ /\b[a-z]{1,2}\.$/i;
1965
            push @sfd, ( 'a' => $a );
1966
        }
1967
1968
        # Subfield t - title from 245, cleaned
1969
        if ( my $f245 = $marc_host->field('245') ) {
1970
            my $f245c = $f245->clone;
1971
            $f245c->delete_subfield( code => 'c' );
1972
            my $t = $f245c->as_string;
1973
            $t =~ s/[\s\/\\.]+$//;
1974
            my $nonfiling = $f245c->indicator('2') // 0;
1975
            $nonfiling = 0 unless $nonfiling =~ /^\d+$/;
1976
            $t         = ucfirst substr( $t, $nonfiling );
1977
            push @sfd, ( 't' => $t );
1978
        }
1979
1980
        # Subfield b - edition from 250
1981
        if ( my $f250 = $marc_host->field('250') ) {
1982
            my $b = $f250->as_string;
1983
            $b =~ s/\.$//;
1984
            if ($b) {
1985
                push @sfd, ( 'b' => $b );
1986
            }
1987
        }
1988
1989
        # Subfield s - uniform title from 240
1990
        if ( my $f240 = $marc_host->field('240') ) {
1991
            my $s = $f240->as_string('a');
1941
            if ($s) {
1992
            if ($s) {
1942
                $sfd{b} = $s;
1993
                push @sfd, ( 's' => $s );
1943
            }
1994
            }
1944
        }
1995
        }
1945
1996
1946
        # Publication
1997
        # Subfield d - publication info from 264/260
1998
        my $d;
1947
        my @publication_fields = $marc_host->field('264');
1999
        my @publication_fields = $marc_host->field('264');
1948
        @publication_fields = $marc_host->field('260') unless (@publication_fields);
2000
        @publication_fields = $marc_host->field('260') unless (@publication_fields);
1949
        my $index = 0;
2001
        my $index = 0;
1950
        for my $host_field (@publication_fields) {
2002
        for my $publication_field (@publication_fields) {
1951
2003
1952
            # Use first entry unless we find a preferred indicator1 = 3
2004
            # Use first entry unless we find a preferred indicator1 = 3
1953
            if ( $index == 0 ) {
2005
            if ( $index == 0 ) {
1954
                my $s = $host_field->as_string('abc');
2006
                my $s = $publication_field->as_string('abc');
2007
                $s =~ s/\.$//;
1955
                if ($s) {
2008
                if ($s) {
1956
                    $sfd{d} = $s;
2009
                    $d = $s;
1957
                }
2010
                }
1958
                $index++;
2011
                $index++;
1959
            }
2012
            }
1960
            if ( $host_field->indicator(1) && ( $host_field->indicator(1) eq '3' ) ) {
2013
            if ( $publication_field->indicator(1) && ( $publication_field->indicator(1) eq '3' ) ) {
1961
                my $s = $host_field->as_string('abc');
2014
                my $s = $publication_field->as_string('abc');
2015
                $s =~ s/\.$//;
1962
                if ($s) {
2016
                if ($s) {
1963
                    $sfd{d} = $s;
2017
                    $d = $s;
1964
                }
2018
                }
1965
                last;
2019
                last;
1966
            }
2020
            }
1967
        }
2021
        }
1968
2022
        push @sfd, ( d => $d ) if $d;
1969
        # Uniform title
2023
1970
        if ( $host_field = $marc_host->field('240') ) {
2024
        # Subfield k - host info from 800-830 fields
1971
            my $s = $host_field->as_string('a');
2025
        for my $f ( $marc_host->field('8[013][01]') ) {
1972
            if ($s) {
2026
            my $k = $f->as_string('abcdnjltnp');
1973
                $sfd{s} = $s;
2027
            $k .= ', ISSN ' . $f->subfield('x') if $f->subfield('x');
1974
            }
2028
            $k .= ' ; ' . $f->subfield('v')     if $f->subfield('v');
2029
            push @sfd, ( 'k' => $k );
1975
        }
2030
        }
1976
2031
1977
        # Title
2032
        # Subfield x - ISSN from 022
1978
        if ( $host_field = $marc_host->field('245') ) {
2033
        for my $f ( $marc_host->field('022') ) {
1979
            my $s = $host_field->as_string('abnp');
2034
            push @sfd, ( 'x' => $f->subfield('a') ) if $f->subfield('a');
1980
            if ($s) {
1981
                $sfd{t} = $s;
1982
            }
1983
        }
2035
        }
1984
2036
1985
        # ISSN
2037
        # Subfield z - ISBN from 020
1986
        if ( $host_field = $marc_host->field('022') ) {
2038
        for my $f ( $marc_host->field('020') ) {
1987
            my $s = $host_field->as_string('a');
2039
            push @sfd, ( 'z' => $f->subfield('a') ) if $f->subfield('a');
1988
            if ($s) {
1989
                $sfd{x} = $s;
1990
            }
1991
        }
2040
        }
1992
2041
1993
        # ISBN
2042
        # Subfield w - control number (001 and optionally 003)
1994
        if ( $host_field = $marc_host->field('020') ) {
2043
        if ( C4::Context->preference('UseControlNumber') ) {
1995
            my $s = $host_field->as_string('a');
2044
            if ( my $f001 = $marc_host->field('001') ) {
1996
            if ($s) {
2045
                my $w = $f001->data;
1997
                $sfd{z} = $s;
2046
                if ( my $f003 = $marc_host->field('003') ) {
2047
                    $w = '(' . $f003->data . ')' . $w;
2048
                }
2049
                push @sfd, ( 'w' => $w );
1998
            }
2050
            }
1999
        }
2051
        }
2000
        if ( C4::Context->preference('UseControlNumber') ) {
2001
2052
2002
            # Control number
2053
        # Construct 773 link field
2003
            if ( $host_field = $marc_host->field('001') ) {
2054
        $link_field = MARC::Field->new( 773, '0', ' ', @sfd );
2004
                $sfd{w} = $host_field->data();
2005
            }
2006
2055
2007
            # Control number identifier
2008
            if ( $host_field = $marc_host->field('003') ) {
2009
                $sfd{w} = '(' . $host_field->data() . ')' . $sfd{w};
2010
            }
2011
        }
2012
        $link_field = MARC::Field->new( 773, '0', ' ', %sfd );
2013
    } elsif ( $marcflavour eq 'UNIMARC' ) {
2056
    } elsif ( $marcflavour eq 'UNIMARC' ) {
2014
2057
2015
        # Author
2058
        # Author (700/710/720)
2016
        if ( $host_field = $marc_host->field('700') || $marc_host->field('710') || $marc_host->field('720') ) {
2059
        if ( $host_field = $marc_host->field('700') || $marc_host->field('710') || $marc_host->field('720') ) {
2017
            my $s = $host_field->as_string('ab');
2060
            my $s = $host_field->as_string('ab');
2018
            if ($s) {
2061
            push @sfd, ( 'a' => $s ) if $s;
2019
                $sfd{a} = $s;
2020
            }
2021
        }
2062
        }
2022
2063
2023
        # Place of publication
2064
        # Title (200)
2065
        if ( $host_field = $marc_host->field('200') ) {
2066
            my $s = $host_field->as_string('a');
2067
            push @sfd, ( 't' => $s ) if $s;
2068
        }
2069
2070
        # Place of publication (210$a)
2024
        if ( $host_field = $marc_host->field('210') ) {
2071
        if ( $host_field = $marc_host->field('210') ) {
2025
            my $s = $host_field->as_string('a');
2072
            my $s = $host_field->as_string('a');
2026
            if ($s) {
2073
            push @sfd, ( 'c' => $s ) if $s;
2027
                $sfd{c} = $s;
2028
            }
2029
        }
2074
        }
2030
2075
2031
        # Date of publication
2076
        # Date of publication (210$d)
2032
        if ( $host_field = $marc_host->field('210') ) {
2077
        if ( $host_field = $marc_host->field('210') ) {
2033
            my $s = $host_field->as_string('d');
2078
            my $s = $host_field->as_string('d');
2034
            if ($s) {
2079
            push @sfd, ( 'd' => $s ) if $s;
2035
                $sfd{d} = $s;
2036
            }
2037
        }
2080
        }
2038
2081
2039
        # Edition statement
2082
        # Edition statement (205)
2040
        if ( $host_field = $marc_host->field('205') ) {
2083
        if ( $host_field = $marc_host->field('205') ) {
2041
            my $s = $host_field->as_string();
2084
            my $s = $host_field->as_string;
2042
            if ($s) {
2085
            push @sfd, ( 'e' => $s ) if $s;
2043
                $sfd{e} = $s;
2044
            }
2045
        }
2086
        }
2046
2087
2047
        # Title
2088
        # URL (856$u)
2048
        if ( $host_field = $marc_host->field('200') ) {
2049
            my $s = $host_field->as_string('a');
2050
            if ($s) {
2051
                $sfd{t} = $s;
2052
            }
2053
        }
2054
2055
        #URL
2056
        if ( $host_field = $marc_host->field('856') ) {
2089
        if ( $host_field = $marc_host->field('856') ) {
2057
            my $s = $host_field->as_string('u');
2090
            my $s = $host_field->as_string('u');
2058
            if ($s) {
2091
            push @sfd, ( u => $s ) if ($s);
2059
                $sfd{u} = $s;
2060
            }
2061
        }
2092
        }
2062
2093
2063
        # ISSN
2094
        # ISSN (011$a)
2064
        if ( $host_field = $marc_host->field('011') ) {
2095
        if ( $host_field = $marc_host->field('011') ) {
2065
            my $s = $host_field->as_string('a');
2096
            my $s = $host_field->as_string('a');
2066
            if ($s) {
2097
            push @sfd, ( x => $s ) if ($s);
2067
                $sfd{x} = $s;
2068
            }
2069
        }
2098
        }
2070
2099
2071
        # ISBN
2100
        # ISBN (010$a)
2072
        if ( $host_field = $marc_host->field('010') ) {
2101
        if ( $host_field = $marc_host->field('010') ) {
2073
            my $s = $host_field->as_string('a');
2102
            my $s = $host_field->as_string('a');
2074
            if ($s) {
2103
            push @sfd, ( y => $s ) if ($s);
2075
                $sfd{y} = $s;
2076
            }
2077
        }
2104
        }
2105
2106
        # Control number (001)
2078
        if ( $host_field = $marc_host->field('001') ) {
2107
        if ( $host_field = $marc_host->field('001') ) {
2079
            $sfd{0} = $host_field->data();
2108
            push @sfd, ( 0 => $host_field->data() );
2080
        }
2109
        }
2081
        $link_field = MARC::Field->new( 461, '0', ' ', %sfd );
2110
2111
        # Construct 461 link field
2112
        $link_field = MARC::Field->new( 461, '0', ' ', @sfd );
2082
    }
2113
    }
2083
2114
2084
    return $link_field;
2115
    return $link_field;
(-)a/Koha/CookieManager.pm (-41 / +89 lines)
Lines 25-31 use CGI::Cookie; Link Here
25
25
26
use C4::Context;
26
use C4::Context;
27
27
28
use constant DENY_LIST_VAR => 'do_not_remove_cookie';
28
# The cookies on the following list are removed unless koha-conf.xml indicates otherwise.
29
# koha-conf.xml may also contain additional cookies to be removed.
30
# Not including here: always_show_holds, catalogue_editor_, issues-table-load-immediately-circulation,
31
# and ItemEditorSessionTemplateId.
32
# TODO Not sure about branch (rotatingcollections), patronSessionConfirmation (circulation.pl)
33
use constant MANAGED_COOKIE_PREFIXES => qw{
34
    bib_list
35
    CGISESSID
36
    holdfor holdforclub
37
    intranet_bib_list
38
    JWT
39
    KohaOpacLanguage
40
    LastCreatedItem
41
    marctagstructure_selectdisplay
42
    search_path_code
43
    searchToOrder
44
};
45
use constant PATH_EXCEPTIONS => {
46
    always_show_holds => '/cgi-bin/koha/reserve',
47
};
48
use constant KEEP_COOKIE_CONF_VAR   => 'do_not_remove_cookie';
49
use constant REMOVE_COOKIE_CONF_VAR => 'remove_cookie';
29
50
30
our $cookies;
51
our $cookies;
31
52
Lines 41-58 Koha::CookieManager - Object for unified handling of cookies in Koha Link Here
41
    # Replace cookies
62
    # Replace cookies
42
    $cookie_list = $mgr->replace_in_list( [ $cookie1, $cookie2_old ], $cookie2_new );
63
    $cookie_list = $mgr->replace_in_list( [ $cookie1, $cookie2_old ], $cookie2_new );
43
64
44
    # Clear cookies (governed by deny list entries in koha-conf)
65
    # Clear cookies
45
    $cookie_list = $mgr->clear_unless( $cookie1, $cookie2, $cookie3_name );
66
    $cookie_list = $mgr->clear_unless( $cookie1, $cookie2, $cookie3_name );
46
67
47
=head1 DESCRIPTION
68
=head1 DESCRIPTION
48
69
49
The current object allows you to clear cookies in a list based on the deny list
70
The current object allows you to remove cookies on the hardcoded list
50
in koha-conf.xml. It also offers a method to replace the old version of a cookie
71
in this module, refined by 'keep' or 'remove' entries in koha-conf.xml.
72
Note that a keep entry overrules a remove.
73
74
This module also offers a method to replace the old version of a cookie
51
by a new one.
75
by a new one.
52
76
53
It could be extended by (gradually) routing cookie creation through it in order
77
The module could be extended by (gradually) routing cookie creation
54
to consistently fill cookie parameters like httponly, secure and samesite flag,
78
through it in order to consistently fill cookie parameters like httponly,
55
etc. And could serve to register all our cookies in a central location.
79
secure and samesite flag, etc. And could serve to register all our cookies
80
in a central location.
56
81
57
=head1 METHODS
82
=head1 METHODS
58
83
Lines 64-74 etc. And could serve to register all our cookies in a central location. Link Here
64
89
65
sub new {
90
sub new {
66
    my ( $class, $params ) = @_;
91
    my ( $class, $params ) = @_;
67
    my $self   = bless $params // {}, $class;
92
    my $self = bless $params // {}, $class;
68
    my $denied = C4::Context->config(DENY_LIST_VAR) || [];    # expecting scalar or arrayref
93
69
    $denied                 = [$denied] if ref($denied) eq q{};
94
    # Get keep and remove list from koha-conf (scalar or arrayref)
70
    $self->{_remove_unless} = { map { $_ => 1 } @$denied };
95
    my $keep_list = C4::Context->config(KEEP_COOKIE_CONF_VAR) || [];
71
    $self->{_secure}        = C4::Context->https_enabled;
96
    $self->{_keep_list} = ref($keep_list) ? $keep_list : [$keep_list];
97
    my $remove_list = C4::Context->config(REMOVE_COOKIE_CONF_VAR) || [];
98
    $self->{_remove_list} = ref($remove_list) ? $remove_list : [$remove_list];
99
100
    $self->{_secure} = C4::Context->https_enabled;
72
    return $self;
101
    return $self;
73
}
102
}
74
103
Lines 80-88 sub new { Link Here
80
    Note: in the example above $query->cookie is a list of cookie names as returned
109
    Note: in the example above $query->cookie is a list of cookie names as returned
81
    by the CGI object.
110
    by the CGI object.
82
111
83
    Returns an arrayref of cookie objects: empty, expired cookies for those passed
112
    Returns an arrayref of cookie objects: empty, expired cookies for
84
    by name or objects that are not on the deny list, together with the remaining
113
    cookies on the remove list, together with the remaining (untouched)
85
    (untouched) cookie objects that are on the deny list.
114
    cookie objects.
86
115
87
=cut
116
=cut
88
117
Lines 102-118 sub clear_unless { Link Here
102
        }
131
        }
103
        next if !$name;
132
        next if !$name;
104
133
105
        if ( $self->_should_be_cleared($name) ) {
134
        if ( $self->_should_be_removed($name) ) {
106
            next if $seen->{$name};
135
            next if $seen->{$name};
107
            push @rv, CGI::Cookie->new(
108
109
                # -expires explicitly omitted to create shortlived 'session' cookie
110
                # -HttpOnly explicitly set to 0: not really needed here for the
111
                # cleared httponly cookies, while the js cookies should be 0
112
                -name => $name, -value => q{}, -HttpOnly => 0,
113
                $self->{_secure} ? ( -secure => 1 ) : (),
114
            );
115
            $seen->{$name} = 1;    # prevent duplicates
136
            $seen->{$name} = 1;    # prevent duplicates
137
            if ($type) {
138
                $c->max_age(0);
139
                push @rv, _correct_path($c);
140
            } else {
141
                push @rv, _correct_path( CGI::Cookie->new( -name => $name, -value => q{}, '-max-age' => 0 ) );
142
            }
116
        } elsif ( $type eq 'CGI::Cookie' ) {    # keep the last occurrence
143
        } elsif ( $type eq 'CGI::Cookie' ) {    # keep the last occurrence
117
            @rv = @{ $self->replace_in_list( \@rv, $c ) };
144
            @rv = @{ $self->replace_in_list( \@rv, $c ) };
118
        }
145
        }
Lines 120-141 sub clear_unless { Link Here
120
    return \@rv;
147
    return \@rv;
121
}
148
}
122
149
123
sub _should_be_cleared {    # when it is not on the deny list in koha-conf
124
    my ( $self, $name ) = @_;
125
126
    return if $self->{_remove_unless}->{$name};    # exact match
127
128
    # Now try the entries as regex
129
    foreach my $k ( keys %{ $self->{_remove_unless} } ) {
130
        my $reg = $self->{_remove_unless}->{$k};
131
132
        # The entry in koha-conf should match the complete string
133
        # So adding a ^ and $
134
        return if $name =~ qr/^${k}$/;
135
    }
136
    return 1;
137
}
138
139
=head2 replace_in_list
150
=head2 replace_in_list
140
151
141
    $list2 = $mgr->replace_in_list( $list1, $cookie );
152
    $list2 = $mgr->replace_in_list( $list1, $cookie );
Lines 162-168 sub replace_in_list { Link Here
162
    return \@result;
173
    return \@result;
163
}
174
}
164
175
165
=head1 INTERNAL ROUTINES
176
# INTERNAL ROUTINES
177
178
sub _should_be_removed {
179
    my ( $self, $name ) = @_;
180
181
    # Is this a controlled cookie? Or is it added as 'keep' or 'remove' in koha-conf.xml?
182
    # The conf entries are treated as prefix (no longer as regex).
183
    return unless grep { $name =~ /^$_/ } MANAGED_COOKIE_PREFIXES(), @{ $self->{_remove_list} };
184
    return !grep { $name =~ /^$_/ } @{ $self->{_keep_list} };
185
}
186
187
sub _correct_path {
188
    my $cookie_object = shift;
189
    my $path          = PATH_EXCEPTIONS->{ $cookie_object->name } or return $cookie_object;
190
    $cookie_object->path($path);
191
    return $cookie_object;
192
}
193
194
=head1 ADDITIONAL COMMENTS
195
196
    How do the keep or remove lines in koha-conf.xml work?
197
198
    <do_not_remove_cookie>some_cookie</do_not_remove_cookie>
199
    The name some_cookie should refer here to a cookie that is on the
200
    hardcoded list in this module. If you do not want it to be cleared
201
    (removed) on logout, include this line.
202
    You might want to do this e.g. for KohaOpacLanguage.
203
204
    <remove_cookie>another_cookie</remove_cookie>
205
    The name another_cookie refers here to a cookie that is not on the
206
    hardcoded list but you want this cookie to be cleared/removed on logout.
207
    It could be a custom cookie.
208
209
    Note that both directives use the cookie name as a prefix. So if you
210
    add a remove line for cookie1, it also affects cookie12, etc.
211
    Since a keep line overrules a remove line, this allows you to add
212
    lines for removing cookie1 and not removing cookie12 in order to
213
    remove cookie1, cookie11, cookie13 but not cookie12, etc.
166
214
167
=cut
215
=cut
168
216
(-)a/Koha/Course.pm (-10 / +10 lines)
Lines 2-19 package Koha::Course; Link Here
2
2
3
# This file is part of Koha.
3
# This file is part of Koha.
4
#
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
5
# Koha is free software; you can redistribute it and/or modify it
6
# terms of the GNU General Public License as published by the Free Software
6
# under the terms of the GNU General Public License as published by
7
# Foundation; either version 3 of the License, or (at your option) any later
7
# the Free Software Foundation; either version 3 of the License, or
8
# version.
8
# (at your option) any later version.
9
#
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10
# Koha is distributed in the hope that it will be useful, but
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
13
#
14
#
14
# You should have received a copy of the GNU General Public License along
15
# You should have received a copy of the GNU General Public License
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
(-)a/Koha/Course/Instructor.pm (-10 / +10 lines)
Lines 2-19 package Koha::Course::Instructor; Link Here
2
2
3
# This file is part of Koha.
3
# This file is part of Koha.
4
#
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
5
# Koha is free software; you can redistribute it and/or modify it
6
# terms of the GNU General Public License as published by the Free Software
6
# under the terms of the GNU General Public License as published by
7
# Foundation; either version 3 of the License, or (at your option) any later
7
# the Free Software Foundation; either version 3 of the License, or
8
# version.
8
# (at your option) any later version.
9
#
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10
# Koha is distributed in the hope that it will be useful, but
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
13
#
14
#
14
# You should have received a copy of the GNU General Public License along
15
# You should have received a copy of the GNU General Public License
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
(-)a/Koha/Course/Instructors.pm (-10 / +10 lines)
Lines 2-19 package Koha::Course::Instructors; Link Here
2
2
3
# This file is part of Koha.
3
# This file is part of Koha.
4
#
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
5
# Koha is free software; you can redistribute it and/or modify it
6
# terms of the GNU General Public License as published by the Free Software
6
# under the terms of the GNU General Public License as published by
7
# Foundation; either version 3 of the License, or (at your option) any later
7
# the Free Software Foundation; either version 3 of the License, or
8
# version.
8
# (at your option) any later version.
9
#
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10
# Koha is distributed in the hope that it will be useful, but
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
13
#
14
#
14
# You should have received a copy of the GNU General Public License along
15
# You should have received a copy of the GNU General Public License
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
(-)a/Koha/Course/Item.pm (-10 / +10 lines)
Lines 2-19 package Koha::Course::Item; Link Here
2
2
3
# This file is part of Koha.
3
# This file is part of Koha.
4
#
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
5
# Koha is free software; you can redistribute it and/or modify it
6
# terms of the GNU General Public License as published by the Free Software
6
# under the terms of the GNU General Public License as published by
7
# Foundation; either version 3 of the License, or (at your option) any later
7
# the Free Software Foundation; either version 3 of the License, or
8
# version.
8
# (at your option) any later version.
9
#
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10
# Koha is distributed in the hope that it will be useful, but
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
13
#
14
#
14
# You should have received a copy of the GNU General Public License along
15
# You should have received a copy of the GNU General Public License
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
(-)a/Koha/Course/Items.pm (-10 / +10 lines)
Lines 2-19 package Koha::Course::Items; Link Here
2
2
3
# This file is part of Koha.
3
# This file is part of Koha.
4
#
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
5
# Koha is free software; you can redistribute it and/or modify it
6
# terms of the GNU General Public License as published by the Free Software
6
# under the terms of the GNU General Public License as published by
7
# Foundation; either version 3 of the License, or (at your option) any later
7
# the Free Software Foundation; either version 3 of the License, or
8
# version.
8
# (at your option) any later version.
9
#
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10
# Koha is distributed in the hope that it will be useful, but
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
13
#
14
#
14
# You should have received a copy of the GNU General Public License along
15
# You should have received a copy of the GNU General Public License
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
(-)a/Koha/Course/Reserve.pm (-10 / +10 lines)
Lines 2-19 package Koha::Course::Reserve; Link Here
2
2
3
# This file is part of Koha.
3
# This file is part of Koha.
4
#
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
5
# Koha is free software; you can redistribute it and/or modify it
6
# terms of the GNU General Public License as published by the Free Software
6
# under the terms of the GNU General Public License as published by
7
# Foundation; either version 3 of the License, or (at your option) any later
7
# the Free Software Foundation; either version 3 of the License, or
8
# version.
8
# (at your option) any later version.
9
#
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10
# Koha is distributed in the hope that it will be useful, but
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
13
#
14
#
14
# You should have received a copy of the GNU General Public License along
15
# You should have received a copy of the GNU General Public License
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
(-)a/Koha/Course/Reserves.pm (-10 / +10 lines)
Lines 2-19 package Koha::Course::Reserves; Link Here
2
2
3
# This file is part of Koha.
3
# This file is part of Koha.
4
#
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
5
# Koha is free software; you can redistribute it and/or modify it
6
# terms of the GNU General Public License as published by the Free Software
6
# under the terms of the GNU General Public License as published by
7
# Foundation; either version 3 of the License, or (at your option) any later
7
# the Free Software Foundation; either version 3 of the License, or
8
# version.
8
# (at your option) any later version.
9
#
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10
# Koha is distributed in the hope that it will be useful, but
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
13
#
14
#
14
# You should have received a copy of the GNU General Public License along
15
# You should have received a copy of the GNU General Public License
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
(-)a/Koha/Courses.pm (-10 / +10 lines)
Lines 2-19 package Koha::Courses; Link Here
2
2
3
# This file is part of Koha.
3
# This file is part of Koha.
4
#
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
5
# Koha is free software; you can redistribute it and/or modify it
6
# terms of the GNU General Public License as published by the Free Software
6
# under the terms of the GNU General Public License as published by
7
# Foundation; either version 3 of the License, or (at your option) any later
7
# the Free Software Foundation; either version 3 of the License, or
8
# version.
8
# (at your option) any later version.
9
#
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10
# Koha is distributed in the hope that it will be useful, but
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
13
#
14
#
14
# You should have received a copy of the GNU General Public License along
15
# You should have received a copy of the GNU General Public License
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
(-)a/Koha/CoverImages.pm (-1 / +1 lines)
Lines 25-31 use base qw(Koha::Objects); Link Here
25
25
26
=head1 NAME
26
=head1 NAME
27
27
28
Koha::Cities - Koha CoverImage Object set class
28
Koha::CoverImages - Koha CoverImage Object set class
29
29
30
=head1 API
30
=head1 API
31
31
(-)a/Koha/Devel/Files.pm (+192 lines)
Line 0 Link Here
1
package Koha::Devel::Files;
2
3
use Modern::Perl;
4
our ( @ISA, @EXPORT_OK );
5
6
=head1 NAME
7
8
Koha::Devel::Files - A utility module for managing and filtering file lists in the Koha codebase
9
10
=head1 SYNOPSIS
11
12
    use Koha::Devel::Files;
13
14
    my $file_manager = Koha::Devel::Files->new({ context => 'tidy' });
15
16
    my @perl_files = $file_manager->ls_perl_files($git_range);
17
    my @js_files   = $file_manager->ls_js_files();
18
    my @tt_files   = $file_manager->ls_tt_files();
19
20
    my $filetype = $file_manager->get_filetype($filename);
21
22
=head1 DESCRIPTION
23
24
Koha::Devel::Files is a utility module designed to assist in managing and filtering lists of files in the Koha codebase. It provides methods to list Perl, JavaScript, and Template Toolkit files, with options to exclude specific files based on a given context.
25
26
=head1 EXCEPTIONS
27
28
The module defines a set of exceptions for different file types and contexts. These exceptions are used to exclude specific files or directories from the file listings.
29
30
=cut
31
32
my $exceptions = {
33
    pl => {
34
        tidy => [
35
            qw(
36
                Koha/Schema/Result
37
                Koha/Schema.pm
38
            )
39
        ],
40
        valid => [
41
            qw(
42
                Koha/Account/Credit.pm
43
                Koha/Account/Debit.pm
44
                Koha/Old/Hold.pm
45
                misc/translator/TmplTokenizer.pm
46
            )
47
        ],
48
        codespell => [
49
            qw(
50
                installer/data/mysql/updatedatabase.pl
51
                installer/data/mysql/update22to30.pl
52
                installer/data/mysql/db_revs/241200035.pl
53
                misc/cronjobs/build_browser_and_cloud.pl
54
            )
55
        ],
56
    },
57
    js => {
58
        tidy => [
59
            qw(
60
                koha-tmpl/intranet-tmpl/lib
61
                koha-tmpl/intranet-tmpl/js/Gettext.js
62
                koha-tmpl/opac-tmpl/lib
63
                Koha/ILL/Backend/
64
            )
65
        ],
66
        codespell => [
67
            qw(
68
                koha-tmpl/intranet-tmpl/lib
69
                koha-tmpl/intranet-tmpl/js/Gettext.js
70
                koha-tmpl/opac-tmpl/lib
71
                koha-tmpl/opac-tmpl/bootstrap/js/Gettext.js
72
            )
73
        ],
74
    },
75
    tt => {
76
        tidy => [
77
            qw(
78
                Koha/ILL/Backend/
79
                *doc-head-open.inc
80
                misc/cronjobs/rss
81
            )
82
        ],
83
        codespell => [],
84
    },
85
};
86
87
=head1 METHODS
88
89
=cut
90
91
=head2 new
92
93
    my $file_manager = Koha::Devel::Files->new({ context => 'tidy' });
94
95
Creates a new instance of Koha::Devel::Files. The constructor accepts a hash reference with a 'context' key, which specifies the context for file exclusions.
96
97
=cut
98
99
sub new {
100
    my ( $class, $args ) = @_;
101
    my $self = { context => $args->{context} };
102
    bless $self, $class;
103
    return $self;
104
}
105
106
=head2 build_git_exclude
107
108
    my $exclude_pattern = $file_manager->build_git_exclude($filetype);
109
110
Builds a Git exclude pattern for a given file type based on the context provided during object creation.
111
112
=cut
113
114
sub build_git_exclude {
115
    my ( $self, $filetype ) = @_;
116
    return $self->{context} && exists $exceptions->{$filetype}->{ $self->{context} }
117
        ? join( " ", map( "':(exclude)$_'", @{ $exceptions->{$filetype}->{ $self->{context} } } ) )
118
        : q{};
119
}
120
121
=head2 ls_perl_files
122
123
    my @perl_files = $file_manager->ls_perl_files($git_range);
124
125
Lists Perl files (with extensions .pl, .PL, .pm, .t) that have been modified within a specified Git range. If no range is provided, it lists all Perl files, excluding those specified in the exceptions.
126
127
=cut
128
129
sub ls_perl_files {
130
    my ($self) = @_;
131
    my $cmd = sprintf q{git ls-files '*.pl' '*.PL' '*.pm' '*.t' svc opac/svc debian/build-git-snapshot %s},
132
        $self->build_git_exclude('pl');
133
    my @files = qx{$cmd};
134
    chomp for @files;
135
    return @files;
136
}
137
138
=head2 ls_js_files
139
140
    my @js_files = $file_manager->ls_js_files();
141
142
Lists JavaScript and TypeScript files (with extensions .js, .ts, .vue) in the repository, excluding those specified in the exceptions.
143
144
=cut
145
146
sub ls_js_files {
147
    my ($self) = @_;
148
    my $cmd    = sprintf q{git ls-files '*.js' '*.ts' '*.vue' %s}, $self->build_git_exclude('js');
149
    my @files  = qx{$cmd};
150
    chomp for @files;
151
    return @files;
152
}
153
154
=head2 ls_tt_files
155
156
    my @tt_files = $file_manager->ls_tt_files();
157
158
Lists Template Toolkit files (with extensions .tt, .inc) in the repository, excluding those specified in the exceptions.
159
160
=cut
161
162
sub ls_tt_files {
163
    my ($self) = @_;
164
    my $cmd    = sprintf q{git ls-files '*.tt' '*.inc' %s}, $self->build_git_exclude('tt');
165
    my @files  = qx{$cmd};
166
    chomp for @files;
167
    return @files;
168
}
169
170
=head2 get_filetype
171
172
    my $filetype = $file_manager->get_filetype($filename);
173
174
Determines the file type of a given file based on its extension or path. Returns 'pl' for Perl files, 'js' for JavaScript/TypeScript files, and 'tt' for Template Toolkit files. Dies with an error message if the file type cannot be determined.
175
176
=cut
177
178
sub get_filetype {
179
    my ( $self, $file ) = @_;
180
    return 'pl' if $file =~ m{^svc}  || $file =~ m{^opac/svc};
181
    return 'pl' if $file =~ m{\.pl$} || $file =~ m{\.pm} || $file =~ m{\.t$};
182
    return 'pl' if $file =~ m{\.PL$};
183
    return 'pl' if $file =~ m{debian/build-git-snapshot};
184
185
    return 'js' if $file =~ m{\.js$} || $file =~ m{\.ts$} || $file =~ m{\.vue$};
186
187
    return 'tt' if $file =~ m{\.inc$} || $file =~ m{\.tt$};
188
189
    die sprintf 'Cannot guess filetype for %s', $file;
190
}
191
192
1;
(-)a/Koha/Encryption.pm (-2 / +3 lines)
Lines 61-68 sub new { Link Here
61
        );
61
        );
62
    }
62
    }
63
    return $class->SUPER::new(
63
    return $class->SUPER::new(
64
        -key    => $encryption_key,
64
        -key         => $encryption_key,
65
        -cipher => 'Cipher::AES'
65
        -cipher      => 'Cipher::AES',
66
        -nodeprecate => 1,
66
    );
67
    );
67
}
68
}
68
69
(-)a/Koha/Exceptions/MarcOverlayRule.pm (-10 / +10 lines)
Lines 2-19 package Koha::Exceptions::MarcOverlayRule; Link Here
2
2
3
# This file is part of Koha.
3
# This file is part of Koha.
4
#
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
5
# Koha is free software; you can redistribute it and/or modify it
6
# terms of the GNU General Public License as published by the Free Software
6
# under the terms of the GNU General Public License as published by
7
# Foundation; either version 3 of the License, or (at your option) any later
7
# the Free Software Foundation; either version 3 of the License, or
8
# version.
8
# (at your option) any later version.
9
#
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10
# Koha is distributed in the hope that it will be useful, but
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
13
#
14
#
14
# You should have received a copy of the GNU General Public License along
15
# You should have received a copy of the GNU General Public License
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
(-)a/Koha/Filter/MARC/TrimFields.pm (-2 / +8 lines)
Lines 60-68 sub filter { Link Here
60
                my $value = $subfield->[1];
60
                my $value = $subfield->[1];
61
                $value =~ s/[\n\r]+/ /g;
61
                $value =~ s/[\n\r]+/ /g;
62
                $value =~ s/^\s+|\s+$//g;
62
                $value =~ s/^\s+|\s+$//g;
63
                $field->add_subfields( $key => $value );    # add subfield to the end of the subfield list
63
                $field->add_subfields( $key => $value )
64
                $field->delete_subfield( pos => 0 );        # delete the subfield at the top of the subfield list
64
                    if $value ne q{}
65
                    ; # add subfield to the end of the subfield list, but only if there is still a non empty value there
66
                $field->delete_subfield( pos => 0 );    # delete the subfield at the top of the subfield list
65
            }
67
            }
68
69
            # if it happed that all existing subfields had whitespaces only,
70
            # the field would be empty now and should be removed from the record
71
            $record->delete_fields($field) unless scalar( $field->subfields );
66
        }
72
        }
67
    }
73
    }
68
    return $record;
74
    return $record;
(-)a/Koha/Hold.pm (+3 lines)
Lines 972-977 sub store { Link Here
972
                }
972
                }
973
            }
973
            }
974
        }
974
        }
975
        if ( exists $updated_columns{branchcode} ) {
976
            Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue( { biblio_ids => [ $self->biblionumber ] } );
977
        }
975
    }
978
    }
976
979
977
    $self = $self->SUPER::store;
980
    $self = $self->SUPER::store;
(-)a/Koha/I18N.pm (+262 lines)
Lines 53-60 our @EXPORT = qw( Link Here
53
    N__np
53
    N__np
54
);
54
);
55
55
56
our @EXPORT_OK = qw(
57
    available_locales
58
);
59
56
our $textdomain = 'Koha';
60
our $textdomain = 'Koha';
57
61
62
=head1 NAME
63
64
Koha::I18N - Internationalization functions for Koha
65
66
=head1 SYNOPSIS
67
68
    use Koha::I18N;
69
70
    # Basic translation functions
71
    my $translated = __('Hello world');
72
    my $with_vars = __x('Hello {name}', name => 'World');
73
    my $plural = __n('one item', '{count} items', $count, count => $count);
74
75
    # Context-aware translations
76
    my $context = __p('menu', 'File');
77
78
    # Get available system locales (explicitly imported)
79
    use Koha::I18N qw(available_locales);
80
    my $locales = available_locales();
81
82
=head1 DESCRIPTION
83
84
This module provides internationalization (i18n) functions for Koha using the
85
GNU gettext system. It handles locale setup, message translation, and provides
86
utility functions for working with system locales.
87
88
The module automatically initializes the locale environment and provides a set
89
of translation functions that support variable substitution, plural forms, and
90
contextual translations.
91
92
=head1 FUNCTIONS
93
94
=head2 init
95
96
Initializes the internationalization system by setting up locale environment
97
variables and configuring gettext. This is called automatically when needed.
98
99
=cut
100
58
sub init {
101
sub init {
59
    my $cache     = Koha::Cache::Memory::Lite->get_instance();
102
    my $cache     = Koha::Cache::Memory::Lite->get_instance();
60
    my $cache_key = 'i18n:initialized';
103
    my $cache_key = 'i18n:initialized';
Lines 92-97 sub init { Link Here
92
    }
135
    }
93
}
136
}
94
137
138
=head2 __
139
140
    my $translated = __('Text to translate');
141
142
Basic translation function. Returns the translated text for the given message ID.
143
144
=cut
145
95
sub __ {
146
sub __ {
96
    my ($msgid) = @_;
147
    my ($msgid) = @_;
97
148
Lines 100-105 sub __ { Link Here
100
    return _gettext( \&gettext, [$msgid] );
151
    return _gettext( \&gettext, [$msgid] );
101
}
152
}
102
153
154
=head2 __x
155
156
    my $translated = __x('Hello {name}', name => 'World');
157
158
Translation with variable substitution. Variables in {brackets} are replaced
159
with the corresponding values from the provided hash.
160
161
=cut
162
103
sub __x {
163
sub __x {
104
    my ( $msgid, %vars ) = @_;
164
    my ( $msgid, %vars ) = @_;
105
165
Lines 108-113 sub __x { Link Here
108
    return _gettext( \&gettext, [$msgid], %vars );
168
    return _gettext( \&gettext, [$msgid], %vars );
109
}
169
}
110
170
171
=head2 __n
172
173
    my $translated = __n('one item', '{count} items', $count);
174
175
Plural-aware translation. Returns singular or plural form based on the count.
176
177
=cut
178
111
sub __n {
179
sub __n {
112
    my ( $msgid, $msgid_plural, $count ) = @_;
180
    my ( $msgid, $msgid_plural, $count ) = @_;
113
181
Lines 117-122 sub __n { Link Here
117
    return _gettext( \&ngettext, [ $msgid, $msgid_plural, $count ] );
185
    return _gettext( \&ngettext, [ $msgid, $msgid_plural, $count ] );
118
}
186
}
119
187
188
=head2 __nx
189
190
    my $translated = __nx('one item', '{count} items', $count, count => $count);
191
192
Plural-aware translation with variable substitution.
193
194
=cut
195
120
sub __nx {
196
sub __nx {
121
    my ( $msgid, $msgid_plural, $count, %vars ) = @_;
197
    my ( $msgid, $msgid_plural, $count, %vars ) = @_;
122
198
Lines 126-135 sub __nx { Link Here
126
    return _gettext( \&ngettext, [ $msgid, $msgid_plural, $count ], %vars );
202
    return _gettext( \&ngettext, [ $msgid, $msgid_plural, $count ], %vars );
127
}
203
}
128
204
205
=head2 __xn
206
207
Alias for __nx.
208
209
=cut
210
129
sub __xn {
211
sub __xn {
130
    return __nx(@_);
212
    return __nx(@_);
131
}
213
}
132
214
215
=head2 __p
216
217
    my $translated = __p('context', 'Text to translate');
218
219
Context-aware translation. Allows the same text to be translated differently
220
based on context (e.g., 'File' in a menu vs 'File' as a document type).
221
222
=cut
223
133
sub __p {
224
sub __p {
134
    my ( $msgctxt, $msgid ) = @_;
225
    my ( $msgctxt, $msgid ) = @_;
135
226
Lines 139-144 sub __p { Link Here
139
    return _gettext( \&pgettext, [ $msgctxt, $msgid ] );
230
    return _gettext( \&pgettext, [ $msgctxt, $msgid ] );
140
}
231
}
141
232
233
=head2 __px
234
235
    my $translated = __px('context', 'Hello {name}', name => 'World');
236
237
Context-aware translation with variable substitution.
238
239
=cut
240
142
sub __px {
241
sub __px {
143
    my ( $msgctxt, $msgid, %vars ) = @_;
242
    my ( $msgctxt, $msgid, %vars ) = @_;
144
243
Lines 148-153 sub __px { Link Here
148
    return _gettext( \&pgettext, [ $msgctxt, $msgid ], %vars );
247
    return _gettext( \&pgettext, [ $msgctxt, $msgid ], %vars );
149
}
248
}
150
249
250
=head2 __np
251
252
    my $translated = __np('context', 'one item', '{count} items', $count);
253
254
Context-aware plural translation.
255
256
=cut
257
151
sub __np {
258
sub __np {
152
    my ( $msgctxt, $msgid, $msgid_plural, $count ) = @_;
259
    my ( $msgctxt, $msgid, $msgid_plural, $count ) = @_;
153
260
Lines 158-163 sub __np { Link Here
158
    return _gettext( \&npgettext, [ $msgctxt, $msgid, $msgid_plural, $count ] );
265
    return _gettext( \&npgettext, [ $msgctxt, $msgid, $msgid_plural, $count ] );
159
}
266
}
160
267
268
=head2 __npx
269
270
    my $translated = __npx('context', 'one item', '{count} items', $count, count => $count);
271
272
Context-aware plural translation with variable substitution.
273
274
=cut
275
161
sub __npx {
276
sub __npx {
162
    my ( $msgctxt, $msgid, $msgid_plural, $count, %vars ) = @_;
277
    my ( $msgctxt, $msgid, $msgid_plural, $count, %vars ) = @_;
163
278
Lines 168-185 sub __npx { Link Here
168
    return _gettext( \&npgettext, [ $msgctxt, $msgid, $msgid_plural, $count ], %vars );
283
    return _gettext( \&npgettext, [ $msgctxt, $msgid, $msgid_plural, $count ], %vars );
169
}
284
}
170
285
286
=head2 N__
287
288
    my $msgid = N__('Text for later translation');
289
290
No-operation translation marker. Returns the original text unchanged but marks
291
it for extraction by translation tools.
292
293
=cut
294
171
sub N__ {
295
sub N__ {
172
    return $_[0];
296
    return $_[0];
173
}
297
}
174
298
299
=head2 N__n
300
301
    my $msgid = N__n('singular', 'plural');
302
303
No-operation plural translation marker.
304
305
=cut
306
175
sub N__n {
307
sub N__n {
176
    return $_[0];
308
    return $_[0];
177
}
309
}
178
310
311
=head2 N__p
312
313
    my $msgid = N__p('context', 'Text for later translation');
314
315
No-operation context translation marker.
316
317
=cut
318
179
sub N__p {
319
sub N__p {
180
    return $_[1];
320
    return $_[1];
181
}
321
}
182
322
323
=head2 N__np
324
325
    my $msgid = N__np('context', 'singular', 'plural');
326
327
No-operation context plural translation marker.
328
329
=cut
330
183
sub N__np {
331
sub N__np {
184
    return $_[1];
332
    return $_[1];
185
}
333
}
Lines 224-227 sub _expand { Link Here
224
    return $text;
372
    return $text;
225
}
373
}
226
374
375
=head2 available_locales
376
377
    my $locales = Koha::I18N::available_locales();
378
379
Returns an arrayref of available system locales for use in system preferences.
380
381
Each locale is a hashref with:
382
  - C<value>: The locale identifier (e.g., 'en_US.utf8', 'default')
383
  - C<text>: Human-readable description (e.g., 'English (United States) - en_US.utf8')
384
385
Always includes 'default' as the first option. Additional locales are detected
386
from the system using C<locale -a> and filtered for UTF-8 locales.
387
388
=cut
389
390
sub available_locales {
391
    my @available_locales = ();
392
393
    # Always include default option
394
    push @available_locales, {
395
        value => 'default',
396
        text  => 'Default Unicode collation'
397
    };
398
399
    # Get system locales using the same approach as init()
400
    my @system_locales = grep { chomp; not( /^C/ || $_ eq 'POSIX' ) } qx/locale -a/;
401
402
    my @filtered_locales = ();
403
    for my $locale (@system_locales) {
404
405
        # Filter for useful locales (UTF-8 ones and common patterns)
406
        if ( $locale =~ /^[a-z]{2}_[A-Z]{2}\.utf8?$/i || $locale =~ /^[a-z]{2}_[A-Z]{2}$/i ) {
407
408
            # Create friendly display names
409
            my $display_name = $locale;
410
            if ( $locale =~ /^([a-z]{2})_([A-Z]{2})/ ) {
411
                my %lang_names = (
412
                    'en' => 'English',
413
                    'fr' => 'French',
414
                    'de' => 'German',
415
                    'es' => 'Spanish',
416
                    'it' => 'Italian',
417
                    'pt' => 'Portuguese',
418
                    'nl' => 'Dutch',
419
                    'pl' => 'Polish',
420
                    'fi' => 'Finnish',
421
                    'sv' => 'Swedish',
422
                    'da' => 'Danish',
423
                    'no' => 'Norwegian',
424
                    'ru' => 'Russian',
425
                    'ja' => 'Japanese',
426
                    'zh' => 'Chinese',
427
                    'ar' => 'Arabic',
428
                    'hi' => 'Hindi'
429
                );
430
                my %country_names = (
431
                    'US' => 'United States',
432
                    'GB' => 'United Kingdom',
433
                    'FR' => 'France',
434
                    'DE' => 'Germany',
435
                    'ES' => 'Spain',
436
                    'IT' => 'Italy',
437
                    'PT' => 'Portugal',
438
                    'BR' => 'Brazil',
439
                    'NL' => 'Netherlands',
440
                    'PL' => 'Poland',
441
                    'FI' => 'Finland',
442
                    'SE' => 'Sweden',
443
                    'DK' => 'Denmark',
444
                    'NO' => 'Norway',
445
                    'RU' => 'Russia',
446
                    'JP' => 'Japan',
447
                    'CN' => 'China',
448
                    'TW' => 'Taiwan',
449
                    'SA' => 'Saudi Arabia',
450
                    'IN' => 'India'
451
                );
452
                my $lang    = $lang_names{$1}    || uc($1);
453
                my $country = $country_names{$2} || $2;
454
                $display_name = "$lang ($country) - $locale";
455
            }
456
            push @filtered_locales, {
457
                value => $locale,
458
                text  => $display_name
459
            };
460
        }
461
    }
462
463
    # Sort locales by display name and add to available list
464
    @filtered_locales = sort { $a->{text} cmp $b->{text} } @filtered_locales;
465
    push @available_locales, @filtered_locales;
466
467
    return \@available_locales;
468
}
469
470
=head1 AUTHOR
471
472
Koha Development Team
473
474
=head1 COPYRIGHT
475
476
Copyright 2012-2014 BibLibre
477
478
=head1 LICENSE
479
480
This file is part of Koha.
481
482
Koha is free software; you can redistribute it and/or modify it under the
483
terms of the GNU General Public License as published by the Free Software
484
Foundation; either version 3 of the License, or (at your option) any later
485
version.
486
487
=cut
488
227
1;
489
1;
(-)a/Koha/ILL/Backend/Standard.pm (-4 / +13 lines)
Lines 1058-1065 sub _openurl_to_ill { Link Here
1058
        volume  => 'volume',
1058
        volume  => 'volume',
1059
        isbn    => 'isbn',
1059
        isbn    => 'isbn',
1060
        issn    => 'issn',
1060
        issn    => 'issn',
1061
        rft_id  => 'doi',
1062
        id      => 'doi',
1063
        doi     => 'doi',
1061
        doi     => 'doi',
1064
        year    => 'year',
1062
        year    => 'year',
1065
        title   => 'title',
1063
        title   => 'title',
Lines 1097-1104 sub _openurl_to_ill { Link Here
1097
            # Otherwise, pass it through untransformed and maybe move it
1095
            # Otherwise, pass it through untransformed and maybe move it
1098
            # to our custom parameters array
1096
            # to our custom parameters array
1099
            if ( !exists $ignore->{$meta_key} ) {
1097
            if ( !exists $ignore->{$meta_key} ) {
1100
                push @{$custom_key},   $meta_key;
1098
                if ( $meta_key eq 'id' || $meta_key eq 'rft_id' ) {
1101
                push @{$custom_value}, $params->{other}->{$meta_key};
1099
                    if ( $params->{other}->{$meta_key} =~ /:/ ) {
1100
                        my ( $k, $v ) = split /:/, $params->{other}->{$meta_key}, 2;
1101
                        if ( defined $k && defined $v ) {
1102
                            $return->{ lc $k } = $v;
1103
                        }
1104
                    } else {
1105
                        $return->{doi} = $params->{other}->{$meta_key};
1106
                    }
1107
                } else {
1108
                    push @{$custom_key},   $meta_key;
1109
                    push @{$custom_value}, $params->{other}->{$meta_key};
1110
                }
1102
            } else {
1111
            } else {
1103
                $return->{$meta_key} = $params->{other}->{$meta_key};
1112
                $return->{$meta_key} = $params->{other}->{$meta_key};
1104
            }
1113
            }
(-)a/Koha/ILL/Request.pm (+2 lines)
Lines 1776-1781 sub send_patron_notice { Link Here
1776
    );
1776
    );
1777
    my @transports = keys %{ $borrower_preferences->{transports} };
1777
    my @transports = keys %{ $borrower_preferences->{transports} };
1778
1778
1779
    return { result => { fail => [ 'email', 'sms' ], success => [] } } unless @transports;
1780
1779
    # Notice should come from the library where the request was placed,
1781
    # Notice should come from the library where the request was placed,
1780
    # not the patrons home library
1782
    # not the patrons home library
1781
    my $branch        = Koha::Libraries->find( $self->branchcode );
1783
    my $branch        = Koha::Libraries->find( $self->branchcode );
(-)a/Koha/Import/OAI/Authorities.pm (+1 lines)
Lines 13-18 package Koha::Import::OAI::Authorities; Link Here
13
# GNU General Public License for more details.
13
# GNU General Public License for more details.
14
#
14
#
15
# You should have received a copy of the GNU General Public License
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>.
16
17
17
use Modern::Perl;
18
use Modern::Perl;
18
19
(-)a/Koha/Import/OAI/Authority.pm (+1 lines)
Lines 13-18 package Koha::Import::OAI::Authority; Link Here
13
# GNU General Public License for more details.
13
# GNU General Public License for more details.
14
#
14
#
15
# You should have received a copy of the GNU General Public License
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>.
16
17
17
use Modern::Perl;
18
use Modern::Perl;
18
19
(-)a/Koha/Import/OAI/Biblio.pm (+1 lines)
Lines 13-18 package Koha::Import::OAI::Biblio; Link Here
13
# GNU General Public License for more details.
13
# GNU General Public License for more details.
14
#
14
#
15
# You should have received a copy of the GNU General Public License
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>.
16
17
17
use Modern::Perl;
18
use Modern::Perl;
18
19
(-)a/Koha/Import/OAI/Biblios.pm (+1 lines)
Lines 13-18 package Koha::Import::OAI::Biblios; Link Here
13
# GNU General Public License for more details.
13
# GNU General Public License for more details.
14
#
14
#
15
# You should have received a copy of the GNU General Public License
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>.
16
17
17
use Modern::Perl;
18
use Modern::Perl;
18
19
(-)a/Koha/Installer.pm (-1 / +1 lines)
Lines 29-35 sub needs_update { Link Here
29
    my $koha_version = Koha->version;
29
    my $koha_version = Koha->version;
30
    my $code_version = TransformToNum($koha_version);
30
    my $code_version = TransformToNum($koha_version);
31
31
32
    if ( $db_version == $code_version ) {
32
    if ( $db_version && $db_version == $code_version ) {
33
        $needs_update = 0;
33
        $needs_update = 0;
34
    }
34
    }
35
35
(-)a/Koha/Item.pm (+18 lines)
Lines 188-193 sub store { Link Here
188
            }
188
            }
189
        }
189
        }
190
190
191
        my $prevent_withdrawing_of = C4::Context->preference('PreventWithdrawingItemsStatus');
192
        my @statuses_to_prevent    = defined $prevent_withdrawing_of ? split( ',', $prevent_withdrawing_of ) : ();
193
        my $prevent_onloan         = grep { $_ eq 'checkedout' } @statuses_to_prevent;
194
        my $prevent_intransit      = grep { $_ eq 'intransit' } @statuses_to_prevent;
195
196
        if ( exists $updated_columns{withdrawn} && $updated_columns{withdrawn} ) {
197
            my $transfer = $pre_mod_item->get_transfer;
198
            if ( $pre_mod_item->onloan && $prevent_onloan ) {
199
                Koha::Exceptions::Item::Transfer::OnLoan->throw( error => "onloan_cannot_withdraw" );
200
                return $self->SUPER::store;
201
            }
202
203
            if ( defined $transfer && $transfer->in_transit && $prevent_intransit ) {
204
                Koha::Exceptions::Item::Transfer::InTransit->throw( error => "intransit_cannot_withdraw" );
205
                return $self->SUPER::store;
206
            }
207
        }
208
191
        if (   exists $updated_columns{itemcallnumber}
209
        if (   exists $updated_columns{itemcallnumber}
192
            or exists $updated_columns{cn_source} )
210
            or exists $updated_columns{cn_source} )
193
        {
211
        {
(-)a/Koha/Items.pm (-1 / +8 lines)
Lines 459-464 sub batch_update { Link Here
459
459
460
    my ( @modified_itemnumbers, $modified_fields );
460
    my ( @modified_itemnumbers, $modified_fields );
461
    my $i;
461
    my $i;
462
    my @errors;
462
    my $schema = Koha::Database->new->schema;
463
    my $schema = Koha::Database->new->schema;
463
    while ( my $item = $self->next ) {
464
    while ( my $item = $self->next ) {
464
465
Lines 578-583 sub batch_update { Link Here
578
                }
579
                }
579
            )
580
            )
580
        } catch {
581
        } catch {
582
            push @errors, {
583
                error => eval { $_->{error} } || "$_",
584
            };
581
            warn $_
585
            warn $_
582
        };
586
        };
583
587
Lines 600-606 sub batch_update { Link Here
600
        }
604
        }
601
    }
605
    }
602
606
603
    return ( { modified_itemnumbers => \@modified_itemnumbers, modified_fields => $modified_fields }, $self );
607
    return (
608
        { modified_itemnumbers => \@modified_itemnumbers, modified_fields => $modified_fields, errors => \@errors },
609
        $self
610
    );
604
}
611
}
605
612
606
=head2 apply_regex
613
=head2 apply_regex
(-)a/Koha/Manual.pm (+1 lines)
Lines 117-122 our $mapping = { Link Here
117
    'admin/preferences#logs'                   => '/logspreferences.html',
117
    'admin/preferences#logs'                   => '/logspreferences.html',
118
    'admin/preferences#opac'                   => '/opacpreferences.html',
118
    'admin/preferences#opac'                   => '/opacpreferences.html',
119
    'admin/preferences#patrons'                => '/patronspreferences.html',
119
    'admin/preferences#patrons'                => '/patronspreferences.html',
120
    'admin/preferences#reports'                => '/reportspreferences.html',
120
    'admin/preferences#searching'              => '/searchingpreferences.html',
121
    'admin/preferences#searching'              => '/searchingpreferences.html',
121
    'admin/preferences#serials'                => '/serialspreferences.html',
122
    'admin/preferences#serials'                => '/serialspreferences.html',
122
    'admin/preferences#staff_interface'        => '/staffclientpreferences.html',
123
    'admin/preferences#staff_interface'        => '/staffclientpreferences.html',
(-)a/Koha/Patron.pm (-10 / +174 lines)
Lines 388-394 sub store { Link Here
388
388
389
                if (    C4::Context->preference('ChildNeedsGuarantor')
389
                if (    C4::Context->preference('ChildNeedsGuarantor')
390
                    and ( $self->is_child or $self->category->can_be_guarantee )
390
                    and ( $self->is_child or $self->category->can_be_guarantee )
391
                    and $self->contactname eq ""
391
                    and ( !defined $self->contactname || $self->contactname eq "" )
392
                    and !@$guarantors )
392
                    and !@$guarantors )
393
                {
393
                {
394
                    Koha::Exceptions::Patron::Relationship::NoGuarantor->throw();
394
                    Koha::Exceptions::Patron::Relationship::NoGuarantor->throw();
Lines 473-481 ListOwnershipUponPatronDeletion pref, but entries from the borrower to other lis Link Here
473
sub delete {
473
sub delete {
474
    my ($self) = @_;
474
    my ($self) = @_;
475
475
476
    my $anonymous_patron = C4::Context->preference("AnonymousPatron");
477
    Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw()
476
    Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw()
478
        if $anonymous_patron && $self->id eq $anonymous_patron;
477
        if $self->is_anonymous;
479
478
480
    # Check if patron is protected
479
    # Check if patron is protected
481
    Koha::Exceptions::Patron::FailedDeleteProtectedPatron->throw() if defined $self->protected && $self->protected == 1;
480
    Koha::Exceptions::Patron::FailedDeleteProtectedPatron->throw() if defined $self->protected && $self->protected == 1;
Lines 736-743 sub siblings { Link Here
736
sub merge_with {
735
sub merge_with {
737
    my ( $self, $patron_ids ) = @_;
736
    my ( $self, $patron_ids ) = @_;
738
737
739
    my $anonymous_patron = C4::Context->preference("AnonymousPatron");
738
    return if $self->is_anonymous;
740
    return if $anonymous_patron && $self->id eq $anonymous_patron;
741
739
742
    # Do not merge other patrons into a protected patron
740
    # Do not merge other patrons into a protected patron
743
    return if $self->protected;
741
    return if $self->protected;
Lines 755-766 sub merge_with { Link Here
755
        sub {
753
        sub {
756
            foreach my $patron_id (@patron_ids) {
754
            foreach my $patron_id (@patron_ids) {
757
755
758
                next if $anonymous_patron && $patron_id eq $anonymous_patron;
759
760
                my $patron = Koha::Patrons->find($patron_id);
756
                my $patron = Koha::Patrons->find($patron_id);
761
757
762
                next unless $patron;
758
                next unless $patron;
763
759
760
                next if $patron->is_anonymous;
761
764
                # Do not merge protected patrons into other patrons
762
                # Do not merge protected patrons into other patrons
765
                next if $patron->protected;
763
                next if $patron->protected;
766
764
Lines 1318-1323 sub move_to_deleted { Link Here
1318
    return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
1316
    return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
1319
}
1317
}
1320
1318
1319
=head3 can_place_holds
1320
1321
    my $result = $patron->can_place_holds();
1322
    my $result = $patron->can_place_holds(
1323
        {
1324
            overrides        => { debt_limit => 1, card_lost => 1 },
1325
            no_short_circuit => 1
1326
        }
1327
    );
1328
1329
    if ( $patron->can_place_holds() ) {
1330
        # patron can place holds
1331
    } else {
1332
        my @messages = $result->messages;
1333
        # handle error messages
1334
    }
1335
1336
Checks if a patron is allowed to place holds based on various patron conditions.
1337
1338
=head4 Parameters
1339
1340
=over 4
1341
1342
=item * C<$options> (optional) - Hashref with the following keys:
1343
1344
=over 8
1345
1346
=item * C<overrides> - Hashref of checks to skip. Keys should match error message codes.
1347
1348
=item * C<no_short_circuit> - Boolean. If true, performs all checks and collects all error messages instead of stopping at first failure. Default: false.
1349
1350
=back
1351
1352
=back
1353
1354
=head4 Returns
1355
1356
Koha::Result::Boolean object - true if patron can place holds, false otherwise.
1357
When false, the result object contains error messages with details about why
1358
holds are blocked.
1359
1360
=head4 Error Messages
1361
1362
The following error message codes may be returned:
1363
1364
=over 4
1365
1366
=item * C<expired> - Patron account has expired and expired patrons are blocked from placing holds
1367
1368
=item * C<debt_limit> - Patron owes more than the maximum allowed outstanding amount
1369
1370
=item * C<bad_address> - Patron's address is marked as incorrect
1371
1372
=item * C<card_lost> - Patron's library card is marked as lost
1373
1374
=item * C<restricted> - Patron account is restricted/debarred
1375
1376
=item * C<hold_limit> - Patron has reached the maximum number of allowed holds
1377
1378
=back
1379
1380
Error messages may include additional payload data with relevant details
1381
(amounts, limits, counts, etc.).
1382
1383
=cut
1384
1385
sub can_place_holds {
1386
    my ( $self, $options ) = @_;
1387
    $options //= {};
1388
1389
    my $overrides        = $options->{overrides}        // {};
1390
    my $no_short_circuit = $options->{no_short_circuit} // 0;
1391
1392
    my $result = Koha::Result::Boolean->new(1);
1393
1394
    # expired patron check
1395
    unless ( $overrides->{expired} ) {
1396
        if ( $self->is_expired && $self->category->effective_BlockExpiredPatronOpacActions_contains('hold') ) {
1397
            $result->set_value(0);
1398
            $result->add_message( { message => 'expired', type => 'error' } );
1399
1400
            return $result unless $no_short_circuit;
1401
        }
1402
    }
1403
1404
    # debt check
1405
    unless ( $overrides->{debt_limit} ) {
1406
        my $max_outstanding = C4::Context->preference("maxoutstanding");
1407
        my $outstanding     = $self->account->balance;
1408
1409
        if ( $max_outstanding && $outstanding && ( $outstanding > $max_outstanding ) ) {
1410
            $result->set_value(0);
1411
            $result->add_message(
1412
                {
1413
                    message => 'debt_limit', type => 'error',
1414
                    payload => { total_outstanding => $outstanding, max_outstanding => $max_outstanding }
1415
                }
1416
            );
1417
1418
            return $result unless $no_short_circuit;
1419
        }
1420
    }
1421
1422
    # check address marked as incorrect
1423
    unless ( $overrides->{bad_address} ) {
1424
        if ( $self->gonenoaddress ) {
1425
            $result->set_value(0);
1426
            $result->add_message( { message => 'bad_address', type => 'error' } );
1427
1428
            return $result unless $no_short_circuit;
1429
        }
1430
    }
1431
1432
    # check lost card
1433
    unless ( $overrides->{card_lost} ) {
1434
        if ( $self->lost ) {
1435
            $result->set_value(0);
1436
            $result->add_message( { message => 'card_lost', type => 'error' } );
1437
            return $result unless $no_short_circuit;
1438
        }
1439
    }
1440
1441
    # check restrictions
1442
    unless ( $overrides->{restricted} ) {
1443
        if ( $self->is_debarred ) {
1444
            $result->set_value(0);
1445
            $result->add_message( { message => 'restricted', type => 'error' } );
1446
1447
            return $result unless $no_short_circuit;
1448
        }
1449
    }
1450
1451
    # check max reserves
1452
    unless ( $overrides->{hold_limit} ) {
1453
        my $max_holds   = C4::Context->preference("maxreserves");
1454
        my $holds_count = $self->holds->count;
1455
        if ( $max_holds && ( $holds_count >= $max_holds ) ) {
1456
            $result->set_value(0);
1457
            $result->add_message(
1458
                {
1459
                    message => 'hold_limit', type => 'error',
1460
                    payload => { total_holds => $holds_count, max_holds => $max_holds }
1461
                }
1462
            );
1463
1464
            return $result unless $no_short_circuit;
1465
        }
1466
    }
1467
1468
    return $result;
1469
}
1470
1321
=head3 can_request_article
1471
=head3 can_request_article
1322
1472
1323
    if ( $patron->can_request_article( $library->id ) ) { ... }
1473
    if ( $patron->can_request_article( $library->id ) ) { ... }
Lines 2525-2530 sub _anonymize_column { Link Here
2525
        $val = $nullable ? undef : 0;
2675
        $val = $nullable ? undef : 0;
2526
    } elsif ( $type =~ /date|time/ ) {
2676
    } elsif ( $type =~ /date|time/ ) {
2527
        $val = $nullable ? undef : dt_from_string;
2677
        $val = $nullable ? undef : dt_from_string;
2678
    } elsif ( $type eq 'enum' ) {
2679
        $val = $nullable ? undef : $col_info->{default_value};
2528
    }
2680
    }
2529
    $self->$col($val);
2681
    $self->$col($val);
2530
}
2682
}
Lines 2894-2904 This method tells if the Koha:Patron object can be deleted. Possible return valu Link Here
2894
sub safe_to_delete {
3046
sub safe_to_delete {
2895
    my ($self) = @_;
3047
    my ($self) = @_;
2896
3048
2897
    my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2898
2899
    my $error;
3049
    my $error;
2900
3050
2901
    if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
3051
    if ( $self->is_anonymous ) {
2902
        $error = 'is_anonymous_patron';
3052
        $error = 'is_anonymous_patron';
2903
    } elsif ( $self->checkouts->count ) {
3053
    } elsif ( $self->checkouts->count ) {
2904
        $error = 'has_checkouts';
3054
        $error = 'has_checkouts';
Lines 3251-3256 sub is_patron_inside_charge_limits { Link Here
3251
    return $patron_charge_limits;
3401
    return $patron_charge_limits;
3252
}
3402
}
3253
3403
3404
=head3 is_anonymous
3405
3406
my $is_anonymous_patron= $patron->is_anonymous();
3407
3408
Returns true if the patron is the anonymous patron (AnonymousPatron)
3409
3410
=cut
3411
3412
sub is_anonymous {
3413
    my ($self) = @_;
3414
    my $anonymous_patron = C4::Context->preference('AnonymousPatron');
3415
    return ( $anonymous_patron && $self->borrowernumber eq $anonymous_patron ) ? 1 : 0;
3416
}
3417
3254
=head2 Internal methods
3418
=head2 Internal methods
3255
3419
3256
=head3 _type
3420
=head3 _type
(-)a/Koha/Patron/Discharge.pm (+92 lines)
Lines 14-19 use Koha::DateUtils qw( dt_from_string output_pref ); Link Here
14
use Koha::Patrons;
14
use Koha::Patrons;
15
use Koha::Patron::Debarments qw( AddDebarment );
15
use Koha::Patron::Debarments qw( AddDebarment );
16
16
17
=head1 NAME
18
19
Koha::Patron::Discharge - Koha Discharge object class
20
21
=head1 API
22
23
=head2 Class Methods
24
25
=cut
26
27
=head3 count
28
29
    Koha::Patron:Discharge->count;
30
31
Return the number of discharges corresponding to the asked criteria
32
33
=cut
34
17
sub count {
35
sub count {
18
    my ($params) = @_;
36
    my ($params) = @_;
19
    my $values = {};
37
    my $values = {};
Lines 31-36 sub count { Link Here
31
    return search_limited($values)->count;
49
    return search_limited($values)->count;
32
}
50
}
33
51
52
=head3 can_be_discharged
53
54
    my $can_be_discharged = Koha::Patron:Discharge->can_be_discharged({borrowernumber => $borrowernumber});
55
56
Return true if the borrower can be discharged
57
58
=cut
59
34
sub can_be_discharged {
60
sub can_be_discharged {
35
    my ($params) = @_;
61
    my ($params) = @_;
36
    return unless $params->{borrowernumber};
62
    return unless $params->{borrowernumber};
Lines 56-61 sub can_be_discharged { Link Here
56
    return ( $can_be_discharged, $problems );
82
    return ( $can_be_discharged, $problems );
57
}
83
}
58
84
85
=head3 is_discharged
86
87
    my $is_discharged = Koha::Patron:Discharge->is_discharged({borrowernumber => $borrowernumber});
88
89
Return true if the borrower is discharged
90
91
=cut
92
59
sub is_discharged {
93
sub is_discharged {
60
    my ($params) = @_;
94
    my ($params) = @_;
61
    return unless $params->{borrowernumber};
95
    return unless $params->{borrowernumber};
Lines 71-76 sub is_discharged { Link Here
71
    }
105
    }
72
}
106
}
73
107
108
=head3 request
109
110
    my $request = Koha::Patron:Discharge->request({borrowernumber => $borrowernumber});
111
112
Place a discharge request on a given borrower after checking the borrower has the right to be discharged.
113
114
=cut
115
74
sub request {
116
sub request {
75
    my ($params) = @_;
117
    my ($params) = @_;
76
    my $borrowernumber = $params->{borrowernumber};
118
    my $borrowernumber = $params->{borrowernumber};
Lines 87-92 sub request { Link Here
87
    );
129
    );
88
}
130
}
89
131
132
=head3 discharge
133
134
    my $request = Koha::Patron:Discharge->discharge({borrowernumber => $borrowernumber});
135
136
Place a discharge request on a given borrower, if a discharge was requested, update the status to discharged and place a suspension on the user.
137
138
=cut
139
90
sub discharge {
140
sub discharge {
91
    my ($params) = @_;
141
    my ($params) = @_;
92
    my $borrowernumber = $params->{borrowernumber};
142
    my $borrowernumber = $params->{borrowernumber};
Lines 124-129 sub discharge { Link Here
124
    }
174
    }
125
}
175
}
126
176
177
=head3 generate_as_pdf
178
179
    my $request = Koha::Patron:Discharge->generate_as_pdf({borrowernumber => $borrowernumber});
180
181
Create a pdf from an existing discharge associated to the borrowernumber.
182
183
=cut
184
127
sub generate_as_pdf {
185
sub generate_as_pdf {
128
    my ($params) = @_;
186
    my ($params) = @_;
129
    return unless $params->{borrowernumber};
187
    return unless $params->{borrowernumber};
Lines 175-180 sub generate_as_pdf { Link Here
175
    return $pdf_path;
233
    return $pdf_path;
176
}
234
}
177
235
236
=head3 get_pendings
237
238
    my $rs = Koha::Patron:Discharge->get_pendings({
239
        borrowernumber => $borrowernumber
240
        branchcode => $branchcode
241
    });
242
243
Get all pending discharges associated to a borrowernumber and/or a given branch
244
245
=cut
246
178
sub get_pendings {
247
sub get_pendings {
179
    my ($params)       = @_;
248
    my ($params)       = @_;
180
    my $branchcode     = $params->{branchcode};
249
    my $branchcode     = $params->{branchcode};
Lines 190-195 sub get_pendings { Link Here
190
    return search_limited($cond);
259
    return search_limited($cond);
191
}
260
}
192
261
262
=head3 get_validated
263
264
    my $rs = Koha::Patron:Discharge->get_validated({
265
        borrowernumber => $borrowernumber
266
        branchcode => $branchcode
267
    });
268
269
Get all validated discharges associated to a borrowernumber and/or a given branch
270
271
=cut
272
193
sub get_validated {
273
sub get_validated {
194
    my ($params)       = @_;
274
    my ($params)       = @_;
195
    my $branchcode     = $params->{branchcode};
275
    my $branchcode     = $params->{branchcode};
Lines 205-210 sub get_validated { Link Here
205
}
285
}
206
286
207
# TODO This module should be based on Koha::Object[s]
287
# TODO This module should be based on Koha::Object[s]
288
289
=head3 search_limited
290
291
    my $rs = Koha::Patron:Discharge->search_limited({
292
        borrower.branchcode => $branchcode
293
    },
294
    $attributes);
295
296
Search all discharges that can be seen by the user and fitting the given conditions
297
298
=cut
299
208
sub search_limited {
300
sub search_limited {
209
    my ( $params, $attributes ) = @_;
301
    my ( $params, $attributes ) = @_;
210
    my $userenv = C4::Context->userenv;
302
    my $userenv = C4::Context->userenv;
(-)a/Koha/Patrons/Import.pm (-2 / +6 lines)
Lines 267-273 LINE: while ( my $borrowerline = <$handle> ) { Link Here
267
267
268
        # Remove warning for int datatype that cannot be null
268
        # Remove warning for int datatype that cannot be null
269
        # Argument "" isn't numeric in numeric eq (==) at /usr/share/perl5/DBIx/Class/Row.pm line 1018
269
        # Argument "" isn't numeric in numeric eq (==) at /usr/share/perl5/DBIx/Class/Row.pm line 1018
270
        for my $field (qw( privacy privacy_guarantor_fines privacy_guarantor_checkouts anonymized login_attempts )) {
270
        for my $field (
271
            qw( privacy privacy_guarantor_fines privacy_guarantor_checkouts anonymized login_attempts checkprevcheckout autorenew_checkouts )
272
            )
273
        {
271
            delete $borrower{$field}
274
            delete $borrower{$field}
272
                if exists $borrower{$field} and $borrower{$field} eq "";
275
                if exists $borrower{$field} and $borrower{$field} eq "";
273
        }
276
        }
Lines 304-310 LINE: while ( my $borrowerline = <$handle> ) { Link Here
304
                next if $col eq 'password'   && !$overwrite_passwords;
307
                next if $col eq 'password'   && !$overwrite_passwords;
305
                next if $col eq 'dateexpiry' && $update_dateexpiry;
308
                next if $col eq 'dateexpiry' && $update_dateexpiry;
306
309
307
                $borrower{$col} = $member->{$col} if $col eq 'dateexpiry' && !$columns[ $csvkeycol{$col} ];
310
                $borrower{$col} = $member->{$col}
311
                    if $col eq 'dateexpiry' && ( !$csvkeycol{$col} || !$columns[ $csvkeycol{$col} ] );
308
312
309
                unless ( exists( $csvkeycol{$col} ) || $defaults->{$col} ) {
313
                unless ( exists( $csvkeycol{$col} ) || $defaults->{$col} ) {
310
                    $borrower{$col} = $member->{$col} if ( $member->{$col} );
314
                    $borrower{$col} = $member->{$col} if ( $member->{$col} );
(-)a/Koha/Quotes.pm (-1 / +1 lines)
Lines 71-77 sub get_daily_quote { Link Here
71
        my $dt = $dtf->format_date(dt_from_string);
71
        my $dt = $dtf->format_date(dt_from_string);
72
        $quote = $self->search(
72
        $quote = $self->search(
73
            {
73
            {
74
                timestamp => { -between => => [ "$dt 00:00:00", "$dt 23:59:59" ] },
74
                timestamp => { -between => [ "$dt 00:00:00", "$dt 23:59:59" ] },
75
            },
75
            },
76
            {
76
            {
77
                order_by => { -desc => 'timestamp' },
77
                order_by => { -desc => 'timestamp' },
(-)a/Koha/REST/V1/CirculationRules.pm (-10 / +10 lines)
Lines 2-19 package Koha::REST::V1::CirculationRules; Link Here
2
2
3
# This file is part of Koha.
3
# This file is part of Koha.
4
#
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
5
# Koha is free software; you can redistribute it and/or modify it
6
# terms of the GNU General Public License as published by the Free Software
6
# under the terms of the GNU General Public License as published by
7
# Foundation; either version 3 of the License, or (at your option) any later
7
# the Free Software Foundation; either version 3 of the License, or
8
# version.
8
# (at your option) any later version.
9
#
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10
# Koha is distributed in the hope that it will be useful, but
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
13
#
14
#
14
# You should have received a copy of the GNU General Public License along
15
# You should have received a copy of the GNU General Public License
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
(-)a/Koha/REST/V1/Holds.pm (-11 / +20 lines)
Lines 88-94 sub add { Link Here
88
        my $non_priority      = $body->{non_priority};
88
        my $non_priority      = $body->{non_priority};
89
89
90
        my $overrides    = $c->stash('koha.overrides');
90
        my $overrides    = $c->stash('koha.overrides');
91
        my $can_override = $overrides->{any} && C4::Context->preference('AllowHoldPolicyOverride');
91
        my $can_override = C4::Context->preference('AllowHoldPolicyOverride') // 0;
92
93
        my $override_all = $overrides->{any} && C4::Context->preference('AllowHoldPolicyOverride') ? 1 : 0;
92
94
93
        if ( !C4::Context->preference('AllowHoldDateInFuture') && $hold_date ) {
95
        if ( !C4::Context->preference('AllowHoldDateInFuture') && $hold_date ) {
94
            return $c->render(
96
            return $c->render(
Lines 142-148 sub add { Link Here
142
        }
144
        }
143
145
144
        # If the hold is being forced, no need to validate
146
        # If the hold is being forced, no need to validate
145
        unless ($can_override) {
147
        unless ($override_all) {
146
148
147
            # Validate pickup location
149
            # Validate pickup location
148
            my $valid_pickup_location;
150
            my $valid_pickup_location;
Lines 161-181 sub add { Link Here
161
                openapi => { error => 'The supplied pickup location is not valid' }
163
                openapi => { error => 'The supplied pickup location is not valid' }
162
            ) unless $valid_pickup_location;
164
            ) unless $valid_pickup_location;
163
165
164
            my $can_place_hold =
166
            my $can_place_holds = $patron->can_place_holds( { overrides => $overrides } );
167
168
            if ( !$can_place_holds ) {
169
                my $error_code = $can_place_holds->messages->[0]->message;
170
                return $c->render(
171
                    status  => 409,
172
                    openapi => {
173
                        error      => 'Hold cannot be placed. Reason: ' . $error_code,
174
                        error_code => $error_code,
175
                    }
176
                );
177
            }
178
179
            my $can_hold_be_placed =
165
                $item
180
                $item
166
                ? C4::Reserves::CanItemBeReserved( $patron, $item )
181
                ? C4::Reserves::CanItemBeReserved( $patron, $item )
167
                : C4::Reserves::CanBookBeReserved( $patron_id, $biblio_id );
182
                : C4::Reserves::CanBookBeReserved( $patron_id, $biblio_id );
168
183
169
            if ( C4::Context->preference('maxreserves')
184
            unless ( $can_hold_be_placed->{status} eq 'OK' ) {
170
                && $patron->holds->count + 1 > C4::Context->preference('maxreserves') )
171
            {
172
                $can_place_hold->{status} = 'tooManyReserves';
173
            }
174
175
            unless ( $can_place_hold->{status} eq 'OK' ) {
176
                return $c->render(
185
                return $c->render(
177
                    status  => 403,
186
                    status  => 403,
178
                    openapi => { error => "Hold cannot be placed. Reason: " . $can_place_hold->{status} }
187
                    openapi => { error => "Hold cannot be placed. Reason: " . $can_hold_be_placed->{status} }
179
                );
188
                );
180
            }
189
            }
181
        }
190
        }
(-)a/Koha/REST/V1/Suggestions.pm (-8 / +9 lines)
Lines 2-18 package Koha::REST::V1::Suggestions; Link Here
2
2
3
# This file is part of Koha.
3
# This file is part of Koha.
4
#
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
5
# Koha is free software; you can redistribute it and/or modify it
6
# terms of the GNU General Public License as published by the Free Software
6
# under the terms of the GNU General Public License as published by
7
# Foundation; either version 3 of the License, or (at your option) any later
7
# the Free Software Foundation; either version 3 of the License, or
8
# version.
8
# (at your option) any later version.
9
#
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10
# Koha is distributed in the hope that it will be useful, but
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
13
#
14
#
14
# You should have received a copy of the GNU General Public License
15
# You should have received a copy of the GNU General Public License
15
# along with Koha; if not, see <http:°www.gnu.org/licenses>.
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
17
17
use Modern::Perl;
18
use Modern::Perl;
18
19
(-)a/Koha/Report.pm (-1 / +19 lines)
Lines 20-28 use Modern::Perl; Link Here
20
use Koha::Database;
20
use Koha::Database;
21
use Koha::Reports;
21
use Koha::Reports;
22
22
23
use Koha::Object;
24
use Koha::Object::Limit::Library;
25
26
use base qw(Koha::Object Koha::Object::Limit::Library);
27
23
#use Koha::DateUtils qw( dt_from_string output_pref );
28
#use Koha::DateUtils qw( dt_from_string output_pref );
24
29
25
use base qw(Koha::Object);
26
#
30
#
27
# FIXME We could only return an error code instead of the arrayref
31
# FIXME We could only return an error code instead of the arrayref
28
# Only 1 error is returned
32
# Only 1 error is returned
Lines 268-271 sub _type { Link Here
268
    return 'SavedSql';
272
    return 'SavedSql';
269
}
273
}
270
274
275
=head3 _library_limits
276
277
Configurable library limits
278
279
=cut
280
281
sub _library_limits {
282
    return {
283
        class   => "ReportsBranch",
284
        id      => "report_id",
285
        library => "branchcode",
286
    };
287
}
288
271
1;
289
1;
(-)a/Koha/Reports.pm (-1 / +28 lines)
Lines 21-27 use Koha::Database; Link Here
21
21
22
use Koha::Report;
22
use Koha::Report;
23
23
24
use base qw(Koha::Objects);
24
use base qw(Koha::Objects Koha::Objects::Limit::Library);
25
25
26
=head1 NAME
26
=head1 NAME
27
27
Lines 33-38 Koha::Reports - Koha Report Object set class Link Here
33
33
34
=cut
34
=cut
35
35
36
=head3 search_with_localization
37
38
my $itemtypes = Koha::ItemTypes->search_with_localization
39
40
=cut
41
42
sub search_with_localization {
43
    my ( $self, $params, $attributes ) = @_;
44
45
    my $language = C4::Languages::getlanguage();
46
    $Koha::Schema::Result::Itemtype::LANGUAGE = $language;
47
    $attributes->{order_by}                   = 'translated_description' unless exists $attributes->{order_by};
48
    $attributes->{join}                       = 'localization';
49
    $attributes->{'+select'}                  = [
50
        {
51
            coalesce => [qw( localization.translation me.description )],
52
            -as      => 'translated_description'
53
        }
54
    ];
55
    if ( defined $params->{branchcode} ) {
56
        my $branchcode = delete $params->{branchcode};
57
        $self->search_with_library_limits( $params, $attributes, $branchcode );
58
    } else {
59
        $self->SUPER::search( $params, $attributes );
60
    }
61
}
62
36
=head3 _type
63
=head3 _type
37
64
38
Returns name of corresponding DBIC resultset
65
Returns name of corresponding DBIC resultset
(-)a/Koha/Schema/Result/Borrower.pm (-6 / +6 lines)
Lines 582-591 controls if relatives can see this patron's checkouts Link Here
582
582
583
=head2 checkprevcheckout
583
=head2 checkprevcheckout
584
584
585
  data_type: 'varchar'
585
  data_type: 'enum'
586
  default_value: 'inherit'
586
  default_value: 'inherit'
587
  extra: {list => ["yes","no","inherit"]}
587
  is_nullable: 0
588
  is_nullable: 0
588
  size: 7
589
589
590
produce a warning for this patron if this item has previously been checked out to this patron if 'yes', not if 'no', defer to category setting if 'inherit'.
590
produce a warning for this patron if this item has previously been checked out to this patron if 'yes', not if 'no', defer to category setting if 'inherit'.
591
591
Lines 832-841 __PACKAGE__->add_columns( Link Here
832
  { data_type => "tinyint", default_value => 0, is_nullable => 0 },
832
  { data_type => "tinyint", default_value => 0, is_nullable => 0 },
833
  "checkprevcheckout",
833
  "checkprevcheckout",
834
  {
834
  {
835
    data_type => "varchar",
835
    data_type => "enum",
836
    default_value => "inherit",
836
    default_value => "inherit",
837
    extra => { list => ["yes", "no", "inherit"] },
837
    is_nullable => 0,
838
    is_nullable => 0,
838
    size => 7,
839
  },
839
  },
840
  "updated_on",
840
  "updated_on",
841
  {
841
  {
Lines 2197-2204 Composing rels: L</user_permissions> -> permission Link Here
2197
__PACKAGE__->many_to_many("permissions", "user_permissions", "permission");
2197
__PACKAGE__->many_to_many("permissions", "user_permissions", "permission");
2198
2198
2199
2199
2200
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-04-28 16:41:47
2200
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-10 07:11:31
2201
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:J1oaW0hFRihEJZ6U+XSwag
2201
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:XFOe2X4k2DUziaNS5pWd/Q
2202
2202
2203
__PACKAGE__->belongs_to(
2203
__PACKAGE__->belongs_to(
2204
  "library",
2204
  "library",
(-)a/Koha/Schema/Result/Category.pm (-6 / +6 lines)
Lines 136-145 Default privacy setting for this patron category Link Here
136
136
137
=head2 checkprevcheckout
137
=head2 checkprevcheckout
138
138
139
  data_type: 'varchar'
139
  data_type: 'enum'
140
  default_value: 'inherit'
140
  default_value: 'inherit'
141
  extra: {list => ["yes","no","inherit"]}
141
  is_nullable: 0
142
  is_nullable: 0
142
  size: 7
143
143
144
produce a warning for this patron category if this item has previously been checked out to this patron if 'yes', not if 'no', defer to syspref setting if 'inherit'.
144
produce a warning for this patron category if this item has previously been checked out to this patron if 'yes', not if 'no', defer to syspref setting if 'inherit'.
145
145
Lines 274-283 __PACKAGE__->add_columns( Link Here
274
  },
274
  },
275
  "checkprevcheckout",
275
  "checkprevcheckout",
276
  {
276
  {
277
    data_type => "varchar",
277
    data_type => "enum",
278
    default_value => "inherit",
278
    default_value => "inherit",
279
    extra => { list => ["yes", "no", "inherit"] },
279
    is_nullable => 0,
280
    is_nullable => 0,
280
    size => 7,
281
  },
281
  },
282
  "can_place_ill_in_opac",
282
  "can_place_ill_in_opac",
283
  { data_type => "tinyint", default_value => 1, is_nullable => 0 },
283
  { data_type => "tinyint", default_value => 1, is_nullable => 0 },
Lines 410-417 __PACKAGE__->has_many( Link Here
410
);
410
);
411
411
412
412
413
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2024-12-03 15:46:23
413
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-10 07:11:31
414
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:jiQq3bW+ZfdYpUpfZq1Rzw
414
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ADt+iDjteg9Jb81L2FMIvg
415
415
416
# You can replace this text with custom code or comments, and it will be preserved on regeneration
416
# You can replace this text with custom code or comments, and it will be preserved on regeneration
417
417
(-)a/Koha/Schema/Result/Deletedborrower.pm (-6 / +6 lines)
Lines 579-588 controls if relatives can see this patron's checkouts Link Here
579
579
580
=head2 checkprevcheckout
580
=head2 checkprevcheckout
581
581
582
  data_type: 'varchar'
582
  data_type: 'enum'
583
  default_value: 'inherit'
583
  default_value: 'inherit'
584
  extra: {list => ["yes","no","inherit"]}
584
  is_nullable: 0
585
  is_nullable: 0
585
  size: 7
586
586
587
produce a warning for this patron if this item has previously been checked out to this patron if 'yes', not if 'no', defer to category setting if 'inherit'.
587
produce a warning for this patron if this item has previously been checked out to this patron if 'yes', not if 'no', defer to category setting if 'inherit'.
588
588
Lines 817-826 __PACKAGE__->add_columns( Link Here
817
  { data_type => "tinyint", default_value => 0, is_nullable => 0 },
817
  { data_type => "tinyint", default_value => 0, is_nullable => 0 },
818
  "checkprevcheckout",
818
  "checkprevcheckout",
819
  {
819
  {
820
    data_type => "varchar",
820
    data_type => "enum",
821
    default_value => "inherit",
821
    default_value => "inherit",
822
    extra => { list => ["yes", "no", "inherit"] },
822
    is_nullable => 0,
823
    is_nullable => 0,
823
    size => 7,
824
  },
824
  },
825
  "updated_on",
825
  "updated_on",
826
  {
826
  {
Lines 857-864 __PACKAGE__->add_columns( Link Here
857
);
857
);
858
858
859
859
860
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-04-28 16:41:47
860
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-10 07:11:31
861
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:5QA+/eORyEKhkG++0f3Hvw
861
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vAkNNQagJcYfZ0/6mDOw8g
862
862
863
__PACKAGE__->add_columns(
863
__PACKAGE__->add_columns(
864
    '+anonymized'                  => { is_boolean => 1 },
864
    '+anonymized'                  => { is_boolean => 1 },
(-)a/Koha/Schema/Result/ReportsBranch.pm (+85 lines)
Line 0 Link Here
1
use utf8;
2
3
package Koha::Schema::Result::ReportsBranch;
4
5
# Created by DBIx::Class::Schema::Loader
6
# DO NOT MODIFY THE FIRST PART OF THIS FILE
7
8
=head1 NAME
9
10
Koha::Schema::Result::ReportsBranch
11
12
=cut
13
14
use strict;
15
use warnings;
16
17
use base 'DBIx::Class::Core';
18
19
=head1 TABLE: C<reports_branches>
20
21
=cut
22
23
__PACKAGE__->table("reports_branches");
24
25
=head1 ACCESSORS
26
27
=head2 report_id
28
29
  data_type: 'integer'
30
  is_foreign_key: 1
31
  is_nullable: 0
32
33
=head2 branchcode
34
35
  data_type: 'varchar'
36
  is_foreign_key: 1
37
  is_nullable: 0
38
  size: 10
39
40
=cut
41
42
__PACKAGE__->add_columns(
43
    "report_id",
44
    { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
45
    "branchcode",
46
    { data_type => "varchar", is_foreign_key => 1, is_nullable => 0, size => 10 },
47
);
48
49
=head1 RELATIONS
50
51
=head2 branchcode
52
53
Type: belongs_to
54
55
Related object: L<Koha::Schema::Result::Branch>
56
57
=cut
58
59
__PACKAGE__->belongs_to(
60
    "branchcode",
61
    "Koha::Schema::Result::Branch",
62
    { branchcode    => "branchcode" },
63
    { is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" },
64
);
65
66
=head2 report
67
68
Type: belongs_to
69
70
Related object: L<Koha::Schema::Result::SavedSql>
71
72
=cut
73
74
__PACKAGE__->belongs_to(
75
    "report",
76
    "Koha::Schema::Result::SavedSql",
77
    { id            => "report_id" },
78
    { is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" },
79
);
80
81
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-06 17:02:11
82
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:SJqUxMgLJHAjbX3GJlpB+A
83
84
# You can replace this text with custom code or comments, and it will be preserved on regeneration
85
1;
(-)a/Koha/Schema/Result/SavedSql.pm (+220 lines)
Lines 185-190 __PACKAGE__->add_columns( Link Here
185
185
186
__PACKAGE__->set_primary_key("id");
186
__PACKAGE__->set_primary_key("id");
187
187
188
=head1 RELATIONS
189
190
=head2 reports_branches
191
192
Type: has_many
193
194
Related object: L<Koha::Schema::Result::ReportsBranch>
195
196
=cut
197
198
__PACKAGE__->has_many(
199
  "reports_branches",
200
  "Koha::Schema::Result::ReportsBranch",
201
  { "foreign.report_id" => "self.id" },
202
  { cascade_copy => 0, cascade_delete => 0 },
203
);
204
205
206
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-06 17:02:11
207
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qFqDaOxgdm2I12cJEiCieg
208
# These lines were loaded from '/kohadevbox/koha/Koha/Schema/Result/SavedSql.pm' found in @INC.
209
# They are now part of the custom portion of this file
210
# for you to hand-edit.  If you do not either delete
211
# this section or remove that file from @INC, this section
212
# will be repeated redundantly when you re-create this
213
# file again via Loader!  See skip_load_external to disable
214
# this feature.
215
216
use utf8;
217
package Koha::Schema::Result::SavedSql;
218
219
# Created by DBIx::Class::Schema::Loader
220
# DO NOT MODIFY THE FIRST PART OF THIS FILE
221
222
=head1 NAME
223
224
Koha::Schema::Result::SavedSql
225
226
=cut
227
228
use strict;
229
use warnings;
230
231
use base 'DBIx::Class::Core';
232
233
=head1 TABLE: C<saved_sql>
234
235
=cut
236
237
__PACKAGE__->table("saved_sql");
238
239
=head1 ACCESSORS
240
241
=head2 id
242
243
  data_type: 'integer'
244
  is_auto_increment: 1
245
  is_nullable: 0
246
247
unique id and primary key assigned by Koha
248
249
=head2 borrowernumber
250
251
  data_type: 'integer'
252
  is_nullable: 1
253
254
the staff member who created this report (borrowers.borrowernumber)
255
256
=head2 date_created
257
258
  data_type: 'datetime'
259
  datetime_undef_if_invalid: 1
260
  is_nullable: 1
261
262
the date this report was created
263
264
=head2 last_modified
265
266
  data_type: 'datetime'
267
  datetime_undef_if_invalid: 1
268
  is_nullable: 1
269
270
the date this report was last edited
271
272
=head2 savedsql
273
274
  data_type: 'mediumtext'
275
  is_nullable: 1
276
277
the SQL for this report
278
279
=head2 last_run
280
281
  data_type: 'datetime'
282
  datetime_undef_if_invalid: 1
283
  is_nullable: 1
284
285
=head2 report_name
286
287
  data_type: 'varchar'
288
  default_value: (empty string)
289
  is_nullable: 0
290
  size: 255
291
292
the name of this report
293
294
=head2 type
295
296
  data_type: 'varchar'
297
  is_nullable: 1
298
  size: 255
299
300
always 1 for tabular
301
302
=head2 notes
303
304
  data_type: 'mediumtext'
305
  is_nullable: 1
306
307
the notes or description given to this report
308
309
=head2 cache_expiry
310
311
  data_type: 'integer'
312
  default_value: 300
313
  is_nullable: 0
314
315
=head2 public
316
317
  data_type: 'tinyint'
318
  default_value: 0
319
  is_nullable: 0
320
321
=head2 report_area
322
323
  data_type: 'varchar'
324
  is_nullable: 1
325
  size: 6
326
327
=head2 report_group
328
329
  data_type: 'varchar'
330
  is_nullable: 1
331
  size: 80
332
333
=head2 report_subgroup
334
335
  data_type: 'varchar'
336
  is_nullable: 1
337
  size: 80
338
339
=head2 mana_id
340
341
  data_type: 'integer'
342
  is_nullable: 1
343
344
=cut
345
346
__PACKAGE__->add_columns(
347
  "id",
348
  { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
349
  "borrowernumber",
350
  { data_type => "integer", is_nullable => 1 },
351
  "date_created",
352
  {
353
    data_type => "datetime",
354
    datetime_undef_if_invalid => 1,
355
    is_nullable => 1,
356
  },
357
  "last_modified",
358
  {
359
    data_type => "datetime",
360
    datetime_undef_if_invalid => 1,
361
    is_nullable => 1,
362
  },
363
  "savedsql",
364
  { data_type => "mediumtext", is_nullable => 1 },
365
  "last_run",
366
  {
367
    data_type => "datetime",
368
    datetime_undef_if_invalid => 1,
369
    is_nullable => 1,
370
  },
371
  "report_name",
372
  { data_type => "varchar", default_value => "", is_nullable => 0, size => 255 },
373
  "type",
374
  { data_type => "varchar", is_nullable => 1, size => 255 },
375
  "notes",
376
  { data_type => "mediumtext", is_nullable => 1 },
377
  "cache_expiry",
378
  { data_type => "integer", default_value => 300, is_nullable => 0 },
379
  "public",
380
  { data_type => "tinyint", default_value => 0, is_nullable => 0 },
381
  "report_area",
382
  { data_type => "varchar", is_nullable => 1, size => 6 },
383
  "report_group",
384
  { data_type => "varchar", is_nullable => 1, size => 80 },
385
  "report_subgroup",
386
  { data_type => "varchar", is_nullable => 1, size => 80 },
387
  "mana_id",
388
  { data_type => "integer", is_nullable => 1 },
389
);
390
391
=head1 PRIMARY KEY
392
393
=over 4
394
395
=item * L</id>
396
397
=back
398
399
=cut
400
401
__PACKAGE__->set_primary_key("id");
402
188
403
189
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-01-21 13:39:29
404
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-01-21 13:39:29
190
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:198dNG9DGQzop+s5IHy7sw
405
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:198dNG9DGQzop+s5IHy7sw
Lines 214-216 sub koha_objects_class { Link Here
214
}
429
}
215
430
216
1;
431
1;
432
# End of lines loaded from '/kohadevbox/koha/Koha/Schema/Result/SavedSql.pm'
433
434
435
# You can replace this text with custom code or comments, and it will be preserved on regeneration
436
1;
(-)a/Koha/SearchEngine/Elasticsearch/Search.pm (-4 / +45 lines)
Lines 54-59 use MARC::File::XML; Link Here
54
use MIME::Base64 qw( decode_base64 );
54
use MIME::Base64 qw( decode_base64 );
55
use JSON;
55
use JSON;
56
56
57
use POSIX qw(setlocale LC_COLLATE);
58
use Unicode::Collate::Locale;
59
57
Koha::SearchEngine::Elasticsearch::Search->mk_accessors(qw( store ));
60
Koha::SearchEngine::Elasticsearch::Search->mk_accessors(qw( store ));
58
61
59
=head2 search
62
=head2 search
Lines 453-458 sub max_result_window { Link Here
453
    return $max_result_window;
456
    return $max_result_window;
454
}
457
}
455
458
459
=head2 _sort_facets
460
461
    my $facets = _sort_facets($facets);
462
463
Sorts facets using a locale.
464
465
=cut
466
467
sub _sort_facets {
468
    my ( $self, $args ) = @_;
469
    my $facets = $args->{facets};
470
    my $locale = $args->{locale};
471
472
    if ( !$locale ) {
473
474
        # Get locale from system preference, falling back to system LC_COLLATE
475
        $locale = C4::Context->preference('FacetSortingLocale') || 'default';
476
        if ( $locale eq 'default' || !$locale ) {
477
478
            #NOTE: When setlocale is run with only the 1st parameter, it is a "get" not a "set" function.
479
            $locale = setlocale(LC_COLLATE) || 'default';
480
        }
481
    }
482
483
    my $collator = Unicode::Collate::Locale->new( locale => $locale );
484
    if ( $collator && $facets ) {
485
        my @sorted_facets = sort { $collator->cmp( $a->{facet_label_value}, $b->{facet_label_value} ) } @{$facets};
486
        if (@sorted_facets) {
487
            return \@sorted_facets;
488
        }
489
    }
490
491
    #NOTE: If there was a problem, at least return the not sorted facets
492
    return $facets;
493
}
494
456
=head2 _convert_facets
495
=head2 _convert_facets
457
496
458
    my $koha_facets = _convert_facets($es_facets);
497
    my $koha_facets = _convert_facets($es_facets);
Lines 526-539 sub _convert_facets { Link Here
526
                facet_count       => $c,
565
                facet_count       => $c,
527
                facet_link_value  => $t,
566
                facet_link_value  => $t,
528
                facet_title_value => $t,
567
                facet_title_value => $t,
529
                facet_label_value => $label,    # TODO either truncate this,
568
                facet_label_value => $label || q{},    # TODO either truncate this,
530
                                                # or make the template do it like it should anyway
569
                                                       # or make the template do it like it should anyway
531
                type_link_value   => $type,
570
                type_link_value   => $type,
532
            };
571
            };
533
        }
572
        }
534
        if ( C4::Context->preference('FacetOrder') eq 'Alphabetical' ) {
573
        if ( C4::Context->preference('FacetOrder') eq 'Alphabetical' ) {
535
            @{ $facet->{facets} } =
574
            my $sorted_facets = $self->_sort_facets( { facets => $facet->{facets} } );
536
                sort { $a->{facet_label_value} cmp $b->{facet_label_value} } @{ $facet->{facets} };
575
            if ($sorted_facets) {
576
                $facet->{facets} = $sorted_facets;
577
            }
537
        }
578
        }
538
        push @facets, $facet if exists $facet->{facets};
579
        push @facets, $facet if exists $facet->{facets};
539
    }
580
    }
(-)a/acqui/basket.pl (-3 / +7 lines)
Lines 613-628 sub edi_close_and_order { Link Here
613
        }
613
        }
614
        exit;
614
        exit;
615
    } else {
615
    } else {
616
617
        my $ean_description = $query->param('ean_description');
618
        my $ean_branch      = $query->param('ean_branch');
619
616
        $template->param(
620
        $template->param(
617
            edi_confirm     => 1,
621
            edi_confirm     => 1,
618
            booksellerid    => $booksellerid,
622
            booksellerid    => $booksellerid,
619
            basketno        => $basket->{basketno},
623
            basketno        => $basket->{basketno},
620
            basketname      => $basket->{basketname},
624
            basketname      => $basket->{basketname},
621
            basketgroupname => $basket->{basketname},
625
            basketgroupname => $basket->{basketname},
626
            ean             => $ean             ? $ean             : '',
627
            ean_description => $ean_description ? $ean_description : '',
628
            ean_branch      => $ean_branch      ? $ean_branch      : '',
622
        );
629
        );
623
        if ($ean) {
624
            $template->param( ean => $ean );
625
        }
626
630
627
    }
631
    }
628
    return;
632
    return;
(-)a/acqui/newordersuggestion.pl (-1 / +25 lines)
Lines 93-99 use Modern::Perl; Link Here
93
use CGI             qw ( -utf8 );
93
use CGI             qw ( -utf8 );
94
use C4::Auth        qw( get_template_and_user );
94
use C4::Auth        qw( get_template_and_user );
95
use C4::Output      qw( output_html_with_http_headers );
95
use C4::Output      qw( output_html_with_http_headers );
96
use C4::Suggestions qw( ConnectSuggestionAndBiblio );
96
use C4::Suggestions qw( ConnectSuggestionAndBiblio ModSuggestion );
97
use C4::Budgets;
97
use C4::Budgets;
98
98
99
use Koha::Acquisition::Booksellers;
99
use Koha::Acquisition::Booksellers;
Lines 111-116 my $op = $input->param('op'); Link Here
111
my $suggestionid    = $input->param('suggestionid');
111
my $suggestionid    = $input->param('suggestionid');
112
my $duplicateNumber = $input->param('duplicateNumber');
112
my $duplicateNumber = $input->param('duplicateNumber');
113
my $uncertainprice  = $input->param('uncertainprice');
113
my $uncertainprice  = $input->param('uncertainprice');
114
my $link_order      = $input->param('link_order');
114
115
115
$op = 'else' unless $op;
116
$op = 'else' unless $op;
116
117
Lines 128-133 if ( $op eq 'connectDuplicate' ) { Link Here
128
    ConnectSuggestionAndBiblio( $suggestionid, $duplicateNumber );
129
    ConnectSuggestionAndBiblio( $suggestionid, $duplicateNumber );
129
}
130
}
130
131
132
if ( $op eq 'cud-link_order' and $link_order ) {
133
    my $order = Koha::Acquisition::Orders->find($link_order);
134
135
    if ( $order->biblionumber ) {
136
        ModSuggestion(
137
            {
138
                suggestionid => $suggestionid,
139
                biblionumber => $order->biblionumber,
140
                STATUS       => 'ORDERED',
141
            }
142
        );
143
        if ( C4::Context->preference('PlaceHoldsOnOrdersFromSuggestions') ) {
144
            my $suggestion = Koha::Suggestions->find($suggestionid);
145
            if ($suggestion) {
146
                $suggestion->place_hold();
147
            }
148
        }
149
    }
150
151
    print $input->redirect( "/cgi-bin/koha/acqui/basket.pl?basketno=" . $basketno );
152
}
153
131
my $suggestions = [
154
my $suggestions = [
132
    Koha::Suggestions->search_limited(
155
    Koha::Suggestions->search_limited(
133
        {
156
        {
Lines 147-152 $template->param( Link Here
147
    booksellerid => $booksellerid,
170
    booksellerid => $booksellerid,
148
    name         => $vendor->name,
171
    name         => $vendor->name,
149
    "op_$op"     => 1,
172
    "op_$op"     => 1,
173
    link_order   => $link_order,
150
);
174
);
151
175
152
output_html_with_http_headers $input, $cookie, $template->output;
176
output_html_with_http_headers $input, $cookie, $template->output;
(-)a/admin/columns_settings.yml (-1 / +6 lines)
Lines 736-741 modules: Link Here
736
              columnname: withdrawn_status
736
              columnname: withdrawn_status
737
            -
737
            -
738
              columnname: damaged_status
738
              columnname: damaged_status
739
            -
740
              columnname: dateaccessioned
739
            -
741
            -
740
              columnname: checkouts
742
              columnname: checkouts
741
            -
743
            -
Lines 1445-1450 modules: Link Here
1445
              columnname: itemtype
1447
              columnname: itemtype
1446
            -
1448
            -
1447
              columnname: status
1449
              columnname: status
1450
            -
1451
              columnname: notes
1448
1452
1449
    checkouthistory:
1453
    checkouthistory:
1450
      checkouthistory-table:
1454
      checkouthistory-table:
Lines 1453-1458 modules: Link Here
1453
            - columnname: type
1457
            - columnname: type
1454
              cannot_be_toggled: 1
1458
              cannot_be_toggled: 1
1455
              cannot_be_modified: 1
1459
              cannot_be_modified: 1
1460
              is_hidden: 1
1456
            -
1461
            -
1457
              columnname: date
1462
              columnname: date
1458
            -
1463
            -
Lines 2580-2583 modules: Link Here
2580
            -
2585
            -
2581
              columnname: actions
2586
              columnname: actions
2582
              cannot_be_toggled: 1
2587
              cannot_be_toggled: 1
2583
              cannot_be_modified: 1
2588
              cannot_be_modified: 1
(-)a/admin/preferences.pl (+12 lines)
Lines 122-127 sub _get_chunk { Link Here
122
        }
122
        }
123
        $chunk->{'languages'} = getTranslatedLanguages( $interface, $theme );
123
        $chunk->{'languages'} = getTranslatedLanguages( $interface, $theme );
124
        $chunk->{'type'}      = 'languages';
124
        $chunk->{'type'}      = 'languages';
125
    } elsif ( $options{'type'} && $options{'type'} eq 'locale-list' ) {
126
127
        # Dynamic locale detection for facet sorting using Koha::I18N
128
        require Koha::I18N;
129
        my $locales = Koha::I18N::available_locales();
130
        foreach my $locale (@$locales) {
131
            if ( $locale->{value} && $value && $locale->{value} eq $value ) {
132
                $locale->{selected} = 1;
133
            }
134
        }
135
        $chunk->{'CHOICES'} = $locales;
136
        $chunk->{'type'}    = 'select';
125
    } elsif ( $options{'choices'} ) {
137
    } elsif ( $options{'choices'} ) {
126
        my $add_blank;
138
        my $add_blank;
127
        if ( $options{'choices'} && ref( $options{'choices'} ) eq '' ) {
139
        if ( $options{'choices'} && ref( $options{'choices'} ) eq '' ) {
(-)a/api/v1/swagger/definitions/item.yaml (+14 lines)
Lines 12-17 properties: Link Here
12
      - string
12
      - string
13
      - "null"
13
      - "null"
14
    description: The item's barcode
14
    description: The item's barcode
15
    maxLength: 20
15
  acquisition_date:
16
  acquisition_date:
16
    type:
17
    type:
17
      - string
18
      - string
Lines 36-41 properties: Link Here
36
      - string
37
      - string
37
      - "null"
38
      - "null"
38
    description: Internal library id for the library the item belongs to
39
    description: Internal library id for the library the item belongs to
40
    maxLength: 10
39
  purchase_price:
41
  purchase_price:
40
    type:
42
    type:
41
      - number
43
      - number
Lines 103-113 properties: Link Here
103
      - string
105
      - string
104
      - "null"
106
      - "null"
105
    description: Call number for this item
107
    description: Call number for this item
108
    maxLength: 255
106
  coded_location_qualifier:
109
  coded_location_qualifier:
107
    type:
110
    type:
108
      - string
111
      - string
109
      - "null"
112
      - "null"
110
    description: Coded location qualifier
113
    description: Coded location qualifier
114
    maxLength: 10
111
  checkouts_count:
115
  checkouts_count:
112
    type:
116
    type:
113
      - integer
117
      - integer
Lines 148-153 properties: Link Here
148
      - string
152
      - string
149
      - "null"
153
      - "null"
150
    description: Library that is currently in possession item
154
    description: Library that is currently in possession item
155
    maxLength: 10
151
  timestamp:
156
  timestamp:
152
    type: string
157
    type: string
153
    format: date-time
158
    format: date-time
Lines 157-162 properties: Link Here
157
      - string
162
      - string
158
      - "null"
163
      - "null"
159
    description: Authorized value for the shelving location for this item
164
    description: Authorized value for the shelving location for this item
165
    maxLength: 80
160
  permanent_location:
166
  permanent_location:
161
    type:
167
    type:
162
      - string
168
      - string
Lines 164-169 properties: Link Here
164
    description:
170
    description:
165
      Linked to the CART and PROC temporary locations feature, stores the
171
      Linked to the CART and PROC temporary locations feature, stores the
166
      permanent shelving location
172
      permanent shelving location
173
    maxLength: 80
167
  checked_out_date:
174
  checked_out_date:
168
    type:
175
    type:
169
      - string
176
      - string
Lines 177-192 properties: Link Here
177
      - string
184
      - string
178
      - "null"
185
      - "null"
179
    description: Classification source used on this item
186
    description: Classification source used on this item
187
    maxLength: 10
180
  call_number_sort:
188
  call_number_sort:
181
    type:
189
    type:
182
      - string
190
      - string
183
      - "null"
191
      - "null"
184
    description: "?"
192
    description: "?"
193
    maxLength: 255
185
  collection_code:
194
  collection_code:
186
    type:
195
    type:
187
      - string
196
      - string
188
      - "null"
197
      - "null"
189
    description: Authorized value for the collection code associated with this item
198
    description: Authorized value for the collection code associated with this item
199
    maxLength: 80
190
  materials_notes:
200
  materials_notes:
191
    type:
201
    type:
192
      - string
202
      - string
Lines 207-212 properties: Link Here
207
      - string
217
      - string
208
      - "null"
218
      - "null"
209
    description: Itemtype defining the type for this item
219
    description: Itemtype defining the type for this item
220
    maxLength: 10
210
  effective_item_type_id:
221
  effective_item_type_id:
211
    type:
222
    type:
212
      - string
223
      - string
Lines 227-242 properties: Link Here
227
      - string
238
      - string
228
      - "null"
239
      - "null"
229
    description: Copy number
240
    description: Copy number
241
    maxLength: 32
230
  inventory_number:
242
  inventory_number:
231
    type:
243
    type:
232
      - string
244
      - string
233
      - "null"
245
      - "null"
234
    description: Inventory number
246
    description: Inventory number
247
    maxLength: 80
235
  new_status:
248
  new_status:
236
    type:
249
    type:
237
      - string
250
      - string
238
      - "null"
251
      - "null"
239
    description: "'new' value, whatever free-text information."
252
    description: "'new' value, whatever free-text information."
253
    maxLength: 32
240
  exclude_from_local_holds_priority:
254
  exclude_from_local_holds_priority:
241
    type: boolean
255
    type: boolean
242
    description: Exclude this item from local holds priority.
256
    description: Exclude this item from local holds priority.
(-)a/api/v1/swagger/definitions/library.yaml (+4 lines)
Lines 29-34 properties: Link Here
29
      - string
29
      - string
30
      - "null"
30
      - "null"
31
    description: the postal code of the library
31
    description: the postal code of the library
32
    maxLength: 25
32
  city:
33
  city:
33
    type:
34
    type:
34
      - string
35
      - string
Lines 84-89 properties: Link Here
84
      - string
85
      - string
85
      - "null"
86
      - "null"
86
    description: the IP address for your library or branch
87
    description: the IP address for your library or branch
88
    maxLength: 15
87
  notes:
89
  notes:
88
    type:
90
    type:
89
      - string
91
      - string
Lines 94-99 properties: Link Here
94
      - string
96
      - string
95
      - "null"
97
      - "null"
96
    description: geolocation of your library
98
    description: geolocation of your library
99
    maxLength: 255
97
  marc_org_code:
100
  marc_org_code:
98
    type:
101
    type:
99
      - string
102
      - string
Lines 101-106 properties: Link Here
101
    description: MARC Organization Code, see
104
    description: MARC Organization Code, see
102
      http://www.loc.gov/marc/organizations/orgshome.html, when empty defaults
105
      http://www.loc.gov/marc/organizations/orgshome.html, when empty defaults
103
      to syspref MARCOrgCode
106
      to syspref MARCOrgCode
107
    maxLength: 16
104
  pickup_location:
108
  pickup_location:
105
    type: boolean
109
    type: boolean
106
    description: If the library can act as a pickup location
110
    description: If the library can act as a pickup location
(-)a/api/v1/swagger/definitions/patron.yaml (+15 lines)
Lines 9-14 properties: Link Here
9
      - string
9
      - string
10
      - "null"
10
      - "null"
11
    description: library assigned user identifier
11
    description: library assigned user identifier
12
    maxLength: 32
12
  surname:
13
  surname:
13
    type:
14
    type:
14
      - string
15
      - string
Lines 178-186 properties: Link Here
178
  library_id:
179
  library_id:
179
    type: string
180
    type: string
180
    description: Internal identifier for the patron's home library
181
    description: Internal identifier for the patron's home library
182
    maxLength: 10
181
  category_id:
183
  category_id:
182
    type: string
184
    type: string
183
    description: Internal identifier for the patron's category
185
    description: Internal identifier for the patron's category
186
    maxLength: 10
184
  date_enrolled:
187
  date_enrolled:
185
    type:
188
    type:
186
      - string
189
      - string
Lines 227-242 properties: Link Here
227
      - string
230
      - string
228
      - "null"
231
      - "null"
229
    description: used for children to include the relationship to their guarantor
232
    description: used for children to include the relationship to their guarantor
233
    maxLength: 100
230
  gender:
234
  gender:
231
    type:
235
    type:
232
      - string
236
      - string
233
      - "null"
237
      - "null"
234
    description: patron's gender
238
    description: patron's gender
239
    maxLength: 1
235
  userid:
240
  userid:
236
    type:
241
    type:
237
      - string
242
      - string
238
      - "null"
243
      - "null"
239
    description: patron's login
244
    description: patron's login
245
    maxLength: 75
240
  opac_notes:
246
  opac_notes:
241
    type:
247
    type:
242
      - string
248
      - string
Lines 247-262 properties: Link Here
247
      - string
253
      - string
248
      - "null"
254
      - "null"
249
    description: a note related to patron's alternate address
255
    description: a note related to patron's alternate address
256
    maxLength: 255
250
  statistics_1:
257
  statistics_1:
251
    type:
258
    type:
252
      - string
259
      - string
253
      - "null"
260
      - "null"
254
    description: a field that can be used for any information unique to the library
261
    description: a field that can be used for any information unique to the library
262
    maxLength: 80
255
  statistics_2:
263
  statistics_2:
256
    type:
264
    type:
257
      - string
265
      - string
258
      - "null"
266
      - "null"
259
    description: a field that can be used for any information unique to the library
267
    description: a field that can be used for any information unique to the library
268
    maxLength: 80
260
  autorenew_checkouts:
269
  autorenew_checkouts:
261
    type: boolean
270
    type: boolean
262
    description: indicate whether auto-renewal is allowed for patron
271
    description: indicate whether auto-renewal is allowed for patron
Lines 311-316 properties: Link Here
311
      - "null"
320
      - "null"
312
    description: the mobile phone number where the patron would like to receive notices (if
321
    description: the mobile phone number where the patron would like to receive notices (if
313
      SMS turned on)
322
      SMS turned on)
323
    maxLength: 50
314
  sms_provider_id:
324
  sms_provider_id:
315
    type:
325
    type:
316
      - integer
326
      - integer
Lines 327-332 properties: Link Here
327
    description: controls if relatives can see this patron's fines
337
    description: controls if relatives can see this patron's fines
328
  check_previous_checkout:
338
  check_previous_checkout:
329
    type: string
339
    type: string
340
    enum:
341
      - yes
342
      - no
343
      - inherit
330
    description: produce a warning for this patron if this item has previously been checked
344
    description: produce a warning for this patron if this item has previously been checked
331
      out to this patron if 'yes', not if 'no', defer to category setting if
345
      out to this patron if 'yes', not if 'no', defer to category setting if
332
      'inherit'
346
      'inherit'
Lines 344-349 properties: Link Here
344
  lang:
358
  lang:
345
    type: string
359
    type: string
346
    description: lang to use to send notices to this patron
360
    description: lang to use to send notices to this patron
361
    maxLength: 25
347
  login_attempts:
362
  login_attempts:
348
    type:
363
    type:
349
      - integer
364
      - integer
(-)a/api/v1/swagger/definitions/patron_category.yaml (+7 lines)
Lines 4-9 properties: Link Here
4
  patron_category_id:
4
  patron_category_id:
5
    type: string
5
    type: string
6
    description: Internal patron category identifier
6
    description: Internal patron category identifier
7
    maxLength: 10
7
  name:
8
  name:
8
    type:
9
    type:
9
      - string
10
      - string
Lines 56-64 properties: Link Here
56
  category_type:
57
  category_type:
57
    type: string
58
    type: string
58
    description: Type of Koha patron (Adult, Child, Professional, Organizational, Statistical, Staff)
59
    description: Type of Koha patron (Adult, Child, Professional, Organizational, Statistical, Staff)
60
    maxLength: 1
59
  block_expired_patron_opac_actions:
61
  block_expired_patron_opac_actions:
60
    type: string
62
    type: string
61
    description: Specific actions expired patrons of this category are blocked from performing. OPAC actions blocked based on the patron category take priority over this preference
63
    description: Specific actions expired patrons of this category are blocked from performing. OPAC actions blocked based on the patron category take priority over this preference
64
    maxLength: 128
62
  default_privacy:
65
  default_privacy:
63
    type: string
66
    type: string
64
    enum:
67
    enum:
Lines 68-73 properties: Link Here
68
    description: Default privacy setting for this patron category
71
    description: Default privacy setting for this patron category
69
  check_prev_checkout:
72
  check_prev_checkout:
70
    type: string
73
    type: string
74
    enum:
75
      - yes
76
      - no
77
      - inherit
71
    description: Produce a warning for this patron category if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to syspref setting if ''inherit''.'
78
    description: Produce a warning for this patron category if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to syspref setting if ''inherit''.'
72
  can_place_ill_in_opac:
79
  can_place_ill_in_opac:
73
    type: boolean
80
    type: boolean
(-)a/api/v1/swagger/paths/holds.yaml (+18 lines)
Lines 221-226 Link Here
221
          type: string
221
          type: string
222
          enum:
222
          enum:
223
            - any
223
            - any
224
            - bad_address
225
            - card_lost
226
            - debt_limit
227
            - expired
228
            - hold_limit
229
            - restricted
224
        collectionFormat: csv
230
        collectionFormat: csv
225
    consumes:
231
    consumes:
226
      - application/json
232
      - application/json
Lines 247-252 Link Here
247
        description: Borrower not found
253
        description: Borrower not found
248
        schema:
254
        schema:
249
          $ref: "../swagger.yaml#/definitions/error"
255
          $ref: "../swagger.yaml#/definitions/error"
256
      "409":
257
        description: |
258
          Conflict. Possible `error_code` attribute values:
259
260
            * `bad_address`
261
            * `card_lost`
262
            * `debt_limit`
263
            * `expired`
264
            * `hold_limit`
265
            * `restricted`
266
        schema:
267
          $ref: "../swagger.yaml#/definitions/error"
250
      "500":
268
      "500":
251
        description: |
269
        description: |
252
          Internal server error. Possible `error_code` attribute values:
270
          Internal server error. Possible `error_code` attribute values:
(-)a/catalogue/moredetail.pl (-4 / +5 lines)
Lines 81-89 my $title = $query->param('title'); Link Here
81
my $bi    = $query->param('bi');
81
my $bi    = $query->param('bi');
82
$bi         = $biblionumber unless $bi;
82
$bi         = $biblionumber unless $bi;
83
$itemnumber = $query->param('itemnumber');
83
$itemnumber = $query->param('itemnumber');
84
my $data         = &GetBiblioData($biblionumber);
84
my $data           = &GetBiblioData($biblionumber);
85
my $dewey        = $data->{'dewey'};
85
my $dewey          = $data->{'dewey'};
86
my $showallitems = $query->param('showallitems');
86
my $showallitems   = $query->param('showallitems');
87
my $withdraw_error = $query->param('nowithdraw');
87
88
88
#coping with subscriptions
89
#coping with subscriptions
89
my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber);
90
my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber);
Lines 325-330 $template->param( Link Here
325
);
326
);
326
327
327
$template->param(
328
$template->param(
329
    withdraw_error      => $withdraw_error,
328
    ITEM_DATA           => \@item_data,
330
    ITEM_DATA           => \@item_data,
329
    moredetailview      => 1,
331
    moredetailview      => 1,
330
    loggedinuser        => $loggedinuser,
332
    loggedinuser        => $loggedinuser,
Lines 341-344 my $holds = $biblio->holds; Link Here
341
$template->param( holdcount => $holds->count );
343
$template->param( holdcount => $holds->count );
342
344
343
output_html_with_http_headers $query, $cookie, $template->output;
345
output_html_with_http_headers $query, $cookie, $template->output;
344
(-)a/catalogue/updateitem.pl (-3 / +7 lines)
Lines 90-98 if ( $op eq "cud-set_non_public_note" ) { Link Here
90
    print $cgi->redirect("moredetail.pl?biblionumber=$biblionumber&itemnumber=$itemnumber#item$itemnumber");
90
    print $cgi->redirect("moredetail.pl?biblionumber=$biblionumber&itemnumber=$itemnumber#item$itemnumber");
91
    exit;
91
    exit;
92
}
92
}
93
93
eval { $item->store; };
94
$item->store;
94
if ($@) {
95
95
    my $error_message = $@->message;
96
    print $cgi->redirect(
97
        "moredetail.pl?biblionumber=$biblionumber&itemnumber=$itemnumber&nowithdraw=$error_message#item$itemnumber");
98
    exit;
99
}
96
LostItem( $itemnumber, 'moredetail' ) if $op eq "cud-set_lost";
100
LostItem( $itemnumber, 'moredetail' ) if $op eq "cud-set_lost";
97
101
98
print $cgi->redirect(
102
print $cgi->redirect(
(-)a/cataloguing/addbiblio.pl (-2 / +3 lines)
Lines 33-39 use C4::Biblio qw( Link Here
33
    GetMarcStructure
33
    GetMarcStructure
34
    GetUsedMarcStructure
34
    GetUsedMarcStructure
35
    ModBiblio
35
    ModBiblio
36
    prepare_host_field
37
    PrepHostMarcField
36
    PrepHostMarcField
38
    TransformHtmlToMarc
37
    TransformHtmlToMarc
39
    ApplyMarcOverlayRules
38
    ApplyMarcOverlayRules
Lines 645-652 if ($hostbiblionumber) { Link Here
645
if ($parentbiblio) {
644
if ($parentbiblio) {
646
    my $marcflavour = C4::Context->preference('marcflavour');
645
    my $marcflavour = C4::Context->preference('marcflavour');
647
    $record = MARC::Record->new();
646
    $record = MARC::Record->new();
647
    $record->leader('     naa a22      i 4500');
648
    SetMarcUnicodeFlag( $record, $marcflavour );
648
    SetMarcUnicodeFlag( $record, $marcflavour );
649
    my $hostfield = prepare_host_field( $parentbiblio, $marcflavour );
649
    my $parent    = Koha::Biblios->find($parentbiblio);
650
    my $hostfield = $parent->generate_marc_host_field;
650
    if ($hostfield) {
651
    if ($hostfield) {
651
        $record->append_fields($hostfield);
652
        $record->append_fields($hostfield);
652
    }
653
    }
(-)a/cataloguing/additem.pl (-1 / +6 lines)
Lines 50-55 use MARC::File::XML; Link Here
50
use MIME::Base64 qw( decode_base64url encode_base64url );
50
use MIME::Base64 qw( decode_base64url encode_base64url );
51
use Storable     qw( freeze thaw );
51
use Storable     qw( freeze thaw );
52
use URI::Escape  qw( uri_escape_utf8 );
52
use URI::Escape  qw( uri_escape_utf8 );
53
use Try::Tiny    qw( catch try );
53
54
54
our $dbh = C4::Context->dbh;
55
our $dbh = C4::Context->dbh;
55
56
Lines 641-647 if ( $op eq "cud-additem" ) { Link Here
641
        if ( $newitemlost && $newitemlost ge '1' && !$olditemlost ) {
642
        if ( $newitemlost && $newitemlost ge '1' && !$olditemlost ) {
642
            LostItem( $item->itemnumber, 'additem' );
643
            LostItem( $item->itemnumber, 'additem' );
643
        }
644
        }
644
        $item->store;
645
        try {
646
            $item->store;
647
        } catch {
648
            push @errors, $_->error;
649
        }
645
    }
650
    }
646
651
647
    $nextop = "cud-additem";
652
    $nextop = "cud-additem";
(-)a/circ/circulation.pl (-1 / +1 lines)
Lines 626-632 if ($patron) { Link Here
626
        $template->param( is_debarred => 1 );
626
        $template->param( is_debarred => 1 );
627
        $noissues = 1;
627
        $noissues = 1;
628
    }
628
    }
629
    if ( $patron->borrowernumber eq C4::Context->preference("AnonymousPatron") ) {
629
    if ( $patron->is_anonymous ) {
630
        $template->param( is_anonymous => 1 );
630
        $template->param( is_anonymous => 1 );
631
        $noissues = 1;
631
        $noissues = 1;
632
    }
632
    }
(-)a/cpanfile (+1 lines)
Lines 107-112 requires 'Storable', '2.20'; Link Here
107
requires 'String::Random', '0.22';
107
requires 'String::Random', '0.22';
108
requires 'Template', '>= 2.27, != 3.008';
108
requires 'Template', '>= 2.27, != 3.008';
109
requires 'Template::Plugin::HtmlToText', '0.03';
109
requires 'Template::Plugin::HtmlToText', '0.03';
110
requires 'Template::Plugin::JSON', '0.08',
110
requires 'Template::Plugin::JSON::Escape', '0.02';
111
requires 'Template::Plugin::JSON::Escape', '0.02';
111
requires 'Term::ANSIColor', '1.1';
112
requires 'Term::ANSIColor', '1.1';
112
requires 'Test', '1.25';
113
requires 'Test', '1.25';
(-)a/cypress.config.ts (-7 / +4 lines)
Lines 12-27 export default defineConfig({ Link Here
12
            return require("./t/cypress/plugins/index.js")(on, config);
12
            return require("./t/cypress/plugins/index.js")(on, config);
13
        },
13
        },
14
        experimentalStudio: true,
14
        experimentalStudio: true,
15
        baseUrl: "http://localhost:8081",
15
        baseUrl: process.env.KOHA_INTRANET_URL || "http://localhost:8081",
16
        specPattern: "t/cypress/integration/**/*.*",
16
        specPattern: "t/cypress/integration/**/*.*",
17
        supportFile: "t/cypress/support/e2e.js",
17
        supportFile: "t/cypress/support/e2e.js",
18
        env: {
18
        env: {
19
            db: {
19
            opacBaseUrl: process.env.KOHA_OPAC_URL || "http://localhost:8080",
20
                host: "db",
20
            apiUsername: "koha",
21
                user: "koha_kohadev",
21
            apiPassword: "koha",
22
                password: "password",
23
                database: "koha_kohadev",
24
            },
25
        },
22
        },
26
    },
23
    },
27
});
24
});
(-)a/debian/build-git-snapshot (-41 / +61 lines)
Lines 24-61 Link Here
24
use Modern::Perl;
24
use Modern::Perl;
25
25
26
use Getopt::Long qw(:config no_ignore_case);
26
use Getopt::Long qw(:config no_ignore_case);
27
use POSIX qw/strftime/;
27
use POSIX        qw/strftime/;
28
28
29
my $basetgz;
29
my $basetgz;
30
my $buildresult;
30
my $buildresult;
31
my $distribution='squeeze-dev';
31
my $distribution   = 'testing';
32
my $git_checks='all';
32
my $git_checks     = 'all';
33
my $version='16.06~git';
33
my $version        = '16.06~git';
34
my $auto_version=1;
34
my $auto_version   = 1;
35
my $auto_changelog = 1;
35
my $need_help;
36
my $need_help;
37
my $incr;
38
my $urgency = 'medium';
36
my $debug;
39
my $debug;
37
40
41
my $release_str;
42
38
GetOptions(
43
GetOptions(
39
    'basetgz|b=s'      => \$basetgz,
44
    'basetgz|b=s'      => \$basetgz,
40
    'buildresult|r=s'   => \$buildresult,
45
    'buildresult|r=s'  => \$buildresult,
41
    'distribution|D=s'  => \$distribution,
46
    'distribution|D=s' => \$distribution,
42
    'git-checks|g=s'    => \$git_checks,
47
    'git-checks|g=s'   => \$git_checks,
43
    'version|v=s'       => \$version,
48
    'version|v=s'      => \$version,
44
    'autoversion!'      => \$auto_version,
49
    'incr|i=s'         => \$incr,
45
    'help|h'            => \$need_help,
50
    'urgency|u=s'      => \$urgency,
46
    'debug|d'           => \$debug,
51
    'autoversion!'     => \$auto_version,
52
    'autochangelog!'   => \$auto_changelog,
53
    'help|h'           => \$need_help,
54
    'debug|d'          => \$debug,
47
);
55
);
48
56
49
help_and_exit() if $need_help;
57
help_and_exit() if $need_help;
50
58
51
52
sub sys_command_output {
59
sub sys_command_output {
53
    my ($command) = @_;
60
    my ($command) = @_;
54
61
55
    print "$command\n" if $debug;
62
    print "$command\n" if $debug;
56
    my $command_output;
63
    my $command_output;
57
    open($command_output, "-|", "$command ")
64
    open( $command_output, "-|", "$command " )
58
      or die qq{Cannot execute "$command": $!"};
65
        or die qq{Cannot execute "$command": $!"};
59
    return map { chomp; $_ } <$command_output>;
66
    return map { chomp; $_ } <$command_output>;
60
}
67
}
61
68
Lines 70-86 sub everything_is_committed { Link Here
70
    my $filter;
77
    my $filter;
71
    for ($git_checks) {
78
    for ($git_checks) {
72
        $_ eq "none"
79
        $_ eq "none"
73
          and return 1;
80
            and return 1;
74
81
75
        $_ eq "modified"
82
        $_ eq "modified"
76
          and $filter = "no",
83
            and $filter = "no",
77
              last;
84
            last;
78
85
79
        $_ eq "all"
86
        $_ eq "all"
80
          and $filter = "normal",
87
            and $filter = "normal",
81
              last;
88
            last;
82
89
83
	    help_and_exit("$0: --git-checks/-g must be one of 'all', 'modified', or 'none'");
90
        help_and_exit("$0: --git-checks/-g must be one of 'all', 'modified', or 'none'");
84
    }
91
    }
85
    my $has_changes = grep /^xxx/, sys_command_output("git status --porcelain -u${filter}");
92
    my $has_changes = grep /^xxx/, sys_command_output("git status --porcelain -u${filter}");
86
93
Lines 88-100 sub everything_is_committed { Link Here
88
}
95
}
89
96
90
sub help_and_exit {
97
sub help_and_exit {
91
	my $msg = shift;
98
    my $msg = shift;
92
	if ($msg) {
99
    if ($msg) {
93
    	print "$msg\n\n";
100
        print "$msg\n\n";
94
    }
101
    }
95
    print <<EOH;
102
    print <<EOH;
96
This builds Koha deb packages, from a git snapshot. It's not suitable for
103
This builds Koha deb packages, from a git snapshot. It's not suitable for
97
making upstreamable verions, but handy for your own local packages.
104
making upstreamable versions, but handy for your own local packages.
98
105
99
Options:
106
Options:
100
    --buildresult, -r
107
    --buildresult, -r
Lines 102-108 Options: Link Here
102
        Default is whatever pdebuild uses.
109
        Default is whatever pdebuild uses.
103
    --distribution, -D
110
    --distribution, -D
104
        the distribution value to set in the changelog when editing it. Default
111
        the distribution value to set in the changelog when editing it. Default
105
        is 'squeeze-dev'.
112
        is 'testing'.
106
    --git-checks, -g
113
    --git-checks, -g
107
        what level of git checks are run to determine if the working copy is
114
        what level of git checks are run to determine if the working copy is
108
        clean enough. One of 'all' (any changes are bad), 'modified' (only
115
        clean enough. One of 'all' (any changes are bad), 'modified' (only
Lines 110-118 Options: Link Here
110
        (checking git status is skipped totally.) Default is 'all'.
117
        (checking git status is skipped totally.) Default is 'all'.
111
    --version, -v
118
    --version, -v
112
        the version string for the resulting package. Default is '$version'.
119
        the version string for the resulting package. Default is '$version'.
120
    --urgency, -u
121
        the urgency string for the resulting package. Default is '$urgency'.
122
    --incr, -i
123
        set debian revision (default = '-1')
113
    --(no)autoversion
124
    --(no)autoversion
114
        whether or not to use the date and git commit ID in the version value.
125
        whether or not to use the date and git commit ID in the version value.
115
        Default is to include it.
126
        Default is to include it.
127
    --(no)autochangelog
128
        whether or not to update the debian/changelog file.
129
        Default is to update it.
116
    --debug, -d
130
    --debug, -d
117
EOH
131
EOH
118
    exit;
132
    exit;
Lines 124-156 sub latest_sha1 { Link Here
124
138
125
sub adjust_debian_changelog {
139
sub adjust_debian_changelog {
126
    my ($newversion) = @_;
140
    my ($newversion) = @_;
127
    # debian revision
141
    $newversion .= $incr ? "-$incr" : "-1";
128
    $newversion .= "-1";
142
143
    $release_str = "New upstream ";
144
    $release_str .= 'SECURITY ' if $urgency eq 'high';
145
    $release_str .= "release ($version)";
129
146
130
    sys_command_output( qq{dch --force-distribution -D "$distribution" -v "$newversion" "Building git snapshot."} );
147
    sys_command_output(
131
    sys_command_output( qq{dch -r "Building git snapshot."} );
148
        qq{dch --urgency $urgency -b --force-distribution -D "$distribution" -v "$newversion" "$release_str"});
149
    sys_command_output(qq{dch -r "$release_str"});
132
}
150
}
133
151
134
sub reset_debian_changelog {
152
sub reset_debian_changelog {
135
    sys_command_output( qq{git checkout -- debian/changelog} );
153
    sys_command_output(qq{git checkout -- debian/changelog});
136
}
154
}
137
155
138
sub build_package {
156
sub build_package {
139
    my ($newversion) = @_;
157
    my ($newversion) = @_;
140
    sys_command_output( qq{git archive --format=tar --prefix="koha-$newversion/" HEAD | gzip -9 > "../koha_$newversion.orig.tar.gz"} );
158
    sys_command_output(
159
        qq{git archive --format=tar --prefix="koha-$newversion/" HEAD | gzip -9 > "../koha_$newversion.orig.tar.gz"});
141
160
142
    my $pdebuildopts = $buildresult ? "--buildresult $buildresult" : "";
161
    my $pdebuildopts    = $buildresult ? "--buildresult $buildresult"                                              : "";
143
    my $pdebuildbasetgz = $basetgz ? "-- --use-network yes --basetgz /var/cache/pbuilder/" . $basetgz . ".tgz" : "";
162
    my $pdebuildbasetgz = $basetgz     ? "-- --use-network yes --basetgz /var/cache/pbuilder/" . $basetgz . ".tgz" : "";
144
    sys_command_output_screen( "pdebuild $pdebuildbasetgz $pdebuildopts" );
163
    sys_command_output_screen("pdebuild --debbuildopts -sa $pdebuildbasetgz $pdebuildopts");
145
}
164
}
146
165
147
everything_is_committed() or die "cannot build: uncommited changes";
166
everything_is_committed() or die "cannot build: uncommitted changes";
148
167
149
my $newversion = $auto_version
168
my $newversion =
150
  ? sprintf ('%s%s.%s', $version, strftime("+%Y%m%d%H%M%S", localtime), latest_sha1())
169
    $auto_version
151
  : $version;
170
    ? sprintf( '%s%s.%s', $version, strftime( "+%Y%m%d%H%M%S", localtime ), latest_sha1() )
171
    : $version;
152
172
153
adjust_debian_changelog( $newversion );
173
adjust_debian_changelog($newversion) if $auto_changelog;
154
build_package( $newversion );
174
build_package($newversion);
155
reset_debian_changelog();
175
reset_debian_changelog();
156
176
(-)a/debian/control (+3 lines)
Lines 137-142 Build-Depends: libalgorithm-checkdigits-perl, Link Here
137
 libtemplate-plugin-gettext-perl,
137
 libtemplate-plugin-gettext-perl,
138
 libtemplate-plugin-htmltotext-perl,
138
 libtemplate-plugin-htmltotext-perl,
139
 libtemplate-plugin-json-escape-perl,
139
 libtemplate-plugin-json-escape-perl,
140
 libtemplate-plugin-json-perl,
140
 libtemplate-plugin-stash-perl,
141
 libtemplate-plugin-stash-perl,
141
 libtest-deep-perl,
142
 libtest-deep-perl,
142
 libtest-exception-perl,
143
 libtest-exception-perl,
Lines 370-375 Depends: libalgorithm-checkdigits-perl, Link Here
370
 libpdf-table-perl,
371
 libpdf-table-perl,
371
 libplack-middleware-logwarn-perl,
372
 libplack-middleware-logwarn-perl,
372
 libplack-middleware-reverseproxy-perl,
373
 libplack-middleware-reverseproxy-perl,
374
 libpod-coverage-perl,
373
 libreadonly-perl,
375
 libreadonly-perl,
374
 libscalar-list-utils-perl,
376
 libscalar-list-utils-perl,
375
 libschedule-at-perl,
377
 libschedule-at-perl,
Lines 386-391 Depends: libalgorithm-checkdigits-perl, Link Here
386
 libtemplate-plugin-gettext-perl,
388
 libtemplate-plugin-gettext-perl,
387
 libtemplate-plugin-htmltotext-perl,
389
 libtemplate-plugin-htmltotext-perl,
388
 libtemplate-plugin-json-escape-perl,
390
 libtemplate-plugin-json-escape-perl,
391
 libtemplate-plugin-json-perl,
389
 libtemplate-plugin-stash-perl,
392
 libtemplate-plugin-stash-perl,
390
 libtest-deep-perl,
393
 libtest-deep-perl,
391
 libtest-exception-perl,
394
 libtest-exception-perl,
(-)a/debian/templates/koha-conf-site.xml.in (-5 / +3 lines)
Lines 474-484 __END_SRU_PUBLICSERVER__ Link Here
474
 </background_jobs_worker>
474
 </background_jobs_worker>
475
475
476
 <do_not_remove_cookie>__KEEP_COOKIE__</do_not_remove_cookie>
476
 <do_not_remove_cookie>__KEEP_COOKIE__</do_not_remove_cookie>
477
 <do_not_remove_cookie>catalogue_editor_\d+</do_not_remove_cookie>
477
 <!-- Example lines. See Koha/CookieManager.pm for more details.
478
 <!-- Uncomment lines like hereunder to not clear cookies at logout:
478
     <do_not_remove_cookie>some_cookie</do_not_remove_cookie>
479
      The cookie name is case sensitive.
479
     <remove_cookie>another_cookie</remove_cookie>
480
      NOTE: You may use regex constructions like the example above.
481
     <do_not_remove_cookie>KohaOpacLanguage</do_not_remove_cookie>
482
 -->
480
 -->
483
481
484
 <message_domain_limits>
482
 <message_domain_limits>
(-)a/debian/templates/zebra-authorities-dom-site.cfg.in (-1 / +1 lines)
Lines 8-14 profilePath:/etc/koha/sites/__KOHASITE__:/etc/koha/zebradb/authorities/etc:/etc/ Link Here
8
8
9
encoding: UTF-8
9
encoding: UTF-8
10
# modulePath - where to look for loadable zebra modules
10
# modulePath - where to look for loadable zebra modules
11
modulePath: /usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules
11
modulePath: /usr/lib64/idzebra-2.0/modules:/usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules
12
12
13
# Files that describe the attribute sets supported.
13
# Files that describe the attribute sets supported.
14
attset: bib1.att
14
attset: bib1.att
(-)a/debian/templates/zebra-biblios-dom-site.cfg.in (-1 / +1 lines)
Lines 5-11 Link Here
5
# Where are the config files located?
5
# Where are the config files located?
6
profilePath:/etc/koha/sites/__KOHASITE__:/etc/koha/zebradb/biblios/etc:/etc/koha/zebradb/etc:/etc/koha/zebradb/marc_defs/__ZEBRA_MARC_FORMAT__/biblios:/etc/koha/zebradb/lang_defs/__ZEBRA_LANGUAGE__:/etc/koha/zebradb/xsl
6
profilePath:/etc/koha/sites/__KOHASITE__:/etc/koha/zebradb/biblios/etc:/etc/koha/zebradb/etc:/etc/koha/zebradb/marc_defs/__ZEBRA_MARC_FORMAT__/biblios:/etc/koha/zebradb/lang_defs/__ZEBRA_LANGUAGE__:/etc/koha/zebradb/xsl
7
# modulePath - where to look for loadable zebra modules
7
# modulePath - where to look for loadable zebra modules
8
modulePath: /usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules
8
modulePath: /usr/lib64/idzebra-2.0/modules:/usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules
9
9
10
encoding: UTF-8
10
encoding: UTF-8
11
# Files that describe the attribute sets supported.
11
# Files that describe the attribute sets supported.
(-)a/docs/teams.yaml (-50 / +121 lines)
Lines 573-580 team: Link Here
573
    translation:
573
    translation:
574
      name: Bernardo González Kriegel
574
      name: Bernardo González Kriegel
575
    packaging:
575
    packaging:
576
      name: Mirko Tietgen
576
      - name: Mirko Tietgen
577
      name: Mason James
577
      - name: Mason James
578
    ci:
578
    ci:
579
      - name: Tomás Cohen Arazi
579
      - name: Tomás Cohen Arazi
580
    maintainer:
580
    maintainer:
Lines 624-631 team: Link Here
624
    translation:
624
    translation:
625
      name: Bernardo González Kriegel
625
      name: Bernardo González Kriegel
626
    packaging:
626
    packaging:
627
      name: Mirko Tietgen
627
      - name: Mirko Tietgen
628
      name: Mason James
628
      - name: Mason James (17.05)
629
    ci:
629
    ci:
630
      - name: Tomás Cohen Arazi
630
      - name: Tomás Cohen Arazi
631
    maintainer:
631
    maintainer:
Lines 680-687 team: Link Here
680
      - name: Indranil Das Gupta
680
      - name: Indranil Das Gupta
681
      - name: Bernardo González Kriegel
681
      - name: Bernardo González Kriegel
682
    packaging:
682
    packaging:
683
      name: Mirko Tietgen
683
      - name: Mirko Tietgen
684
      name: Mason James
684
      - name: Mason James
685
    ci:
685
    ci:
686
      - name: Tomás Cohen Arazi
686
      - name: Tomás Cohen Arazi
687
      - name: Jonathan Druart
687
      - name: Jonathan Druart
Lines 1008-1013 team: Link Here
1008
        name: Owen Leonard
1008
        name: Owen Leonard
1009
      - area: Accounts
1009
      - area: Accounts
1010
        name: Martin Renvoize
1010
        name: Martin Renvoize
1011
      - area: Elasticsearch
1012
        name: Fridolin Somers
1011
      - area: Zebra
1013
      - area: Zebra
1012
        name: Fridolin Somers
1014
        name: Fridolin Somers
1013
    bugwrangler:
1015
    bugwrangler:
Lines 1027-1032 team: Link Here
1027
      - name: Mason James
1029
      - name: Mason James
1028
    ci:
1030
    ci:
1029
      - name: Mason James
1031
      - name: Mason James
1032
    wiki:
1033
      - name: Thomas Dukleth
1030
    accessibility_advocates:
1034
    accessibility_advocates:
1031
      - name: Henry Bolshaw
1035
      - name: Henry Bolshaw
1032
      - name: Wainui Witika-Park
1036
      - name: Wainui Witika-Park
Lines 1050-1065 team: Link Here
1050
    qa_manager:
1054
    qa_manager:
1051
      name: Katrin Fischer
1055
      name: Katrin Fischer
1052
    qa:
1056
    qa:
1053
      - name: Tomás Cohen Arazi
1057
      - name: Andrew Nugged
1054
      - name: Nick Clemens
1055
      - name: Jonathan Druart
1058
      - name: Jonathan Druart
1056
      - name: Victor Grousset
1057
      - name: Kyle M Hall
1058
      - name: Joonas Kylmälä
1059
      - name: Joonas Kylmälä
1059
      - name: Andrew Nugged
1060
      - name: Kyle M Hall
1060
      - name: Martin Renvoize
1061
      - name: Marcel de Rooy
1061
      - name: Marcel de Rooy
1062
      - name: Martin Renvoize
1063
      - name: Nick Clemens
1062
      - name: Petro Vashchuk
1064
      - name: Petro Vashchuk
1065
      - name: Tomás Cohen Arazi
1066
      - name: Victor Grousset
1063
    te:
1067
    te:
1064
      - area: UI Design
1068
      - area: UI Design
1065
        name: Owen Leonard
1069
        name: Owen Leonard
Lines 1076-1086 team: Link Here
1076
      name: David Nind
1080
      name: David Nind
1077
    documentation_team:
1081
    documentation_team:
1078
      - name: Aude Charillon
1082
      - name: Aude Charillon
1079
      - name: Rocio Lopez
1080
      - name: Kelly McElligott
1081
      - name: Martin Renvoize
1082
      - name: Caroline Cyr La Rose
1083
      - name: Caroline Cyr La Rose
1084
      - name: Kelly McElligott
1083
      - name: Lucy Vaux-Harvey
1085
      - name: Lucy Vaux-Harvey
1086
      - name: Martin Renvoize
1087
      - name: Rocio Lopez
1084
    meeting_facilitator:
1088
    meeting_facilitator:
1085
      - name: Jessica Zairo
1089
      - name: Jessica Zairo
1086
    translations:
1090
    translations:
Lines 1126-1131 team: Link Here
1126
      - name: Marcel de Rooy
1130
      - name: Marcel de Rooy
1127
      - name: Fridolin Somers
1131
      - name: Fridolin Somers
1128
      - name: Petro Vashchuk
1132
      - name: Petro Vashchuk
1133
      - name: David Cook
1129
    te:
1134
    te:
1130
      - area: UI Design
1135
      - area: UI Design
1131
        name: Owen Leonard
1136
        name: Owen Leonard
Lines 1193-1201 team: Link Here
1193
      - area: REST API
1198
      - area: REST API
1194
        name: Martin Renvoize
1199
        name: Martin Renvoize
1195
      - area: ERM
1200
      - area: ERM
1196
        name: Pedro Amorim
1201
        name: Pedro Amorim, Agustín Moyano
1197
      - area: ERM
1198
        name: Agustín Moyano
1199
    bugwrangler:
1202
    bugwrangler:
1200
      - name: Aleisha Amohia
1203
      - name: Aleisha Amohia
1201
      - name: Indranil Das Gupta
1204
      - name: Indranil Das Gupta
Lines 1243-1259 team: Link Here
1243
      name: Katrin Fischer
1246
      name: Katrin Fischer
1244
    qa:
1247
    qa:
1245
      - name: Aleisha Amohia
1248
      - name: Aleisha Amohia
1246
      - name: Nick Clemens
1249
      - name: Andrii Nugged
1247
      - name: David Cook
1250
      - name: David Cook
1251
      - name: Emily Lamancusa
1248
      - name: Jonathan Druart
1252
      - name: Jonathan Druart
1249
      - name: Lucas Gass
1250
      - name: Victor Grousset
1251
      - name: Kyle M Hall
1253
      - name: Kyle M Hall
1252
      - name: Emily Lamancusa
1254
      - name: Lucas Gass
1253
      - name: Andrii Nugged
1254
      - name: Martin Renvoize
1255
      - name: Marcel de Rooy
1255
      - name: Marcel de Rooy
1256
      - name: Martin Renvoize
1257
      - name: Nick Clemens
1256
      - name: Petro Vashchuk
1258
      - name: Petro Vashchuk
1259
      - name: Victor Grousset
1257
    te:
1260
    te:
1258
      - area: UI Design
1261
      - area: UI Design
1259
        name: Owen Leonard
1262
        name: Owen Leonard
Lines 1363-1371 team: Link Here
1363
    ktd:
1366
    ktd:
1364
      - name: Tomás Cohen Arazi
1367
      - name: Tomás Cohen Arazi
1365
    wiki:
1368
    wiki:
1366
      - name: Thomas Dukleth
1367
      - name: George Williams
1369
      - name: George Williams
1370
      - name: Katrin Fischer
1368
      - name: Mason James
1371
      - name: Mason James
1372
      - name: Thomas Dukleth
1369
    website:
1373
    website:
1370
      - name: Liz Rea
1374
      - name: Liz Rea
1371
    newsletter:
1375
    newsletter:
Lines 1390-1412 team: Link Here
1390
    qa_manager:
1394
    qa_manager:
1391
      name: Martin Renvoize
1395
      name: Martin Renvoize
1392
    qa:
1396
    qa:
1393
      - name: Marcel de Rooy
1394
      - name: Kyle M Hall
1395
      - name: Emily Lamancusa
1396
      - name: Nick Clemens
1397
      - name: Lucas Gass
1398
      - name: Tomás Cohen Arazi
1399
      - name: Julian Maurice
1400
      - name: Victor Grousset
1401
      - name: Aleisha Amohia
1397
      - name: Aleisha Amohia
1398
      - name: Chris Cormack
1402
      - name: David Cook
1399
      - name: David Cook
1403
      - name: Laura Escamilla
1400
      - name: Emily Lamancusa
1404
      - name: Jonathan Druart
1401
      - name: Jonathan Druart
1405
      - name: Pedro Amorim
1402
      - name: Julian Maurice
1403
      - name: Kyle M Hall
1404
      - name: Laura Escamilla
1405
      - name: Lucas Gass
1406
      - name: Marcel de Rooy
1406
      - name: Matt Blenkinsop
1407
      - name: Matt Blenkinsop
1407
      - name: Thomas Klausner
1408
      - name: Nick Clemens
1408
      - name: Paul Derscheid
1409
      - name: Paul Derscheid
1409
      - name: Chris Cormack
1410
      - name: Pedro Amorim
1411
      - name: Thomas Klausner
1412
      - name: Tomás Cohen Arazi
1413
      - name: Victor Grousset
1410
    te:
1414
    te:
1411
      - area: UI Design
1415
      - area: UI Design
1412
        name: Owen Leonard
1416
        name: Owen Leonard
Lines 1463-1495 team: Link Here
1463
      - version: 24.11
1467
      - version: 24.11
1464
        name: Paul Derscheid
1468
        name: Paul Derscheid
1465
      - version: 24.05
1469
      - version: 24.05
1466
        name: Wainui Witika-Park
1470
        name: Catalyst (Wainui Witika-Park, Alex Buckley, Aleisha Amoha)
1467
      - version: 23.11
1471
      - version: 23.11
1468
        name: Fridolin Somers
1472
        name: Fridolin Somers
1469
      - version: 22.11
1473
      - version: 22.11
1470
      - name: Laura Escamilla
1474
        name: Jesse Maseto
1471
    qa_manager:
1475
    qa_manager:
1472
      name: Martin Renvoize
1476
      name: Martin Renvoize
1473
    qa:
1477
    qa:
1474
      - name: Victor Grousset
1478
      - name: Aleisha Amohia
1475
      - name: Lisette Scheer
1479
      - name: Baptiste Wojtowski
1476
      - name: Emily Lamancusa
1480
      - name: Brendan Lawlor
1477
      - name: David Cook
1481
      - name: David Cook
1482
      - name: David Nind
1483
      - name: Emily Lamancusa
1478
      - name: Jonathan Druart
1484
      - name: Jonathan Druart
1479
      - name: Julian Maurice
1485
      - name: Julian Maurice
1480
      - name: Baptiste Wojtowski
1481
      - name: Paul Derscheid
1482
      - name: Aleisha Amohia
1483
      - name: Laura Escamilla
1484
      - name: Tomás Cohen Arazi
1485
      - name: Kyle M Hall
1486
      - name: Kyle M Hall
1486
      - name: Nick Clemens
1487
      - name: Laura Escamilla
1488
      - name: Lisette Scheer
1487
      - name: Lucas Gass
1489
      - name: Lucas Gass
1490
      - name: Magnus Enger
1488
      - name: Marcel de Rooy
1491
      - name: Marcel de Rooy
1492
      - name: Martin Renvoize
1489
      - name: Matt Blenkinsop
1493
      - name: Matt Blenkinsop
1494
      - name: Nick Clemens
1495
      - name: Owen Leonard
1496
      - name: Paul Derscheid
1490
      - name: Pedro Amorim
1497
      - name: Pedro Amorim
1491
      - name: Brendan Lawlor
1492
      - name: Thomas Klausner
1498
      - name: Thomas Klausner
1499
      - name: Tomás Cohen Arazi
1500
      - name: Victor Grousset
1501
      - name: Wainui Witika-Park
1493
    security_manager:
1502
    security_manager:
1494
      - name: Tomás Cohen Arazi
1503
      - name: Tomás Cohen Arazi
1495
    te:
1504
    te:
Lines 1508-1514 team: Link Here
1508
    documentation_team:
1517
    documentation_team:
1509
      - name: Aude Charillon
1518
      - name: Aude Charillon
1510
      - name: David Nind
1519
      - name: David Nind
1511
      - name: Caroline Cyr La Rose
1520
      - name: Caroline Cyr la Rose
1512
    translation:
1521
    translation:
1513
      name: Jonathan Druart
1522
      name: Jonathan Druart
1514
    meeting_facilitator:
1523
    meeting_facilitator:
Lines 1531-1533 team: Link Here
1531
      - name: Lauren Purton
1540
      - name: Lauren Purton
1532
    newsletter:
1541
    newsletter:
1533
      name: Michael Kuhn
1542
      name: Michael Kuhn
1543
    accessibility_advocate:
1544
      name: David Nind
1545
  25.11:
1546
    release_date: 1763855999
1547
    manager:
1548
      name: Lucas Gass
1549
    maintainer:
1550
      - version: 25.05
1551
        name: Paul Derscheid
1552
      - version: 24.11
1553
        name: Fridolin Somers
1554
      - version: 24.05
1555
        name: Jesse Maseto
1556
      - version: 22.11
1557
        name: "Catalyst IT (Wainui, Alex, Aleisha)"
1558
    maintainer_assistants:
1559
      - version: 25.05
1560
        name: Martin Renvoize
1561
      - version: 24.11
1562
        name: Baptiste Wojtkowski
1563
      - version: 24.05
1564
        name: Laura Escamilla
1565
    qa_manager:
1566
      name: Martin Renvoize
1567
    qa:
1568
      - name: Andrew Fuerste-Henry
1569
      - name: Andrii Nugged
1570
      - name: Baptiste Wojtkowski
1571
      - name: Brendan Lawlor
1572
      - name: David Cook
1573
      - name: Emily Lamancusa
1574
      - name: Jonathan Druart
1575
      - name: Julian Maurice
1576
      - name: Kyle Hall
1577
      - name: Laura Escamilla
1578
      - name: Lisette Scheer
1579
      - name: Marcel de Rooy
1580
      - name: Nick Clemens
1581
      - name: Paul Derscheid
1582
      - name: Petro V
1583
      - name: Tomás Cohen Arazi
1584
      - name: Victor Grousset
1585
    documentation:
1586
      name: David Nind
1587
    documentation_team:
1588
      - name: Aude Charillon
1589
      - name: Caroline Cyr La Rose
1590
      - name: Donna Bachowski
1591
      - name: Heather Hernandez
1592
      - name: Kristi Krueger
1593
      - name: Philip Orr
1594
    translation:
1595
      name: Jonathan Druart
1596
    website:
1597
      name: David Nind
1598
    wiki:
1599
      - name: George Williams
1600
      - name: Thomas Dukleth
1601
    social_media:
1602
      name: David Nind
1603
    newsletter:
1604
      name: Michael Kuhn
(-)a/etc/koha-conf.xml (-5 / +3 lines)
Lines 285-295 Link Here
285
     <max_processes>1</max_processes>
285
     <max_processes>1</max_processes>
286
 </background_jobs_worker>
286
 </background_jobs_worker>
287
287
288
 <do_not_remove_cookie>catalogue_editor_\d+</do_not_remove_cookie>
288
 <!-- Example lines. See Koha/CookieManager.pm for more details.
289
 <!-- Uncomment lines like hereunder to not clear cookies at logout:
289
     <do_not_remove_cookie>some_cookie</do_not_remove_cookie>
290
      The cookie name is case sensitive.
290
     <remove_cookie>another_cookie</remove_cookie>
291
      NOTE: You may use regex constructions like the example above.
292
     <do_not_remove_cookie>KohaOpacLanguage</do_not_remove_cookie>
293
 -->
291
 -->
294
292
295
 <message_domain_limits>
293
 <message_domain_limits>
(-)a/etc/zebradb/zebra-authorities-dom.cfg (-1 / +1 lines)
Lines 8-14 profilePath:__ZEBRA_CONF_DIR__/authorities/etc:__ZEBRA_CONF_DIR__/etc:__ZEBRA_CO Link Here
8
8
9
encoding: UTF-8
9
encoding: UTF-8
10
# modulePath - where to look for loadable zebra modules
10
# modulePath - where to look for loadable zebra modules
11
modulePath: /usr/lib/idzebra-2.0/modules:/usr/lib64/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules/
11
modulePath: /usr/lib64/idzebra-2.0/modules:/usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules
12
12
13
# Files that describe the attribute sets supported.
13
# Files that describe the attribute sets supported.
14
attset: bib1.att
14
attset: bib1.att
(-)a/etc/zebradb/zebra-biblios-dom.cfg (-1 / +1 lines)
Lines 5-11 Link Here
5
# Where are the config files located?
5
# Where are the config files located?
6
profilePath:__ZEBRA_CONF_DIR__/biblios/etc:__ZEBRA_CONF_DIR__/etc:__ZEBRA_CONF_DIR__/marc_defs/__ZEBRA_MARC_FORMAT__/biblios:__ZEBRA_CONF_DIR__/lang_defs/__ZEBRA_LANGUAGE__:__ZEBRA_CONF_DIR__/xsl
6
profilePath:__ZEBRA_CONF_DIR__/biblios/etc:__ZEBRA_CONF_DIR__/etc:__ZEBRA_CONF_DIR__/marc_defs/__ZEBRA_MARC_FORMAT__/biblios:__ZEBRA_CONF_DIR__/lang_defs/__ZEBRA_LANGUAGE__:__ZEBRA_CONF_DIR__/xsl
7
# modulePath - where to look for loadable zebra modules
7
# modulePath - where to look for loadable zebra modules
8
modulePath: /usr/lib/idzebra-2.0/modules:/usr/lib64/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules/
8
modulePath: /usr/lib64/idzebra-2.0/modules:/usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules
9
9
10
encoding: UTF-8
10
encoding: UTF-8
11
# Files that describe the attribute sets supported.
11
# Files that describe the attribute sets supported.
(-)a/ill/ill-requests.pl (-2 / +2 lines)
Lines 117-123 if ($backends_available) { Link Here
117
        $template->param(
117
        $template->param(
118
            notices => $notices,
118
            notices => $notices,
119
            request => $request,
119
            request => $request,
120
            ( $params->{tran_error}   ? ( tran_error   => $params->{tran_error} )   : () ),
120
            ( $params->{tran_fail}    ? ( tran_fail    => $params->{tran_fail} )    : () ),
121
            ( $params->{tran_success} ? ( tran_success => $params->{tran_success} ) : () ),
121
            ( $params->{tran_success} ? ( tran_success => $params->{tran_success} ) : () ),
122
        );
122
        );
123
123
Lines 478-484 if ($backends_available) { Link Here
478
            $append .= '&tran_success=' . join( ',', @{ $ret->{result}->{success} } );
478
            $append .= '&tran_success=' . join( ',', @{ $ret->{result}->{success} } );
479
        }
479
        }
480
        if ( $ret->{result} && scalar @{ $ret->{result}->{fail} } > 0 ) {
480
        if ( $ret->{result} && scalar @{ $ret->{result}->{fail} } > 0 ) {
481
            $append .= '&tran_fail=' . join( ',', @{ $ret->{result}->{fail} } . join(',') );
481
            $append .= '&tran_fail=' . join( ',', @{ $ret->{result}->{fail} } );
482
        }
482
        }
483
483
484
        # Redirect to view the whole request
484
        # Redirect to view the whole request
(-)a/installer/data/mysql/db_revs/250600001.pl (+20 lines)
Line 0 Link Here
1
use Modern::Perl;
2
use Koha::Installer::Output qw(say_warning say_success say_info);
3
4
return {
5
    bug_number  => "23010",
6
    description => "Add new PreventWithdrawingItemsStatus system preference",
7
    up          => sub {
8
        my ($args) = @_;
9
        my ( $dbh, $out ) = @$args{qw(dbh out)};
10
11
        $dbh->do(
12
            q{
13
            INSERT IGNORE INTO systempreferences (`variable`,`value`,`options`,`explanation`,`type`)
14
            VALUES ('PreventWithdrawingItemsStatus','','','Prevent the withdrawing of items based on statuses','multiple')
15
        }
16
        );
17
18
        say_success( $out, "Added new system preference 'PreventWithdrawingItemsStatus'" );
19
    },
20
};
(-)a/installer/data/mysql/db_revs/250600002.pl (+40 lines)
Line 0 Link Here
1
use Modern::Perl;
2
3
return {
4
    bug_number  => "34563",
5
    description => "Move StaffReportsHomeHTML to HTML customizations",
6
    up          => sub {
7
        my ($args) = @_;
8
        my ( $dbh, $out ) = @$args{qw(dbh out)};
9
10
        # Get any existing value from the IntranetReportsHomeHTML system preference
11
        my ($staffreportshome) = $dbh->selectrow_array(
12
            q|
13
            SELECT value FROM systempreferences WHERE variable='IntranetReportsHomeHTML';
14
        |
15
        );
16
        if ($staffreportshome) {
17
18
            $dbh->do(
19
                "INSERT INTO additional_contents ( category, code, location, branchcode, published_on ) VALUES ('html_customizations', 'StaffReportsHome', 'StaffReportsHome', NULL, CAST(NOW() AS date) )"
20
            );
21
22
            my ($insert_id) = $dbh->selectrow_array(
23
                "SELECT id FROM additional_contents WHERE category = 'html_customizations' AND code = 'StaffReportsHome' AND location = 'StaffReportsHome' LIMIT 1",
24
                {}
25
            );
26
27
            $dbh->do(
28
                "INSERT INTO additional_contents_localizations ( additional_content_id, title, content, lang ) VALUES ( ?, 'StaffReportsHome default', ?, 'default' )",
29
                undef, $insert_id, $staffreportshome
30
            );
31
32
            say $out "Added 'StaffReportsHome' HTML customization";
33
        }
34
35
        # Remove old system preference
36
        $dbh->do("DELETE FROM systempreferences WHERE variable='IntranetReportsHomeHTML'");
37
        say $out "Removed system preference 'IntranetReportsHomeHTML'";
38
39
    },
40
};
(-)a/installer/data/mysql/db_revs/250600003.pl (+32 lines)
Line 0 Link Here
1
use Modern::Perl;
2
use Koha::Installer::Output qw(say_warning say_success say_info);
3
4
return {
5
    bug_number  => "40337",
6
    description => "Define checkprevcheckout as ENUM",
7
    up          => sub {
8
        my ($args) = @_;
9
        my ( $dbh, $out ) = @$args{qw(dbh out)};
10
11
        $dbh->do(
12
            q{
13
            ALTER TABLE borrowers
14
            MODIFY COLUMN `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.'
15
        }
16
        );
17
18
        $dbh->do(
19
            q{
20
            ALTER TABLE categories
21
            MODIFY COLUMN `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron category if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to syspref setting if ''inherit''.'
22
        }
23
        );
24
25
        $dbh->do(
26
            q{
27
            ALTER TABLE deletedborrowers
28
            MODIFY COLUMN `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.'
29
        }
30
        );
31
    },
32
};
(-)a/installer/data/mysql/db_revs/250600004.pl (+22 lines)
Line 0 Link Here
1
use Modern::Perl;
2
use Koha::Installer::Output qw(say_warning say_success say_info);
3
4
return {
5
    bug_number  => "36947",
6
    description => "Add FacetSortingLocale system preference for locale-based facet sorting",
7
    up          => sub {
8
        my ($args) = @_;
9
        my ( $dbh, $out ) = @$args{qw(dbh out)};
10
11
        $dbh->do(
12
            q{
13
            INSERT IGNORE INTO systempreferences (`variable`,`value`,`options`,`explanation`,`type`)
14
            VALUES ('FacetSortingLocale','default','',
15
                'Choose the locale for sorting facet names when FacetOrder is set to Alphabetical. This enables proper Unicode-aware sorting of accented characters and locale-specific alphabetical ordering.',
16
                'Choice')
17
        }
18
        );
19
20
        say_success( $out, "Added new system preference 'FacetSortingLocale'" );
21
    },
22
};
(-)a/installer/data/mysql/en/optional/sample_news.yml (-6 / +6 lines)
Lines 80-91 tables: Link Here
80
          content:
80
          content:
81
            - "Now that you've installed Koha, what's next? Here are some suggestions:"
81
            - "Now that you've installed Koha, what's next? Here are some suggestions:"
82
            - "<ul>"
82
            - "<ul>"
83
            - "<li><a href=\"http://koha-community.org/documentation/\">Read Koha Documentation</a></li>"
83
            - "<li><a href=\"https://koha-community.org/documentation/\">Read Koha Documentation</a></li>"
84
            - "<li><a href=\"http://wiki.koha-community.org\">Read/Write to the Koha Wiki</a></li>"
84
            - "<li><a href=\"https://wiki.koha-community.org\">Read/Write to the Koha Wiki</a></li>"
85
            - "<li><a href=\"http://koha-community.org/support/\">Read and Contribute to Discussions</a></li>"
85
            - "<li><a href=\"https://koha-community.org/support/\">Read and Contribute to Discussions</a></li>"
86
            - "<li><a href=\"http://bugs.koha-community.org\">Report Koha Bugs</a></li>"
86
            - "<li><a href=\"https://bugs.koha-community.org\">Report Koha Bugs</a></li>"
87
            - "<li><a href=\"http://wiki.koha-community.org/wiki/Version_Control_Using_Git\">Submit Patches to Koha using Git (Version Control System)</a></li>"
87
            - "<li><a href=\"https://wiki.koha-community.org/wiki/Version_Control_Using_Git\">Submit Patches to Koha using Git (Version Control System)</a></li>"
88
            - "<li><a href=\"http://koha-community.org/support/\">Chat with Koha users and developers</a></li>"
88
            - "<li><a href=\"https://koha-community.org/support/\">Chat with Koha users and developers</a></li>"
89
            - "</ul>"
89
            - "</ul>"
90
            - ""
90
            - ""
91
          lang: "default"
91
          lang: "default"
(-)a/installer/data/mysql/kohastructure.sql (-3 / +16 lines)
Lines 1598-1604 CREATE TABLE `borrowers` ( Link Here
1598
  `privacy` int(11) NOT NULL DEFAULT 1 COMMENT 'patron/borrower''s privacy settings related to their checkout history',
1598
  `privacy` int(11) NOT NULL DEFAULT 1 COMMENT 'patron/borrower''s privacy settings related to their checkout history',
1599
  `privacy_guarantor_fines` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s fines',
1599
  `privacy_guarantor_fines` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s fines',
1600
  `privacy_guarantor_checkouts` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s checkouts',
1600
  `privacy_guarantor_checkouts` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s checkouts',
1601
  `checkprevcheckout` varchar(7) NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.',
1601
  `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.',
1602
  `updated_on` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT 'time of last change could be useful for synchronization with external systems (among others)',
1602
  `updated_on` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT 'time of last change could be useful for synchronization with external systems (among others)',
1603
  `lastseen` datetime DEFAULT NULL COMMENT 'last time a patron has been seen (connected at the OPAC or staff interface)',
1603
  `lastseen` datetime DEFAULT NULL COMMENT 'last time a patron has been seen (connected at the OPAC or staff interface)',
1604
  `lang` varchar(25) NOT NULL DEFAULT 'default' COMMENT 'lang to use to send notices to this patron',
1604
  `lang` varchar(25) NOT NULL DEFAULT 'default' COMMENT 'lang to use to send notices to this patron',
Lines 1808-1814 CREATE TABLE `categories` ( Link Here
1808
  `category_type` varchar(1) NOT NULL DEFAULT 'A' COMMENT 'type of Koha patron (Adult, Child, Professional, Organizational, Statistical, Staff)',
1808
  `category_type` varchar(1) NOT NULL DEFAULT 'A' COMMENT 'type of Koha patron (Adult, Child, Professional, Organizational, Statistical, Staff)',
1809
  `BlockExpiredPatronOpacActions` varchar(128) NOT NULL DEFAULT 'follow_syspref_BlockExpiredPatronOpacActions' COMMENT 'specific actions expired patrons of this category are blocked from performing or if the BlockExpiredPatronOpacActions system preference is to be followed',
1809
  `BlockExpiredPatronOpacActions` varchar(128) NOT NULL DEFAULT 'follow_syspref_BlockExpiredPatronOpacActions' COMMENT 'specific actions expired patrons of this category are blocked from performing or if the BlockExpiredPatronOpacActions system preference is to be followed',
1810
  `default_privacy` enum('default','never','forever') NOT NULL DEFAULT 'default' COMMENT 'Default privacy setting for this patron category',
1810
  `default_privacy` enum('default','never','forever') NOT NULL DEFAULT 'default' COMMENT 'Default privacy setting for this patron category',
1811
  `checkprevcheckout` varchar(7) NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron category if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to syspref setting if ''inherit''.',
1811
  `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron category if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to syspref setting if ''inherit''.',
1812
  `can_place_ill_in_opac` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'can this patron category place interlibrary loan requests',
1812
  `can_place_ill_in_opac` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'can this patron category place interlibrary loan requests',
1813
  `can_be_guarantee` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'if patrons of this category can be guarantees',
1813
  `can_be_guarantee` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'if patrons of this category can be guarantees',
1814
  `reset_password` tinyint(1) DEFAULT NULL COMMENT 'if patrons of this category can do the password reset flow,',
1814
  `reset_password` tinyint(1) DEFAULT NULL COMMENT 'if patrons of this category can do the password reset flow,',
Lines 2760-2766 CREATE TABLE `deletedborrowers` ( Link Here
2760
  `privacy` int(11) NOT NULL DEFAULT 1 COMMENT 'patron/borrower''s privacy settings related to their checkout history  KEY `borrowernumber` (`borrowernumber`),',
2760
  `privacy` int(11) NOT NULL DEFAULT 1 COMMENT 'patron/borrower''s privacy settings related to their checkout history  KEY `borrowernumber` (`borrowernumber`),',
2761
  `privacy_guarantor_fines` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s fines',
2761
  `privacy_guarantor_fines` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s fines',
2762
  `privacy_guarantor_checkouts` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s checkouts',
2762
  `privacy_guarantor_checkouts` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s checkouts',
2763
  `checkprevcheckout` varchar(7) NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.',
2763
  `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.',
2764
  `updated_on` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT 'time of last change could be useful for synchronization with external systems (among others)',
2764
  `updated_on` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT 'time of last change could be useful for synchronization with external systems (among others)',
2765
  `lastseen` datetime DEFAULT NULL COMMENT 'last time a patron has been seen (connected at the OPAC or staff interface)',
2765
  `lastseen` datetime DEFAULT NULL COMMENT 'last time a patron has been seen (connected at the OPAC or staff interface)',
2766
  `lang` varchar(25) NOT NULL DEFAULT 'default' COMMENT 'lang to use to send notices to this patron',
2766
  `lang` varchar(25) NOT NULL DEFAULT 'default' COMMENT 'lang to use to send notices to this patron',
Lines 5772-5777 CREATE TABLE `saved_reports` ( Link Here
5772
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
5772
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
5773
/*!40101 SET character_set_client = @saved_cs_client */;
5773
/*!40101 SET character_set_client = @saved_cs_client */;
5774
5774
5775
DROP TABLE IF EXISTS `reports_branches`;
5776
/*!40101 SET @saved_cs_client     = @@character_set_client */;
5777
/*!40101 SET character_set_client = utf8mb4 */;
5778
CREATE TABLE `reports_branches` (
5779
  `report_id` int(11) NOT NULL,
5780
  `branchcode` varchar(10) NOT NULL,
5781
  KEY `report_id` (`report_id`),
5782
  KEY `branchcode` (`branchcode`),
5783
  CONSTRAINT `reports_branches_ibfk_1` FOREIGN KEY (`report_id`) REFERENCES `saved_sql` (`id`) ON DELETE CASCADE,
5784
  CONSTRAINT `reports_branches_ibfk_2` FOREIGN KEY (`branchcode`) REFERENCES `branches` (`branchcode`) ON DELETE CASCADE
5785
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
5786
/*!40101 SET character_set_client = @saved_cs_client */;
5787
5775
--
5788
--
5776
-- Table structure for table `saved_sql`
5789
-- Table structure for table `saved_sql`
5777
--
5790
--
(-)a/installer/data/mysql/mandatory/sysprefs.sql (-2 / +3 lines)
Lines 278-283 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
278
('FacetLabelTruncationLength','20',NULL,'Specify the facet max length in OPAC','Integer'),
278
('FacetLabelTruncationLength','20',NULL,'Specify the facet max length in OPAC','Integer'),
279
('FacetMaxCount','20',NULL,'Specify the max facet count for each category','Integer'),
279
('FacetMaxCount','20',NULL,'Specify the max facet count for each category','Integer'),
280
('FacetOrder','Alphabetical','Alphabetical|Usage','Specify the order of facets within each category','Choice'),
280
('FacetOrder','Alphabetical','Alphabetical|Usage','Specify the order of facets within each category','Choice'),
281
('FacetSortingLocale','default','','Choose the locale for sorting facet names when FacetOrder is set to Alphabetical. This enables proper Unicode-aware sorting of accented characters and locale-specific alphabetical ordering.','Choice'),
281
('FailedLoginAttempts','','','Number of login attempts before lockout the patron account','Integer'),
282
('FailedLoginAttempts','','','Number of login attempts before lockout the patron account','Integer'),
282
('FallbackToSMSIfNoEmail', 0, 'Enable|Disable', 'Send messages by SMS if no patron email is defined', 'YesNo'),
283
('FallbackToSMSIfNoEmail', 0, 'Enable|Disable', 'Send messages by SMS if no patron email is defined', 'YesNo'),
283
('FeeOnChangePatronCategory','1','','If set, when a patron changes to a category with enrolment fee, a fee is charged','YesNo'),
284
('FeeOnChangePatronCategory','1','','If set, when a patron changes to a category with enrolment fee, a fee is charged','YesNo'),
Lines 357-363 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
357
('IntranetNumbersPreferPhrase','0',NULL,'Control the use of phr operator in callnumber and standard number staff interface searches','YesNo'),
358
('IntranetNumbersPreferPhrase','0',NULL,'Control the use of phr operator in callnumber and standard number staff interface searches','YesNo'),
358
('intranetreadinghistory','1','','If ON, Checkout history is enabled for all patrons','YesNo'),
359
('intranetreadinghistory','1','','If ON, Checkout history is enabled for all patrons','YesNo'),
359
('IntranetReadingHistoryHolds', 1, '', 'If ON, Holds history is enabled for all patrons','YesNo'),
360
('IntranetReadingHistoryHolds', 1, '', 'If ON, Holds history is enabled for all patrons','YesNo'),
360
('IntranetReportsHomeHTML', '', NULL, 'Show the following HTML in a div on the bottom of the reports home page', 'Free'),
361
('IntranetSlipPrinterJS','','','Use this JavaScript for printing slips. Define at least function printThenClose(). For use e.g. with Firefox PlugIn jsPrintSetup, see http://jsprintsetup.mozdev.org/','Free'),
361
('IntranetSlipPrinterJS','','','Use this JavaScript for printing slips. Define at least function printThenClose(). For use e.g. with Firefox PlugIn jsPrintSetup, see http://jsprintsetup.mozdev.org/','Free'),
362
('intranetstylesheet','','50','Enter a complete URL to use an alternate layout stylesheet in Intranet','free'),
362
('intranetstylesheet','','50','Enter a complete URL to use an alternate layout stylesheet in Intranet','free'),
363
('IntranetUserCSS','',NULL,'Add CSS to be included in the intranet in an embedded <style> tag.','free'),
363
('IntranetUserCSS','',NULL,'Add CSS to be included in the intranet in an embedded <style> tag.','free'),
Lines 507-513 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
507
('OPACFineNoRenewalsIncludeCredits','1',NULL,'If enabled the value specified in OPACFineNoRenewals should include any unapplied account credits in the calculation','YesNo'),
507
('OPACFineNoRenewalsIncludeCredits','1',NULL,'If enabled the value specified in OPACFineNoRenewals should include any unapplied account credits in the calculation','YesNo'),
508
('OPACFinesTab','1','','If OFF the patron fines tab in the OPAC is disabled.','YesNo'),
508
('OPACFinesTab','1','','If OFF the patron fines tab in the OPAC is disabled.','YesNo'),
509
('OPACFRBRizeEditions','0','','If ON, the OPAC will query one or more ISBN web services for associated ISBNs and display an Editions tab on the details pages','YesNo'),
509
('OPACFRBRizeEditions','0','','If ON, the OPAC will query one or more ISBN web services for associated ISBNs and display an Editions tab on the details pages','YesNo'),
510
('OpacHiddenItems','','','This syspref allows to define custom rules for hiding specific items at the OPAC. See http://wiki.koha-community.org/wiki/OpacHiddenItems for more information.','Textarea'),
510
('OpacHiddenItems','','','This syspref allows to define custom rules for hiding specific items at the OPAC. See https://wiki.koha-community.org/wiki/OpacHiddenItems for more information.','Textarea'),
511
('OpacHiddenItemsExceptions','',NULL,'List of borrower categories, separated by comma, that can see items otherwise hidden by OpacHiddenItems','Textarea'),
511
('OpacHiddenItemsExceptions','',NULL,'List of borrower categories, separated by comma, that can see items otherwise hidden by OpacHiddenItems','Textarea'),
512
('OpacHiddenItemsHidesRecord','1','','Hide biblio record when all its items are hidden because of OpacHiddenItems','YesNo'),
512
('OpacHiddenItemsHidesRecord','1','','Hide biblio record when all its items are hidden because of OpacHiddenItems','YesNo'),
513
('OpacHighlightedWords','1','','If Set, then queried words are higlighted in OPAC','YesNo'),
513
('OpacHighlightedWords','1','','If Set, then queried words are higlighted in OPAC','YesNo'),
Lines 645-650 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
645
('PreservationNotForLoanDefaultTrainIn', '', '', 'Not for loan to apply to items removed from the preservation waiting list', 'TextArea'),
645
('PreservationNotForLoanDefaultTrainIn', '', '', 'Not for loan to apply to items removed from the preservation waiting list', 'TextArea'),
646
('PreservationNotForLoanWaitingListIn', '', '', 'Not for loan to apply to items added to the preservation waiting list', 'TextArea'),
646
('PreservationNotForLoanWaitingListIn', '', '', 'Not for loan to apply to items added to the preservation waiting list', 'TextArea'),
647
('PreserveSerialNotes','1','','When a new "Expected" issue is generated, should it be prefilled with last created issue notes?','YesNo'),
647
('PreserveSerialNotes','1','','When a new "Expected" issue is generated, should it be prefilled with last created issue notes?','YesNo'),
648
('PreventWithdrawingItemsStatus', '', '', 'Prevent the withdrawing of items based on certain statuses' , 'multiple'),
648
('previousIssuesDefaultSortOrder','asc','asc|desc','Specify the sort order of Previous Issues on the circulation page','Choice'),
649
('previousIssuesDefaultSortOrder','asc','asc|desc','Specify the sort order of Previous Issues on the circulation page','Choice'),
649
('PrintNoticesMaxLines','0','','If greater than 0, sets the maximum number of lines an overdue notice will print. If the number of items is greater than this number, the notice will end with a warning asking the borrower to check their online account for a full list of overdue items.','Integer'),
650
('PrintNoticesMaxLines','0','','If greater than 0, sets the maximum number of lines an overdue notice will print. If the number of items is greater than this number, the notice will end with a warning asking the borrower to check their online account for a full list of overdue items.','Integer'),
650
('PrivacyPolicyConsent','','Enforced|Permissive|Disabled','Data privacy policy consent in the OPAC', 'Choice'),
651
('PrivacyPolicyConsent','','Enforced|Permissive|Disabled','Data privacy policy consent in the OPAC', 'Choice'),
(-)a/installer/data/mysql/updatedatabase.pl (+16 lines)
Lines 29455-29460 if ( CheckVersion($DBversion) ) { Link Here
29455
    NewVersion( $DBversion, 28108, "Add new systempreference OpacHiddenItemsHidesRecord" );
29455
    NewVersion( $DBversion, 28108, "Add new systempreference OpacHiddenItemsHidesRecord" );
29456
}
29456
}
29457
29457
29458
$DBversion = '25.06.00.003';
29459
if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) {
29460
    $dbh->do(
29461
        "CREATE TABLE `reports_branches` (
29462
            `report_id` int(11) NOT NULL,
29463
            `branchcode` varchar(10) NOT NULL,
29464
            KEY `report_id` (`report_id`),
29465
            KEY `branchcode` (`branchcode`),
29466
            CONSTRAINT `reports_branches_ibfk_1` FOREIGN KEY (`report_id`) REFERENCES `saved_sql` (`id`) ON DELETE CASCADE,
29467
            CONSTRAINT `reports_branches_ibfk_2` FOREIGN KEY (`branchcode`) REFERENCES `branches` (`branchcode`) ON DELETE CASCADE
29468
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"
29469
    );
29470
    print "Upgrade to $DBversion done (reports_branches added)\n";
29471
    SetVersion($DBversion);
29472
}
29473
29458
$DBversion = '21.05.00.000';
29474
$DBversion = '21.05.00.000';
29459
if ( CheckVersion($DBversion) ) {
29475
if ( CheckVersion($DBversion) ) {
29460
    NewVersion( $DBversion, "", "Koha 21.05.00 release" );
29476
    NewVersion( $DBversion, "", "Koha 21.05.00 release" );
(-)a/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.js (-1 / +1 lines)
Lines 59-65 function diffString( o, n ) { Link Here
59
        for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) {
59
        for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) {
60
          pre += '<del>' + escape(out.o[n]) + oSpace[n] + "</del>";
60
          pre += '<del>' + escape(out.o[n]) + oSpace[n] + "</del>";
61
        }
61
        }
62
        str += " " + out.n[i].text + nSpace[i] + pre;
62
        str += " " + escape(out.n[i].text) + nSpace[i] + pre;
63
      }
63
      }
64
    }
64
    }
65
  }
65
  }
(-)a/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.min.js (-1 / +1 lines)
Lines 7-10 Link Here
7
 *
7
 *
8
 * More Info:
8
 * More Info:
9
 *  http://ejohn.org/projects/javascript-diff-algorithm/
9
 *  http://ejohn.org/projects/javascript-diff-algorithm/
10
 */function escape(a){var b=a;return b=b.replace(/&/g,"&amp;"),b=b.replace(/</g,"&lt;"),b=b.replace(/>/g,"&gt;"),b=b.replace(/"/g,"&quot;"),b}function diffString(a,b){a=a.replace(/\s+$/,""),b=b.replace(/\s+$/,"");var c=diff(a==""?[]:a.split(/\s+/),b==""?[]:b.split(/\s+/)),d="",e=a.match(/\s+/g);e==null?e=["\n"]:e.push("\n");var f=b.match(/\s+/g);f==null?f=["\n"]:f.push("\n");if(c.n.length==0)for(var g=0;g<c.o.length;g++)d+="<del>"+escape(c.o[g])+e[g]+"</del>";else{if(c.n[0].text==null)for(b=0;b<c.o.length&&c.o[b].text==null;b++)d+="<del>"+escape(c.o[b])+e[b]+"</del>";for(var g=0;g<c.n.length;g++)if(c.n[g].text==null)d+="<ins>"+escape(c.n[g])+f[g]+"</ins>";else{var h="";for(b=c.n[g].row+1;b<c.o.length&&c.o[b].text==null;b++)h+="<del>"+escape(c.o[b])+e[b]+"</del>";d+=" "+c.n[g].text+f[g]+h}}return d}function randomColor(){return"rgb("+Math.random()*100+"%, "+Math.random()*100+"%, "+Math.random()*100+"%)"}function diffString2(a,b){a=a.replace(/\s+$/,""),b=b.replace(/\s+$/,"");var c=diff(a==""?[]:a.split(/\s+/),b==""?[]:b.split(/\s+/)),d=a.match(/\s+/g);d==null?d=["\n"]:d.push("\n");var e=b.match(/\s+/g);e==null?e=["\n"]:e.push("\n");var f="",g=new Array;for(var h=0;h<c.o.length;h++)g[h]=randomColor(),c.o[h].text!=null?f+='<span style="background-color: '+g[h]+'">'+escape(c.o[h].text)+d[h]+"</span>":f+="<del>"+escape(c.o[h])+d[h]+"</del>";var i="";for(var h=0;h<c.n.length;h++)c.n[h].text!=null?i+='<span style="background-color: '+g[c.n[h].row]+'">'+escape(c.n[h].text)+e[h]+"</span>":i+="<ins>"+escape(c.n[h])+e[h]+"</ins>";return{o:f,n:i}}function diff(a,b){var c=new Object,d=new Object;for(var e=0;e<b.length;e++)c[b[e]]==null&&(c[b[e]]={rows:new Array,o:null}),c[b[e]].rows.push(e);for(var e=0;e<a.length;e++)d[a[e]]==null&&(d[a[e]]={rows:new Array,n:null}),d[a[e]].rows.push(e);for(var e in c)c[e].rows.length==1&&typeof d[e]!="undefined"&&d[e].rows.length==1&&(b[c[e].rows[0]]={text:b[c[e].rows[0]],row:d[e].rows[0]},a[d[e].rows[0]]={text:a[d[e].rows[0]],row:c[e].rows[0]});for(var e=0;e<b.length-1;e++)b[e].text!=null&&b[e+1].text==null&&b[e].row+1<a.length&&a[b[e].row+1].text==null&&b[e+1]==a[b[e].row+1]&&(b[e+1]={text:b[e+1],row:b[e].row+1},a[b[e].row+1]={text:a[b[e].row+1],row:e+1});for(var e=b.length-1;e>0;e--)b[e].text!=null&&b[e-1].text==null&&b[e].row>0&&a[b[e].row-1].text==null&&b[e-1]==a[b[e].row-1]&&(b[e-1]={text:b[e-1],row:b[e].row-1},a[b[e].row-1]={text:a[b[e].row-1],row:e-1});return{o:a,n:b}};
10
 */function escape(a){var b=a;return b=b.replace(/&/g,"&amp;"),b=b.replace(/</g,"&lt;"),b=b.replace(/>/g,"&gt;"),b=b.replace(/"/g,"&quot;"),b}function diffString(a,b){a=a.replace(/\s+$/,""),b=b.replace(/\s+$/,"");var c=diff(a==""?[]:a.split(/\s+/),b==""?[]:b.split(/\s+/)),d="",e=a.match(/\s+/g);e==null?e=["\n"]:e.push("\n");var f=b.match(/\s+/g);f==null?f=["\n"]:f.push("\n");if(c.n.length==0)for(var g=0;g<c.o.length;g++)d+="<del>"+escape(c.o[g])+e[g]+"</del>";else{if(c.n[0].text==null)for(b=0;b<c.o.length&&c.o[b].text==null;b++)d+="<del>"+escape(c.o[b])+e[b]+"</del>";for(var g=0;g<c.n.length;g++)if(c.n[g].text==null)d+="<ins>"+escape(c.n[g])+f[g]+"</ins>";else{var h="";for(b=c.n[g].row+1;b<c.o.length&&c.o[b].text==null;b++)h+="<del>"+escape(c.o[b])+e[b]+"</del>";d+=" "+escape(c.n[g].text)+f[g]+h}}return d}function randomColor(){return"rgb("+Math.random()*100+"%, "+Math.random()*100+"%, "+Math.random()*100+"%)"}function diffString2(a,b){a=a.replace(/\s+$/,""),b=b.replace(/\s+$/,"");var c=diff(a==""?[]:a.split(/\s+/),b==""?[]:b.split(/\s+/)),d=a.match(/\s+/g);d==null?d=["\n"]:d.push("\n");var e=b.match(/\s+/g);e==null?e=["\n"]:e.push("\n");var f="",g=new Array;for(var h=0;h<c.o.length;h++)g[h]=randomColor(),c.o[h].text!=null?f+='<span style="background-color: '+g[h]+'">'+escape(c.o[h].text)+d[h]+"</span>":f+="<del>"+escape(c.o[h])+d[h]+"</del>";var i="";for(var h=0;h<c.n.length;h++)c.n[h].text!=null?i+='<span style="background-color: '+g[c.n[h].row]+'">'+escape(c.n[h].text)+e[h]+"</span>":i+="<ins>"+escape(c.n[h])+e[h]+"</ins>";return{o:f,n:i}}function diff(a,b){var c=new Object,d=new Object;for(var e=0;e<b.length;e++)c[b[e]]==null&&(c[b[e]]={rows:new Array,o:null}),c[b[e]].rows.push(e);for(var e=0;e<a.length;e++)d[a[e]]==null&&(d[a[e]]={rows:new Array,n:null}),d[a[e]].rows.push(e);for(var e in c)c[e].rows.length==1&&typeof d[e]!="undefined"&&d[e].rows.length==1&&(b[c[e].rows[0]]={text:b[c[e].rows[0]],row:d[e].rows[0]},a[d[e].rows[0]]={text:a[d[e].rows[0]],row:c[e].rows[0]});for(var e=0;e<b.length-1;e++)b[e].text!=null&&b[e+1].text==null&&b[e].row+1<a.length&&a[b[e].row+1].text==null&&b[e+1]==a[b[e].row+1]&&(b[e+1]={text:b[e+1],row:b[e].row+1},a[b[e].row+1]={text:a[b[e].row+1],row:e+1});for(var e=b.length-1;e>0;e--)b[e].text!=null&&b[e-1].text==null&&b[e].row>0&&a[b[e].row-1].text==null&&b[e-1]==a[b[e].row-1]&&(b[e-1]={text:b[e-1],row:b[e].row-1},a[b[e].row-1]={text:a[b[e].row-1],row:e-1});return{o:a,n:b}};
(-)a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss (+10 lines)
Lines 2192-2197 li { Link Here
2192
td {
2192
td {
2193
    &.actions {
2193
    &.actions {
2194
        white-space: nowrap;
2194
        white-space: nowrap;
2195
2196
        a,
2197
        button {
2198
            display: inline-block;
2199
            margin-left: .3rem;
2200
2201
            &:first-child {
2202
                margin-left: 0;
2203
            }
2204
        }
2195
    }
2205
    }
2196
2206
2197
    &.bookcoverimg {
2207
    &.bookcoverimg {
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/about-team.inc (-1 / +70 lines)
Lines 8-13 Link Here
8
        <span>Release manager</span>
8
        <span>Release manager</span>
9
    [%- CASE 'manager_assistant' -%]
9
    [%- CASE 'manager_assistant' -%]
10
        <span>Release manager assistant</span>
10
        <span>Release manager assistant</span>
11
    [%- CASE 'manager_assistants' -%]
12
        <span>Release manager assistants</span>
11
    [%- CASE 'manager_mentor' -%]
13
    [%- CASE 'manager_mentor' -%]
12
        <span>Release manager mentor</span>
14
        <span>Release manager mentor</span>
13
    [%- CASE 'qa_manager' -%]
15
    [%- CASE 'qa_manager' -%]
Lines 32-39 Link Here
32
        <span>Release maintainer</span>
34
        <span>Release maintainer</span>
33
    [%- CASE 'maintainer_assistant' -%]
35
    [%- CASE 'maintainer_assistant' -%]
34
        <span>Release maintainer assistant</span>
36
        <span>Release maintainer assistant</span>
37
    [%- CASE 'maintainer_assistants' -%]
38
        <span>Release maintainer assistants</span>
35
    [%- CASE 'maintainer_mentor' -%]
39
    [%- CASE 'maintainer_mentor' -%]
36
        <span>Release maintainer mentor</span>
40
        <span>Release maintainer mentor</span>
41
    [%- CASE 'maintainer_mentors' -%]
42
        <span>Release maintainer mentors</span>
37
    [%- CASE 'wiki' -%]
43
    [%- CASE 'wiki' -%]
38
        <span>Wiki curator</span>
44
        <span>Wiki curator</span>
39
    [%- CASE 'ci' -%]
45
    [%- CASE 'ci' -%]
Lines 56-67 Link Here
56
        <span>Live CD maintainer</span>
62
        <span>Live CD maintainer</span>
57
    [%- CASE 'accessibility_advocate' -%]
63
    [%- CASE 'accessibility_advocate' -%]
58
        <span>Accessibility advocate</span>
64
        <span>Accessibility advocate</span>
65
    [%- CASE 'accessibility_advocates' -%]
66
        <span>Accessibility advocates</span>
59
    [%- CASE 'meeting_facilitator' -%]
67
    [%- CASE 'meeting_facilitator' -%]
60
        <span>Meeting facilitator</span>
68
        <span>Meeting facilitator</span>
61
    [%- CASE 'ktd' -%]
69
    [%- CASE 'ktd' -%]
62
        <span>Developer tooling</span>
70
        <span>Developer tooling</span>
63
    [%- CASE 'website' -%]
71
    [%- CASE 'website' -%]
64
        <span>Website administrator</span>
72
        <span>Website administrator</span>
73
    [%- CASE 'social_media' -%]
74
        <span>Social media manager</span>
65
    [%- CASE 'security_manager' -%]
75
    [%- CASE 'security_manager' -%]
66
        <span>Security release manager</span>
76
        <span>Security release manager</span>
67
    [%- CASE -%]
77
    [%- CASE -%]
Lines 77-83 Link Here
77
[%- BLOCK contributions -%]
87
[%- BLOCK contributions -%]
78
    [%- IF p.roles || p.notes -%]
88
    [%- IF p.roles || p.notes -%]
79
        <ul>
89
        <ul>
80
            [% FOREACH r IN p.roles %]<li>[% INCLUDE role role=r %] ([% r.value.join(', ') | html %])</li>[% END %]
90
            [% FOREACH r IN p.roles %]
91
                [% SET sorted_versions = r.value.sort.reverse %]
92
                <li>
93
                    [% INCLUDE role role=r %]
94
                    ([% IF sorted_versions.size <= 2 %]
95
                        [% sorted_versions.join(', ') | html %]
96
                    [% ELSE %]
97
                        [% sorted_versions.slice(0,1).join(', ') | html %]<span class="version-ellipsis" data-bs-toggle="tooltip" title="[% sorted_versions.slice(2, -1).join(', ') | html %]">, ...</span>
98
                    [% END %])
99
                </li>
100
            [% END %]
81
            [% IF p.notes %]<li>[% p.notes | html %]</li>[% END %]
101
            [% IF p.notes %]<li>[% p.notes | html %]</li>[% END %]
82
        </ul>
102
        </ul>
83
    [%- END -%]
103
    [%- END -%]
Lines 329-334 Link Here
329
            </li>
349
            </li>
330
        [% END %]
350
        [% END %]
331
351
352
        [% IF t.social_media %]
353
            <li
354
                ><strong>Social media manager:</strong>
355
                [% INCLUDE person p=t.social_media %]
356
            </li>
357
        [% END %]
358
332
        [% IF t.wiki %]
359
        [% IF t.wiki %]
333
            [% IF t.wiki.size == 1 %]
360
            [% IF t.wiki.size == 1 %]
334
                <li
361
                <li
Lines 347-352 Link Here
347
            [% END %]
374
            [% END %]
348
        [% END %]
375
        [% END %]
349
376
377
        [% IF t.meeting_facilitator %]
378
            <li
379
                ><strong>Meeting facilitator:</strong>
380
                [% INCLUDE person p=t.meeting_facilitator %]
381
            </li>
382
        [% END %]
383
384
        [% IF t.chairperson %]
385
            <li
386
                ><strong>Meetings chairperson:</strong>
387
                [% INCLUDE person p=t.chairperson %]
388
            </li>
389
        [% END %]
390
391
        [% IF t.database %]
392
            <li
393
                ><strong>Documentation specialist:</strong>
394
                [% INCLUDE person p=t.database %]
395
            </li>
396
        [% END %]
397
398
        [% IF t.live_cd %]
399
            <li
400
                ><strong>Live CD maintainer:</strong>
401
                [% INCLUDE person p=t.live_cd %]
402
            </li>
403
        [% END %]
404
405
        [% IF t.vm %]
406
            <li
407
                ><strong>Virtual machine maintainer:</strong>
408
                [% INCLUDE person p=t.vm %]
409
            </li>
410
        [% END %]
411
412
        [% IF t.translation_assistant %]
413
            <li
414
                ><strong>Translation manager assistant:</strong>
415
                [% INCLUDE person p=t.translation_assistant %]
416
            </li>
417
        [% END %]
418
350
        [% IF v != 'release' %]
419
        [% IF v != 'release' %]
351
            <li
420
            <li
352
                ><strong>Release manager:</strong>
421
                ><strong>Release manager:</strong>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_modification.inc (+7 lines)
Lines 13-18 Link Here
13
            [% IF job.status == 'cancelled' %]<span>The job has been cancelled before it finished.</span>[% END %]
13
            [% IF job.status == 'cancelled' %]<span>The job has been cancelled before it finished.</span>[% END %]
14
            <a href="/cgi-bin/koha/tools/batchMod.pl" title="New batch item modification">New batch item modification</a>
14
            <a href="/cgi-bin/koha/tools/batchMod.pl" title="New batch item modification">New batch item modification</a>
15
        </div>
15
        </div>
16
        <div>
17
            [% IF report.errors.size %]
18
                <div class="alert alert-warning">
19
                    <span>[% report.errors.size | html %] item(s) could not be modified.</span>
20
                </div>
21
            [% END %]
22
        </div>
16
    [% END %]
23
    [% END %]
17
[% END %]
24
[% END %]
18
25
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc (-2 / +2 lines)
Lines 17-21 Link Here
17
"[% AuthorisedValues.GetDescriptionByKohaField(frameworkcode => biblio.frameworkcode, kohafield => 'items.notforloan', authorised_value => item.notforloan) | replace('"', '""') | $raw %]" [%- delimiter | $raw -%]
17
"[% AuthorisedValues.GetDescriptionByKohaField(frameworkcode => biblio.frameworkcode, kohafield => 'items.notforloan', authorised_value => item.notforloan) | replace('"', '""') | $raw %]" [%- delimiter | $raw -%]
18
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.itemlost', authorised_value => item.itemlost ) || "" | replace('"', '""') | $raw %]" [%- delimiter | $raw -%]
18
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.itemlost', authorised_value => item.itemlost ) || "" | replace('"', '""') | $raw %]" [%- delimiter | $raw -%]
19
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.withdrawn', authorised_value => item.withdrawn ) || "" | replace('"', '""') | $raw %]" [%- delimiter | $raw -%]
19
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.withdrawn', authorised_value => item.withdrawn ) || "" | replace('"', '""') | $raw %]" [%- delimiter | $raw -%]
20
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.damaged', authorised_value => item.damaged ) || "" | replace('"', '""') | $raw %]" [%- delimiter | $raw -%] "[% (item.issues || 0) | $raw %]" [%- delimiter | $raw -%]
20
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.damaged', authorised_value => item.damaged ) || "" | replace('"', '""') | $raw %]" [%- delimiter | $raw -%] "[% item.dateaccessioned | $KohaDates | $raw %]"
21
"[% item.datelastborrowed | $KohaDates | $raw %]" [%- delimiter | $raw -%] "[% IF item.checkout %][% item.checkout.date_due | $KohaDates | $raw %][% END %]"
21
[%- delimiter | $raw -%] "[% (item.issues || 0) | $raw %]" [%- delimiter | $raw -%] "[% item.datelastborrowed | $KohaDates | $raw %]" [%- delimiter | $raw -%] "[% IF item.checkout %][% item.checkout.date_due | $KohaDates | $raw %][% END %]"
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.json.inc (-2 / +2 lines)
Lines 24-31 Link Here
24
"[% item.stocknumber | html | $To %]", "[% AuthorisedValues.GetDescriptionByKohaField( frameworkcode => biblio.frameworkcode, kohafield => 'items.notforloan', authorised_value => item.notforloan) | html %]",
24
"[% item.stocknumber | html | $To %]", "[% AuthorisedValues.GetDescriptionByKohaField( frameworkcode => biblio.frameworkcode, kohafield => 'items.notforloan', authorised_value => item.notforloan) | html %]",
25
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.itemlost', authorised_value => item.itemlost ) || "" | html %]",
25
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.itemlost', authorised_value => item.itemlost ) || "" | html %]",
26
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.withdrawn', authorised_value => item.withdrawn ) || "" | html %]",
26
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.withdrawn', authorised_value => item.withdrawn ) || "" | html %]",
27
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.damaged', authorised_value => item.damaged ) || "" | html %]", "[% (item.issues || 0) | html %]", "[% item.datelastborrowed | $KohaDates %]",
27
"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.damaged', authorised_value => item.damaged ) || "" | html %]", "[% item.dateaccessioned | $KohaDates %]", "[% (item.issues || 0) | html %]",
28
"[% IF item.checkout %][% item.checkout.date_due | $KohaDates %][% END %]",
28
"[% item.datelastborrowed | $KohaDates %]", "[% IF item.checkout %][% item.checkout.date_due | $KohaDates %][% END %]",
29
"[% FILTER escape_quotes ~%]
29
"[% FILTER escape_quotes ~%]
30
    <div class="btn-group dropup"
30
    <div class="btn-group dropup"
31
        ><button type="button" class="btn btn-xs btn-default dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <i class="fa-solid fa-pencil" aria-hidden="true"></i> Edit </button>
31
        ><button type="button" class="btn btn-xs btn-default dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <i class="fa-solid fa-pencil" aria-hidden="true"></i> Edit </button>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/csv_headers/catalogue/itemsearch.tt (-1 / +1 lines)
Lines 3-7 Link Here
3
[%- PROCESS 'i18n.inc' -%]
3
[%- PROCESS 'i18n.inc' -%]
4
[%- SET delimiter = Koha.CSVDelimiter() -%]
4
[%- SET delimiter = Koha.CSVDelimiter() -%]
5
[%- BLOCK -%]
5
[%- BLOCK -%]
6
    "[% t("Title") | html %]"[%- delimiter | $raw -%]"[% t("Publication date") | html %]"[%- delimiter | $raw -%]"[% t("Publisher") | html %]"[%- delimiter | $raw -%]"[% t("Collection") | html %]"[%- delimiter | $raw -%]"[% t("Barcode") | html %]"[%- delimiter | $raw -%]"[% t("Item number") | html %]"[%- delimiter | $raw -%]"[% t("Serial enumeration") | html %]"[%- delimiter | $raw -%]"[% t("Call number") | html %]"[%- delimiter | $raw -%]"[% t("Home library") | html %]"[%- delimiter | $raw -%]"[% t("Current library") | html %]"[%- delimiter | $raw -%]"[% t("Shelving location") | html %]"[%- delimiter | $raw -%]"[% t("Item type") | html %]"[%- delimiter | $raw -%]"[% t("Inventory number") | html %]"[%- delimiter | $raw -%]"[% t("Not for loan status") | html %]"[%- delimiter | $raw -%]"[% t("Lost status") | html %]"[%- delimiter | $raw -%]"[% t("Withdrawn status") | html %]"[%- delimiter | $raw -%]"[% t("Damaged status") | html %]"[%- delimiter | $raw -%]"[% t("Checkouts") | html %]"[%- delimiter | $raw -%]"[% t("Last checkout date") | html %]"[%- delimiter | $raw -%]"[% t("Due date") | html %]"
6
    "[% t("Title") | html %]"[%- delimiter | $raw -%]"[% t("Publication date") | html %]"[%- delimiter | $raw -%]"[% t("Publisher") | html %]"[%- delimiter | $raw -%]"[% t("Collection") | html %]"[%- delimiter | $raw -%]"[% t("Barcode") | html %]"[%- delimiter | $raw -%]"[% t("Item number") | html %]"[%- delimiter | $raw -%]"[% t("Serial enumeration") | html %]"[%- delimiter | $raw -%]"[% t("Call number") | html %]"[%- delimiter | $raw -%]"[% t("Home library") | html %]"[%- delimiter | $raw -%]"[% t("Current library") | html %]"[%- delimiter | $raw -%]"[% t("Shelving location") | html %]"[%- delimiter | $raw -%]"[% t("Item type") | html %]"[%- delimiter | $raw -%]"[% t("Inventory number") | html %]"[%- delimiter | $raw -%]"[% t("Not for loan status") | html %]"[%- delimiter | $raw -%]"[% t("Lost status") | html %]"[%- delimiter | $raw -%]"[% t("Withdrawn status") | html %]"[%- delimiter | $raw -%]"[% t("Damaged status") | html %]"[% t("Date accessioned") | html %]"[%- delimiter | $raw -%]"[%- delimiter | $raw -%]"[% t("Checkouts") | html %]"[%- delimiter | $raw -%]"[% t("Last checkout date") | html %]"[%- delimiter | $raw -%]"[% t("Due date") | html %]"
7
[%- END -%]
7
[%- END -%]
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/guided-reports-view.inc (-3 / +3 lines)
Lines 10-21 Link Here
10
    </ul>
10
    </ul>
11
    <h5>Useful resources</h5>
11
    <h5>Useful resources</h5>
12
    <ul>
12
    <ul>
13
        <li><a href="http://wiki.koha-community.org/wiki/SQL_Reports_Library" target="blank">Koha report library</a></li>
13
        <li><a href="https://wiki.koha-community.org/wiki/SQL_Reports_Library" target="blank">Koha report library</a></li>
14
        [% SET koha_version = Koha.Version %]
14
        [% SET koha_version = Koha.Version %]
15
        [% IF koha_version.development %]
15
        [% IF koha_version.development %]
16
            <li><a href="http://schema.koha-community.org/main" target="blank">Koha database schema</a></li>
16
            <li><a href="https://schema.koha-community.org/main" target="blank">Koha database schema</a></li>
17
        [% ELSE %]
17
        [% ELSE %]
18
            <li><a href="http://schema.koha-community.org/[% koha_version.major | uri %]_[% koha_version.minor | uri %]" target="blank">Koha database schema</a></li>
18
            <li><a href="https://schema.koha-community.org/[% koha_version.major | uri %]_[% koha_version.minor | uri %]" target="blank">Koha database schema</a></li>
19
        [% END %]
19
        [% END %]
20
    </ul>
20
    </ul>
21
</div>
21
</div>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/header.inc (-8 / +10 lines)
Lines 104-117 Link Here
104
                            </span>
104
                            </span>
105
                            <span id="logged-in-info-full">
105
                            <span id="logged-in-info-full">
106
                                [% SET is_superlibrarian = CAN_user_superlibrarian ? 'is_superlibrarian' : '' %]
106
                                [% SET is_superlibrarian = CAN_user_superlibrarian ? 'is_superlibrarian' : '' %]
107
                                <span class="loggedinusername [% is_superlibrarian | html %]">[% logged_in_user.userid | html %]</span>
107
                                <span class="loggedinusername [% is_superlibrarian | html %]" data-loggedinusername="[% logged_in_user.userid | html %]" data-is-superlibrarian="[% is_superlibrarian | html %]"
108
                                <span class="loggedincategorycode content_hidden">[% logged_in_user.categorycode | html %]</span>
108
                                    >[% logged_in_user.userid | html %]</span
109
                                >
110
                                <span class="loggedincategorycode content_hidden" data-loggedincategorycode="[% logged_in_user.categorycode | html %]">[% logged_in_user.categorycode | html %]</span>
109
                                [% IF ( StaffLoginRestrictLibraryByIP ) %]
111
                                [% IF ( StaffLoginRestrictLibraryByIP ) %]
110
                                    <brand> [% Branches.GetLoggedInBranchname | html %] </brand>
112
                                    <brand> [% Branches.GetLoggedInBranchname | html %] </brand>
111
                                [% ELSE %]
113
                                [% ELSE %]
112
                                    <strong>
114
                                    <strong>
113
                                        <span class="logged-in-branch-name">[% Branches.GetLoggedInBranchname | html %]</span>
115
                                        <span class="logged-in-branch-name" data-logged-in-branch-name="[% Branches.GetLoggedInBranchname | html %]">[% Branches.GetLoggedInBranchname | html %]</span>
114
                                        <span class="logged-in-branch-code content_hidden">[% Branches.GetLoggedInBranchcode | html %]</span>
116
                                        <span class="logged-in-branch-code content_hidden" data-logged-in-branch-code="[% Branches.GetLoggedInBranchcode | html %]">[% Branches.GetLoggedInBranchcode | html %]</span>
115
                                    </strong>
117
                                    </strong>
116
                                [% END %]
118
                                [% END %]
117
                                [% IF Koha.Preference('UseCirculationDesks') && Desks.ListForLibrary.count %]
119
                                [% IF Koha.Preference('UseCirculationDesks') && Desks.ListForLibrary.count %]
Lines 120-135 Link Here
120
                                        [% IF ( Desks.GetLoggedInDeskName == '' ) %]
122
                                        [% IF ( Desks.GetLoggedInDeskName == '' ) %]
121
                                            <span class="logged-in-desk-name">NO DESK SET</span>
123
                                            <span class="logged-in-desk-name">NO DESK SET</span>
122
                                        [% ELSE %]
124
                                        [% ELSE %]
123
                                            <span class="logged-in-desk-name">[% Desks.GetLoggedInDeskName | html %]</span>
125
                                            <span class="logged-in-desk-name" data-logged-in-desk-name="[% Desks.GetLoggedInDeskName | html %]">[% Desks.GetLoggedInDeskName | html %]</span>
124
                                            <span class="logged-in-desk-id content_hidden">[% Desks.GetLoggedInDeskId | html %]</span>
126
                                            <span class="logged-in-desk-id content_hidden" data-logged-in-desk-id="[% Desks.GetLoggedInDeskId | html %]">[% Desks.GetLoggedInDeskId | html %]</span>
125
                                        [% END %]
127
                                        [% END %]
126
                                    </strong>
128
                                    </strong>
127
                                [% END %]
129
                                [% END %]
128
                                [% IF Koha.Preference('UseCashRegisters') && !(Registers.session_register_name == '') %]
130
                                [% IF Koha.Preference('UseCashRegisters') && !(Registers.session_register_name == '') %]
129
                                    <span class="separator">|</span>
131
                                    <span class="separator">|</span>
130
                                    <strong>
132
                                    <strong>
131
                                        <span class="logged-in-register-name">[% Registers.session_register_name | html %]</span>
133
                                        <span class="logged-in-register-name" data-logged-in-register-name="[% Registers.session_register_name | html %]">[% Registers.session_register_name | html %]</span>
132
                                        <span class="logged-in-register-id content_hidden">[% Registers.session_register_id | html %]</span>
134
                                        <span class="logged-in-register-id content_hidden" data-logged-in-register-id="[% Registers.session_register_id | html %]">[% Registers.session_register_id | html %]</span>
133
                                    </strong>
135
                                    </strong>
134
                                [% END %]
136
                                [% END %]
135
                            </span>
137
                            </span>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/html-customization-help.inc (+2 lines)
Lines 16-21 Link Here
16
16
17
<div id="IntranetmainUserblock_notes" class="hint customization_note"> Include this content in its own section on the main page of the staff interface </div>
17
<div id="IntranetmainUserblock_notes" class="hint customization_note"> Include this content in its own section on the main page of the staff interface </div>
18
18
19
<div id="StaffReportsHome_notes" class="hint customization_note"> Include this content in its own section on the reports home page</div>
20
19
<div id="opaccredits_notes" class="hint customization_note"> Include this content in the footer of all pages in the OPAC. </div>
21
<div id="opaccredits_notes" class="hint customization_note"> Include this content in the footer of all pages in the OPAC. </div>
20
22
21
<div id="OpacCustomSearch_notes" class="hint customization_note"> Replace the search box at the top of OPAC pages with this content. </div>
23
<div id="OpacCustomSearch_notes" class="hint customization_note"> Replace the search box at the top of OPAC pages with this content. </div>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/member-display-address-style.inc (-13 / +37 lines)
Lines 29-38 Link Here
29
    [%~ END ~%]
29
    [%~ END ~%]
30
    [%~ IF ( patron.city || patron.zipcode || patron.state || patron.country ) ~%]
30
    [%~ IF ( patron.city || patron.zipcode || patron.state || patron.country ) ~%]
31
        <li class="patroncity">
31
        <li class="patroncity">
32
            <span class="city">[%~ patron.city |html ~%]</span>[%~ IF ( patron.state ) %][%~ IF ( patron.city ) ~%],[% END ~%]<span class="state">[% patron.state |html ~%]</span>[%~ END ~%]
32
            <span class="city">[%~ patron.city |html ~%]</span>
33
            [%~ IF ( patron.zipcode ) %]<span class="zipcode">[%~ " " _ patron.zipcode |html ~%]</span>[% END %][%~ IF ( patron.country ) %][%~ IF ( patron.zipcode || patron.state || patron.city ) ~%]
33
            [%~ IF ( patron.state ) %]
34
                <span class="comma">,</span>
34
                [%~ IF ( patron.city ) ~%]
35
            [% END ~%]<span class="country">[% patron.country |html ~%]</span>[%~ END ~%]
35
                    ,
36
                [% END ~%]
37
                <span class="state">[% patron.state |html ~%]</span>
38
            [%~ END ~%]
39
            [%~ IF ( patron.zipcode ) %]<span class="zipcode">[%~ " " _ patron.zipcode |html ~%]</span>[% END %]
40
            [%~ IF ( patron.country ) %]
41
                [% line_break | $raw %]
42
                <span class="country">[% patron.country |html ~%]</span>
43
            [%~ END ~%]
36
        </li>
44
        </li>
37
    [%~ END ~%]
45
    [%~ END ~%]
38
[%~ END ~%]
46
[%~ END ~%]
Lines 44-54 Link Here
44
            [%~ IF patron.streettype ~%]
52
            [%~ IF patron.streettype ~%]
45
                [%~ SET roadtype_desc = AuthorisedValues.GetByCode('ROADTYPE', patron.streettype) ~%]
53
                [%~ SET roadtype_desc = AuthorisedValues.GetByCode('ROADTYPE', patron.streettype) ~%]
46
            [%~ END ~%]
54
            [%~ END ~%]
47
            <li class="patronaddress1"
55
            <li class="patronaddress1">
48
                ><span class="address1">[%~ patron.address | html ~%]</span>[%~ IF roadtype_desc %]<span class="roadtype">[% roadtype_desc | html ~%]</span>[%~ END ~%][%~ IF patron.streetnumber %]
56
                <span class="address1">[%~ patron.address | html ~%]</span>
57
                [%~ IF roadtype_desc %]
58
                    <span class="roadtype">[% roadtype_desc | html ~%]</span>
59
                [%~ END ~%]
60
                [%~ IF patron.streetnumber %]
49
                    <span class="streetnumber">[% patron.streetnumber | html ~%]</span>
61
                    <span class="streetnumber">[% patron.streetnumber | html ~%]</span>
50
                [%~ END ~%]</li
62
                [%~ END ~%]
51
            >
63
            </li>
52
        [%~ END ~%]
64
        [%~ END ~%]
53
        [%~ IF ( patron.address2 ) ~%]
65
        [%~ IF ( patron.address2 ) ~%]
54
            <li class="patronaddress2">[%~ patron.address2 | html ~%]</li>
66
            <li class="patronaddress2">[%~ patron.address2 | html ~%]</li>
Lines 56-62 Link Here
56
    [%~ END ~%]
68
    [%~ END ~%]
57
    [%~ IF ( patron.city || patron.zipcode || patron.state || patron.country ) ~%]
69
    [%~ IF ( patron.city || patron.zipcode || patron.state || patron.country ) ~%]
58
        <li class="patroncity">
70
        <li class="patroncity">
59
            [%~ IF ( patron.zipcode ) ~%]<span class="zipcode">[%~ patron.zipcode | html %]</span>[% END ~%]<span class="city">[%~ patron.city | html ~%]</span>[%~ IF ( patron.state ) ~%]
71
            [%~ IF ( patron.zipcode ) ~%]
72
                <span class="zipcode">[%~ patron.zipcode | html %]</span>
73
            [% END ~%]
74
            <span class="city">[%~ patron.city | html ~%]</span>
75
            [%~ IF ( patron.state ) ~%]
60
                [% line_break | $raw %]<span class="state">[%~ patron.state | html ~%]</span>
76
                [% line_break | $raw %]<span class="state">[%~ patron.state | html ~%]</span>
61
            [%~ END ~%]
77
            [%~ END ~%]
62
            [%~ IF ( patron.country ) ~%][% line_break | $raw %]<span class="country">[%~ patron.country | html ~%]</span>[%~ END ~%]
78
            [%~ IF ( patron.country ) ~%][% line_break | $raw %]<span class="country">[%~ patron.country | html ~%]</span>[%~ END ~%]
Lines 71-79 Link Here
71
            [%~ IF patron.streettype ~%]
87
            [%~ IF patron.streettype ~%]
72
                [%~ SET roadtype_desc = AuthorisedValues.GetByCode('ROADTYPE', patron.streettype) ~%]
88
                [%~ SET roadtype_desc = AuthorisedValues.GetByCode('ROADTYPE', patron.streettype) ~%]
73
            [%~ END ~%]
89
            [%~ END ~%]
74
            <li class="patronaddress1"
90
            <li class="patronaddress1">
75
                >[%~ IF patron.streetnumber ~%]<span class="streetnumber">[%~ patron.streetnumber | html %]</span>[% END ~%]
91
                [%~ IF patron.streetnumber ~%]
76
                [%~ IF roadtype_desc ~%]<span class="roadtype">[%~ roadtype_desc | html %]</span>[% END ~%]
92
                    <span class="streetnumber">[%~ patron.streetnumber | html %]</span>
93
                [% END ~%]
94
                [%~ IF roadtype_desc ~%]
95
                    <span class="roadtype">[%~ roadtype_desc | html %]</span>
96
                [% END ~%]
77
                <span class="address1">[%~ patron.address | html ~%]</span>
97
                <span class="address1">[%~ patron.address | html ~%]</span>
78
            </li>
98
            </li>
79
        [%~ END ~%]
99
        [%~ END ~%]
Lines 83-89 Link Here
83
    [%~ END ~%]
103
    [%~ END ~%]
84
    [%~ IF ( patron.city || patron.zipcode || patron.state || patron.country ) ~%]
104
    [%~ IF ( patron.city || patron.zipcode || patron.state || patron.country ) ~%]
85
        <li class="patroncity">
105
        <li class="patroncity">
86
            [%~ IF ( patron.zipcode ) ~%]<span class="zipcode">[%~ patron.zipcode | html %]</span>[% END ~%]<span class="city">[%~ patron.city | html ~%]</span>[%~ IF ( patron.state ) ~%]
106
            [%~ IF ( patron.zipcode ) ~%]
107
                <span class="zipcode">[%~ patron.zipcode | html %]</span>
108
            [% END ~%]
109
            <span class="city">[%~ patron.city | html ~%]</span>
110
            [%~ IF ( patron.state ) ~%]
87
                [% line_break | $raw %]<span class="state">[%~ patron.state | html ~%]</span>
111
                [% line_break | $raw %]<span class="state">[%~ patron.state | html ~%]</span>
88
            [%~ END ~%]
112
            [%~ END ~%]
89
            [%~ IF ( patron.country ) ~%][% line_break | $raw %]<span class="country">[%~ patron.country | html ~%]</span>[%~ END ~%]
113
            [%~ IF ( patron.country ) ~%][% line_break | $raw %]<span class="country">[%~ patron.country | html ~%]</span>[%~ END ~%]
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/patron_messages.inc (-25 / +21 lines)
Lines 92-123 Link Here
92
92
93
        [% IF ( userdebarred ) %]
93
        [% IF ( userdebarred ) %]
94
            <li class="userdebarred blocker">
94
            <li class="userdebarred blocker">
95
                <span class="circ-hlt"> Restricted since [% debarredsince | $KohaDates %]:</span> Patron's account is restricted
95
                <span class="circ-hlt">Account restricted since [% debarredsince | $KohaDates %]</span>
96
                [% IF ( userdebarreddate ) %]
96
                <ul>
97
                    until [% userdebarreddate | $KohaDates %]
97
                    [% FOREACH restriction IN patron.restrictions %]
98
                [% END %]
98
                        <li class="[% restriction.type.code | lower | html %]_restriction">
99
99
                            <span class="restriction_expiration">
100
                [% IF ( debarredcomment ) %]
100
                                [% IF restriction.expiration %]
101
                    with the explanation: <br />
101
                                    <strong>Restriction expiring [% restriction.expiration | $KohaDates %]</strong>
102
                    <em>
102
                                [% ELSE %]
103
                        [% IF debarredcomment.search('OVERDUES_PROCESS') %]
103
                                    <strong>Indefinite restriction</strong>
104
                            Restriction added by overdues process [% debarredcomment.remove('OVERDUES_PROCESS ') | $raw | html_line_break %]
104
                                [% END %]
105
                        [% ELSE %]
105
                            </span>
106
                            [% FOREACH restriction IN patron.restrictions %]
106
                            [% IF restriction.comment.search('OVERDUES_PROCESS') %]
107
                                <div class="[% restriction.type.code | lower | html %]_restriction">
107
                                Restriction added by overdues process [% restriction.comment.remove('OVERDUES_PROCESS ') | $raw | html_line_break %]
108
                                    <span class="restriction_expiration">
108
                            [% ELSE %]
109
                                        [% IF restriction.expiration %]
109
                                [% IF restriction.comment %]
110
                                            [% restriction.expiration | $KohaDates %]
110
                                    <span class="restriction_detail">[%- restriction.comment | html_line_break -%]</span>
111
                                        [% ELSE %]
111
                                [% END %]
112
                                            <strong>Indefinite</strong>
113
                                        [% END %]
114
                                    </span>
115
                                    <span class="restriction_detail"> [%- restriction.type.display_text | html -%][%- IF restriction.comment -%]: [%- restriction.comment | html_line_break -%][%- END -%] </span>
116
                                </div>
117
                            [% END %]
112
                            [% END %]
118
                        [% END %] </em
113
                            <span class="restriction_type">([%- restriction.type.display_text | html -%])</span>
119
                    ><br />
114
                        </li>
120
                [% END %]
115
                    [% END %]
116
                </ul>
121
                <a class="btn btn-xs btn-default" href="#reldebarments-tab" onclick="(new bootstrap.Tab($('#reldebarments-tab'))).show()"><i class="fa fa-ban"></i> View restrictions</a>
117
                <a class="btn btn-xs btn-default" href="#reldebarments-tab" onclick="(new bootstrap.Tab($('#reldebarments-tab'))).show()"><i class="fa fa-ban"></i> View restrictions</a>
122
            </li>
118
            </li>
123
            <!-- /.blocker -->
119
            <!-- /.blocker -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/prefs-menu.inc (+11 lines)
Lines 165-170 Link Here
165
            </li>
165
            </li>
166
        [% END %]
166
        [% END %]
167
167
168
        [% IF ( reports ) %]
169
            <li class="active">
170
                <a title="Reports" href="/cgi-bin/koha/admin/preferences.pl?tab=reports">Reports</a>
171
                [% PROCESS subtabs %]
172
            </li>
173
        [% ELSE %]
174
            <li>
175
                <a title="Reports" href="/cgi-bin/koha/admin/preferences.pl?tab=reports">Reports</a>
176
            </li>
177
        [% END %]
178
168
        [% IF ( searching ) %]
179
        [% IF ( searching ) %]
169
            <li class="active">
180
            <li class="active">
170
                <a title="Searching" href="/cgi-bin/koha/admin/preferences.pl?tab=searching">Searching</a>
181
                <a title="Searching" href="/cgi-bin/koha/admin/preferences.pl?tab=searching">Searching</a>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc (-1 / +1 lines)
Lines 6-12 Link Here
6
    <ul>
6
    <ul>
7
        <li><a href="/cgi-bin/koha/tools/tools-home.pl">Tools home</a></li>
7
        <li><a href="/cgi-bin/koha/tools/tools-home.pl">Tools home</a></li>
8
    </ul>
8
    </ul>
9
    [% IF ( CAN_user_tools_manage_patron_lists || CAN_user_clubs || CAN_user_tools_moderate_comments || CAN_user_tools_import_patrons  || CAN_user_tools_edit_notices || CAN_user_tools_edit_notice_status_triggers || CAN_user_tools_label_creator || CAN_user_tools_delete_anonymize_patrons  || CAN_user_tools_edit_patrons || CAN_user_tools_moderate_tags || ( CAN_user_tools_batch_upload_patron_images && Koha.Preference('patronimages') ) || CAN_user_tools_rotating_collections ) %]
9
    [% IF ( CAN_user_tools_manage_patron_lists || CAN_user_clubs || CAN_user_tools_moderate_comments || CAN_user_tools_import_patrons  || CAN_user_tools_edit_notices || CAN_user_tools_edit_notice_status_triggers || CAN_user_tools_label_creator || CAN_user_tools_delete_anonymize_patrons  || CAN_user_tools_edit_patrons || CAN_user_tools_batch_extend_due_dates || CAN_user_tools_moderate_tags || ( CAN_user_tools_batch_upload_patron_images && Koha.Preference('patronimages') ) || CAN_user_tools_rotating_collections ) %]
10
        <h5>Patrons and circulation</h5>
10
        <h5>Patrons and circulation</h5>
11
        <ul>
11
        <ul>
12
            [% IF ( CAN_user_tools_manage_patron_lists ) %]
12
            [% IF ( CAN_user_tools_manage_patron_lists ) %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/about.tt (-5 / +6 lines)
Lines 13-18 Link Here
13
    [% END %]</title
13
    [% END %]</title
14
>
14
>
15
[% INCLUDE 'doc-head-close.inc' %]
15
[% INCLUDE 'doc-head-close.inc' %]
16
<style>
17
    .version-ellipsis {
18
        text-decoration: underline;
19
    }
20
</style>
16
</head>
21
</head>
17
<body id="about_about" class="about">
22
<body id="about_about" class="about">
18
[% INCLUDE 'header.inc' %]
23
[% INCLUDE 'header.inc' %]
Lines 801-811 Link Here
801
        </div>
806
        </div>
802
        [% IF db_audit.diff_found %]
807
        [% IF db_audit.diff_found %]
803
            <div>
808
            <div>
804
                <p>Run the following SQL to fix the database:</p>
809
                <pre><code>[% db_audit.diff | html %]</code></pre>
805
                <pre>
806
                    <code>[% db_audit.diff | $raw %]</code>
807
                </pre
808
                >
809
            </div>
810
            </div>
810
        [% END %]
811
        [% END %]
811
    [% END # tab=database %]
812
    [% END # tab=database %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt (-3 / +22 lines)
Lines 150-155 Link Here
150
                                                            class="btn btn-default btn-xs submit-form-link"
150
                                                            class="btn btn-default btn-xs submit-form-link"
151
                                                            href="#"
151
                                                            href="#"
152
                                                            data-ean="[% eanacct.ean | html %]"
152
                                                            data-ean="[% eanacct.ean | html %]"
153
                                                            data-ean_description="[% eanacct.description | html %]"
154
                                                            data-ean_branch="[% eanacct.branch.branchname | html %]"
153
                                                            data-basketno="[% basketno | html %]"
155
                                                            data-basketno="[% basketno | html %]"
154
                                                            data-action="basket.pl"
156
                                                            data-action="basket.pl"
155
                                                            data-method="post"
157
                                                            data-method="post"
Lines 812-817 Link Here
812
                                                        <a href="neworderempty.pl?ordernumber=[% books_loo.ordernumber | uri %]&amp;booksellerid=[% booksellerid | uri %]&amp;basketno=[% basketno | uri %]">Modify</a>
814
                                                        <a href="neworderempty.pl?ordernumber=[% books_loo.ordernumber | uri %]&amp;booksellerid=[% booksellerid | uri %]&amp;basketno=[% basketno | uri %]">Modify</a>
813
                                                        <br />
815
                                                        <br />
814
                                                        <a href="#" class="transfer_order" data-ordernumber="[% books_loo.ordernumber | html %]">Transfer</a>
816
                                                        <a href="#" class="transfer_order" data-ordernumber="[% books_loo.ordernumber | html %]">Transfer</a>
817
                                                        <br />
818
                                                    [% END %]
819
                                                    [% UNLESS ( books_loo.suggestionid ) %]
820
                                                        <a
821
                                                            data-action="/cgi-bin/koha/acqui/newordersuggestion.pl"
822
                                                            data-booksellerid="[% booksellerid | html %]"
823
                                                            data-basketno="[% basketno | html %]"
824
                                                            data-link_order="[% books_loo.ordernumber | html %]"
825
                                                            data-method="get"
826
                                                            class="submit-form-link"
827
                                                            href="#"
828
                                                            >[% tp('verb', 'Link suggestion') | html %]</a
829
                                                        >
815
                                                    [% END %]
830
                                                    [% END %]
816
                                                </td>
831
                                                </td>
817
                                            [% END %]
832
                                            [% END %]
Lines 1015-1021 Link Here
1015
            <div id="closebasket_needsconfirmation" class="alert alert-warning">
1030
            <div id="closebasket_needsconfirmation" class="alert alert-warning">
1016
                <form action="/cgi-bin/koha/acqui/basket.pl" method="post">
1031
                <form action="/cgi-bin/koha/acqui/basket.pl" method="post">
1017
                    [% INCLUDE 'csrf-token.inc' %]
1032
                    [% INCLUDE 'csrf-token.inc' %]
1018
                    <h1>Are you sure you want to generate an EDIFACT order and close basket [% basketname | html %]?</h1>
1033
                    <h1>Are you sure you want to generate this EDIFACT order?</h1>
1034
                    <ul>
1035
                        <li> An EDIFACT order will be generated for <b> [% ean_branch | html %] ([% ean | html %]) [% IF ean_description %][% ean_description | html %][% END %]</b></li>
1036
                        <li> The basket <b>[% basketname | html %]</b> will be closed </li>
1037
                    </ul>
1019
                    [% IF CAN_user_acquisition_group_manage %]
1038
                    [% IF CAN_user_acquisition_group_manage %]
1020
                        <p>
1039
                        <p>
1021
                            <label for="createbasketgroup">Attach this basket to a new basket group with the same name</label>
1040
                            <label for="createbasketgroup">Attach this basket to a new basket group with the same name</label>
Lines 1028-1038 Link Here
1028
                    <input type="hidden" name="booksellerid" value="[% booksellerid | html %]" />
1047
                    <input type="hidden" name="booksellerid" value="[% booksellerid | html %]" />
1029
                    <input type="hidden" name="confirm" value="1" />
1048
                    <input type="hidden" name="confirm" value="1" />
1030
                    <input type="hidden" name="basketgroupname" value="[% basketgroupname | html %]" />
1049
                    <input type="hidden" name="basketgroupname" value="[% basketgroupname | html %]" />
1031
                    <button type="submit" class="btn btn-default approve" accesskey="Y"><i class="fa fa-fw fa-check"></i> Yes, close (Y)</button>
1050
                    <button type="submit" class="btn btn-default approve" accesskey="Y"><i class="fa fa-fw fa-check"></i> Yes, generate order and close basket (Y)</button>
1032
                </form>
1051
                </form>
1033
                <form action="/cgi-bin/koha/acqui/basket.pl" method="get">
1052
                <form action="/cgi-bin/koha/acqui/basket.pl" method="get">
1034
                    <input type="hidden" name="basketno" value="[% basketno | html %]" />
1053
                    <input type="hidden" name="basketno" value="[% basketno | html %]" />
1035
                    <button type="submit" class="btn btn-default deny" accesskey="N"><i class="fa fa-fw fa-times"></i> No, don't close (N)</button>
1054
                    <button type="submit" class="btn btn-default deny" accesskey="N"><i class="fa fa-fw fa-times"></i> Cancel (C)</button>
1036
                </form>
1055
                </form>
1037
            </div>
1056
            </div>
1038
            <!-- /#closebasket_needsconfirmation -->
1057
            <!-- /#closebasket_needsconfirmation -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/newordersuggestion.tt (-1 / +14 lines)
Lines 89-95 Link Here
89
                            </td>
89
                            </td>
90
                            <td> [% suggestion.total | $Price %] </td>
90
                            <td> [% suggestion.total | $Price %] </td>
91
                            <td class="actions">
91
                            <td class="actions">
92
                                [% IF ( suggestion.biblionumber ) %]
92
                                [% IF link_order %]
93
                                    <a
94
                                        data-action="/cgi-bin/koha/acqui/newordersuggestion.pl"
95
                                        data-op="cud-link_order"
96
                                        data-link_order="[% link_order | html %]"
97
                                        data-booksellerid="[% booksellerid | html %]"
98
                                        data-basketno="[% basketno | html %]"
99
                                        data-suggestionid="[% suggestion.suggestionid | html %]"
100
                                        data-method="post"
101
                                        class="btn btn-default btn-xs submit-form-link"
102
                                        href="#"
103
                                        ><i class="fa fa-plus"></i> [% tp('verb', 'Link suggestion') | html %]</a
104
                                    >
105
                                [% ELSIF ( suggestion.biblionumber ) %]
93
                                    <a
106
                                    <a
94
                                        href="neworderempty.pl?booksellerid=[% booksellerid | uri %]&amp;basketno=[% basketno | uri %]&amp;suggestionid=[% suggestion.suggestionid | uri %]&amp;biblio=[% suggestion.biblionumber | uri %]"
107
                                        href="neworderempty.pl?booksellerid=[% booksellerid | uri %]&amp;basketno=[% basketno | uri %]&amp;suggestionid=[% suggestion.suggestionid | uri %]&amp;biblio=[% suggestion.biblionumber | uri %]"
95
                                        class="btn btn-default btn-xs"
108
                                        class="btn btn-default btn-xs"
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt (-4 / +4 lines)
Lines 486-498 Link Here
486
                        <label for="noissuescharge">Checkout charge limit: </label>
486
                        <label for="noissuescharge">Checkout charge limit: </label>
487
                        <input type="text" pattern="^\d+(\.\d{2})?$" name="noissuescharge" id="noissuescharge" value="[% category.noissuescharge | $Price on_editing => 1 %]" size="5" maxlength="8" />
487
                        <input type="text" pattern="^\d+(\.\d{2})?$" name="noissuescharge" id="noissuescharge" value="[% category.noissuescharge | $Price on_editing => 1 %]" size="5" maxlength="8" />
488
                        [%- SET pref_noissuescharge_link = '<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=noissuescharge&ok=Search">noissuescharge</a>' -%]
488
                        [%- SET pref_noissuescharge_link = '<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=noissuescharge&ok=Search">noissuescharge</a>' -%]
489
                        <div class="hint">If set, this will override the global value set in the [%- pref_noissuescharge_link | $raw | $KohaSpan -%] preference.</div>
489
                        <div class="hint">If set, this will override the global value set in the [%- pref_noissuescharge_link | $raw | $KohaSpan -%] system preference.</div>
490
                    </li>
490
                    </li>
491
                    <li>
491
                    <li>
492
                        <label for="noissueschargeguarantees">Guarantees checkout charge limit: </label>
492
                        <label for="noissueschargeguarantees">Guarantees checkout charge limit: </label>
493
                        <input type="text" pattern="^\d+(\.\d{2})?$" name="noissueschargeguarantees" id="noissueschargeguarantees" value="[% category.noissueschargeguarantees | $Price on_editing => 1 %]" size="5" maxlength="8" />
493
                        <input type="text" pattern="^\d+(\.\d{2})?$" name="noissueschargeguarantees" id="noissueschargeguarantees" value="[% category.noissueschargeguarantees | $Price on_editing => 1 %]" size="5" maxlength="8" />
494
                        [%- SET pref_NoIssuesChargeGuarantees_link = '<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=NoIssuesChargeGuarantees&ok=Search">NoIssuesChargeGuarantees</a>' -%]
494
                        [%- SET pref_NoIssuesChargeGuarantees_link = '<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=NoIssuesChargeGuarantees&ok=Search">NoIssuesChargeGuarantees</a>' -%]
495
                        <div class="hint">If set, this will override the global value set in the [%- pref_NoIssuesChargeGuarantees_link | $raw | $KohaSpan -%] preference.</div>
495
                        <div class="hint">If set, this will override the global value set in the [%- pref_NoIssuesChargeGuarantees_link | $raw | $KohaSpan -%] system preference.</div>
496
                    </li>
496
                    </li>
497
                    <li>
497
                    <li>
498
                        <label for="noissueschargeguarantorswithguarantees">Guarantors with guarantees checkout charge limit: </label>
498
                        <label for="noissueschargeguarantorswithguarantees">Guarantors with guarantees checkout charge limit: </label>
Lines 505-512 Link Here
505
                            size="5"
505
                            size="5"
506
                            maxlength="8"
506
                            maxlength="8"
507
                        />
507
                        />
508
                        [%- SET pref_NoIssuesChargeGuarantorsWithGuarantees_link = '<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=NoIssuesChargeGuarantees&ok=Search">NoIssuesChargeGuarantees</a>' -%]
508
                        [%- SET pref_NoIssuesChargeGuarantorsWithGuarantees_link = '<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=NoIssuesChargeGuarantorsWithGuarantees&ok=Search">NoIssuesChargeGuarantorsWithGuarantees</a>' -%]
509
                        <div class="hint">If set, this will override the global value set in the [%- pref_NoIssuesChargeGuarantorsWithGuarantees_link | $raw | $KohaSpan -%] preference.</div>
509
                        <div class="hint">If set, this will override the global value set in the [%- pref_NoIssuesChargeGuarantorsWithGuarantees_link | $raw | $KohaSpan -%] system preference.</div>
510
                    </li>
510
                    </li>
511
                    <li>
511
                    <li>
512
                        <label for="enforce_expiry_notice">Enforce patron account expiry notice:</label>
512
                        <label for="enforce_expiry_notice">Enforce patron account expiry notice:</label>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/localization.tt (-2 / +3 lines)
Lines 198-208 Link Here
198
        $(document).ready(function() {
198
        $(document).ready(function() {
199
            $(".dialog").hide();
199
            $(".dialog").hide();
200
200
201
            var table = $("#localization").kohaTable({
201
            let table = $("#localization").kohaTable({
202
                dom: "t",
202
                dom: "t",
203
                paging: false,
203
                paging: false,
204
                autoWidth: false,
204
                autoWidth: false,
205
            });
205
            });
206
            let table_dt = table.DataTable();
206
207
207
            var languages_select = $('<select name="lang" id="lang"></select>');
208
            var languages_select = $('<select name="lang" id="lang"></select>');
208
            [% FOR language IN languages %]
209
            [% FOR language IN languages %]
Lines 275-281 Link Here
275
                        if ( success.error ) {
276
                        if ( success.error ) {
276
                            show_message({ type: 'error_on_insert', data: success });
277
                            show_message({ type: 'error_on_insert', data: success });
277
                        } else {
278
                        } else {
278
                            var new_row = table.row.add( [ success.id, success.entity, success.code, success.lang, success.translation, "<a href=\"#\" class=\"delete\"><i class=\"fa fa-trash-can\"></i> Delete</a>" ] ).draw().node();
279
                            var new_row = table_dt.row.add( [ success.id, success.entity, success.code, success.lang, success.translation, "<a href=\"#\" class=\"delete\"><i class=\"fa fa-trash-can\"></i> Delete</a>" ] ).draw().node();
279
                            $( new_row ).attr("id", "row_id_" + success.id ).data("id", success.id );
280
                            $( new_row ).attr("id", "row_id_" + success.id ).data("id", success.id );
280
                            show_message({ type: 'success_on_insert', data: success });
281
                            show_message({ type: 'success_on_insert', data: success });
281
                        }
282
                        }
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marctagstructure.tt (-1 / +3 lines)
Lines 94-100 Link Here
94
    [% IF ( else ) %]
94
    [% IF ( else ) %]
95
        <div id="toolbar" class="btn-toolbar">
95
        <div id="toolbar" class="btn-toolbar">
96
            <a class="btn btn-default" id="addtag" href="/cgi-bin/koha/admin/marctagstructure.pl?op=add_form&amp;frameworkcode=[% framework.frameworkcode | uri %]"><i class="fa fa-plus"></i> New tag</a>
96
            <a class="btn btn-default" id="addtag" href="/cgi-bin/koha/admin/marctagstructure.pl?op=add_form&amp;frameworkcode=[% framework.frameworkcode | uri %]"><i class="fa fa-plus"></i> New tag</a>
97
            <a class="btn btn-default" href="/cgi-bin/koha/admin/biblio_framework.pl?op=add_form&amp;frameworkcode=[% framework.frameworkcode | uri %]"><i class="fa-solid fa-pencil" aria-hidden="true"></i> Edit framework</a>
97
            [% IF framework %]
98
                <a class="btn btn-default" href="/cgi-bin/koha/admin/biblio_framework.pl?op=add_form&amp;frameworkcode=[% framework.frameworkcode | uri %]"><i class="fa-solid fa-pencil" aria-hidden="true"></i> Edit framework</a>
99
            [% END %]
98
        </div>
100
        </div>
99
    [% END %]
101
    [% END %]
100
102
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref (-1 / +1 lines)
Lines 9-15 Acquisitions: Link Here
9
                  cataloguing: cataloging the record.
9
                  cataloguing: cataloging the record.
10
            - This is only the default behavior, and can be changed per-basket.
10
            - This is only the default behavior, and can be changed per-basket.
11
        -
11
        -
12
            - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/items.html' target='blank'>database columns</a> should be unique in an item:"
12
            - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/items.html' target='blank'>database columns</a> should be unique in an item:"
13
            - pref: UniqueItemFields
13
            - pref: UniqueItemFields
14
              type: modalselect
14
              type: modalselect
15
              source: items
15
              source: items
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref (-1 / +8 lines)
Lines 677-682 Circulation: Link Here
677
                  1: Block
677
                  1: Block
678
                  0: "Don't block"
678
                  0: "Don't block"
679
            - returning of items that have been withdrawn.
679
            - returning of items that have been withdrawn.
680
        -
681
            - "Prevent the ability to withdraw items with the following statuses:"
682
            - pref: PreventWithdrawingItemsStatus
683
              multiple:
684
                intransit: In-transit
685
                checkedout: Checked out
686
            -
680
        -
687
        -
681
            - pref: BlockReturnOfLostItems
688
            - pref: BlockReturnOfLostItems
682
              choices:
689
              choices:
Lines 1527-1530 Circulation: Link Here
1527
              choices:
1534
              choices:
1528
                  1: Enable
1535
                  1: Enable
1529
                  0: Disable
1536
                  0: Disable
1530
            - "the curbside pickup module."
1537
            - "the curbside pickup module."
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/i18n_l10n.pref (+5 lines)
Lines 63-68 I18N/L10N: Link Here
63
              de: German style ([Address] [Street number] - [ZIP/Postal Code] [City] - [Country])
63
              de: German style ([Address] [Street number] - [ZIP/Postal Code] [City] - [Country])
64
              fr: French style ([Street number] [Address] - [ZIP/Postal Code] [City] - [Country])
64
              fr: French style ([Street number] [Address] - [ZIP/Postal Code] [City] - [Country])
65
        - .
65
        - .
66
    -
67
        - Sort facet names using
68
        - pref: FacetSortingLocale
69
          type: locale-list
70
        - locale when FacetOrder is set to Alphabetical. This enables proper Unicode-aware sorting of accented characters and locale-specific alphabetical ordering.
66
    -
71
    -
67
        - pref: TranslateNotices
72
        - pref: TranslateNotices
68
          choices:
73
          choices:
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref (-5 / +5 lines)
Lines 668-674 OPAC: Link Here
668
              type: textarea
668
              type: textarea
669
              syntax: text/x-yaml
669
              syntax: text/x-yaml
670
              class: code
670
              class: code
671
            - Define custom rules to hide specific items from search and view on the OPAC. How to write these rules is documented on the <a href="http://wiki.koha-community.org/wiki/OpacHiddenItems" target="_blank">Koha wiki</a>.
671
            - Define custom rules to hide specific items from search and view on the OPAC. How to write these rules is documented on the <a href="https://wiki.koha-community.org/wiki/OpacHiddenItems" target="_blank">Koha wiki</a>.
672
        -
672
        -
673
            - 'List of patron categories, that can see items otherwise hidden by <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpacHiddenItems">OpacHiddenItems</a>:'
673
            - 'List of patron categories, that can see items otherwise hidden by <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpacHiddenItems">OpacHiddenItems</a>:'
674
            - pref: OpacHiddenItemsExceptions
674
            - pref: OpacHiddenItemsExceptions
Lines 851-869 OPAC: Link Here
851
            - "days after account creation."
851
            - "days after account creation."
852
            - "<br><strong>NOTE:</strong> This system preference requires the <code>misc/cronjobs/cleanup_database.pl</code> cronjob. Ask your system administrator to schedule it.<br>No patrons will be deleted if you set the pref to zero or make it empty."
852
            - "<br><strong>NOTE:</strong> This system preference requires the <code>misc/cronjobs/cleanup_database.pl</code> cronjob. Ask your system administrator to schedule it.<br>No patrons will be deleted if you set the pref to zero or make it empty."
853
        -
853
        -
854
            - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron modification screen:"
854
            - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron modification screen:"
855
            - pref: PatronSelfModificationMandatoryField
855
            - pref: PatronSelfModificationMandatoryField
856
              type: modalselect
856
              type: modalselect
857
              source: borrowers
857
              source: borrowers
858
              exclusions: password|cardnumber|dateexpiry|categorycode|sort1|sort2|opacnote|borrowernotes|gonenoaddress|lost|flags
858
              exclusions: password|cardnumber|dateexpiry|categorycode|sort1|sort2|opacnote|borrowernotes|gonenoaddress|lost|flags
859
        -
859
        -
860
            - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron entry screen:"
860
            - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron entry screen:"
861
            - pref: PatronSelfRegistrationBorrowerMandatoryField
861
            - pref: PatronSelfRegistrationBorrowerMandatoryField
862
              type: modalselect
862
              type: modalselect
863
              source: borrowers
863
              source: borrowers
864
              exclusions: sort1|sort2|opacnote|borrowernotes|gonenoaddress|lost|flags
864
              exclusions: sort1|sort2|opacnote|borrowernotes|gonenoaddress|lost|flags
865
        -
865
        -
866
            - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron self-registration screen:"
866
            - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron self-registration screen:"
867
            - pref: PatronSelfRegistrationBorrowerUnwantedField
867
            - pref: PatronSelfRegistrationBorrowerUnwantedField
868
              type: modalselect
868
              type: modalselect
869
              source: borrowers
869
              source: borrowers
Lines 871-877 OPAC: Link Here
871
              inclusions: categorycode|dateexpiry
871
              inclusions: categorycode|dateexpiry
872
            - '<br /> NOTE: preferred_name will be set to firstname if not included in the form.'
872
            - '<br /> NOTE: preferred_name will be set to firstname if not included in the form.'
873
        -
873
        -
874
            - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron self-modification screen:"
874
            - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron self-modification screen:"
875
            - pref: PatronSelfModificationBorrowerUnwantedField
875
            - pref: PatronSelfModificationBorrowerUnwantedField
876
              type: modalselect
876
              type: modalselect
877
              source: borrowers
877
              source: borrowers
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref (-5 / +5 lines)
Lines 211-217 Patrons: Link Here
211
         - pref: EmailFieldPrecedence
211
         - pref: EmailFieldPrecedence
212
           class: multi
212
           class: multi
213
         - '<br><strong>NOTE:</strong> All patron fields can be used, but to work correctly they must contain a valid email address or an empty string.'
213
         - '<br><strong>NOTE:</strong> All patron fields can be used, but to work correctly they must contain a valid email address or an empty string.'
214
         - "Valid options are the <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> of the borrowers table, separated by | (pipe)."
214
         - "Valid options are the <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> of the borrowers table, separated by | (pipe)."
215
         - "Example: email|emailpro|B_email"
215
         - "Example: email|emailpro|B_email"
216
     -
216
     -
217
         - "When <a href='/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=EmailFieldPrimary'>EmailFieldPrimary</a> is set to '<strong>selected addresses</strong>', send email to all valid email addresses in the selected fields:"
217
         - "When <a href='/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=EmailFieldPrimary'>EmailFieldPrimary</a> is set to '<strong>selected addresses</strong>', send email to all valid email addresses in the selected fields:"
Lines 274-287 Patrons: Link Here
274
               cron: (Deprecated) according to --send-notices cron switch
274
               cron: (Deprecated) according to --send-notices cron switch
275
    Patron forms:
275
    Patron forms:
276
     -
276
     -
277
         - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron entry screen:"
277
         - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron entry screen:"
278
         - pref: BorrowerMandatoryField
278
         - pref: BorrowerMandatoryField
279
           type: modalselect
279
           type: modalselect
280
           source: borrowers
280
           source: borrowers
281
           exclusions: flags
281
           exclusions: flags
282
         - '<strong>NOTE:</strong> If <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=autoMemberNum">autoMemberNum</a> is enabled, the system preference <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=BorrowerMandatoryField">BorrowerMandatoryField</a> must not contain the field <code>cardnumber</code>.'
282
         - '<strong>NOTE:</strong> If <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=autoMemberNum">autoMemberNum</a> is enabled, the system preference <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=BorrowerMandatoryField">BorrowerMandatoryField</a> must not contain the field <code>cardnumber</code>.'
283
     -
283
     -
284
         - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron entry screen:"
284
         - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron entry screen:"
285
         - pref: BorrowerUnwantedField
285
         - pref: BorrowerUnwantedField
286
           type: modalselect
286
           type: modalselect
287
           source: borrowers
287
           source: borrowers
Lines 311-317 Patrons: Link Here
311
               0: "Don't"
311
               0: "Don't"
312
         - store and display surnames in upper case.
312
         - store and display surnames in upper case.
313
     -
313
     -
314
         - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a>:"
314
         - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a>:"
315
         - pref: PatronQuickAddFields
315
         - pref: PatronQuickAddFields
316
           type: modalselect
316
           type: modalselect
317
           source: borrowers
317
           source: borrowers
Lines 337-343 Patrons: Link Here
337
               messaging: "Patron messaging preferences"
337
               messaging: "Patron messaging preferences"
338
         - .
338
         - .
339
     -
339
     -
340
         - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a>:"
340
         - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a>:"
341
         - pref: PatronDuplicateMatchingAddFields
341
         - pref: PatronDuplicateMatchingAddFields
342
           type: modalselect
342
           type: modalselect
343
           source: borrowers
343
           source: borrowers
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/reports.pref (+9 lines)
Line 0 Link Here
1
Reports:
2
    Reports Access:
3
     -
4
         - pref: EnableFilteringReports
5
           choices:
6
               1: Enable
7
               0: Disable
8
         - "filtering report access based on staff home library."
9
         - "<br><strong>NOTE:</strong> A NOTE ABOUT THE EFFECTS OF THIS PREFERENCE / SETTING"
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref (-6 lines)
Lines 113-124 Staff interface: Link Here
113
              type: textarea
113
              type: textarea
114
              syntax: text/html
114
              syntax: text/html
115
              class: code
115
              class: code
116
        -
117
            - "Show the following HTML in its own div on the bottom of the home page of the reports module:"
118
            - pref: IntranetReportsHomeHTML
119
              type: textarea
120
              syntax: text/html
121
              class: code
122
        -
116
        -
123
            - pref: StaffHighlightedWords
117
            - pref: StaffHighlightedWords
124
              type: boolean
118
              type: boolean
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/search_filters.tt (-10 / +10 lines)
Lines 30-40 Link Here
30
[% END #/ WRAPPER sub-header.inc %]
30
[% END #/ WRAPPER sub-header.inc %]
31
31
32
[% WRAPPER 'main-container.inc' aside='admin-menu' %]
32
[% WRAPPER 'main-container.inc' aside='admin-menu' %]
33
33
    <h1>Search filters</h1>
34
    [% IF filters_count %]
34
    [% IF filters_count %]
35
        <div id="search_filters_list">
35
        <div id="search_filters_list" class="page-section">
36
            <h2>Search filters</h2>
37
38
            <table id="search_filters_table">
36
            <table id="search_filters_table">
39
                <thead>
37
                <thead>
40
                    <tr>
38
                    <tr>
Lines 44-50 Link Here
44
                        <th>Limits</th>
42
                        <th>Limits</th>
45
                        <th>OPAC</th>
43
                        <th>OPAC</th>
46
                        <th>Staff interface</th>
44
                        <th>Staff interface</th>
47
                        <th>&nbsp;</th>
45
                        <th data-class-name="actions noExport">Actions</th>
48
                    </tr>
46
                    </tr>
49
                </thead>
47
                </thead>
50
            </table>
48
            </table>
Lines 62-75 Link Here
62
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
60
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
63
            </div>
61
            </div>
64
            <div class="modal-body">
62
            <div class="modal-body">
65
                <div class="form-group">
63
                <div class="mb-3">
66
                    <input type="hidden" id="filter_edit_id" name="filter_edit_id" />
64
                    <input type="hidden" id="filter_edit_id" name="filter_edit_id" />
67
                    <label for="filter_edit_name">Name:</label>
65
                    <label for="filter_edit_name">Name:</label>
68
                    <input id="filter_edit_name" name="filter_edit_name" type="text" />
66
                    <input id="filter_edit_name" name="filter_edit_name" type="text" />
69
                    <label for="filter_edit_opac">Show in OPAC?</label>
67
                </div>
70
                    <input type="checkbox" id="filter_edit_opac" name="filter_edit_opac" />
68
                <div class="mb-3">
71
                    <label for="filter_edit_staff_client">Show in staff interface?</label>
69
                    <label for="filter_edit_opac"><input type="checkbox" id="filter_edit_opac" name="filter_edit_opac" /> Show in OPAC?</label>
72
                    <input type="checkbox" id="filter_edit_staff_client" name="filter_edit_staff_client" />
70
                </div>
71
                <div class="mb-3">
72
                    <label for="filter_edit_staff_client"><input type="checkbox" id="filter_edit_staff_client" name="filter_edit_staff_client" /> Show in staff interface?</label>
73
                </div>
73
                </div>
74
            </div>
74
            </div>
75
            <div class="modal-footer">
75
            <div class="modal-footer">
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt (-1 / +1 lines)
Lines 59-65 Link Here
59
    [% INCLUDE 'messages.inc' %]
59
    [% INCLUDE 'messages.inc' %]
60
60
61
    <div id="login">
61
    <div id="login">
62
        <h1><a href="http://koha-community.org">Koha</a></h1>
62
        <h1><a href="https://koha-community.org">Koha</a></h1>
63
63
64
        [% SET StaffLoginInstructions = AdditionalContents.get( location => "StaffLoginInstructions", lang => lang ) %]
64
        [% SET StaffLoginInstructions = AdditionalContents.get( location => "StaffLoginInstructions", lang => lang ) %]
65
65
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/authorities.tt (-1 / +1 lines)
Lines 242-248 Link Here
242
                if ( elt.nodeName == 'SELECT' ) {
242
                if ( elt.nodeName == 'SELECT' ) {
243
                    $(elt).siblings('.select2').find("span[role='combobox']").addClass(notFilledClass);
243
                    $(elt).siblings('.select2').find("span[role='combobox']").addClass(notFilledClass);
244
                } else {
244
                } else {
245
                    elt.setAttribute('class','input_marceditor noEnterSubmit ' + notFilledClass);
245
                    elt.setAttribute('class','input_marceditor noEnterSubmit framework_plugin ' + notFilledClass);
246
                }
246
                }
247
                $('#' + subfields[i]).focus();
247
                $('#' + subfields[i]).focus();
248
                tabflag[tag+subfield+tagnumber][1]=label[i];
248
                tabflag[tag+subfield+tagnumber][1]=label[i];
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/basket/basket.tt (-1 / +1 lines)
Lines 39-45 Link Here
39
<body id="cart_basket" class="cart">
39
<body id="cart_basket" class="cart">
40
40
41
[% WRAPPER 'main-container.inc' wide_centered => 1 %]
41
[% WRAPPER 'main-container.inc' wide_centered => 1 %]
42
    <div class="container">
42
    <div class="container-fluid">
43
        <h1>Your cart</h1>
43
        <h1>Your cart</h1>
44
44
45
        <div id="toolbar" class="btn-toolbar">
45
        <div id="toolbar" class="btn-toolbar">
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/batch/print-notices.tt (-3 lines)
Lines 14-22 Link Here
14
        .message { page-break-after: always }
14
        .message { page-break-after: always }
15
        pre { font-family: monospace }
15
        pre { font-family: monospace }
16
        pre {white-space: pre-wrap;}
16
        pre {white-space: pre-wrap;}
17
        pre {white-space: -moz-pre-wrap;}
18
        pre {white-space: -o-pre-wrap;}
19
        pre {word-wrap: break-work;}
20
        -->
17
        -->
21
    </style>
18
    </style>
22
    <!-- prettier-ignore-end -->
19
    <!-- prettier-ignore-end -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt (-42 / +43 lines)
Lines 1262-1267 Link Here
1262
    [% Asset.js("js/recalls.js") | $raw %]
1262
    [% Asset.js("js/recalls.js") | $raw %]
1263
    [% Asset.js("js/coce.js") | $raw %]
1263
    [% Asset.js("js/coce.js") | $raw %]
1264
    [% Asset.js("lib/Chocolat/js/chocolat.js") | $raw %]
1264
    [% Asset.js("lib/Chocolat/js/chocolat.js") | $raw %]
1265
1266
    [%# The following PROCESS needs: %]
1267
    [%# can_edit_items_from item_type_image_locations %]
1268
    [% PROCESS build_items_table_js biblio => biblio %]
1269
1265
    [% IF ( Koha.Preference('CatalogConcerns') ) %]
1270
    [% IF ( Koha.Preference('CatalogConcerns') ) %]
1266
        <script>
1271
        <script>
1267
            /* Set a variable needed by add_catalog_concern.js */
1272
            /* Set a variable needed by add_catalog_concern.js */
Lines 1725-1730 Link Here
1725
            var bundle_lost_value = [% Koha.Preference('BundleLostValue') | html %];
1730
            var bundle_lost_value = [% Koha.Preference('BundleLostValue') | html %];
1726
        [% END %]
1731
        [% END %]
1727
1732
1733
        let items_tab_ids = [ 'holdings', 'otherholdings' ];
1734
        items_tab_ids.forEach( function( tab_id, index ) {
1735
1736
            // Early return if the tab is not shown (ie. no table)
1737
            if (!$("#%s_table".format(tab_id)).length) return;
1738
            [% IF Koha.Preference('AlwaysShowHoldingsTableFilters') %]
1739
                build_items_table(tab_id, true, {}, build_items_table_drawncallback);
1740
            [% ELSE %]
1741
                build_items_table(tab_id, false, {}, build_items_table_drawncallback);
1742
            [% END %]
1743
1744
            [% IF bundlesEnabled %]
1745
                // Add event listener for opening and closing bundle details
1746
                $('#' + tab_id + '_table tbody').on('click', 'button.details-control', function () {
1747
                    var button = $(this);
1748
                    var tr = button.closest('tr');
1749
                    var dTable = button.closest('table').DataTable({ 'retrieve': true });
1750
1751
                    let row = dTable.row( tr );
1752
                    let data = row.data();
1753
                    let itemnumber = data.item_id;
1754
                    let duedate = (data.checkout&&data.checkout.due_date) || null;
1755
1756
                    if ( row.child.isShown() ) {
1757
                        // This row is already open - close it
1758
                        row.child.hide();
1759
                        tr.removeClass('shown');
1760
                        button.removeClass('active');
1761
                    } else {
1762
                        // Open this row
1763
                        createChild(row, itemnumber, duedate);
1764
                        tr.addClass('shown');
1765
                        button.addClass('active');
1766
                    }
1767
                });
1768
            [% END # /IF bundlesEnabled %]
1769
        });
1770
1728
        $(document).ready(function() {
1771
        $(document).ready(function() {
1729
            [% IF bundlesEnabled %] // Bundle handling
1772
            [% IF bundlesEnabled %] // Bundle handling
1730
                function createChild ( row, itemnumber, duedate ) {
1773
                function createChild ( row, itemnumber, duedate ) {
Lines 2036-2079 Link Here
2036
                // End bundle handling
2079
                // End bundle handling
2037
            [% END # /IF bundlesEnabled %]
2080
            [% END # /IF bundlesEnabled %]
2038
2081
2039
            let items_tab_ids = [ 'holdings', 'otherholdings' ];
2040
            items_tab_ids.forEach( function( tab_id, index ) {
2041
2042
                // Early return if the tab is not shown (ie. no table)
2043
                if (!$("#%s-tab".format(tab_id)).length) return;
2044
                [% IF Koha.Preference('AlwaysShowHoldingsTableFilters') %]
2045
                    build_items_table(tab_id, true, {}, build_items_table_drawncallback);
2046
                [% ELSE %]
2047
                    build_items_table(tab_id, false, {}, build_items_table_drawncallback);
2048
                [% END %]
2049
2050
                [% IF bundlesEnabled %]
2051
                    // Add event listener for opening and closing bundle details
2052
                    $('#' + tab_id + '_table tbody').on('click', 'button.details-control', function () {
2053
                        var button = $(this);
2054
                        var tr = button.closest('tr');
2055
                        var dTable = button.closest('table').DataTable({ 'retrieve': true });
2056
2057
                        let row = dTable.row( tr );
2058
                        let data = row.data();
2059
                        let itemnumber = data.item_id;
2060
                        let duedate = (data.checkout&&data.checkout.due_date) || null;
2061
2062
                        if ( row.child.isShown() ) {
2063
                            // This row is already open - close it
2064
                            row.child.hide();
2065
                            tr.removeClass('shown');
2066
                            button.removeClass('active');
2067
                        } else {
2068
                            // Open this row
2069
                            createChild(row, itemnumber, duedate);
2070
                            tr.addClass('shown');
2071
                            button.addClass('active');
2072
                        }
2073
                    });
2074
                [% END # /IF bundlesEnabled %]
2075
            });
2076
2077
            [% IF Koha.Preference('AcquisitionDetails') %]
2082
            [% IF Koha.Preference('AcquisitionDetails') %]
2078
                var table_settings = [% TablesSettings.GetTableSettings('catalogue', 'detail', 'acquisitiondetails-table', 'json') | $raw %];
2083
                var table_settings = [% TablesSettings.GetTableSettings('catalogue', 'detail', 'acquisitiondetails-table', 'json') | $raw %];
2079
                var acquisitiondetails_table = $("#orders").kohaTable(
2084
                var acquisitiondetails_table = $("#orders").kohaTable(
Lines 2360-2369 Link Here
2360
            return confirm( _("Are you sure you want to delete this comment?") );
2365
            return confirm( _("Are you sure you want to delete this comment?") );
2361
        });
2366
        });
2362
    </script>
2367
    </script>
2363
    [%# The following PROCESS needs: %]
2364
    [%# can_edit_items_from item_type_image_locations %]
2365
    [% PROCESS build_items_table_js biblio => biblio %]
2366
2367
    [% CoverImagePlugins | $raw %]
2368
    [% CoverImagePlugins | $raw %]
2368
[% END # /jsinclude %]
2369
[% END # /jsinclude %]
2369
[% INCLUDE 'intranet-bottom.inc' %]
2370
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.tt (+2 lines)
Lines 490-495 Link Here
490
                + '      <th id="items_itemlost" data-colname="lost_status">' + _("Lost status") + '</th>'
490
                + '      <th id="items_itemlost" data-colname="lost_status">' + _("Lost status") + '</th>'
491
                + '      <th id="items_widthdrawn" data-colname="withdrawn_status">' + _("Withdrawn status") + '</th>'
491
                + '      <th id="items_widthdrawn" data-colname="withdrawn_status">' + _("Withdrawn status") + '</th>'
492
                + '      <th id="items_damaged" data-colname="damaged_status">' + _("Damaged status") + '</th>'
492
                + '      <th id="items_damaged" data-colname="damaged_status">' + _("Damaged status") + '</th>'
493
                + '      <th id="items_dateaccessioned" data-colname="dateaccessioned">' + _("Date accessioned") + '</th>'
493
                + '      <th id="items_checkouts" data-colname="checkouts">' + _("Checkouts") + '</th>'
494
                + '      <th id="items_checkouts" data-colname="checkouts">' + _("Checkouts") + '</th>'
494
                + '      <th id="items_datelastborrowed" data-colname="last_checkout_date">' + _("Last checkout date") + '</th>'
495
                + '      <th id="items_datelastborrowed" data-colname="last_checkout_date">' + _("Last checkout date") + '</th>'
495
                + '      <th id="items_date_due" data-colname="due_date">' + _("Due date") + '</th>'
496
                + '      <th id="items_date_due" data-colname="due_date">' + _("Due date") + '</th>'
Lines 685-690 Link Here
685
                    { 'name': 'itemlost' },
686
                    { 'name': 'itemlost' },
686
                    { 'name': 'withdrawn' },
687
                    { 'name': 'withdrawn' },
687
                    { 'name': 'damaged' },
688
                    { 'name': 'damaged' },
689
                    { 'name': 'dateaccessioned' },
688
                    { 'name': 'issues' },
690
                    { 'name': 'issues' },
689
                    { 'name': 'datelastborrowed' },
691
                    { 'name': 'datelastborrowed' },
690
                    { 'name': 'date_due' },
692
                    { 'name': 'date_due' },
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt (+10 lines)
Lines 152-157 Link Here
152
                        [% SET not_for_loan = 1 %]
152
                        [% SET not_for_loan = 1 %]
153
                        [% SET not_for_loan_description = AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.notforloan', authorised_value => item.notforloan ) %]
153
                        [% SET not_for_loan_description = AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.notforloan', authorised_value => item.notforloan ) %]
154
                    [% END %]
154
                    [% END %]
155
                    [% IF withdraw_error %]
156
                        <div class="alert alert-warning">
157
                            [% IF withdraw_error == 'intransit_cannot_withdraw' %]
158
                                Cannot withdraw item in transit.
159
                            [% END %]
160
                            [% IF withdraw_error == 'onloan_cannot_withdraw' %]
161
                                Cannot withdraw checked out item.
162
                            [% END %]
163
                        </div>
164
                    [% END %]
155
                    <div class="listgroup">
165
                    <div class="listgroup">
156
                        <h4>
166
                        <h4>
157
                            Item information
167
                            Item information
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt (-1 / +1 lines)
Lines 576-582 Link Here
576
                    if ( elt.nodeName == 'SELECT' ) {
576
                    if ( elt.nodeName == 'SELECT' ) {
577
                        $(elt).siblings('.select2').find("span[role='combobox']").addClass(notFilledClass);
577
                        $(elt).siblings('.select2').find("span[role='combobox']").addClass(notFilledClass);
578
                    } else {
578
                    } else {
579
                        elt.setAttribute('class','input_marceditor noEnterSubmit ' + notFilledClass);
579
                        elt.setAttribute('class','input_marceditor noEnterSubmit framework_plugin ' + notFilledClass);
580
                    }
580
                    }
581
                    $('#' + subfields[i]).focus();
581
                    $('#' + subfields[i]).focus();
582
                    tabflag[tag+subfield+tagnumber][1]=label[i];
582
                    tabflag[tag+subfield+tagnumber][1]=label[i];
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbooks.tt (-3 / +3 lines)
Lines 237-250 Link Here
237
                                            <a class="btn btn-default btn-xs dropdown-toggle" id="reservoirsearchactions[% breeding_loo.id | html %]" role="button" data-bs-toggle="dropdown" href="#"> Actions </a>
237
                                            <a class="btn btn-default btn-xs dropdown-toggle" id="reservoirsearchactions[% breeding_loo.id | html %]" role="button" data-bs-toggle="dropdown" href="#"> Actions </a>
238
                                            <ul class="dropdown-menu dropdown-menu-end" role="menu" aria-labelledby="reservoirsearchactions[% breeding_loo.id | html %]">
238
                                            <ul class="dropdown-menu dropdown-menu-end" role="menu" aria-labelledby="reservoirsearchactions[% breeding_loo.id | html %]">
239
                                                <li
239
                                                <li
240
                                                    ><a href="/cgi-bin/koha/catalogue/showmarc.pl?importid=[% breeding_loo.id | uri %]" class="previewData"><i class="fa-solid fa-eye"></i> MARC preview</a></li
240
                                                    ><a href="/cgi-bin/koha/catalogue/showmarc.pl?importid=[% breeding_loo.id | uri %]" class="dropdown-item previewData"><i class="fa-solid fa-eye"></i> MARC preview</a></li
241
                                                >
241
                                                >
242
                                                <li
242
                                                <li
243
                                                    ><a href="/cgi-bin/koha/catalogue/showmarc.pl?viewas=card&amp;importid=[% breeding_loo.id | uri %]" class="previewData"><i class="fa-solid fa-eye"></i> Card preview</a></li
243
                                                    ><a href="/cgi-bin/koha/catalogue/showmarc.pl?viewas=card&amp;importid=[% breeding_loo.id | uri %]" class="dropdown-item previewData"><i class="fa-solid fa-eye"></i> Card preview</a></li
244
                                                >
244
                                                >
245
                                                [% IF ( CAN_user_editcatalogue_edit_catalogue ) %]
245
                                                [% IF ( CAN_user_editcatalogue_edit_catalogue ) %]
246
                                                    <li
246
                                                    <li
247
                                                        ><a href="/cgi-bin/koha/cataloguing/addbiblio.pl?breedingid=[% breeding_loo.id | uri %]"><i class="fa fa-plus"></i> Add biblio</a></li
247
                                                        ><a class="dropdown-item" href="/cgi-bin/koha/cataloguing/addbiblio.pl?breedingid=[% breeding_loo.id | uri %]"><i class="fa fa-plus"></i> Add biblio</a></li
248
                                                    >
248
                                                    >
249
                                                [% END %]
249
                                                [% END %]
250
                                            </ul>
250
                                            </ul>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt (+2 lines)
Lines 88-93 Link Here
88
    <h1>Items for [% biblio.title | html %] [% IF ( biblio.author ) %]by [% biblio.author | html %][% END %] (Record #[% biblio.biblionumber | html %])</h1>
88
    <h1>Items for [% biblio.title | html %] [% IF ( biblio.author ) %]by [% biblio.author | html %][% END %] (Record #[% biblio.biblionumber | html %])</h1>
89
    <a id="newitem_jump" href="#f"><i class="fa fa-arrow-down"></i> Jump to form</a>
89
    <a id="newitem_jump" href="#f"><i class="fa fa-arrow-down"></i> Jump to form</a>
90
90
91
    [% IF ( onloan_cannot_withdraw ) %]<div class="alert alert-warning"><strong>Error saving item</strong>: Onloan item cannot be withdrawn.</div>[% END %]
92
    [% IF ( intransit_cannot_withdraw ) %]<div class="alert alert-warning"><strong>Error saving item</strong>: In transit item cannot be withdrawn.</div>[% END %]
91
    [% IF ( barcode_not_unique ) %]<div class="alert alert-warning"><strong>Error saving item</strong>: Barcode must be unique.</div>[% END %]
93
    [% IF ( barcode_not_unique ) %]<div class="alert alert-warning"><strong>Error saving item</strong>: Barcode must be unique.</div>[% END %]
92
    [% IF ( no_next_barcode ) %]<div class="alert alert-warning"><strong>Error saving items</strong>: Unable to automatically determine values for barcodes. No item has been inserted.</div>[% END %]
94
    [% IF ( no_next_barcode ) %]<div class="alert alert-warning"><strong>Error saving items</strong>: Unable to automatically determine values for barcodes. No item has been inserted.</div>[% END %]
93
    [% IF ( book_on_loan ) %]<div class="alert alert-warning"><strong>Cannot delete</strong>: item is checked out.</div>[% END %]
95
    [% IF ( book_on_loan ) %]<div class="alert alert-warning"><strong>Cannot delete</strong>: item is checked out.</div>[% END %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181a.tt (-1 / +1 lines)
Lines 18-24 Link Here
18
    <div class="page-section">
18
    <div class="page-section">
19
        <table>
19
        <table>
20
            <tr>
20
            <tr>
21
                <td><label for="f1">Content Form </label></td>
21
                <td><label for="f1">Content form </label></td>
22
                <td>
22
                <td>
23
                    <select name="f1" id="f1">
23
                    <select name="f1" id="f1">
24
                        [% IF ( f1 == "a" ) %]
24
                        [% IF ( f1 == "a" ) %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181b.tt (-6 / +6 lines)
Lines 18-24 Link Here
18
    <div class="page-section">
18
    <div class="page-section">
19
        <table>
19
        <table>
20
            <tr>
20
            <tr>
21
                <td><label for="f1">Specification of Type </label></td>
21
                <td><label for="f1">Specification of type </label></td>
22
                <td>
22
                <td>
23
                    <select name="f1" id="f1">
23
                    <select name="f1" id="f1">
24
                        [% IF ( f1 == "a" ) %]
24
                        [% IF ( f1 == "a" ) %]
Lines 54-60 Link Here
54
                </td>
54
                </td>
55
            </tr>
55
            </tr>
56
            <tr>
56
            <tr>
57
                <td><label for="f2">Specification of Motion </label></td>
57
                <td><label for="f2">Specification of motion </label></td>
58
                <td>
58
                <td>
59
                    <select name="f2" id="f2">
59
                    <select name="f2" id="f2">
60
                        [% IF ( f2 == "a" ) %]
60
                        [% IF ( f2 == "a" ) %]
Lines 84-90 Link Here
84
                </td>
84
                </td>
85
            </tr>
85
            </tr>
86
            <tr>
86
            <tr>
87
                <td><label for="f3">Specification of Dimensionality </label></td>
87
                <td><label for="f3">Specification of dimensionality </label></td>
88
                <td>
88
                <td>
89
                    <select name="f3" id="f3">
89
                    <select name="f3" id="f3">
90
                        [% IF ( f3 == "a" ) %]
90
                        [% IF ( f3 == "a" ) %]
Lines 114-120 Link Here
114
                </td>
114
                </td>
115
            </tr>
115
            </tr>
116
            <tr>
116
            <tr>
117
                <td><label for="f4">Sensory Specification 1 </label></td>
117
                <td><label for="f4">Sensory specification 1 </label></td>
118
                <td>
118
                <td>
119
                    <select name="f4" id="f4">
119
                    <select name="f4" id="f4">
120
                        [% IF ( f4 == "a" ) %]
120
                        [% IF ( f4 == "a" ) %]
Lines 156-162 Link Here
156
                </td>
156
                </td>
157
            </tr>
157
            </tr>
158
            <tr>
158
            <tr>
159
                <td><label for="f5">Sensory Specification 2 </label></td>
159
                <td><label for="f5">Sensory specification 2 </label></td>
160
                <td>
160
                <td>
161
                    <select name="f5" id="f5">
161
                    <select name="f5" id="f5">
162
                        [% IF ( f5 == "a" ) %]
162
                        [% IF ( f5 == "a" ) %]
Lines 198-204 Link Here
198
                </td>
198
                </td>
199
            </tr>
199
            </tr>
200
            <tr>
200
            <tr>
201
                <td><label for="f6">Sensory Specification 3 </label></td>
201
                <td><label for="f6">Sensory specification 3 </label></td>
202
                <td>
202
                <td>
203
                    <select name="f6" id="f6">
203
                    <select name="f6" id="f6">
204
                        [% IF ( f6 == "a" ) %]
204
                        [% IF ( f6 == "a" ) %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181c.tt (-1 / +1 lines)
Lines 18-24 Link Here
18
    <div class="page-section">
18
    <div class="page-section">
19
        <table>
19
        <table>
20
            <tr>
20
            <tr>
21
                <td><label for="f1">Content Type </label></td>
21
                <td><label for="f1">Content type </label></td>
22
                <td>
22
                <td>
23
                    <select name="f1" id="f1">
23
                    <select name="f1" id="f1">
24
                        [% IF ( f1 == "cri" ) %]
24
                        [% IF ( f1 == "cri" ) %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_182a.tt (-1 / +1 lines)
Lines 18-24 Link Here
18
    <div class="page-section">
18
    <div class="page-section">
19
        <table>
19
        <table>
20
            <tr>
20
            <tr>
21
                <td><label for="f1">Media Type Code </label></td>
21
                <td><label for="f1">Media type code </label></td>
22
                <td>
22
                <td>
23
                    <select name="f1" id="f1">
23
                    <select name="f1" id="f1">
24
                        [% IF ( f1 == "a" ) %]
24
                        [% IF ( f1 == "a" ) %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_182c.tt (-1 / +1 lines)
Lines 18-24 Link Here
18
    <div class="page-section">
18
    <div class="page-section">
19
        <table>
19
        <table>
20
            <tr>
20
            <tr>
21
                <td><label for="f1">Media Type </label></td>
21
                <td><label for="f1">Media type </label></td>
22
                <td>
22
                <td>
23
                    <select name="f1" id="f1">
23
                    <select name="f1" id="f1">
24
                        [% IF ( f1 == "s" ) %]
24
                        [% IF ( f1 == "s" ) %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_183a.tt (-5 / +5 lines)
Lines 19-25 Link Here
19
            <tr>
19
            <tr>
20
                <td>
20
                <td>
21
                    <select name="f1" id="f1">
21
                    <select name="f1" id="f1">
22
                        <optgroup label="Audio Carriers">
22
                        <optgroup label="Audio carriers">
23
                            [% IF ( f1 == "sg" ) %]
23
                            [% IF ( f1 == "sg" ) %]
24
                                <option value="sg" selected="selected">sg - audio cartridge</option>
24
                                <option value="sg" selected="selected">sg - audio cartridge</option>
25
                            [% ELSE %]
25
                            [% ELSE %]
Lines 81-87 Link Here
81
                            [% END %]
81
                            [% END %]
82
                        </optgroup>
82
                        </optgroup>
83
83
84
                        <optgroup label="Computer Carriers">
84
                        <optgroup label="Computer carriers">
85
                            [% IF ( f1 == "ck" ) %]
85
                            [% IF ( f1 == "ck" ) %]
86
                                <option value="ck" selected="selected">ck - computer card</option>
86
                                <option value="ck" selected="selected">ck - computer card</option>
87
                            [% ELSE %]
87
                            [% ELSE %]
Lines 137-143 Link Here
137
                            [% END %]
137
                            [% END %]
138
                        </optgroup>
138
                        </optgroup>
139
139
140
                        <optgroup label="Microform Carriers">
140
                        <optgroup label="Microform carriers">
141
                            [% IF ( f1 == "ha" ) %]
141
                            [% IF ( f1 == "ha" ) %]
142
                                <option value="ha" selected="selected">ha - aperture card</option>
142
                                <option value="ha" selected="selected">ha - aperture card</option>
143
                            [% ELSE %]
143
                            [% ELSE %]
Lines 199-205 Link Here
199
                            [% END %]
199
                            [% END %]
200
                        </optgroup>
200
                        </optgroup>
201
201
202
                        <optgroup label="Microscopic Carriers">
202
                        <optgroup label="Microscopic carriers">
203
                            [% IF ( f1 == "pp" ) %]
203
                            [% IF ( f1 == "pp" ) %]
204
                                <option value="pp" selected="selected">pp - microscope slide</option>
204
                                <option value="pp" selected="selected">pp - microscope slide</option>
205
                            [% ELSE %]
205
                            [% ELSE %]
Lines 275-281 Link Here
275
                            [% END %]
275
                            [% END %]
276
                        </optgroup>
276
                        </optgroup>
277
277
278
                        <optgroup label="Stereographic Carriers">
278
                        <optgroup label="Stereographic carriers">
279
                            [% IF ( f1 == "eh" ) %]
279
                            [% IF ( f1 == "eh" ) %]
280
                                <option value="eh" selected="selected">eh - stereograph card</option>
280
                                <option value="eh" selected="selected">eh - stereograph card</option>
281
                            [% ELSE %]
281
                            [% ELSE %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt (-12 / +17 lines)
Lines 524-530 Link Here
524
    [% INCLUDE 'datatables.inc' %]
524
    [% INCLUDE 'datatables.inc' %]
525
    <script>
525
    <script>
526
        var active_tab = "#article-requests-requested_panel";
526
        var active_tab = "#article-requests-requested_panel";
527
        var last_cancel_reason, requested_datatable, pending_datatable, processing_datatable, active_datatable;
527
        var last_cancel_reason, requested_datatable, pending_datatable, processing_datatable, active_datatable, requested_dt, pending_dt, processing_dt;
528
        $(document).ready(function() {
528
        $(document).ready(function() {
529
529
530
            $("#article-request-tabs a[data-bs-toggle='tab']").on("shown.bs.tab", function (e) {
530
            $("#article-request-tabs a[data-bs-toggle='tab']").on("shown.bs.tab", function (e) {
Lines 575-582 Link Here
575
            });
575
            });
576
576
577
            requested_datatable = $("#article-requests-requested-table").kohaTable();
577
            requested_datatable = $("#article-requests-requested-table").kohaTable();
578
            requested_dt = requested_datatable.DataTable();
579
578
            pending_datatable = $("#article-requests-pending-table").kohaTable();
580
            pending_datatable = $("#article-requests-pending-table").kohaTable();
581
            pending_dt = pending_datatable.DataTable();
582
579
            processing_datatable = $("#article-requests-processing-table").kohaTable();
583
            processing_datatable = $("#article-requests-processing-table").kohaTable();
584
            processing_dt = processing_datatable.DataTable();
585
580
            active_datatable = requested_datatable;
586
            active_datatable = requested_datatable;
581
            activateBatchActions( active_tab );
587
            activateBatchActions( active_tab );
582
        });
588
        });
Lines 634-645 Link Here
634
640
635
641
636
                a.closest('td').prepend('<img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif"/>').find('div.dropdown').hide();
642
                a.closest('td').prepend('<img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif"/>').find('div.dropdown').hide();
637
638
                $.ajax({
643
                $.ajax({
639
                    type: "DELETE",
644
                    type: "DELETE",
640
                    url: '/api/v1/article_requests/'+id+query,
645
                    url: '/api/v1/article_requests/'+id+query,
641
                    success: function( data ) {
646
                    success: function( data ) {
642
                        active_datatable.row( a.closest('tr') ).remove().draw();
647
                        active_datatable.DataTable().row( table_row ).remove().draw();
643
                        UpdateTabCounts();
648
                        UpdateTabCounts();
644
                        activateBatchActions( active_tab );
649
                        activateBatchActions( active_tab );
645
                    }
650
                    }
Lines 674-680 Link Here
674
        async function Process( id, a ) {
679
        async function Process( id, a ) {
675
            var table_row = a.closest('tr');
680
            var table_row = a.closest('tr');
676
            var table = a.closest('table');
681
            var table = a.closest('table');
677
            var orig_datatable = table.attr('id')==='article-requests-pending-table'?pending_datatable:requested_datatable;
682
            var orig_dt = table.attr('id')==='article-requests-pending-table'?pending_dt:requested_dt;
678
683
679
            a.closest('td').prepend('<img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" class="spinner"/>').find('div.dropdown').hide();
684
            a.closest('td').prepend('<img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" class="spinner"/>').find('div.dropdown').hide();
680
685
Lines 684-691 Link Here
684
                    $("img.spinner").remove();
689
                    $("img.spinner").remove();
685
                    table_row.find('.ar-process-request').remove();
690
                    table_row.find('.ar-process-request').remove();
686
                    table_row.find('input[type="checkbox"]').prop('checked', false);
691
                    table_row.find('input[type="checkbox"]').prop('checked', false);
687
                    orig_datatable.row( table_row ).remove().draw();
692
                    orig_dt.row( table_row ).remove().draw();
688
                    processing_datatable.row.add( table_row ).draw();
693
                    processing_dt.row.add( table_row ).draw();
689
                    UpdateTabCounts();
694
                    UpdateTabCounts();
690
                    activateBatchActions( active_tab );
695
                    activateBatchActions( active_tab );
691
                },
696
                },
Lines 711-717 Link Here
711
            await client.articleRequests.complete(id).then(
716
            await client.articleRequests.complete(id).then(
712
                success => {
717
                success => {
713
                    $("img.spinner").remove();
718
                    $("img.spinner").remove();
714
                    active_datatable.row( a.closest('tr') ).remove().draw();
719
                    active_datatable.DataTable().row( a.closest('tr') ).remove().draw();
715
                    UpdateTabCounts();
720
                    UpdateTabCounts();
716
                    activateBatchActions( active_tab );
721
                    activateBatchActions( active_tab );
717
                },
722
                },
Lines 734-741 Link Here
734
            await client.articleRequests.pending(id).then(
739
            await client.articleRequests.pending(id).then(
735
                success => {
740
                success => {
736
                    $("img.spinner").remove();
741
                    $("img.spinner").remove();
737
                    requested_datatable.row( table_row ).remove().draw();
742
                    requested_dt.row( table_row ).remove().draw();
738
                    pending_datatable.row.add( table_row ).draw();
743
                    pending_dt.row.add( table_row ).draw();
739
                    UpdateTabCounts();
744
                    UpdateTabCounts();
740
                    activateBatchActions( active_tab );
745
                    activateBatchActions( active_tab );
741
                },
746
                },
Lines 750-758 Link Here
750
        });
755
        });
751
756
752
        function UpdateTabCounts() {
757
        function UpdateTabCounts() {
753
            $("#ar_requested_count").html( requested_datatable.rows().count() );
758
            $("#ar_requested_count").html( requested_dt.rows().count() );
754
            $("#ar_pending_count").html( pending_datatable.rows().count() );
759
            $("#ar_pending_count").html( pending_dt.rows().count() );
755
            $("#ar_processing_count").html( processing_datatable.rows().count() );
760
            $("#ar_processing_count").html( processing_dt.rows().count() );
756
        }
761
        }
757
762
758
        function EditURLs(id) {
763
        function EditURLs(id) {
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/branchoverdues.tt (-1 / +1 lines)
Lines 23-29 Link Here
23
            <a href="/cgi-bin/koha/circ/circulation-home.pl">Circulation</a>
23
            <a href="/cgi-bin/koha/circ/circulation-home.pl">Circulation</a>
24
        [% END %]
24
        [% END %]
25
        [% WRAPPER breadcrumb_item bc_active= 1 %]
25
        [% WRAPPER breadcrumb_item bc_active= 1 %]
26
            [% tx('Overdues at with fines {library}', { library = Branches.GetLoggedInBranchname }) %]
26
            [% tx('Overdues with fines at {library}', { library = Branches.GetLoggedInBranchname }) %]
27
        [% END %]
27
        [% END %]
28
    [% END #/ WRAPPER breadcrumbs %]
28
    [% END #/ WRAPPER breadcrumbs %]
29
[% END #/ WRAPPER sub-header.inc %]
29
[% END #/ WRAPPER sub-header.inc %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/pendingreserves.tt (-1 / +1 lines)
Lines 298-304 Link Here
298
            let options = [... new Set(column
298
            let options = [... new Set(column
299
                .data()
299
                .data()
300
                .toArray()
300
                .toArray()
301
                .map(d => d.replace(regex, '').trim().split(/\n/gi).flat())
301
                .map(d => d.replace(regex, '').trim().split(/\n/gi).map(s => s.trim()).flat())
302
                .flat()
302
                .flat()
303
                .sort())];
303
                .sort())];
304
304
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/transferstoreceive.tt (+3 lines)
Lines 78-83 Link Here
78
                                                    [% BLOCK subject %]Hold:[% END %]
78
                                                    [% BLOCK subject %]Hold:[% END %]
79
                                                    <a href="mailto:[% reser.patron.notice_email_address | uri %]?subject=[% INCLUDE subject %] [% reser.title | uri %]"> [% reser.patron.notice_email_address | html %] </a>
79
                                                    <a href="mailto:[% reser.patron.notice_email_address | uri %]?subject=[% INCLUDE subject %] [% reser.title | uri %]"> [% reser.patron.notice_email_address | html %] </a>
80
                                                [% END %]
80
                                                [% END %]
81
                                                [% IF reser.patron.cardnumber %]
82
                                                    <div class="cardnumber">Card number: [% reser.patron.cardnumber | html %]</div>
83
                                                [% END %]
81
                                            [% ELSIF ( reser.recall ) %]
84
                                            [% ELSIF ( reser.recall ) %]
82
                                                Recall requested by
85
                                                Recall requested by
83
                                                <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% reser.recall.patron_id | uri %]"
86
                                                <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% reser.recall.patron_id | uri %]"
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/waitingreserves.tt (-9 / +25 lines)
Lines 15-20 Link Here
15
    [% END %]</title
15
    [% END %]</title
16
>
16
>
17
[% INCLUDE 'doc-head-close.inc' %]
17
[% INCLUDE 'doc-head-close.inc' %]
18
[% FILTER collapse %]
19
    <style>
20
        .tab-toolbar {
21
            padding: 0.5rem 0;
22
            margin-bottom: 0.5rem;
23
            border-bottom: 1px solid #eee;
24
        }
25
    </style>
26
[% END %]
18
</head>
27
</head>
19
28
20
<body id="circ_waitingreserves" class="circ">
29
<body id="circ_waitingreserves" class="circ">
Lines 97-104 Link Here
97
            [% WRAPPER tab_panels %]
106
            [% WRAPPER tab_panels %]
98
                [% WRAPPER tab_panel tabname="holdswaiting" bt_active= 1 %]
107
                [% WRAPPER tab_panel tabname="holdswaiting" bt_active= 1 %]
99
                    [% IF ( reserveloop ) %]
108
                    [% IF ( reserveloop ) %]
100
                        <div id="toolbar" class="btn-toolbar">
109
                        <div class="tab-toolbar">
101
                            <button class="btn cancel_selected_holds" data-bulk="true"></button>
110
                            <button class="btn btn-default cancel_selected_holds" data-bulk="true"></button>
102
                        </div>
111
                        </div>
103
                        [% INCLUDE waiting_holds.inc table_name='holdst' reserveloop=reserveloop tab='holdwaiting' %]
112
                        [% INCLUDE waiting_holds.inc table_name='holdst' reserveloop=reserveloop tab='holdwaiting' %]
104
                    [% ELSE %]
113
                    [% ELSE %]
Lines 108-115 Link Here
108
117
109
                [% WRAPPER tab_panel tabname="holdsover" %]
118
                [% WRAPPER tab_panel tabname="holdsover" %]
110
                    [% IF ( overloop ) %]
119
                    [% IF ( overloop ) %]
111
                        <div id="toolbar" class="btn-toolbar">
120
                        <div class="tab-toolbar">
112
                            <button class="btn cancel_selected_holds" data-bulk="true"></button>
121
                            <button class="btn btn-default cancel_selected_holds" data-bulk="true"></button>
113
                            <div class="btn-group">
122
                            <div class="btn-group">
114
                                <form name="cancelAllReserve" action="waitingreserves.pl" method="post">
123
                                <form name="cancelAllReserve" action="waitingreserves.pl" method="post">
115
                                    [% INCLUDE 'csrf-token.inc' %]
124
                                    [% INCLUDE 'csrf-token.inc' %]
Lines 117-131 Link Here
117
                                    <input type="hidden" name="allbranches" value="[% allbranches | html %]" />
126
                                    <input type="hidden" name="allbranches" value="[% allbranches | html %]" />
118
                                    <input type="hidden" name="tab" value="holdsover" />
127
                                    <input type="hidden" name="tab" value="holdsover" />
119
                                    [% IF TransferWhenCancelAllWaitingHolds %]
128
                                    [% IF TransferWhenCancelAllWaitingHolds %]
120
                                        <input type="submit" class="btn btn-primary" value="Cancel and transfer all" />
129
                                        <button type="submit" class="btn btn-primary">Cancel and transfer all</button>
121
                                    [% ELSE %]
130
                                    [% ELSE %]
122
                                        <input type="submit" class="btn btn-primary" value="Cancel all" />
131
                                        <button type="submit" class="btn btn-primary">Cancel all</button>
123
                                    [% END %]
132
                                    [% END %]
124
                                </form>
133
                                </form>
125
                            </div>
134
                            </div>
126
                        </div>
135
                        </div>
127
                        [% UNLESS TransferWhenCancelAllWaitingHolds %]
136
                        [% UNLESS TransferWhenCancelAllWaitingHolds %]
128
                            <div class="hint">Only items that need not be transferred will be cancelled (TransferWhenCancelAllWaitingHolds syspref)</div>
137
                            <div class="hint"
138
                                >Only items that need not be transferred will be cancelled
139
                                [% IF ( CAN_user_parameters_manage_sysprefs ) %]
140
                                    (<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=TransferWhenCancelAllWaitingHolds">TransferWhenCancelAllWaitingHolds</a> system preference)
141
                                [% ELSE %]
142
                                    (TransferWhenCancelAllWaitingHolds system preference)
143
                                [% END %]
144
                            </div>
129
                        [% END %]
145
                        [% END %]
130
                        [% INCLUDE waiting_holds.inc table_name='holdso' reserveloop=overloop tab='holdsover' %]
146
                        [% INCLUDE waiting_holds.inc table_name='holdso' reserveloop=overloop tab='holdsover' %]
131
                    [% ELSE %]
147
                    [% ELSE %]
Lines 135-142 Link Here
135
151
136
                [% WRAPPER tab_panel tabname="holdscancelled" %]
152
                [% WRAPPER tab_panel tabname="holdscancelled" %]
137
                    [% IF cancel_reqs_count %]
153
                    [% IF cancel_reqs_count %]
138
                        <div id="toolbar" class="btn-toolbar">
154
                        <div class="tab-toolbar">
139
                            <button class="btn cancel_selected_holds" data-bulk="true"></button>
155
                            <button class="btn btn-default cancel_selected_holds" data-bulk="true"></button>
140
                        </div>
156
                        </div>
141
                        [% INCLUDE waiting_holds.inc select_column='1' table_name='holdscr' reserveloop=cancel_reqs tab='holdscr' %]
157
                        [% INCLUDE waiting_holds.inc select_column='1' table_name='holdscr' reserveloop=cancel_reqs tab='holdscr' %]
142
                    [% ELSE %]
158
                    [% ELSE %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/clubs/patron-enroll.tt (-1 / +1 lines)
Lines 2-8 Link Here
2
2
3
<h1 id="heading"> Enroll in <em>[% club.name | html %]</em> </h1>
3
<h1 id="heading"> Enroll in <em>[% club.name | html %]</em> </h1>
4
4
5
<div class="container">
5
<div class="container-fluid">
6
    <form method="get" id="patron-enrollment-form">
6
    <form method="get" id="patron-enrollment-form">
7
        <input type="hidden" name="id" value="[% club.id | html %]" />
7
        <input type="hidden" name="id" value="[% club.id | html %]" />
8
        <input type="hidden" name="borrowernumber" value="[% borrowernumber | html %]" />
8
        <input type="hidden" name="borrowernumber" value="[% borrowernumber | html %]" />
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/holdshistory.tt (-1 / +11 lines)
Lines 45-51 Link Here
45
45
46
    [% UNLESS Koha.Preference('IntranetReadingHistoryHolds') %]
46
    [% UNLESS Koha.Preference('IntranetReadingHistoryHolds') %]
47
        <div class="alert alert-warning">Staff members are not allowed to access patron's holds history</div>
47
        <div class="alert alert-warning">Staff members are not allowed to access patron's holds history</div>
48
    [% ELSIF is_anonymous %]
48
    [% ELSIF patron.is_anonymous %]
49
        <div class="alert alert-warning">This is the anonymous patron, so no holds history is displayed.</div>
49
        <div class="alert alert-warning">This is the anonymous patron, so no holds history is displayed.</div>
50
    [% ELSE %]
50
    [% ELSE %]
51
51
Lines 97-102 Link Here
97
                                <th>Requested item type</th>
97
                                <th>Requested item type</th>
98
                            [% END %]
98
                            [% END %]
99
                            <th>Status</th>
99
                            <th>Status</th>
100
                            <th>Notes</th>
100
                        </tr>
101
                        </tr>
101
                    </thead>
102
                    </thead>
102
                </table>
103
                </table>
Lines 141-146 Link Here
141
                                <th>Requested item type</th>
142
                                <th>Requested item type</th>
142
                            [% END %]
143
                            [% END %]
143
                            <th>Status</th>
144
                            <th>Status</th>
145
                            <th>Notes</th>
144
                        </tr>
146
                        </tr>
145
                    </thead>
147
                    </thead>
146
                </table>
148
                </table>
Lines 332-337 Link Here
332
                                return _("Pending");
334
                                return _("Pending");
333
                            }
335
                            }
334
                        },
336
                        },
337
                        {
338
                            data: "notes",
339
                            searchable: true,
340
                            orderable: true,
341
                            render: function (data, type, row, meta) {
342
                                return row.notes;
343
                            }
344
                        },
335
                    ],
345
                    ],
336
                }, table_settings, true, additional_filters);
346
                }, table_settings, true, additional_filters);
337
            }
347
            }
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member-flags.tt (-1 / +1 lines)
Lines 54-60 Link Here
54
                <a id="UncheckAllFlags" class="btn btn-link" href="#"> <i class="fa fa-times"></i> Clear all </a>
54
                <a id="UncheckAllFlags" class="btn btn-link" href="#"> <i class="fa fa-times"></i> Clear all </a>
55
55
56
                <div class="btn-group">
56
                <div class="btn-group">
57
                    <button id="copyPermissions" class="btn btn-link"><i class="fa fa-copy"></i> Copy settings</button>
57
                    <button id="copyPermissions" class="btn btn-link"><i class="fa fa-copy"></i> Copy permissions</button>
58
                    <button class="btn btn-default dropdown-toggle" data-bs-toggle="dropdown"><span class="caret"></span></button>
58
                    <button class="btn btn-default dropdown-toggle" data-bs-toggle="dropdown"><span class="caret"></span></button>
59
                    <ul class="dropdown-menu">
59
                    <ul class="dropdown-menu">
60
                        <li><a id="clearCopied" href="#">Forget copied permissions</a></li>
60
                        <li><a id="clearCopied" href="#">Forget copied permissions</a></li>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt (-2 / +2 lines)
Lines 489-500 Link Here
489
                                                            <li class="guarantor-details" data-borrowernumber="[% r.guarantor_id | $raw %]">
489
                                                            <li class="guarantor-details" data-borrowernumber="[% r.guarantor_id | $raw %]">
490
                                                                <span class="label">Guarantor:</span>
490
                                                                <span class="label">Guarantor:</span>
491
                                                                [% INCLUDE 'patron-title.inc' patron=r.guarantor hide_patron_infos_if_needed=1 %]
491
                                                                [% INCLUDE 'patron-title.inc' patron=r.guarantor hide_patron_infos_if_needed=1 %]
492
                                                                <input type="hidden" class="new_guarantor_id relation-[% r.id | html %]" name="new_guarantor_id" value="[% r.guarantor_id | html %]" />
492
                                                                <input type="hidden" class="guarantor_id relation-[% r.id | html %]" name="guarantor_id" value="[% r.guarantor_id | html %]" />
493
                                                            </li>
493
                                                            </li>
494
                                                            <li>
494
                                                            <li>
495
                                                                <span class="label">Relationship:</span>
495
                                                                <span class="label">Relationship:</span>
496
                                                                <span>[% r.relationship | html %]</span>
496
                                                                <span>[% r.relationship | html %]</span>
497
                                                                <input type="hidden" class="new_guarantor_relationship relation-[% r.id | html %]" name="new_guarantor_relationship" value="[% r.relationship | html %]" />
497
                                                                <input type="hidden" class="guarantor_relationship relation-[% r.id | html %]" name="guarantor_relationship" value="[% r.relationship | html %]" />
498
                                                            </li>
498
                                                            </li>
499
499
500
                                                            <li>
500
                                                            <li>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt (-1 / +1 lines)
Lines 50-56 Link Here
50
50
51
    [% INCLUDE 'members-toolbar.inc' %]
51
    [% INCLUDE 'members-toolbar.inc' %]
52
52
53
    [% IF is_anonymous %]
53
    [% IF patron.is_anonymous %]
54
        <div class="alert alert-warning">This is the anonymous patron.</div>
54
        <div class="alert alert-warning">This is the anonymous patron.</div>
55
    [% END %]
55
    [% END %]
56
56
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt (-8 / +39 lines)
Lines 3-8 Link Here
3
[% USE AuthorisedValues %]
3
[% USE AuthorisedValues %]
4
[% USE KohaDates %]
4
[% USE KohaDates %]
5
[% USE Koha %]
5
[% USE Koha %]
6
[% USE Branches %]
6
[% USE TablesSettings %]
7
[% USE TablesSettings %]
7
[% USE HtmlScrubber %]
8
[% USE HtmlScrubber %]
8
[% USE JSON.Escape %]
9
[% USE JSON.Escape %]
Lines 850-855 Link Here
850
                            <input type="hidden" name="reportname" value="[% reportname | html %]" />
851
                            <input type="hidden" name="reportname" value="[% reportname | html %]" />
851
                            <input type="hidden" name="group" value="[% group | html %]" />
852
                            <input type="hidden" name="group" value="[% group | html %]" />
852
                            <input type="hidden" name="subgroup" value="[% subgroup | html %]" />
853
                            <input type="hidden" name="subgroup" value="[% subgroup | html %]" />
854
                            <input type="hidden" name="branches" value="[% branches | html %] />
853
                            <input type="hidden" name="notes" value="[% notes | scrub_html type => 'note' | $raw %]" />
855
                            <input type="hidden" name="notes" value="[% notes | scrub_html type => 'note' | $raw %]" />
854
                            <input type="hidden" name="cache_expiry" value="[% cache_expiry | html %]" />
856
                            <input type="hidden" name="cache_expiry" value="[% cache_expiry | html %]" />
855
                            <input type="hidden" name="cache_expiry_units" value="[% cache_expiry_units | html %]" />
857
                            <input type="hidden" name="cache_expiry_units" value="[% cache_expiry_units | html %]" />
Lines 1434-1440 Link Here
1434
                                <span class="required">Required</span>
1436
                                <span class="required">Required</span>
1435
                            </div>
1437
                            </div>
1436
                        </fieldset>
1438
                        </fieldset>
1437
1439
                        [% IF ( Koha.Preference('EnableFilteringReports') ) %]
1440
                            <fieldset class="rows">
1441
                                <legend>Library limitation:</legend>
1442
                                <div
1443
                                ><label for="library_limitation">Library limitation: </label>
1444
                                   <select id="library_limitation" name="branches" multiple size="10">
1445
                                        [% PROCESS options_for_libraries libraries => Branches.all( selected => report.get_library_limits, unfiltered => 1, do_not_select_my_library => 1 ) %]
1446
                                    </select>
1447
                                    <div class="hint">Limits the use of this report to the selected libraries.</div>
1448
                                </div>
1449
                            </fieldset>
1450
                        [% END %]       
1438
                        <fieldset class="action">
1451
                        <fieldset class="action">
1439
                            <input type="hidden" name="op" value="cud-save" />
1452
                            <input type="hidden" name="op" value="cud-save" />
1440
                            <input type="submit" name="submit" class="btn btn-primary" value="Save report" />
1453
                            <input type="submit" name="submit" class="btn btn-primary" value="Save report" />
Lines 1548-1554 Link Here
1548
                            <br />
1561
                            <br />
1549
                            <span class="required" style="margin-left:30px;">Required</span>
1562
                            <span class="required" style="margin-left:30px;">Required</span>
1550
                        </fieldset>
1563
                        </fieldset>
1551
1564
                        [% IF ( Koha.Preference('EnableFilteringReports') ) %]
1565
                            <fieldset class="rows">
1566
                                <legend>Library limitation:</legend>
1567
                                <div
1568
                                ><label for="library_limitation">Library limitation: </label>
1569
                                   <select id="library_limitation" name="branches" multiple size="10">
1570
                                        [% PROCESS options_for_libraries libraries => Branches.all( selected => report.get_library_limits, unfiltered => 1, do_not_select_my_library => 1 ) %]
1571
                                    </select>
1572
                                    <div class="hint">Limits the use of this report to the selected libraries.</div>
1573
                                </div>
1574
                            </fieldset>
1575
                        [% END %]                        
1552
                        <fieldset class="action">
1576
                        <fieldset class="action">
1553
                            <button class="btn btn-primary" type="submit" name="op" value="cud-update_sql">Update SQL</button>
1577
                            <button class="btn btn-primary" type="submit" name="op" value="cud-update_sql">Update SQL</button>
1554
                            <button class="btn btn-default" type="submit" name="op" value="cud-update_and_run_sql">Update and run SQL</button>
1578
                            <button class="btn btn-default" type="submit" name="op" value="cud-update_and_run_sql">Update and run SQL</button>
Lines 1984-1989 Link Here
1984
        $(document).ready(function(){
2008
        $(document).ready(function(){
1985
2009
1986
            var activeTab = localStorage.getItem("sql_reports_activetab");
2010
            var activeTab = localStorage.getItem("sql_reports_activetab");
2011
            if( activeTab == 0 ){
2012
                $("#subgroup_filter_block").hide();
2013
            }
1987
2014
1988
            $("body").on('click',".fetch_chart_data",function(){
2015
            $("body").on('click',".fetch_chart_data",function(){
1989
                if( [% unlimited_total || 0 | $raw %] > 1000 ){
2016
                if( [% unlimited_total || 0 | $raw %] > 1000 ){
Lines 2492-2504 Link Here
2492
2519
2493
            if (g_id && g_id.length > 0) {
2520
            if (g_id && g_id.length > 0) {
2494
                col4.search(g_id, {exact:true}).visible(false);
2521
                col4.search(g_id, {exact:true}).visible(false);
2495
                for(var i in group_subgroups[g_id]) {
2522
                if( group_subgroups[g_id] && group_subgroups[g_id].length > 0 ){
2496
                    $("#subgroup_filter").append(
2523
                    for(var i in group_subgroups[g_id]) {
2497
                        '<option value="' + group_subgroups[g_id][i][0] + '">'
2524
                        $("#subgroup_filter").append(
2498
                        + group_subgroups[g_id][i][1] + '</option>'
2525
                            '<option value="' + group_subgroups[g_id][i][0] + '">'
2499
                    );
2526
                            + group_subgroups[g_id][i][1] + '</option>'
2527
                        );
2528
                    }
2529
                    $("#subgroup_filter_block").show();
2530
                } else {
2531
                    $("#subgroup_filter_block").hide();
2500
                }
2532
                }
2501
                $("#subgroup_filter_block").show();
2502
            } else {
2533
            } else {
2503
                $("#subgroup_filter_block").hide();
2534
                $("#subgroup_filter_block").hide();
2504
            }
2535
            }
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/issues_stats.tt (-21 / +51 lines)
Lines 1-6 Link Here
1
[% USE raw %]
1
[% USE raw %]
2
[% USE Koha %]
2
[% USE Koha %]
3
[% USE Branches %]
3
[% USE Branches %]
4
[% USE Categories %]
5
[% USE ItemTypes %]
6
[% USE AuthorisedValues %]
4
[% PROCESS 'i18n.inc' %]
7
[% PROCESS 'i18n.inc' %]
5
[% SET footerjs = 1 %]
8
[% SET footerjs = 1 %]
6
[% INCLUDE 'doc-head-open.inc' %]
9
[% INCLUDE 'doc-head-open.inc' %]
Lines 67-103 Link Here
67
                            [% END %]
70
                            [% END %]
68
                            [% SWITCH loopfilte.crit %]
71
                            [% SWITCH loopfilte.crit %]
69
                            [% CASE 0 %]
72
                            [% CASE 0 %]
70
                                <span>Period from [% loopfilte.filter | html %]</span>
73
                                <strong>Period from:</strong> [% loopfilte.filter | html %]
71
                            [% CASE 1 %]
74
                            [% CASE 1 %]
72
                                <span>Period to [% loopfilte.filter | html %]</span>
75
                                <strong>Period to:</strong> [% loopfilte.filter | html %]
73
                            [% CASE 2 %]
76
                            [% CASE 2 %]
74
                                <span>Patron category = [% loopfilte.filter | html %]</span>
77
                                <strong>Patron category:</strong> [% Categories.GetName( loopfilte.filter ) | html %]
75
                            [% CASE 3 %]
78
                            [% CASE 3 %]
76
                                <span>Item type = [% loopfilte.filter | html %]</span>
79
                                <strong>Item type:</strong> [% ItemTypes.GetDescription( loopfilte.filter ) | html %]
77
                            [% CASE 4 %]
80
                            [% CASE 4 %]
78
                                <span>Issuing library = [% loopfilte.filter | html %]</span>
81
                                <strong>Issuing library:</strong> [% Branches.GetName( loopfilte.filter ) | html %]
79
                            [% CASE 5 %]
82
                            [% CASE 5 %]
80
                                <span>Collection = [% loopfilte.filter | html %]</span>
83
                                <strong>Collection:</strong> [% AuthorisedValues.GetByCode( 'CCODE', loopfilte.filter ) | html %]
81
                            [% CASE 6 %]
84
                            [% CASE 6 %]
82
                                <span>Location = [% loopfilte.filter | html %]</span>
85
                                <strong>Location:</strong> [% AuthorisedValues.GetByCode( 'LOC', loopfilte.filter ) | html %]
83
                            [% CASE 7 %]
86
                            [% CASE 7 %]
84
                                <span>Item call number &gt;= [% loopfilte.filter | html %]</span>
87
                                <strong>Item call number &gt; [% loopfilte.filter | html %] </strong>
85
                            [% CASE 8 %]
88
                            [% CASE 8 %]
86
                                <span>Item call number &lt; [% loopfilte.filter | html %]</span>
89
                                <strong>Item call number &lt; [% loopfilte.filter | html %] </strong>
87
                            [% CASE 9 %]
90
                            [% CASE 9 %]
88
                                <span>Patron sort1 = [% loopfilte.filter | html %]</span>
91
                                <strong>Patron sort1:</strong> [% AuthorisedValues.GetByCode('Bsort1', loopfilte.filter ) | html %]
89
                            [% CASE 10 %]
92
                            [% CASE 10 %]
90
                                <span>Patron sort2 = [% loopfilte.filter | html %]</span>
93
                                <strong>Patron sort2:</strong> [% AuthorisedValues.GetByCode('Bsort2', loopfilte.filter ) | html %]
91
                            [% CASE 11 %]
94
                            [% CASE 11 %]
92
                                <span>Home library = [% loopfilte.filter | html %]</span>
95
                                <strong>Home library:</strong> [% Branches.GetName( loopfilte.filter ) | html %]
93
                            [% CASE 12 %]
96
                            [% CASE 12 %]
94
                                <span>Holding library = [% loopfilte.filter | html %]</span>
97
                                <strong>Holding library:</strong> [% Branches.GetName( loopfilte.filter ) | html %]
95
                            [% CASE 13 %]
98
                            [% CASE 13 %]
96
                                <span>Patron library = [% loopfilte.filter | html %]</span>
99
                                <strong>Patron library:</strong> [% Branches.GetName( loopfilte.filter ) | html %]
97
                            [% CASE 14 %]
100
                            [% CASE 14 %]
98
                                <span>Issuing library = [% loopfilte.filter | html %]</span>
101
                                <strong>Issuing library:</strong> [% Branches.GetName( loopfilte.filter ) | html %]
102
                            [% CASE "Event" %]
103
                                <strong>Event:</strong>
104
                                [% SWITCH ( loopfilte.filter ) %]
105
                                [% CASE "issue" %]
106
                                    <span>Checkout</span>
107
                                [% CASE "return" %]
108
                                    <span>Check-in</span>
109
                                [% CASE "renew" %]
110
                                    <span>Renewal</span>
111
                                [% CASE %]
112
                                    [% loopfilte.filter | html %]
113
                                [% END %]
114
                            [% CASE "Select Day" %]
115
                                <strong>Select day:</strong> [% loopfilte.filter | html %]
116
                            [% CASE "Select Month" %]
117
                                <strong>Select month:</strong> [% loopfilte.filter | html %]
118
                            [% CASE "Display by" %]
119
                                <strong>Group by:</strong>
120
                                [% SWITCH ( loopfilte.filter ) %]
121
                                [% CASE "1" %]
122
                                    <span>Day of week</span>
123
                                [% CASE "2" %]
124
                                    <span>Month</span>
125
                                [% CASE "3" %]
126
                                    <span>Year</span>
127
                                [% CASE "4" %]
128
                                    <span>Hour</span>
129
                                [% END %]
99
                            [% CASE %]
130
                            [% CASE %]
100
                                <span>[% loopfilte.crit | html %] = [% loopfilte.filter | html %]</span>
131
                                [% loopfilte.crit | html %]:
132
                                [% loopfilte.filter | html %]
101
                            [% END %]
133
                            [% END %]
102
                        </li>
134
                        </li>
103
                    [% END %]
135
                    [% END %]
Lines 336-346 Link Here
336
                            <td><input type="radio" name="Line" value="borrowers.branchcode" /></td>
368
                            <td><input type="radio" name="Line" value="borrowers.branchcode" /></td>
337
                            <td><input type="radio" name="Column" value="borrowers.branchcode" /></td>
369
                            <td><input type="radio" name="Column" value="borrowers.branchcode" /></td>
338
                            <td>
370
                            <td>
339
                                <select name="Filter" id="patronbranch">
371
                                <select name="Filter" id="patron_library">
340
                                    <option value="">&nbsp;</option>
372
                                    <option value=""> </option>
341
                                    [% FOREACH branch IN branchloop %]
373
                                    [% PROCESS options_for_libraries libraries => Branches.all() %]
342
                                        <option value="[% branch.value | html %]"> [% branch.branchname | html %] </option>
343
                                    [% END %]
344
                                </select>
374
                                </select>
345
                            </td>
375
                            </td>
346
                        </tr>
376
                        </tr>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/itemslost.tt (-1 / +1 lines)
Lines 83-89 Link Here
83
                            [% IF csv_profiles.count %]
83
                            [% IF csv_profiles.count %]
84
                                <th class="no-sort"></th>
84
                                <th class="no-sort"></th>
85
                            [% END %]
85
                            [% END %]
86
                            <th>Title</th>
86
                            <th class="anti-the">Title</th>
87
                            <th>Author</th>
87
                            <th>Author</th>
88
                            <th>Lost status</th>
88
                            <th>Lost status</th>
89
                            <th>Lost on</th>
89
                            <th>Lost on</th>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/orders_by_budget.tt (-1 / +1 lines)
Lines 58-64 Link Here
58
                            <th>Basket</th>
58
                            <th>Basket</th>
59
                            <th>Basket name</th>
59
                            <th>Basket name</th>
60
                            <th>Basket by</th>
60
                            <th>Basket by</th>
61
                            <th>Title</th>
61
                            <th class="anti-the">Title</th>
62
                            <th>Currency</th>
62
                            <th>Currency</th>
63
                            <th>List price</th>
63
                            <th>List price</th>
64
                            <th>RRP</th>
64
                            <th>RRP</th>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/reports-home.tt (-6 / +23 lines)
Lines 1-5 Link Here
1
[% USE raw %]
1
[% USE raw %]
2
[% USE Koha %]
2
[% USE Koha %]
3
[% USE AdditionalContents %]
3
[% PROCESS 'i18n.inc' %]
4
[% PROCESS 'i18n.inc' %]
4
[% INCLUDE 'doc-head-open.inc' %]
5
[% INCLUDE 'doc-head-open.inc' %]
5
<title
6
<title
Lines 109-120 Link Here
109
                            <li><a href="/cgi-bin/koha/reports/issues_avg_stats.pl">Average loan time</a></li>
110
                            <li><a href="/cgi-bin/koha/reports/issues_avg_stats.pl">Average loan time</a></li>
110
                            [% SET koha_version = Koha.Version %]
111
                            [% SET koha_version = Koha.Version %]
111
                            [% IF koha_version.development %]
112
                            [% IF koha_version.development %]
112
                                <li><a href="http://schema.koha-community.org/main" target="blank">Koha database schema</a></li>
113
                                <li><a href="https://schema.koha-community.org/main" target="blank">Koha database schema</a></li>
113
                            [% ELSE %]
114
                            [% ELSE %]
114
                                <li><a href="http://schema.koha-community.org/[% koha_version.major | uri %]_[% koha_version.minor | uri %]" target="blank">Koha database schema</a></li>
115
                                <li><a href="https://schema.koha-community.org/[% koha_version.major | uri %]_[% koha_version.minor | uri %]" target="blank">Koha database schema</a></li>
115
                            [% END %]
116
                            [% END %]
116
117
117
                            <li><a href="http://wiki.koha-community.org/wiki/SQL_Reports_Library" target="blank">Koha reports library</a></li>
118
                            <li><a href="https://wiki.koha-community.org/wiki/SQL_Reports_Library" target="blank">Koha reports library</a></li>
118
                        </ul>
119
                        </ul>
119
                    </div>
120
                    </div>
120
                    [% IF ( Koha.Preference('Mana') == 2 ) %]
121
                    [% IF ( Koha.Preference('Mana') == 2 ) %]
Lines 126-135 Link Here
126
                </div>
127
                </div>
127
            </div>
128
            </div>
128
            <!-- /.row -->
129
            <!-- /.row -->
130
131
            [% SET StaffReportsHome = AdditionalContents.get( location => "StaffReportsHome", lang => lang, library => Branches.GetLoggedInBranchcode || default_branch ) %]
132
133
            [% IF ( StaffReportsHome.content && StaffReportsHome.content.count > 0 ) %]
134
                <div class="row">
135
                    <div class="col-sm-12">
136
                        <div id="[% StaffReportsHome.location | html %]">
137
                            [% FOREACH n IN StaffReportsHome.content %]
138
                                <div class="[% n.lang | html %]_item">
139
                                    <div class="[% n.lang | html %]_body">[% n.content | $raw %]</div>
140
                                </div>
141
                            [% END %]
142
                        </div>
143
                        <!-- /#StaffReportsHome -->
144
                    </div>
145
                    <!-- /.col-sm-12 -->
146
                </div>
147
                <!-- /.row -->
148
            [% END # /IF StaffReportsHome %]
129
        </div>
149
        </div>
130
    </div>
150
    </div>
131
    <div class="row">
132
        <div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2" id="intranet-reports-home-html"> [% Koha.Preference('IntranetReportsHomeHTML') | $raw %] </div>
133
    </div>
134
</div>
151
</div>
135
[% INCLUDE 'intranet-bottom.inc' %]
152
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/serials/serials-search.tt (-13 / +11 lines)
Lines 136-154 Link Here
136
                                    [% IF closed %]
136
                                    [% IF closed %]
137
                                        <a class="btn btn-default btn-xs dropdown-toggle" id="closedsubactions[% subscription.subscriptionid | html %]" role="button" data-bs-toggle="dropdown" href="#"> Actions </a>
137
                                        <a class="btn btn-default btn-xs dropdown-toggle" id="closedsubactions[% subscription.subscriptionid | html %]" role="button" data-bs-toggle="dropdown" href="#"> Actions </a>
138
                                        <ul class="dropdown-menu" role="menu" aria-labelledby="closedsubactions[% subscription.subscriptionid | html %]">
138
                                        <ul class="dropdown-menu" role="menu" aria-labelledby="closedsubactions[% subscription.subscriptionid | html %]">
139
                                            [% IF ( routing && CAN_user_serials_routing ) %]
139
                                            [% UNLESS ( subscription.cannotedit ) %]
140
                                                [% UNLESS ( subscription.cannotedit ) %]
140
                                                <li>
141
                                                    <li>
141
                                                    <a
142
                                                        <a
142
                                                        class="dropdown-item"
143
                                                            class="dropdown-item"
143
                                                        href="/cgi-bin/koha/serials/serials-search.pl?subscriptionid=[% subscription.subscriptionid | uri %]&amp;op=reopen&amp;[% IF ( routing && CAN_user_serials_routing ) %]routing=[% subscription.routing | uri %]&amp;[% END %]searched=1&amp;title_filter=[% title_filter | uri %]&amp;ISSN_filter=[% ISSN_filter | uri %]&amp;EAN_filter=[% EAN_filter | uri %]&amp;published_filter=[% publisher_filter | uri %]&amp;bookseller_filter=[% bookseller_filter | uri %]&amp;branch_filter=[% branch_filter | uri %]"
144
                                                            href="/cgi-bin/koha/serials/serials-search.pl?subscriptionid=[% subscription.subscriptionid | uri %]&amp;op=reopen&amp;routing=[% subscription.routing | uri %]&amp;searched=1&amp;title_filter=[% title_filter | uri %]&amp;ISSN_filter=[% ISSN_filter | uri %]&amp;EAN_filter=[% EAN_filter | uri %]&amp;published_filter=[% publisher_filter | uri %]&amp;bookseller_filter=[% bookseller_filter | uri %]&amp;branch_filter=[% branch_filter | uri %]"
144
                                                        id="reopensub"
145
                                                            id="reopensub"
145
                                                    >
146
                                                        >
146
                                                        <i class="fa-solid fa-arrow-rotate-right"></i> Reopen</a
147
                                                            <i class="fa-solid fa-arrow-rotate-right"></i> Reopen</a
147
                                                    >
148
                                                        >
148
                                                </li>
149
                                                    </li>
149
                                            [% END %]
150
                                                [% END %]
151
                                            [% END # IF ( routing && CAN_user_serials_routing ) %]
152
150
153
                                            <li>
151
                                            <li>
154
                                                <a class="dropdown-item" href="/cgi-bin/koha/serials/serials-collection.pl?subscriptionid=[% subscription.subscriptionid | uri %]"><i class="fa-solid fa-rectangle-list"></i> Issue history</a>
152
                                                <a class="dropdown-item" href="/cgi-bin/koha/serials/serials-collection.pl?subscriptionid=[% subscription.subscriptionid | uri %]"><i class="fa-solid fa-rectangle-list"></i> Issue history</a>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/additional-contents.tt (-1 / +1 lines)
Lines 542-548 Link Here
542
                [% END %]
542
                [% END %]
543
            [% END %]
543
            [% END %]
544
        </optgroup>
544
        </optgroup>
545
        [% SET staff_available_options = [ 'IntranetmainUserblock', 'RoutingListNote', 'StaffAcquisitionsHome', 'StaffAuthoritiesHome', 'StaffCataloguingHome', 'StaffListsHome', 'StaffLoginInstructions', 'StaffPatronsHome', 'StaffPOSHome', 'StaffSerialsHome' ] %]
545
        [% SET staff_available_options = [ 'IntranetmainUserblock', 'StaffReportsHome', 'RoutingListNote', 'StaffAcquisitionsHome', 'StaffAuthoritiesHome', 'StaffCataloguingHome', 'StaffListsHome', 'StaffLoginInstructions', 'StaffPatronsHome', 'StaffPOSHome', 'StaffSerialsHome' ] %]
546
        <optgroup label="Staff interface">
546
        <optgroup label="Staff interface">
547
            [% FOREACH l IN staff_available_options.sort %]
547
            [% FOREACH l IN staff_available_options.sort %]
548
                [% IF l == location %]
548
                [% IF l == location %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_extend_due_dates.tt (+1 lines)
Lines 272-277 Link Here
272
                dom: "t",
272
                dom: "t",
273
                order: [],
273
                order: [],
274
                paging: false,
274
                paging: false,
275
                pageLength: -1,
275
            });
276
            });
276
277
277
            $("#extend_due_dates_form").on("submit", function (e) {
278
            $("#extend_due_dates_form").on("submit", function (e) {
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stage-marc-import.tt (-1 / +1 lines)
Lines 471-477 Link Here
471
                $('#fileuploadbutton').show();
471
                $('#fileuploadbutton').show();
472
                $("#fileuploadcancel").hide();
472
                $("#fileuploadcancel").hide();
473
                var filename=$('#fileToUpload').prop('files')[0].name;
473
                var filename=$('#fileToUpload').prop('files')[0].name;
474
                if( filename.match( new RegExp(/\.[^.]+xml$/) ) ) {
474
                if( filename.match( new RegExp(/[^.]+\.xml$/) ) ) {
475
                    $('#format').val('MARCXML');
475
                    $('#format').val('MARCXML');
476
                }
476
                }
477
                $("#processfile").show();
477
                $("#processfile").show();
(-)a/koha-tmpl/intranet-tmpl/prog/en/xslt/UNIMARCslimUtils.xsl (-2 / +2 lines)
Lines 158-164 Link Here
158
  <xsl:template name="tag_214_s">
158
  <xsl:template name="tag_214_s">
159
    <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='s']">
159
    <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='s']">
160
      <span class="results_summary tag_214_s">
160
      <span class="results_summary tag_214_s">
161
        <span class="label">Printing and/or Publishing Information Transcribed as Found in the Colophon: </span>
161
        <span class="label">Printing and/or publishing information transcribed as found in the colophon: </span>
162
        <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='s']">
162
        <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='s']">
163
          <xsl:value-of select="."/>
163
          <xsl:value-of select="."/>
164
          <xsl:choose>
164
          <xsl:choose>
Lines 177-183 Link Here
177
  <xsl:template name="tag_214_r">
177
  <xsl:template name="tag_214_r">
178
    <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='r']">
178
    <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='r']">
179
      <span class="results_summary tag_214_r">
179
      <span class="results_summary tag_214_r">
180
        <span class="label">Printing and/or Publishing Information Transcribed as Found in the Main Source of Information: </span>
180
        <span class="label">Printing and/or publishing information transcribed as found in the main source of information: </span>
181
        <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='r']">
181
        <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='r']">
182
          <xsl:value-of select="."/>
182
          <xsl:value-of select="."/>
183
          <xsl:choose>
183
          <xsl:choose>
(-)a/koha-tmpl/intranet-tmpl/prog/js/datatables.js (-23 / +32 lines)
Lines 1057-1081 function _dt_add_filters(table_node, table_dt, filters_options = {}) { Link Here
1057
                } else if (typeof filters_options[i] === "function") {
1057
                } else if (typeof filters_options[i] === "function") {
1058
                    filters_options[i] = filters_options[i](table_dt);
1058
                    filters_options[i] = filters_options[i](table_dt);
1059
                }
1059
                }
1060
                $(filters_options[i]).each(function () {
1060
                $(filters_options[i])
1061
                    let o = $(
1061
                    .filter(function () {
1062
                        '<option value="^%s$">%s</option>'.format(
1062
                        return this._id && this._str;
1063
                            this._id,
1063
                    })
1064
                            this._str
1064
                    .each(function () {
1065
                        )
1065
                        let optionValue =
1066
                    );
1066
                            table_dt.settings()[0].ajax !== null
1067
                    // Compare with lc, or selfreg won't match ^SELFREG$ for instance, see bug 32517
1067
                                ? `^${this._id}$`
1068
                    // This is only for category, we might want to apply it only in this case.
1068
                                : this._id;
1069
                    existing_search = existing_search.toLowerCase();
1069
                        let o = $(
1070
                    if (
1070
                            `<option value="${optionValue}">${this._str}</option>`
1071
                        existing_search === this._id ||
1071
                        );
1072
                        (existing_search &&
1072
1073
                            this._id.toLowerCase().match(existing_search))
1073
                        // Compare with lc, or selfreg won't match ^SELFREG$ for instance, see bug 32517
1074
                    ) {
1074
                        // This is only for category, we might want to apply it only in this case.
1075
                        o.prop("selected", "selected");
1075
                        existing_search = existing_search.toLowerCase();
1076
                    }
1076
                        if (
1077
                    o.appendTo(select);
1077
                            existing_search === this._id ||
1078
                });
1078
                            (existing_search &&
1079
                                this._id.toLowerCase().match(existing_search))
1080
                        ) {
1081
                            o.prop("selected", "selected");
1082
                        }
1083
                        o.appendTo(select);
1084
                    });
1079
                $(th).html(select);
1085
                $(th).html(select);
1080
            } else {
1086
            } else {
1081
                var title = $(th).text();
1087
                var title = $(th).text();
Lines 1122-1129 function _dt_add_delay_filters(table_dt, table_node) { Link Here
1122
    let col_input_search = DataTable.util.debounce(function (i, val) {
1128
    let col_input_search = DataTable.util.debounce(function (i, val) {
1123
        table_dt.column(i).search(val).draw();
1129
        table_dt.column(i).search(val).draw();
1124
    }, delay_ms);
1130
    }, delay_ms);
1125
    let col_select_search = DataTable.util.debounce(function (i, val) {
1131
    let col_select_search = DataTable.util.debounce(function (
1126
        table_dt.column(i).search(val, true, false).draw();
1132
        i,
1133
        val,
1134
        regex_search = true
1135
    ) {
1136
        table_dt.column(i).search(val, regex_search, false).draw();
1127
    }, delay_ms);
1137
    }, delay_ms);
1128
1138
1129
    $(table_node)
1139
    $(table_node)
Lines 1142-1149 function _dt_add_delay_filters(table_dt, table_node) { Link Here
1142
                .find("select")
1152
                .find("select")
1143
                .unbind()
1153
                .unbind()
1144
                .bind("keyup change", function () {
1154
                .bind("keyup change", function () {
1145
                    let value = this.value.length ? "^" + this.value + "$" : "";
1155
                    col_select_search(i, this.value, false);
1146
                    col_select_search(i, this.value);
1147
                });
1156
                });
1148
        });
1157
        });
1149
}
1158
}
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js (+2 lines)
Lines 13-18 import RecallAPIClient from "./recall-api-client.js"; Link Here
13
import SysprefAPIClient from "./system-preferences-api-client.js";
13
import SysprefAPIClient from "./system-preferences-api-client.js";
14
import TicketAPIClient from "./ticket-api-client.js";
14
import TicketAPIClient from "./ticket-api-client.js";
15
import AcquisitionAPIClient from "./acquisition-api-client.js";
15
import AcquisitionAPIClient from "./acquisition-api-client.js";
16
import DefaultAPIClient from "./default-api-client.js";
16
17
17
export const APIClient = {
18
export const APIClient = {
18
    article_request: new ArticleRequestAPIClient(HttpClient),
19
    article_request: new ArticleRequestAPIClient(HttpClient),
Lines 28-31 export const APIClient = { Link Here
28
    recall: new RecallAPIClient(HttpClient),
29
    recall: new RecallAPIClient(HttpClient),
29
    sysprefs: new SysprefAPIClient(HttpClient),
30
    sysprefs: new SysprefAPIClient(HttpClient),
30
    ticket: new TicketAPIClient(HttpClient),
31
    ticket: new TicketAPIClient(HttpClient),
32
    default: new DefaultAPIClient(HttpClient),
31
};
33
};
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/default-api-client.js (+19 lines)
Line 0 Link Here
1
export class DefaultAPIClient {
2
    constructor(HttpClient) {
3
        this.httpClient = new HttpClient({
4
            baseURL: "",
5
        });
6
    }
7
8
    get koha() {
9
        return {
10
            get: params => this.httpClient.get(params),
11
            getAll: params => this.httpClient.getAll(params),
12
            post: params => this.httpClient.post(params),
13
            put: params => this.httpClient.put(params),
14
            delete: params => this.httpClient.delete(params),
15
        };
16
    }
17
}
18
19
export default DefaultAPIClient;
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/http-client.js (-16 / +78 lines)
Lines 1-16 Link Here
1
function _ifDocumentAvailable(callback) {
2
    if (typeof document !== "undefined" && document.getElementById) {
3
        callback();
4
    }
5
}
6
1
class Dialog {
7
class Dialog {
2
    constructor(options = {}) {}
8
    constructor(options = {}) {}
3
9
10
    _appendMessage(type, message) {
11
        _ifDocumentAvailable(() => {
12
            const messagesContainer = document.getElementById("messages");
13
            if (!messagesContainer) {
14
                return;
15
            }
16
17
            const htmlString =
18
                `<div class="alert alert-${type}">%s</div>`.format(message);
19
            messagesContainer.insertAdjacentHTML("beforeend", htmlString);
20
        });
21
    }
22
4
    setMessage(message) {
23
    setMessage(message) {
5
        $("#messages").append(
24
        this._appendMessage("info", message);
6
            '<div class="alert alert-info">%s</div>'.format(message)
7
        );
8
    }
25
    }
9
26
10
    setError(error) {
27
    setError(error) {
11
        $("#messages").append(
28
        this._appendMessage("warning", error);
12
            '<div class="alert alert-warning">%s</div>'.format(error)
13
        );
14
    }
29
    }
15
}
30
}
16
31
Lines 22-28 class HttpClient { Link Here
22
            "Content-Type": "application/json;charset=utf-8",
37
            "Content-Type": "application/json;charset=utf-8",
23
            "X-Requested-With": "XMLHttpRequest",
38
            "X-Requested-With": "XMLHttpRequest",
24
        };
39
        };
25
        this.csrf_token = $('meta[name="csrf-token"]').attr("content");
40
        this.csrf_token = this._getCsrfToken(options);
41
    }
42
43
    _getCsrfToken(options) {
44
        let token = null;
45
        _ifDocumentAvailable(() => {
46
            const metaTag = document.querySelector('meta[name="csrf-token"]');
47
            if (metaTag) {
48
                token = metaTag.getAttribute("content");
49
            }
50
        });
51
        return token !== null ? token : options.csrfToken || null;
26
    }
52
    }
27
53
28
    async _fetchJSON(
54
    async _fetchJSON(
Lines 42-47 class HttpClient { Link Here
42
                const is_json = response.headers
68
                const is_json = response.headers
43
                    .get("content-type")
69
                    .get("content-type")
44
                    ?.includes("application/json");
70
                    ?.includes("application/json");
71
72
                if (return_response || !is_json) {
73
                    return response;
74
                }
75
45
                if (!response.ok) {
76
                if (!response.ok) {
46
                    return response.text().then(text => {
77
                    return response.text().then(text => {
47
                        let message;
78
                        let message;
Lines 57-65 class HttpClient { Link Here
57
                        throw new Error(message);
88
                        throw new Error(message);
58
                    });
89
                    });
59
                }
90
                }
60
                if (return_response || !is_json) {
61
                    return response;
62
                }
63
                return response.json();
91
                return response.json();
64
            })
92
            })
65
            .then(result => {
93
            .then(result => {
Lines 79-84 class HttpClient { Link Here
79
        return res;
107
        return res;
80
    }
108
    }
81
109
110
    get(params = {}) {
111
        return this._fetchJSON(
112
            params.endpoint,
113
            params.headers,
114
            {
115
                ...params.options,
116
                method: "GET",
117
            },
118
            params.return_response ?? false,
119
            params.mark_submitting ?? false
120
        );
121
    }
122
123
    getAll(params = {}) {
124
        let url =
125
            params.endpoint +
126
            "?" +
127
            new URLSearchParams({
128
                _per_page: -1,
129
                ...(params.params && params.params),
130
                ...(params.query && { q: JSON.stringify(params.query) }),
131
            });
132
        return this._fetchJSON(
133
            url,
134
            params.headers,
135
            {
136
                ...params.options,
137
                method: "GET",
138
            },
139
            params.return_response ?? false,
140
            params.mark_submitting ?? false
141
        );
142
    }
143
82
    post(params = {}) {
144
    post(params = {}) {
83
        const body = params.body
145
        const body = params.body
84
            ? typeof params.body === "string"
146
            ? typeof params.body === "string"
Lines 95-102 class HttpClient { Link Here
95
                body,
157
                body,
96
                method: "POST",
158
                method: "POST",
97
            },
159
            },
98
            false,
160
            params.return_response ?? false,
99
            true
161
            params.mark_submitting ?? true
100
        );
162
        );
101
    }
163
    }
102
164
Lines 116-123 class HttpClient { Link Here
116
                body,
178
                body,
117
                method: "PUT",
179
                method: "PUT",
118
            },
180
            },
119
            false,
181
            params.return_response ?? false,
120
            true
182
            params.mark_submitting ?? true
121
        );
183
        );
122
    }
184
    }
123
185
Lines 132-139 class HttpClient { Link Here
132
                ...params.options,
194
                ...params.options,
133
                method: "DELETE",
195
                method: "DELETE",
134
            },
196
            },
135
            true,
197
            params.return_response ?? true,
136
            true
198
            params.mark_submitting ?? true
137
        );
199
        );
138
    }
200
    }
139
}
201
}
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue (-1 / +1 lines)
Lines 121-127 export default { Link Here
121
                table_settings: this.agreement_table_settings,
121
                table_settings: this.agreement_table_settings,
122
                add_filters: true,
122
                add_filters: true,
123
                filters_options: {
123
                filters_options: {
124
                    1: () =>
124
                    2: () =>
125
                        this.vendors.map(e => {
125
                        this.vendors.map(e => {
126
                            e["_id"] = e["id"];
126
                            e["_id"] = e["id"];
127
                            e["_str"] = e["name"];
127
                            e["_str"] = e["name"];
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesList.vue (-3 / +3 lines)
Lines 75-88 export default { Link Here
75
                table_settings: this.license_table_settings,
75
                table_settings: this.license_table_settings,
76
                add_filters: true,
76
                add_filters: true,
77
                filters_options: {
77
                filters_options: {
78
                    1: () =>
78
                    2: () =>
79
                        this.vendors.map(e => {
79
                        this.vendors.map(e => {
80
                            e["_id"] = e["id"];
80
                            e["_id"] = e["id"];
81
                            e["_str"] = e["name"];
81
                            e["_str"] = e["name"];
82
                            return e;
82
                            return e;
83
                        }),
83
                        }),
84
                    3: () => this.map_av_dt_filter("av_license_types"),
84
                    4: () => this.map_av_dt_filter("av_license_types"),
85
                    4: () => this.map_av_dt_filter("av_license_statuses"),
85
                    5: () => this.map_av_dt_filter("av_license_statuses"),
86
                },
86
                },
87
                actions: {
87
                actions: {
88
                    0: ["show"],
88
                    0: ["show"],
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Vendors/VendorList.vue (-1 / +1 lines)
Lines 142-148 export default { Link Here
142
        },
142
        },
143
        doReceive({ id }, dt, event) {
143
        doReceive({ id }, dt, event) {
144
            event.preventDefault();
144
            event.preventDefault();
145
            window.open(`/cgi-bin/koha/acqui/parcels.pl?booksellerid=${id}`);
145
            window.location.href = `/cgi-bin/koha/acqui/parcels.pl?booksellerid=${id}`;
146
        },
146
        },
147
        doEdit({ id }, dt, event) {
147
        doEdit({ id }, dt, event) {
148
            this.$router.push({
148
            this.$router.push({
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/http-client.js (-15 / +33 lines)
Lines 59-68 class HttpClient { Link Here
59
    }
59
    }
60
60
61
    get(params = {}) {
61
    get(params = {}) {
62
        return this._fetchJSON(params.endpoint, params.headers, {
62
        return this._fetchJSON(
63
            ...params.options,
63
            params.endpoint,
64
            method: "GET",
64
            params.headers,
65
        });
65
            {
66
                ...params.options,
67
                method: "GET",
68
            },
69
            params.return_response ?? false,
70
            params.mark_submitting ?? false
71
        );
66
    }
72
    }
67
73
68
    getAll(params = {}) {
74
    getAll(params = {}) {
Lines 74-83 class HttpClient { Link Here
74
                ...(params.params && params.params),
80
                ...(params.params && params.params),
75
                ...(params.query && { q: JSON.stringify(params.query) }),
81
                ...(params.query && { q: JSON.stringify(params.query) }),
76
            });
82
            });
77
        return this._fetchJSON(url, params.headers, {
83
        return this._fetchJSON(
78
            ...params.options,
84
            url,
79
            method: "GET",
85
            params.headers,
80
        });
86
            {
87
                ...params.options,
88
                method: "GET",
89
            },
90
            params.return_response ?? false,
91
            params.mark_submitting ?? false
92
        );
81
    }
93
    }
82
94
83
    post(params = {}) {
95
    post(params = {}) {
Lines 96-103 class HttpClient { Link Here
96
                body,
108
                body,
97
                method: "POST",
109
                method: "POST",
98
            },
110
            },
99
            false,
111
            params.return_response ?? false,
100
            true
112
            params.mark_submitting ?? true
101
        );
113
        );
102
    }
114
    }
103
115
Lines 117-124 class HttpClient { Link Here
117
                body,
129
                body,
118
                method: "PUT",
130
                method: "PUT",
119
            },
131
            },
120
            false,
132
            params.return_response ?? false,
121
            true
133
            params.mark_submitting ?? true
122
        );
134
        );
123
    }
135
    }
124
136
Lines 133-146 class HttpClient { Link Here
133
                ...params.options,
145
                ...params.options,
134
                method: "DELETE",
146
                method: "DELETE",
135
            },
147
            },
136
            true,
148
            params.return_response ?? true,
137
            true
149
            params.mark_submitting ?? true
138
        );
150
        );
139
    }
151
    }
140
152
141
    count(params = {}) {
153
    count(params = {}) {
142
        let res;
154
        let res;
143
        return this._fetchJSON(params.endpoint, params.headers, {}, 1).then(
155
        return this._fetchJSON(
156
            params.endpoint,
157
            params.headers,
158
            {},
159
            params.return_response ?? true,
160
            params.mark_submitting ?? false
161
        ).then(
144
            response => {
162
            response => {
145
                if (response) {
163
                if (response) {
146
                    return response.headers.get("X-Total-Count");
164
                    return response.headers.get("X-Total-Count");
(-)a/koha-tmpl/opac-tmpl/bootstrap/css/src/_responsive.scss (+5 lines)
Lines 555-558 Link Here
555
    .koha_url_text {
555
    .koha_url_text {
556
        display: inline;
556
        display: inline;
557
    }
557
    }
558
559
    #patron-virtual-card {
560
        max-width: 500px;
561
        min-width: 300px;
562
    }
558
}
563
}
(-)a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss (-23 / +28 lines)
Lines 2940-2977 $star-selected: #EDB867; Link Here
2940
}
2940
}
2941
2941
2942
#patron-virtual-card {
2942
#patron-virtual-card {
2943
    border: 1px solid #BBB;
2944
    border-radius: 15px;
2945
    box-shadow: 5px 5px 6px rgba( 4, 0, 0, .2 );
2946
    margin: 1rem 0;
2947
    padding: 1rem;
2943
    width: 100%;
2948
    width: 100%;
2944
    min-width: 300px;
2949
}
2945
    max-width: 500px;
2950
2951
#image-container {
2952
    padding: 0 .5rem;
2953
}
2954
2955
#patron-image {
2946
    border-radius: 15px;
2956
    border-radius: 15px;
2947
    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.3);
2948
    display: flex;
2957
    display: flex;
2949
    flex-direction: column;
2958
    flex-direction: column;
2950
    align-items: flex-start;
2959
    flex-shrink: 1;
2960
    width: auto;
2961
}
2951
2962
2952
    #image-container {
2963
#card-details {
2953
        width: 100%;
2964
    display: flex;
2954
        #patron-image {
2965
    flex-direction: column;
2955
            height: 75%;
2966
    flex-grow: 1;
2956
            width: auto;
2967
    padding: 0 .5rem;
2957
            border-radius: 15px;
2968
}
2958
            padding: 0.5rem;
2959
        }
2960
    }
2961
2969
2962
    #barcode-container {
2970
#barcode-container {
2971
    margin-bottom: 1rem;
2972
2973
    svg {
2974
        display: block;
2963
        width: 100%;
2975
        width: 100%;
2964
        padding: 0.25rem 0.5rem 0.25rem 0.5rem;
2965
        &.qrcode {
2966
            width: 50%; // Set a smaller width for QR codes
2967
        }
2968
    }
2976
    }
2969
2977
2970
    #lib-container {
2978
    &.qrcode {
2971
        width: 100%;
2979
        width: 50%; // Set a smaller width for QR codes
2972
        #patron-lib {
2973
            padding-left: 0.5rem;
2974
        }
2975
    }
2980
    }
2976
}
2981
}
2977
2982
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc (-1 / +1 lines)
Lines 96-102 Link Here
96
                            instance.clear();
96
                            instance.clear();
97
                        })
97
                        })
98
                        .addClass("fa fa-fw fa-times")
98
                        .addClass("fa fa-fw fa-times")
99
                        .attr("aria-hidden", true)
99
                        .attr("aria-hidden", false)
100
                        .attr("aria-label", _("Clear date"))
100
                        .attr("aria-label", _("Clear date"))
101
                )
101
                )
102
                .keydown(function (e) {
102
                .keydown(function (e) {
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc (+1 lines)
Lines 312-317 Link Here
312
312
313
                                    [% IF ( Koha.Preference( 'OpacAddMastheadLibraryPulldown' ) == 1 && AllPublicBranches.size > 1 ) %]
313
                                    [% IF ( Koha.Preference( 'OpacAddMastheadLibraryPulldown' ) == 1 && AllPublicBranches.size > 1 ) %]
314
                                        <div class="col-sm col-md-3 col-lg-2 order-3 order-sm-4">
314
                                        <div class="col-sm col-md-3 col-lg-2 order-3 order-sm-4">
315
                                            <label for="select_library" class="visually-hidden">Search the catalog in:</label>
315
                                            <select name="limit" id="select_library" class="form-select">
316
                                            <select name="limit" id="select_library" class="form-select">
316
                                                <option value="">All libraries</option>
317
                                                <option value="">All libraries</option>
317
318
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc (-2 / +2 lines)
Lines 22-28 Link Here
22
    </div> <!-- / #wrapper in masthead.inc -->
22
    </div> <!-- / #wrapper in masthead.inc -->
23
    <!-- prettier-ignore-end -->
23
    <!-- prettier-ignore-end -->
24
24
25
    [% IF ( Koha.Preference('OPACReportProblem') && Koha.Preference('KohaAdminEmailAddress') ) || Koha.Preference( 'CookieConsent' ) || OpacKohaUrl || ( ( OpacLangSelectorMode == 'both' || OpacLangSelectorMode == 'footer') ) %]
25
    [% IF ( Koha.Preference('OPACReportProblem') && Koha.Preference('KohaAdminEmailAddress') ) || Koha.Preference( 'CookieConsent' ) || OpacKohaUrl || ( ( opaclanguagesdisplay ) && ( ! one_language_enabled ) && ( languages_loop ) && ( OpacLangSelectorMode == 'both' || OpacLangSelectorMode == 'footer') ) %]
26
        <footer id="changelanguage" class="navbar navbar-expand navbar-light bg-light noprint">
26
        <footer id="changelanguage" class="navbar navbar-expand navbar-light bg-light noprint">
27
            <div class="container-fluid">
27
            <div class="container-fluid">
28
                <div class="collapse navbar-collapse">
28
                <div class="collapse navbar-collapse">
Lines 49-55 Link Here
49
                    </div>
49
                    </div>
50
                    [% IF OpacKohaUrl %]
50
                    [% IF OpacKohaUrl %]
51
                        <div class="navbar-nav">
51
                        <div class="navbar-nav">
52
                            <a id="koha_url" class="nav-link koha_url" href="http://koha-community.org">
52
                            <a id="koha_url" class="nav-link koha_url" href="https://koha-community.org">
53
                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
53
                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
54
                                    <path
54
                                    <path
55
                                        fill="currentColor"
55
                                        fill="currentColor"
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/patron-restrictions.inc (+27 lines)
Line 0 Link Here
1
<li class="userdebarred blocker">
2
    <strong>Your account has been frozen</strong>
3
    <ul>
4
        [% FOREACH restriction IN logged_in_user.restrictions %]
5
            <li class="[% restriction.type.code | lower | html %]_restriction">
6
                <span class="restriction_expiration">
7
                    [% IF restriction.expiration %]
8
                        <strong>Restriction expiring [% restriction.expiration | $KohaDates %]</strong>
9
                    [% ELSE %]
10
                        <strong>Indefinite restriction</strong>
11
                    [% END %]
12
                </span>
13
                [% IF restriction.comment.search('OVERDUES_PROCESS') %]
14
                    <span class="restriction_detail">Restriction added by overdues process [% restriction.comment.remove('OVERDUES_PROCESS ') | $raw | html_line_break %]</span>
15
                [% ELSE %]
16
                    [% IF restriction.comment %]
17
                        <span class="restriction_detail">[%- restriction.comment | html_line_break -%]</span>
18
                    [% END %]
19
                [% END %]
20
            </li>
21
        [% END %]
22
        <li>
23
            <span>Usually the reason for freezing an account is old overdues or damage fees. If it appears that your account is clear, please contact the library.</span>
24
            <a href="/cgi-bin/koha/opac-account.pl">Check your charges page.</a> <a href="/cgi-bin/koha/opac-user.pl?opac-user-overdues=1">Check your overdues.</a>
25
        </li>
26
    </ul>
27
</li>
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/subtypes_unimarc.inc (-22 / +24 lines)
Lines 1-8 Link Here
1
<fieldset>
1
<fieldset>
2
    <legend>Coded fields</legend>
2
    <legend>Coded fields</legend>
3
    <p>
3
    <p>
4
        <label>Audience</label>
4
        <label for="audience">Audience</label>
5
        <select name="limit" class="subtype">
5
        <select id="audience" name="limit" class="subtype">
6
            <option value="" selected="selected">Any</option>
6
            <option value="" selected="selected">Any</option>
7
            <option value="aud:a">juvenile, general</option>
7
            <option value="aud:a">juvenile, general</option>
8
            <option value="aud:b">pre-primary (0-5)</option>
8
            <option value="aud:b">pre-primary (0-5)</option>
Lines 15-22 Link Here
15
        </select>
15
        </select>
16
    </p>
16
    </p>
17
    <p>
17
    <p>
18
        <label>Physical presentation</label>
18
        <label for="physical_presentation">Physical presentation</label>
19
        <select name="limit" class="subtype">
19
        <select id="physical_presentation" name="limit" class="subtype">
20
            <option value="" selected="selected">Any</option>
20
            <option value="" selected="selected">Any</option>
21
            <option value="Material-type:r">regular print</option>
21
            <option value="Material-type:r">regular print</option>
22
            <option value="Material-type:d">large print</option>
22
            <option value="Material-type:d">large print</option>
Lines 32-39 Link Here
32
        </select>
32
        </select>
33
    </p>
33
    </p>
34
    <p>
34
    <p>
35
        <label>Literary genre</label>
35
        <label for="literary_genre">Literary genre</label>
36
        <select name="limit" class="subtype">
36
        <select id="literary_genre" name="limit" class="subtype">
37
            <option value="" selected="selected">Any</option>
37
            <option value="" selected="selected">Any</option>
38
            <option value="Literature-Code:a">fiction</option>
38
            <option value="Literature-Code:a">fiction</option>
39
            <option value="Literature-Code:b">drama</option>
39
            <option value="Literature-Code:b">drama</option>
Lines 49-56 Link Here
49
        </select>
49
        </select>
50
    </p>
50
    </p>
51
    <p>
51
    <p>
52
        <label>Biography</label>
52
        <label for="biography">Biography </label>
53
        <select name="limit" class="subtype">
53
        <select id="biography" name="limit" class="subtype">
54
            <option value="">Any</option>
54
            <option value="">Any</option>
55
            <option value="Biography-code:y">not a biography</option>
55
            <option value="Biography-code:y">not a biography</option>
56
            <option value="Biography-code:a">autobiography</option>
56
            <option value="Biography-code:a">autobiography</option>
Lines 60-67 Link Here
60
        </select>
60
        </select>
61
    </p>
61
    </p>
62
    <p>
62
    <p>
63
        <label>Illustration</label>
63
        <label for="illustration">Illustration</label>
64
        <select name="limit" class="subtype">
64
        <select id="illustration" name="limit" class="subtype">
65
            <option value="">Any</option>
65
            <option value="">Any</option>
66
            <option value="Illustration-Code:a">illustrations</option>
66
            <option value="Illustration-Code:a">illustrations</option>
67
            <option value="Illustration-Code:b">maps</option>
67
            <option value="Illustration-Code:b">maps</option>
Lines 82-89 Link Here
82
        </select>
82
        </select>
83
    </p>
83
    </p>
84
    <p>
84
    <p>
85
        <label>Content</label>
85
        <label for="content">Content</label>
86
        <select name="limit" class="subtype">
86
        <select id="content" name="limit" class="subtype">
87
            <option value="">Any</option>
87
            <option value="">Any</option>
88
            <option value="ctype:a">bibliography</option>
88
            <option value="ctype:a">bibliography</option>
89
            <option value="ctype:b">catalogue</option>
89
            <option value="ctype:b">catalogue</option>
Lines 111-118 Link Here
111
        </select>
111
        </select>
112
    </p>
112
    </p>
113
    <p>
113
    <p>
114
        <label>Video types</label>
114
        <label for="video_types">Video types </label>
115
        <select name="limit" class="subtype">
115
        <select id="video_types" name="limit" class="subtype">
116
            <option value="">Any</option>
116
            <option value="">Any</option>
117
            <option value="Video-mt:a">motion picture</option>
117
            <option value="Video-mt:a">motion picture</option>
118
            <option value="Video-mt:b">visual projection</option>
118
            <option value="Video-mt:b">visual projection</option>
Lines 124-131 Link Here
124
<fieldset>
124
<fieldset>
125
    <legend>Serials</legend>
125
    <legend>Serials</legend>
126
    <p>
126
    <p>
127
        <label>Serial type</label>
127
        <label for="serial_type">Serial type </label>
128
        <select name="limit" class="subtype">
128
        <select id="serial_type" name="limit" class="subtype">
129
            <option value="">Any type</option>
129
            <option value="">Any type</option>
130
            <option value="Type-Of-Serial:a">Periodical</option>
130
            <option value="Type-Of-Serial:a">Periodical</option>
131
            <option value="Type-Of-Serial:b">Monographic series</option>
131
            <option value="Type-Of-Serial:b">Monographic series</option>
Lines 137-144 Link Here
137
        </select>
137
        </select>
138
    </p>
138
    </p>
139
    <p>
139
    <p>
140
        <label>Periodicity</label>
140
        <label for="periodicity">Periodicity</label>
141
        <select name="limit" class="subtype">
141
        <select id="periodicity" name="limit" class="subtype">
142
            <option value="">Any</option>
142
            <option value="">Any</option>
143
            <option value="Frequency-code:a">Daily</option>
143
            <option value="Frequency-code:a">Daily</option>
144
            <option value="Frequency-code:b">Semiweekly</option>
144
            <option value="Frequency-code:b">Semiweekly</option>
Lines 161-168 Link Here
161
        </select>
161
        </select>
162
    </p>
162
    </p>
163
    <p>
163
    <p>
164
        <label>Regularity</label>
164
        <label for="regularity">Regularity </label>
165
        <select name="limit" class="subtype">
165
        <select id="regularity" name="limit" class="subtype">
166
            <option value="">Any regularity</option>
166
            <option value="">Any regularity</option>
167
            <option value="Regularity-code:a">regular</option>
167
            <option value="Regularity-code:a">regular</option>
168
            <option value="Regularity-code:b">normalised irregular</option>
168
            <option value="Regularity-code:b">normalised irregular</option>
Lines 174-180 Link Here
174
174
175
<fieldset>
175
<fieldset>
176
    <legend>Picture</legend>
176
    <legend>Picture</legend>
177
    <select name="limit" class="subtype">
177
    <label for="graphics-type" class="sr-only">Graphics type</label>
178
    <select id="graphics-type" name="limit" class="subtype">
178
        <option value="">Any</option>
179
        <option value="">Any</option>
179
        <option value="Graphics-type:a">collage</option>
180
        <option value="Graphics-type:a">collage</option>
180
        <option value="Graphics-type:b">drawing</option>
181
        <option value="Graphics-type:b">drawing</option>
Lines 187-193 Link Here
187
        <option value="Graphics-type:k">technical drawing</option>
188
        <option value="Graphics-type:k">technical drawing</option>
188
        <option value="Graphics-type:z">other non-projected graphic type</option>
189
        <option value="Graphics-type:z">other non-projected graphic type</option>
189
    </select>
190
    </select>
190
    <select name="limit" class="subtype">
191
    <label for="graphics-support" class="sr-only">Graphics support</label>
192
    <select id="graphics-support" name="limit" class="subtype">
191
        <option value="">Any</option>
193
        <option value="">Any</option>
192
        <option value="Graphics-support:a">canvas</option>
194
        <option value="Graphics-support:a">canvas</option>
193
        <option value="Graphics-support:b">bristol board</option>
195
        <option value="Graphics-support:b">bristol board</option>
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc (-4 / +5 lines)
Lines 4-9 Link Here
4
    <div id="menu">
4
    <div id="menu">
5
        <ul>
5
        <ul>
6
            <li [% IF userview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-user.pl">Summary</a></li>
6
            <li [% IF userview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-user.pl">Summary</a></li>
7
8
            [% IF Koha.Preference( 'OPACVirtualCard' ) %]
9
                <li [% IF virtualcardview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-virtual-card.pl">Library card</a></li>
10
            [% END %]
11
7
            [% IF ( OPACFinesTab ) %]
12
            [% IF ( OPACFinesTab ) %]
8
                <li [% IF accountview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-account.pl">Charges</a></li>
13
                <li [% IF accountview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-account.pl">Charges</a></li>
9
            [% END %]
14
            [% END %]
Lines 76-85 Link Here
76
            [% IF logged_in_user.alert_subscriptions.count %]
81
            [% IF logged_in_user.alert_subscriptions.count %]
77
                <li [% IF alertsview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-alert-subscriptions.pl">Alert subscriptions ([% logged_in_user.alert_subscriptions.count | html %])</a></li>
82
                <li [% IF alertsview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-alert-subscriptions.pl">Alert subscriptions ([% logged_in_user.alert_subscriptions.count | html %])</a></li>
78
            [% END %]
83
            [% END %]
79
80
            [% IF Koha.Preference( 'OPACVirtualCard' ) %]
81
                <li [% IF virtualcardview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-virtual-card.pl">My virtual card</a></li>
82
            [% END %]
83
        </ul>
84
        </ul>
84
    </div>
85
    </div>
85
[% END %]
86
[% END %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/clubs/enroll.tt (-1 / +1 lines)
Lines 2-8 Link Here
2
2
3
<h2> Enroll in <em>[% club.name | html %]</em> </h2>
3
<h2> Enroll in <em>[% club.name | html %]</em> </h2>
4
4
5
<div class="container">
5
<div class="container-fluid">
6
    <form method="get" id="patron-enrollment-form">
6
    <form method="get" id="patron-enrollment-form">
7
        <legend class="sr-only">Enrollment</legend>
7
        <legend class="sr-only">Enrollment</legend>
8
        <input type="hidden" name="id" value="[% club.id | html %]" />
8
        <input type="hidden" name="id" value="[% club.id | html %]" />
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-addbybiblionumber.tt (-1 / +1 lines)
Lines 98-104 Link Here
98
                                    <legend>Add to a new list:</legend>
98
                                    <legend>Add to a new list:</legend>
99
                                    <label for="newvirtualshelf">List name:</label>
99
                                    <label for="newvirtualshelf">List name:</label>
100
                                    <input type="text" name="newvirtualshelf" id="newvirtualshelf" size="40" />
100
                                    <input type="text" name="newvirtualshelf" id="newvirtualshelf" size="40" />
101
                                    <label for="category">Category:</label>
101
                                    <label for="public">Category:</label>
102
                                    <select name="public" id="public">
102
                                    <select name="public" id="public">
103
                                        <option value="0">Private</option>
103
                                        <option value="0">Private</option>
104
                                        [% IF (OpacAllowPublicListCreation) %]
104
                                        [% IF (OpacAllowPublicListCreation) %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt (-1 / +1 lines)
Lines 103-109 Link Here
103
                                        <a class="expand-result" href="#" data-bs-toggle="collapse" aria-expanded="false" aria-controls="collapse"> </a>
103
                                        <a class="expand-result" href="#" data-bs-toggle="collapse" aria-expanded="false" aria-controls="collapse"> </a>
104
                                    </div>
104
                                    </div>
105
                                    <!-- /#heading.card-header -->
105
                                    <!-- /#heading.card-header -->
106
                                    <div id="collapse" class="collapse" aria-labelledby="heading" data-parent="#browse-searchresults">
106
                                    <div id="collapse" class="collapse" aria-labelledby="heading" data-bs-parent="#browse-searchresults">
107
                                        <div class="card-body"> </div>
107
                                        <div class="card-body"> </div>
108
                                    </div>
108
                                    </div>
109
                                    <!-- /#collapse.collapse -->
109
                                    <!-- /#collapse.collapse -->
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browser.tt (-1 / +1 lines)
Lines 85-91 Link Here
85
                            <!-- prettier-ignore-end -->
85
                            <!-- prettier-ignore-end -->
86
                    [% ELSE %]
86
                    [% ELSE %]
87
                        <div class="warning"
87
                        <div class="warning"
88
                            >The browser table is empty. this feature is not fully set-up. See the <a href="http://wiki.koha-community.org/wiki/Opac_browse_feature">Koha Wiki</a> for more information on what it does and how to configure
88
                            >The browser table is empty. this feature is not fully set-up. See the <a href="https://wiki.koha-community.org/wiki/Opac_browse_feature">Koha Wiki</a> for more information on what it does and how to configure
89
                            it.</div
89
                            it.</div
90
                        >
90
                        >
91
                    [% END # / IF have_hierarchy %]
91
                    [% END # / IF have_hierarchy %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt (-1 / +1 lines)
Lines 207-213 Link Here
207
                                        <li>
207
                                        <li>
208
                                            <label for="notesopac">Notes:</label>
208
                                            <label for="notesopac">Notes:</label>
209
                                            [% IF !request.completed %]
209
                                            [% IF !request.completed %]
210
                                                <textarea name="notesopac" rows="5" cols="50">[% request.notesopac | html %]</textarea>
210
                                                <textarea id="notesopac" name="notesopac" rows="5" cols="50">[% request.notesopac | html %]</textarea>
211
                                            [% ELSE %]
211
                                            [% ELSE %]
212
                                                [% request.notesopac | html %]
212
                                                [% request.notesopac | html %]
213
                                            [% END %]
213
                                            [% END %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt (-6 / +6 lines)
Lines 57-70 Link Here
57
                            [% END %]
57
                            [% END %]
58
                            <fieldset>
58
                            <fieldset>
59
                                [% IF consent.given_on %]
59
                                [% IF consent.given_on %]
60
                                    <input type="radio" name="check_[% consent_type | html %]" value="1" checked="checked" /> Yes<br />
60
                                    <label><input type="radio" name="check_[% consent_type | html %]" value="1" checked="checked" /> Yes</label><br />
61
                                    <input type="radio" name="check_[% consent_type | html %]" value="0" /> No
61
                                    <label><input type="radio" name="check_[% consent_type | html %]" value="0" /> No</label>
62
                                [% ELSIF consent.refused_on %]
62
                                [% ELSIF consent.refused_on %]
63
                                    <input type="radio" name="check_[% consent_type | html %]" value="1" /> Yes<br />
63
                                    <label> <input type="radio" name="check_[% consent_type | html %]" value="1" /> Yes</label><br />
64
                                    <input type="radio" name="check_[% consent_type | html %]" value="0" checked="checked" /> No
64
                                    <label><input type="radio" name="check_[% consent_type | html %]" value="0" checked="checked" /> No </label>
65
                                [% ELSE %]
65
                                [% ELSE %]
66
                                    <input type="radio" name="check_[% consent_type | html %]" value="1" /> Yes<br />
66
                                    <label><input type="radio" name="check_[% consent_type | html %]" value="1" /> Yes</label><br />
67
                                    <input type="radio" name="check_[% consent_type | html %]" value="0" /> No
67
                                    <label><input type="radio" name="check_[% consent_type | html %]" value="0" /> No</label>
68
                                [% END %]
68
                                [% END %]
69
                                [% IF consent.given_on %]
69
                                [% IF consent.given_on %]
70
                                    <p class="consent_info"><strong>Your consent was registered on [% consent.given_on | html %].</strong></p>
70
                                    <p class="consent_info"><strong>Your consent was registered on [% consent.given_on | html %].</strong></p>
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reserve.tt (-41 / +7 lines)
Lines 26-37 Link Here
26
        [% END %]
26
        [% END %]
27
    [% END #/ WRAPPER breadcrumbs %]
27
    [% END #/ WRAPPER breadcrumbs %]
28
28
29
    <div class="container">
29
    <div class="container-fluid">
30
        <div id="holds" class="maincontent">
30
        <div id="holds" class="maincontent">
31
            <h1>Placing a hold</h1>
31
            <h1>Placing a hold</h1>
32
            [% IF ( message ) %]
32
            [% IF ( message ) %]
33
                <div id="holdmessages" class="alert">
33
                <div id="holdmessages" class="alert alert-warning">
34
                    <p>Sorry, you cannot place holds.</p>
34
                    <h2>Sorry, you cannot place holds.</h2>
35
                    <ul>
35
                    <ul>
36
                        [% IF ( GNA ) %]
36
                        [% IF ( GNA ) %]
37
                            <li id="gna">
37
                            <li id="gna">
Lines 49-89 Link Here
49
                        [% END %]
49
                        [% END %]
50
50
51
                        [% IF ( debarred ) %]
51
                        [% IF ( debarred ) %]
52
                            <li id="debarred">
52
                            [% INCLUDE "patron-restrictions.inc" %]
53
                                Your account has been frozen.
54
                                [% IF debarred_comment %]
55
                                    Comment:<br />
56
                                    <span id="userdebarred_comment">
57
                                        <strong>
58
                                            [% IF debarred_comment.search('OVERDUES_PROCESS') %]
59
                                                Restriction added by overdues process [% debarred_comment.remove('OVERDUES_PROCESS ') | html_line_break %]
60
                                            [% ELSE %]
61
                                                [% FOR restriction IN logged_in_user.restrictions %]
62
                                                    <div class="patron_restriction">
63
                                                        [%- restriction.type.display_text | html -%][%- IF restriction.comment -%]
64
                                                            : <span class="restriction_comment">[% restriction.comment | html_line_break %]</span>, frozen until:
65
                                                            <span class="restriction_expiration">
66
                                                                [%- IF restriction.expiration -%]
67
                                                                    [%- restriction.expiration | $KohaDates -%]
68
                                                                [%- ELSE -%]
69
                                                                    <strong>Indefinite</strong>
70
                                                                [%- END -%]
71
                                                            </span>
72
                                                        [% END %]
73
                                                    </div>
74
                                                [% END %]
75
                                            [% END %]
76
                                        </strong>
77
                                    </span>
78
                                [% END %]
79
80
                                [% IF debarred_date && debarred_date != '9999-12-31' %]
81
                                    End date:
82
                                    <span id="userdebarred_date">[% debarred_date | $KohaDates %]</span>
83
                                [% END %]
84
                                <br /><em>Usually the reason for freezing an account is old overdues or damage fees. If shows your account to be clear, please contact the library.</em>
85
                                <a href="/cgi-bin/koha/opac-account.pl">Go to your charges page</a>
86
                            </li>
87
                        [% END %]
53
                        [% END %]
88
54
89
                        [% IF ( too_much_oweing ) %]
55
                        [% IF ( too_much_oweing ) %]
Lines 273-281 Link Here
273
                                                                [% END %]
239
                                                                [% END %]
274
                                                            </select>
240
                                                            </select>
275
                                                            [% IF at_least_one_library_not_available_for_pickup %]
241
                                                            [% IF at_least_one_library_not_available_for_pickup %]
276
                                                                <div class="at_least_one_library_not_available_note"
242
                                                                <div class="at_least_one_library_not_available_note">
277
                                                                    >Note: Library policy does not allow hold/pickup of an item available locally. Please come to the library to retrieve these items</div
243
                                                                    Note: Library policy does not allow hold/pickup of an item available locally. Please come to the library to retrieve these items
278
                                                                >
244
                                                                </div>
279
                                                            [% END %]
245
                                                            [% END %]
280
                                                        [% END # / UNLESS bibitemloo.holdable %]
246
                                                        [% END # / UNLESS bibitemloo.holdable %]
281
                                                    </li>
247
                                                    </li>
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt (-8 / +4 lines)
Lines 432-449 Link Here
432
                    [% END # / IF total %]
432
                    [% END # / IF total %]
433
433
434
                    [%# Display "Not finding what you're looking for" for suggestion or ILL %]
434
                    [%# Display "Not finding what you're looking for" for suggestion or ILL %]
435
                    [% IF suggestion || ( Koha.Preference( 'ILLModule' ) == 1 && ( loggedinusername || Koha.Preference( 'ILLOpacUnauthenticatedRequest') ) ) %]
435
                    [% IF Koha.Preference( 'suggestion' ) || Koha.Preference( 'ILLModule' ) %]
436
                        <div class="suggestion">
436
                        <div class="suggestion">
437
                            Not finding what you're looking for?
437
                            Not finding what you're looking for?
438
                            <ul>
438
                            <ul>
439
                                [% IF suggestion %]
439
                                [% IF Koha.Preference( 'suggestion' ) %]
440
                                    [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
440
                                    <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add_form">purchase suggestion</a></li>
441
                                        <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add_form">purchase suggestion</a></li>
442
                                    [% ELSIF loggedinusername %]
443
                                        <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add_form">purchase suggestion</a></li>
444
                                    [% END %]
445
                                [% END %]
441
                                [% END %]
446
                                [% IF Koha.Preference( 'ILLModule' ) == 1 && ( loggedinusername || Koha.Preference( 'ILLOpacUnauthenticatedRequest' ) ) %]
442
                                [% IF Koha.Preference( 'ILLModule' ) %]
447
                                    [% IF Koha.Preference( 'AutoILLBackendPriority' ) %]
443
                                    [% IF Koha.Preference( 'AutoILLBackendPriority' ) %]
448
                                        <li>Make an <a href="/cgi-bin/koha/opac-illrequests.pl?op=create&amp;backend=Standard">interlibrary loan request</a></li>
444
                                        <li>Make an <a href="/cgi-bin/koha/opac-illrequests.pl?op=create&amp;backend=Standard">interlibrary loan request</a></li>
449
                                    [% ELSE %]
445
                                    [% ELSE %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-suggestions.tt (-1 / +1 lines)
Lines 449-455 Link Here
449
                                                    [% END %]
449
                                                    [% END %]
450
                                                    <td>
450
                                                    <td>
451
                                                        <p>
451
                                                        <p>
452
                                                            <label for="id[% suggestions_loo.suggestionid | html %]">
452
                                                            <label for="id[% suggestion.suggestionid | html %]">
453
                                                                [% IF suggestion.biblionumber %]
453
                                                                [% IF suggestion.biblionumber %]
454
                                                                    <strong><a href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% suggestion.biblionumber | uri %]">[% suggestion.title | html %]</a></strong>
454
                                                                    <strong><a href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% suggestion.biblionumber | uri %]">[% suggestion.title | html %]</a></strong>
455
                                                                [% ELSE %]
455
                                                                [% ELSE %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-tags.tt (-3 / +3 lines)
Lines 109-115 Link Here
109
109
110
                    <form method="get" action="opac-tags.pl" class="row">
110
                    <form method="get" action="opac-tags.pl" class="row">
111
                        <div class="col-auto gx-2 ms-2 my-1">
111
                        <div class="col-auto gx-2 ms-2 my-1">
112
                            <label>
112
                            <label for="limit-tag">
113
                                [% IF Koha.Preference( 'opacuserlogin' ) == 1 %]
113
                                [% IF Koha.Preference( 'opacuserlogin' ) == 1 %]
114
                                    <span>Tags to show from other users:</span>
114
                                    <span>Tags to show from other users:</span>
115
                                [% ELSE %]
115
                                [% ELSE %]
Lines 118-124 Link Here
118
                            </label>
118
                            </label>
119
                        </div>
119
                        </div>
120
                        <div class="col-auto gx-2 my-1">
120
                        <div class="col-auto gx-2 my-1">
121
                            <input type="text" name="limit" class="form-control form-control-sm" maxlength="4" size="4" value="[% limit or '100' | html %]" />
121
                            <input id="limit-tag" type="text" name="limit" class="form-control form-control-sm" maxlength="4" size="4" value="[% limit or '100' | html %]" />
122
                        </div>
122
                        </div>
123
                        <div class="col-auto gx-2 my-1">
123
                        <div class="col-auto gx-2 my-1">
124
                            <input type="submit" value="OK" class="btn btn-sm btn-primary" />
124
                            <input type="submit" value="OK" class="btn btn-sm btn-primary" />
Lines 185-191 Link Here
185
                                                        data-title="[% MY_TAG.title | html %]"
185
                                                        data-title="[% MY_TAG.title | html %]"
186
                                                        data-tagname="[% MY_TAG.term | html %]"
186
                                                        data-tagname="[% MY_TAG.term | html %]"
187
                                                        data-tagid="[% MY_TAG.tag_id | html %]"
187
                                                        data-tagid="[% MY_TAG.tag_id | html %]"
188
                                                        aria-label="[% check_title | html %]"
188
                                                        aria-label="[% MY_TAG.term | html %]"
189
                                                    />
189
                                                    />
190
                                                </td>
190
                                                </td>
191
                                                <td class="tagterm">
191
                                                <td class="tagterm">
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-tags_subject.tt (-1 / +1 lines)
Lines 38-44 Link Here
38
                            <fieldset>
38
                            <fieldset>
39
                                <legend>Show</legend>
39
                                <legend>Show</legend>
40
                                <form class="form-inline" action="/cgi-bin/koha/opac-tags_subject.pl" method="get">
40
                                <form class="form-inline" action="/cgi-bin/koha/opac-tags_subject.pl" method="get">
41
                                    <p>up to <input type="text" name="number" value="[% number | html %]" size="4" maxlength="4" /> subjects <input type="submit" class="btn btn-primary" value="OK" /></p>
41
                                    <label>up to <input type="text" name="number" value="[% number | html %]" size="4" maxlength="4" /> </label> <label>subjects <input type="submit" class="btn btn-primary" value="OK" /></label>
42
                                </form>
42
                                </form>
43
                            </fieldset>
43
                            </fieldset>
44
44
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt (-32 / +4 lines)
Lines 167-202 Link Here
167
                                    [% IF ( discharge_available ) %]
167
                                    [% IF ( discharge_available ) %]
168
                                        <li id="discharged"> <strong>Please note:</strong> Your account is frozen because it has been discharged. <a href="/cgi-bin/koha/opac-discharge.pl?op=get">Get your discharge</a> </li>
168
                                        <li id="discharged"> <strong>Please note:</strong> Your account is frozen because it has been discharged. <a href="/cgi-bin/koha/opac-discharge.pl?op=get">Get your discharge</a> </li>
169
                                    [% ELSE %]
169
                                    [% ELSE %]
170
                                        <li id="userdebarred">
170
                                        [% INCLUDE "patron-restrictions.inc" %]
171
                                            <strong>Please note:</strong> Your account has been frozen.
172
                                            [% IF ( borrower.debarredcomment ) %]
173
                                                Comment:<br />
174
                                                <ul id="userdebarred_comment">
175
                                                    [% FOREACH restriction IN logged_in_user.restrictions %]
176
                                                        <li class="patron_restriction">
177
                                                            <strong>[%- restriction.type.display_text | html -%]: </strong>
178
                                                            [% IF restriction.comment.search('OVERDUES_PROCESS') %]
179
                                                                Restriction added by overdues process [% restriction.comment.remove('OVERDUES_PROCESS ') | html_line_break %]
180
                                                            [% ELSE %]
181
                                                                [%- IF restriction.comment -%]
182
                                                                    <span class="restriction_comment">[% restriction.comment | html_line_break %]</span>.
183
                                                                [%- END -%]
184
                                                            [% END %]
185
                                                            [%- IF restriction.expiration -%]
186
                                                                Account frozen until <span class="restriction_expiration">[%- restriction.expiration | $KohaDates -%]</span>
187
                                                            [%- ELSE -%]
188
                                                                <strong>Account frozen indefinitely</strong>
189
                                                            [%- END -%]
190
                                                        </li>
191
                                                    [% END %]
192
                                                </ul>
193
                                            [% END %]
194
195
                                            <p
196
                                                ><em>Usually the reason for freezing an account is old overdues or damage fees. If your account shows to be clear, please contact the library.</em>
197
                                                <a href="/cgi-bin/koha/opac-account.pl">Go to your charges page</a></p
198
                                            >
199
                                        </li>
200
                                    [% END %]
171
                                    [% END %]
201
                                [% END %]
172
                                [% END %]
202
                                [% IF ( borrower.gonenoaddress ) %]
173
                                [% IF ( borrower.gonenoaddress ) %]
Lines 1093-1100 Link Here
1093
        $(document).ready(function(){
1064
        $(document).ready(function(){
1094
            [% IF ( opac_user_holds ) %]
1065
            [% IF ( opac_user_holds ) %]
1095
                $("#opac-user-views a[href='#opac-user-holds_panel']").tab("show");
1066
                $("#opac-user-views a[href='#opac-user-holds_panel']").tab("show");
1096
            [% END %]
1067
            [% ELSIF ( opac_user_overdues ) %]
1097
            [% IF ( opac_user_article_requests ) %]
1068
                $("#opac-user-views a[href='#opac-user-overdues_panel']").tab("show");
1069
            [% ELSIF ( opac_user_article_requests ) %]
1098
                $("#opac-user-views a[href='#opac-user-article-requests_panel']").tab("show");
1070
                $("#opac-user-views a[href='#opac-user-article-requests_panel']").tab("show");
1099
            [% END %]
1071
            [% END %]
1100
            $('#article-requests-table caption .count').html(AR_CAPTION_COUNT.format('[% current_article_requests.size | html %]'));
1072
            $('#article-requests-table caption .count').html(AR_CAPTION_COUNT.format('[% current_article_requests.size | html %]'));
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-virtual-card.tt (-9 / +18 lines)
Lines 3-12 Link Here
3
[% USE Asset %]
3
[% USE Asset %]
4
[% USE Koha %]
4
[% USE Koha %]
5
[% USE Branches %]
5
[% USE Branches %]
6
[% USE KohaDates %]
6
[% SET OpacNav = AdditionalContents.get( location => "OpacNav", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %]
7
[% SET OpacNav = AdditionalContents.get( location => "OpacNav", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %]
7
[% SET OpacNavBottom = AdditionalContents.get( location => "OpacNavBottom", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %]
8
[% SET OpacNavBottom = AdditionalContents.get( location => "OpacNavBottom", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %]
8
[% INCLUDE 'doc-head-open.inc' %]
9
[% INCLUDE 'doc-head-open.inc' %]
9
<title>My virtual card &rsaquo; [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog</title>
10
<title>Your library card &rsaquo; [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog</title>
10
[% INCLUDE 'doc-head-close.inc' %]
11
[% INCLUDE 'doc-head-close.inc' %]
11
[% BLOCK cssinclude %]
12
[% BLOCK cssinclude %]
12
[% END %]
13
[% END %]
Lines 20-26 Link Here
20
            <a href="/cgi-bin/koha/opac-user.pl">[% INCLUDE 'patron-title.inc' patron = logged_in_user %]</a>
21
            <a href="/cgi-bin/koha/opac-user.pl">[% INCLUDE 'patron-title.inc' patron = logged_in_user %]</a>
21
        [% END %]
22
        [% END %]
22
        [% WRAPPER breadcrumb_item bc_active= 1 %]
23
        [% WRAPPER breadcrumb_item bc_active= 1 %]
23
            <span>My virtual card</span>
24
            <span>Your library card</span>
24
        [% END %]
25
        [% END %]
25
    [% END #/ WRAPPER breadcrumbs %]
26
    [% END #/ WRAPPER breadcrumbs %]
26
27
Lines 29-47 Link Here
29
            <div class="col-lg-2">
30
            <div class="col-lg-2">
30
                <div id="navigation"> [% INCLUDE 'navigation.inc' IsPatronPage=1 %] </div>
31
                <div id="navigation"> [% INCLUDE 'navigation.inc' IsPatronPage=1 %] </div>
31
            </div>
32
            </div>
32
            <div class="col-10 order-first order-lg-2">
33
            <div class="col-lg-10 order-first order-lg-2">
33
                <h1>My virtual card </h1>
34
                <h1>Your library card </h1>
34
                <div id="patron-virtual-card">
35
                <div id="patron-virtual-card">
35
                    [% IF ( display_patron_image ) %]
36
                    [% IF ( display_patron_image ) %]
36
                        <div id="image-container">
37
                        <div id="image-container">
37
                            <img id="patron-image" src="/cgi-bin/koha/opac-patron-image.pl" alt="" />
38
                            <img id="patron-image" src="/cgi-bin/koha/opac-patron-image.pl" alt="" />
38
                        </div>
39
                        </div>
39
                    [% END %]
40
                    [% END %]
40
                    <div id="barcode-container">
41
                    <div id="card-details">
41
                        <svg id="patron-barcode" data-barcode="[% patron.cardnumber | html %]" data-barcode-format="[% barcode_format | html %]"></svg>
42
                        <div id="barcode-container">
42
                    </div>
43
                            <svg id="patron-barcode" data-barcode="[% patron.cardnumber | html %]" data-barcode-format="[% barcode_format | html %]"></svg>
43
                    <div id="lib-container">
44
                        </div>
44
                        <p id="patron-lib"><strong>Library:</strong> [% Branches.GetName( patron.branchcode ) | html %]</p>
45
                        <div id="lib-container">
46
                            <p id="patron-lib"><strong>Library:</strong> [% Branches.GetName( patron.branchcode ) | html %]</p>
47
                        </div>
48
                        <div id="cardnumber-container">
49
                            <p id="patron-cardnumber"><strong>Card number:</strong> [% patron.cardnumber | html %]</p>
50
                        </div>
51
                        <div id="dateexpiry-container">
52
                            <p id="patron-dateexpiry"><strong>Expiration date:</strong> [% patron.dateexpiry | $KohaDates %]</p>
53
                        </div>
45
                    </div>
54
                    </div>
46
                </div>
55
                </div>
47
            </div>
56
            </div>
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/sci/sci-main.tt (-19 / +24 lines)
Lines 189-195 Link Here
189
</main>
189
</main>
190
<!-- / .main -->
190
<!-- / .main -->
191
191
192
[% # Help modal %]
192
[%# Help modal %]
193
<div id="helpModal" class="modal" tabindex="-1" role="dialog" aria-labelledby="helpModalLabel" aria-hidden="true">
193
<div id="helpModal" class="modal" tabindex="-1" role="dialog" aria-labelledby="helpModalLabel" aria-hidden="true">
194
    <div class="modal-dialog">
194
    <div class="modal-dialog">
195
        <div class="modal-content">
195
        <div class="modal-content">
Lines 222-228 Link Here
222
222
223
[% INCLUDE 'opac-bottom.inc' %]
223
[% INCLUDE 'opac-bottom.inc' %]
224
[% BLOCK jsinclude %]
224
[% BLOCK jsinclude %]
225
    [% Asset.js('js/timeout.js') | $raw %]
226
    <script>
225
    <script>
227
        function mungeHistory() {
226
        function mungeHistory() {
228
                    // prevent back button from allowing form resubmission
227
                    // prevent back button from allowing form resubmission
Lines 273-279 Link Here
273
                    $("#sci_barcodes_table").show();
272
                    $("#sci_barcodes_table").show();
274
                    $('#sci_checkin_button').show();
273
                    $('#sci_checkin_button').show();
275
                    $('#sci_refresh_button').show();
274
                    $('#sci_refresh_button').show();
276
                    login_timeout();
277
275
278
                    // Add barcode to the array
276
                    // Add barcode to the array
279
                    barcodes.push(barcode);
277
                    barcodes.push(barcode);
Lines 293-316 Link Here
293
            dofocus();
291
            dofocus();
294
        });
292
        });
295
293
296
            document.addEventListener("DOMContentLoaded",function(){
294
297
                if ( document.querySelector('#sci_finish_button,#sci_append_button') ){
295
                var idleTime = 0;
298
                    login_timeout();
296
                $(document).ready(function () {
299
                }
297
                    //Increment the idle time counter every second
300
            });
298
                    var idleInterval = setInterval(timerIncrement, 1000);
301
            function login_timeout(){
299
302
                //NOTE: There can only be 1 sci_login_timer at a time
300
                    //Zero the idle timer on mouse movement.
303
                if ( ! window.sci_login_timer ){
301
                    $(this).mousemove(function (e) {
304
                    const idleTimeout = "[% Koha.Preference('SelfCheckInTimeOut') || 120 | html %]";
302
                        idleTime = 0;
305
                    const home_href = "/cgi-bin/koha/sci/sci-main.pl";
303
                    });
306
                    const sci_timer = new sc_timer({
304
                    $(this).keypress(function (e) {
307
                        "idle_timeout": idleTimeout,
305
                        idleTime = 0;
308
                        "redirect_url": home_href
309
                    });
306
                    });
310
                    window.sci_login_timer = sci_timer;
307
                });
311
                    sci_timer.start_timer();
308
309
                function timerIncrement() {
310
                    if ( $("#sci_finish_button").is(":visible") || $("#sci_refresh_button").is(":visible") ) {
311
                        idleTime = idleTime + 1;
312
                        idleTimeout = [% refresh_timeout | html %];
313
                        if (idleTime >= idleTimeout ) {
314
                            location.href = '/cgi-bin/koha/sci/sci-main.pl';
315
                        }
316
                    }
312
                }
317
                }
313
            }
318
314
319
315
                function checkBarcodeInput() {
320
                function checkBarcodeInput() {
316
                    var inputField = document.getElementById("barcode_input");
321
                    var inputField = document.getElementById("barcode_input");
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/sco/sco-main.tt (-20 / +9 lines)
Lines 55-61 Link Here
55
[% END %]
55
[% END %]
56
[% Asset.js('js/Gettext.js') | $raw %]
56
[% Asset.js('js/Gettext.js') | $raw %]
57
[% Asset.js('js/i18n.js') | $raw %]
57
[% Asset.js('js/i18n.js') | $raw %]
58
[% Asset.js('js/timeout.js') | $raw %]
59
</head>
58
</head>
60
<body id="sco_main" class="sco">
59
<body id="sco_main" class="sco">
61
<div id="wrapper">
60
<div id="wrapper">
Lines 339-345 Link Here
339
                                                            <input type="password" id="patronpw" class="form-control" size="20" name="patronpw" autocomplete="off" />
338
                                                            <input type="password" id="patronpw" class="form-control" size="20" name="patronpw" autocomplete="off" />
340
                                                        </div>
339
                                                        </div>
341
                                                        <div class="col-md-12">
340
                                                        <div class="col-md-12">
342
                                                            <button id="sco_patron_login" type="submit" class="btn btn-primary">Log in</button>
341
                                                            <button type="submit" class="btn btn-primary">Log in</button>
343
                                                        </div>
342
                                                        </div>
344
                                                    </div>
343
                                                    </div>
345
                                                    <!-- /.row -->
344
                                                    <!-- /.row -->
Lines 352-358 Link Here
352
                                                            <input type="text" id="patronid" class="form-control focus" size="20" name="patronid" autocomplete="off" />
351
                                                            <input type="text" id="patronid" class="form-control focus" size="20" name="patronid" autocomplete="off" />
353
                                                        </div>
352
                                                        </div>
354
                                                        <div class="col-md-12">
353
                                                        <div class="col-md-12">
355
                                                            <button id="sco_patron_login" type="submit" class="btn btn-primary">Submit</button>
354
                                                            <button type="submit" class="btn btn-primary">Submit</button>
356
                                                        </div>
355
                                                        </div>
357
                                                    </div>
356
                                                    </div>
358
                                                    <!-- /.row -->
357
                                                    <!-- /.row -->
Lines 412-434 Link Here
412
                        history.replaceState(null, document.title, window.location.href);
411
                        history.replaceState(null, document.title, window.location.href);
413
                    }
412
                    }
414
                }
413
                }
415
                document.addEventListener("DOMContentLoaded",function(){
414
                var mainTimeout;
416
                    if ( document.querySelector('#sco_patron_login,#logout_form') ){
415
                function sco_init() {
417
                        login_timeout();
416
                    mainTimeout = setTimeout(function() {
418
                    }
417
                        location.href = '/cgi-bin/koha/sco/sco-main.pl?op=logout';
419
                });
418
                    }, [% SelfCheckTimeout | html %]);
420
                function login_timeout(){
421
                    //NOTE: There can only be 1 sco_login_timer at a time
422
                    if ( ! window.sco_login_timer ){
423
                        const idleTimeout = "[% Koha.Preference('SelfCheckTimeout') || 120 | html %]";
424
                        const home_href = "/cgi-bin/koha/sco/sco-main.pl?op=logout";
425
                        const sco_timer = new sc_timer({
426
                            "idle_timeout": idleTimeout,
427
                            "redirect_url": home_href
428
                        });
429
                        window.sco_login_timer = sco_timer;
430
                        sco_timer.start_timer();
431
                    }
432
                }
419
                }
433
                function dofocus() {    // named function req'd for body onload event by some FF and IE7 security models
420
                function dofocus() {    // named function req'd for body onload event by some FF and IE7 security models
434
                    // alert("dofocus called");
421
                    // alert("dofocus called");
Lines 481-486 Link Here
481
468
482
                $(document).ready(function() {
469
                $(document).ready(function() {
483
                    dofocus();
470
                    dofocus();
471
                    [% IF ( patronid ) %]sco_init();[% END %]
484
472
485
                    var dTables = $("#loanTable, #holdst, #finestable");
473
                    var dTables = $("#loanTable, #holdst, #finestable");
486
                    dTables.each(function(){
474
                    dTables.each(function(){
Lines 512-517 Link Here
512
500
513
                    $("#logout_form").on("click", function(e){
501
                    $("#logout_form").on("click", function(e){
514
                        e.preventDefault(e);
502
                        e.preventDefault(e);
503
                        clearTimeout(mainTimeout);
515
                        [% IF Koha.Preference('SelfCheckReceiptPrompt') %]
504
                        [% IF Koha.Preference('SelfCheckReceiptPrompt') %]
516
                            confirmModal("", _("Would you like to print a receipt?"), _("Print receipt and end session"), _("End session"), function(result) {
505
                            confirmModal("", _("Would you like to print a receipt?"), _("Print receipt and end session"), _("End session"), function(result) {
517
                                if ( result ){
506
                                if ( result ){
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslimUtils.xsl (-2 / +2 lines)
Lines 319-325 Link Here
319
  <xsl:template name="tag_214_s">
319
  <xsl:template name="tag_214_s">
320
    <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='s']">
320
    <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='s']">
321
      <span class="results_summary tag_214_s">
321
      <span class="results_summary tag_214_s">
322
        <span class="label">Printing and/or Publishing Information Transcribed as Found in the Colophon: </span>
322
        <span class="label">Printing and/or publishing information transcribed as found in the colophon: </span>
323
        <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='s']">
323
        <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='s']">
324
          <xsl:value-of select="."/>
324
          <xsl:value-of select="."/>
325
          <xsl:choose>
325
          <xsl:choose>
Lines 338-344 Link Here
338
  <xsl:template name="tag_214_r">
338
  <xsl:template name="tag_214_r">
339
    <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='r']">
339
    <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='r']">
340
      <span class="results_summary tag_214_r">
340
      <span class="results_summary tag_214_r">
341
        <span class="label">Printing and/or Publishing Information Transcribed as Found in the Main Source of Information: </span>
341
        <span class="label">Printing and/or publishing information transcribed as found in the main source of information: </span>
342
        <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='r']">
342
        <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='r']">
343
          <xsl:value-of select="."/>
343
          <xsl:value-of select="."/>
344
          <xsl:choose>
344
          <xsl:choose>
(-)a/koha-tmpl/opac-tmpl/bootstrap/js/browse.js (-2 / +2 lines)
Lines 68-74 $(document).ready(function () { Link Here
68
                    card.find(".card-header")
68
                    card.find(".card-header")
69
                        .attr("id", "heading" + index)
69
                        .attr("id", "heading" + index)
70
                        .find("a")
70
                        .find("a")
71
                        .attr("data-target", "#collapse" + index)
71
                        .attr("data-bs-target", "#collapse" + index)
72
                        .attr("aria-controls", "collapse" + index)
72
                        .attr("aria-controls", "collapse" + index)
73
                        .text(object.text);
73
                        .text(object.text);
74
                    card.find(".collapse")
74
                    card.find(".collapse")
Lines 96-102 $(document).ready(function () { Link Here
96
        }
96
        }
97
97
98
        var link = $(this);
98
        var link = $(this);
99
        var target = link.data("target");
99
        var target = link.data("bs-target");
100
        var term = link.text();
100
        var term = link.text();
101
101
102
        var field = $("#browse-searchresults").data("field");
102
        var field = $("#browse-searchresults").data("field");
(-)a/koha-tmpl/opac-tmpl/bootstrap/js/timeout.js (-39 lines)
Lines 1-39 Link Here
1
class sc_timer {
2
    constructor(args) {
3
        const idle_timeout = args["idle_timeout"];
4
        const redirect_url = args["redirect_url"];
5
        if (idle_timeout) {
6
            this.idle_timeout = idle_timeout;
7
        }
8
        if (redirect_url) {
9
            this.redirect_url = redirect_url;
10
        }
11
        this.idle_time = 0;
12
    }
13
14
    start_timer() {
15
        const self = this;
16
        //Increment the idle time counter every 1 second
17
        const idle_interval = setInterval(function () {
18
            self._timer_increment();
19
        }, 1000);
20
21
        document.addEventListener("mousemove", function () {
22
            self.reset_timer();
23
        });
24
        document.addEventListener("keypress", function () {
25
            self.reset_timer();
26
        });
27
    }
28
29
    reset_timer() {
30
        this.idle_time = 0;
31
    }
32
33
    _timer_increment() {
34
        this.idle_time++;
35
        if (this.idle_time >= this.idle_timeout) {
36
            location.href = this.redirect_url;
37
        }
38
    }
39
}
(-)a/members/holdshistory.pl (-7 lines)
Lines 44-56 unless ($patron) { Link Here
44
    exit;
44
    exit;
45
}
45
}
46
46
47
if ( $borrowernumber eq C4::Context->preference('AnonymousPatron') ) {
48
49
    # use of 'eq' in the above comparison is intentional -- the
50
    # system preference value could be blank
51
    $template->param( is_anonymous => 1 );
52
}
53
54
$template->param(
47
$template->param(
55
    holdshistoryview => 1,
48
    holdshistoryview => 1,
56
    patron           => $patron,
49
    patron           => $patron,
(-)a/members/memberentry.pl (-3 / +10 lines)
Lines 560-565 if ( ( !$nok ) and $nodouble and ( $op eq 'cud-insert' or $op eq 'cud-save' ) ) Link Here
560
560
561
        delete $newdata{password2};
561
        delete $newdata{password2};
562
562
563
        delete $newdata{guarantor_id};
564
        delete $newdata{guarantor_relationship};
565
563
        try {
566
        try {
564
            $patron->set( \%newdata )->store( { guarantors => \@guarantors } ) if scalar( keys %newdata ) > 1;
567
            $patron->set( \%newdata )->store( { guarantors => \@guarantors } ) if scalar( keys %newdata ) > 1;
565
568
Lines 838-847 if ( C4::Context->preference('ExtendedPatronAttributes') ) { Link Here
838
}
841
}
839
842
840
if ( C4::Context->preference('EnhancedMessagingPreferences') ) {
843
if ( C4::Context->preference('EnhancedMessagingPreferences') ) {
841
    if ( $op eq 'add_form' ) {
844
    unless ( $nok && $input->param('setting_messaging_prefs') ) {
842
        C4::Form::MessagingPreferences::set_form_values( { categorycode => $categorycode }, $template );
845
        if ( $op eq 'add_form' ) {
846
            C4::Form::MessagingPreferences::set_form_values( { categorycode => $categorycode }, $template );
847
        } else {
848
            C4::Form::MessagingPreferences::set_form_values( { borrowernumber => $borrowernumber }, $template );
849
        }
843
    } else {
850
    } else {
844
        C4::Form::MessagingPreferences::set_form_values( { borrowernumber => $borrowernumber }, $template );
851
        C4::Form::MessagingPreferences::restore_form_values( $input, $template );
845
    }
852
    }
846
    $template->param( SMSSendDriver         => C4::Context->preference("SMSSendDriver") );
853
    $template->param( SMSSendDriver         => C4::Context->preference("SMSSendDriver") );
847
    $template->param( SMSnumber             => $data{'smsalertnumber'} );
854
    $template->param( SMSnumber             => $data{'smsalertnumber'} );
(-)a/members/moremember.pl (-4 lines)
Lines 78-87 output_and_exit_if_error( Link Here
78
78
79
my $category_type = $patron->category->category_type;
79
my $category_type = $patron->category->category_type;
80
80
81
if ( $patron->borrowernumber eq C4::Context->preference("AnonymousPatron") ) {
82
    $template->param( is_anonymous => 1 );
83
}
84
85
for (qw(gonenoaddress lost borrowernotes is_debarred)) {
81
for (qw(gonenoaddress lost borrowernotes is_debarred)) {
86
    $patron->$_ and $template->param( flagged => 1 ) and last;
82
    $patron->$_ and $template->param( flagged => 1 ) and last;
87
}
83
}
(-)a/members/readingrec.pl (-4 / +1 lines)
Lines 86-95 if ( $op eq 'export_barcodes' ) { Link Here
86
}
86
}
87
87
88
# Do not request the old issues of anonymous patron
88
# Do not request the old issues of anonymous patron
89
if ( $patron->borrowernumber eq C4::Context->preference('AnonymousPatron') ) {
89
if ( $patron->is_anonymous ) {
90
91
    # use of 'eq' in the above comparison is intentional -- the
92
    # system preference value could be blank
93
    $template->param( is_anonymous => 1 );
90
    $template->param( is_anonymous => 1 );
94
} else {
91
} else {
95
    $template->param(
92
    $template->param(
(-)a/misc/cronjobs/overdue_notices.pl (-3 lines)
Lines 1034-1042 sub _get_html_start { Link Here
1034
<style type='text/css'>
1034
<style type='text/css'>
1035
pre {page-break-after: always;}
1035
pre {page-break-after: always;}
1036
pre {white-space: pre-wrap;}
1036
pre {white-space: pre-wrap;}
1037
pre {white-space: -moz-pre-wrap;}
1038
pre {white-space: -o-pre-wrap;}
1039
pre {word-wrap: break-work;}
1040
</style>
1037
</style>
1041
</head>
1038
</head>
1042
<body>";
1039
<body>";
(-)a/misc/devel/Koha/Schema/Result/ReportsBranch.pm (+85 lines)
Line 0 Link Here
1
use utf8;
2
3
package Koha::Schema::Result::ReportsBranch;
4
5
# Created by DBIx::Class::Schema::Loader
6
# DO NOT MODIFY THE FIRST PART OF THIS FILE
7
8
=head1 NAME
9
10
Koha::Schema::Result::ReportsBranch
11
12
=cut
13
14
use strict;
15
use warnings;
16
17
use base 'DBIx::Class::Core';
18
19
=head1 TABLE: C<reports_branches>
20
21
=cut
22
23
__PACKAGE__->table("reports_branches");
24
25
=head1 ACCESSORS
26
27
=head2 report_id
28
29
  data_type: 'integer'
30
  is_foreign_key: 1
31
  is_nullable: 0
32
33
=head2 branchcode
34
35
  data_type: 'varchar'
36
  is_foreign_key: 1
37
  is_nullable: 0
38
  size: 10
39
40
=cut
41
42
__PACKAGE__->add_columns(
43
    "report_id",
44
    { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
45
    "branchcode",
46
    { data_type => "varchar", is_foreign_key => 1, is_nullable => 0, size => 10 },
47
);
48
49
=head1 RELATIONS
50
51
=head2 branchcode
52
53
Type: belongs_to
54
55
Related object: L<Koha::Schema::Result::Branch>
56
57
=cut
58
59
__PACKAGE__->belongs_to(
60
    "branchcode",
61
    "Koha::Schema::Result::Branch",
62
    { branchcode    => "branchcode" },
63
    { is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" },
64
);
65
66
=head2 report
67
68
Type: belongs_to
69
70
Related object: L<Koha::Schema::Result::SavedSql>
71
72
=cut
73
74
__PACKAGE__->belongs_to(
75
    "report",
76
    "Koha::Schema::Result::SavedSql",
77
    { id            => "report_id" },
78
    { is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" },
79
);
80
81
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-06 17:02:11
82
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:SJqUxMgLJHAjbX3GJlpB+A
83
84
# You can replace this text with custom code or comments, and it will be preserved on regeneration
85
1;
(-)a/misc/devel/tidy.pl (-55 / +12 lines)
Lines 8-13 use File::Slurp qw( read_file write_file ); Link Here
8
use IPC::Cmd     qw( run );
8
use IPC::Cmd     qw( run );
9
use Parallel::ForkManager;
9
use Parallel::ForkManager;
10
10
11
use Koha::Devel::Files;
12
11
my ( $perl_files, $js_files, $tt_files, $nproc, $no_write, $silent, $help );
13
my ( $perl_files, $js_files, $tt_files, $nproc, $no_write, $silent, $help );
12
14
13
our $perltidyrc = '.perltidyrc';
15
our $perltidyrc = '.perltidyrc';
Lines 34-46 pod2usage("--no-write can only be passed with a single file") if $no_write && @f Link Here
34
pod2usage("--perl, --js and --tt can only be passed without any other files in parameter")
36
pod2usage("--perl, --js and --tt can only be passed without any other files in parameter")
35
    if @files && ( $perl_files || $js_files || $tt_files );
37
    if @files && ( $perl_files || $js_files || $tt_files );
36
38
37
my $exceptions = {
39
my $dev_files = Koha::Devel::Files->new( { context => 'tidy' } );
38
    pl => [qw(Koha/Schema/Result Koha/Schema.pm)],
39
    js => [
40
        qw(koha-tmpl/intranet-tmpl/lib koha-tmpl/intranet-tmpl/js/Gettext.js koha-tmpl/opac-tmpl/lib Koha/ILL/Backend/)
41
    ],
42
    tt => [qw(Koha/ILL/Backend/ *doc-head-open.inc misc/cronjobs/rss)],
43
};
44
40
45
my @original_files = @files;
41
my @original_files = @files;
46
if (@files) {
42
if (@files) {
Lines 48-55 if (@files) { Link Here
48
    # This is inefficient if the list of files is long but most of the time we will have only one
44
    # This is inefficient if the list of files is long but most of the time we will have only one
49
    @files = map {
45
    @files = map {
50
        my $file     = $_;
46
        my $file     = $_;
51
        my $filetype = get_filetype($file);
47
        my $filetype = $dev_files->get_filetype($file);
52
        my $cmd      = sprintf q{git ls-files %s | grep %s}, build_git_exclude($filetype), $file;
48
        my $cmd      = sprintf q{git ls-files %s | grep %s}, $dev_files->build_git_exclude($filetype), $file;
53
        my $output   = qx{$cmd};
49
        my $output   = qx{$cmd};
54
        chomp $output;
50
        chomp $output;
55
        $output ? $file : ();
51
        $output ? $file : ();
Lines 69-82 if (@files) { Link Here
69
        }
65
        }
70
    }
66
    }
71
} else {
67
} else {
72
    push @files, get_perl_files() if $perl_files;
68
    push @files, $dev_files->ls_perl_files() if $perl_files;
73
    push @files, get_js_files()   if $js_files;
69
    push @files, $dev_files->ls_js_files()   if $js_files;
74
    push @files, get_tt_files()   if $tt_files;
70
    push @files, $dev_files->ls_tt_files()   if $tt_files;
75
71
76
    unless (@files) {
72
    unless (@files) {
77
        push @files, get_perl_files();
73
        push @files, $dev_files->ls_perl_files();
78
        push @files, get_js_files();
74
        push @files, $dev_files->ls_js_files();
79
        push @files, get_tt_files();
75
        push @files, $dev_files->ls_tt_files();
80
    }
76
    }
81
}
77
}
82
78
Lines 124-130 if (@errors) { Link Here
124
sub tidy {
120
sub tidy {
125
    my ($file) = @_;
121
    my ($file) = @_;
126
122
127
    my $filetype = get_filetype($file);
123
    my $filetype = $dev_files->get_filetype($file);
128
124
129
    if ( $filetype eq 'pl' ) {
125
    if ( $filetype eq 'pl' ) {
130
        return tidy_perl($file);
126
        return tidy_perl($file);
Lines 137-168 sub tidy { Link Here
137
    }
133
    }
138
}
134
}
139
135
140
sub build_git_exclude {
141
    my ($filetype) = @_;
142
    return join( " ", map( "':(exclude)$_'", @{ $exceptions->{$filetype} } ) );
143
}
144
145
sub get_perl_files {
146
    my $cmd   = sprintf q{git ls-files '*.pl' '*.PL' '*.pm' '*.t' svc opac/svc %s}, build_git_exclude('pl');
147
    my @files = qx{$cmd};
148
    chomp for @files;
149
    return @files;
150
}
151
152
sub get_js_files {
153
    my $cmd   = sprintf q{git ls-files '*.js' '*.ts' '*.vue' %s}, build_git_exclude('js');
154
    my @files = qx{$cmd};
155
    chomp for @files;
156
    return @files;
157
}
158
159
sub get_tt_files {
160
    my $cmd   = sprintf q{git ls-files '*.tt' '*.inc' %s}, build_git_exclude('tt');
161
    my @files = qx{$cmd};
162
    chomp for @files;
163
    return @files;
164
}
165
166
sub tidy_perl {
136
sub tidy_perl {
167
    my ($file) = @_;
137
    my ($file) = @_;
168
    my $cmd =
138
    my $cmd =
Lines 221-239 sub tidy_tt { Link Here
221
    return ( $success, $error_message, $full_buf, $stdout_buf, $stderr_buf );
191
    return ( $success, $error_message, $full_buf, $stdout_buf, $stderr_buf );
222
}
192
}
223
193
224
sub get_filetype {
225
    my ($file) = @_;
226
    return 'pl' if $file =~ m{^svc}  || $file =~ m{^opac/svc};
227
    return 'pl' if $file =~ m{\.pl$} || $file =~ m{\.pm} || $file =~ m{\.t$};
228
    return 'pl' if $file =~ m{\.PL$};
229
230
    return 'js' if $file =~ m{\.js$} || $file =~ m{\.ts$} || $file =~ m{\.vue$};
231
232
    return 'tt' if $file =~ m{\.inc$} || $file =~ m{\.tt$};
233
234
    die sprintf 'Cannot guess filetype for %s', $file;
235
}
236
237
sub l {
194
sub l {
238
    say shift unless $silent;
195
    say shift unless $silent;
239
}
196
}
(-)a/misc/maintenance/search_for_data_inconsistencies.pl (-1 / +1 lines)
Lines 390-396 use C4::Biblio qw( GetMarcFromKohaField ); Link Here
390
                "Column " . $rs->result_source->name . "." . $column . " contains $invalid_dates invalid dates" );
390
                "Column " . $rs->result_source->name . "." . $column . " contains $invalid_dates invalid dates" );
391
391
392
            if ( $invalid_dates > 0 ) {
392
            if ( $invalid_dates > 0 ) {
393
                new_hint("You may change the dates with script: misc/cronjobs/fix_invalid_dates.pl (-c -v)");
393
                new_hint("You may change the dates with script: misc/maintenance/fix_invalid_dates.pl (-c -v)");
394
            }
394
            }
395
395
396
        }
396
        }
(-)a/misc/sip_cli_emulator.pl (-4 / +30 lines)
Lines 53-60 my $fee_amount; Link Here
53
my $fee_identifier;
53
my $fee_identifier;
54
my $transaction_id;
54
my $transaction_id;
55
my $pickup_location;
55
my $pickup_location;
56
my $hold_mode;
56
my $hold_mode = '+';
57
my $no_block = 'N';
57
my $no_block  = 'N';
58
my $start_item;
58
my $start_item;
59
my $end_item;
59
my $end_item;
60
60
Lines 306-311 my $handlers = { Link Here
306
    },
306
    },
307
};
307
};
308
308
309
my $param_to_cli = {
310
    currency_type     => 'currency-type',
311
    current_location  => 'location',
312
    end_item          => 'end-item',
313
    fee_acknowledged  => 'fee-acknowledged',
314
    fee_amount        => 'fee-amount',
315
    fee_identifier    => 'fee-identifier',
316
    fee_type          => 'fee-type',
317
    hold_mode         => 'hold-mode',
318
    institution_id    => 'location',
319
    item_identifier   => 'item',
320
    location_code     => 'location',
321
    login_password    => 'sip_pass',
322
    login_user_id     => 'sip_user',
323
    no_block          => 'no-block',
324
    patron_identifier => 'patron',
325
    patron_password   => 'password',
326
    payment_type      => 'payment-type',
327
    pickup_location   => 'pickup-location',
328
    start_item        => 'start-item',
329
    summary           => 'summary',
330
    terminal_password => 'sip_pass',
331
    transaction_id    => 'transaction-id',
332
};
333
309
my $data = run_command_message('login');
334
my $data = run_command_message('login');
310
335
311
if ( $data =~ '^941' ) {    ## we are logged in
336
if ( $data =~ '^941' ) {    ## we are logged in
Lines 335-341 sub build_command_message { Link Here
335
    foreach my $key ( keys %$parameters ) {
360
    foreach my $key ( keys %$parameters ) {
336
        unless ( $parameters->{$key} ) {
361
        unless ( $parameters->{$key} ) {
337
            unless ( $optional{$key} ) {
362
            unless ( $optional{$key} ) {
338
                say "$key is required for $message";
363
                my $param_name = $param_to_cli->{$key} // $key;
364
                say "$param_name is required for $message";
339
                return;
365
                return;
340
            }
366
            }
341
        }
367
        }
Lines 356-362 sub run_command_message { Link Here
356
382
357
    my $data = <$socket>;
383
    my $data = <$socket>;
358
384
359
    say "READ: $data";
385
    say "READ: " . ( defined $data ? $data : 'undef' );
360
386
361
    return $data;
387
    return $data;
362
}
388
}
(-)a/opac/opac-readingrecord.pl (-5 / +5 lines)
Lines 57-63 if ( $order eq 'title' ) { Link Here
57
} elsif ( $order eq 'author' ) {
57
} elsif ( $order eq 'author' ) {
58
    $template->param( orderbyauthor => 1 );
58
    $template->param( orderbyauthor => 1 );
59
} else {
59
} else {
60
    $order = "date_due desc";
60
    $order = { -desc => "date_due" };
61
    $template->param( orderbydate => 1 );
61
    $template->param( orderbydate => 1 );
62
}
62
}
63
63
Lines 66-88 $limit //= ''; Link Here
66
$limit = ( $limit eq 'full' ) ? 0 : 50;
66
$limit = ( $limit eq 'full' ) ? 0 : 50;
67
67
68
my $checkouts = [
68
my $checkouts = [
69
    $patron->checkouts(
69
    $patron->checkouts->search(
70
        {},
70
        {},
71
        {
71
        {
72
            order_by => $order,
72
            order_by => $order,
73
            prefetch => { item => { biblio => 'biblioitems' } },
73
            prefetch => { item => { biblio => 'biblioitems' } },
74
            ( $limit ? ( limit => $limit ) : () ),
74
            ( $limit ? ( rows => $limit ) : () ),
75
        }
75
        }
76
    )->as_list
76
    )->as_list
77
];
77
];
78
$limit -= scalar(@$checkouts) if $limit;
78
$limit -= scalar(@$checkouts) if $limit;
79
my $old_checkouts = [
79
my $old_checkouts = [
80
    $patron->old_checkouts(
80
    $patron->old_checkouts->search(
81
        {},
81
        {},
82
        {
82
        {
83
            order_by => $order,
83
            order_by => $order,
84
            prefetch => { item => { biblio => 'biblioitems' } },
84
            prefetch => { item => { biblio => 'biblioitems' } },
85
            ( $limit ? ( limit => $limit ) : () ),
85
            ( $limit ? ( rows => $limit ) : () ),
86
        }
86
        }
87
    )->as_list
87
    )->as_list
88
];
88
];
(-)a/opac/opac-reserve.pl (-50 / +28 lines)
Lines 39-44 use Koha::Items; Link Here
39
use Koha::ItemTypes;
39
use Koha::ItemTypes;
40
use Koha::Checkouts;
40
use Koha::Checkouts;
41
use Koha::Libraries;
41
use Koha::Libraries;
42
use Koha::Logger;
42
use Koha::Patrons;
43
use Koha::Patrons;
43
use List::MoreUtils qw( uniq );
44
use List::MoreUtils qw( uniq );
44
45
Lines 112-176 if ( $#biblionumbers < 0 && $op ne 'cud-place_reserve' ) { Link Here
112
# Here we check that the borrower can actually make reserves Stage 1.
113
# Here we check that the borrower can actually make reserves Stage 1.
113
#
114
#
114
#
115
#
115
my $noreserves = 0;
116
if ( $category->effective_BlockExpiredPatronOpacActions_contains('hold') ) {
117
    if ( $patron->is_expired ) {
118
116
119
        # cannot reserve, their card has expired and the rules set mean this is not allowed
117
my $can_place_holds = $patron->can_place_holds( { no_short_circuit => 1 } );
120
        $noreserves = 1;
121
        $template->param( message => 1, expired_patron => 1 );
122
    }
123
}
124
125
my $maxoutstanding    = C4::Context->preference("maxoutstanding");
126
my $amountoutstanding = $patron->account->balance;
127
if ( $amountoutstanding && ( $amountoutstanding > $maxoutstanding ) ) {
128
    my $amount = sprintf "%.02f", $amountoutstanding;
129
    $template->param( message => 1 );
130
    $noreserves = 1;
131
    $template->param( too_much_oweing => $amount );
132
}
133
134
if ( $patron->gonenoaddress && ( $patron->gonenoaddress == 1 ) ) {
135
    $noreserves = 1;
136
    $template->param(
137
        message => 1,
138
        GNA     => 1
139
    );
140
}
141
118
142
if ( $patron->lost && ( $patron->lost == 1 ) ) {
119
if ( !$can_place_holds ) {
143
    $noreserves = 1;
144
    $template->param(
145
        message => 1,
146
        lost    => 1
147
    );
148
}
149
120
150
if ( $patron->is_debarred ) {
151
    $noreserves = 1;
152
    $template->param(
153
        message          => 1,
154
        debarred         => 1,
155
        debarred_comment => $patron->debarredcomment,
156
        debarred_date    => $patron->debarred,
157
    );
158
}
159
160
my $holds          = $patron->holds;
161
my $reserves_count = $holds->count;
162
$template->param( RESERVES => $holds->unblessed );
163
if ( $maxreserves && ( $reserves_count >= $maxreserves ) ) {
164
    $template->param( message => 1 );
121
    $template->param( message => 1 );
165
    $noreserves = 1;
166
    $template->param( too_many_reserves => $holds->count );
167
}
168
122
169
if ($noreserves) {
123
    my $messages = $can_place_holds->messages();
124
    foreach my $msg ( @{$messages} ) {
125
        if ( $msg->message eq 'expired' ) {
126
            $template->param( expired_patron => 1 );
127
        } elsif ( $msg->message eq 'debt_limit' ) {
128
            $template->param( too_much_oweing => sprintf( "%.02f", $msg->{payload}->{total_outstanding} ) );
129
        } elsif ( $msg->message eq 'bad_address' ) {
130
            $template->param( GNA => 1 );
131
        } elsif ( $msg->message eq 'card_lost' ) {
132
            $template->param( lost => 1 );
133
        } elsif ( $msg->message eq 'restricted' ) {
134
            $template->param(
135
                debarred         => 1,
136
                debarred_comment => $patron->debarredcomment,
137
                debarred_date    => $patron->debarred,
138
            );
139
        } elsif ( $msg->message eq 'hold_limit' ) {
140
            $template->param( too_many_reserves => $msg->{payload}->{total_holds} );
141
        } else {
142
            Koha::Logger->get->warn( sprintf( "Unhandled 'can_place_holds' error code: %s", $msg->message ) );
143
        }
144
    }
145
170
    output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 };
146
    output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 };
171
    exit;
147
    exit;
172
}
148
}
173
149
150
my $reserves_count = $patron->holds->count;
151
174
# pass the pickup branch along....
152
# pass the pickup branch along....
175
my $branch = $query->param('branch') || $patron->branchcode || C4::Context->userenv->{branch} || '';
153
my $branch = $query->param('branch') || $patron->branchcode || C4::Context->userenv->{branch} || '';
176
$template->param( branch => $branch );
154
$template->param( branch => $branch );
(-)a/opac/opac-user.pl (+1 lines)
Lines 430-435 $template->param( Link Here
430
    OpacHoldNotes              => C4::Context->preference('OpacHoldNotes'),
430
    OpacHoldNotes              => C4::Context->preference('OpacHoldNotes'),
431
    failed_holds               => scalar $query->param('failed_holds'),
431
    failed_holds               => scalar $query->param('failed_holds'),
432
    opac_user_holds            => scalar $query->param('opac-user-holds')            || 0,
432
    opac_user_holds            => scalar $query->param('opac-user-holds')            || 0,
433
    opac_user_overdues         => scalar $query->param('opac-user-overdues')         || 0,
433
    opac_user_article_requests => scalar $query->param('opac-user-article-requests') || 0,
434
    opac_user_article_requests => scalar $query->param('opac-user-article-requests') || 0,
434
);
435
);
435
436
(-)a/opac/sci/sci-main.pl (+4 lines)
Lines 102-105 if ( $op eq 'cud-check_in' ) { Link Here
102
    $template->param( success => \@success, errors => \@errors, checkins => 1 );
102
    $template->param( success => \@success, errors => \@errors, checkins => 1 );
103
}
103
}
104
104
105
# Make sure timeout has a reasonable value
106
my $timeout = C4::Context->preference('SelfCheckInTimeout') || 120;
107
$template->param( refresh_timeout => $timeout );
108
105
output_html_with_http_headers $cgi, $cookie, $template->output, undef, { force_no_caching => 1 };
109
output_html_with_http_headers $cgi, $cookie, $template->output, undef, { force_no_caching => 1 };
(-)a/opac/sco/sco-main.pl (+7 lines)
Lines 74-79 my ( $template, $loggedinuser, $cookie ) = get_template_and_user( Link Here
74
    }
74
    }
75
);
75
);
76
76
77
# Get the self checkout timeout preference, or use 120 seconds as a default
78
my $selfchecktimeout = 120000;
79
if ( C4::Context->preference('SelfCheckTimeout') ) {
80
    $selfchecktimeout = C4::Context->preference('SelfCheckTimeout') * 1000;
81
}
82
$template->param( SelfCheckTimeout => $selfchecktimeout );
83
77
# Checks policy laid out by SCOAllowCheckin, defaults to 'on' if preference is undefined
84
# Checks policy laid out by SCOAllowCheckin, defaults to 'on' if preference is undefined
78
my $allowselfcheckreturns = 1;
85
my $allowselfcheckreturns = 1;
79
if ( defined C4::Context->preference('SCOAllowCheckin') ) {
86
if ( defined C4::Context->preference('SCOAllowCheckin') ) {
(-)a/package.json (-1 / +1 lines)
Lines 19-25 Link Here
19
    "bootstrap": "^5.3.3",
19
    "bootstrap": "^5.3.3",
20
    "css-loader": "^6.6.0",
20
    "css-loader": "^6.6.0",
21
    "cypress": "^12.17.2",
21
    "cypress": "^12.17.2",
22
    "cypress-mysql": "^1.0.0",
23
    "datatables.net-buttons": "^2.3.4",
22
    "datatables.net-buttons": "^2.3.4",
24
    "datatables.net-vue3": "^2.0.0",
23
    "datatables.net-vue3": "^2.0.0",
25
    "gulp": "^4.0.2",
24
    "gulp": "^4.0.2",
Lines 82-87 Link Here
82
    "globals": "^16.0.0",
81
    "globals": "^16.0.0",
83
    "gulp-tap": "^1.0.1",
82
    "gulp-tap": "^1.0.1",
84
    "html-webpack-plugin": "^5.5.0",
83
    "html-webpack-plugin": "^5.5.0",
84
    "mysql2": "^3.14.1",
85
    "node-sass-tilde-importer": "^1.0.2",
85
    "node-sass-tilde-importer": "^1.0.2",
86
    "postcss": "^8.4.14",
86
    "postcss": "^8.4.14",
87
    "postcss-selector-parser": "^6.0.10",
87
    "postcss-selector-parser": "^6.0.10",
(-)a/reports/guided_reports.pl (-24 / +81 lines)
Lines 42-47 use Koha::Notice::Templates; Link Here
42
use Koha::TemplateUtils qw( process_tt );
42
use Koha::TemplateUtils qw( process_tt );
43
use C4::ClassSource     qw( GetClassSources );
43
use C4::ClassSource     qw( GetClassSources );
44
use C4::Scrubber;
44
use C4::Scrubber;
45
use Data::Dumper;
45
46
46
=head1 NAME
47
=head1 NAME
47
48
Lines 57-62 my $input = CGI->new; Link Here
57
my $usecache = Koha::Caches->get_instance->memcached_cache;
58
my $usecache = Koha::Caches->get_instance->memcached_cache;
58
59
59
my $op = $input->param('op') // '';
60
my $op = $input->param('op') // '';
61
print STDERR "DEBUG op: $op\n";
62
63
# my @branches = grep { $_ ne q{} } $input->multi_param('branches');
60
my $flagsrequired;
64
my $flagsrequired;
61
if (   ( $op eq 'add_form' )
65
if (   ( $op eq 'add_form' )
62
    || ( $op eq 'add_form_sql' )
66
    || ( $op eq 'add_form_sql' )
Lines 127-133 if ( !$op ) { Link Here
127
        'showsql'       => 1,
131
        'showsql'       => 1,
128
        'mana_success'  => scalar $input->param('mana_success'),
132
        'mana_success'  => scalar $input->param('mana_success'),
129
        'mana_id'       => $report->{mana_id},
133
        'mana_id'       => $report->{mana_id},
130
        'mana_comments' => $report->{comments}
134
        'mana_comments' => $report->{comments},
131
    );
135
    );
132
136
133
} elsif ( $op eq 'edit_form' ) {
137
} elsif ( $op eq 'edit_form' ) {
Lines 136-141 if ( !$op ) { Link Here
136
    my $group    = $report->report_group;
140
    my $group    = $report->report_group;
137
    my $subgroup = $report->report_subgroup;
141
    my $subgroup = $report->report_subgroup;
138
    my $tables   = get_tables();
142
    my $tables   = get_tables();
143
139
    $template->param(
144
    $template->param(
140
        'sql'                   => $report->savedsql,
145
        'sql'                   => $report->savedsql,
141
        'reportname'            => $report->report_name,
146
        'reportname'            => $report->report_name,
Lines 148-158 if ( !$op ) { Link Here
148
        'editsql'               => 1,
153
        'editsql'               => 1,
149
        'mana_id'               => $report->{mana_id},
154
        'mana_id'               => $report->{mana_id},
150
        'mana_comments'         => $report->{comments},
155
        'mana_comments'         => $report->{comments},
151
        'tables'                => $tables
156
        'tables'                => $tables,
157
        'report'                => $report,
152
    );
158
    );
153
159
154
} elsif ( $op eq 'cud-update_sql' || $op eq 'cud-update_and_run_sql' ) {
160
} elsif ( $op eq 'cud-update_sql' || $op eq 'cud-update_and_run_sql' ) {
155
    my $id                 = $input->param('id');
161
    my $id                 = $input->param('id');
162
    my $report             = Koha::Reports->find($id);
156
    my $sql                = $input->param('sql');
163
    my $sql                = $input->param('sql');
157
    my $reportname         = $input->param('reportname');
164
    my $reportname         = $input->param('reportname');
158
    my $group              = $input->param('group');
165
    my $group              = $input->param('group');
Lines 163-171 if ( !$op ) { Link Here
163
    my $public             = $input->param('public');
170
    my $public             = $input->param('public');
164
    my $save_anyway        = $input->param('save_anyway');
171
    my $save_anyway        = $input->param('save_anyway');
165
    my @errors;
172
    my @errors;
166
    my $tables = get_tables();
173
    my $tables   = get_tables();
174
    my @branches = grep { $_ ne q{} } $input->multi_param('branches');
167
175
168
    # if we have the units, then we came from creating a report from SQL and thus need to handle converting units
169
    if ($cache_expiry_units) {
176
    if ($cache_expiry_units) {
170
        if ( $cache_expiry_units eq "minutes" ) {
177
        if ( $cache_expiry_units eq "minutes" ) {
171
            $cache_expiry *= 60;
178
            $cache_expiry *= 60;
Lines 211-216 if ( !$op ) { Link Here
211
                'problematic_authvals' => $problematic_authvals,
218
                'problematic_authvals' => $problematic_authvals,
212
                'warn_authval_problem' => 1,
219
                'warn_authval_problem' => 1,
213
                'phase_update'         => 1,
220
                'phase_update'         => 1,
221
                'branches'             => @branches,
222
                'report'               => $report,
214
            );
223
            );
215
224
216
        } else {
225
        } else {
Lines 226-234 if ( !$op ) { Link Here
226
                    notes        => $notes,
235
                    notes        => $notes,
227
                    public       => $public,
236
                    public       => $public,
228
                    cache_expiry => $cache_expiry,
237
                    cache_expiry => $cache_expiry,
238
239
                    # report       => $report,
229
                }
240
                }
230
            );
241
            );
231
242
243
            $report->store;
244
            $report->replace_library_limits( \@branches );
245
232
            my $editsql = 1;
246
            my $editsql = 1;
233
            if ( $op eq 'cud-update_and_run_sql' ) {
247
            if ( $op eq 'cud-update_and_run_sql' ) {
234
                $editsql = 0;
248
                $editsql = 0;
Lines 245-251 if ( !$op ) { Link Here
245
                'cache_expiry'          => $cache_expiry,
259
                'cache_expiry'          => $cache_expiry,
246
                'public'                => $public,
260
                'public'                => $public,
247
                'usecache'              => $usecache,
261
                'usecache'              => $usecache,
248
                'tables'                => $tables
262
                'tables'                => $tables,
263
264
                # 'branches'              => @branches,
265
                'report' => $report,
249
            );
266
            );
250
            logaction( "REPORTS", "MODIFY", $id, "$reportname | $sql" ) if C4::Context->preference("ReportsLog");
267
            logaction( "REPORTS", "MODIFY", $id, "$reportname | $sql" ) if C4::Context->preference("ReportsLog");
251
        }
268
        }
Lines 489-494 if ( !$op ) { Link Here
489
    my $area = $input->param('area');
506
    my $area = $input->param('area');
490
    my $sql  = $input->param('sql');
507
    my $sql  = $input->param('sql');
491
    my $type = $input->param('type');
508
    my $type = $input->param('type');
509
492
    $template->param(
510
    $template->param(
493
        'save'                  => 1,
511
        'save'                  => 1,
494
        'area'                  => $area,
512
        'area'                  => $area,
Lines 514-519 if ( !$op ) { Link Here
514
    my $public             = $input->param('public');
532
    my $public             = $input->param('public');
515
    my $save_anyway        = $input->param('save_anyway');
533
    my $save_anyway        = $input->param('save_anyway');
516
    my $tables             = get_tables();
534
    my $tables             = get_tables();
535
    my @branches           = grep { $_ ne q{} } $input->multi_param('branches');
517
536
518
    # if we have the units, then we came from creating a report from SQL and thus need to handle converting units
537
    # if we have the units, then we came from creating a report from SQL and thus need to handle converting units
519
    if ($cache_expiry_units) {
538
    if ($cache_expiry_units) {
Lines 591-596 if ( !$op ) { Link Here
591
                    public         => $public,
610
                    public         => $public,
592
                }
611
                }
593
            );
612
            );
613
            my $report = Koha::Reports->find($id);
614
            $report->replace_library_limits( \@branches );
615
594
            logaction( "REPORTS", "ADD", $id, "$name | $sql" ) if C4::Context->preference("ReportsLog");
616
            logaction( "REPORTS", "ADD", $id, "$name | $sql" ) if C4::Context->preference("ReportsLog");
595
            $template->param(
617
            $template->param(
596
                'save_successful'       => 1,
618
                'save_successful'       => 1,
Lines 603-609 if ( !$op ) { Link Here
603
                'cache_expiry'          => $cache_expiry,
625
                'cache_expiry'          => $cache_expiry,
604
                'public'                => $public,
626
                'public'                => $public,
605
                'usecache'              => $usecache,
627
                'usecache'              => $usecache,
606
                'tables'                => $tables
628
                'tables'                => $tables,
629
                'report'                => $report,
607
            );
630
            );
608
        }
631
        }
609
    }
632
    }
Lines 746-765 if ( !$op ) { Link Here
746
769
747
} elsif ( $op eq 'add_form_sql' || $op eq 'duplicate' ) {
770
} elsif ( $op eq 'add_form_sql' || $op eq 'duplicate' ) {
748
771
749
    my ( $group, $subgroup, $sql, $reportname, $notes );
772
    my ( $group, $subgroup, $sql, $reportname, $notes, @branches, $report );
750
    if ( $input->param('sql') ) {
773
    if ( $input->param('sql') ) {
751
        $group      = $input->param('report_group');
774
        $group      = $input->param('report_group');
752
        $subgroup   = $input->param('report_subgroup');
775
        $subgroup   = $input->param('report_subgroup');
753
        $sql        = $input->param('sql')        // '';
776
        $sql        = $input->param('sql')        // '';
754
        $reportname = $input->param('reportname') // '';
777
        $reportname = $input->param('reportname') // '';
755
        $notes      = $input->param('notes')      // '';
778
        $notes      = $input->param('notes')      // '';
779
        @branches   = grep { $_ ne q{} } $input->multi_param('branches');
780
756
    } elsif ( my $report_id = $input->param('id') ) {
781
    } elsif ( my $report_id = $input->param('id') ) {
757
        my $report = Koha::Reports->find($report_id);
782
        $report     = Koha::Reports->find($report_id);
758
        $group      = $report->report_group;
783
        $group      = $report->report_group;
759
        $subgroup   = $report->report_subgroup;
784
        $subgroup   = $report->report_subgroup;
760
        $sql        = $report->savedsql    // '';
785
        $sql        = $report->savedsql    // '';
761
        $reportname = $report->report_name // '';
786
        $reportname = $report->report_name // '';
762
        $notes      = $report->notes       // '';
787
        $notes      = $report->notes       // '';
788
        @branches   = grep { $_ ne q{} } $input->multi_param('branches');
789
763
    }
790
    }
764
791
765
    my $tables = get_tables();
792
    my $tables = get_tables();
Lines 775-780 if ( !$op ) { Link Here
775
        'usecache'              => $usecache,
802
        'usecache'              => $usecache,
776
        'tables'                => $tables,
803
        'tables'                => $tables,
777
804
805
        # 'branches'              => \@branches,
806
        'report' => $report,
807
778
    );
808
    );
779
}
809
}
780
810
Lines 1081-1104 if ( $op eq 'list' || $op eq 'convert' ) { Link Here
1081
    my $subgroup = $input->param('subgroup');
1111
    my $subgroup = $input->param('subgroup');
1082
    $filter->{group}    = $group;
1112
    $filter->{group}    = $group;
1083
    $filter->{subgroup} = $subgroup;
1113
    $filter->{subgroup} = $subgroup;
1084
    my $reports = get_saved_reports($filter);
1114
1085
    my $has_obsolete_reports;
1115
    my $pref_enable_filtering_reports = C4::Context->preference("EnableFilteringReports");
1086
    for my $report (@$reports) {
1116
    if ( $pref_enable_filtering_reports == "1" ) {
1087
        $report->{results} = C4::Reports::Guided::get_results( $report->{id} );
1117
        my $reports_with_library_limits_results =
1088
        if ( $report->{savedsql} =~ m|biblioitems| and $report->{savedsql} =~ m|marcxml| ) {
1118
            Koha::Reports->search_with_library_limits( {}, {}, C4::Context::mybranch() );
1089
            $report->{seems_obsolete} = 1;
1119
        my $reports_list = $reports_with_library_limits_results->unblessed;
1090
            $has_obsolete_reports++;
1120
        my $has_obsolete_reports;
1121
        while ( my $report = $reports_with_library_limits_results->next ) {
1122
            $report->{results} = C4::Reports::Guided::get_results( $report->{id} );
1123
            if ( $report->{savedsql} =~ m|biblioitems| and $report->{savedsql} =~ m|marcxml| ) {
1124
                $report->{seems_obsolete} = 1;
1125
                $has_obsolete_reports++;
1126
            }
1127
            $template->param(
1128
                'manamsg'               => $input->param('manamsg') || '',
1129
                'saved1'                => 1,
1130
                'savedreports'          => $reports_list,
1131
                'usecache'              => $usecache,
1132
                'groups_with_subgroups' => groups_with_subgroups( $group, $subgroup ),
1133
                filters                 => $filter,
1134
                has_obsolete_reports    => $has_obsolete_reports,
1135
            );
1091
        }
1136
        }
1137
    } else {
1138
1139
        my $reports = get_saved_reports($filter);
1140
        my $has_obsolete_reports;
1141
1142
        for my $report (@$reports) {
1143
            $report->{results} = C4::Reports::Guided::get_results( $report->{id} );
1144
            if ( $report->{savedsql} =~ m|biblioitems| and $report->{savedsql} =~ m|marcxml| ) {
1145
                $report->{seems_obsolete} = 1;
1146
                $has_obsolete_reports++;
1147
            }
1148
        }
1149
        $template->param(
1150
            'manamsg'               => $input->param('manamsg') || '',
1151
            'saved1'                => 1,
1152
            'savedreports'          => $reports,
1153
            'usecache'              => $usecache,
1154
            'groups_with_subgroups' => groups_with_subgroups( $group, $subgroup ),
1155
            filters                 => $filter,
1156
            has_obsolete_reports    => $has_obsolete_reports,
1157
        );
1092
    }
1158
    }
1093
    $template->param(
1094
        'manamsg'               => $input->param('manamsg') || '',
1095
        'saved1'                => 1,
1096
        'savedreports'          => $reports,
1097
        'usecache'              => $usecache,
1098
        'groups_with_subgroups' => groups_with_subgroups( $group, $subgroup ),
1099
        filters                 => $filter,
1100
        has_obsolete_reports    => $has_obsolete_reports,
1101
    );
1102
}
1159
}
1103
1160
1104
# pass $sth, get back an array of names for the column headers
1161
# pass $sth, get back an array of names for the column headers
(-)a/reports/orders_by_fund.pl (-13 / +13 lines)
Lines 1-22 Link Here
1
#!/usr/bin/perl
1
#!/usr/bin/perl
2
2
3
# This file is part of Koha.
3
# Copyright Frédérick Capovilla, 2011 - SYS-TECH
4
# Copyright Élyse Morin, 2012 - Libéo
4
#
5
#
5
# Author : Frédérick Capovilla, 2011 - SYS-TECH
6
# This file is part of Koha.
6
# Modified by : Élyse Morin, 2012 - Libéo
7
#
7
#
8
# Koha is free software; you can redistribute it and/or modify it under the
8
# Koha is free software; you can redistribute it and/or modify it
9
# terms of the GNU General Public License as published by the Free Software
9
# under the terms of the GNU General Public License as published by
10
# Foundation; either version 3 of the License, or (at your option) any later
10
# the Free Software Foundation; either version 3 of the License, or
11
# version.
11
# (at your option) any later version.
12
#
12
#
13
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# Koha is distributed in the hope that it will be useful, but
14
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# WITHOUT ANY WARRANTY; without even the implied warranty of
15
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU General Public License for more details.
16
#
17
#
17
# You should have received a copy of the GNU General Public License along with
18
# You should have received a copy of the GNU General Public License
18
# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
19
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
# Suite 330, Boston, MA  02111-1307 USA
20
20
21
=head1 orders_by_budget
21
=head1 orders_by_budget
22
22
(-)a/rspack.config.js (+34 lines)
Lines 161-164 module.exports = [ Link Here
161
            "datatables.net-buttons/js/buttons.colVis": "DataTable",
161
            "datatables.net-buttons/js/buttons.colVis": "DataTable",
162
        },
162
        },
163
    },
163
    },
164
    {
165
        entry: {
166
            "api-client.cjs":
167
                "./koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js",
168
        },
169
        output: {
170
            filename: "[name].js",
171
            path: path.resolve(__dirname, "t/cypress/plugins/dist/"),
172
            library: {
173
                type: "commonjs",
174
            },
175
            globalObject: "global",
176
        },
177
        target: "node",
178
        module: {
179
            rules: [
180
                {
181
                    test: /\.js$/,
182
                    loader: "builtin:swc-loader",
183
                    options: {
184
                        jsc: {
185
                            parser: {
186
                                syntax: "ecmascript",
187
                            },
188
                        },
189
                    },
190
                    exclude: [/node_modules/],
191
                    type: "javascript/auto",
192
                },
193
            ],
194
        },
195
        externals: [],
196
        plugins: [],
197
    },
164
];
198
];
(-)a/t/CookieManager.t (-50 / +47 lines)
Lines 19-27 Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
use CGI;
21
use CGI;
22
use Data::Dumper qw(Dumper);
22
23
#use Data::Dumper qw(Dumper);
23
use Test::NoWarnings;
24
use Test::NoWarnings;
24
use Test::More tests => 4;
25
use Test::More tests => 5;
25
26
26
use t::lib::Mocks;
27
use t::lib::Mocks;
27
28
Lines 29-57 use C4::Context; Link Here
29
use Koha::CookieManager;
30
use Koha::CookieManager;
30
31
31
subtest 'new' => sub {
32
subtest 'new' => sub {
32
    plan tests => 3;
33
    plan tests => 4;
33
34
34
    t::lib::Mocks::mock_config( Koha::CookieManager::DENY_LIST_VAR, 'just_one' );
35
    t::lib::Mocks::mock_config( Koha::CookieManager::KEEP_COOKIE_CONF_VAR, 'just_one' );
35
    my $cmgr = Koha::CookieManager->new;
36
    my $cmgr = Koha::CookieManager->new;
36
    is( scalar keys %{ $cmgr->{_remove_unless} }, 1, 'one entry' );
37
    is( scalar @{ $cmgr->{_keep_list} }, 1, 'one entry to keep' );
37
    is( exists $cmgr->{_secure},                  1, 'secure key found' );
38
    is( exists $cmgr->{_secure},         1, 'secure key found' );
38
39
39
    t::lib::Mocks::mock_config( Koha::CookieManager::DENY_LIST_VAR, [ 'two', 'entries' ] );
40
    t::lib::Mocks::mock_config( Koha::CookieManager::KEEP_COOKIE_CONF_VAR,   [ 'two', 'entries' ] );
41
    t::lib::Mocks::mock_config( Koha::CookieManager::REMOVE_COOKIE_CONF_VAR, ['test'] );
40
    $cmgr = Koha::CookieManager->new;
42
    $cmgr = Koha::CookieManager->new;
41
    is( scalar keys %{ $cmgr->{_remove_unless} }, 2, 'two entries' );
43
    is( scalar @{ $cmgr->{_keep_list} },   2, 'two entries to keep' );
44
    is( scalar @{ $cmgr->{_remove_list} }, 1, 'one entry to remove' );
42
};
45
};
43
46
44
subtest 'clear_unless' => sub {
47
subtest 'clear_unless' => sub {
45
    plan tests => 17;
48
    plan tests => 14;
46
49
47
    t::lib::Mocks::mock_config( Koha::CookieManager::DENY_LIST_VAR, [ 'aap', 'noot' ] );
50
    t::lib::Mocks::mock_config( Koha::CookieManager::KEEP_COOKIE_CONF_VAR,   [ 'aap', 'noot' ] );
51
    t::lib::Mocks::mock_config( Koha::CookieManager::REMOVE_COOKIE_CONF_VAR, ['mies'] );
48
52
49
    my $q    = CGI->new;
53
    my $q    = CGI->new;
50
    my $cmgr = Koha::CookieManager->new;
54
    my $cmgr = Koha::CookieManager->new;
51
55
52
    my $cookie1 = $q->cookie( -name => 'aap',  -value => 'aap', -expires => '+1d' );
56
    my $cookie1 = $q->cookie( -name => 'aap',  -value => 'aap', -expires => '+1d' );
53
    my $cookie2 = $q->cookie( -name => 'noot', -value => 'noot' );
57
    my $cookie2 = $q->cookie( -name => 'noot', -value => 'noot' );
54
    my $cookie3 = $q->cookie( -name => 'wim',  -value => q{wim},  -HttpOnly => 1 );
58
    my $cookie3 = $q->cookie( -name => 'wim',  -value => q{wim},  -HttpOnly => 0 );
55
    my $cookie4 = $q->cookie( -name => 'aap',  -value => q{aap2}, -HttpOnly => 1 );
59
    my $cookie4 = $q->cookie( -name => 'aap',  -value => q{aap2}, -HttpOnly => 1 );
56
    my $list    = [ $cookie1, $cookie2, $cookie3, $cookie4, 'mies', 'zus' ];    # 4 cookies, 2 names
60
    my $list    = [ $cookie1, $cookie2, $cookie3, $cookie4, 'mies', 'zus' ];    # 4 cookies, 2 names
57
61
Lines 59-112 subtest 'clear_unless' => sub { Link Here
59
    is( @{ $cmgr->clear_unless },                                 0, 'Empty list' );
63
    is( @{ $cmgr->clear_unless },                                 0, 'Empty list' );
60
    is( @{ $cmgr->clear_unless( { hash => 1 }, ['array'], $q ) }, 0, 'Empty list for invalid arguments' );
64
    is( @{ $cmgr->clear_unless( { hash => 1 }, ['array'], $q ) }, 0, 'Empty list for invalid arguments' );
61
65
62
    # Pass list, expect 5 cookies (3 cleared, last aap kept)
66
    # Pass list, expecting 4 cookies (2 kept, 1 untouched, 1 cleared); duplicate aap and zus discarded
63
    my @rv = @{ $cmgr->clear_unless(@$list) };
67
    my @rv = @{ $cmgr->clear_unless(@$list) };
64
    is( @rv,              5,       '5 expected' );
68
    is( @rv,              4,       '4 expected' );
65
    is( $rv[0]->name,     'noot',  '1st cookie' );
69
    is( $rv[0]->name,     'noot',  '1st cookie' );
66
    is( $rv[1]->name,     'wim',   '2nd cookie' );
70
    is( $rv[1]->name,     'wim',   '2nd cookie' );
67
    is( $rv[2]->name,     'aap',   '3rd cookie' );
71
    is( $rv[2]->name,     'aap',   '3rd cookie' );
68
    is( $rv[3]->name,     'mies',  '4th cookie' );
72
    is( $rv[3]->name,     'mies',  '4th cookie' );
69
    is( $rv[4]->name,     'zus',   '5th cookie' );
73
    is( $rv[0]->value,    q{noot}, 'noot kept' );
70
    is( $rv[0]->value,    q{noot}, 'noot not empty' );
74
    is( $rv[1]->value,    q{wim},  'wim untouched' );
71
    is( $rv[1]->value,    q{},     'wim empty' );
75
    is( $rv[2]->value,    q{aap2}, 'aap kept, last entry' );
72
    is( $rv[2]->value,    q{aap2}, 'aap not empty' );
76
    is( $rv[3]->value,    q{},     'mies cleared' );
73
    is( $rv[3]->value,    q{},     'mies empty' );
77
    is( $rv[1]->httponly, undef,   'wim still not httponly' );
74
    is( $rv[4]->value,    q{},     'zus empty' );
75
    is( $rv[1]->httponly, 0,       'cleared wim is not httponly' );
76
    is( $rv[2]->httponly, 1,       'aap httponly' );
78
    is( $rv[2]->httponly, 1,       'aap httponly' );
77
79
78
    # Test with numeric suffix (via regex)
80
    # Test with prefix (note trailing underscore)
79
    t::lib::Mocks::mock_config( Koha::CookieManager::DENY_LIST_VAR, ['catalogue_editor_\d+'] );
81
    t::lib::Mocks::mock_config( Koha::CookieManager::KEEP_COOKIE_CONF_VAR,   'catalogue_editor_' );
82
    t::lib::Mocks::mock_config( Koha::CookieManager::REMOVE_COOKIE_CONF_VAR, 'catalogue_editor' );
80
    $cmgr    = Koha::CookieManager->new;
83
    $cmgr    = Koha::CookieManager->new;
81
    $cookie1 = $q->cookie( -name => 'catalogue_editor_abc',  -value => '1', -expires => '+1y' );
84
    $cookie1 = $q->cookie( -name => 'catalogue_editor',   -value => '1' );
82
    $cookie2 = $q->cookie( -name => 'catalogue_editor_345',  -value => '1', -expires => '+1y' );
85
    $cookie2 = $q->cookie( -name => 'catalogue_editor2',  -value => '2' );
83
    $cookie3 = $q->cookie( -name => 'catalogue_editor_',     -value => '1', -expires => '+1y' );
86
    $cookie3 = $q->cookie( -name => 'catalogue_editor_3', -value => '3' );
84
    $cookie4 = $q->cookie( -name => 'catalogue_editor_123x', -value => '1', -expires => '+1y' );
87
85
88
    $list = [ $cookie1, $cookie2, $cookie3, 'catalogue_editor4' ];
86
    $list = [ $cookie1, $cookie2, $cookie3, $cookie4 ];
89
    my $result = [ map { defined( $_->max_age ) ? () : $_->name } @{ $cmgr->clear_unless(@$list) } ];
87
    @rv   = @{ $cmgr->clear_unless(@$list) };
90
    is_deeply( $result, ['catalogue_editor_3'], 'Only cookie3 is kept (not expired)' );
88
    is_deeply(
91
};
89
        [ map { $_->value ? $_->name : () } @rv ],
90
        ['catalogue_editor_345'],
91
        'Cookie2 should be found only'
92
    );
93
94
    # Test with another regex (yes, highly realistic examples :)
95
    t::lib::Mocks::mock_config( Koha::CookieManager::DENY_LIST_VAR, ['next_\w+_number\d{2}_(now|never)'] );
96
    $cmgr = Koha::CookieManager->new;
97
    my $cookie5;
98
    $cookie1 = $q->cookie( -name => 'next_mynewword_number99_never',          -value => '1', -expires => '+1y' );  #fine
99
    $cookie2 = $q->cookie( -name => 'prefixed_next_mynewword_number99_never', -value => '1', -expires => '+1y' )
100
        ;    # wrong prefix
101
    $cookie3 = $q->cookie( -name => 'next_mynew-word_number99_never', -value => '1', -expires => '+1y' )
102
        ;    # wrong: hyphen in word
103
    $cookie4 =
104
        $q->cookie( -name => 'mynewword_number999_never', -value => '1', -expires => '+1y' );    # wrong: three digits
105
    $cookie5 =
106
        $q->cookie( -name => 'next_mynewword_number99_always', -value => '1', -expires => '+1y' );    # wrong: always
107
    @rv = @{ $cmgr->clear_unless( $cookie1, $cookie2, $cookie3, $cookie4, $cookie5 ) };
108
    is_deeply( [ map { $_->value ? $_->name : () } @rv ], ['next_mynewword_number99_never'], 'Only cookie1 matched' );
109
92
93
subtest 'path exception' => sub {
94
    plan tests => 4;
95
96
    t::lib::Mocks::mock_config( Koha::CookieManager::REMOVE_COOKIE_CONF_VAR, ['always_show_holds'] );
97
    my $q       = CGI->new;
98
    my $cmgr    = Koha::CookieManager->new;
99
    my $cookie1 = $q->cookie( -name => 'always_show_holds', -value => 'DO', path => '/cgi-bin/koha/reserve' );
100
    my @rv      = @{ $cmgr->clear_unless($cookie1) };
101
    is( $rv[0]->name,    'always_show_holds',     'Check name' );
102
    is( $rv[0]->path,    '/cgi-bin/koha/reserve', 'Check path' );
103
    is( $rv[0]->max_age, 0,                       'Check max_age' );
104
    my $cookie2 = $q->cookie( -name => 'always_show_holds', -value => 'DONT' );    # default path
105
    @rv = @{ $cmgr->clear_unless($cookie2) };
106
    is( $rv[0]->path, '/cgi-bin/koha/reserve', 'Check path cookie2, corrected here' );
110
};
107
};
111
108
112
subtest 'replace_in_list' => sub {
109
subtest 'replace_in_list' => sub {
(-)a/t/Form_MessagingPreferences.t (-10 / +68 lines)
Lines 1-15 Link Here
1
#!/usr/bin/perl
1
#!/usr/bin/perl
2
#
3
# This Koha test module is a stub!
4
# Add more tests here!!!
5
2
6
use strict;
3
use Modern::Perl;
7
use warnings;
4
use CGI;
5
use Template;
8
6
9
use Test::NoWarnings;
7
use Test::More tests => 1;
10
use Test::More tests => 2;
8
use Test::MockModule;
11
9
12
BEGIN {
10
#use Test::NoWarnings;
13
    use_ok('C4::Form::MessagingPreferences');
11
use t::lib::TestBuilder;
14
}
12
use t::lib::Mocks;
15
13
14
use C4::Form::MessagingPreferences;
15
16
my $builder = t::lib::TestBuilder->new;
17
my $schema  = Koha::Database->new->schema;
18
19
subtest 'restore_form_values' => sub {
20
21
    plan tests => 2;
22
23
    my $cgi             = CGI->new;
24
    my $template_module = Test::MockModule->new('Template');
25
    my $vars            = {};
26
    $template_module->mock( 'param', sub { my ( $self, $key, $val ) = @_; $vars->{$key} = $val; } );
27
    my $template = Template->new( ENCODING => 'UTF-8' );
28
29
    $schema->storage->txn_begin;
30
31
    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
32
    t::lib::Mocks::mock_preference( 'EnhancedMessagingPreferences', 1 );
33
34
    C4::Form::MessagingPreferences::set_form_values( { borrowernumber => $patron->id }, $template );
35
    my $set_form_values_vars = {%$vars};
36
    $vars = {};
37
38
    C4::Form::MessagingPreferences::restore_form_values( $cgi, $template );
39
    my $restore_form_values_vars = {%$vars};
40
41
    is_deeply(
42
        $set_form_values_vars, $restore_form_values_vars,
43
        "Default messaging preferences don't change when handled with restore_form_values."
44
    );
45
46
    C4::Members::Messaging::SetMessagingPreference(
47
        {
48
            borrowernumber          => $patron->id,
49
            message_transport_types => ['email'],
50
            message_attribute_id    => 2,
51
            days_in_advance         => 10,
52
            wants_digest            => 1
53
        }
54
    );
55
56
    C4::Form::MessagingPreferences::set_form_values( { borrowernumber => $patron->id }, $template );
57
    $set_form_values_vars = {%$vars};
58
    $vars                 = {};
59
60
    $cgi->param( -name => '2',      -value => 'email' );
61
    $cgi->param( -name => '2-DAYS', -value => '10' );
62
    $cgi->param( -name => 'digest', -value => '2' );
63
64
    C4::Form::MessagingPreferences::restore_form_values( $cgi, $template );
65
    $restore_form_values_vars = {%$vars};
66
67
    is_deeply(
68
        $set_form_values_vars, $restore_form_values_vars,
69
        "Patrons messaging preferences don't change when handled with restore_form_values."
70
    );
71
72
    $schema->storage->txn_rollback;
73
};
(-)a/t/ImportBatch.t (-3 / +21 lines)
Lines 17-27 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 4;
21
use Test::NoWarnings;
20
use File::Temp qw|tempfile|;
22
use File::Temp qw|tempfile|;
21
use MARC::Field;
23
use MARC::Field;
22
use MARC::File::XML;
24
use MARC::File::XML;
23
use MARC::Record;
25
use MARC::Record;
24
use Test::More tests => 3;
25
use t::lib::Mocks;
26
use t::lib::Mocks;
26
27
27
BEGIN {
28
BEGIN {
Lines 55-65 subtest 'RecordsFromMARCXMLFile' => sub { Link Here
55
56
56
    my ( $errors, $recs );
57
    my ( $errors, $recs );
57
    my $file = create_file( { whitespace => 1, format => 'marcxml' } );
58
    my $file = create_file( { whitespace => 1, format => 'marcxml' } );
58
    ( $errors, $recs ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, 'UTF-8' );
59
    {
60
        # Ignore the following warning
61
        # Use of uninitialized value in concatenation (.) or string at /usr/share/perl5/MARC/File/XML.pm line 399, <__ANONIO__> chunk 1.
62
        # We do not want to expect it (using Test::Warn): it is a bug from MARC::File::XML
63
        local $SIG{__WARN__} = sub { };
64
        my $dup_err;
65
        local *STDERR;
66
        open STDERR, ">>", \$dup_err;
67
        ( $errors, $recs ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, 'UTF-8' );
68
        close STDERR;
69
    }
59
    is( @$recs, 0, 'No records from empty marcxml file' );
70
    is( @$recs, 0, 'No records from empty marcxml file' );
60
71
61
    $file = create_file( { garbage => 1, format => 'marcxml' } );
72
    $file = create_file( { garbage => 1, format => 'marcxml' } );
62
    ( $errors, $recs ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, 'UTF-8' );
73
    {
74
        local $SIG{__WARN__} = sub { };
75
        my $dup_err;
76
        local *STDERR;
77
        open STDERR, ">>", \$dup_err;
78
        ( $errors, $recs ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, 'UTF-8' );
79
        close STDERR;
80
    }
63
    is( @$recs, 0, 'Garbage returns no records' );
81
    is( @$recs, 0, 'Garbage returns no records' );
64
82
65
    $file = create_file( { two => 1, format => 'marcxml' } );
83
    $file = create_file( { two => 1, format => 'marcxml' } );
(-)a/t/Koha/I18N.t (-1 / +47 lines)
Lines 2-8 Link Here
2
2
3
use Modern::Perl;
3
use Modern::Perl;
4
use Test::NoWarnings;
4
use Test::NoWarnings;
5
use Test::More tests => 36;
5
use Test::More tests => 37;
6
use Test::MockModule;
6
use Test::MockModule;
7
use FindBin qw($Bin);
7
use FindBin qw($Bin);
8
use Encode;
8
use Encode;
Lines 61-63 my @tests = ( Link Here
61
foreach my $test (@tests) {
61
foreach my $test (@tests) {
62
    is( $test->[0], decode_utf8( $test->[1] ), $test->[1] );
62
    is( $test->[0], decode_utf8( $test->[1] ), $test->[1] );
63
}
63
}
64
65
subtest 'available_locales' => sub {
66
    plan tests => 6;
67
68
    # Test basic functionality
69
    my $locales = Koha::I18N::available_locales();
70
71
    # Should return an arrayref
72
    is( ref($locales), 'ARRAY', 'available_locales returns an arrayref' );
73
74
    # Should have at least the default option
75
    ok( scalar(@$locales) >= 1, 'At least one locale returned (default)' );
76
77
    # First locale should be default
78
    is( $locales->[0]->{value}, 'default',                   'First locale is default' );
79
    is( $locales->[0]->{text},  'Default Unicode collation', 'Default locale has correct text' );
80
81
    # All locales should have value and text keys
82
    my $all_have_keys = 1;
83
    for my $locale (@$locales) {
84
        unless ( exists $locale->{value} && exists $locale->{text} ) {
85
            $all_have_keys = 0;
86
            last;
87
        }
88
    }
89
    ok( $all_have_keys, 'All locales have value and text keys' );
90
91
    # Test structure for real system locales (if any)
92
    my $system_locales = [ grep { $_->{value} ne 'default' } @$locales ];
93
    if (@$system_locales) {
94
95
        # Should have friendly display names for common locales
96
        my $has_friendly_name = 0;
97
        for my $locale (@$system_locales) {
98
            if ( $locale->{text} =~ /^[A-Z][a-z]+ \([^)]+\) - / ) {
99
                $has_friendly_name = 1;
100
                last;
101
            }
102
        }
103
        ok( $has_friendly_name, 'System locales have friendly display names' ) if @$system_locales;
104
    } else {
105
106
        # If no system locales, just pass this test
107
        ok( 1, 'No system locales found (test environment)' );
108
    }
109
};
(-)a/t/Koha/SearchEngine/Elasticsearch/Search.t (-1 / +126 lines)
Lines 18-31 Link Here
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::NoWarnings;
20
use Test::NoWarnings;
21
use Test::More tests => 3;
21
use Test::More tests => 5;
22
use Test::MockModule;
22
use Test::MockModule;
23
use t::lib::Mocks;
23
use t::lib::Mocks;
24
use Encode       qw( encode );
24
use Encode       qw( encode );
25
use MIME::Base64 qw( encode_base64 );
25
use MIME::Base64 qw( encode_base64 );
26
26
27
use utf8;
28
27
use_ok('Koha::SearchEngine::Elasticsearch::Search');
29
use_ok('Koha::SearchEngine::Elasticsearch::Search');
28
30
31
subtest '_sort_facets' => sub {
32
    plan tests => 3;
33
    t::lib::Mocks::mock_preference( 'SearchEngine', 'Elasticsearch' );
34
35
    my $facets = _get_facets();
36
37
    my @normal_sort_facets     = sort { $a->{facet_label_value} cmp $b->{facet_label_value} } @$facets;
38
    my @normal_expected_facets = (
39
        { facet_label_value => 'Ari' },
40
        { facet_label_value => 'Fairy' },
41
        { facet_label_value => 'Harry' },
42
        { facet_label_value => 'Mary' },
43
        { facet_label_value => 'Zambidis' },
44
        { facet_label_value => 'ari' },
45
        { facet_label_value => 'fairy' },
46
        { facet_label_value => 'harry' },
47
        { facet_label_value => 'mary' },
48
        { facet_label_value => 'Ã…berg, Erik' },
49
        { facet_label_value => 'Ã…uthor' },
50
        { facet_label_value => 'étienne' },
51
        { facet_label_value => 'Šostakovitš, Dmitri' },
52
    );
53
54
    #NOTE: stringwise/bytewise is not UTF-8 friendly
55
    is_deeply( \@normal_sort_facets, \@normal_expected_facets, "Perl's built-in sort is stringwise/bytewise." );
56
57
    my $search = Koha::SearchEngine::Elasticsearch::Search->new(
58
        { index => $Koha::SearchEngine::Elasticsearch::AUTHORITIES_INDEX } );
59
60
    #NOTE: The 'default' locale uses the Default Unicode Collation Element Table, which
61
    #is used for the locales of English (en) and French (fr).
62
    my $sorted_facets = $search->_sort_facets( { facets => $facets, locale => 'default' } );
63
    my $expected      = [
64
        { facet_label_value => 'Ã…berg, Erik' },
65
        { facet_label_value => 'ari' },
66
        { facet_label_value => 'Ari' },
67
        { facet_label_value => 'Ã…uthor' },
68
        { facet_label_value => 'étienne' },
69
        { facet_label_value => 'fairy' },
70
        { facet_label_value => 'Fairy' },
71
        { facet_label_value => 'harry' },
72
        { facet_label_value => 'Harry' },
73
        { facet_label_value => 'mary' },
74
        { facet_label_value => 'Mary' },
75
        { facet_label_value => 'Šostakovitš, Dmitri' },
76
        { facet_label_value => 'Zambidis' },
77
    ];
78
    is_deeply( $sorted_facets, $expected, "Facets sorted correctly with default locale" );
79
80
    # Test system preference integration
81
    t::lib::Mocks::mock_preference( 'FacetSortingLocale', 'en_US.utf8' );
82
    my $sorted_facets_syspref = $search->_sort_facets( { facets => $facets } );
83
84
    # Should return sorted facets (exact order may vary by system locale availability)
85
    is( ref($sorted_facets_syspref), 'ARRAY', "System preference integration works" );
86
87
    #NOTE: If "locale" is not provided to _sort_facets, it will look up the LC_COLLATE
88
    #for the local system. This is what allows this function to work well in production.
89
    #However, since LC_COLLATE could vary from system to system running these unit tests,
90
    #we can't test it reliably here.
91
};
92
93
subtest '_sort_facets_zebra with fi_FI locale' => sub {
94
    plan tests => 1;
95
    my $locale_map = _get_locale_map();
96
SKIP: {
97
        skip( "fi_FI.utf8 locale not available on this system", 1 ) unless $locale_map->{"fi_FI.utf8"};
98
99
        my $facets = _get_facets();
100
101
        my $search = Koha::SearchEngine::Elasticsearch::Search->new(
102
            { index => $Koha::SearchEngine::Elasticsearch::AUTHORITIES_INDEX } );
103
104
        # Test with explicit locale parameter
105
        my $sorted_facets_explicit = $search->_sort_facets( { facets => $facets, locale => 'fi_FI' } );
106
        my $expected               = [
107
            { facet_label_value => 'ari' },
108
            { facet_label_value => 'Ari' },
109
            { facet_label_value => 'étienne' },
110
            { facet_label_value => 'fairy' },
111
            { facet_label_value => 'Fairy' },
112
            { facet_label_value => 'harry' },
113
            { facet_label_value => 'Harry' },
114
            { facet_label_value => 'mary' },
115
            { facet_label_value => 'Mary' },
116
            { facet_label_value => 'Šostakovitš, Dmitri' },
117
            { facet_label_value => 'Zambidis' },
118
            { facet_label_value => 'Ã…berg, Erik' },
119
            { facet_label_value => 'Ã…uthor' },
120
        ];
121
        is_deeply( $sorted_facets_explicit, $expected, "Zebra facets sorted correctly with explicit locale" );
122
    }
123
};
124
29
subtest 'search_auth_compat' => sub {
125
subtest 'search_auth_compat' => sub {
30
    plan tests => 7;
126
    plan tests => 7;
31
127
Lines 102-105 subtest 'search_auth_compat' => sub { Link Here
102
    is( @$results[0]->{series}, 1, 'Valid main heading with ShowHeadingUse' );
198
    is( @$results[0]->{series}, 1, 'Valid main heading with ShowHeadingUse' );
103
};
199
};
104
200
201
sub _get_facets {
202
    my $facets = [
203
        { facet_label_value => 'Mary' },
204
        { facet_label_value => 'Harry' },
205
        { facet_label_value => 'Fairy' },
206
        { facet_label_value => 'Ari' },
207
        { facet_label_value => 'mary' },
208
        { facet_label_value => 'harry' },
209
        { facet_label_value => 'Ã…berg, Erik' },
210
        { facet_label_value => 'Ã…uthor' },
211
        { facet_label_value => 'fairy' },
212
        { facet_label_value => 'ari' },
213
        { facet_label_value => 'étienne' },
214
        { facet_label_value => 'Šostakovitš, Dmitri' },
215
        { facet_label_value => 'Zambidis' },
216
    ];
217
    return $facets;
218
}
219
220
sub _get_locale_map {
221
    my $map     = {};
222
    my @locales = `locale -a`;
223
    foreach my $locale (@locales) {
224
        chomp($locale);
225
        $map->{$locale} = 1;
226
    }
227
    return $map;
228
}
229
105
1;
230
1;
(-)a/t/Labels.t (-3 / +3 lines)
Lines 17-27 Link Here
17
#
17
#
18
# for context, see http://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=2691
18
# for context, see http://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=2691
19
19
20
use strict;
20
use Modern::Perl;
21
use warnings;
22
21
23
use C4::ClassSplitRoutine::LCC qw( split_callnumber );
22
use C4::ClassSplitRoutine::LCC qw( split_callnumber );
24
use Test::More tests => 11;
23
use Test::More tests => 12;
24
use Test::NoWarnings;
25
25
26
BEGIN {
26
BEGIN {
27
    use_ok( 'C4::Labels::Label', qw( _get_text_fields _check_params _guide_box ) );
27
    use_ok( 'C4::Labels::Label', qw( _get_text_fields _check_params _guide_box ) );
(-)a/t/RecordProcessor.t (-1 / +9 lines)
Lines 215-221 subtest 'options() tests' => sub { Link Here
215
215
216
subtest "'TrimFields' filter tests" => sub {
216
subtest "'TrimFields' filter tests" => sub {
217
217
218
    plan tests => 2;
218
    plan tests => 4;
219
219
220
    # Test default values with a MARC::Record record
220
    # Test default values with a MARC::Record record
221
    my $record = MARC::Record->new();
221
    my $record = MARC::Record->new();
Lines 225-230 subtest "'TrimFields' filter tests" => sub { Link Here
225
        [ '150', ' ', ' ', a => 'Test' ],
225
        [ '150', ' ', ' ', a => 'Test' ],
226
        [ '520', ' ', ' ', a => "This is\na test!\t" ],
226
        [ '520', ' ', ' ', a => "This is\na test!\t" ],
227
        [ '521', ' ', ' ', a => "This is a\t test!\t" ],
227
        [ '521', ' ', ' ', a => "This is a\t test!\t" ],
228
        [ '522', ' ', ' ', a => "This is a test!", b => "   " ],
229
        [ '523', ' ', ' ', a => "   " ],
228
    );
230
    );
229
231
230
    my $p = Koha::RecordProcessor->new( { filters => ['TrimFields'] } );
232
    my $p = Koha::RecordProcessor->new( { filters => ['TrimFields'] } );
Lines 235-238 subtest "'TrimFields' filter tests" => sub { Link Here
235
237
236
    my $get521a = $record->subfield( '521', 'a' );
238
    my $get521a = $record->subfield( '521', 'a' );
237
    is( $get521a, "This is a\t test!", "Trailing tabs are stripped while inner tabs are kept" );
239
    is( $get521a, "This is a\t test!", "Trailing tabs are stripped while inner tabs are kept" );
240
241
    my $get522b = $record->subfield( '522', 'b' );
242
    isnt( $get522b, "", "Subfield containing spaces only removed from the field" );
243
244
    my $get523 = $record->field('523');
245
    is( $get523, undef, "Field with only a subfield containing spaces removed from the record" );
238
};
246
};
(-)a/t/Scrubber.t (-77 / +141 lines)
Lines 1-94 Link Here
1
#!/usr/bin/perl
1
#!/usr/bin/perl
2
2
3
# Copyright 2025 Koha Development team
4
#
5
# This file is part of Koha
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>
19
3
use Modern::Perl;
20
use Modern::Perl;
4
21
5
$| = 1;
22
use Test::More tests => 7;
6
use Test::NoWarnings;
23
use Test::NoWarnings;
7
use Test::More tests => 27;
24
use Test::Exception;
8
use Test::Warn;
25
use Test::Warn;
9
26
10
BEGIN {
27
BEGIN {
11
    use FindBin;
12
    use lib $FindBin::Bin;
13
    use_ok('C4::Scrubber');
28
    use_ok('C4::Scrubber');
14
}
29
}
15
30
16
sub pretty_line {
31
subtest 'new() constructor tests' => sub {
17
    my $max = 54;
32
    plan tests => 8;
18
    (@_) or return "#" x $max . "\n";
19
    my $phrase = "  " . shift() . "  ";
20
    my $half   = "#" x ( ( $max - length($phrase) ) / 2 );
21
    return $half . $phrase . $half . "\n";
22
}
23
33
24
my ( $scrubber, $html, $result, @types, $collapse );
34
    my $scrubber;
25
$collapse = 1;
35
    lives_ok { $scrubber = C4::Scrubber->new() } 'Constructor with no parameters succeeds';
26
@types    = qw(default comment note);
36
    isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor returns HTML::Scrubber object' );
27
$html     = q|
37
28
<![CDATA[selfdestruct]]&#x5d;>
38
    lives_ok { $scrubber = C4::Scrubber->new('default') } 'Constructor with default type succeeds';
29
<?php  echo(" EVIL EVIL EVIL "); ?>    <!-- COMMENT -->
39
    isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor with default type returns HTML::Scrubber object' );
30
<hr> <!-- TMPL_VAR NAME="password" -->
31
<style type="text/css">body{display:none;}</style>
32
<link media="screen" type="text/css" rev="stylesheet" rel="stylesheet" href="css.css">
33
<I FAKE="attribute" > I am ITALICS with fake="attribute" </I><br />
34
<em FAKE="attribute" > I am em with fake="attribute" </em><br />
35
<B> I am BOLD </B><br />
36
<span style="background-image: url(http://hackersite.cn/porno.jpg);"> I am a span w/ style.  Bad style.</span>
37
<span> I am a span trying to inject a link: &lt;a href="badlink.html"&gt; link &lt;/a&gt;</span>
38
<br>
39
<A NAME="evil">
40
	<A HREF="javascript:alert('OMG YOO R HACKED');">I am a link firing javascript.</A>
41
	<br />
42
	<A HREF="image/bigone.jpg" ONMOUSEOVER="alert('OMG YOO R HACKED');"> 
43
		<IMG SRC="image/smallone.jpg" ALT="ONMOUSEOVER JAVASCRIPT">
44
	</A>
45
</A> <br> 
46
At the end here, I actually have some regular text.
47
|;
48
49
ok( $scrubber = C4::Scrubber->new(), "Constructor: C4::Scrubber->new()" );
50
51
isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor returns HTML::Scrubber object' );
52
53
warning_like { $scrubber->default() } '', "\$scrubber->default ran without fault.";
54
warning_like { $scrubber->comment() } '', "\$scrubber->comment ran without fault.";
55
warning_like { $scrubber->process() } '', "\$scrubber->process ran without fault.";
56
57
ok( $result = $scrubber->scrub($html), "Getting scrubbed text (type: [default])" );
58
59
foreach (@types) {
60
    ok( $scrubber = C4::Scrubber->new($_), "testing Constructor: C4::Scrubber->new($_)" );
61
62
    warning_like { $scrubber->default() } '', "\$scrubber->default ran without fault.";
63
    warning_like { $scrubber->comment() } '', "\$scrubber->comment ran without fault.";
64
    warning_like { $scrubber->process() } '', "\$scrubber->process ran without fault.";
65
66
    ok( $result = $scrubber->scrub($html), "Getting scrubbed text (type: $_)" );
67
}
68
40
69
#Test for invalid new entry
41
    lives_ok { $scrubber = C4::Scrubber->new('comment') } 'Constructor with comment type succeeds';
70
eval {
42
    isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor with comment type returns HTML::Scrubber object' );
71
    C4::Scrubber->new("");
43
72
    fail("test should fail on entry of ''");
44
    lives_ok { $scrubber = C4::Scrubber->new('note') } 'Constructor with note type succeeds';
45
    isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor with note type returns HTML::Scrubber object' );
73
};
46
};
74
if ($@) {
75
    pass("Test should have failed on entry of '' (empty string) and it did. YAY!");
76
}
77
47
78
eval {
48
subtest 'constructor error handling' => sub {
79
    C4::Scrubber->new("Client");
49
    plan tests => 5;
80
    fail("test should fail on entry of 'Client'");
50
51
    my $scrubber;
52
    lives_ok { $scrubber = C4::Scrubber->new(undef) } 'Constructor with undef type succeeds (treated as default)';
53
    isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor with undef type returns HTML::Scrubber object' );
54
55
    throws_ok {
56
        C4::Scrubber->new('');
57
    }
58
    qr/New called with unrecognized type/, 'Constructor throws exception for empty string type';
59
60
    throws_ok {
61
        C4::Scrubber->new('invalid_type');
62
    }
63
    qr/New called with unrecognized type/, 'Constructor throws exception for invalid type';
64
65
    throws_ok {
66
        C4::Scrubber->new('Client');
67
    }
68
    qr/New called with unrecognized type/, 'Constructor throws exception for Client type';
69
};
70
71
subtest 'default scrubber functionality' => sub {
72
    plan tests => 5;
73
74
    my $scrubber = C4::Scrubber->new('default');
75
76
    my $malicious_html = q|
77
        <![CDATA[selfdestruct]]&#x5d;>
78
        <?php echo("EVIL EVIL EVIL"); ?>
79
        <script>alert('XSS Attack!');</script>
80
        <style type="text/css">body{display:none;}</style>
81
        <link href="evil.css" rel="stylesheet">
82
        <img src="x" onerror="alert('XSS')">
83
        <a href="javascript:alert('XSS')">Click me</a>
84
        <p onclick="alert('XSS')">Paragraph</p>
85
        <div style="background:url(javascript:alert('XSS'))">Content</div>
86
        Plain text content
87
    |;
88
89
    my $result = $scrubber->scrub($malicious_html);
90
91
    unlike( $result, qr/<script/i,     'Script tags are removed' );
92
    unlike( $result, qr/<style/i,      'Style tags are removed' );
93
    unlike( $result, qr/<link/i,       'Link tags are removed' );
94
    unlike( $result, qr/javascript:/i, 'JavaScript URLs are removed' );
95
    like( $result, qr/Plain text content/, 'Plain text content is preserved' );
96
};
97
98
subtest 'comment scrubber functionality' => sub {
99
    plan tests => 10;
100
101
    my $scrubber = C4::Scrubber->new('comment');
102
103
    my $test_html =
104
        '<p>Paragraph</p><b>Bold</b><i>Italic</i><em>Emphasis</em><big>Big</big><small>Small</small><strong>Strong</strong><br><u>Underline</u><hr><span>Span</span><div>Div</div><script>Evil</script>';
105
106
    my $result = $scrubber->scrub($test_html);
107
108
    like( $result, qr/<b>Bold<\/b>/,             'Bold tags are preserved' );
109
    like( $result, qr/<i>Italic<\/i>/,           'Italic tags are preserved' );
110
    like( $result, qr/<em>Emphasis<\/em>/,       'Em tags are preserved' );
111
    like( $result, qr/<big>Big<\/big>/,          'Big tags are preserved' );
112
    like( $result, qr/<small>Small<\/small>/,    'Small tags are preserved' );
113
    like( $result, qr/<strong>Strong<\/strong>/, 'Strong tags are preserved' );
114
    like( $result, qr/<br>/,                     'Break tags are preserved' );
115
116
    unlike( $result, qr/<p>/,      'Paragraph tags are removed' );
117
    unlike( $result, qr/<span>/,   'Span tags are removed' );
118
    unlike( $result, qr/<script>/, 'Script tags are removed' );
81
};
119
};
82
if ($@) {
83
    pass("Test should have failed on entry of 'Client' and it did. YAY!");
84
}
85
120
86
my $scrub_text =
121
subtest 'note scrubber functionality' => sub {
87
    '<div><span><p><b>bold</b><i>ital</i><em>emphatic</em><big>embiggen</big><small>shrink</small><strong>strongbad</strong><br><u>under</u><hr></p></span></div>';
122
    plan tests => 22;
88
my $scrub_comment =
123
89
    '<b>bold</b><i>ital</i><em>emphatic</em><big>embiggen</big><small>shrink</small><strong>strongbad</strong><br>under';
124
    my $scrubber = C4::Scrubber->new('note');
90
is( C4::Scrubber->new('comment')->scrub($scrub_text), $scrub_comment, "Comment scrubber removes expected elements" );
125
91
is(
126
    my $comprehensive_html =
92
    C4::Scrubber->new('note')->scrub($scrub_text), $scrub_text,
127
        '<div><span><p><b>Bold</b><i>Italic</i><em>Emphasis</em><big>Big</big><small>Small</small><strong>Strong</strong><br><u>Underline</u><hr><ol><li>Ordered item 1</li><li>Ordered item 2</li></ol><ul><li>Unordered item 1</li><li>Unordered item 2</li></ul><dl><dt>Term</dt><dd>Definition</dd></dl></p></span></div>';
93
    "Note scrubber removes (additional) expected elements"
128
94
);
129
    my $result = $scrubber->scrub($comprehensive_html);
130
131
    like( $result, qr/<div>/,                    'Div tags are preserved' );
132
    like( $result, qr/<span>/,                   'Span tags are preserved' );
133
    like( $result, qr/<p>/,                      'Paragraph tags are preserved' );
134
    like( $result, qr/<b>Bold<\/b>/,             'Bold tags are preserved' );
135
    like( $result, qr/<i>Italic<\/i>/,           'Italic tags are preserved' );
136
    like( $result, qr/<em>Emphasis<\/em>/,       'Em tags are preserved' );
137
    like( $result, qr/<big>Big<\/big>/,          'Big tags are preserved' );
138
    like( $result, qr/<small>Small<\/small>/,    'Small tags are preserved' );
139
    like( $result, qr/<strong>Strong<\/strong>/, 'Strong tags are preserved' );
140
    like( $result, qr/<br>/,                     'Break tags are preserved' );
141
    like( $result, qr/<u>Underline<\/u>/,        'Underline tags are preserved' );
142
    like( $result, qr/<hr>/,                     'HR tags are preserved' );
143
    like( $result, qr/<ol>/,                     'Ordered list tags are preserved' );
144
    like( $result, qr/<ul>/,                     'Unordered list tags are preserved' );
145
    like( $result, qr/<li>/,                     'List item tags are preserved' );
146
    like( $result, qr/<dl>/,                     'Description list tags are preserved' );
147
    like( $result, qr/<dt>Term<\/dt>/,           'Description term tags are preserved' );
148
    like( $result, qr/<dd>Definition<\/dd>/,     'Description definition tags are preserved' );
149
150
    is( $result, $comprehensive_html, 'All allowed tags in note scrubber are preserved exactly' );
151
152
    my $malicious_note = '<p>Safe content</p><script>alert("XSS")</script><iframe src="evil.html"></iframe>';
153
    my $safe_result    = $scrubber->scrub($malicious_note);
154
155
    like( $safe_result, qr/<p>Safe content<\/p>/, 'Safe content is preserved' );
156
    unlike( $safe_result, qr/<script>/, 'Script tags are removed from notes' );
157
    unlike( $safe_result, qr/<iframe>/, 'Iframe tags are removed from notes' );
158
};
(-)a/t/Test/Mock/Logger.t (-5 / +15 lines)
Lines 1-5 Link Here
1
use Modern::Perl;
1
use Modern::Perl;
2
use Test::More tests => 10;
2
use Test::More tests => 11;
3
use Test::NoWarnings;
3
use Test::Warn;
4
use Test::Warn;
4
5
5
# Module under test
6
# Module under test
Lines 132-147 subtest 'Method chaining tests' => sub { Link Here
132
    isa_ok( $result, 't::lib::Mocks::Logger', 'Method chaining returns the logger object' );
133
    isa_ok( $result, 't::lib::Mocks::Logger', 'Method chaining returns the logger object' );
133
};
134
};
134
135
135
# Test diag method (output capture is complex, just verify it runs)
136
subtest 'Diag method test' => sub {
136
subtest 'Diag method test' => sub {
137
    plan tests => 1;
137
    plan tests => 1;
138
138
139
    $logger->clear();
139
    $logger->clear();
140
    $mocked_logger->debug('Debug message');
140
    $mocked_logger->debug('Debug message');
141
141
142
    # Just make sure it doesn't throw an exception
142
    # Capture Test::Builder diag output
143
    eval { $logger->diag(); };
143
    my $diag_output = '';
144
    is( $@, '', 'diag() method executed without errors' );
144
    open my $fake_fh, '>', \$diag_output or die "Can't open: $!";
145
146
    my $tb          = Test::More->builder;
147
    my $original_fh = $tb->failure_output;
148
    $tb->failure_output($fake_fh);    # Redirect diag output
149
150
    $logger->diag();
151
152
    $tb->failure_output($original_fh);
153
154
    like( $diag_output, qr/debug:\n#\s*"Debug message"/xms, 'Captured diag output' );
145
};
155
};
146
156
147
# Test handling of empty log buffers
157
# Test handling of empty log buffers
(-)a/t/cypress/integration/Acquisitions/Vendors_spec.ts (+66 lines)
Lines 233-235 describe("Vendor CRUD operations", () => { Link Here
233
            .contains("deleted");
233
            .contains("deleted");
234
    });
234
    });
235
});
235
});
236
237
describe("Vendor module", () => {
238
    beforeEach(() => {
239
        cy.login();
240
        cy.title().should("eq", "Koha staff interface");
241
242
        cy.task("buildSampleObject", {
243
            object: "vendor",
244
            values: { active: 1 },
245
        })
246
            .then(generatedVendor => {
247
                delete generatedVendor.list_currency;
248
                delete generatedVendor.invoice_currency;
249
                return cy.task("insertObject", {
250
                    type: "vendor",
251
                    object: generatedVendor,
252
                });
253
            })
254
            .then(vendor => {
255
                cy.wrap(vendor).as("vendor");
256
                return cy.task("buildSampleObject", {
257
                    object: "basket",
258
                    values: { vendor_id: vendor.id },
259
                });
260
            })
261
            .then(generatedBasket => {
262
                return cy.task("insertObject", {
263
                    type: "basket",
264
                    object: generatedBasket,
265
                });
266
            })
267
            .then(basket => {
268
                cy.wrap(basket).as("basket");
269
            });
270
    });
271
272
    afterEach(function () {
273
        cy.task("deleteSampleObjects", [
274
            { vendor: this.vendor, basket: this.basket },
275
        ]);
276
    });
277
278
    it("receive should open in the same tab", function () {
279
        cy.visit("/cgi-bin/koha/acquisition/vendors");
280
281
        // table_id is currently 'DataTables_Table_0', and it should be fixed
282
        cy.get("#vendors_list table.dataTable")
283
            .invoke("attr", "id")
284
            .then(table_id => {
285
                cy.intercept("GET", "/api/v1/acquisitions/vendors*").as(
286
                    "get-vendors"
287
                );
288
                cy.get(`#${table_id}_wrapper input.dt-input`).type(
289
                    this.vendor.name
290
                );
291
                cy.wait("@get-vendors");
292
                cy.get(`#${table_id} tbody tr:first`)
293
                    .contains("Receive shipments")
294
                    .click();
295
                cy.url().should(
296
                    "contain",
297
                    `/cgi-bin/koha/acqui/parcels.pl?booksellerid=${this.vendor.id}`
298
                );
299
            });
300
    });
301
});
(-)a/t/cypress/integration/Auth/csrf.ts (-33 / +33 lines)
Lines 5-11 const branchname = "test_branchname"; Link Here
5
5
6
function cleanup() {
6
function cleanup() {
7
    const sql = "DELETE FROM branches WHERE branchcode=?";
7
    const sql = "DELETE FROM branches WHERE branchcode=?";
8
    cy.query(sql, branchcode);
8
    cy.task("query", { sql, values: [branchcode] });
9
}
9
}
10
10
11
describe("CSRF", () => {
11
describe("CSRF", () => {
Lines 32-41 describe("CSRF", () => { Link Here
32
            .find(".alert")
32
            .find(".alert")
33
            .contains(/No CSRF token passed for POST/);
33
            .contains(/No CSRF token passed for POST/);
34
34
35
        cy.query(
35
        cy.task("query", {
36
            "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
36
            sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
37
            branchcode
37
            values: [branchcode],
38
        ).then(result => {
38
        }).then(result => {
39
            expect(result[0].count).to.equal(0);
39
            expect(result[0].count).to.equal(0);
40
        });
40
        });
41
    });
41
    });
Lines 53-62 describe("CSRF", () => { Link Here
53
            .find(".alert")
53
            .find(".alert")
54
            .contains(/Wrong CSRF token/);
54
            .contains(/Wrong CSRF token/);
55
55
56
        cy.query(
56
        cy.task("query", {
57
            "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
57
            sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
58
            branchcode
58
            values: [branchcode],
59
        ).then(result => {
59
        }).then(result => {
60
            expect(result[0].count).to.equal(0);
60
            expect(result[0].count).to.equal(0);
61
        });
61
        });
62
    });
62
    });
Lines 89-98 describe("CSRF", () => { Link Here
89
        // We do not want Wrong CSRF token here
89
        // We do not want Wrong CSRF token here
90
        cy.get(".message").should("not.exist");
90
        cy.get(".message").should("not.exist");
91
91
92
        cy.query(
92
        cy.task("query", {
93
            "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
93
            sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
94
            branchcode
94
            values: [branchcode],
95
        ).then(result => {
95
        }).then(result => {
96
            expect(result[0].count).to.equal(0);
96
            expect(result[0].count).to.equal(0);
97
        });
97
        });
98
    });
98
    });
Lines 112-130 describe("CSRF", () => { Link Here
112
        cy.get("select[name='libraries_length']").select("-1");
112
        cy.get("select[name='libraries_length']").select("-1");
113
        cy.get("td").contains(branchcode);
113
        cy.get("td").contains(branchcode);
114
114
115
        cy.query(
115
        cy.task("query", {
116
            "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
116
            sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
117
            branchcode
117
            values: [branchcode],
118
        ).then(result => {
118
        }).then(result => {
119
            expect(result[0].count).to.equal(1);
119
            expect(result[0].count).to.equal(1);
120
        });
120
        });
121
    });
121
    });
122
122
123
    it("Delete without CSRF", () => {
123
    it("Delete without CSRF", () => {
124
        cy.query("INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", [
124
        cy.task("query", {
125
            branchcode,
125
            sql: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)",
126
            branchname,
126
            values: [branchcode, branchname],
127
        ]);
127
        });
128
128
129
        cy.visit("/cgi-bin/koha/admin/branches.pl");
129
        cy.visit("/cgi-bin/koha/admin/branches.pl");
130
        cy.get("select[name='libraries_length']").select("-1");
130
        cy.get("select[name='libraries_length']").select("-1");
Lines 141-159 describe("CSRF", () => { Link Here
141
            .find(".alert")
141
            .find(".alert")
142
            .contains(/No CSRF token passed for POST/);
142
            .contains(/No CSRF token passed for POST/);
143
143
144
        cy.query(
144
        cy.task("query", {
145
            "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
145
            sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
146
            branchcode
146
            values: [branchcode],
147
        ).then(result => {
147
        }).then(result => {
148
            expect(result[0].count).to.equal(1);
148
            expect(result[0].count).to.equal(1);
149
        });
149
        });
150
    });
150
    });
151
151
152
    it("Delete", () => {
152
    it("Delete", () => {
153
        cy.query("INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", [
153
        cy.task("query", {
154
            branchcode,
154
            sql: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)",
155
            branchname,
155
            values: [branchcode, branchname],
156
        ]);
156
        });
157
157
158
        cy.visit("/cgi-bin/koha/admin/branches.pl");
158
        cy.visit("/cgi-bin/koha/admin/branches.pl");
159
        cy.get("select[name='libraries_length']").select("-1");
159
        cy.get("select[name='libraries_length']").select("-1");
Lines 165-174 describe("CSRF", () => { Link Here
165
            .find(".alert")
165
            .find(".alert")
166
            .contains(/Library deleted successfully/);
166
            .contains(/Library deleted successfully/);
167
167
168
        cy.query(
168
        cy.task("query", {
169
            "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
169
            sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
170
            branchcode
170
            values: [branchcode],
171
        ).then(result => {
171
        }).then(result => {
172
            expect(result[0].count).to.equal(0);
172
            expect(result[0].count).to.equal(0);
173
        });
173
        });
174
    });
174
    });
(-)a/t/cypress/integration/ERM/Agreements_spec.ts (-10 / +4 lines)
Lines 569-577 describe("Agreement CRUD operations", () => { Link Here
569
                "X-Base-Total-Count": "1",
569
                "X-Base-Total-Count": "1",
570
                "X-Total-Count": "1",
570
                "X-Total-Count": "1",
571
            },
571
            },
572
        });
572
        }).as("get-agreements");
573
        cy.intercept("GET", "/api/v1/erm/agreements/*", agreement);
573
        cy.intercept("GET", "/api/v1/erm/agreements/*", agreement);
574
        cy.visit("/cgi-bin/koha/erm/agreements");
574
        cy.visit("/cgi-bin/koha/erm/agreements");
575
        cy.wait("@get-agreements");
575
576
576
        cy.get("#agreements_list table tbody tr:first")
577
        cy.get("#agreements_list table tbody tr:first")
577
            .contains("Delete")
578
            .contains("Delete")
Lines 608-621 describe("Agreement CRUD operations", () => { Link Here
608
609
609
        // Delete from show
610
        // Delete from show
610
        // Click the "name" link from the list
611
        // Click the "name" link from the list
611
        cy.intercept("GET", "/api/v1/erm/agreements*", {
612
        cy.visit("/cgi-bin/koha/erm/agreements");
612
            statusCode: 200,
613
        cy.wait("@get-agreements");
613
            body: agreements,
614
            headers: {
615
                "X-Base-Total-Count": "1",
616
                "X-Total-Count": "1",
617
            },
618
        });
619
        cy.intercept("GET", "/api/v1/erm/agreements/*", agreement).as(
614
        cy.intercept("GET", "/api/v1/erm/agreements/*", agreement).as(
620
            "get-agreement"
615
            "get-agreement"
621
        );
616
        );
Lines 629-635 describe("Agreement CRUD operations", () => { Link Here
629
        name_link.should("have.text", agreement.name);
624
        name_link.should("have.text", agreement.name);
630
        name_link.click();
625
        name_link.click();
631
        cy.wait("@get-agreement");
626
        cy.wait("@get-agreement");
632
        cy.wait(500); // Cypress is too fast! Vue hasn't populated the form yet!
633
        cy.get("#agreements_show h2").contains(
627
        cy.get("#agreements_show h2").contains(
634
            "Agreement #" + agreement.agreement_id
628
            "Agreement #" + agreement.agreement_id
635
        );
629
        );
(-)a/t/cypress/integration/ERM/DataProviders_spec.ts (-1 / +1 lines)
Lines 636-642 describe("Data provider tab options", () => { Link Here
636
            }
636
            }
637
        );
637
        );
638
638
639
        cy.get("#files > form > fieldset > input[type=submit]").click();
639
        cy.get("#files > form > fieldset > button").click();
640
640
641
        cy.get("main div[class='alert alert-info']").should(
641
        cy.get("main div[class='alert alert-info']").should(
642
            "have.text",
642
            "have.text",
(-)a/t/cypress/integration/ERM/UsageReports_spec.ts (-1 / +3 lines)
Lines 105-111 describe("Saved reports", () => { Link Here
105
            { force: true }
105
            { force: true }
106
        );
106
        );
107
107
108
        cy.get("#report_builder .default-report .action input").click();
108
        cy.get("#report_builder .default-report .action button")
109
            .contains("Submit")
110
            .click();
109
111
110
        cy.url({ decode: true }).then(url => {
112
        cy.url({ decode: true }).then(url => {
111
            const urlParams = url.split("viewer?")[1].split("data=")[1];
113
            const urlParams = url.split("viewer?")[1].split("data=")[1];
(-)a/t/cypress/integration/Islands/AcquisitionsMenu_spec.ts (-4 / +6 lines)
Lines 23-34 describe("Acquisitions menu", () => { Link Here
23
    it("Should show/hide links based on permissions", () => {
23
    it("Should show/hide links based on permissions", () => {
24
        cy.get(".sidebar_menu").should("be.visible");
24
        cy.get(".sidebar_menu").should("be.visible");
25
25
26
        cy.query(
26
        cy.task("query", {
27
            "UPDATE borrowers SET flags=2052 WHERE borrowernumber=51"
27
            sql: "UPDATE borrowers SET flags=2052 WHERE borrowernumber=51",
28
        ).then(() => {
28
        }).then(() => {
29
            cy.reload(true);
29
            cy.reload(true);
30
            cy.get(".sidebar_menu a").should("have.length", 8);
30
            cy.get(".sidebar_menu a").should("have.length", 8);
31
            cy.query("UPDATE borrowers SET flags=1 WHERE borrowernumber=51");
31
            cy.task("query", {
32
                sql: "UPDATE borrowers SET flags=1 WHERE borrowernumber=51",
33
            });
32
        });
34
        });
33
    });
35
    });
34
    it("Should correctly apply the 'current' class", () => {
36
    it("Should correctly apply the 'current' class", () => {
(-)a/t/cypress/integration/KohaTable/CirculationHistory_spec.ts (+23 lines)
Line 0 Link Here
1
describe("members/readingrec", () => {
2
    const table_id = "table_readingrec";
3
    beforeEach(() => {
4
        cy.login();
5
        cy.title().should("eq", "Koha staff interface");
6
        cy.task("insertSampleCheckout").then(objects_checkout => {
7
            cy.wrap(objects_checkout).as("objects_checkout");
8
        });
9
    });
10
11
    afterEach(function () {
12
        cy.task("deleteSampleObjects", [this.objects_checkout]);
13
    });
14
15
    it("'Type' column should be hidden", function () {
16
        cy.visit(
17
            `/cgi-bin/koha/members/readingrec.pl?borrowernumber=${this.objects_checkout.patron.patron_id}`
18
        );
19
20
        cy.get(`#${table_id} th`).contains("Type").should("not.exist");
21
        cy.get(`#${table_id} th:first`).contains("Date");
22
    });
23
});
(-)a/t/cypress/integration/KohaTable/Holdings_spec.ts (-345 / +231 lines)
Lines 1-7 Link Here
1
const RESTdefaultPageSize = "20"; // FIXME Mock this
1
const RESTdefaultPageSize = "20"; // FIXME Mock this
2
const baseTotalCount = "42";
2
const baseTotalCount = "21";
3
3
4
describe("catalogue/detail/holdings_table", () => {
4
describe("catalogue/detail/holdings_table with items", () => {
5
    const table_id = "holdings_table";
5
    const table_id = "holdings_table";
6
    beforeEach(() => {
6
    beforeEach(() => {
7
        cy.login();
7
        cy.login();
Lines 10-153 describe("catalogue/detail/holdings_table", () => { Link Here
10
            win.localStorage.clear();
10
            win.localStorage.clear();
11
        });
11
        });
12
12
13
        // FIXME All the following code should not be reused as it
13
        cy.task("query", {
14
        // It must be moved to a Cypress command or task "buildSampleBiblio" or even "insertSampleBiblio"
14
            sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'",
15
        let generated_objects = {};
15
        }).then(value => {
16
        const objects = [{ object: "library" }, { object: "item_type" }];
17
        cy.wrap(Promise.resolve())
18
            .then(() => {
19
                return objects.reduce((chain, { object }) => {
20
                    return chain.then(() => {
21
                        return cy
22
                            .task("buildSampleObject", { object })
23
                            .then(attributes => {
24
                                generated_objects[object] = attributes;
25
                            });
26
                    });
27
                }, Promise.resolve());
28
            })
29
            .then(() => {
30
                const library = generated_objects["library"];
31
                const item_type = generated_objects["item_type"];
32
                const queries = [
33
                    {
34
                        query: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)",
35
                        values: [library.library_id, library.name],
36
                    },
37
                    {
38
                        query: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)",
39
                        values: [item_type.item_type_id, item_type.description],
40
                    },
41
                ];
42
                cy.wrap(Promise.resolve())
43
                    .then(() => {
44
                        return queries.reduce((chain, { query, values }) => {
45
                            return chain.then(() => cy.query(query, values));
46
                        }, Promise.resolve());
47
                    })
48
                    .then(() => {
49
                        let biblio = {
50
                            leader: "     nam a22     7a 4500",
51
                            fields: [
52
                                { "005": "20250120101920.0" },
53
                                {
54
                                    "245": {
55
                                        ind1: "",
56
                                        ind2: "",
57
                                        subfields: [{ a: "Some boring read" }],
58
                                    },
59
                                },
60
                                {
61
                                    "100": {
62
                                        ind1: "",
63
                                        ind2: "",
64
                                        subfields: [
65
                                            { c: "Some boring author" },
66
                                        ],
67
                                    },
68
                                },
69
                                {
70
                                    "942": {
71
                                        ind1: "",
72
                                        ind2: "",
73
                                        subfields: [
74
                                            { c: item_type.item_type_id },
75
                                        ],
76
                                    },
77
                                },
78
                            ],
79
                        };
80
                        cy.request({
81
                            method: "POST",
82
                            url: "/api/v1/biblios",
83
                            headers: {
84
                                "Content-Type": "application/marc-in-json",
85
                                "x-confirm-not-duplicate": 1,
86
                            },
87
                            body: biblio,
88
                        }).then(response => {
89
                            const biblio_id = response.body.id;
90
                            cy.wrap(biblio_id).as("biblio_id");
91
                            cy.request({
92
                                method: "POST",
93
                                url: `/api/v1/biblios/${biblio_id}/items`,
94
                                headers: {
95
                                    "Content-Type": "application/json",
96
                                },
97
                                body: {
98
                                    home_library_id: library.library_id,
99
                                    holding_library_id: library.library_id,
100
                                },
101
                            });
102
                        });
103
                    });
104
            });
105
        cy.query(
106
            "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'"
107
        ).then(value => {
108
            cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters");
16
            cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters");
109
        });
17
        });
110
    });
111
18
112
    afterEach(
19
        cy.task("insertSampleBiblio", { item_count: baseTotalCount }).then(
113
        () =>
20
            objects => {
114
            function () {
21
                cy.wrap(objects).as("objects");
115
                cleanup();
116
                cy.set_syspref(
117
                    "AlwaysShowHoldingsTableFilters",
118
                    this.syspref_AlwaysShowHoldingsTableFilters
119
                );
120
            }
22
            }
121
    );
23
        );
24
    });
122
25
123
    it("Correctly init the table", function () {
26
    afterEach(function () {
124
        // Do not use `() => {` or this.biblio_id won't be retrieved
27
        cy.task("deleteSampleObjects", [this.objects]);
125
        const biblio_id = this.biblio_id;
28
        cy.set_syspref(
126
        cy.task("buildSampleObjects", {
29
            "AlwaysShowHoldingsTableFilters",
127
            object: "item",
30
            this.syspref_AlwaysShowHoldingsTableFilters
128
            count: RESTdefaultPageSize,
31
        );
129
            values: {
32
    });
130
                biblio_id,
131
                checkout: null,
132
                transfer: null,
133
                lost_status: 0,
134
                withdrawn: 0,
135
                damaged_status: 0,
136
                not_for_loan_status: 0,
137
                course_item: null,
138
                cover_image_ids: [],
139
                _status: ["available"],
140
            },
141
        }).then(items => {
142
            cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, {
143
                statuscode: 200,
144
                body: items,
145
                headers: {
146
                    "X-Base-Total-Count": baseTotalCount,
147
                    "X-Total-Count": baseTotalCount,
148
                },
149
            });
150
33
34
    it("Correctly init the table", function () {
35
        // Do not use `() => {` or this.objects won't be retrieved
36
        const biblio_id = this.objects.biblio.biblio_id;
37
        cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => {
151
            cy.visit(
38
            cy.visit(
152
                "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id
39
                "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id
153
            );
40
            );
Lines 164-411 describe("catalogue/detail/holdings_table", () => { Link Here
164
    });
51
    });
165
52
166
    it("Show filters", function () {
53
    it("Show filters", function () {
167
        // Do not use `() => {` or this.biblio_id won't be retrieved
54
        // Do not use `() => {` or this.objects won't be retrieved
168
        const biblio_id = this.biblio_id;
55
        const biblio_id = this.objects.biblio.biblio_id;
169
        cy.task("buildSampleObjects", {
170
            object: "item",
171
            count: RESTdefaultPageSize,
172
            values: {
173
                biblio_id,
174
                checkout: null,
175
                transfer: null,
176
                lost_status: 0,
177
                withdrawn: 0,
178
                damaged_status: 0,
179
                not_for_loan_status: 0,
180
                course_item: null,
181
                cover_image_ids: [],
182
                _status: ["available"],
183
            },
184
        }).then(items => {
185
            cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, {
186
                statuscode: 200,
187
                body: items,
188
                headers: {
189
                    "X-Base-Total-Count": baseTotalCount,
190
                    "X-Total-Count": baseTotalCount,
191
                },
192
            });
193
56
194
            cy.set_syspref("AlwaysShowHoldingsTableFilters", 0).then(() => {
57
        cy.set_syspref("AlwaysShowHoldingsTableFilters", 0).then(() => {
195
                cy.visit(
58
            cy.visit(
196
                    "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" +
59
                "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id
197
                        biblio_id
60
            );
198
                );
199
61
200
                // Hide the 'URL' column
62
            // Hide the 'URL' column
201
                cy.mock_table_settings(
63
            cy.mock_table_settings(
202
                    {
64
                {
203
                        columns: { uri: { is_hidden: 1 } },
65
                    columns: { uri: { is_hidden: 1 } },
204
                    },
66
                },
205
                    "items_table_settings.holdings"
67
                "items_table_settings.holdings"
206
                );
68
            );
207
69
208
                cy.get("@columns").then(columns => {
70
            cy.get("@columns").then(columns => {
209
                    cy.get(`#${table_id}_wrapper tbody tr`).should(
71
                cy.get(`#${table_id}_wrapper tbody tr`).should(
210
                        "have.length",
72
                    "have.length",
211
                        RESTdefaultPageSize
73
                    RESTdefaultPageSize
212
                    );
74
                );
213
75
214
                    // Filters are not displayed
76
                // Filters are not displayed
215
                    cy.get(`#${table_id} thead tr`).should("have.length", 1);
77
                cy.get(`#${table_id} thead tr`).should("have.length", 1);
216
78
217
                    cy.get(`#${table_id} th`).contains("Status");
79
                cy.get(`#${table_id} th`).contains("Status");
218
                    cy.get(`#${table_id} th`)
80
                cy.get(`#${table_id} th`).contains("URL").should("not.exist");
219
                        .contains("URL")
81
                cy.get(`#${table_id} th`)
220
                        .should("not.exist");
82
                    .contains("Course reserves")
221
                    cy.get(`#${table_id} th`)
83
                    .should("not.exist");
222
                        .contains("Course reserves")
223
                        .should("not.exist");
224
84
225
                    cy.get(`.${table_id}_table_controls .show_filters`).click();
85
                cy.get(`.${table_id}_table_controls .show_filters`).click();
226
                    cy.get(`#${table_id}_wrapper .dt-info`).contains(
86
                cy.get(`#${table_id}_wrapper .dt-info`).contains(
227
                        `Showing 1 to ${RESTdefaultPageSize} of ${baseTotalCount} entries`
87
                    `Showing 1 to ${RESTdefaultPageSize} of ${baseTotalCount} entries`
228
                    );
88
                );
229
                    // Filters are displayed
89
                // Filters are displayed
230
                    cy.get(`#${table_id} thead tr`).should("have.length", 2);
90
                cy.get(`#${table_id} thead tr`).should("have.length", 2);
231
91
232
                    cy.get(`#${table_id} th`).contains("Status");
92
                cy.get(`#${table_id} th`).contains("Status");
233
                    cy.get(`#${table_id} th`)
93
                cy.get(`#${table_id} th`).contains("URL").should("not.exist");
234
                        .contains("URL")
94
                cy.get(`#${table_id} th`)
235
                        .should("not.exist");
95
                    .contains("Course reserves")
236
                    cy.get(`#${table_id} th`)
96
                    .should("not.exist");
237
                        .contains("Course reserves")
238
                        .should("not.exist");
239
                });
240
            });
97
            });
98
        });
241
99
242
            cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => {
100
        cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => {
243
                cy.visit(
101
            cy.visit(
244
                    "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" +
102
                "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id
245
                        biblio_id
103
            );
246
                );
247
104
248
                // Hide the 'URL' column
105
            // Hide the 'URL' column
249
                cy.mock_table_settings(
106
            cy.mock_table_settings(
250
                    {
107
                {
251
                        columns: { uri: { is_hidden: 1 } },
108
                    columns: { uri: { is_hidden: 1 } },
252
                    },
109
                },
253
                    "items_table_settings.holdings"
110
                "items_table_settings.holdings"
254
                );
111
            );
255
112
256
                cy.get("@columns").then(columns => {
113
            cy.get("@columns").then(columns => {
257
                    cy.get(`#${table_id}_wrapper tbody tr`).should(
114
                cy.get(`#${table_id}_wrapper tbody tr`).should(
258
                        "have.length",
115
                    "have.length",
259
                        RESTdefaultPageSize
116
                    RESTdefaultPageSize
260
                    );
117
                );
261
118
262
                    // Filters are displayed
119
                // Filters are displayed
263
                    cy.get(`#${table_id} thead tr`).should("have.length", 2);
120
                cy.get(`#${table_id} thead tr`).should("have.length", 2);
264
121
265
                    cy.get(`.${table_id}_table_controls .hide_filters`).click();
122
                cy.get(`.${table_id}_table_controls .hide_filters`).click();
266
123
267
                    // Filters are not displayed
124
                // Filters are not displayed
268
                    cy.get(`#${table_id} thead tr`).should("have.length", 1);
125
                cy.get(`#${table_id} thead tr`).should("have.length", 1);
269
                });
270
            });
126
            });
271
        });
127
        });
272
    });
128
    });
273
129
274
    it("Filters by code and description", function () {
130
    it("Filters by code and description", function () {
275
        // Do not use `() => {` or this.biblio_id won't be retrieved
131
        // Do not use `() => {` or this.objects won't be retrieved
276
        const biblio_id = this.biblio_id;
132
        const biblio_id = this.objects.biblio.biblio_id;
277
        cy.task("buildSampleObjects", {
133
278
            object: "item",
134
        cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`).as(
279
            count: RESTdefaultPageSize,
135
            "searchItems"
280
            values: {
136
        );
281
                biblio_id,
137
282
                checkout: null,
138
        cy.visit("/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id);
283
                transfer: null,
139
284
                lost_status: 0,
140
        cy.wait("@searchItems");
285
                withdrawn: 0,
141
286
                damaged_status: 0,
142
        cy.task("query", {
287
                not_for_loan_status: 0,
143
            sql: "SELECT homebranch FROM items WHERE biblionumber=? LIMIT 1",
288
                course_item: null,
144
            values: [biblio_id],
289
                cover_image_ids: [],
145
        }).then(result => {
290
                _status: ["available"],
146
            let library_id = result[0].homebranch;
291
            },
147
            cy.task("query", {
292
        }).then(items => {
148
                sql: "SELECT branchname FROM branches WHERE branchcode=?",
293
            cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, {
149
                values: [library_id],
294
                statuscode: 200,
150
            }).then(result => {
295
                body: items,
151
                let library_name = result[0].branchname;
296
                headers: {
152
                cy.get(`#${table_id}_wrapper input.dt-input`).type(library_id);
297
                    "X-Base-Total-Count": baseTotalCount,
153
298
                    "X-Total-Count": baseTotalCount,
154
                cy.wait("@searchItems").then(interception => {
299
                },
155
                    const q = interception.request.query.q;
300
            }).as("searchItems");
156
                    expect(q).to.match(
301
157
                        new RegExp(
302
            cy.visit(
158
                            `"me.home_library_id":{"like":"%${library_id}%"}`
303
                "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id
159
                        )
304
            );
160
                    );
161
                });
305
162
306
            cy.window().then(win => {
163
                cy.get(`#${table_id}_wrapper input.dt-input`).clear();
307
                win.coded_values.library = new Map(
164
                cy.wait("@searchItems");
308
                    items.map(i => [
165
                cy.get(`#${table_id}_wrapper input.dt-input`).type(
309
                        i.home_library.name,
166
                    library_name
310
                        i.home_library.library_id,
311
                    ])
312
                );
313
                win.coded_values.item_type = new Map(
314
                    items.map(i => [
315
                        i.item_type.description,
316
                        i.item_type.item_type_id,
317
                    ])
318
                );
167
                );
168
169
                cy.wait("@searchItems").then(interception => {
170
                    const q = interception.request.query.q;
171
                    expect(q).to.match(
172
                        new RegExp(`"me.home_library_id":\\["${library_id}"\\]`)
173
                    );
174
                });
319
            });
175
            });
320
            cy.wait("@searchItems");
176
        });
321
177
322
            let library_id = items[0].home_library.library_id;
178
        cy.task("query", {
323
            let library_name = items[0].home_library.name;
179
            sql: "SELECT itype FROM items WHERE biblionumber=? LIMIT 1",
324
            cy.get(`#${table_id}_wrapper input.dt-input`).type(library_id);
180
            values: [biblio_id],
325
181
        }).then(result => {
326
            cy.wait("@searchItems").then(interception => {
182
            let item_type_id = result[0].itype;
327
                const q = interception.request.query.q;
183
            cy.task("query", {
328
                expect(q).to.match(
184
                sql: "SELECT description FROM itemtypes WHERE itemtype=?",
329
                    new RegExp(
185
                values: [item_type_id],
330
                        `"me.home_library_id":{"like":"%${library_id}%"}`
186
            }).then(result => {
331
                    )
187
                let item_type_description = result[0].description;
188
189
                cy.get(`#${table_id}_wrapper input.dt-input`).clear();
190
                cy.wait("@searchItems");
191
                cy.get(`#${table_id}_wrapper input.dt-input`).type(
192
                    item_type_id
332
                );
193
                );
333
            });
334
194
335
            cy.get(`#${table_id}_wrapper input.dt-input`).clear();
195
                cy.wait("@searchItems").then(interception => {
336
            cy.wait("@searchItems");
196
                    const q = interception.request.query.q;
337
            cy.get(`#${table_id}_wrapper input.dt-input`).type(library_name);
197
                    expect(q).to.match(
198
                        new RegExp(
199
                            `"me.item_type_id":{"like":"%${item_type_id}%"}`
200
                        )
201
                    );
202
                });
338
203
339
            cy.wait("@searchItems").then(interception => {
204
                cy.get(`#${table_id}_wrapper input.dt-input`).clear();
340
                const q = interception.request.query.q;
205
                cy.wait("@searchItems");
341
                expect(q).to.match(
206
                cy.get(`#${table_id}_wrapper input.dt-input`).type(
342
                    new RegExp(`"me.home_library_id":\\["${library_id}"\\]`)
207
                    item_type_description
343
                );
208
                );
344
            });
345
209
346
            let item_type_id = items[0].item_type.item_type_id;
210
                cy.wait("@searchItems").then(interception => {
347
            let item_type_description = items[0].item_type.description;
211
                    const q = interception.request.query.q;
348
            cy.get(`#${table_id}_wrapper input.dt-input`).clear();
212
                    expect(q).to.match(
349
            cy.wait("@searchItems");
213
                        new RegExp(`"me.item_type_id":\\["${item_type_id}"\\]`)
350
            cy.get(`#${table_id}_wrapper input.dt-input`).type(item_type_id);
214
                    );
215
                });
351
216
352
            cy.wait("@searchItems").then(interception => {
217
                cy.viewport(2999, 2999);
353
                const q = interception.request.query.q;
218
                cy.get(`#${table_id}_wrapper input.dt-input`).clear();
354
                expect(q).to.match(
219
                cy.wait("@searchItems");
355
                    new RegExp(`"me.item_type_id":{"like":"%${item_type_id}%"}`)
220
                // Show filters if not there already
356
                );
221
                cy.get(`.${table_id}_table_controls .show_filters`)
222
                    .then(link => {
223
                        if (link.is(":visible")) {
224
                            cy.wrap(link).click();
225
                            cy.wait("@searchItems");
226
                        }
227
                    })
228
                    .then(() => {
229
                        // Select first (non-empty) option
230
                        cy.get(
231
                            `#${table_id}_wrapper th#holdings_itype select`
232
                        ).then(select => {
233
                            const raw_value = select.find("option").eq(1).val();
234
                            expect(raw_value).to.match(/^\^/);
235
                            expect(raw_value).to.match(/\$$/);
236
                            item_type_id = raw_value.replace(/^\^|\$$/g, ""); // Remove ^ and $
237
                        });
238
                        cy.get(
239
                            `#${table_id}_wrapper th#holdings_itype select option`
240
                        )
241
                            .eq(1)
242
                            .then(o => {
243
                                cy.get(
244
                                    `#${table_id}_wrapper th#holdings_itype select`
245
                                ).select(o.val(), { force: true });
246
                            });
247
                        cy.wait("@searchItems").then(interception => {
248
                            const q = interception.request.query.q;
249
                            expect(q).to.match(
250
                                new RegExp(
251
                                    `{"me.item_type_id":"${item_type_id}"}`
252
                                )
253
                            );
254
                        });
255
                    });
357
            });
256
            });
257
        });
258
    });
259
});
358
260
359
            cy.get(`#${table_id}_wrapper input.dt-input`).clear();
261
describe("catalogue/detail/holdings_table without items", () => {
360
            cy.wait("@searchItems");
262
    const table_id = "holdings_table";
361
            cy.get(`#${table_id}_wrapper input.dt-input`).type(
263
    beforeEach(() => {
362
                item_type_description
264
        cy.login();
363
            );
265
        cy.title().should("eq", "Koha staff interface");
266
        cy.window().then(win => {
267
            win.localStorage.clear();
268
        });
364
269
365
            cy.wait("@searchItems").then(interception => {
270
        cy.task("query", {
366
                const q = interception.request.query.q;
271
            sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'",
367
                expect(q).to.match(
272
        }).then(value => {
368
                    new RegExp(`"me.item_type_id":\\["${item_type_id}"\\]`)
273
            cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters");
369
                );
274
        });
370
            });
371
275
372
            cy.viewport(2999, 2999);
276
        cy.task("insertSampleBiblio", { item_count: 0 }).then(objects => {
373
            cy.get(`#${table_id}_wrapper input.dt-input`).clear();
277
            cy.wrap(objects).as("objects");
374
            cy.wait("@searchItems");
375
            // Show filters if not there already
376
            cy.get(`.${table_id}_table_controls .show_filters`)
377
                .then(link => {
378
                    if (link.is(":visible")) {
379
                        cy.wrap(link).click();
380
                        cy.wait("@searchItems");
381
                    }
382
                })
383
                .then(() => {
384
                    // Select first (non-empty) option
385
                    cy.get(
386
                        `#${table_id}_wrapper th#holdings_itype select`
387
                    ).then(select => {
388
                        const raw_value = select.find("option").eq(1).val();
389
                        expect(raw_value).to.match(/^\^/);
390
                        expect(raw_value).to.match(/\$$/);
391
                        item_type_id = raw_value.replace(/^\^|\$$/g, ""); // Remove ^ and $
392
                    });
393
                    cy.get(
394
                        `#${table_id}_wrapper th#holdings_itype select option`
395
                    )
396
                        .eq(1)
397
                        .then(o => {
398
                            cy.get(
399
                                `#${table_id}_wrapper th#holdings_itype select`
400
                            ).select(o.val(), { force: true });
401
                        });
402
                    cy.wait("@searchItems").then(interception => {
403
                        const q = interception.request.query.q;
404
                        expect(q).to.match(
405
                            new RegExp(`{"me.item_type_id":"${item_type_id}"}`)
406
                        );
407
                    });
408
                });
409
        });
278
        });
410
    });
279
    });
280
281
    afterEach(function () {
282
        cy.task("deleteSampleObjects", [this.objects]);
283
        cy.set_syspref(
284
            "AlwaysShowHoldingsTableFilters",
285
            this.syspref_AlwaysShowHoldingsTableFilters
286
        );
287
    });
288
289
    it("Do not display the table", function () {
290
        // Do not use `() => {` or this.objects won't be retrieved
291
        const biblio_id = this.objects.biblio.biblio_id;
292
293
        cy.visit("/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id);
294
295
        cy.get(`#${table_id}_wrapper`).should("not.exist");
296
    });
411
});
297
});
(-)a/t/cypress/integration/KohaTable/KohaTable_spec.ts (-2 / +2 lines)
Lines 333-339 describe("kohaTable (using REST API)", () => { Link Here
333
333
334
                cy.window().then(win => {
334
                cy.window().then(win => {
335
                    win.categories_map = patrons.reduce((map, p) => {
335
                    win.categories_map = patrons.reduce((map, p) => {
336
                        map[p.category_id] = p.category_id;
336
                        map[p.category_id.toLowerCase()] = p.category_id;
337
                        return map;
337
                        return map;
338
                    }, {});
338
                    }, {});
339
                });
339
                });
Lines 399-405 describe("kohaTable (using REST API)", () => { Link Here
399
399
400
                cy.window().then(win => {
400
                cy.window().then(win => {
401
                    win.categories_map = patrons.reduce((map, p) => {
401
                    win.categories_map = patrons.reduce((map, p) => {
402
                        map[p.category_id] = p.category_id;
402
                        map[p.category_id.toLowerCase()] = p.category_id;
403
                        return map;
403
                        return map;
404
                    }, {});
404
                    }, {});
405
                });
405
                });
(-)a/t/cypress/integration/KohaTable/OPACCirculationHistory_spec.ts (+53 lines)
Line 0 Link Here
1
describe("opac-readingrecord", () => {
2
    beforeEach(() => {
3
        cy.loginOpac();
4
        let objects_to_cleanup = [];
5
        cy.task("apiGet", {
6
            endpoint: "/api/v1/patrons/51",
7
        })
8
            .then(patron => {
9
                [...Array(51)].forEach(() => {
10
                    cy.task("insertSampleCheckout", {
11
                        patron: patron,
12
                    }).then(objects_checkout => {
13
                        cy.task("query", {
14
                            sql: "INSERT INTO old_issues SELECT * FROM issues WHERE issue_id=?",
15
                            values: [objects_checkout.checkout.checkout_id],
16
                        })
17
                            .then(() => {
18
                                cy.task("query", {
19
                                    sql: "DELETE FROM issues WHERE issue_id=?",
20
                                    values: [
21
                                        objects_checkout.checkout.checkout_id,
22
                                    ],
23
                                });
24
                            })
25
                            .then(() => {
26
                                objects_checkout.old_checkout =
27
                                    objects_checkout.checkout;
28
                                delete objects_checkout.checkout;
29
                                objects_to_cleanup.push(objects_checkout);
30
                            });
31
                    });
32
                });
33
            })
34
            .then(() => {
35
                cy.wrap(objects_to_cleanup).as("objects_to_cleanup");
36
            });
37
    });
38
39
    afterEach(function () {
40
        cy.task("deleteSampleObjects", this.objects_to_cleanup);
41
    });
42
43
    it("50 items should be displayed by default", function () {
44
        cy.visitOpac("/cgi-bin/koha/opac-readingrecord.pl");
45
46
        cy.contains("Showing 1 to 50 of 50 entries");
47
        cy.get("table#readingrec tbody tr").should("have.length", 50);
48
49
        cy.contains("Show all items").click();
50
        cy.contains("Showing 1 to 51 of 51 entries");
51
        cy.get("table#readingrec tbody tr").should("have.length", 51);
52
    });
53
});
(-)a/t/cypress/integration/KohaTable/PatronSearch_spec.ts (-28 / +25 lines)
Lines 9-15 const patron_attr_type = "attribute_type4TEST"; Link Here
9
9
10
function cleanup() {
10
function cleanup() {
11
    const sql = "DELETE FROM borrower_attribute_types WHERE code=?";
11
    const sql = "DELETE FROM borrower_attribute_types WHERE code=?";
12
    cy.query(sql, patron_attr_type);
12
    cy.task("query", { sql, values: [patron_attr_type] });
13
}
13
}
14
describe("ExtendedPatronAttributes", () => {
14
describe("ExtendedPatronAttributes", () => {
15
    beforeEach(() => {
15
    beforeEach(() => {
Lines 19-41 describe("ExtendedPatronAttributes", () => { Link Here
19
        cy.window().then(win => {
19
        cy.window().then(win => {
20
            win.localStorage.clear();
20
            win.localStorage.clear();
21
        });
21
        });
22
        cy.query(
22
        cy.task("query", {
23
            "SELECT value FROM systempreferences WHERE variable='ExtendedPatronAttributes'"
23
            sql: "SELECT value FROM systempreferences WHERE variable='ExtendedPatronAttributes'",
24
        ).then(value => {
24
        }).then(value => {
25
            cy.wrap(value).as("syspref_ExtendedPatronAttributes");
25
            cy.wrap(value).as("syspref_ExtendedPatronAttributes");
26
        });
26
        });
27
    });
27
    });
28
28
29
    afterEach(
29
    afterEach(function () {
30
        () =>
30
        cleanup();
31
            function () {
31
        cy.set_syspref(
32
                cleanup();
32
            "ExtendedPatronAttributes",
33
                cy.set_syspref(
33
            this.syspref_ExtendedPatronAttributes
34
                    "ExtendedPatronAttributes",
34
        );
35
                    this.syspref_ExtendedPatronAttributes
35
    });
36
                );
37
            }
38
    );
39
36
40
    const table_id = "memberresultst";
37
    const table_id = "memberresultst";
41
38
Lines 48-56 describe("ExtendedPatronAttributes", () => { Link Here
48
            cy.get("#search_patron_filter").type("something");
45
            cy.get("#search_patron_filter").type("something");
49
            cy.get("form.patron_search_form input[type='submit']").click();
46
            cy.get("form.patron_search_form input[type='submit']").click();
50
47
51
            cy.query(
48
            cy.task("query", {
52
                "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1"
49
                sql: "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1",
53
            ).then(result => {
50
            }).then(result => {
54
                const has_searchable = result[0].nb_searchable;
51
                const has_searchable = result[0].nb_searchable;
55
                cy.wait("@searchPatrons").then(interception => {
52
                cy.wait("@searchPatrons").then(interception => {
56
                    const q = interception.request.query.q;
53
                    const q = interception.request.query.q;
Lines 58-67 describe("ExtendedPatronAttributes", () => { Link Here
58
                });
55
                });
59
            });
56
            });
60
57
61
            cy.query(
58
            cy.task("query", {
62
                "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)",
59
                sql: "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)",
63
                patron_attr_type
60
                values: [patron_attr_type],
64
            ).then(() => {
61
            }).then(() => {
65
                cy.visit("/cgi-bin/koha/members/members-home.pl");
62
                cy.visit("/cgi-bin/koha/members/members-home.pl");
66
63
67
                cy.get("#search_patron_filter").type("something");
64
                cy.get("#search_patron_filter").type("something");
Lines 83-91 describe("ExtendedPatronAttributes", () => { Link Here
83
            cy.get("#search_patron_filter").type("something");
80
            cy.get("#search_patron_filter").type("something");
84
            cy.get("form.patron_search_form input[type='submit']").click();
81
            cy.get("form.patron_search_form input[type='submit']").click();
85
82
86
            cy.query(
83
            cy.task("query", {
87
                "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1 AND searched_by_default=1"
84
                sql: "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1 AND searched_by_default=1",
88
            ).then(result => {
85
            }).then(result => {
89
                const has_searchable = result[0].nb_searchable;
86
                const has_searchable = result[0].nb_searchable;
90
                cy.wait("@searchPatrons").then(interception => {
87
                cy.wait("@searchPatrons").then(interception => {
91
                    const q = interception.request.query.q;
88
                    const q = interception.request.query.q;
Lines 97-106 describe("ExtendedPatronAttributes", () => { Link Here
97
                });
94
                });
98
            });
95
            });
99
96
100
            cy.query(
97
            cy.task("query", {
101
                "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)",
98
                sql: "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)",
102
                patron_attr_type
99
                values: [patron_attr_type],
103
            ).then(() => {
100
            }).then(() => {
104
                cy.visit("/cgi-bin/koha/members/members-home.pl");
101
                cy.visit("/cgi-bin/koha/members/members-home.pl");
105
102
106
                cy.get("#search_patron_filter").type("something");
103
                cy.get("#search_patron_filter").type("something");
(-)a/t/cypress/integration/KohaTable/PendingHolds_spec.ts (+101 lines)
Line 0 Link Here
1
describe("circ/pendingreserves/holdst", () => {
2
    const table_id = "holdst";
3
    beforeEach(() => {
4
        cy.login();
5
        cy.title().should("eq", "Koha staff interface");
6
        cy.task("insertSampleBiblio", {
7
            item_count: 2,
8
            options: { different_libraries: true },
9
        }).then(objects_biblio_1 => {
10
            cy.wrap(objects_biblio_1).as("objects_biblio_1");
11
            const commonLibraryId = objects_biblio_1.items[0].home_library_id;
12
            cy.task("insertSampleHold", {
13
                biblio: objects_biblio_1.biblio,
14
                library_id: commonLibraryId,
15
            }).then(objects_hold_1 => {
16
                cy.wrap(objects_hold_1).as("objects_hold_1");
17
            });
18
            cy.task("insertSampleBiblio", { item_count: 1 }).then(
19
                objects_biblio_2 => {
20
                    cy.wrap(objects_biblio_2).as("objects_biblio_2");
21
                    cy.task("insertSampleHold", {
22
                        biblio: objects_biblio_2.biblio,
23
                        library_id: objects_biblio_2.items[0].home_library_id,
24
                    }).then(objects_hold_2 => {
25
                        cy.wrap(objects_hold_2).as("objects_hold_2");
26
                    });
27
                    cy.task("query", {
28
                        sql: "UPDATE items SET homebranch=?, holdingbranch=? WHERE itemnumber=?",
29
                        values: [
30
                            commonLibraryId,
31
                            commonLibraryId,
32
                            objects_biblio_2.items[0].item_id,
33
                        ],
34
                    });
35
                }
36
            );
37
        });
38
    });
39
40
    afterEach(function () {
41
        cy.task("deleteSampleObjects", [
42
            this.objects_hold_1,
43
            this.objects_hold_2,
44
            this.objects_biblio_1,
45
            this.objects_biblio_2,
46
        ]);
47
    });
48
49
    it("Should render library filters", function () {
50
        cy.visit(
51
            "/cgi-bin/koha/circ/pendingreserves.pl?from=2000-01-01&to=2999-12-31&run_report=Submit"
52
        );
53
54
        cy.get(`#${table_id} thead tr:eq(1) th:eq(5)`).should(
55
            "have.text",
56
            "Libraries"
57
        );
58
59
        // select has library names in value and text, this table is not using server-side processing
60
        let libraries = this.objects_biblio_1.libraries
61
            .map(library => library.name)
62
            .sort();
63
        cy.get(`#${table_id} thead tr:eq(1) th:eq(5) select`)
64
            .children()
65
            .should("have.length", 3)
66
            .then(options => {
67
                expect(options.eq(0).val()).to.eq("");
68
                expect(options.eq(1).val()).to.eq(libraries[0]);
69
                expect(options.eq(2).val()).to.eq(libraries[1]);
70
            });
71
    });
72
73
    it("Should filter table on library", function () {
74
        cy.visit(
75
            "/cgi-bin/koha/circ/pendingreserves.pl?from=2000-01-01&to=2999-12-31&run_report=Submit"
76
        );
77
78
        cy.get(`#${table_id} thead tr:eq(1) th:eq(5) select`).select(
79
            this.objects_biblio_1.libraries[0].name,
80
            { force: true }
81
        );
82
        cy.get(`#${table_id} tbody tr:eq(0) td:eq(4)`).should(
83
            "contain",
84
            this.objects_biblio_1.biblio.title
85
        );
86
        cy.get(`#${table_id} tbody tr:eq(1) td:eq(4)`).should(
87
            "contain",
88
            this.objects_biblio_2.biblio.title
89
        );
90
        cy.get(`#${table_id} tbody tr`).should("have.length", 2);
91
92
        cy.get(`#${table_id} thead tr:eq(1) th:eq(5) select`).select(
93
            this.objects_biblio_1.libraries[1].name
94
        );
95
        cy.get(`#${table_id} tbody tr:eq(0) td:eq(4)`).should(
96
            "contain",
97
            this.objects_biblio_1.biblio.title
98
        );
99
        cy.get(`#${table_id} tbody tr`).should("have.length", 1);
100
    });
101
});
(-)a/t/cypress/integration/Toolbar_spec.ts (-31 / +50 lines)
Lines 1-39 Link Here
1
describe("Sticky toolbar", () => {
1
describe("Sticky toolbar - basic behavior", () => {
2
    beforeEach(() => {
2
    beforeEach(() => {
3
        cy.login();
3
        cy.login();
4
        cy.title().should("eq", "Koha staff interface");
4
        cy.title().should("eq", "Koha staff interface");
5
    });
5
    });
6
6
7
    it("Should open non-Vue links correctly in the same tab", () => {
8
        const vendor = cy.getVendor();
9
        vendor.baskets_count = 1;
10
        // Click the "name" link from the list
11
        cy.intercept("GET", "/api/v1/acquisitions/vendors\?*", {
12
            statusCode: 200,
13
            body: [vendor],
14
            headers: {
15
                "X-Base-Total-Count": "1",
16
                "X-Total-Count": "1",
17
            },
18
        }).as("get-vendors");
19
        cy.intercept(
20
            "GET",
21
            new RegExp("/api/v1/acquisitions/vendors/(?!config$).+"),
22
            vendor
23
        ).as("get-vendor");
24
25
        cy.visit("/cgi-bin/koha/acquisition/vendors");
26
        cy.wait("@get-vendors");
27
28
        const name_link = cy.get(
29
            "#vendors_list table tbody tr:first td:first a"
30
        );
31
        name_link.click();
32
        cy.wait("@get-vendor");
33
        cy.get("#toolbar a").contains("Receive shipments").click();
34
        cy.get("h1").contains("Receive shipment from vendor " + vendor.name);
35
    });
36
37
    it("Should stick on scroll", () => {
7
    it("Should stick on scroll", () => {
38
        cy.visit("/cgi-bin/koha/acqui/acqui-home.pl");
8
        cy.visit("/cgi-bin/koha/acqui/acqui-home.pl");
39
9
Lines 46-48 describe("Sticky toolbar", () => { Link Here
46
        cy.get("#toolbar").should("not.have.class", "floating");
16
        cy.get("#toolbar").should("not.have.class", "floating");
47
    });
17
    });
48
});
18
});
19
20
describe("Sticky toolbar - vendors", () => {
21
    beforeEach(() => {
22
        cy.login();
23
        cy.title().should("eq", "Koha staff interface");
24
25
        cy.task("buildSampleObject", {
26
            object: "vendor",
27
            values: { active: 1 },
28
        })
29
            .then(generatedVendor => {
30
                delete generatedVendor.list_currency;
31
                delete generatedVendor.invoice_currency;
32
                return cy.task("insertObject", {
33
                    type: "vendor",
34
                    object: generatedVendor,
35
                });
36
            })
37
            .then(vendor => {
38
                cy.wrap(vendor).as("vendor");
39
                return cy.task("buildSampleObject", {
40
                    object: "basket",
41
                    values: { vendor_id: vendor.id },
42
                });
43
            })
44
            .then(generatedBasket => {
45
                return cy.task("insertObject", {
46
                    type: "basket",
47
                    object: generatedBasket,
48
                });
49
            })
50
            .then(basket => {
51
                cy.wrap(basket).as("basket");
52
            });
53
    });
54
    afterEach(function () {
55
        cy.task("deleteSampleObjects", [
56
            { vendor: this.vendor, basket: this.basket },
57
        ]);
58
    });
59
    it("Should open non-Vue links correctly in the same tab", function () {
60
        cy.visit(`/cgi-bin/koha/acquisition/vendors/${this.vendor.id}`);
61
62
        cy.get("#toolbar a").contains("Receive shipments").click();
63
        cy.get("h1").contains(
64
            `Receive shipment from vendor ${this.vendor.name}`
65
        );
66
    });
67
});
(-)a/t/cypress/integration/Tools/ManageMarcImport_spec.ts (-36 / +24 lines)
Lines 27-91 describe("loads the manage MARC import page", () => { Link Here
27
    it("upload a MARC record", () => {
27
    it("upload a MARC record", () => {
28
        cy.visit("/cgi-bin/koha/tools/stage-marc-import.pl");
28
        cy.visit("/cgi-bin/koha/tools/stage-marc-import.pl");
29
29
30
        cy.get('input[type="file"]').selectFile(
30
        cy.fixture("sample.xml", null).as("sample_xml");
31
            "t/cypress/fixtures/sample.mrc"
31
        cy.get("input[type=file]").selectFile("@sample_xml");
32
        );
32
        cy.get("#fileuploadbutton").click();
33
        cy.get('form[id="uploadfile"]').within(() => {
34
            cy.get('button[id="fileuploadbutton"]').click();
35
        });
36
33
37
        //wait after file upload, it can go to quickly here
34
        cy.get("#fileuploadstatus").contains("100%");
38
        cy.wait(2000);
35
        cy.get("legend")
36
            .contains("Look for existing records in catalog?")
37
            .should("be.visible");
39
38
40
        //check default values
39
        //check default values
41
        cy.get('select[name="matcher"] option:selected').should(
40
        cy.get("select#matcher option:selected").should("have.value", "");
42
            "have.value",
41
        cy.get("select#overlay_action option:selected").should(
43
            ""
44
        );
45
        cy.get('select[name="overlay_action"] option:selected').should(
46
            "have.value",
42
            "have.value",
47
            "replace"
43
            "replace"
48
        );
44
        );
49
        cy.get('select[name="nomatch_action"] option:selected').should(
45
        cy.get("select#nomatch_action option:selected").should(
50
            "have.value",
46
            "have.value",
51
            "create_new"
47
            "create_new"
52
        );
48
        );
53
        cy.get('select[name="item_action"] option:selected').should(
49
        cy.get("select#item_action option:selected").should(
54
            "have.value",
50
            "have.value",
55
            "always_add"
51
            "always_add"
56
        );
52
        );
57
53
58
        cy.get('select[name="format"]').select("MARCXML", { force: true });
54
        cy.get('select[name="format"]').should("have.value", "MARCXML");
59
        cy.get("#format").should("have.value", "MARCXML");
60
55
61
        //select some new options
56
        //select some new options
62
        cy.get("#matcher").select("3", { force: true });
57
        cy.get("#matcher").select("3", { force: true });
63
        cy.get("#overlay_action").select("create_new", { force: true });
58
        cy.get("#matcher")
64
        cy.get("#nomatch_action").select("ignore", { force: true });
59
            .select("3", { force: true })
65
        cy.get("#item_action").select("ignore", { force: true });
60
            .should("have.value", "3");
61
        cy.get("#overlay_action")
62
            .select("create_new", { force: true })
63
            .should("have.value", "create_new");
64
        cy.get("#nomatch_action")
65
            .select("ignore", { force: true })
66
            .should("have.value", "ignore");
67
        cy.get("#item_action")
68
            .select("ignore", { force: true })
69
            .should("have.value", "ignore");
66
70
67
        //remove focus
68
        //cy.get('#item_action').blur();
69
        cy.screenshot("after_selection");
70
71
        // Now verify all values
72
        cy.get("#matcher").should("have.value", "3");
73
        cy.get("#overlay_action").should("have.value", "create_new");
74
        cy.get("#nomatch_action").should("have.value", "ignore");
75
        cy.get("#item_action").should("have.value", "ignore");
76
77
        cy.screenshot("right_before_submission");
78
        cy.get("#mainformsubmit").click();
71
        cy.get("#mainformsubmit").click();
79
72
80
        cy.get("#job_callback").should("exist");
73
        cy.get("#job_callback").should("exist");
81
74
82
        //wait for View batch link to load with the batch ID
83
        cy.wait(5000);
84
85
        cy.screenshot("after_waiting");
86
        cy.contains("View batch").click();
75
        cy.contains("View batch").click();
87
76
88
        cy.wait(2000);
89
        // Now verify all values are retained
77
        // Now verify all values are retained
90
        cy.get("#new_matcher_id").should("have.value", "3");
78
        cy.get("#new_matcher_id").should("have.value", "3");
91
        cy.get("#overlay_action").should("have.value", "create_new");
79
        cy.get("#overlay_action").should("have.value", "create_new");
(-)a/t/cypress/integration/t/api-client.ts (+28 lines)
Line 0 Link Here
1
const { APIClient } = require("./../../plugins/dist/api-client.cjs.js");
2
3
describe("Using APIClient", () => {
4
    let client = APIClient.default;
5
    it("should 404 for non-existent biblio", () => {
6
        try {
7
            client.koha.get({
8
                endpoint: "/api/v1/public/biblios/99999",
9
                return_response: true,
10
            });
11
        } catch (error) {
12
            expect(error.response.status).to.equal(404);
13
        }
14
    });
15
});
16
17
describe("Using the api-client plugin", () => {
18
    it("should 404 for non-existent biblio", () => {
19
        try {
20
            cy.task("apiGet", {
21
                endpoint: "/api/v1/public/biblios/99999",
22
                return_response: true,
23
            });
24
        } catch (error) {
25
            expect(error.response.status).to.equal(404);
26
        }
27
    });
28
});
(-)a/t/cypress/integration/t/commands.ts (+27 lines)
Line 0 Link Here
1
describe("visit", () => {
2
    it("should visit staff", () => {
3
        cy.visit("/");
4
        cy.title().should("eq", "Log in to Koha › Koha");
5
    });
6
});
7
8
describe("login", () => {
9
    it("should log in at the staff interface", () => {
10
        cy.login();
11
        cy.title().should("eq", "Koha staff interface");
12
    });
13
});
14
15
describe("visitOpac", () => {
16
    it("should visit OPAC", () => {
17
        cy.visitOpac("/");
18
        cy.title().should("eq", "Koha online catalog");
19
    });
20
});
21
22
describe("loginOpac", () => {
23
    it("should log in at the OPAC interface", () => {
24
        cy.loginOpac();
25
        cy.title().should("eq", "Your summary › Koha online catalog");
26
    });
27
});
(-)a/t/cypress/integration/t/db.ts (+15 lines)
Line 0 Link Here
1
describe("DB tests", () => {
2
    it("should be able to SELECT", () => {
3
        cy.task("query", { sql: "SELECT count(*) FROM borrowers" }).then(
4
            rows => {
5
                expect(typeof rows.length).to.be.equal("number");
6
            }
7
        );
8
        cy.task("query", {
9
            sql: "SELECT count(*) FROM borrowers WHERE `surname` = ?",
10
            values: ["john"],
11
        }).then(rows => {
12
            expect(typeof rows.length).to.be.equal("number");
13
        });
14
    });
15
});
(-)a/t/cypress/integration/t/insertData.ts (+278 lines)
Line 0 Link Here
1
const { query } = require("./../../plugins/db.js");
2
const { getBasicAuthHeader } = require("./../../plugins/auth.js");
3
4
describe("insertData", () => {
5
    let tablesToCheck = [
6
        "borrowers",
7
        "branches",
8
        "items",
9
        "biblio",
10
        "reserves",
11
        "issues",
12
    ];
13
    beforeEach(() => {
14
        const counts = {};
15
16
        const queries = tablesToCheck.map(table => {
17
            return cy
18
                .task("query", {
19
                    sql: `SELECT COUNT(*) as count FROM ${table}`,
20
                })
21
                .then(result => {
22
                    counts[table] = result[0].count;
23
                });
24
        });
25
26
        cy.wrap(Promise.all(queries)).then(() => {
27
            cy.wrap(counts).as("initialCounts");
28
        });
29
    });
30
31
    describe("deleteSampleObjects", () => {
32
        it("should delete everything from Object", () => {
33
            cy.task("insertSampleBiblio", { item_count: 2 }).then(objects => {
34
                cy.task("deleteSampleObjects", objects);
35
            });
36
        });
37
        it("should delete everything from Array", () => {
38
            cy.task("insertSampleBiblio", { item_count: 2 }).then(objects => {
39
                cy.task("deleteSampleObjects", [
40
                    { biblio: objects.biblio },
41
                    { item_type: objects.item_type },
42
                    { item: objects.items[0] },
43
                    { item: objects.items[1] },
44
                    { library: objects.libraries[0] },
45
                ]);
46
            });
47
        });
48
    });
49
50
    describe("insertSampleBiblio", () => {
51
        it("should generate library and item type", () => {
52
            cy.task("insertSampleBiblio", { item_count: 3 }).then(objects => {
53
                expect(typeof objects.biblio.biblio_id).to.be.equal("number");
54
                expect(typeof objects.biblio.title).to.be.equal("string");
55
                expect(typeof objects.biblio.author).to.be.equal("string");
56
57
                const biblio_id = objects.biblio.biblio_id;
58
59
                cy.task("query", {
60
                    sql: "SELECT COUNT(*) as count FROM biblio WHERE biblionumber=?",
61
                    values: [biblio_id],
62
                }).then(result => {
63
                    expect(result[0].count).to.be.equal(1);
64
                });
65
66
                cy.task("query", {
67
                    sql: "SELECT COUNT(*) as count FROM items WHERE biblionumber=?",
68
                    values: [biblio_id],
69
                }).then(result => {
70
                    expect(result[0].count).to.be.equal(3);
71
                });
72
73
                cy.task("query", {
74
                    sql: "SELECT DISTINCT(itype) as count FROM items WHERE biblionumber=?",
75
                    values: [biblio_id],
76
                }).then(result => {
77
                    expect(result.length).to.be.equal(1);
78
                });
79
80
                cy.task("query", {
81
                    sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
82
                    values: [objects.libraries[0].library_id],
83
                }).then(result => {
84
                    expect(result[0].count).to.be.equal(1);
85
                });
86
87
                cy.task("query", {
88
                    sql: "SELECT COUNT(*) as count FROM itemtypes WHERE itemtype=?",
89
                    values: [objects.item_type.item_type_id],
90
                }).then(result => {
91
                    expect(result[0].count).to.be.equal(1);
92
                });
93
94
                cy.task("deleteSampleObjects", objects);
95
96
                cy.task("query", {
97
                    sql: "SELECT COUNT(*) as count FROM biblio WHERE biblionumber=?",
98
                    values: [biblio_id],
99
                }).then(result => {
100
                    expect(result[0].count).to.be.equal(0);
101
                });
102
103
                cy.task("query", {
104
                    sql: "SELECT COUNT(*) as count FROM items WHERE biblionumber=?",
105
                    values: [biblio_id],
106
                }).then(result => {
107
                    expect(result[0].count).to.be.equal(0);
108
                });
109
110
                cy.task("query", {
111
                    sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?",
112
                    values: [objects.libraries[0].library_id],
113
                }).then(result => {
114
                    expect(result[0].count).to.be.equal(0);
115
                });
116
117
                cy.task("query", {
118
                    sql: "SELECT COUNT(*) as count FROM itemtypes WHERE itemtype=?",
119
                    values: [objects.item_type.item_type_id],
120
                }).then(result => {
121
                    expect(result[0].count).to.be.equal(0);
122
                });
123
            });
124
        });
125
126
        it("insertSampleBiblio - options.different_libraries", () => {
127
            cy.task("insertSampleBiblio", {
128
                item_count: 3,
129
                options: { different_libraries: true },
130
            }).then(objects => {
131
                expect(objects.libraries.length).to.be.equal(3);
132
                let libraries = objects.libraries
133
                    .map(library => library.library_id)
134
                    .sort();
135
                let itemsLibraries = objects.items
136
                    .map(item => item.home_library_id)
137
                    .sort();
138
                expect(libraries).deep.to.equal(itemsLibraries);
139
140
                cy.task("deleteSampleObjects", objects);
141
            });
142
        });
143
    });
144
145
    describe("insertSampleHold", () => {
146
        it("insertSampleHold - item/biblio", () => {
147
            cy.task("insertSampleBiblio", { item_count: 2 }).then(
148
                objects_biblio => {
149
                    cy.task("insertSampleHold", {
150
                        item: objects_biblio.items[0],
151
                    }).then(objects_hold_1 => {
152
                        cy.task("insertSampleHold", {
153
                            biblio: objects_biblio.biblio,
154
                            library_id: objects_biblio.items[0].home_library_id,
155
                        }).then(objects_hold_2 => {
156
                            cy.task("apiGet", {
157
                                // No /holds/:hold_id (yet)
158
                                // No q={} (yet)
159
                                endpoint: `/api/v1/holds?hold_id=${objects_hold_1.hold.hold_id}`,
160
                                headers: {
161
                                    "Content-Type": "application/json",
162
                                },
163
                            }).then(holds => {
164
                                expect(holds.length).to.be.equal(1);
165
                                expect(holds[0].biblio_id).to.be.equal(
166
                                    objects_biblio.biblio.biblio_id
167
                                );
168
                                expect(holds[0].item_id).to.be.equal(
169
                                    objects_biblio.items[0].item_id
170
                                );
171
                            });
172
                            cy.task("apiGet", {
173
                                // No /holds/:hold_id (yet)
174
                                // No q={} (yet)
175
                                endpoint: `/api/v1/holds?hold_id=${objects_hold_2.hold.hold_id}`,
176
                                headers: {
177
                                    "Content-Type": "application/json",
178
                                },
179
                            }).then(holds => {
180
                                expect(holds.length).to.be.equal(1);
181
                                expect(holds[0].biblio_id).to.be.equal(
182
                                    objects_biblio.biblio.biblio_id
183
                                );
184
                                expect(holds[0].item_id).to.be.equal(null);
185
                            });
186
187
                            cy.task("deleteSampleObjects", [
188
                                objects_biblio,
189
                                objects_hold_1,
190
                                objects_hold_2,
191
                            ]);
192
                        });
193
                    });
194
                }
195
            );
196
        });
197
198
        // How to properly test for Error?
199
        it.skip("insertSampleHold - missing library_id", () => {
200
            cy.task("insertSampleBiblio", { item_count: 2 }).then(
201
                objects_biblio => {
202
                    cy.task("insertSampleHold", {
203
                        biblio: objects_biblio.biblio,
204
                    }).then(
205
                        () => {
206
                            throw new Error("Task should have failed");
207
                        },
208
                        err => {
209
                            expect(err.message).to.include(
210
                                "Could not generate sample hold without library_id or item"
211
                            );
212
                        }
213
                    );
214
                }
215
            );
216
        });
217
    });
218
219
    describe("insertSampleCheckout", () => {
220
        it("insertSampleCheckout - without parameter", () => {
221
            cy.task("insertSampleCheckout").then(objects_checkout => {
222
                cy.task("apiGet", {
223
                    endpoint: `/api/v1/checkouts/${objects_checkout.checkout.checkout_id}`,
224
                }).then(checkout => {
225
                    expect(checkout.item_id).to.be.equal(
226
                        objects_checkout.items[0].item_id
227
                    );
228
                });
229
230
                cy.task("deleteSampleObjects", objects_checkout);
231
            });
232
        });
233
234
        it("insertSampleCheckout - pass an already generated patron", () => {
235
            cy.task("insertSamplePatron").then(objects_patron => {
236
                cy.task("insertSampleCheckout", {
237
                    patron: objects_patron.patron,
238
                }).then(objects_checkout => {
239
                    expect(objects_checkout.patron).to.not.exist;
240
                    cy.task("apiGet", {
241
                        endpoint: `/api/v1/checkouts/${objects_checkout.checkout.checkout_id}`,
242
                    }).then(checkout => {
243
                        expect(checkout.item_id).to.be.equal(
244
                            objects_checkout.items[0].item_id
245
                        );
246
                        expect(checkout.patron_id).to.be.equal(
247
                            objects_patron.patron.patron_id
248
                        );
249
                    });
250
                    cy.task("deleteSampleObjects", [
251
                        objects_checkout,
252
                        objects_patron,
253
                    ]);
254
                });
255
            });
256
        });
257
    });
258
259
    afterEach(function () {
260
        cy.get("@initialCounts").then(initialCounts => {
261
            const queries = tablesToCheck.map(table => {
262
                return cy
263
                    .task("query", {
264
                        sql: `SELECT COUNT(*) as count FROM ${table}`,
265
                    })
266
                    .then(result => {
267
                        const finalCount = result[0].count;
268
                        expect(
269
                            finalCount,
270
                            `Row count for ${table} should match`
271
                        ).to.eq(initialCounts[table]);
272
                    });
273
            });
274
275
            return Promise.all(queries);
276
        });
277
    });
278
});
(-)a/t/cypress/integration/t/mockData.ts (+20 lines)
Lines 20-25 describe("Generate Random Patron", () => { Link Here
20
            );
20
            );
21
        });
21
        });
22
    });
22
    });
23
24
    it("should not overwrite _id if passed", () => {
25
        const home_library_id = "LIB4TEST";
26
        cy.task("buildSampleObject", {
27
            object: "item",
28
            values: { home_library_id },
29
        }).then(mockItem => {
30
            expect(mockItem.home_library_id).to.equal(home_library_id);
31
        });
32
    });
23
});
33
});
24
34
25
describe("Generate Random Patrons", () => {
35
describe("Generate Random Patrons", () => {
Lines 74-77 describe("Generate objects", () => { Link Here
74
            );
84
            );
75
        });
85
        });
76
    });
86
    });
87
88
    it("should not overwrite _id if passed", () => {
89
        const home_library_id = "LIB4TEST";
90
        cy.task("buildSampleObject", {
91
            object: "item",
92
            values: { home_library_id },
93
        }).then(mockItem => {
94
            expect(mockItem.home_library_id).to.equal(home_library_id);
95
        });
96
    });
77
});
97
});
(-)a/t/cypress/plugins/api-client.js (+177 lines)
Line 0 Link Here
1
/**
2
 * Koha API Client for Cypress Testing
3
 *
4
 * This module provides a wrapper around the Koha API client for use in Cypress tests.
5
 * It handles authentication, request preparation, and provides convenient methods
6
 * for making API calls during test execution.
7
 *
8
 * @module api-client
9
 */
10
11
const { APIClient } = require("./dist/api-client.cjs.js");
12
13
const client = APIClient.default.koha;
14
15
/**
16
 * Prepares request parameters for API calls by extracting and organizing headers and URL.
17
 *
18
 * @function prepareRequest
19
 * @param {Object} params - Request parameters
20
 * @param {string} params.baseUrl - Base URL for the API
21
 * @param {string} params.endpoint - API endpoint path
22
 * @param {string} [params.authHeader] - Authorization header value
23
 * @param {Object} [params.headers={}] - Additional headers to include
24
 * @param {...*} params.rest - Other parameters to pass through
25
 * @returns {Object} Prepared request object
26
 * @returns {string} returns.url - Complete URL for the request
27
 * @returns {Object} returns.headers - Combined headers object
28
 * @returns {Object} returns.rest - Pass-through parameters
29
 * @private
30
 */
31
const prepareRequest = params => {
32
    const { baseUrl, endpoint, authHeader, headers = {}, ...rest } = params;
33
    const url = baseUrl + endpoint;
34
    const finalHeaders = {
35
        ...headers,
36
        ...(authHeader ? { Authorization: authHeader } : {}),
37
    };
38
    return { url, headers: finalHeaders, rest };
39
};
40
41
/**
42
 * Performs a GET request to the Koha API.
43
 *
44
 * @function apiGet
45
 * @param {Object} params - Request parameters
46
 * @param {string} params.baseUrl - Base URL for the API
47
 * @param {string} params.endpoint - API endpoint path
48
 * @param {string} [params.authHeader] - Authorization header value
49
 * @param {Object} [params.headers={}] - Additional headers to include
50
 * @param {...*} params.rest - Additional parameters for the request
51
 * @returns {Promise<*>} API response data
52
 * @example
53
 * // Get a list of patrons
54
 * const patrons = await apiGet({
55
 *   baseUrl: 'http://localhost:8081',
56
 *   endpoint: '/api/v1/patrons',
57
 *   authHeader: 'Basic dGVzdDp0ZXN0'
58
 * });
59
 *
60
 * @example
61
 * // Get a specific patron with query parameters
62
 * const patron = await apiGet({
63
 *   baseUrl: 'http://localhost:8081',
64
 *   endpoint: '/api/v1/patrons?q={"patron_id":123}',
65
 *   authHeader: 'Basic dGVzdDp0ZXN0'
66
 * });
67
 */
68
const apiGet = params => {
69
    const { url, headers, rest } = prepareRequest(params);
70
    return client.get({
71
        endpoint: url,
72
        headers,
73
        ...rest,
74
    });
75
};
76
77
/**
78
 * Performs a POST request to the Koha API.
79
 *
80
 * @function apiPost
81
 * @param {Object} params - Request parameters
82
 * @param {string} params.baseUrl - Base URL for the API
83
 * @param {string} params.endpoint - API endpoint path
84
 * @param {string} [params.authHeader] - Authorization header value
85
 * @param {Object} [params.headers={}] - Additional headers to include
86
 * @param {Object} [params.body] - Request body data
87
 * @param {...*} params.rest - Additional parameters for the request
88
 * @returns {Promise<*>} API response data
89
 * @example
90
 * // Create a new patron
91
 * const newPatron = await apiPost({
92
 *   baseUrl: 'http://localhost:8081',
93
 *   endpoint: '/api/v1/patrons',
94
 *   authHeader: 'Basic dGVzdDp0ZXN0',
95
 *   body: {
96
 *     firstname: 'John',
97
 *     surname: 'Doe',
98
 *     library_id: 'CPL',
99
 *     category_id: 'PT'
100
 *   }
101
 * });
102
 */
103
const apiPost = params => {
104
    const { url, headers, rest } = prepareRequest(params);
105
    return client.post({
106
        endpoint: url,
107
        headers,
108
        ...rest,
109
    });
110
};
111
112
/**
113
 * Performs a PUT request to the Koha API.
114
 *
115
 * @function apiPut
116
 * @param {Object} params - Request parameters
117
 * @param {string} params.baseUrl - Base URL for the API
118
 * @param {string} params.endpoint - API endpoint path
119
 * @param {string} [params.authHeader] - Authorization header value
120
 * @param {Object} [params.headers={}] - Additional headers to include
121
 * @param {Object} [params.body] - Request body data
122
 * @param {...*} params.rest - Additional parameters for the request
123
 * @returns {Promise<*>} API response data
124
 * @example
125
 * // Update a patron
126
 * const updatedPatron = await apiPut({
127
 *   baseUrl: 'http://localhost:8081',
128
 *   endpoint: '/api/v1/patrons/123',
129
 *   authHeader: 'Basic dGVzdDp0ZXN0',
130
 *   body: {
131
 *     email: 'newemail@example.com'
132
 *   }
133
 * });
134
 */
135
const apiPut = params => {
136
    const { url, headers, rest } = prepareRequest(params);
137
    return client.put({
138
        endpoint: url,
139
        headers,
140
        ...rest,
141
    });
142
};
143
144
/**
145
 * Performs a DELETE request to the Koha API.
146
 *
147
 * @function apiDelete
148
 * @param {Object} params - Request parameters
149
 * @param {string} params.baseUrl - Base URL for the API
150
 * @param {string} params.endpoint - API endpoint path
151
 * @param {string} [params.authHeader] - Authorization header value
152
 * @param {Object} [params.headers={}] - Additional headers to include
153
 * @param {...*} params.rest - Additional parameters for the request
154
 * @returns {Promise<*>} API response data
155
 * @example
156
 * // Delete a patron
157
 * await apiDelete({
158
 *   baseUrl: 'http://localhost:8081',
159
 *   endpoint: '/api/v1/patrons/123',
160
 *   authHeader: 'Basic dGVzdDp0ZXN0'
161
 * });
162
 */
163
const apiDelete = params => {
164
    const { url, headers, rest } = prepareRequest(params);
165
    return client.delete({
166
        endpoint: url,
167
        headers,
168
        ...rest,
169
    });
170
};
171
172
module.exports = {
173
    apiGet,
174
    apiPost,
175
    apiPut,
176
    apiDelete,
177
};
(-)a/t/cypress/plugins/auth.js (+40 lines)
Line 0 Link Here
1
/**
2
 * Authentication utilities for Cypress testing
3
 *
4
 * This module provides authentication helper functions for use in Cypress tests
5
 * when making API calls that require authentication.
6
 *
7
 * @module auth
8
 */
9
10
const { Buffer } = require("buffer");
11
12
/**
13
 * Generates a Basic Authentication header from username and password.
14
 *
15
 * @function getBasicAuthHeader
16
 * @param {string} username - Username for authentication
17
 * @param {string} password - Password for authentication
18
 * @returns {string} Basic authentication header value in format "Basic <base64>"
19
 * @example
20
 * // Generate auth header for API calls
21
 * const authHeader = getBasicAuthHeader('koha', 'koha');
22
 * // Returns: "Basic a29oYTprb2hh"
23
 *
24
 * // Use with API client
25
 * const response = await apiGet({
26
 *   baseUrl: 'http://localhost:8081',
27
 *   endpoint: '/api/v1/patrons',
28
 *   authHeader: getBasicAuthHeader('koha', 'koha')
29
 * });
30
 */
31
const getBasicAuthHeader = (username, password) => {
32
    const credentials = Buffer.from(`${username}:${password}`).toString(
33
        "base64"
34
    );
35
    return `Basic ${credentials}`;
36
};
37
38
module.exports = {
39
    getBasicAuthHeader,
40
};
(-)a/t/cypress/plugins/db.js (+66 lines)
Line 0 Link Here
1
/**
2
 * Database Query Utilities for Cypress Testing
3
 *
4
 * This module provides direct database access for Cypress tests when API
5
 * endpoints are not available or when direct database operations are needed
6
 * for test setup and cleanup.
7
 *
8
 * @module db
9
 */
10
11
const mysql = require("mysql2/promise");
12
13
/**
14
 * Database connection configuration
15
 *
16
 * @todo Replace hardcoded credentials with environment variables
17
 * @type {Object}
18
 */
19
const connectionConfig = {
20
    host: "db",
21
    user: "koha_kohadev",
22
    password: "password",
23
    database: "koha_kohadev",
24
};
25
26
/**
27
 * Executes a SQL query with optional parameters.
28
 *
29
 * @async
30
 * @function query
31
 * @param {string} sql - SQL query string with optional parameter placeholders (?)
32
 * @param {Array} [params=[]] - Array of parameter values for the query
33
 * @returns {Promise<Array>} Query results as an array of rows
34
 * @throws {Error} When database connection or query execution fails
35
 * @description This function:
36
 * - Creates a new database connection for each query
37
 * - Uses parameterized queries to prevent SQL injection
38
 * - Automatically closes the connection after execution
39
 * - Returns the raw result rows from the database
40
 *
41
 * @example
42
 * // Simple SELECT query
43
 * const patrons = await query('SELECT * FROM borrowers LIMIT 10');
44
 *
45
 * @example
46
 * // Parameterized query for safety
47
 * const patron = await query(
48
 *   'SELECT * FROM borrowers WHERE borrowernumber = ?',
49
 *   [123]
50
 * );
51
 *
52
 * @example
53
 * // DELETE query with multiple parameters
54
 * await query(
55
 *   'DELETE FROM issues WHERE issue_id IN (?, ?, ?)',
56
 *   [1, 2, 3]
57
 * );
58
 */
59
async function query(sql, params = []) {
60
    const connection = await mysql.createConnection(connectionConfig);
61
    const [rows] = await connection.execute(sql, params);
62
    await connection.end();
63
    return rows;
64
}
65
66
module.exports = { query };
(-)a/t/cypress/plugins/index.js (-4 / +105 lines)
Lines 1-21 Link Here
1
const { startDevServer } = require("@cypress/webpack-dev-server");
1
/**
2
 * Cypress Plugin Configuration
3
 *
4
 * This is the main Cypress plugin configuration file that registers all
5
 * testing utilities as Cypress tasks. It provides a bridge between Cypress
6
 * tests and the various utility modules for data generation, API access,
7
 * and database operations.
8
 *
9
 * @module cypress-plugins
10
 */
2
11
3
const mysql = require("cypress-mysql");
12
const { startDevServer } = require("@cypress/webpack-dev-server");
4
13
5
const { buildSampleObject, buildSampleObjects } = require("./mockData.js");
14
const { buildSampleObject, buildSampleObjects } = require("./mockData.js");
6
15
16
const {
17
    insertSampleBiblio,
18
    insertSampleHold,
19
    insertSampleCheckout,
20
    insertSamplePatron,
21
    insertObject,
22
    deleteSampleObjects,
23
} = require("./insertData.js");
24
25
const { getBasicAuthHeader } = require("./auth.js");
26
27
const { query } = require("./db.js");
28
29
const { apiGet, apiPost, apiPut, apiDelete } = require("./api-client.js");
30
31
/**
32
 * Cypress plugin configuration function.
33
 *
34
 * @function
35
 * @param {Function} on - Cypress plugin registration function
36
 * @param {Object} config - Cypress configuration object
37
 * @param {string} config.baseUrl - Base URL for the application under test
38
 * @param {Object} config.env - Environment variables from cypress.config.js
39
 * @param {string} config.env.apiUsername - Username for API authentication
40
 * @param {string} config.env.apiPassword - Password for API authentication
41
 * @returns {Object} Modified Cypress configuration
42
 * @description This function:
43
 * - Registers all testing utilities as Cypress tasks
44
 * - Sets up authentication headers for API calls
45
 * - Configures the development server for component testing
46
 * - Provides automatic parameter injection for common arguments
47
 *
48
 * Available Cypress tasks:
49
 * - Data Generation: buildSampleObject, buildSampleObjects
50
 * - Data Insertion: insertSampleBiblio, insertSampleHold, insertSampleCheckout, insertSamplePatron
51
 * - Data Cleanup: deleteSampleObjects
52
 * - API Access: apiGet, apiPost, apiPut, apiDelete
53
 * - Database Access: query
54
 * - Authentication: getBasicAuthHeader
55
 *
56
 * @example
57
 * // Usage in Cypress tests
58
 * cy.task('insertSampleBiblio', { item_count: 2 }).then(result => {
59
 *   // Test with the created biblio
60
 * });
61
 *
62
 * @example
63
 * // API call through task
64
 * cy.task('apiGet', { endpoint: '/api/v1/patrons' }).then(patrons => {
65
 *   // Work with patron data
66
 * });
67
 */
7
module.exports = (on, config) => {
68
module.exports = (on, config) => {
69
    const baseUrl = config.baseUrl;
70
    const authHeader = getBasicAuthHeader(
71
        config.env.apiUsername,
72
        config.env.apiPassword
73
    );
74
8
    on("dev-server:start", options =>
75
    on("dev-server:start", options =>
9
        startDevServer({
76
        startDevServer({
10
            options,
77
            options,
11
        })
78
        })
12
    );
79
    );
13
80
14
    mysql.configurePlugin(on);
15
16
    on("task", {
81
    on("task", {
82
        getBasicAuthHeader() {
83
            return getBasicAuthHeader(
84
                config.env.apiUsername,
85
                config.env.apiPassword
86
            );
87
        },
17
        buildSampleObject,
88
        buildSampleObject,
18
        buildSampleObjects,
89
        buildSampleObjects,
90
        insertSampleBiblio(args) {
91
            return insertSampleBiblio({ ...args, baseUrl, authHeader });
92
        },
93
        insertSampleHold(args) {
94
            return insertSampleHold({ ...args, baseUrl, authHeader });
95
        },
96
        insertSampleCheckout(args) {
97
            return insertSampleCheckout({ ...args, baseUrl, authHeader });
98
        },
99
        insertSamplePatron(args) {
100
            return insertSamplePatron({ ...args, baseUrl, authHeader });
101
        },
102
        insertObject(args) {
103
            return insertObject({ ...args, baseUrl, authHeader });
104
        },
105
        deleteSampleObjects,
106
        query,
107
108
        apiGet(args) {
109
            return apiGet({ ...args, baseUrl, authHeader });
110
        },
111
        apiPost(args) {
112
            return apiPost({ ...args, baseUrl, authHeader });
113
        },
114
        apiPut(args) {
115
            return apiPut({ ...args, baseUrl, authHeader });
116
        },
117
        apiDelete(args) {
118
            return apiDelete({ ...args, baseUrl, authHeader });
119
        },
19
    });
120
    });
20
    return config;
121
    return config;
21
};
122
};
(-)a/t/cypress/plugins/insertData.js (+825 lines)
Line 0 Link Here
1
/**
2
 * Koha Cypress Testing Data Insertion Utilities
3
 *
4
 * This module provides functions to create and manage test data for Cypress tests.
5
 * It handles creating complete bibliographic records, patrons, holds, checkouts,
6
 * and other Koha objects with proper relationships and dependencies.
7
 *
8
 * @module insertData
9
 */
10
11
const { buildSampleObject, buildSampleObjects } = require("./mockData.js");
12
const { query } = require("./db.js");
13
14
const { apiGet, apiPost } = require("./api-client.js");
15
16
/**
17
 * Creates a complete bibliographic record with associated items and libraries.
18
 *
19
 * @async
20
 * @function insertSampleBiblio
21
 * @param {Object} params - Configuration parameters
22
 * @param {number} params.item_count - Number of items to create for this biblio
23
 * @param {Object} [params.options] - Additional options
24
 * @param {boolean} [params.options.different_libraries] - If true, creates different libraries for each item
25
 * @param {string} params.baseUrl - Base URL for API calls
26
 * @param {string} params.authHeader - Authorization header for API calls
27
 * @returns {Promise<Object>} Created biblio with items, libraries, and item_type
28
 * @returns {Object} returns.biblio - The created bibliographic record
29
 * @returns {Array<Object>} returns.items - Array of created item records
30
 * @returns {Array<Object>} returns.libraries - Array of created library records
31
 * @returns {Object} returns.item_type - The created item type record
32
 * @example
33
 * // Create a biblio with 3 items using the same library
34
 * const result = await insertSampleBiblio({
35
 *   item_count: 3,
36
 *   baseUrl: 'http://localhost:8081',
37
 *   authHeader: 'Basic dGVzdDp0ZXN0'
38
 * });
39
 *
40
 * @example
41
 * // Create a biblio with 2 items using different libraries
42
 * const result = await insertSampleBiblio({
43
 *   item_count: 2,
44
 *   options: { different_libraries: true },
45
 *   baseUrl: 'http://localhost:8081',
46
 *   authHeader: 'Basic dGVzdDp0ZXN0'
47
 * });
48
 */
49
const insertSampleBiblio = async ({
50
    item_count,
51
    options,
52
    baseUrl,
53
    authHeader,
54
}) => {
55
    const generatedItemType = await buildSampleObject({ object: "item_type" });
56
    const item_type = await insertObject({
57
        type: "item_type",
58
        object: generatedItemType,
59
        baseUrl,
60
        authHeader,
61
    });
62
63
    let title = "Some boring read";
64
    let author = "Some boring author";
65
    let biblio = {
66
        leader: "     nam a22     7a 4500",
67
        fields: [
68
            { "005": "20250120101920.0" },
69
            {
70
                245: {
71
                    ind1: "",
72
                    ind2: "",
73
                    subfields: [{ a: title }],
74
                },
75
            },
76
            {
77
                100: {
78
                    ind1: "",
79
                    ind2: "",
80
                    subfields: [{ c: author }],
81
                },
82
            },
83
            {
84
                942: {
85
                    ind1: "",
86
                    ind2: "",
87
                    subfields: [{ c: item_type.item_type_id }],
88
                },
89
            },
90
        ],
91
    };
92
    let result = await apiPost({
93
        endpoint: "/api/v1/biblios",
94
        headers: {
95
            "Content-Type": "application/marc-in-json",
96
            "x-confirm-not-duplicate": 1,
97
        },
98
        body: biblio,
99
        baseUrl,
100
        authHeader,
101
    });
102
    const biblio_id = result.id;
103
    // We do not have a route to get a biblio as it is stored in DB
104
    // We might need to refine that in the future
105
    biblio = {
106
        biblio_id,
107
        title,
108
        author,
109
    };
110
111
    let items = buildSampleObjects({
112
        object: "item",
113
        count: item_count,
114
        values: {
115
            biblio_id,
116
            lost_status: 0,
117
            withdrawn: 0,
118
            damaged_status: 0,
119
            not_for_loan_status: 0,
120
            restricted_status: 0,
121
            new_status: null,
122
            issues: 0,
123
            checked_out_date: null,
124
            item_type_id: item_type.item_type_id,
125
        },
126
    });
127
    items = items.map(
128
        ({
129
            item_id,
130
            checkout,
131
            transfer,
132
            lost_date,
133
            withdrawn_date,
134
            damaged_date,
135
            course_item,
136
            _strings,
137
            biblio,
138
            bundle_host,
139
            item_group_item,
140
            recall,
141
            return_claim,
142
            return_claims,
143
            serial_item,
144
            first_hold,
145
            checkouts_count,
146
            renewals_count,
147
            holds_count,
148
            bundle_items_lost_count,
149
            analytics_count,
150
            effective_not_for_loan_status,
151
            effective_item_type_id,
152
            home_library,
153
            holding_library,
154
            bundle_items_not_lost_count,
155
            item_type,
156
            _status,
157
            effective_bookable,
158
            in_bundle,
159
            cover_image_ids,
160
            localuse,
161
            ...rest
162
        }) => rest
163
    );
164
    let createdItems = [];
165
    let libraries = [];
166
    let commonLibrary;
167
    if (!options || !options.different_libraries) {
168
        const generatedLibrary = await buildSampleObject({ object: "library" });
169
        commonLibrary = await insertObject({
170
            type: "library",
171
            object: generatedLibrary,
172
            baseUrl,
173
            authHeader,
174
        });
175
        libraries.push(commonLibrary);
176
    }
177
    for (const item of items) {
178
        if (options?.different_libraries) {
179
            const generatedLibrary = await buildSampleObject({
180
                object: "library",
181
            });
182
            const library = await insertObject({
183
                type: "library",
184
                object: generatedLibrary,
185
                baseUrl,
186
                authHeader,
187
            });
188
            libraries.push(library);
189
            item.home_library_id = library.library_id;
190
            item.holding_library_id = library.library_id;
191
        } else {
192
            item.home_library_id = commonLibrary.library_id;
193
            item.holding_library_id = commonLibrary.library_id;
194
        }
195
196
        await apiPost({
197
            endpoint: `/api/v1/biblios/${biblio_id}/items`,
198
            body: item,
199
            baseUrl,
200
            authHeader,
201
        }).then(i => createdItems.push(i));
202
    }
203
    return { biblio, items: createdItems, libraries, item_type };
204
};
205
206
/**
207
 * Creates a hold request for a bibliographic record or item.
208
 *
209
 * @async
210
 * @function insertSampleHold
211
 * @param {Object} params - Configuration parameters
212
 * @param {Object} [params.item] - Item to place hold on (optional if biblio provided)
213
 * @param {Object} [params.biblio] - Biblio to place hold on (optional if item provided)
214
 * @param {string} [params.library_id] - Library ID for pickup location (defaults to item's home library)
215
 * @param {string} params.baseUrl - Base URL for API calls
216
 * @param {string} params.authHeader - Authorization header for API calls
217
 * @returns {Promise<Object>} Created hold with associated patron and patron_category
218
 * @returns {Object} returns.hold - The created hold record
219
 * @returns {Object} returns.patron - The patron who placed the hold
220
 * @returns {Object} returns.patron_category - The patron's category
221
 * @throws {Error} When neither library_id nor item is provided
222
 * @example
223
 * // Create a hold on a specific item
224
 * const holdResult = await insertSampleHold({
225
 *   item: { item_id: 123, home_library_id: 'CPL' },
226
 *   baseUrl: 'http://localhost:8081',
227
 *   authHeader: 'Basic dGVzdDp0ZXN0'
228
 * });
229
 *
230
 * @example
231
 * // Create a biblio-level hold
232
 * const holdResult = await insertSampleHold({
233
 *   biblio: { biblio_id: 456 },
234
 *   library_id: 'CPL',
235
 *   baseUrl: 'http://localhost:8081',
236
 *   authHeader: 'Basic dGVzdDp0ZXN0'
237
 * });
238
 */
239
const insertSampleHold = async ({
240
    item,
241
    biblio,
242
    library_id,
243
    baseUrl,
244
    authHeader,
245
}) => {
246
    library_id ||= item?.home_library_id;
247
248
    if (!library_id) {
249
        throw new Error(
250
            "Could not generate sample hold without library_id or item"
251
        );
252
    }
253
254
    const { patron, patron_category } = await insertSamplePatron({
255
        library: { library_id },
256
        baseUrl,
257
        authHeader,
258
    });
259
260
    const generatedHold = buildSampleObject({
261
        object: "hold",
262
        values: {
263
            patron_id: patron.patron_id,
264
            biblio_id: item?.biblio_id || biblio.biblio_id,
265
            pickup_library_id: library_id,
266
            item_id: item?.item_id || null,
267
        },
268
    });
269
    const hold = await insertObject({
270
        type: "hold",
271
        object: generatedHold,
272
        baseUrl,
273
        authHeader,
274
    });
275
    return { hold, patron, patron_category };
276
};
277
278
/**
279
 * Creates a checkout record with associated biblio, item, and optional patron.
280
 *
281
 * @async
282
 * @function insertSampleCheckout
283
 * @param {Object} params - Configuration parameters
284
 * @param {Object} [params.patron] - Existing patron to check out to (creates new if not provided)
285
 * @param {string} params.baseUrl - Base URL for API calls
286
 * @param {string} params.authHeader - Authorization header for API calls
287
 * @returns {Promise<Object>} Created checkout with all associated records
288
 * @returns {Object} returns.biblio - The bibliographic record
289
 * @returns {Array<Object>} returns.items - Array of item records
290
 * @returns {Array<Object>} returns.libraries - Array of library records
291
 * @returns {Object} returns.item_type - The item type record
292
 * @returns {Object} returns.checkout - The checkout record
293
 * @returns {Object} [returns.patron] - The patron record (if generated)
294
 * @returns {Object} [returns.patron_category] - The patron category (if generated)
295
 * @example
296
 * // Create a checkout with a new patron
297
 * const checkoutResult = await insertSampleCheckout({
298
 *   baseUrl: 'http://localhost:8081',
299
 *   authHeader: 'Basic dGVzdDp0ZXN0'
300
 * });
301
 *
302
 * @example
303
 * // Create a checkout for an existing patron
304
 * const checkoutResult = await insertSampleCheckout({
305
 *   patron: { patron_id: 123 },
306
 *   baseUrl: 'http://localhost:8081',
307
 *   authHeader: 'Basic dGVzdDp0ZXN0'
308
 * });
309
 */
310
const insertSampleCheckout = async ({ patron, baseUrl, authHeader }) => {
311
    const { biblio, items, libraries, item_type } = await insertSampleBiblio({
312
        item_count: 1,
313
        baseUrl,
314
        authHeader,
315
    });
316
317
    let generatedPatron;
318
    let patronCategory;
319
    if (!patron) {
320
        generatedPatron = true;
321
        const patron_objects = await insertSamplePatron({
322
            library: { library_id: libraries[0].library_id },
323
            baseUrl,
324
            authHeader,
325
        });
326
        generatedCategory = patron_objects.category;
327
        patron = patron_objects.patron;
328
    }
329
330
    const generatedCheckout = buildSampleObject({
331
        object: "checkout",
332
        values: {
333
            patron_id: patron.patron_id,
334
            item_id: items[0].item_id,
335
        },
336
    });
337
    delete generatedCheckout.external_id;
338
    const checkout = await insertObject({
339
        type: "checkout",
340
        object: generatedCheckout,
341
        baseUrl,
342
        authHeader,
343
    });
344
    return {
345
        biblio,
346
        items,
347
        libraries,
348
        item_type,
349
        checkout,
350
        ...(generatedPatron
351
            ? {
352
                  patron,
353
                  patron_category: generatedCategory,
354
              }
355
            : {}),
356
    };
357
};
358
359
/**
360
 * Creates a patron record with associated library and category.
361
 *
362
 * @async
363
 * @function insertSamplePatron
364
 * @param {Object} params - Configuration parameters
365
 * @param {Object} [params.library] - Library to assign patron to (creates new if not provided)
366
 * @param {Object} [params.patron_category] - Patron category to assign (creates new if not provided)
367
 * @param {string} params.baseUrl - Base URL for API calls
368
 * @param {string} params.authHeader - Authorization header for API calls
369
 * @returns {Promise<Object>} Created patron with associated records
370
 * @returns {Object} returns.patron - The created patron record
371
 * @returns {Object} [returns.library] - The library record (if generated)
372
 * @returns {Object} [returns.patron_category] - The patron category record (if generated)
373
 * @example
374
 * // Create a patron with new library and category
375
 * const patronResult = await insertSamplePatron({
376
 *   baseUrl: 'http://localhost:8081',
377
 *   authHeader: 'Basic dGVzdDp0ZXN0'
378
 * });
379
 *
380
 * @example
381
 * // Create a patron for an existing library
382
 * const patronResult = await insertSamplePatron({
383
 *   library: { library_id: 'CPL' },
384
 *   baseUrl: 'http://localhost:8081',
385
 *   authHeader: 'Basic dGVzdDp0ZXN0'
386
 * });
387
 */
388
const insertSamplePatron = async ({
389
    library,
390
    patron_category,
391
    baseUrl,
392
    authHeader,
393
}) => {
394
    let generatedLibrary;
395
    let generatedCategory;
396
    if (!library) {
397
        generatedLibrary = await buildSampleObject({ object: "library" });
398
        library = await insertLibrary({
399
            library: generatedLibrary,
400
            baseUrl,
401
            authHeader,
402
        });
403
    }
404
    if (!patron_category) {
405
        generatedCategory = await buildSampleObject({
406
            object: "patron_category",
407
        });
408
        query({
409
            sql: "INSERT INTO categories(categorycode, description) VALUES (?, ?)",
410
            values: [
411
                generatedCategory.patron_category_id,
412
                `description for ${generatedCategory.patron_category_id}`,
413
            ],
414
        });
415
        // FIXME We need /patron_categories/:patron_category_id
416
        await apiGet({
417
            endpoint: `/api/v1/patron_categories?q={"me.patron_category_id":"${generatedCategory.patron_category_id}"}`,
418
            baseUrl,
419
            authHeader,
420
        }).then(categories => (patron_category = categories[0]));
421
    }
422
423
    let generatedPatron = await buildSampleObject({
424
        object: "patron",
425
        values: {
426
            library_id: library.library_id,
427
            category_id: patron_category.patron_category_id,
428
            incorrect_address: null,
429
            patron_card_lost: null,
430
        },
431
    });
432
433
    let {
434
        patron_id,
435
        _strings,
436
        anonymized,
437
        restricted,
438
        expired,
439
        extended_attributes,
440
        checkouts_count,
441
        overdues_count,
442
        account_balance,
443
        lang,
444
        login_attempts,
445
        sms_provider_id,
446
        ...patron
447
    } = generatedPatron;
448
    delete patron.library;
449
450
    patron = await apiPost({
451
        endpoint: `/api/v1/patrons`,
452
        body: patron,
453
        baseUrl,
454
        authHeader,
455
    });
456
457
    return {
458
        patron,
459
        ...(generatedLibrary ? { library } : {}),
460
        ...(generatedCategory ? { patron_category } : {}),
461
    };
462
};
463
464
/**
465
 * Deletes test objects from the database in the correct order to respect foreign key constraints.
466
 *
467
 * @async
468
 * @function deleteSampleObjects
469
 * @param {Object|Array<Object>} allObjects - Object(s) to delete, can be single object or array
470
 * @returns {Promise<boolean>} True if deletion was successful
471
 * @description This function handles cleanup of test data by:
472
 * - Accepting single objects or arrays of objects
473
 * - Grouping objects by type (holds, checkouts, patrons, items, etc.)
474
 * - Deleting in dependency order to avoid foreign key violations
475
 * - Supporting all major Koha object types
476
 * @example
477
 * // Delete a single test result
478
 * await deleteSampleObjects(checkoutResult);
479
 *
480
 * @example
481
 * // Delete multiple test results
482
 * await deleteSampleObjects([biblioResult, holdResult, checkoutResult]);
483
 *
484
 * @example
485
 * // Delete after creating test data
486
 * const biblio = await insertSampleBiblio({ item_count: 2, baseUrl, authHeader });
487
 * const hold = await insertSampleHold({ item: biblio.items[0], baseUrl, authHeader });
488
 * // ... run tests ...
489
 * await deleteSampleObjects([biblio, hold]);
490
 */
491
const deleteSampleObjects = async allObjects => {
492
    if (!Array.isArray(allObjects)) {
493
        allObjects = [allObjects];
494
    }
495
496
    const pluralMap = {
497
        hold: "holds",
498
        checkout: "checkouts",
499
        old_checkout: "old_checkouts",
500
        basket: "baskets",
501
        vendor: "vendors",
502
        patron: "patrons",
503
        item: "items",
504
        biblio: "biblios",
505
        library: "libraries",
506
        item_type: "item_types",
507
    };
508
    // Merge by type
509
    const mergedObjects = {};
510
    for (const objects of allObjects) {
511
        for (const [type, value] of Object.entries(objects)) {
512
            let plural = pluralMap?.[type] || type;
513
            if (!mergedObjects[plural]) {
514
                mergedObjects[plural] = [];
515
            }
516
517
            if (Array.isArray(value)) {
518
                mergedObjects[plural].push(...value);
519
            } else {
520
                mergedObjects[plural].push(value);
521
            }
522
        }
523
    }
524
525
    const deletionOrder = [
526
        "holds",
527
        "checkouts",
528
        "old_checkouts",
529
        "baskets",
530
        "vendors",
531
        "patrons",
532
        "items",
533
        "biblios",
534
        "libraries",
535
        "item_types",
536
    ];
537
538
    for (const type of deletionOrder) {
539
        if (!mergedObjects[type] || mergedObjects[type].length === 0) {
540
            continue;
541
        }
542
543
        const objects = mergedObjects[type];
544
        let ids = [];
545
        switch (type) {
546
            case "biblios":
547
                ids = objects.map(i => i.biblio_id);
548
                await query({
549
                    sql: `DELETE FROM biblio WHERE biblionumber IN (${ids.map(() => "?").join(",")})`,
550
                    values: ids,
551
                });
552
                break;
553
            case "items":
554
                ids = objects.map(i => i.item_id);
555
                await query({
556
                    sql: `DELETE FROM items WHERE itemnumber IN (${ids.map(() => "?").join(",")})`,
557
                    values: ids,
558
                });
559
                break;
560
            case "libraries":
561
                ids = objects.map(i => i.library_id);
562
                await query({
563
                    sql: `DELETE FROM branches WHERE branchcode IN (${ids.map(() => "?").join(",")})`,
564
                    values: ids,
565
                });
566
                break;
567
            case "holds":
568
                ids = objects.map(i => i.hold_id);
569
                await query({
570
                    sql: `DELETE FROM reserves WHERE reserve_id IN (${ids.map(() => "?").join(",")})`,
571
                    values: ids,
572
                });
573
                break;
574
            case "checkouts":
575
                ids = objects.map(i => i.checkout_id);
576
                await query({
577
                    sql: `DELETE FROM issues WHERE issue_id IN (${ids.map(() => "?").join(",")})`,
578
                    values: ids,
579
                });
580
                break;
581
            case "old_checkouts":
582
                ids = objects.map(i => i.checkout_id);
583
                await query({
584
                    sql: `DELETE FROM old_issues WHERE issue_id IN (${ids.map(() => "?").join(",")})`,
585
                    values: ids,
586
                });
587
                break;
588
            case "item_types":
589
                ids = objects.map(i => i.item_type_id);
590
                await query({
591
                    sql: `DELETE FROM itemtypes WHERE itemtype IN (${ids.map(() => "?").join(",")})`,
592
                    values: ids,
593
                });
594
                break;
595
            case "patrons":
596
                ids = objects.map(i => i.patron_id);
597
                await query({
598
                    sql: `DELETE FROM borrowers WHERE borrowernumber IN (${ids.map(() => "?").join(",")})`,
599
                    values: ids,
600
                });
601
                break;
602
            case "baskets":
603
                ids = objects.map(i => i.basket_id);
604
                await query({
605
                    sql: `DELETE FROM aqbasket WHERE basketno IN (${ids.map(() => "?").join(",")})`,
606
                    values: ids,
607
                });
608
                break;
609
            case "vendors":
610
                ids = objects.map(i => i.id);
611
                await query({
612
                    sql: `DELETE FROM aqbooksellers WHERE id IN (${ids.map(() => "?").join(",")})`,
613
                    values: ids,
614
                });
615
                break;
616
            default:
617
                throw Error(
618
                    `Not implemented yet: cannot deleted object '${type}'`
619
                );
620
        }
621
    }
622
    return true;
623
};
624
625
/**
626
 * Creates a library record via API, filtering out unsupported fields.
627
 *
628
 * @async
629
 * @function insertLibrary
630
 * @param {Object} params - Configuration parameters
631
 * @param {Object} params.library - Library object to insert
632
 * @param {string} params.baseUrl - Base URL for API calls
633
 * @param {string} params.authHeader - Authorization header for API calls
634
 * @returns {Promise<Object>} Created library record
635
 * @private
636
 * @description This is a helper function that removes fields not supported by the API
637
 * before creating the library record.
638
 */
639
const insertLibrary = async ({ library, baseUrl, authHeader }) => {
640
    const {
641
        pickup_items,
642
        smtp_server,
643
        cash_registers,
644
        desks,
645
        library_hours,
646
        needs_override,
647
        ...new_library
648
    } = library;
649
    return apiPost({
650
        endpoint: "/api/v1/libraries",
651
        body: new_library,
652
        baseUrl,
653
        authHeader,
654
    });
655
};
656
657
/**
658
 * Generic function to insert various types of Koha objects.
659
 *
660
 * @async
661
 * @function insertObject
662
 * @param {Object} params - Configuration parameters
663
 * @param {string} params.type - Type of object to insert ('library', 'item_type', 'hold', 'checkout', 'vendor', 'basket')
664
 * @param {Object} params.object - Object data to insert
665
 * @param {string} params.baseUrl - Base URL for API calls
666
 * @param {string} params.authHeader - Authorization header for API calls
667
 * @returns {Promise<Object|boolean>} Created object or true if successful
668
 * @throws {Error} When object type is not supported
669
 * @private
670
 * @description This is a generic helper function that handles the specifics of creating
671
 * different types of Koha objects. Each object type may require different field filtering,
672
 * API endpoints, or database operations.
673
 *
674
 * Supported object types:
675
 * - library: Creates library via API
676
 * - item_type: Creates item type via database query
677
 * - hold: Creates hold via API
678
 * - checkout: Creates checkout via API with confirmation token support
679
 * - vendor: Creates vendor via API
680
 * - basket: Creates basket via database query
681
 */
682
const insertObject = async ({ type, object, baseUrl, authHeader }) => {
683
    if (type == "library") {
684
        const keysToKeep = ["library_id", "name"];
685
        const library = Object.fromEntries(
686
            Object.entries(object).filter(([key]) => keysToKeep.includes(key))
687
        );
688
        return apiPost({
689
            endpoint: "/api/v1/libraries",
690
            body: library,
691
            baseUrl,
692
            authHeader,
693
        });
694
    } else if (type == "item_type") {
695
        const keysToKeep = ["item_type_id", "description"];
696
        const item_type = Object.fromEntries(
697
            Object.entries(object).filter(([key]) => keysToKeep.includes(key))
698
        );
699
        return query({
700
            sql: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)",
701
            values: [item_type.item_type_id, item_type.description],
702
        })
703
            .then(result => {
704
                // FIXME We need /item_types/:item_type_id
705
                return apiGet({
706
                    endpoint: `/api/v1/item_types?q={"item_type_id":"${item_type.item_type_id}"}`,
707
                    baseUrl,
708
                    authHeader,
709
                });
710
            })
711
            .then(item_types => item_types[0]);
712
    } else if (type == "hold") {
713
        const {
714
            hold_id,
715
            deleted_biblio_id,
716
            item_group_id,
717
            desk_id,
718
            cancellation_date,
719
            cancellation_reason,
720
            notes,
721
            priority,
722
            status,
723
            timestamp,
724
            waiting_date,
725
            expiration_date,
726
            lowest_priority,
727
            suspended,
728
            suspended_until,
729
            non_priority,
730
            item_type,
731
            item_level,
732
            cancellation_requested,
733
            biblio,
734
            deleted_biblio,
735
            item,
736
            pickup_library,
737
            hold_date,
738
            ...hold
739
        } = object;
740
741
        return apiPost({
742
            endpoint: `/api/v1/holds`,
743
            body: hold,
744
            baseUrl,
745
            authHeader,
746
        });
747
    } else if (type == "checkout") {
748
        const { issuer, patron, ...checkout } = object;
749
750
        let endpoint = "/api/v1/checkouts";
751
        // Force the checkout - we might need a parameter to control this behaviour later
752
        await apiGet({
753
            endpoint: `/api/v1/checkouts/availability?item_id=${object.item_id}&patron_id=${object.patron_id}`,
754
            baseUrl,
755
            authHeader,
756
        }).then(result => {
757
            if (result.confirmation_token) {
758
                endpoint += `?confirmation=${result.confirmation_token}`;
759
            }
760
        });
761
762
        return apiPost({
763
            endpoint,
764
            body: checkout,
765
            baseUrl,
766
            authHeader,
767
        });
768
    } else if (type == "vendor") {
769
        const {
770
            id,
771
            baskets_count,
772
            invoices_count,
773
            subscriptions_count,
774
            external_id,
775
            aliases,
776
            baskets,
777
            contacts,
778
            contracts,
779
            interfaces,
780
            invoices,
781
            ...vendor
782
        } = object;
783
784
        let endpoint = "/api/v1/acquisitions/vendors";
785
786
        return apiPost({
787
            endpoint,
788
            body: vendor,
789
            baseUrl,
790
            authHeader,
791
        });
792
    } else if (type == "basket") {
793
        const keysToKeep = ["name", "vendor_id", "close_date"];
794
        const basket = Object.fromEntries(
795
            Object.entries(object).filter(([key]) => keysToKeep.includes(key))
796
        );
797
        return query({
798
            sql: "INSERT INTO aqbasket(basketname, booksellerid, closedate) VALUES (?, ?, ?)",
799
            values: [basket.name, basket.vendor_id, basket.close_date],
800
        })
801
            .then(result => {
802
                const basket_id = result.insertId;
803
                // FIXME We need /acquisitions/baskets/:basket_id
804
                return apiGet({
805
                    endpoint: `/api/v1/acquisitions/baskets?q={"basket_id":"${basket_id}"}`,
806
                    baseUrl,
807
                    authHeader,
808
                });
809
            })
810
            .then(baskets => baskets[0]);
811
    } else {
812
        throw Error(`Unsupported object type '${type}' to insert`);
813
    }
814
815
    return true;
816
};
817
818
module.exports = {
819
    insertSampleBiblio,
820
    insertSampleHold,
821
    insertSampleCheckout,
822
    insertSamplePatron,
823
    insertObject,
824
    deleteSampleObjects,
825
};
(-)a/t/cypress/plugins/mockData.js (-11 / +204 lines)
Lines 1-21 Link Here
1
/**
2
 * Mock Data Generation for Cypress Testing
3
 *
4
 * This module provides functions to generate realistic test data for Koha objects
5
 * based on OpenAPI schema definitions. It uses Faker.js to generate random data
6
 * that conforms to the API specifications.
7
 *
8
 * @module mockData
9
 */
10
1
const { faker } = require("@faker-js/faker");
11
const { faker } = require("@faker-js/faker");
2
const { readYamlFile } = require("./../plugins/readYamlFile.js");
12
const { readYamlFile } = require("./../plugins/readYamlFile.js");
13
const { query } = require("./db.js");
3
const fs = require("fs");
14
const fs = require("fs");
4
15
16
/**
17
 * Cache to store generated ID values to prevent duplicates
18
 * @type {Set<string>}
19
 */
20
const generatedDataCache = new Set();
21
22
/**
23
 * Generates mock data for a specific data type based on OpenAPI schema properties.
24
 *
25
 * @function generateMockData
26
 * @param {string} type - The data type (string, integer, boolean, array, number, date, date-time)
27
 * @param {Object} properties - OpenAPI schema properties for the field
28
 * @param {Array} [properties.enum] - Enumerated values to choose from
29
 * @param {number} [properties.maxLength] - Maximum length for strings
30
 * @param {number} [properties.minLength] - Minimum length for strings
31
 * @returns {*} Generated mock data appropriate for the type
32
 * @private
33
 * @example
34
 * // Generate a string with max length 50
35
 * const name = generateMockData('string', { maxLength: 50 });
36
 *
37
 * // Generate from enum values
38
 * const status = generateMockData('string', { enum: ['active', 'inactive'] });
39
 */
5
const generateMockData = (type, properties) => {
40
const generateMockData = (type, properties) => {
41
    if (properties.hasOwnProperty("enum")) {
42
        let values = properties.enum;
43
        return values[Math.floor(Math.random() * values.length)];
44
    }
45
6
    switch (type) {
46
    switch (type) {
7
        case "string":
47
        case "string":
8
            if (properties?.maxLength) {
48
            if (properties?.maxLength) {
9
                return faker.string.alpha({
49
                // The propability to have a string with length=1 is the same as length=10
50
                // We have very limited pool of possible values for length=1 which will result in a "Duplicate ID" error from the server
51
                // Setting minLength to 3 to prevent this kind of failures
52
                let minLength =
53
                    properties.minLength === 1 ||
54
                    properties.minLength === undefined
55
                        ? 3
56
                        : properties.minLength;
57
58
                if (
59
                    properties.maxLength !== undefined &&
60
                    properties.maxLength < minLength
61
                ) {
62
                    minLength = properties.maxLength;
63
                }
64
                return (value = faker.string.alpha({
10
                    length: {
65
                    length: {
11
                        min: properties.minLength || 1,
66
                        min: minLength,
12
                        max: properties.maxLength,
67
                        max: properties.maxLength,
13
                    },
68
                    },
14
                });
69
                }));
15
            }
70
            }
16
            return faker.lorem.words(3);
71
            return (value = faker.lorem.words(3));
17
        case "integer":
72
        case "integer":
18
            return faker.number.int();
73
            // Do not return more than int(11);
74
            return faker.number.int(2 ** 31 - 1);
19
        case "boolean":
75
        case "boolean":
20
            return faker.datatype.boolean();
76
            return faker.datatype.boolean();
21
        case "array":
77
        case "array":
Lines 31-36 const generateMockData = (type, properties) => { Link Here
31
    }
87
    }
32
};
88
};
33
89
90
/**
91
 * Generates mock data for an entire object based on OpenAPI schema properties.
92
 *
93
 * @function generateDataFromSchema
94
 * @param {Object} properties - OpenAPI schema properties object
95
 * @param {Object} [values={}] - Override values for specific fields
96
 * @returns {Object} Generated mock object with all required fields
97
 * @private
98
 * @description This function:
99
 * - Iterates through all properties in the schema
100
 * - Generates appropriate mock data for each field
101
 * - Handles object relationships (libraries, items, etc.)
102
 * - Ensures unique values for ID fields
103
 * - Applies any override values provided
104
 *
105
 * Special handling for object relationships:
106
 * - home_library/holding_library -> generates library object
107
 * - item_type -> generates item_type object
108
 * - Automatically sets corresponding _id fields
109
 */
34
const generateDataFromSchema = (properties, values = {}) => {
110
const generateDataFromSchema = (properties, values = {}) => {
35
    const mockData = {};
111
    const mockData = {};
36
    const ids = {};
112
    const ids = {};
Lines 56-80 const generateDataFromSchema = (properties, values = {}) => { Link Here
56
                        data = buildSampleObject({ object: "library" });
132
                        data = buildSampleObject({ object: "library" });
57
                        fk_name = "library_id";
133
                        fk_name = "library_id";
58
                        break;
134
                        break;
135
                    case "pickup_library":
136
                        data = buildSampleObject({ object: "library" });
137
                        fk_name = "pickup_library_id";
138
                        break;
139
                    case "library":
140
                        data = buildSampleObject({ object: "library" });
141
                        fk_name = "library_id";
142
                        break;
59
                    case "item_type":
143
                    case "item_type":
60
                        data = buildSampleObject({ object: "item_type" });
144
                        data = buildSampleObject({ object: "item_type" });
61
                        fk_name = "item_type_id";
145
                        fk_name = "item_type_id";
62
                        break;
146
                        break;
147
                    case "item":
148
                        data = buildSampleObject({ object: "item" });
149
                        fk_name = "item_id";
150
                        break;
63
                    default:
151
                    default:
64
                        data = generateMockData(type, value);
152
                        try {
153
                            data = generateMockData(type, value);
154
                        } catch (e) {
155
                            throw new Error(
156
                                `Failed to generate data for (${key}): ${e}`
157
                            );
158
                        }
65
                }
159
                }
66
                if (typeof data === "object") {
160
                if (typeof data === "object") {
67
                    ids[key] = data[fk_name];
161
                    ids[key] = data[fk_name];
68
                }
162
                }
69
            } else {
163
            } else {
70
                data = generateMockData(type, value);
164
                try {
165
                    if (key.match(/_id$/)) {
166
                        let attempts = 0;
167
168
                        do {
169
                            data = generateMockData(type, value);
170
                            attempts++;
171
                            if (attempts > 10) {
172
                                throw new Error(
173
                                    "Could not generate unique string after 10 attempts"
174
                                );
175
                            }
176
                        } while (generatedDataCache.has(data));
177
178
                        generatedDataCache.add(data);
179
                    } else {
180
                        data = generateMockData(type, value);
181
                    }
182
                } catch (e) {
183
                    throw new Error(
184
                        `Failed to generate data for ${key} (${type}): ${e}`
185
                    );
186
                }
71
            }
187
            }
72
            mockData[key] = data;
188
            mockData[key] = data;
73
        }
189
        }
74
    });
190
    });
75
191
76
    Object.keys(ids).forEach(k => {
192
    Object.keys(ids).forEach(k => {
77
        if (mockData.hasOwnProperty(k + "_id")) {
193
        if (
194
            mockData.hasOwnProperty(k + "_id") &&
195
            !values.hasOwnProperty(k + "_id")
196
        ) {
78
            mockData[k + "_id"] = ids[k];
197
            mockData[k + "_id"] = ids[k];
79
        }
198
        }
80
    });
199
    });
Lines 82-87 const generateDataFromSchema = (properties, values = {}) => { Link Here
82
    return mockData;
201
    return mockData;
83
};
202
};
84
203
204
/**
205
 * Builds an array of sample objects based on OpenAPI schema definitions.
206
 *
207
 * @function buildSampleObjects
208
 * @param {Object} params - Configuration parameters
209
 * @param {string} params.object - Object type to generate (must match YAML file name)
210
 * @param {Object} [params.values] - Override values for specific fields
211
 * @param {number} [params.count=1] - Number of objects to generate
212
 * @returns {Array<Object>} Array of generated objects
213
 * @throws {Error} When object type is not supported or generation fails
214
 * @description This function:
215
 * - Reads the OpenAPI schema from api/v1/swagger/definitions/{object}.yaml
216
 * - Generates the specified number of objects
217
 * - Applies any override values to all generated objects
218
 * - Ensures all objects conform to the API schema
219
 *
220
 * @example
221
 * // Generate 3 patron objects
222
 * const patrons = buildSampleObjects({
223
 *   object: 'patron',
224
 *   count: 3
225
 * });
226
 *
227
 * @example
228
 * // Generate 2 items with specific library
229
 * const items = buildSampleObjects({
230
 *   object: 'item',
231
 *   values: { library_id: 'CPL' },
232
 *   count: 2
233
 * });
234
 */
85
const buildSampleObjects = ({ object, values, count = 1 }) => {
235
const buildSampleObjects = ({ object, values, count = 1 }) => {
86
    const yamlPath = `api/v1/swagger/definitions/${object}.yaml`;
236
    const yamlPath = `api/v1/swagger/definitions/${object}.yaml`;
87
    if (!fs.existsSync(yamlPath)) {
237
    if (!fs.existsSync(yamlPath)) {
Lines 90-100 const buildSampleObjects = ({ object, values, count = 1 }) => { Link Here
90
        );
240
        );
91
    }
241
    }
92
    const schema = readYamlFile(yamlPath);
242
    const schema = readYamlFile(yamlPath);
93
    return Array.from({ length: count }, () =>
243
    let generatedObject;
94
        generateDataFromSchema(schema.properties, values)
244
    try {
95
    );
245
        generatedObject = Array.from({ length: count }, () =>
246
            generateDataFromSchema(schema.properties, values)
247
        );
248
    } catch (e) {
249
        throw new Error(`Failed to generate data for object '${object}': ${e}`);
250
    }
251
    return generatedObject;
96
};
252
};
97
253
254
/**
255
 * Builds a single sample object based on OpenAPI schema definitions.
256
 *
257
 * @function buildSampleObject
258
 * @param {Object} params - Configuration parameters
259
 * @param {string} params.object - Object type to generate (must match YAML file name)
260
 * @param {Object} [params.values={}] - Override values for specific fields
261
 * @returns {Object} Generated object conforming to API schema
262
 * @throws {Error} When object type is not supported or generation fails
263
 * @description This is a convenience function that generates a single object
264
 * by calling buildSampleObjects with count=1 and returning the first result.
265
 *
266
 * Supported object types include:
267
 * - patron: Library patron/borrower
268
 * - item: Bibliographic item
269
 * - biblio: Bibliographic record
270
 * - library: Library/branch
271
 * - hold: Hold/reservation request
272
 * - checkout: Circulation checkout
273
 * - vendor: Acquisitions vendor
274
 * - basket: Acquisitions basket
275
 * - And others as defined in api/v1/swagger/definitions/
276
 *
277
 * @example
278
 * // Generate a single patron
279
 * const patron = buildSampleObject({ object: 'patron' });
280
 *
281
 * @example
282
 * // Generate an item with specific values
283
 * const item = buildSampleObject({
284
 *   object: 'item',
285
 *   values: {
286
 *     barcode: '12345678',
287
 *     home_library_id: 'CPL'
288
 *   }
289
 * });
290
 */
98
const buildSampleObject = ({ object, values = {} }) => {
291
const buildSampleObject = ({ object, values = {} }) => {
99
    return buildSampleObjects({ object, values })[0];
292
    return buildSampleObjects({ object, values })[0];
100
};
293
};
(-)a/t/cypress/plugins/readYamlFile.js (+31 lines)
Lines 1-7 Link Here
1
/**
2
 * YAML File Reading Utilities for Cypress Testing
3
 *
4
 * This module provides utilities for reading and parsing YAML files,
5
 * primarily used for loading OpenAPI schema definitions during test
6
 * data generation.
7
 *
8
 * @module readYamlFile
9
 */
10
1
const path = require("path");
11
const path = require("path");
2
const fs = require("fs");
12
const fs = require("fs");
3
const yaml = require("yaml");
13
const yaml = require("yaml");
4
14
15
/**
16
 * Reads and parses a YAML file.
17
 *
18
 * @function readYamlFile
19
 * @param {string} filePath - Path to the YAML file (relative or absolute)
20
 * @returns {Object} Parsed YAML content as a JavaScript object
21
 * @throws {Error} When file doesn't exist or YAML parsing fails
22
 * @description This function:
23
 * - Resolves the file path to an absolute path
24
 * - Checks if the file exists before attempting to read
25
 * - Reads the file content as UTF-8 text
26
 * - Parses the YAML content into a JavaScript object
27
 *
28
 * @example
29
 * // Read an OpenAPI schema definition
30
 * const patronSchema = readYamlFile('api/v1/swagger/definitions/patron.yaml');
31
 *
32
 * @example
33
 * // Read a configuration file
34
 * const config = readYamlFile('./config/test-config.yaml');
35
 */
5
const readYamlFile = filePath => {
36
const readYamlFile = filePath => {
6
    const absolutePath = path.resolve(filePath);
37
    const absolutePath = path.resolve(filePath);
7
    if (!fs.existsSync(absolutePath)) {
38
    if (!fs.existsSync(absolutePath)) {
(-)a/t/cypress/support/e2e.js (-3 / +31 lines)
Lines 32-37 function get_fallback_login_value(param) { Link Here
32
        : Cypress.env(env_var);
32
        : Cypress.env(env_var);
33
}
33
}
34
34
35
Cypress.Commands.add("visitOpac", path => {
36
    cy.visit(Cypress.env("opacBaseUrl") + path);
37
});
38
35
Cypress.Commands.add("login", (username, password) => {
39
Cypress.Commands.add("login", (username, password) => {
36
    var user =
40
    var user =
37
        typeof username === "undefined"
41
        typeof username === "undefined"
Lines 47-52 Cypress.Commands.add("login", (username, password) => { Link Here
47
    cy.get("#submit-button").click();
51
    cy.get("#submit-button").click();
48
});
52
});
49
53
54
Cypress.Commands.add("loginOpac", (username, password) => {
55
    var user =
56
        typeof username === "undefined"
57
            ? get_fallback_login_value("username")
58
            : username;
59
    var pass =
60
        typeof password === "undefined"
61
            ? get_fallback_login_value("password")
62
            : password;
63
    cy.visitOpac("/cgi-bin/koha/opac-main.pl?logout.x=1");
64
    cy.get("#userid").type(user);
65
    cy.get("#password").type(pass);
66
    cy.get("#auth .action").contains("Log in").click();
67
});
68
50
Cypress.Commands.add("left_menu_active_item_is", label => {
69
Cypress.Commands.add("left_menu_active_item_is", label => {
51
    cy.get(".sidebar_menu a.current:not(.disabled)")
70
    cy.get(".sidebar_menu a.current:not(.disabled)")
52
        .should("have.length", 1)
71
        .should("have.length", 1)
Lines 1874-1882 cy.getVendor = () => { Link Here
1874
    };
1893
    };
1875
};
1894
};
1876
1895
1877
const mysql = require("cypress-mysql");
1878
mysql.addCommands();
1879
1880
Cypress.Commands.add("set_syspref", (variable, value) => {
1896
Cypress.Commands.add("set_syspref", (variable, value) => {
1881
    cy.window().then(win => {
1897
    cy.window().then(win => {
1882
        const client = win.APIClient.sysprefs;
1898
        const client = win.APIClient.sysprefs;
Lines 1919-1921 Cypress.Commands.add("mock_table_settings", (settings, table_settings_var) => { Link Here
1919
        cy.wrap(table_settings.columns).as("columns");
1935
        cy.wrap(table_settings.columns).as("columns");
1920
    });
1936
    });
1921
});
1937
});
1938
1939
before(() => {
1940
    cy.task("query", {
1941
        sql: "SELECT value FROM systempreferences WHERE variable='RESTBasicAuth'",
1942
    }).then(value => {
1943
        if (value[0].value !== "1") {
1944
            throw new Error(
1945
                "Cypress tests tests require 'RESTBasicAuth'. Skipping suite."
1946
            );
1947
        }
1948
    });
1949
});
(-)a/t/db_dependent/Auth.t (-1 / +2 lines)
Lines 7-13 use CGI qw ( -utf8 ); Link Here
7
use Test::MockObject;
7
use Test::MockObject;
8
use Test::MockModule;
8
use Test::MockModule;
9
use List::MoreUtils qw/all any none/;
9
use List::MoreUtils qw/all any none/;
10
use Test::More tests => 23;
10
use Test::More tests => 24;
11
use Test::NoWarnings;
11
use Test::Warn;
12
use Test::Warn;
12
use t::lib::Mocks;
13
use t::lib::Mocks;
13
use t::lib::TestBuilder;
14
use t::lib::TestBuilder;
(-)a/t/db_dependent/Auth_with_shibboleth.t (-17 / +14 lines)
Lines 20-28 Link Here
20
use Modern::Perl;
20
use Modern::Perl;
21
use utf8;
21
use utf8;
22
22
23
use Test::More tests => 6;
23
use Test::More tests => 7;
24
use Test::MockModule;
24
use Test::MockModule;
25
use Test::Warn;
25
use Test::Warn;
26
use Test::NoWarnings;
26
use CGI        qw(-utf8 );
27
use CGI        qw(-utf8 );
27
use File::Temp qw(tempdir);
28
use File::Temp qw(tempdir);
28
29
Lines 67-83 $context->mock( 'interface', sub { return $interface; } ); Link Here
67
# Mock Letters: GetPreparedLetter, EnqueueLetter and SendQueuedMessages
68
# Mock Letters: GetPreparedLetter, EnqueueLetter and SendQueuedMessages
68
# We want to test the params
69
# We want to test the params
69
my $mocked_letters = Test::MockModule->new('C4::Letters');
70
my $mocked_letters = Test::MockModule->new('C4::Letters');
71
my $sub_called     = {};
70
$mocked_letters->mock(
72
$mocked_letters->mock(
71
    'GetPreparedLetter',
73
    'GetPreparedLetter',
72
    sub {
74
    sub {
73
        warn "GetPreparedLetter called";
75
        $sub_called->{GetPreparedLetter}++;
74
        return 1;
76
        return 1;
75
    }
77
    }
76
);
78
);
77
$mocked_letters->mock(
79
$mocked_letters->mock(
78
    'EnqueueLetter',
80
    'EnqueueLetter',
79
    sub {
81
    sub {
80
        warn "EnqueueLetter called";
82
        $sub_called->{EnqueueLetter}++;
81
83
82
        # return a 'message_id'
84
        # return a 'message_id'
83
        return 42;
85
        return 42;
Lines 87-93 $mocked_letters->mock( Link Here
87
    'SendQueuedMessages',
89
    'SendQueuedMessages',
88
    sub {
90
    sub {
89
        my $params = shift;
91
        my $params = shift;
90
        warn "SendQueuedMessages called with message_id: $params->{message_id}";
92
        $sub_called->{SendQueuedMessages}->{ $params->{message_id} }++;
91
        return 1;
93
        return 1;
92
    }
94
    }
93
);
95
);
Lines 172-178 subtest "get_login_shib tests" => sub { Link Here
172
174
173
subtest "checkpw_shib tests" => sub {
175
subtest "checkpw_shib tests" => sub {
174
176
175
    plan tests => 54;
177
    plan tests => 56;
176
178
177
    # Test borrower data
179
    # Test borrower data
178
    my $test_borrowers = [
180
    my $test_borrowers = [
Lines 251-268 subtest "checkpw_shib tests" => sub { Link Here
251
    $ENV{'emailpro'} = 'me@myemail.com';
253
    $ENV{'emailpro'} = 'me@myemail.com';
252
    $ENV{branchcode} = $library->branchcode;      # needed since T::D::C does no longer hides the FK constraint
254
    $ENV{branchcode} = $library->branchcode;      # needed since T::D::C does no longer hides the FK constraint
253
255
254
    warnings_are {
256
    ( $retval, $retcard, $retuserid, $retpatron ) = checkpw_shib($shib_login);
255
        ( $retval, $retcard, $retuserid, $retpatron ) = checkpw_shib($shib_login);
257
    is( $sub_called->{GetPreparedLetter},        1,              'GetPreparedLetter called' );
256
    }
258
    is( $sub_called->{EnqueueLetter},            1,              'EnqueueLetter called' );
257
    [
259
    is( $sub_called->{SendQueuedMessages}->{42}, 1,              'SendQueuedMessages called with message_id: 42' );
258
        'GetPreparedLetter called',
260
    is( $retval,                                 "1",            "user authenticated" );
259
        'EnqueueLetter called',
261
    is( $retuserid,                              "test4321",     "expected userid returned" );
260
        'SendQueuedMessages called with message_id: 42'
262
    is( ref($retpatron),                         'Koha::Patron', "expected Koha::Patron object returned" );
261
    ],
262
        "WELCOME notice Prepared, Enqueued and Send";
263
    is( $retval,         "1",            "user authenticated" );
264
    is( $retuserid,      "test4321",     "expected userid returned" );
265
    is( ref($retpatron), 'Koha::Patron', "expected Koha::Patron object returned" );
266
    $logger->debug_is( "koha borrower field to match: userid", "borrower match field debug info" )
263
    $logger->debug_is( "koha borrower field to match: userid", "borrower match field debug info" )
267
        ->debug_is( "shibboleth attribute to match: uid", "shib match attribute debug info" )->clear();
264
        ->debug_is( "shibboleth attribute to match: uid", "shib match attribute debug info" )->clear();
268
265
(-)a/t/db_dependent/AuthorisedValues.t (-2 / +2 lines)
Lines 194-201 subtest 'search_by_*_field + find_by_koha_field + get_description + authorised_v Link Here
194
            kohafield => 'items.restricted'
194
            kohafield => 'items.restricted'
195
        }
195
        }
196
    )->store;
196
    )->store;
197
    Koha::MarcSubfieldStructure->new( { tagfield => '003', frameworkcode => '', authorised_value => 'CONTROL_TEST', } )
197
    Koha::MarcSubfieldStructure->new(
198
        ->store;
198
        { tagfield => '003', tagsubfield => '@', frameworkcode => '', authorised_value => 'CONTROL_TEST', } )->store;
199
    Koha::AuthorisedValue->new( { category => 'TEST', authorised_value => 'location_1', lib => 'location_1' } )->store;
199
    Koha::AuthorisedValue->new( { category => 'TEST', authorised_value => 'location_1', lib => 'location_1' } )->store;
200
    Koha::AuthorisedValue->new( { category => 'TEST', authorised_value => 'location_2', lib => 'location_2' } )->store;
200
    Koha::AuthorisedValue->new( { category => 'TEST', authorised_value => 'location_2', lib => 'location_2' } )->store;
201
    Koha::AuthorisedValue->new( { category => 'TEST', authorised_value => 'location_3', lib => 'location_3' } )->store;
201
    Koha::AuthorisedValue->new( { category => 'TEST', authorised_value => 'location_3', lib => 'location_3' } )->store;
(-)a/t/db_dependent/Biblio.t (-10 / +8 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 25;
20
use Test::More tests => 26;
21
use Test::NoWarnings;
21
use Test::MockModule;
22
use Test::MockModule;
22
use Test::Warn;
23
use Test::Warn;
23
use List::MoreUtils qw( uniq );
24
use List::MoreUtils qw( uniq );
Lines 293-303 subtest "Test caching of authority types in LinkBibHeadingsToAuthorities" => sub Link Here
293
        }
294
        }
294
    );
295
    );
295
    my $authorities_type = Test::MockModule->new('Koha::Authority::Types');
296
    my $authorities_type = Test::MockModule->new('Koha::Authority::Types');
297
    my $found_auth_type  = {};
296
    $authorities_type->mock(
298
    $authorities_type->mock(
297
        'find',
299
        'find',
298
        sub {
300
        sub {
299
            my ( $self, $params ) = @_;
301
            my ( $self, $params ) = @_;
300
            warn "Finding auth type $params";
302
            $found_auth_type->{$params}++;
301
            return $authorities_type->original("find")->( $self, $params );
303
            return $authorities_type->original("find")->( $self, $params );
302
        }
304
        }
303
    );
305
    );
Lines 305-321 subtest "Test caching of authority types in LinkBibHeadingsToAuthorities" => sub Link Here
305
    my $field1      = MARC::Field->new( 655, ' ', ' ', 'a' => 'Magical realism' );
307
    my $field1      = MARC::Field->new( 655, ' ', ' ', 'a' => 'Magical realism' );
306
    my $field2      = MARC::Field->new( 655, ' ', ' ', 'a' => 'Magical falsism' );
308
    my $field2      = MARC::Field->new( 655, ' ', ' ', 'a' => 'Magical falsism' );
307
    $marc_record->append_fields( ( $field1, $field2 ) );
309
    $marc_record->append_fields( ( $field1, $field2 ) );
308
    my ( $num_changed, $results );
310
    my ( $num_changed, $results ) = LinkBibHeadingsToAuthorities( $linker, $marc_record, "", undef );
309
    warning_like { ( $num_changed, $results ) = LinkBibHeadingsToAuthorities( $linker, $marc_record, "", undef ) }
311
    is_deeply( $found_auth_type, { "GENRE/FORM" => 1 }, "Type fetched only once" );
310
    qr/Finding auth type GENRE\/FORM/,
311
        "Type fetched only once";
312
    my $gf_type = $cache->get_from_cache("LinkBibHeadingsToAuthorities:AuthorityType:GENRE/FORM");
312
    my $gf_type = $cache->get_from_cache("LinkBibHeadingsToAuthorities:AuthorityType:GENRE/FORM");
313
    ok( $gf_type, "GENRE/FORM type is found in cache" );
313
    ok( $gf_type, "GENRE/FORM type is found in cache" );
314
314
315
    warning_like { ( $num_changed, $results ) = LinkBibHeadingsToAuthorities( $linker, $marc_record, "", undef ) }
315
    ( $num_changed, $results ) = LinkBibHeadingsToAuthorities( $linker, $marc_record, "", undef );
316
    undef,
316
    is_deeply( $found_auth_type, { "GENRE/FORM" => 1 }, "Type not fetched a second time" );
317
        "Type not fetched a second time";
318
319
};
317
};
320
318
321
# Mocking variables
319
# Mocking variables
(-)a/t/db_dependent/Circulation/OfflineCirculation.t (-10 / +52 lines)
Lines 18-24 Link Here
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::NoWarnings;
20
use Test::NoWarnings;
21
use Test::More tests => 3;
21
use Test::More tests => 4;
22
use Test::MockModule;
22
use Test::MockModule;
23
use Test::Warn;
23
use Test::Warn;
24
24
Lines 85-102 subtest "Bug 34529: Offline circulation should be able to accept userid as well Link Here
85
        }
85
        }
86
    );
86
    );
87
87
88
    my ( $message, $checkout ) = ProcessOfflineIssue(
89
        {
90
            cardnumber => $borrower1->{cardnumber},
91
            barcode    => $item1->barcode
92
        }
93
    );
94
88
    is(
95
    is(
89
        ProcessOfflineIssue(
96
        $message, "Success.",
90
            {
91
                cardnumber => $borrower1->{cardnumber},
92
                barcode    => $item1->barcode
93
            }
94
        ),
95
        "Success.",
96
        "ProcessOfflineIssue succeeds with cardnumber"
97
        "ProcessOfflineIssue succeeds with cardnumber"
97
    );
98
    );
99
100
    ( $message, $checkout ) = ProcessOfflineIssue( { cardnumber => $borrower1->{userid}, barcode => $item2->barcode } );
98
    is(
101
    is(
99
        ProcessOfflineIssue( { cardnumber => $borrower1->{userid}, barcode => $item2->barcode } ),
102
        $message,
100
        "Success.",
103
        "Success.",
101
        "ProcessOfflineIssue succeeds with user id"
104
        "ProcessOfflineIssue succeeds with user id"
102
    );
105
    );
Lines 192-200 subtest "Bug 30114 - Koha offline circulation will always cancel the next hold w Link Here
192
195
193
    my $op = GetOfflineOperation( $offline_rs->next->id );
196
    my $op = GetOfflineOperation( $offline_rs->next->id );
194
197
195
    my $ret = ProcessOfflineOperation($op);
198
    my ($ret) = ProcessOfflineOperation($op);
196
199
197
    is( Koha::Holds->search( { biblionumber => $biblionumber } )->count, 2, "Still found two holds for the record" );
200
    is( Koha::Holds->search( { biblionumber => $biblionumber } )->count, 2, "Still found two holds for the record" );
198
};
201
};
199
202
203
subtest "Bug 32934: ProcessOfflineIssue returns checkout object for SIP no block due date" => sub {
204
    plan tests => 4;
205
206
    $branch     = $builder->build( { source => 'Branch' } )->{branchcode};
207
    $manager_id = $builder->build( { source => 'Borrower' } )->{borrowernumber};
208
209
    my $borrower = $builder->build(
210
        {
211
            source => 'Borrower',
212
            value  => { branchcode => $branch }
213
        }
214
    );
215
216
    my $biblio = $builder->build_sample_biblio;
217
    my $item   = $builder->build_sample_item(
218
        {
219
            biblionumber => $biblio->id,
220
            library      => $branch,
221
        }
222
    );
223
224
    my $due_date = dt_from_string->add( days => 7 )->ymd;
225
226
    # Test ProcessOfflineIssue returns both message and checkout object
227
    my ( $message, $checkout ) = ProcessOfflineIssue(
228
        {
229
            cardnumber => $borrower->{cardnumber},
230
            barcode    => $item->barcode,
231
            due_date   => $due_date,
232
            timestamp  => dt_from_string
233
        }
234
    );
235
236
    is( $message, "Success.", "ProcessOfflineIssue returns success message" );
237
    isa_ok( $checkout, 'Koha::Checkout', "ProcessOfflineIssue returns checkout object" );
238
    is( $checkout->borrowernumber,                  $borrower->{borrowernumber}, "Checkout has correct borrower" );
239
    is( dt_from_string( $checkout->date_due )->ymd, $due_date, "Checkout respects specified due_date" );
240
};
241
200
$schema->storage->txn_rollback;
242
$schema->storage->txn_rollback;
(-)a/t/db_dependent/Circulation_holdsqueue.t (-1 / +2 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 1;
20
use Test::More tests => 2;
21
use Test::NoWarnings;
21
use Test::MockModule;
22
use Test::MockModule;
22
23
23
use C4::Circulation qw( AddIssue AddReturn );
24
use C4::Circulation qw( AddIssue AddReturn );
(-)a/t/db_dependent/Holds.t (-2 / +3 lines)
Lines 7-13 use t::lib::TestBuilder; Link Here
7
7
8
use C4::Context;
8
use C4::Context;
9
9
10
use Test::More tests => 72;
10
use Test::More tests => 73;
11
use Test::NoWarnings;
11
use Test::Exception;
12
use Test::Exception;
12
13
13
use MARC::Record;
14
use MARC::Record;
Lines 1653-1659 subtest 'non priority holds' => sub { Link Here
1653
            itemnumber     => $item->itemnumber,
1654
            itemnumber     => $item->itemnumber,
1654
            branchcode     => $item->homebranch
1655
            branchcode     => $item->homebranch
1655
        }
1656
        }
1656
    )->store;
1657
    )->store->get_from_storage;
1657
1658
1658
    my $hid = AddReserve(
1659
    my $hid = AddReserve(
1659
        {
1660
        {
(-)a/t/db_dependent/HoldsQueue/TransportCostOptimizations.t (-16 / +27 lines)
Lines 9-15 Link Here
9
9
10
use Modern::Perl;
10
use Modern::Perl;
11
11
12
use Test::More tests => 117;
12
use Test::More tests => 120;
13
use Test::NoWarnings;
14
use Test::Warn;
13
use Data::Dumper;
15
use Data::Dumper;
14
16
15
use C4::Context;
17
use C4::Context;
Lines 214-236 sub test_allocation { Link Here
214
216
215
    $schema->txn_rollback;
217
    $schema->txn_rollback;
216
}
218
}
217
219
warning_like(
218
test_allocation(
220
    sub {
219
    "trivial case",
221
        test_allocation(
220
    [],
222
            "trivial case",
221
    [],
223
            [],
222
    [],
224
            [],
223
    [],
225
            [],
224
    0
226
            [],
227
            0
228
        );
229
    },
230
    qr{UseTransportCostMatrix set to yes, but matrix not populated}
225
);
231
);
226
232
227
test_allocation(
233
warning_like(
228
    "unit case",
234
    sub {
229
    [ [0] ],
235
        test_allocation(
230
    [1],
236
            "unit case",
231
    [1],
237
            [ [0] ],
232
    [ [ 0, 0 ] ],
238
            [1],
233
    0
239
            [1],
240
            [ [ 0, 0 ] ],
241
            0
242
        );
243
    },
244
    qr{UseTransportCostMatrix set to yes, but matrix not populated}
234
);
245
);
235
246
236
test_allocation(
247
test_allocation(
(-)a/t/db_dependent/Items/AutomaticItemModificationByAge.t (-1 / +105 lines)
Lines 2-8 Link Here
2
2
3
use Modern::Perl;
3
use Modern::Perl;
4
use Test::NoWarnings;
4
use Test::NoWarnings;
5
use Test::More tests => 21;
5
use Test::More tests => 23;
6
use MARC::Record;
6
use MARC::Record;
7
use MARC::Field;
7
use MARC::Field;
8
use DateTime;
8
use DateTime;
Lines 391-396 C4::Items::ToggleNewStatus( { rules => \@rules } ); Link Here
391
$modified_item = Koha::Items->find($itemnumber);
391
$modified_item = Koha::Items->find($itemnumber);
392
is( $modified_item->new_status, 'new_updated_value_biblio', q|ToggleNewStatus: conditions on biblio| );
392
is( $modified_item->new_status, 'new_updated_value_biblio', q|ToggleNewStatus: conditions on biblio| );
393
393
394
# Test for error handling in ToggleNewStatus with an on-loan item
395
subtest "ToggleNewStatus onloan error handling" => sub {
396
    plan tests => 3;
397
398
    # Create a new test item
399
    my $test_item2 = $builder->build_object( { class => 'Koha::Items' } );
400
    my $patron2    = $builder->build_object( { class => 'Koha::Patrons' } );
401
402
    # Check out the item to create the condition for an error
403
    $test_item2->checkout( $patron2->borrowernumber );
404
    ok( $test_item2->onloan, "Item is checked out" );
405
406
    # Create rules to try to modify the withdrawn status
407
    my @withdrawal_rules = (
408
        {
409
            conditions => [
410
                {
411
                    field => "items.itemnumber",
412
                    value => $test_item2->itemnumber
413
                }
414
            ],
415
            substitutions => [
416
                {
417
                    field => "items.withdrawn",
418
                    value => 1
419
                }
420
            ]
421
        }
422
    );
423
424
    # Run ToggleNewStatus with the rules and catch errors in the report
425
    my $error_report = C4::Items::ToggleNewStatus(
426
        {
427
            rules       => \@withdrawal_rules,
428
            report_only => 0
429
        }
430
    );
431
432
    # Verify report structure
433
    ok( exists $error_report->{ $test_item2->itemnumber }, "Error item appears in report" );
434
435
    is( $test_item2->withdrawn, 0, 'Item should not be withdrawn' );
436
437
};
438
439
subtest "ToggleNewStatus in-transit error handling" => sub {
440
    plan tests => 3;
441
442
    # Create a new test item
443
    my $test_item3 = $builder->build_object( { class => 'Koha::Items' } );
444
445
    # Create a transfer to put item in-transit
446
    my $from_library = $builder->build_object( { class => 'Koha::Libraries' } );
447
    my $to_library   = $builder->build_object( { class => 'Koha::Libraries' } );
448
449
    my $transfer = Koha::Item::Transfer->new(
450
        {
451
            itemnumber => $test_item3->itemnumber,
452
            frombranch => $from_library->branchcode,
453
            tobranch   => $to_library->branchcode,
454
            datesent   => dt_from_string(),
455
        }
456
    )->store;
457
458
    # Mark transfer as in transit
459
    $transfer->datearrived(undef);
460
    $transfer->store;
461
462
    # Verify item is in transit
463
    my $item_transfer = $test_item3->get_transfer;
464
    ok( $item_transfer && !$item_transfer->datearrived, "Item is in transit" );
465
466
    # Create rules to try to modify the withdrawn status
467
    my @withdrawal_rules = (
468
        {
469
            conditions => [
470
                {
471
                    field => "items.itemnumber",
472
                    value => $test_item3->itemnumber
473
                }
474
            ],
475
            substitutions => [
476
                {
477
                    field => "items.withdrawn",
478
                    value => 1
479
                }
480
            ]
481
        }
482
    );
483
484
    # Run ToggleNewStatus with the rules and catch errors in the report
485
    my $error_report = C4::Items::ToggleNewStatus(
486
        {
487
            rules       => \@withdrawal_rules,
488
            report_only => 0
489
        }
490
    );
491
492
    # Verify report structure
493
    ok( exists $error_report->{ $test_item3->itemnumber }, "Error item appears in report" );
494
495
    is( $test_item3->withdrawn, 0, 'Item should not be withdrawn' );
496
};
497
394
# Run twice
498
# Run twice
395
t::lib::Mocks::mock_preference( 'CataloguingLog', 1 );
499
t::lib::Mocks::mock_preference( 'CataloguingLog', 1 );
396
my $actions_nb = $schema->resultset('ActionLog')->count();
500
my $actions_nb = $schema->resultset('ActionLog')->count();
(-)a/t/db_dependent/Koha.t (-1 / +5 lines)
Lines 11-16 use Test::Warn; Link Here
11
use Test::Deep;
11
use Test::Deep;
12
12
13
use t::lib::TestBuilder;
13
use t::lib::TestBuilder;
14
use t::lib::Mocks;
14
15
15
use C4::Context;
16
use C4::Context;
16
use Koha::Database;
17
use Koha::Database;
Lines 33-39 my $dbh = C4::Context->dbh; Link Here
33
our $itype_1 = $builder->build( { source => 'Itemtype' } );
34
our $itype_1 = $builder->build( { source => 'Itemtype' } );
34
35
35
subtest 'Authorized Values Tests' => sub {
36
subtest 'Authorized Values Tests' => sub {
36
    plan tests => 4;
37
    plan tests => 5;
37
38
38
    my $data = {
39
    my $data = {
39
        category         => 'CATEGORY',
40
        category         => 'CATEGORY',
Lines 166-171 subtest 'Authorized Values Tests' => sub { Link Here
166
167
167
    warning_is { GetAuthorisedValues() } [], 'No warning when no parameter passed to GetAuthorisedValues';
168
    warning_is { GetAuthorisedValues() } [], 'No warning when no parameter passed to GetAuthorisedValues';
168
169
170
    C4::Context->set_userenv();
171
    warning_is { GetAuthorisedValues("BUG10656") } [], 'No warning when userenv is anonymous';
172
169
};
173
};
170
174
171
subtest 'isbn tests' => sub {
175
subtest 'isbn tests' => sub {
(-)a/t/db_dependent/Koha/Biblio.t (-48 / +161 lines)
Lines 1016-1061 subtest 'get_volumes_query' => sub { Link Here
1016
};
1016
};
1017
1017
1018
subtest 'generate_marc_host_field' => sub {
1018
subtest 'generate_marc_host_field' => sub {
1019
    plan tests => 24;
1019
    plan tests => 36;
1020
1020
1021
    $schema->storage->txn_begin;
1021
    $schema->storage->txn_begin;
1022
1022
1023
    # Set up MARC21 tests
1023
    t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' );
1024
    t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' );
1024
1025
1025
    my $biblio = $builder->build_sample_biblio();
1026
    # 1. Complete MARC21 record test
1026
    my $record = $biblio->metadata->record;
1027
    my $record = MARC::Record->new();
1028
    $record->leader('00000nam a22000007a 4500');
1027
    $record->append_fields(
1029
    $record->append_fields(
1028
        MARC::Field->new( '001', '1234' ),
1030
        MARC::Field->new( '001', '12345' ),
1029
        MARC::Field->new( '003', 'FIRST' ),
1031
        MARC::Field->new( '003', 'NB' ),
1030
        MARC::Field->new( '240', '', '', a => 'A uniform title' ),
1032
        MARC::Field->new( '020', '',  '',  'a' => '978-3-16-148410-0' ),
1031
        MARC::Field->new( '260', '', '', a => 'Publication 260' ),
1033
        MARC::Field->new( '022', '',  '',  'a' => '1234-5678' ),
1032
        MARC::Field->new( '250', '', '', a => 'Edition a', b => 'Edition b' ),
1034
        MARC::Field->new( '100', '1', '',  'a' => 'Smith, John',  'e' => 'author',   '9' => 'xyz', '4' => 'aut' ),
1033
        MARC::Field->new( '022', '', '', a => '0317-8471' ),
1035
        MARC::Field->new( '245', '1', '0', 'a' => 'The Title',    'b' => 'Subtitle', 'c' => 'John Smith' ),
1036
        MARC::Field->new( '250', '',  '',  'a' => '2nd edition',  'b' => 'revised' ),
1037
        MARC::Field->new( '260', '',  '',  'a' => 'New York',     'b' => 'Publisher', 'c' => '2023' ),
1038
        MARC::Field->new( '830', '',  '',  'a' => 'Series Title', 'v' => 'vol. 2',    'x' => '2345-6789' )
1034
    );
1039
    );
1035
    C4::Biblio::ModBiblio( $record, $biblio->biblionumber );
1040
    my ($biblio_id) = AddBiblio( $record, qw{} );
1036
    $biblio = Koha::Biblios->find( $biblio->biblionumber );
1041
    my $biblio = Koha::Biblios->find($biblio_id);
1037
1042
1038
    t::lib::Mocks::mock_preference( 'UseControlNumber', '0' );
1043
    # Test MARC21 with UseControlNumber off
1044
    t::lib::Mocks::mock_preference( 'UseControlNumber', 0 );
1039
    my $link = $biblio->generate_marc_host_field();
1045
    my $link = $biblio->generate_marc_host_field();
1040
1046
1041
    is( ref($link),           'MARC::Field',         "->generate_marc_host_field returns a MARC::Field object" );
1047
    # Test standard MARC21 field
1042
    is( $link->tag,           '773',                 "MARC::Field->tag returns '773' when marcflavour is 'MARC21" );
1048
    is( ref($link),          'MARC::Field', 'Returns a MARC::Field object' );
1043
    is( $link->subfield('a'), 'Some boring author',  'MARC::Field->subfield(a) returns content from 100ab' );
1049
    is( $link->tag(),        '773',         'Field tag is 773 for MARC21' );
1044
    is( $link->subfield('b'), 'Edition a Edition b', 'MARC::Field->subfield(b) returns content from 250ab' );
1050
    is( $link->indicator(1), '0',           'First indicator is 0' );
1045
    is( $link->subfield('d'), 'Publication 260',     'MARC::Field->subfield(c) returns content from 260abc' );
1051
    is( $link->indicator(2), ' ',           'Second indicator is blank' );
1046
    is( $link->subfield('s'), 'A uniform title',     'MARC::Field->subfield(s) returns content from 240a' );
1047
    is( $link->subfield('t'), 'Some boring read',    'MARC::Field->subfield(s) returns content from 245ab' );
1048
    is( $link->subfield('x'), '0317-8471',           'MARC::Field->subfield(s) returns content from 022a' );
1049
    is( $link->subfield('z'), undef,                 'MARC::Field->subfield(s) returns undef when 020a is empty' );
1050
    is( $link->subfield('w'), undef, 'MARC::Field->subfield(w) returns undef when "UseControlNumber" is disabled' );
1051
1052
1053
    # Check all subfields
1054
    is( $link->subfield('7'), 'p1am',        'Subfield 7 correctly formed' );
1055
    is( $link->subfield('a'), 'Smith, John', 'Subfield a contains author from 100a' );
1056
    is(
1057
        $link->subfield('t'), 'The Title Subtitle',
1058
        'Subfield t contains title without trailing punctuation from 245ab'
1059
    );
1060
    is( $link->subfield('b'), '2nd edition revised',     'Subfield b contains edition info from 250ab' );
1061
    is( $link->subfield('d'), 'New York Publisher 2023', 'Subfield d contains publication info from 260abc' );
1062
    is( $link->subfield('k'), 'Series Title, ISSN 2345-6789 ; vol. 2', 'Subfield k contains series info from 830' );
1063
    is( $link->subfield('x'), '1234-5678',                             'Subfield x contains ISSN from 022a' );
1064
    is( $link->subfield('z'), '978-3-16-148410-0',                     'Subfield z contains ISBN from 020a' );
1065
    is( $link->subfield('w'), undef, 'Subfield w is undefined when UseControlNumber is disabled' );
1066
1067
    # Test with UseControlNumber enabled
1052
    t::lib::Mocks::mock_preference( 'UseControlNumber', '1' );
1068
    t::lib::Mocks::mock_preference( 'UseControlNumber', '1' );
1053
    $link = $biblio->generate_marc_host_field();
1069
    $link = $biblio->generate_marc_host_field();
1054
    is(
1070
    is(
1055
        $link->subfield('w'), '(FIRST)1234',
1071
        $link->subfield('w'), '(NB)12345',
1056
        'MARC::Field->subfield(w) returns content from 003 and 001 when "UseControlNumber" is enabled'
1072
        'Subfield w contains control number with source when UseControlNumber is enabled'
1057
    );
1073
    );
1058
1074
1075
    # 245 punctuation handling tests
1076
    # Trailing slash
1077
    $record->field('245')->update( a => 'A title /', b => '', c => '', 'ind2' => '0' );
1078
    ($biblio_id) = AddBiblio( $record, qw{} );
1079
    $biblio = Koha::Biblios->find($biblio_id);
1080
    $link   = $biblio->generate_marc_host_field();
1081
    is( $link->subfield('t'), 'A title', "Trailing slash is removed from 245a" );
1082
1083
    # Trailing period
1084
    $record->field('245')->update( a => 'Another title.', 'ind2' => '0' );
1085
    ($biblio_id) = AddBiblio( $record, qw{} );
1086
    $biblio = Koha::Biblios->find($biblio_id);
1087
    $link   = $biblio->generate_marc_host_field();
1088
    is( $link->subfield('t'), 'Another title', "Trailing period is removed from 245a" );
1089
1090
    # Offset from indicator 2 = 4
1091
    $record->field('245')->update( a => 'The offset title', 'ind2' => '4' );
1092
    ($biblio_id) = AddBiblio( $record, qw{} );
1093
    $biblio = Koha::Biblios->find($biblio_id);
1094
    $link   = $biblio->generate_marc_host_field();
1095
    is( $link->subfield('t'), 'Offset title', "Title offset applied from indicator 2" );
1096
1097
    # Capitalization after offset
1098
    $record->field('245')->update( a => 'the capital test', 'ind2' => '0' );
1099
    ($biblio_id) = AddBiblio( $record, qw{} );
1100
    $biblio = Koha::Biblios->find($biblio_id);
1101
    $link   = $biblio->generate_marc_host_field();
1102
    is( $link->subfield('t'), 'The capital test', "Title is capitalized after indicator offset" );
1103
1104
    # 240 uniform title tests
1105
    $record->append_fields( MARC::Field->new( '240', '1', '0', 'a' => 'Bible. English', 'l' => 'English' ) );
1106
    ($biblio_id) = AddBiblio( $record, qw{} );
1107
    $biblio = Koha::Biblios->find($biblio_id);
1108
    $link   = $biblio->generate_marc_host_field();
1109
    is( $link->subfield('s'), 'Bible. English', "Subfield s contains uniform title from 240a" );
1110
1111
    # 260/264 handling tests
1059
    $record->append_fields(
1112
    $record->append_fields(
1060
        MARC::Field->new( '264', '', '', a => 'Publication 264' ),
1113
        MARC::Field->new( '264', '', '', a => 'Publication 264' ),
1061
    );
1114
    );
Lines 1080-1115 subtest 'generate_marc_host_field' => sub { Link Here
1080
        'MARC::Field->subfield(d) returns content from 264 with indicator 1 = 3 in preference to 264 without'
1133
        'MARC::Field->subfield(d) returns content from 264 with indicator 1 = 3 in preference to 264 without'
1081
    );
1134
    );
1082
1135
1083
    # UNIMARC tests
1136
    # 2. Test MARC21 with corporate author (110)
1084
    t::lib::Mocks::mock_preference( 'marcflavour', 'UNIMARC' );
1137
    my $record_corporate = MARC::Record->new();
1138
    $record_corporate->leader('00000nam a22000007a 4500');
1139
    $record_corporate->append_fields(
1140
        MARC::Field->new( '110', '2', '',  'a' => 'Corporate Author', 'e' => 'sponsor', '9' => 'xyz', '4' => 'spn' ),
1141
        MARC::Field->new( '245', '1', '0', 'a' => 'The Title' )
1142
    );
1143
    ($biblio_id) = AddBiblio( $record_corporate, qw{} );
1144
    $biblio = Koha::Biblios->find($biblio_id);
1085
1145
1086
    $biblio = $builder->build_sample_biblio();
1146
    $link = $biblio->generate_marc_host_field();
1087
    $record = $biblio->metadata->record;
1147
    is( $link->subfield('7'), 'c2am',             'Subfield 7 correctly formed for corporate author' );
1088
    $record->append_fields(
1148
    is( $link->subfield('a'), 'Corporate Author', 'Subfield a contains corporate author' );
1089
        MARC::Field->new( '001', '1234' ),
1149
1090
        MARC::Field->new( '700', '', '', a => 'A nice author' ),
1150
    # 3. Test MARC21 with meeting name (111)
1091
        MARC::Field->new( '210', '', '', a => 'A publication', d => 'A date' ),
1151
    my $record_meeting = MARC::Record->new();
1092
        MARC::Field->new( '205', '', '', a => "Fun things" ),
1152
    $record_meeting->leader('00000nam a22000007a 4500');
1093
        MARC::Field->new( '856', '', '', u => 'http://myurl.com/' ),
1153
    $record_meeting->append_fields(
1094
        MARC::Field->new( '011', '', '', a => '0317-8471' ),
1154
        MARC::Field->new( '111', '2', '',  'a' => 'Conference Name', 'j' => 'relator', '9' => 'xyz', '4' => 'spn' ),
1095
        MARC::Field->new( '545', '', '', a => 'Invisible on OPAC' ),
1155
        MARC::Field->new( '245', '1', '0', 'a' => 'The Title' )
1096
    );
1156
    );
1097
    C4::Biblio::ModBiblio( $record, $biblio->biblionumber );
1157
    ($biblio_id) = AddBiblio( $record_meeting, qw{} );
1158
    $biblio = Koha::Biblios->find($biblio_id);
1159
1160
    $link = $biblio->generate_marc_host_field();
1161
    is( $link->subfield('7'), 'm2am', 'Subfield 7 correctly formed for meeting name' );
1162
1163
    # 4. Test MARC21 with minimal record
1164
    my $record_minimal = MARC::Record->new();
1165
    $record_minimal->leader('00000nam a22000007a 4500');
1166
    $record_minimal->append_fields( MARC::Field->new( '245', '0', '0', 'a' => 'Title Only' ) );
1167
    ($biblio_id) = AddBiblio( $record_minimal, qw{} );
1168
    $biblio = Koha::Biblios->find($biblio_id);
1169
1170
    $link = $biblio->generate_marc_host_field();
1171
    is( $link->subfield('7'), 'nnam', 'Subfield 7 correctly formed with no main entry' );
1172
1173
    # 5. Test UNIMARC
1174
    t::lib::Mocks::mock_preference( 'marcflavour', 'UNIMARC' );
1175
    $biblio = $builder->build_sample_biblio();
1176
    my $record_unimarc = MARC::Record->new();
1177
    $record_unimarc->append_fields(
1178
        MARC::Field->new( '001', '54321' ),
1179
        MARC::Field->new( '010', '', '', 'a' => '978-0-12-345678-9' ),
1180
        MARC::Field->new( '011', '', '', 'a' => '2345-6789' ),
1181
        MARC::Field->new( '200', '', '', 'a' => 'UNIMARC Title' ),
1182
        MARC::Field->new( '205', '', '', 'a' => 'Third edition' ),
1183
        MARC::Field->new( '210', '', '', 'a' => 'Paris', 'd' => '2023' ),
1184
        MARC::Field->new( '700', '', '', 'a' => 'Doe',   'b' => 'Jane' ),
1185
        MARC::Field->new( '856', '', '', 'u' => 'http://example.com' )
1186
    );
1187
    ($biblio_id) = AddBiblio( $record_unimarc, qw{} );
1188
    $biblio = Koha::Biblios->find($biblio_id);
1189
1190
    $link = $biblio->generate_marc_host_field();
1191
1192
    is( ref($link),          'MARC::Field', 'Returns a MARC::Field object for UNIMARC' );
1193
    is( $link->tag(),        '461',         'Field tag is 461 for UNIMARC' );
1194
    is( $link->indicator(1), '0',           'First indicator is 0 for UNIMARC' );
1195
    is( $link->indicator(2), ' ',           'Second indicator is blank for UNIMARC' );
1196
1197
    # Check UNIMARC subfields
1198
    is( $link->subfield('a'), 'Doe Jane',      'Subfield a contains author for UNIMARC' );
1199
    is( $link->subfield('t'), 'UNIMARC Title', 'Subfield t contains title for UNIMARC' );
1200
    is( $link->subfield('c'), 'Paris',         'Subfield c contains place of publication for UNIMARC' );
1201
    is( $link->subfield('d'), '2023',          'Subfield d contains date of publication for UNIMARC' );
1202
    is( $link->subfield('0'), '54321',         'Subfield 0 contains control number for UNIMARC' );
1203
1204
    # 6. Test UNIMARC with different author types
1205
    my $record_unimarc_corporate = MARC::Record->new();
1206
    $record_unimarc_corporate->append_fields(
1207
        MARC::Field->new( '710', '', '', 'a' => 'Corporate', 'b' => 'Department' ),
1208
        MARC::Field->new( '200', '', '', 'a' => 'Title' )
1209
    );
1210
    C4::Biblio::ModBiblio( $record_unimarc_corporate, $biblio->biblionumber );
1098
    $biblio = Koha::Biblios->find( $biblio->biblionumber );
1211
    $biblio = Koha::Biblios->find( $biblio->biblionumber );
1099
1212
1100
    $link = $biblio->generate_marc_host_field();
1213
    $link = $biblio->generate_marc_host_field();
1214
    is( $link->subfield('a'), 'Corporate Department', 'Subfield a contains corporate author for UNIMARC' );
1215
1216
    my $record_unimarc_family = MARC::Record->new();
1217
    $record_unimarc_family->append_fields(
1218
        MARC::Field->new( '720', '', '', 'a' => 'Family', 'b' => 'Name' ),
1219
        MARC::Field->new( '200', '', '', 'a' => 'Title' )
1220
    );
1221
    C4::Biblio::ModBiblio( $record_unimarc_family, $biblio->biblionumber );
1222
    $biblio = Koha::Biblios->find( $biblio->biblionumber );
1101
1223
1102
    is( ref($link),           'MARC::Field',       "->generate_marc_host_field returns a MARC::Field object" );
1224
    $link = $biblio->generate_marc_host_field();
1103
    is( $link->tag,           '461',               "MARC::Field->tag returns '461' when marcflavour is 'UNIMARC" );
1225
    is( $link->subfield('a'), 'Family Name', 'Subfield a contains family name for UNIMARC' );
1104
    is( $link->subfield('a'), 'A nice author',     'MARC::Field->subfield(a) returns content from 700ab' );
1105
    is( $link->subfield('c'), 'A publication',     'MARC::Field->subfield(b) returns content from 210a' );
1106
    is( $link->subfield('d'), 'A date',            'MARC::Field->subfield(c) returns content from 210d' );
1107
    is( $link->subfield('e'), 'Fun things',        'MARC::Field->subfield(s) returns content from 205' );
1108
    is( $link->subfield('t'), 'Some boring read',  'MARC::Field->subfield(s) returns content from 200a' );
1109
    is( $link->subfield('u'), 'http://myurl.com/', 'MARC::Field->subfield(s) returns content from 856u' );
1110
    is( $link->subfield('x'), '0317-8471',         'MARC::Field->subfield(s) returns content from 011a' );
1111
    is( $link->subfield('y'), undef,               'MARC::Field->subfield(w) returns undef if 010a is empty' );
1112
    is( $link->subfield('0'), '1234',              'MARC::Field->subfield(0) returns content from 001' );
1113
1226
1114
    $schema->storage->txn_rollback;
1227
    $schema->storage->txn_rollback;
1115
    t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' );
1228
    t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' );
(-)a/t/db_dependent/Koha/Booking.t (-6 / +23 lines)
Lines 20-26 Link Here
20
use Modern::Perl;
20
use Modern::Perl;
21
use utf8;
21
use utf8;
22
22
23
use Test::More tests => 2;
23
use Test::More tests => 3;
24
use Test::NoWarnings;
25
26
use Test::Warn;
24
27
25
use Test::Exception;
28
use Test::Exception;
26
29
Lines 369-375 subtest 'store() tests' => sub { Link Here
369
    };
372
    };
370
373
371
    subtest 'confirmation notice trigger' => sub {
374
    subtest 'confirmation notice trigger' => sub {
372
        plan tests => 2;
375
        plan tests => 4;
373
376
374
        # FIXME: This is a bandaid solution to prevent test failures when running
377
        # FIXME: This is a bandaid solution to prevent test failures when running
375
        # the Koha_Main_My8 job because notices are not added at upgrade time.
378
        # the Koha_Main_My8 job because notices are not added at upgrade time.
Lines 412-418 subtest 'store() tests' => sub { Link Here
412
        )->count;
415
        )->count;
413
416
414
        # Reuse previous booking to produce a clash
417
        # Reuse previous booking to produce a clash
415
        eval { $booking = Koha::Booking->new( $booking->unblessed )->store };
418
        warning_like(
419
            sub {
420
                throws_ok {
421
                    Koha::Booking->new( $booking->unblessed )->store
422
                }
423
                'Koha::Exceptions::Object::DuplicateID',
424
                    'Exception is thrown correctly';
425
            },
426
            qr{Duplicate entry '(.*?)' for key '(.*\.?)PRIMARY'}
427
        );
416
428
417
        my $post_notices_count = Koha::Notice::Messages->search(
429
        my $post_notices_count = Koha::Notice::Messages->search(
418
            {
430
            {
Lines 612-623 subtest 'store() tests' => sub { Link Here
612
    };
624
    };
613
625
614
    subtest 'status change exception' => sub {
626
    subtest 'status change exception' => sub {
615
        plan tests => 2;
627
        plan tests => 3;
616
628
617
        $booking->discard_changes;
629
        $booking->discard_changes;
618
        my $status = $booking->status;
630
        my $status = $booking->status;
619
        throws_ok { $booking->update( { status => 'blah' } ) } 'Koha::Exceptions::Object::BadValue',
631
        warning_like(
620
            'Throws exception when passed booking status would fail enum constraint';
632
            sub {
633
                throws_ok { $booking->update( { status => 'blah' } ) } 'Koha::Exceptions::Object::BadValue',
634
                    'Throws exception when passed booking status would fail enum constraint';
635
            },
636
            qr{Data truncated for column 'status'}
637
        );
621
638
622
        # Status unchanged
639
        # Status unchanged
623
        $booking->discard_changes;
640
        $booking->discard_changes;
(-)a/t/db_dependent/Koha/Hold.t (-5 / +44 lines)
Lines 346-370 subtest 'fill() tests' => sub { Link Here
346
346
347
    subtest 'holds_queue update tests' => sub {
347
    subtest 'holds_queue update tests' => sub {
348
348
349
        plan tests => 1;
349
        plan tests => 2;
350
350
351
        my $biblio = $builder->build_sample_biblio;
351
        my $biblio = $builder->build_sample_biblio;
352
352
353
        my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
353
        # The check of the pref is in the Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue
354
        # so we mock the base enqueue method here to see if it is called
355
        my $mock = Test::MockModule->new('Koha::BackgroundJob');
354
        $mock->mock(
356
        $mock->mock(
355
            'enqueue',
357
            'enqueue',
356
            sub {
358
            sub {
357
                my ( $self, $args ) = @_;
359
                my ( $self, $args ) = @_;
358
                is_deeply(
360
                is_deeply(
359
                    $args->{biblio_ids},
361
                    $args->{job_args}->{biblio_ids},
360
                    [ $biblio->id ],
362
                    [ $biblio->id ],
361
                    '->fill triggers a holds queue update for the related biblio'
363
                    'when pref enabled the previous action triggers a holds queue update for the related biblio'
362
                );
364
                );
363
            }
365
            }
364
        );
366
        );
365
367
366
        t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
368
        t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
367
369
370
        # Filling a hold when pref enabled should trigger a test
368
        $builder->build_object(
371
        $builder->build_object(
369
            {
372
            {
370
                class => 'Koha::Holds',
373
                class => 'Koha::Holds',
Lines 376-382 subtest 'fill() tests' => sub { Link Here
376
379
377
        t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
380
        t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
378
381
379
        # this call shouldn't add a new test
382
        # Filling a hold when pref disabled should not trigger a test
380
        $builder->build_object(
383
        $builder->build_object(
381
            {
384
            {
382
                class => 'Koha::Holds',
385
                class => 'Koha::Holds',
Lines 385-390 subtest 'fill() tests' => sub { Link Here
385
                }
388
                }
386
            }
389
            }
387
        )->fill;
390
        )->fill;
391
392
        my $library_1 = $builder->build_object(
393
            {
394
                class => 'Koha::Libraries',
395
            }
396
        )->store;
397
        my $library_2 = $builder->build_object(
398
            {
399
                class => 'Koha::Libraries',
400
            }
401
        )->store;
402
403
        my $hold = $builder->build_object(
404
            {
405
                class => 'Koha::Holds',
406
                value => {
407
                    biblionumber => $biblio->id,
408
                    branchcode   => $library_1->branchcode,
409
                }
410
            }
411
        )->store;
412
413
        # Pref is off, no test triggered
414
        # Updating a hold location when pref disabled should not trigger a test
415
        $hold->branchcode( $library_2->branchcode )->store;
416
417
        t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
418
419
        # Updating a hold location when pref enabled should trigger a test
420
421
        # Pref is on, test triggered
422
        $hold->branchcode( $library_1->branchcode )->store;
423
424
        # Update with no change to pickup location should not trigger a test
425
        $hold->branchcode( $library_1->branchcode )->store;
426
388
    };
427
    };
389
428
390
    $schema->storage->txn_rollback;
429
    $schema->storage->txn_rollback;
(-)a/t/db_dependent/Koha/Installer.t (-1 / +2 lines)
Lines 18-24 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 Test::More tests => 4;
21
use Test::More tests => 5;
22
use Test::NoWarnings;
22
23
23
use Test::MockModule;
24
use Test::MockModule;
24
use File::Temp qw/tempdir/;
25
use File::Temp qw/tempdir/;
(-)a/t/db_dependent/Koha/Item.t (-2 / +52 lines)
Lines 21-27 use Modern::Perl; Link Here
21
use utf8;
21
use utf8;
22
22
23
use Test::NoWarnings;
23
use Test::NoWarnings;
24
use Test::More tests => 40;
24
use Test::More tests => 41;
25
use Test::Exception;
25
use Test::Exception;
26
use Test::MockModule;
26
use Test::MockModule;
27
use Test::Warn;
27
use Test::Warn;
Lines 188-193 subtest '_status() tests' => sub { Link Here
188
    }
188
    }
189
189
190
    t::lib::Mocks::mock_preference( 'UseRecalls', 0 );
190
    t::lib::Mocks::mock_preference( 'UseRecalls', 0 );
191
    $schema->storage->txn_rollback;
192
};
193
194
subtest 'store PreventWithdrawingItemsStatus' => sub {
195
    plan tests => 2;
196
    $schema->storage->txn_begin;
197
198
    t::lib::Mocks::mock_preference( 'PreventWithdrawingItemsStatus', 'intransit,checkedout' );
199
    my $library_1 = $builder->build( { source => 'Branch' } );
200
    my $library_2 = $builder->build( { source => 'Branch' } );
201
202
    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
203
    t::lib::Mocks::mock_userenv( { branchcode => $patron->branchcode } );
204
205
    my $item = $builder->build_sample_item(
206
        {
207
            withdrawn => 0,
208
        }
209
    );
210
211
    #Check item out
212
    C4::Circulation::AddIssue( $patron, $item->barcode );
213
214
    throws_ok { $item->withdrawn('1')->store }
215
    'Koha::Exceptions::Item::Transfer::OnLoan',
216
        'Exception thrown when trying to withdraw checked-out item';
217
218
    my $item_2 = $builder->build_sample_item(
219
        {
220
            withdrawn => 0,
221
        }
222
    );
223
224
    #set in_transit
225
    my $transfer_1 = $builder->build_object(
226
        {
227
            class => 'Koha::Item::Transfers',
228
            value => {
229
                itemnumber => $item_2->itemnumber,
230
                frombranch => $library_1->{branchcode},
231
                tobranch   => $library_2->{branchcode},
232
                datesent   => '1999-12-31',
233
            }
234
        }
235
    );
236
237
    throws_ok { $item_2->withdrawn('1')->store }
238
    'Koha::Exceptions::Item::Transfer::InTransit',
239
        'Exception thrown when trying to withdraw item in transit';
240
241
    t::lib::Mocks::mock_preference( 'PreventWithdrawingItemsStatus', '' );
191
242
192
    $schema->storage->txn_rollback;
243
    $schema->storage->txn_rollback;
193
};
244
};
Lines 197-203 subtest 'z3950_status' => sub { Link Here
197
248
198
    $schema->storage->txn_begin;
249
    $schema->storage->txn_begin;
199
    t::lib::Mocks::mock_preference( 'z3950Status', '' );
250
    t::lib::Mocks::mock_preference( 'z3950Status', '' );
200
201
    my $itemtype = $builder->build_object( { class => "Koha::ItemTypes" } );
251
    my $itemtype = $builder->build_object( { class => "Koha::ItemTypes" } );
202
    my $item     = $builder->build_sample_item(
252
    my $item     = $builder->build_sample_item(
203
        {
253
        {
(-)a/t/db_dependent/Koha/Items/BatchUpdate.t (-6 / +61 lines)
Lines 284-290 subtest 'mark_items_returned' => sub { Link Here
284
};
284
};
285
285
286
subtest 'report' => sub {
286
subtest 'report' => sub {
287
    plan tests => 5;
287
    plan tests => 7;
288
288
289
    my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
289
    my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
290
    my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
290
    my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
Lines 297-303 subtest 'report' => sub { Link Here
297
        $report,
297
        $report,
298
        {
298
        {
299
            modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ],
299
            modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ],
300
            modified_fields      => 2
300
            modified_fields      => 2,
301
            errors               => []
301
        }
302
        }
302
    );
303
    );
303
304
Lines 308-314 subtest 'report' => sub { Link Here
308
        $report,
309
        $report,
309
        {
310
        {
310
            modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ],
311
            modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ],
311
            modified_fields      => 2
312
            modified_fields      => 2,
313
            errors               => []
312
        }
314
        }
313
    );
315
    );
314
316
Lines 320-326 subtest 'report' => sub { Link Here
320
        $report,
322
        $report,
321
        {
323
        {
322
            modified_itemnumbers => [ $item_1->itemnumber ],
324
            modified_itemnumbers => [ $item_1->itemnumber ],
323
            modified_fields      => 1
325
            modified_fields      => 1,
326
            errors               => []
324
        }
327
        }
325
    );
328
    );
326
329
Lines 331-337 subtest 'report' => sub { Link Here
331
        $report,
334
        $report,
332
        {
335
        {
333
            modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ],
336
            modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ],
334
            modified_fields      => 4
337
            modified_fields      => 4,
338
            errors               => []
335
        }
339
        }
336
    );
340
    );
337
341
Lines 349-358 subtest 'report' => sub { Link Here
349
        $report,
353
        $report,
350
        {
354
        {
351
            modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ],
355
            modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ],
352
            modified_fields      => 7
356
            modified_fields      => 7,
357
            errors               => []
358
        }
359
    );
360
    t::lib::Mocks::mock_preference( 'PreventWithDrawingItemsStatus', 'intransit,checkedout' );
361
362
    $item_2->get_from_storage->update( { onloan => '2025-01-01' } );
363
364
    local $SIG{__WARN__} = sub { };
365
    my ($report2) = $items->batch_update( { new_values => { withdrawn => 1 } } );
366
367
    $items->reset;
368
369
    is_deeply(
370
        $report2,
371
        {
372
            modified_itemnumbers => [ $item_1->itemnumber ],
373
            modified_fields      => 1,
374
            errors               => [
375
                { error => "Exception 'Koha::Exceptions::Item::Transfer::OnLoan' thrown 'onloan_cannot_withdraw'\n" }
376
            ]
353
        }
377
        }
354
    );
378
    );
355
379
380
    my $item_3 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
381
    my $item_4 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
382
383
    Koha::Item::Transfer->new(
384
        {
385
            itemnumber => $item_3->itemnumber,
386
            frombranch => $item_3->homebranch,
387
            tobranch   => $builder->build( { source => 'Branch' } )->{branchcode},
388
            datesent   => '1999-01-01',
389
        }
390
    )->store;
391
392
    my $items2 = Koha::Items->search( { itemnumber => [ $item_3->itemnumber, $item_4->itemnumber ] } );
393
394
    local $SIG{__WARN__} = sub { };
395
    my ($report3) = $items2->batch_update( { new_values => { withdrawn => 1 } } );
396
397
    $items2->reset;
398
    is_deeply(
399
        $report3,
400
        {
401
            modified_itemnumbers => [ $item_4->itemnumber ],
402
            modified_fields      => 1,
403
            errors               => [
404
                {
405
                    error =>
406
                        "Exception 'Koha::Exceptions::Item::Transfer::InTransit' thrown 'intransit_cannot_withdraw'\n"
407
                }
408
            ]
409
        }
410
    );
356
};
411
};
357
412
358
Koha::Caches->get_instance->clear_from_cache("MarcStructure-1-");
413
Koha::Caches->get_instance->clear_from_cache("MarcStructure-1-");
(-)a/t/db_dependent/Koha/Old/Hold.t (-2 / +3 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 3;
20
use Test::More tests => 4;
21
use Test::NoWarnings;
21
use Test::Exception;
22
use Test::Exception;
22
23
23
use Koha::Database;
24
use Koha::Database;
Lines 103-109 subtest 'biblio() tests' => sub { Link Here
103
    my $hold_2 = $builder->build_object(
104
    my $hold_2 = $builder->build_object(
104
        {
105
        {
105
            class => 'Koha::Old::Holds',
106
            class => 'Koha::Old::Holds',
106
            value => { biblionumber => '' }
107
            value => { biblionumber => undef }
107
        }
108
        }
108
    );
109
    );
109
110
(-)a/t/db_dependent/Koha/Patron.t (-2 / +252 lines)
Lines 19-25 Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use Test::More tests => 39;
22
use Test::More tests => 42;
23
use Test::NoWarnings;
23
use Test::Exception;
24
use Test::Exception;
24
use Test::Warn;
25
use Test::Warn;
25
use Time::Fake;
26
use Time::Fake;
Lines 29-35 use Koha::Database; Link Here
29
use Koha::DateUtils qw(dt_from_string);
30
use Koha::DateUtils qw(dt_from_string);
30
use Koha::ArticleRequests;
31
use Koha::ArticleRequests;
31
use Koha::Patrons;
32
use Koha::Patrons;
32
use Koha::List::Patron qw(AddPatronList AddPatronsToList);
33
use Koha::List::Patron       qw(AddPatronList AddPatronsToList);
34
use Koha::Patron::Debarments qw(AddDebarment);
33
use Koha::Patron::Relationships;
35
use Koha::Patron::Relationships;
34
use C4::Circulation qw( AddIssue AddReturn );
36
use C4::Circulation qw( AddIssue AddReturn );
35
37
Lines 738-743 subtest 'to_api() tests' => sub { Link Here
738
        }
740
        }
739
    );
741
    );
740
742
743
    t::lib::Mocks::mock_userenv( { patron => $consumer } );
744
741
    my $restricted = $patron->to_api( { user => $consumer } )->{restricted};
745
    my $restricted = $patron->to_api( { user => $consumer } )->{restricted};
742
    ok( defined $restricted, 'restricted is defined' );
746
    ok( defined $restricted, 'restricted is defined' );
743
    ok( !$restricted,        'debarred is undef, restricted evaluates to false' );
747
    ok( !$restricted,        'debarred is undef, restricted evaluates to false' );
Lines 2868-2870 subtest 'ill_requests() tests' => sub { Link Here
2868
2872
2869
    $schema->storage->txn_rollback;
2873
    $schema->storage->txn_rollback;
2870
};
2874
};
2875
2876
subtest 'can_place_holds() tests' => sub {
2877
2878
    plan tests => 7;
2879
2880
    subtest "'expired' tests" => sub {
2881
2882
        plan tests => 4;
2883
2884
        $schema->storage->txn_begin;
2885
2886
        t::lib::Mocks::mock_preference( 'BlockExpiredPatronOpacActions', 'hold' );
2887
2888
        my $patron = $builder->build_object(
2889
            {
2890
                class => 'Koha::Patrons',
2891
                value => { dateexpiry => \'DATE_ADD(NOW(), INTERVAL -1 DAY)' }
2892
            }
2893
        );
2894
2895
        my $result = $patron->can_place_holds();
2896
        ok( !$result, 'expired, cannot place holds' );
2897
2898
        my $messages = $result->messages();
2899
        is( $messages->[0]->type,    'error' );
2900
        is( $messages->[0]->message, 'expired' );
2901
2902
        ok( $patron->can_place_holds( { overrides => { expired => 1 } } ), "Override works for 'expired'" );
2903
2904
        $schema->storage->txn_rollback;
2905
    };
2906
2907
    subtest "'debt_limit' tests" => sub {
2908
2909
        plan tests => 7;
2910
2911
        $schema->storage->txn_begin;
2912
2913
        # Add a patron, making sure it is not (yet) expired
2914
        my $patron = $builder->build_object( { class => 'Koha::Patrons', } );
2915
        $patron->account->add_debit( { amount => 10, interface => 'opac', type => 'ACCOUNT' } );
2916
2917
        t::lib::Mocks::mock_preference( 'maxoutstanding', undef );
2918
2919
        ok( $patron->can_place_holds(), "No 'maxoutstanding', can place holds" );
2920
2921
        t::lib::Mocks::mock_preference( 'maxoutstanding', '5' );
2922
2923
        my $result = $patron->can_place_holds();
2924
        ok( !$result, 'debt, cannot place holds' );
2925
2926
        my $messages = $result->messages();
2927
        is( $messages->[0]->type,                         'error' );
2928
        is( $messages->[0]->message,                      'debt_limit' );
2929
        is( $messages->[0]->payload->{total_outstanding}, 10 );
2930
        is( $messages->[0]->payload->{max_outstanding},   5 );
2931
2932
        ok( $patron->can_place_holds( { overrides => { debt_limit => 1 } } ), "Override works for 'debt_limit'" );
2933
2934
        $schema->storage->txn_rollback;
2935
    };
2936
2937
    subtest "'bad_address' tests" => sub {
2938
2939
        plan tests => 4;
2940
2941
        $schema->storage->txn_begin;
2942
2943
        # Add a patron, making sure it is not (yet) expired
2944
        my $patron = $builder->build_object(
2945
            {
2946
                class => 'Koha::Patrons',
2947
                value => {
2948
                    gonenoaddress => 1,
2949
                }
2950
            }
2951
        );
2952
2953
        my $result = $patron->can_place_holds();
2954
        ok( !$result, 'flagged for bad address, cannot place holds' );
2955
2956
        my $messages = $result->messages();
2957
        is( $messages->[0]->type,    'error' );
2958
        is( $messages->[0]->message, 'bad_address' );
2959
2960
        ok( $patron->can_place_holds( { overrides => { bad_address => 1 } } ), "Override works for 'bad_address'" );
2961
2962
        $schema->storage->txn_rollback;
2963
    };
2964
2965
    subtest "'card_lost' tests" => sub {
2966
2967
        plan tests => 4;
2968
2969
        $schema->storage->txn_begin;
2970
2971
        # Add a patron, making sure it is not (yet) expired
2972
        my $patron = $builder->build_object(
2973
            {
2974
                class => 'Koha::Patrons',
2975
                value => {
2976
                    lost => 1,
2977
                }
2978
            }
2979
        );
2980
2981
        my $result = $patron->can_place_holds();
2982
        ok( !$result, 'flagged for lost card, cannot place holds' );
2983
2984
        my $messages = $result->messages();
2985
        is( $messages->[0]->type,    'error' );
2986
        is( $messages->[0]->message, 'card_lost' );
2987
2988
        ok( $patron->can_place_holds( { overrides => { card_lost => 1 } } ), "Override works for 'card_lost'" );
2989
2990
        $schema->storage->txn_rollback;
2991
    };
2992
2993
    subtest "'restricted' tests" => sub {
2994
2995
        plan tests => 4;
2996
2997
        $schema->storage->txn_begin;
2998
2999
        # Add a patron, making sure it is not (yet) expired
3000
        my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
3001
        AddDebarment( { borrowernumber => $patron->borrowernumber } );
3002
        $patron->discard_changes();
3003
3004
        my $result = $patron->can_place_holds();
3005
        ok( !$result, 'restricted, cannot place holds' );
3006
3007
        my $messages = $result->messages();
3008
        is( $messages->[0]->type,    'error' );
3009
        is( $messages->[0]->message, 'restricted' );
3010
3011
        ok( $patron->can_place_holds( { overrides => { restricted => 1 } } ), "Override works for 'restricted'" );
3012
3013
        $schema->storage->txn_rollback;
3014
    };
3015
3016
    subtest "'hold_limit' tests" => sub {
3017
3018
        plan tests => 5;
3019
3020
        $schema->storage->txn_begin;
3021
3022
        # Add a patron, making sure it is not (yet) expired
3023
        my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
3024
3025
        # Add a hold
3026
        $builder->build_object( { class => 'Koha::Holds', value => { borrowernumber => $patron->borrowernumber } } );
3027
3028
        t::lib::Mocks::mock_preference( 'maxreserves', undef );
3029
3030
        ok( $patron->can_place_holds(), "No 'maxreserves', can place holds" );
3031
3032
        t::lib::Mocks::mock_preference( 'maxreserves', 1 );
3033
3034
        my $result = $patron->can_place_holds();
3035
        ok( !$result, 'hold limit reached, cannot place holds' );
3036
3037
        my $messages = $result->messages();
3038
        is( $messages->[0]->type,    'error' );
3039
        is( $messages->[0]->message, 'hold_limit' );
3040
3041
        ok( $patron->can_place_holds( { overrides => { hold_limit => 1 } } ), "Override works for 'hold_limit'" );
3042
3043
        $schema->storage->txn_rollback;
3044
    };
3045
3046
    subtest "'no_short_circuit' tests" => sub {
3047
3048
        plan tests => 9;
3049
3050
        $schema->storage->txn_begin;
3051
3052
        # Create a patron with multiple issues
3053
        my $patron = $builder->build_object(
3054
            {
3055
                class => 'Koha::Patrons',
3056
                value => {
3057
                    dateexpiry    => \'DATE_ADD(NOW(), INTERVAL -1 DAY)',    # expired
3058
                    gonenoaddress => 1,                                      # bad address
3059
                    lost          => 1,                                      # card lost
3060
                }
3061
            }
3062
        );
3063
3064
        # Add debt
3065
        $patron->account->add_debit( { amount => 10, interface => 'opac', type => 'ACCOUNT' } );
3066
3067
        # Add restriction
3068
        AddDebarment( { borrowernumber => $patron->borrowernumber } );
3069
        $patron->discard_changes();
3070
3071
        # Mock preferences
3072
        t::lib::Mocks::mock_preference( 'BlockExpiredPatronOpacActions', 'hold' );
3073
        t::lib::Mocks::mock_preference( 'maxoutstanding',                '5' );
3074
3075
        # Test short-circuit behavior (default)
3076
        my $result = $patron->can_place_holds();
3077
        ok( !$result, 'patron cannot place holds' );
3078
        my $messages = $result->messages();
3079
        is( scalar @$messages, 1, 'short-circuit: only one error message returned' );
3080
3081
        # Test no_short_circuit behavior
3082
        $result = $patron->can_place_holds( { no_short_circuit => 1 } );
3083
        ok( !$result, 'patron still cannot place holds with no_short_circuit' );
3084
        $messages = $result->messages();
3085
        is( scalar @$messages, 5, 'no_short_circuit: all error messages collected' );
3086
3087
        # Verify we got all expected error types
3088
        my %message_types = map { $_->message => 1 } @$messages;
3089
        ok( $message_types{expired},     "'expired' error included" );
3090
        ok( $message_types{debt_limit},  "'debt_limit' error included" );
3091
        ok( $message_types{bad_address}, "'bad_address' error included" );
3092
        ok( $message_types{card_lost},   "'card_lost' error included" );
3093
        ok( $message_types{restricted},  "'restricted' error included" );
3094
3095
        $schema->storage->txn_rollback;
3096
    };
3097
};
3098
3099
subtest 'is_anonymous' => sub {
3100
    plan tests => 3;
3101
3102
    $schema->storage->txn_begin;
3103
3104
    t::lib::Mocks::mock_preference( 'AnonymousPatron', '' );
3105
3106
    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
3107
3108
    is( $patron->is_anonymous, 0, 'is_anonymous returns 0 if pref is empty' );
3109
3110
    t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->borrowernumber );
3111
3112
    is( $patron->is_anonymous, 1, q{is_anonymous returns 1 if pref is equal to patron's id} );
3113
3114
    t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->borrowernumber + 1 );
3115
3116
    is( $patron->is_anonymous, 0, q{is_anonymous returns 0 if pref is not equal to patron's id} );
3117
3118
    $schema->storage->txn_rollback;
3119
3120
};
(-)a/t/db_dependent/Koha/Patron/Modifications.t (-15 / +23 lines)
Lines 19-25 use Modern::Perl; Link Here
19
19
20
use utf8;
20
use utf8;
21
21
22
use Test::More tests => 7;
22
use Test::More tests => 8;
23
use Test::NoWarnings;
23
use Test::Exception;
24
use Test::Exception;
24
25
25
use t::lib::TestBuilder;
26
use t::lib::TestBuilder;
Lines 49-57 subtest 'new() tests' => sub { Link Here
49
50
50
    Koha::Patron::Modifications->search->delete;
51
    Koha::Patron::Modifications->search->delete;
51
52
53
    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
54
52
    # Create new pending modification
55
    # Create new pending modification
53
    Koha::Patron::Modification->new(
56
    Koha::Patron::Modification->new(
54
        {
57
        {
58
            borrowernumber     => $patron->borrowernumber,
55
            verification_token => '1234567890',
59
            verification_token => '1234567890',
56
            changed_fields     => 'surname,firstname',
60
            changed_fields     => 'surname,firstname',
57
            surname            => 'Hall',
61
            surname            => 'Hall',
Lines 443-451 subtest 'dateofbirth tests' => sub { Link Here
443
        { class => 'Koha::Patrons', value => { dateofbirth => '1980-01-01', surname => 'a_surname' } } );
447
        { class => 'Koha::Patrons', value => { dateofbirth => '1980-01-01', surname => 'a_surname' } } );
444
    my $patron_modification = Koha::Patron::Modification->new(
448
    my $patron_modification = Koha::Patron::Modification->new(
445
        {
449
        {
446
            changed_fields => 'borrowernumber,dateofbirth',
450
            verification_token => '1234567890',
447
            borrowernumber => $patron->borrowernumber,
451
            changed_fields     => 'borrowernumber,dateofbirth',
448
            dateofbirth    => undef
452
            borrowernumber     => $patron->borrowernumber,
453
            dateofbirth        => undef
449
        }
454
        }
450
    )->store;
455
    )->store;
451
    $patron_modification->approve;
456
    $patron_modification->approve;
Lines 459-467 subtest 'dateofbirth tests' => sub { Link Here
459
    # Adding a dateofbirth
464
    # Adding a dateofbirth
460
    $patron_modification = Koha::Patron::Modification->new(
465
    $patron_modification = Koha::Patron::Modification->new(
461
        {
466
        {
462
            changed_fields => 'borrowernumber,dateofbirth',
467
            verification_token => '1234567890',
463
            borrowernumber => $patron->borrowernumber,
468
            changed_fields     => 'borrowernumber,dateofbirth',
464
            dateofbirth    => '1980-02-02'
469
            borrowernumber     => $patron->borrowernumber,
470
            dateofbirth        => '1980-02-02'
465
        }
471
        }
466
    )->store;
472
    )->store;
467
    $patron_modification->approve;
473
    $patron_modification->approve;
Lines 476-485 subtest 'dateofbirth tests' => sub { Link Here
476
    # Modifying a dateofbirth
482
    # Modifying a dateofbirth
477
    $patron_modification = Koha::Patron::Modification->new(
483
    $patron_modification = Koha::Patron::Modification->new(
478
        {
484
        {
479
            changed_fields => 'borrowernumber,dateofbirth',
485
            verification_token => '1234567890',
480
            borrowernumber => $patron->borrowernumber,
486
            changed_fields     => 'borrowernumber,dateofbirth',
481
            dateofbirth    => '1980-03-03',
487
            borrowernumber     => $patron->borrowernumber,
482
            surname        => undef
488
            dateofbirth        => '1980-03-03',
489
            surname            => undef
483
        }
490
        }
484
    )->store;
491
    )->store;
485
    $patron_modification->approve;
492
    $patron_modification->approve;
Lines 494-503 subtest 'dateofbirth tests' => sub { Link Here
494
    # Modifying something else
501
    # Modifying something else
495
    $patron_modification = Koha::Patron::Modification->new(
502
    $patron_modification = Koha::Patron::Modification->new(
496
        {
503
        {
497
            changed_fields => 'borrowernumber,surname',
504
            verification_token => '1234567890',
498
            borrowernumber => $patron->borrowernumber,
505
            changed_fields     => 'borrowernumber,surname',
499
            surname        => 'another_surname',
506
            borrowernumber     => $patron->borrowernumber,
500
            dateofbirth    => undef
507
            surname            => 'another_surname',
508
            dateofbirth        => undef
501
        }
509
        }
502
    )->store;
510
    )->store;
503
    $patron_modification->approve;
511
    $patron_modification->approve;
(-)a/t/db_dependent/Koha/Patrons.t (-8 / +9 lines)
Lines 2675-2684 subtest 'lock' => sub { Link Here
2675
};
2675
};
2676
2676
2677
subtest 'anonymize' => sub {
2677
subtest 'anonymize' => sub {
2678
    plan tests => 10;
2678
    plan tests => 11;
2679
2679
2680
    my $patron1 = $builder->build_object( { class => 'Koha::Patrons' } );
2680
    my $patron1 = $builder->build_object( { class => 'Koha::Patrons' } )->store;
2681
    my $patron2 = $builder->build_object( { class => 'Koha::Patrons' } );
2681
    my $patron2 = $builder->build_object( { class => 'Koha::Patrons' } )->store;
2682
2682
2683
    # First try patron with issues
2683
    # First try patron with issues
2684
    my $issue = $builder->build_object(
2684
    my $issue = $builder->build_object(
Lines 2696-2711 subtest 'anonymize' => sub { Link Here
2696
    is( $patron1->firstname,   undef, 'First name cleared' );
2696
    is( $patron1->firstname,   undef, 'First name cleared' );
2697
    isnt( $patron1->surname, $surname, 'Surname changed' );
2697
    isnt( $patron1->surname, $surname, 'Surname changed' );
2698
    ok( $patron1->surname =~ /^\w{10}$/, 'Mandatory surname randomized' );
2698
    ok( $patron1->surname =~ /^\w{10}$/, 'Mandatory surname randomized' );
2699
    is( $patron1->branchcode, $branchcode, 'Branch code skipped' );
2699
    is( $patron1->branchcode,        $branchcode, 'Branch code skipped' );
2700
    is( $patron1->email,      undef,       'Email was mandatory, must be cleared' );
2700
    is( $patron1->email,             undef,       'Email was mandatory, must be cleared' );
2701
    is( $patron1->checkprevcheckout, 'inherit',   'Enum checkprevcheckout is reset to the default value' );
2701
2702
2702
    # Test wrapper in Koha::Patrons
2703
    # Test wrapper in Koha::Patrons
2703
    $patron1->surname($surname)->store;       # restore
2704
    $patron1->surname($surname)->store;    # restore
2704
    my $rs = Koha::Patrons->search( { borrowernumber => [ $patron1->borrowernumber, $patron2->borrowernumber ] } )
2705
    my $rs = Koha::Patrons->search( { borrowernumber => [ $patron1->borrowernumber, $patron2->borrowernumber ] } )
2705
        ->anonymize;
2706
        ->anonymize;
2706
    $patron1->discard_changes;                # refresh
2707
    $patron1->discard_changes;             # refresh
2707
    isnt( $patron1->surname, $surname, 'Surname patron1 changed again' );
2708
    isnt( $patron1->surname, $surname, 'Surname patron1 changed again' );
2708
    $patron2->discard_changes;                # refresh
2709
    $patron2->discard_changes;             # refresh
2709
    is( $patron2->firstname, undef, 'First name patron2 cleared' );
2710
    is( $patron2->firstname, undef, 'First name patron2 cleared' );
2710
};
2711
};
2711
2712
(-)a/t/db_dependent/Koha/Patrons/Import.t (-1 / +2 lines)
Lines 18-24 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 Test::More tests => 180;
21
use Test::More tests => 181;
22
use Test::NoWarnings;
22
use Test::Warn;
23
use Test::Warn;
23
use Test::Exception;
24
use Test::Exception;
24
use Encode qw( encode_utf8 );
25
use Encode qw( encode_utf8 );
(-)a/t/db_dependent/Koha/Plugins/Patron.t (-1 / +7 lines)
Lines 16-23 Link Here
16
16
17
use Modern::Perl;
17
use Modern::Perl;
18
18
19
use Test::More tests => 5;
19
use Test::More tests => 6;
20
use Test::NoWarnings;
20
use Test::Exception;
21
use Test::Exception;
22
use Test::MockModule;
21
23
22
use File::Basename;
24
use File::Basename;
23
25
Lines 51-56 subtest 'check_password hook tests' => sub { Link Here
51
    my $plugins = Koha::Plugins->new;
53
    my $plugins = Koha::Plugins->new;
52
    $plugins->InstallPlugins;
54
    $plugins->InstallPlugins;
53
55
56
    # Mock patron_barcode_transform: we don't want to call it as it generates warnings
57
    my $plugin_mock = Test::MockModule->new("Koha::Plugin::Test");
58
    $plugin_mock->mock( "patron_barcode_transform", sub { } );
59
54
    # Test Plugin enforces a 4 digit numeric pin for passwords
60
    # Test Plugin enforces a 4 digit numeric pin for passwords
55
    my $plugin = Koha::Plugin::Test->new->enable;
61
    my $plugin = Koha::Plugin::Test->new->enable;
56
62
(-)a/t/db_dependent/Koha/Reports.t (-1 / +37 lines)
Lines 18-24 Link Here
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::NoWarnings;
20
use Test::NoWarnings;
21
use Test::More tests => 9;
21
use Test::More tests => 10;
22
22
23
use Koha::Report;
23
use Koha::Report;
24
use Koha::Reports;
24
use Koha::Reports;
Lines 185-188 subtest '_might_add_limit' => sub { Link Here
185
    );
185
    );
186
};
186
};
187
187
188
subtest 'reports_branches are added and removed from report_branches table' => sub {
189
    plan tests => 4;
190
191
    my $updated_nb_of_reports = Koha::Reports->search->count;
192
    my $report                = Koha::Report->new(
193
        {
194
            report_name => 'report_name_for_test_1',
195
            savedsql    => 'SELECT * FROM items WHERE itemnumber IN <<Test|list>>',
196
        }
197
    )->store;
198
199
    my $id       = $report->id;
200
    my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
201
    my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
202
    my $library3 = $builder->build_object( { class => 'Koha::Libraries' } );
203
    my @branches = ( $library1->branchcode, $library2->branchcode, $library3->branchcode );
204
205
    $report->replace_library_limits( \@branches );
206
207
    my @branches_loop = $report->get_library_limits->as_list;
208
    is( scalar @branches_loop, 3, '3 branches added to report_branches table' );
209
210
    $report->replace_library_limits( [ $library1->branchcode, $library2->branchcode ] );
211
212
    @branches_loop = $report->get_library_limits->as_list;
213
    is( scalar @branches_loop, 2, '1 branch removed from report_branches table' );
214
215
    $report->delete;
216
    is( Koha::Reports->search->count, $updated_nb_of_reports, 'Report deleted, count is back to original' );
217
    is(
218
        $schema->resultset('ReportsBranch')->search( { report_id => $id } )->count,
219
        0,
220
        'No branches left in reports_branches table after report deletion'
221
    );
222
};
223
188
$schema->storage->txn_rollback;
224
$schema->storage->txn_rollback;
(-)a/t/db_dependent/Koha/SearchEngine/Elasticsearch.t (-15 / +17 lines)
Lines 18-24 Link Here
18
use Modern::Perl;
18
use Modern::Perl;
19
use Encode;
19
use Encode;
20
20
21
use Test::More tests => 8;
21
use Test::More tests => 9;
22
use Test::NoWarnings;
23
use Test::Warn;
22
use Test::Exception;
24
use Test::Exception;
23
25
24
use t::lib::Mocks;
26
use t::lib::Mocks;
Lines 209-215 subtest 'get_elasticsearch_mappings() tests' => sub { Link Here
209
211
210
subtest 'Koha::SearchEngine::Elasticsearch::marc_records_to_documents () tests' => sub {
212
subtest 'Koha::SearchEngine::Elasticsearch::marc_records_to_documents () tests' => sub {
211
213
212
    plan tests => 70;
214
    plan tests => 71;
213
215
214
    t::lib::Mocks::mock_preference( 'marcflavour',             'MARC21' );
216
    t::lib::Mocks::mock_preference( 'marcflavour',             'MARC21' );
215
    t::lib::Mocks::mock_preference( 'ElasticsearchMARCFormat', 'base64ISO2709' );
217
    t::lib::Mocks::mock_preference( 'ElasticsearchMARCFormat', 'base64ISO2709' );
Lines 851-857 subtest 'Koha::SearchEngine::Elasticsearch::marc_records_to_documents () tests' Link Here
851
        MARC::Field->new( '999', '', '', c => '1234567' ),
853
        MARC::Field->new( '999', '', '', c => '1234567' ),
852
    );
854
    );
853
855
854
    $docs = $see->marc_records_to_documents( [$marc_record_with_large_field] );
856
    warning_is {
857
        $docs = $see->marc_records_to_documents( [$marc_record_with_large_field] );
858
    }
859
    "Warnings encountered while roundtripping a MARC record to/from USMARC. Failing over to MARCXML.";
855
860
856
    subtest '_process_mappings() split tests' => sub {
861
    subtest '_process_mappings() split tests' => sub {
857
862
Lines 1225-1245 subtest 'marc_records_to_documents should set the "available" field' => sub { Link Here
1225
    # sort_fields will call this and use the actual db values unless we call it first
1230
    # sort_fields will call this and use the actual db values unless we call it first
1226
    $see->get_elasticsearch_mappings();
1231
    $see->get_elasticsearch_mappings();
1227
1232
1228
    my $marc_record_1 = MARC::Record->new();
1233
    my $builder       = t::lib::TestBuilder->new;
1229
    $marc_record_1->leader('     cam  22      a 4500');
1234
    my $biblio        = $builder->build_sample_biblio;
1230
    $marc_record_1->append_fields(
1235
    my $marc_record_1 = $biblio->metadata->record;
1231
        MARC::Field->new( '245', '', '', a => 'Title' ),
1232
    );
1233
    my ($biblionumber) = C4::Biblio::AddBiblio( $marc_record_1, '' );
1234
1236
1235
    my $docs = $see->marc_records_to_documents( [$marc_record_1] );
1237
    my $docs = $see->marc_records_to_documents( [$marc_record_1] );
1236
    is_deeply( $docs->[0]->{available}, \0, 'a biblio without items is not available' );
1238
    is_deeply( $docs->[0]->{available}, \0, 'a biblio without items is not available' );
1237
1239
1238
    my $item = Koha::Item->new(
1240
    my $item = $builder->build_sample_item(
1239
        {
1241
        {
1240
            biblionumber => $biblionumber,
1242
            biblionumber => $biblio->biblionumber,
1241
        }
1243
        }
1242
    )->store();
1244
    );
1243
1245
1244
    $docs = $see->marc_records_to_documents( [$marc_record_1] );
1246
    $docs = $see->marc_records_to_documents( [$marc_record_1] );
1245
    is_deeply( $docs->[0]->{available}, \1, 'a biblio with one item that has no particular status is available' );
1247
    is_deeply( $docs->[0]->{available}, \1, 'a biblio with one item that has no particular status is available' );
Lines 1264-1274 subtest 'marc_records_to_documents should set the "available" field' => sub { Link Here
1264
    $docs = $see->marc_records_to_documents( [$marc_record_1] );
1266
    $docs = $see->marc_records_to_documents( [$marc_record_1] );
1265
    is_deeply( $docs->[0]->{available}, \1, 'a biblio with one item that is damaged is available' );
1267
    is_deeply( $docs->[0]->{available}, \1, 'a biblio with one item that is damaged is available' );
1266
1268
1267
    my $item2 = Koha::Item->new(
1269
    my $item2 = $builder->build_sample_item(
1268
        {
1270
        {
1269
            biblionumber => $biblionumber,
1271
            biblionumber => $biblio->biblionumber,
1270
        }
1272
        }
1271
    )->store();
1273
    );
1272
    $docs = $see->marc_records_to_documents( [$marc_record_1] );
1274
    $docs = $see->marc_records_to_documents( [$marc_record_1] );
1273
    is_deeply(
1275
    is_deeply(
1274
        $docs->[0]->{available}, \1,
1276
        $docs->[0]->{available}, \1,
(-)a/t/db_dependent/Koha/SearchEngine/Elasticsearch/ExportConfig.t (-4 / +8 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 19;
20
use Test::More tests => 20;
21
use Test::NoWarnings;
21
22
22
use Koha::Database;
23
use Koha::Database;
23
use Koha::SearchFields;
24
use Koha::SearchFields;
Lines 59-65 $search_field->add_to_search_marc_maps( Link Here
59
    {
60
    {
60
        facet       => 0,
61
        facet       => 0,
61
        suggestible => 0,
62
        suggestible => 0,
62
        sort        => 0
63
        sort        => 0,
64
        filter      => '',
63
    }
65
    }
64
);
66
);
65
67
Lines 76-82 $search_field->add_to_search_marc_maps( Link Here
76
    {
78
    {
77
        facet       => 0,
79
        facet       => 0,
78
        suggestible => 0,
80
        suggestible => 0,
79
        sort        => 0
81
        sort        => 0,
82
        filter      => '',
80
    }
83
    }
81
);
84
);
82
85
Lines 93-99 $search_field->add_to_search_marc_maps( Link Here
93
    {
96
    {
94
        facet       => 0,
97
        facet       => 0,
95
        suggestible => 1,
98
        suggestible => 1,
96
        sort        => 0
99
        sort        => 0,
100
        filter      => '',
97
    }
101
    }
98
);
102
);
99
103
(-)a/t/db_dependent/Koha/SearchEngine/Elasticsearch/Search.t (-1 / +2 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 14;
20
use Test::More tests => 15;
21
use Test::NoWarnings;
21
use t::lib::Mocks;
22
use t::lib::Mocks;
22
use t::lib::TestBuilder;
23
use t::lib::TestBuilder;
23
24
(-)a/t/db_dependent/Letters.t (-1 / +2 lines)
Lines 19-25 Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
use File::Basename qw(dirname);
21
use File::Basename qw(dirname);
22
use Test::More tests => 104;
22
use Test::More tests => 105;
23
use Test::NoWarnings;
23
24
24
use Test::MockModule;
25
use Test::MockModule;
25
use Test::Warn;
26
use Test::Warn;
(-)a/t/db_dependent/Overdues.t (-5 / +5 lines)
Lines 1-7 Link Here
1
#!/usr/bin/perl;
1
#!/usr/bin/perl;
2
2
3
use Modern::Perl;
3
use Modern::Perl;
4
use Test::More tests => 18;
4
use Test::More tests => 19;
5
use Test::NoWarnings;
5
use Test::Warn;
6
use Test::Warn;
6
7
7
use C4::Context;
8
use C4::Context;
Lines 549-558 subtest 'UpdateFine tests' => sub { Link Here
549
        { order_by       => { '-asc' => 'accountlines_id' } }
550
        { order_by       => { '-asc' => 'accountlines_id' } }
550
    );
551
    );
551
    is( $fines->count, 4, "New amount should be 0 so no fine added" );
552
    is( $fines->count, 4, "New amount should be 0 so no fine added" );
552
    ok(
553
    t::lib::Mocks::mock_userenv( { patron => $patron_1 } );
553
        C4::Circulation::AddReturn( $item_1->barcode, $item_1->homebranch, 1 ),
554
    my @r = C4::Circulation::AddReturn( $item_1->barcode, $item_1->homebranch, 1 );
554
        "Returning the item and forgiving fines succeeds"
555
    is( $r[1]->{WasReturned}, 1, "Returning the item and forgiving fines succeeds" );
555
    );
556
556
557
    t::lib::Mocks::mock_preference( 'MaxFine', 0 );
557
    t::lib::Mocks::mock_preference( 'MaxFine', 0 );
558
558
(-)a/t/db_dependent/Reserves.t (-2 / +10 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 70;
20
use Test::More tests => 71;
21
use Test::NoWarnings;
21
use Test::MockModule;
22
use Test::MockModule;
22
use Test::Warn;
23
use Test::Warn;
23
24
Lines 413-419 is( Link Here
413
    $which_highest->{reserve_id}, $reserve_id,
414
    $which_highest->{reserve_id}, $reserve_id,
414
    'CheckReserves returns higher priority future reserve with sufficient lookahead'
415
    'CheckReserves returns higher priority future reserve with sufficient lookahead'
415
);
416
);
416
ModReserve( { reserve_id => $now_reserve_id, rank => 'del', cancellation_reason => 'test reserve' } );
417
418
{
419
    # Prevent warning 'No reserves HOLD_CANCELLATION letter transported by email'
420
    my $mock_letters = Test::MockModule->new('C4::Letters');
421
    $mock_letters->mock( 'GetPreparedLetter', sub { return } );
422
423
    ModReserve( { reserve_id => $now_reserve_id, rank => 'del', cancellation_reason => 'test reserve' } );
424
}
417
425
418
# End of tests for bug 9761 (ConfirmFutureHolds)
426
# End of tests for bug 9761 (ConfirmFutureHolds)
419
427
(-)a/t/db_dependent/Reserves/CancelExpiredReserves.t (-2 / +10 lines)
Lines 1-7 Link Here
1
#!/usr/bin/perl
1
#!/usr/bin/perl
2
2
3
use Modern::Perl;
3
use Modern::Perl;
4
use Test::More tests => 4;
4
use Test::More tests => 5;
5
use Test::NoWarnings;
6
use Test::MockModule;
5
7
6
use t::lib::Mocks;
8
use t::lib::Mocks;
7
use t::lib::TestBuilder;
9
use t::lib::TestBuilder;
Lines 240-246 subtest 'Test handling of cancellation reason if passed' => sub { Link Here
240
    );
242
    );
241
    my $reserve_id = $reserve->{reserve_id};
243
    my $reserve_id = $reserve->{reserve_id};
242
    my $count      = Koha::Holds->search->count;
244
    my $count      = Koha::Holds->search->count;
243
    CancelExpiredReserves("EXPIRED");
245
    {
246
        # Prevent warning 'No reserves HOLD_CANCELLATION letter transported by email'
247
        my $mock_letters = Test::MockModule->new('C4::Letters');
248
        $mock_letters->mock( 'GetPreparedLetter', sub { return } );
249
250
        CancelExpiredReserves("EXPIRED");
251
    }
244
    is( Koha::Holds->search->count, $count - 1, "Hold is cancelled when reason is passed" );
252
    is( Koha::Holds->search->count, $count - 1, "Hold is cancelled when reason is passed" );
245
    my $old_reserve = Koha::Old::Holds->find($reserve_id);
253
    my $old_reserve = Koha::Old::Holds->find($reserve_id);
246
    is( $old_reserve->cancellation_reason, 'EXPIRED', "Hold cancellation_reason was set correctly" );
254
    is( $old_reserve->cancellation_reason, 'EXPIRED', "Hold cancellation_reason was set correctly" );
(-)a/t/db_dependent/SIP/Transaction.t (-1 / +20 lines)
Lines 983-989 subtest do_checkout_with_patron_blocked => sub { Link Here
983
};
983
};
984
984
985
subtest do_checkout_with_noblock => sub {
985
subtest do_checkout_with_noblock => sub {
986
    plan tests => 1;
986
    plan tests => 3;
987
987
988
    my $mockILS = Test::MockObject->new;
988
    my $mockILS = Test::MockObject->new;
989
    my $server  = { ils => $mockILS };
989
    my $server  = { ils => $mockILS };
Lines 1033-1038 subtest do_checkout_with_noblock => sub { Link Here
1033
        $patron->checkouts->count,
1033
        $patron->checkouts->count,
1034
        1, 'No Block checkout was performed for debarred patron'
1034
        1, 'No Block checkout was performed for debarred patron'
1035
    );
1035
    );
1036
1037
    # Test Bug 32934: SIP checkout with no_block_due_date should honor the specified due date
1038
    $sip_patron = C4::SIP::ILS::Patron->new( $patron->cardnumber );
1039
    my $sip_item_2  = C4::SIP::ILS::Item->new( $item->barcode );
1040
    my $transaction = C4::SIP::ILS::Transaction::Checkout->new();
1041
1042
    $transaction->patron($sip_patron);
1043
    $transaction->item($sip_item_2);
1044
1045
    # Set no_block_due_date to test the fix (YYYYMMDDZZZZHHMMSS)
1046
    my $expected_due_date = '20250115    000000';
1047
    my $checkout_result   = $transaction->do_checkout( undef, $expected_due_date );
1048
1049
    my $checkout_obj = $patron->checkouts->find( { itemnumber => $item->itemnumber } );
1050
    isnt( $checkout_obj, undef, 'Checkout object exists after no block checkout' );
1051
    like(
1052
        dt_from_string( $checkout_obj->date_due )->ymd(''), qr/^20250115/,
1053
        'No block due date is honored in SIP checkout'
1054
    );
1036
};
1055
};
1037
1056
1038
subtest do_checkout_with_holds => sub {
1057
subtest do_checkout_with_holds => sub {
(-)a/t/db_dependent/Search.t (+2 lines)
Lines 170-175 $contextmodule->mock( Link Here
170
            return q{};
170
            return q{};
171
        } elsif ( $pref eq 'COinSinOPACResults' ) {
171
        } elsif ( $pref eq 'COinSinOPACResults' ) {
172
            return q{};
172
            return q{};
173
        } elsif ( $pref eq 'FacetSortingLocale' ) {
174
            return 'default';
173
        } else {
175
        } else {
174
            warn
176
            warn
175
                "The syspref $pref was requested but I don't know what to say; this indicates that the test requires updating"
177
                "The syspref $pref was requested but I don't know what to say; this indicates that the test requires updating"
(-)a/t/db_dependent/Search_FacetSorting.t (+119 lines)
Line 0 Link Here
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 Test::NoWarnings;
21
use Test::More tests => 3;
22
use Test::MockModule;
23
use t::lib::Mocks;
24
use C4::Search;
25
26
use utf8;
27
28
subtest '_sort_facets_zebra with system preference' => sub {
29
    plan tests => 3;
30
31
    my $facets = _get_facets();
32
33
    # Test with explicit locale parameter
34
    my $sorted_facets_explicit = C4::Search::_sort_facets_zebra( $facets, 'default' );
35
    my $expected               = [
36
        { facet_label_value => 'Ã…berg, Erik' },
37
        { facet_label_value => 'ari' },
38
        { facet_label_value => 'Ari' },
39
        { facet_label_value => 'Ã…uthor' },
40
        { facet_label_value => 'étienne' },
41
        { facet_label_value => 'fairy' },
42
        { facet_label_value => 'Fairy' },
43
        { facet_label_value => 'harry' },
44
        { facet_label_value => 'Harry' },
45
        { facet_label_value => 'mary' },
46
        { facet_label_value => 'Mary' },
47
        { facet_label_value => 'Šostakovitš, Dmitri' },
48
        { facet_label_value => 'Zambidis' },
49
    ];
50
    is_deeply( $sorted_facets_explicit, $expected, "Zebra facets sorted correctly with explicit locale" );
51
52
    # Test with system preference
53
    t::lib::Mocks::mock_preference( 'FacetSortingLocale', 'default' );
54
    my $sorted_facets_syspref = C4::Search::_sort_facets_zebra($facets);
55
    is_deeply( $sorted_facets_syspref, $expected, "Zebra facets sorted correctly with system preference" );
56
57
    # Test fallback behavior
58
    t::lib::Mocks::mock_preference( 'FacetSortingLocale', '' );
59
    my $sorted_facets_fallback = C4::Search::_sort_facets_zebra($facets);
60
    is( ref($sorted_facets_fallback), 'ARRAY', "Zebra facets sorting fallback works" );
61
};
62
63
subtest '_sort_facets_zebra with fi_FI locale' => sub {
64
    plan tests => 1;
65
    my $locale_map = _get_locale_map();
66
SKIP: {
67
        skip( "fi_FI.utf8 locale not available on this system", 1 ) unless $locale_map->{"fi_FI.utf8"};
68
69
        my $facets = _get_facets();
70
71
        # Test with explicit locale parameter
72
        my $sorted_facets_explicit = C4::Search::_sort_facets_zebra( $facets, 'fi_FI' );
73
        my $expected               = [
74
            { facet_label_value => 'ari' },
75
            { facet_label_value => 'Ari' },
76
            { facet_label_value => 'étienne' },
77
            { facet_label_value => 'fairy' },
78
            { facet_label_value => 'Fairy' },
79
            { facet_label_value => 'harry' },
80
            { facet_label_value => 'Harry' },
81
            { facet_label_value => 'mary' },
82
            { facet_label_value => 'Mary' },
83
            { facet_label_value => 'Šostakovitš, Dmitri' },
84
            { facet_label_value => 'Zambidis' },
85
            { facet_label_value => 'Ã…berg, Erik' },
86
            { facet_label_value => 'Ã…uthor' },
87
        ];
88
        is_deeply( $sorted_facets_explicit, $expected, "Zebra facets sorted correctly with explicit locale" );
89
    }
90
};
91
92
sub _get_facets {
93
    my $facets = [
94
        { facet_label_value => 'Mary' },
95
        { facet_label_value => 'Harry' },
96
        { facet_label_value => 'Fairy' },
97
        { facet_label_value => 'Ari' },
98
        { facet_label_value => 'mary' },
99
        { facet_label_value => 'harry' },
100
        { facet_label_value => 'Ã…berg, Erik' },
101
        { facet_label_value => 'Ã…uthor' },
102
        { facet_label_value => 'fairy' },
103
        { facet_label_value => 'ari' },
104
        { facet_label_value => 'étienne' },
105
        { facet_label_value => 'Šostakovitš, Dmitri' },
106
        { facet_label_value => 'Zambidis' },
107
    ];
108
    return $facets;
109
}
110
111
sub _get_locale_map {
112
    my $map     = {};
113
    my @locales = `locale -a`;
114
    foreach my $locale (@locales) {
115
        chomp($locale);
116
        $map->{$locale} = 1;
117
    }
118
    return $map;
119
}
(-)a/t/db_dependent/api/v1/biblios.t (-13 / +20 lines)
Lines 20-26 use Modern::Perl; Link Here
20
use utf8;
20
use utf8;
21
use Encode;
21
use Encode;
22
22
23
use Test::More tests => 15;
23
use Test::More tests => 16;
24
use Test::NoWarnings;
24
use Test::MockModule;
25
use Test::MockModule;
25
use Test::Mojo;
26
use Test::Mojo;
26
use Test::Warn;
27
use Test::Warn;
Lines 1889-1895 subtest 'list() tests' => sub { Link Here
1889
1890
1890
subtest 'add_item() tests' => sub {
1891
subtest 'add_item() tests' => sub {
1891
1892
1892
    plan tests => 8;
1893
    plan tests => 10;
1893
1894
1894
    $schema->storage->txn_begin;
1895
    $schema->storage->txn_begin;
1895
1896
Lines 1939-1955 subtest 'add_item() tests' => sub { Link Here
1939
1940
1940
    my $item = $builder->build_sample_item();
1941
    my $item = $builder->build_sample_item();
1941
1942
1942
    $t->post_ok(
1943
    warnings_like {
1943
        "//$userid:$password@/api/v1/biblios/$biblio_id/items" => json => {
1944
        $t->post_ok(
1944
            external_id => $item->barcode,
1945
            "//$userid:$password@/api/v1/biblios/$biblio_id/items" => json => {
1945
        }
1946
                external_id => $item->barcode,
1946
    )->status_is( 409, 'Duplicate barcode' );
1947
            }
1948
        )->status_is( 409, 'Duplicate barcode' )->json_is( "/error" => "Duplicate barcode." );
1949
    }
1950
    qr{DBD::mysql::st execute failed: Duplicate entry '(.*?)' for key '(.*\.?)itembarcodeidx'};
1947
1951
1948
    $schema->storage->txn_rollback;
1952
    $schema->storage->txn_rollback;
1949
};
1953
};
1950
1954
1951
subtest 'update_item() tests' => sub {
1955
subtest 'update_item() tests' => sub {
1952
    plan tests => 7;
1956
    plan tests => 9;
1953
1957
1954
    $schema->storage->txn_begin;
1958
    $schema->storage->txn_begin;
1955
1959
Lines 1993-2003 subtest 'update_item() tests' => sub { Link Here
1993
1997
1994
    my $other_item = $builder->build_sample_item();
1998
    my $other_item = $builder->build_sample_item();
1995
1999
1996
    $t->put_ok(
2000
    warnings_like {
1997
        "//$userid:$password@/api/v1/biblios/$biblio_id/items/$item_id" => json => {
2001
        $t->put_ok(
1998
            external_id => $other_item->barcode,
2002
            "//$userid:$password@/api/v1/biblios/$biblio_id/items/$item_id" => json => {
1999
        }
2003
                external_id => $other_item->barcode,
2000
    )->status_is( 409, 'Barcode not unique' );
2004
            }
2005
        )->status_is( 409, 'Barcode not unique' )->json_is( "/error" => "Duplicate barcode." );
2006
    }
2007
    qr{DBD::mysql::st execute failed: Duplicate entry '(.*?)' for key '(.*\.?)itembarcodeidx'};
2001
2008
2002
    $t->put_ok(
2009
    $t->put_ok(
2003
        "//$userid:$password@/api/v1/biblios/$biblio_id/items/$item_id" => json => {
2010
        "//$userid:$password@/api/v1/biblios/$biblio_id/items/$item_id" => json => {
(-)a/t/db_dependent/api/v1/holds.t (-3 / +255 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 15;
20
use Test::More tests => 17;
21
use Test::NoWarnings;
21
use Test::MockModule;
22
use Test::MockModule;
22
use Test::Mojo;
23
use Test::Mojo;
23
use t::lib::TestBuilder;
24
use t::lib::TestBuilder;
Lines 37-42 use Koha::Biblios; Link Here
37
use Koha::Biblioitems;
38
use Koha::Biblioitems;
38
use Koha::Items;
39
use Koha::Items;
39
use Koha::CirculationRules;
40
use Koha::CirculationRules;
41
use Koha::Patron::Debarments qw(AddDebarment);
40
42
41
my $schema  = Koha::Database->new->schema;
43
my $schema  = Koha::Database->new->schema;
42
my $builder = t::lib::TestBuilder->new();
44
my $builder = t::lib::TestBuilder->new();
Lines 198-203 subtest "Test endpoints with permission" => sub { Link Here
198
200
199
    plan tests => 45;
201
    plan tests => 45;
200
202
203
    # Prevent warning 'No reserves HOLD_CANCELLATION letter transported by email'
204
    my $mock_letters = Test::MockModule->new('C4::Letters');
205
    $mock_letters->mock( 'GetPreparedLetter', sub { return } );
206
201
    $t->get_ok("//$userid_1:$password@/api/v1/holds")->status_is(200)->json_has('/0')->json_has('/1')->json_hasnt('/2');
207
    $t->get_ok("//$userid_1:$password@/api/v1/holds")->status_is(200)->json_has('/0')->json_has('/1')->json_hasnt('/2');
202
208
203
    $t->get_ok("//$userid_1:$password@/api/v1/holds?priority=2")->status_is(200)
209
    $t->get_ok("//$userid_1:$password@/api/v1/holds?priority=2")->status_is(200)
Lines 697-704 subtest 'add() tests (maxreserves behaviour)' => sub { Link Here
697
        item_id           => $item_3->itemnumber
703
        item_id           => $item_3->itemnumber
698
    };
704
    };
699
705
700
    $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )->status_is(403)
706
    $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )->status_is(409)->json_is(
701
        ->json_is( { error => 'Hold cannot be placed. Reason: tooManyReserves' } );
707
        {
708
            error      => 'Hold cannot be placed. Reason: hold_limit',
709
            error_code => 'hold_limit'
710
        }
711
    );
702
712
703
    t::lib::Mocks::mock_preference( 'maxreserves', 0 );
713
    t::lib::Mocks::mock_preference( 'maxreserves', 0 );
704
714
Lines 717-722 subtest 'add() tests (maxreserves behaviour)' => sub { Link Here
717
    $schema->storage->txn_rollback;
727
    $schema->storage->txn_rollback;
718
};
728
};
719
729
730
subtest 'add() + can_place_holds() tests' => sub {
731
732
    plan tests => 7;
733
734
    $schema->storage->txn_begin;
735
736
    my $password = 'AbcdEFG123';
737
738
    my $library = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1 } } );
739
    my $staff   = $builder->build_object( { class => 'Koha::Patrons',   value => { flags           => 1 } } );
740
    $staff->set_password( { password => $password, skip_validation => 1 } );
741
    my $userid = $staff->userid;
742
743
    subtest "'expired' tests" => sub {
744
745
        plan tests => 5;
746
747
        t::lib::Mocks::mock_preference( 'BlockExpiredPatronOpacActions', 'hold' );
748
749
        my $patron = $builder->build_object(
750
            {
751
                class => 'Koha::Patrons',
752
                value => { dateexpiry => \'DATE_ADD(NOW(), INTERVAL -1 DAY)' }
753
            }
754
        );
755
756
        my $item = $builder->build_sample_item( { library => $library->id } );
757
758
        my $post_data = {
759
            patron_id         => $patron->id,
760
            pickup_library_id => $item->holdingbranch,
761
            item_id           => $item->id
762
        };
763
764
        $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
765
            ->status_is( 409, "Expected error related to 'expired'" )
766
            ->json_is( { error => "Hold cannot be placed. Reason: expired", error_code => 'expired' } );
767
768
        $t->post_ok( "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'expired' } => json => $post_data )
769
            ->status_is( 201, "Override works for 'expired'" );
770
    };
771
772
    subtest "'debt_limit' tests" => sub {
773
774
        plan tests => 7;
775
776
        # Add a patron, making sure it is not (yet) expired
777
        my $patron = $builder->build_object( { class => 'Koha::Patrons', } );
778
        $patron->account->add_debit( { amount => 10, interface => 'opac', type => 'ACCOUNT' } );
779
780
        my $item = $builder->build_sample_item( { library => $library->id } );
781
782
        my $post_data = {
783
            patron_id         => $patron->id,
784
            pickup_library_id => $item->holdingbranch,
785
            item_id           => $item->id
786
        };
787
788
        t::lib::Mocks::mock_preference( 'maxoutstanding', '5' );
789
790
        $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
791
            ->status_is( 409, "Expected error related to 'debt_limit'" )
792
            ->json_is( { error => "Hold cannot be placed. Reason: debt_limit", error_code => 'debt_limit' } );
793
794
        my $hold_id =
795
            $t->post_ok(
796
            "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'debt_limit' } => json => $post_data )
797
            ->status_is( 201, "Override works for 'debt_limit'" )->tx->res->json->{hold_id};
798
799
        Koha::Holds->find($hold_id)->delete();
800
801
        t::lib::Mocks::mock_preference( 'maxoutstanding', undef );
802
803
        $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
804
            ->status_is( 201, "No 'maxoutstanding', can place holds" );
805
    };
806
807
    subtest "'bad_address' tests" => sub {
808
809
        plan tests => 5;
810
811
        # Add a patron, making sure it is not (yet) expired
812
        my $patron = $builder->build_object(
813
            {
814
                class => 'Koha::Patrons',
815
                value => {
816
                    gonenoaddress => 1,
817
                }
818
            }
819
        );
820
821
        my $item = $builder->build_sample_item( { library => $library->id } );
822
823
        my $post_data = {
824
            patron_id         => $patron->id,
825
            pickup_library_id => $item->holdingbranch,
826
            item_id           => $item->id
827
        };
828
829
        $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
830
            ->status_is( 409, "Expected error related to 'bad_address'" )
831
            ->json_is( { error => "Hold cannot be placed. Reason: bad_address", error_code => 'bad_address' } );
832
833
        $t->post_ok(
834
            "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'bad_address' } => json => $post_data )
835
            ->status_is( 201, "Override works for 'bad_address'" );
836
    };
837
838
    subtest "'card_lost' tests" => sub {
839
840
        plan tests => 5;
841
842
        # Add a patron, making sure it is not (yet) expired
843
        my $patron = $builder->build_object(
844
            {
845
                class => 'Koha::Patrons',
846
                value => {
847
                    lost => 1,
848
                }
849
            }
850
        );
851
852
        my $item = $builder->build_sample_item( { library => $library->id } );
853
854
        my $post_data = {
855
            patron_id         => $patron->id,
856
            pickup_library_id => $item->holdingbranch,
857
            item_id           => $item->id
858
        };
859
860
        $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
861
            ->status_is( 409, "Expected error related to 'card_lost'" )
862
            ->json_is( { error => "Hold cannot be placed. Reason: card_lost", error_code => 'card_lost' } );
863
864
        $t->post_ok( "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'card_lost' } => json => $post_data )
865
            ->status_is( 201, "Override works for 'card_lost'" );
866
    };
867
868
    subtest "'restricted' tests" => sub {
869
870
        plan tests => 5;
871
872
        # Add a patron, making sure it is not (yet) expired
873
        my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
874
        AddDebarment( { borrowernumber => $patron->borrowernumber } );
875
        $patron->discard_changes();
876
877
        my $item = $builder->build_sample_item( { library => $library->id } );
878
879
        my $post_data = {
880
            patron_id         => $patron->id,
881
            pickup_library_id => $item->holdingbranch,
882
            item_id           => $item->id
883
        };
884
885
        $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
886
            ->status_is( 409, "Expected error related to 'restricted'" )
887
            ->json_is( { error => "Hold cannot be placed. Reason: restricted", error_code => 'restricted' } );
888
889
        $t->post_ok(
890
            "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'restricted' } => json => $post_data )
891
            ->status_is( 201, "Override works for 'restricted'" );
892
    };
893
894
    subtest "'hold_limit' tests" => sub {
895
896
        plan tests => 7;
897
898
        # Add a patron, making sure it is not (yet) expired
899
        my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
900
901
        # Add a hold
902
        $builder->build_object( { class => 'Koha::Holds', value => { borrowernumber => $patron->borrowernumber } } );
903
904
        t::lib::Mocks::mock_preference( 'maxreserves', 1 );
905
906
        my $item = $builder->build_sample_item( { library => $library->id } );
907
908
        my $post_data = {
909
            patron_id         => $patron->id,
910
            pickup_library_id => $item->holdingbranch,
911
            item_id           => $item->id
912
        };
913
914
        $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
915
            ->status_is( 409, "Expected error related to 'hold_limit'" )
916
            ->json_is( { error => "Hold cannot be placed. Reason: hold_limit", error_code => 'hold_limit' } );
917
918
        my $hold_id = $t->post_ok(
919
            "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'hold_limit' } => json => $post_data )
920
            ->status_is( 201, "Override works for 'hold_limit'" )->tx->res->json->{hold_id};
921
922
        Koha::Holds->find($hold_id)->delete();
923
924
        t::lib::Mocks::mock_preference( 'maxreserves', undef );
925
926
        $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
927
            ->status_is( 201, "No 'max_reserves', can place holds" );
928
    };
929
930
    subtest "Multiple blocking conditions tests" => sub {
931
932
        plan tests => 8;
933
934
        t::lib::Mocks::mock_preference( 'BlockExpiredPatronOpacActions', 'hold' );
935
936
        my $patron = $builder->build_object(
937
            {
938
                class => 'Koha::Patrons',
939
                value => { dateexpiry => \'DATE_ADD(NOW(), INTERVAL -1 DAY)' }
940
            }
941
        );
942
943
        t::lib::Mocks::mock_preference( 'maxoutstanding', '5' );
944
        $patron->account->add_debit( { amount => 10, interface => 'opac', type => 'ACCOUNT' } );
945
946
        my $item = $builder->build_sample_item( { library => $library->id } );
947
948
        my $post_data = {
949
            patron_id         => $patron->id,
950
            pickup_library_id => $item->holdingbranch,
951
            item_id           => $item->id
952
        };
953
954
        ## NOTICE: this tests rely on 'expired' being checked before 'debt_limit' in Koha::Patron->can_place_holds
955
956
        # patron meets 'expired' and 'debt_limit'
957
        $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
958
            ->status_is( 409, "Expected error related to 'expired'" )
959
            ->json_is( { error => "Hold cannot be placed. Reason: expired", error_code => 'expired' } );
960
961
        # patron meets 'expired' AND 'debt_limit'
962
        $t->post_ok( "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'expired' } => json => $post_data )
963
            ->status_is( 409, "Expected error related to 'debt_limit'" )
964
            ->json_is( { error => "Hold cannot be placed. Reason: debt_limit", error_code => 'debt_limit' } );
965
966
        $t->post_ok(
967
            "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'expired,debt_limit' } => json => $post_data )
968
            ->status_is( 201, "Override works for both 'expired' and 'debt_limit'" );
969
    };
970
};
971
720
subtest 'pickup_locations() tests' => sub {
972
subtest 'pickup_locations() tests' => sub {
721
973
722
    plan tests => 15;
974
    plan tests => 15;
(-)a/t/db_dependent/api/v1/two_factor_auth.t (-1 / +2 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 2;
20
use Test::More tests => 3;
21
use Test::NoWarnings;
21
use Test::Mojo;
22
use Test::Mojo;
22
use Test::MockModule;
23
use Test::MockModule;
23
24
(-)a/t/db_dependent/selenium/basic_workflow.t (-2 / +5 lines)
Lines 32-37 Link Here
32
32
33
use Modern::Perl;
33
use Modern::Perl;
34
34
35
use Test::More tests => 23;
36
use Test::NoWarnings;
37
35
use Time::HiRes qw(gettimeofday);
38
use Time::HiRes qw(gettimeofday);
36
use POSIX       qw(strftime);
39
use POSIX       qw(strftime);
37
use C4::Context;
40
use C4::Context;
Lines 39-45 use C4::Biblio qw( AddBiblio ); Link Here
39
42
40
use Koha::CirculationRules;
43
use Koha::CirculationRules;
41
44
42
use Test::More tests => 22;
43
use MARC::Record;
45
use MARC::Record;
44
use MARC::Field;
46
use MARC::Field;
45
47
Lines 326-331 sub cleanup { Link Here
326
sub time_diff {
328
sub time_diff {
327
    my $lib = shift;
329
    my $lib = shift;
328
    my $now = gettimeofday;
330
    my $now = gettimeofday;
329
    warn "CP $lib = " . sprintf( "%.2f", $now - $prev_time ) . "\n";
331
332
    #warn "CP $lib = " . sprintf( "%.2f", $now - $prev_time ) . "\n";
330
    $prev_time = $now;
333
    $prev_time = $now;
331
}
334
}
(-)a/t/db_dependent/selenium/opac_ill_requests.t (-4 / +5 lines)
Lines 53-66 SKIP: { Link Here
53
                'Correctly in search results page'
53
                'Correctly in search results page'
54
            );
54
            );
55
55
56
            # Get the second li, the first one is "Make a purchase suggestion"
56
            is(
57
            is(
57
                $driver->find_element('//div[@class="suggestion"]/ul/li')->get_text,
58
                $driver->find_element('//div[@class="suggestion"]/ul/li[2]')->get_text,
58
                'Make an interlibrary loan request',
59
                'Make an interlibrary loan request',
59
                'Placing an ILL request through the OPAC is allowed',
60
                'Placing an ILL request through the OPAC is allowed',
60
            );
61
            );
61
62
62
            # Clicking on the search results page link works
63
            # Clicking on the search results page link works
63
            $driver->find_element('//div[@class="suggestion"]/ul/li/a')->click;
64
            $driver->find_element('//div[@class="suggestion"]/ul/li[2]/a')->click;
64
            is(
65
            is(
65
                $driver->find_element('(//nav[@id="breadcrumbs"]/ol/li)[last()]')->get_text,
66
                $driver->find_element('(//nav[@id="breadcrumbs"]/ol/li)[last()]')->get_text,
66
                'New interlibrary loan request',
67
                'New interlibrary loan request',
Lines 94-101 SKIP: { Link Here
94
95
95
            is(
96
            is(
96
                scalar @{$link_exists},
97
                scalar @{$link_exists},
97
                0,
98
                2,
98
                'Search page - Place ILL request link should be absent. '
99
                'Search page - Place ILL request link should be present. '
99
            );
100
            );
100
101
101
            # Visiting the create request page directly does not work
102
            # Visiting the create request page directly does not work
(-)a/t/db_dependent/selenium/regressions.t (-3 / +8 lines)
Lines 17-27 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
use utf8;
19
use utf8;
20
use Encode qw(encode_utf8);
20
21
21
use C4::Context;
22
use C4::Context;
22
23
23
use Test::More;
24
use Test::More;
24
use Test::MockModule;
25
use Test::MockModule;
26
use Test::NoWarnings;
25
27
26
use C4::Context;
28
use C4::Context;
27
use C4::Biblio      qw( AddBiblio );
29
use C4::Biblio      qw( AddBiblio );
Lines 36-42 eval { require Selenium::Remote::Driver; }; Link Here
36
if ($@) {
38
if ($@) {
37
    plan skip_all => "Selenium::Remote::Driver is needed for selenium tests.";
39
    plan skip_all => "Selenium::Remote::Driver is needed for selenium tests.";
38
} else {
40
} else {
39
    plan tests => 9;
41
    plan tests => 10;
40
}
42
}
41
43
42
my $s = t::lib::Selenium->new;
44
my $s = t::lib::Selenium->new;
Lines 354-360 subtest 'Encoding in session variables' => sub { Link Here
354
            is(
356
            is(
355
                $driver->find_element('//span[@class="logged-in-branch-name"]')->get_text(),
357
                $driver->find_element('//span[@class="logged-in-branch-name"]')->get_text(),
356
                $branchname,
358
                $branchname,
357
                sprintf( "logged-in-branch-name set - SessionStorage=%s, branchname=%s", $SessionStorage, $branchname )
359
                sprintf(
360
                    "logged-in-branch-name set - SessionStorage=%s, branchname=%s", $SessionStorage,
361
                    encode_utf8($branchname)
362
                )
358
            );
363
            );
359
364
360
            $driver->find_element('//input[@id="barcode"]')->send_keys( $item->barcode );
365
            $driver->find_element('//input[@id="barcode"]')->send_keys( $item->barcode );
Lines 372-378 subtest 'Encoding in session variables' => sub { Link Here
372
                $branchname,
377
                $branchname,
373
                sprintf(
378
                sprintf(
374
                    "'Checked out from' column should contain the branchname - SessionStorage=%s, branchname=%s",
379
                    "'Checked out from' column should contain the branchname - SessionStorage=%s, branchname=%s",
375
                    $SessionStorage, $branchname
380
                    $SessionStorage, encode_utf8($branchname)
376
                )
381
                )
377
            );
382
            );
378
383
(-)a/t/db_dependent/www/auth_values_input_www.t (-7 / +4 lines)
Lines 19-26 use Modern::Perl; Link Here
19
19
20
use utf8;
20
use utf8;
21
use Test::More;    #See plan tests => \d+ below
21
use Test::More;    #See plan tests => \d+ below
22
use Test::NoWarnings;
22
use Test::WWW::Mechanize;
23
use Test::WWW::Mechanize;
23
use XML::Simple;
24
use JSON;
24
use JSON;
25
use File::Basename;
25
use File::Basename;
26
use File::Spec;
26
use File::Spec;
Lines 32-42 use Koha::AuthorisedValueCategories; Link Here
32
32
33
my $testdir = File::Spec->rel2abs( dirname(__FILE__) );
33
my $testdir = File::Spec->rel2abs( dirname(__FILE__) );
34
34
35
my $koha_conf = $ENV{KOHA_CONF};
35
my $user     = $ENV{KOHA_USER} || 'koha';
36
my $xml       = XMLin($koha_conf);
36
my $password = $ENV{KOHA_PASS} || 'koha';
37
38
my $user     = $ENV{KOHA_USER} || $xml->{config}->{user};
39
my $password = $ENV{KOHA_PASS} || $xml->{config}->{pass};
40
my $intranet = $ENV{KOHA_INTRANET_URL};
37
my $intranet = $ENV{KOHA_INTRANET_URL};
41
38
42
eval { use C4::Context; };
39
eval { use C4::Context; };
Lines 45-51 if ($@) { Link Here
45
} elsif ( not defined $intranet ) {
42
} elsif ( not defined $intranet ) {
46
    plan skip_all => "Tests skip. You must set env. variable KOHA_INTRANET_URL to do tests\n";
43
    plan skip_all => "Tests skip. You must set env. variable KOHA_INTRANET_URL to do tests\n";
47
} else {
44
} else {
48
    plan tests => 30;
45
    plan tests => 31;
49
}
46
}
50
47
51
my $dbh = C4::Context->dbh;
48
my $dbh = C4::Context->dbh;
(-)a/t/db_dependent/www/batch.t (-7 / +4 lines)
Lines 20-27 use Modern::Perl; Link Here
20
20
21
use utf8;
21
use utf8;
22
use Test::More;    #See plan tests => \d+ below
22
use Test::More;    #See plan tests => \d+ below
23
use Test::NoWarnings;
23
use Test::WWW::Mechanize;
24
use Test::WWW::Mechanize;
24
use XML::Simple;
25
use JSON;
25
use JSON;
26
use File::Basename;
26
use File::Basename;
27
use File::Spec;
27
use File::Spec;
Lines 31-39 use Koha::BackgroundJobs; Link Here
31
31
32
my $testdir = File::Spec->rel2abs( dirname(__FILE__) );
32
my $testdir = File::Spec->rel2abs( dirname(__FILE__) );
33
33
34
my $koha_conf = $ENV{KOHA_CONF};
35
my $xml       = XMLin($koha_conf);
36
37
use C4::Context;
34
use C4::Context;
38
my $marcflavour = C4::Context->preference('marcflavour') || 'MARC21';
35
my $marcflavour = C4::Context->preference('marcflavour') || 'MARC21';
39
36
Lines 42-49 my $file = Link Here
42
    ? "$testdir/data/unimarcrecord.mrc"
39
    ? "$testdir/data/unimarcrecord.mrc"
43
    : "$testdir/data/marc21record.mrc";
40
    : "$testdir/data/marc21record.mrc";
44
41
45
my $user     = $ENV{KOHA_USER} || $xml->{config}->{user};
42
my $user     = $ENV{KOHA_USER} || 'koha';
46
my $password = $ENV{KOHA_PASS} || $xml->{config}->{pass};
43
my $password = $ENV{KOHA_PASS} || 'koha';
47
my $intranet = $ENV{KOHA_INTRANET_URL};
44
my $intranet = $ENV{KOHA_INTRANET_URL};
48
45
49
if ( not defined $intranet ) {
46
if ( not defined $intranet ) {
Lines 52-58 if ( not defined $intranet ) { Link Here
52
        . "KOHA_CONF set, you must also set KOHA_USER and KOHA_PASS for "
49
        . "KOHA_CONF set, you must also set KOHA_USER and KOHA_PASS for "
53
        . "your username and password";
50
        . "your username and password";
54
} else {
51
} else {
55
    plan tests => 24;
52
    plan tests => 25;
56
}
53
}
57
54
58
$intranet =~ s#/$##;
55
$intranet =~ s#/$##;
(-)a/t/db_dependent/www/history.t (-7 / +4 lines)
Lines 18-34 Link Here
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use utf8;
20
use utf8;
21
use XML::Simple;
22
use Encode;
21
use Encode;
23
22
24
use Test::More;    #See plan tests => \d+ below
23
use Test::More;    #See plan tests => \d+ below
24
use Test::NoWarnings;
25
use Test::WWW::Mechanize;
25
use Test::WWW::Mechanize;
26
26
27
my $koha_conf = $ENV{KOHA_CONF};
27
my $user     = $ENV{KOHA_USER} || 'koha';
28
my $xml       = XMLin($koha_conf);
28
my $password = $ENV{KOHA_PASS} || 'koha';
29
30
my $user     = $ENV{KOHA_USER} || $xml->{config}->{user};
31
my $password = $ENV{KOHA_PASS} || $xml->{config}->{pass};
32
my $intranet = $ENV{KOHA_INTRANET_URL};
29
my $intranet = $ENV{KOHA_INTRANET_URL};
33
30
34
eval { use C4::Context; };
31
eval { use C4::Context; };
Lines 37-43 if ($@) { Link Here
37
} elsif ( not defined $intranet ) {
34
} elsif ( not defined $intranet ) {
38
    plan skip_all => "Tests skip. You must set env. variable KOHA_INTRANET_URL to do tests\n";
35
    plan skip_all => "Tests skip. You must set env. variable KOHA_INTRANET_URL to do tests\n";
39
} else {
36
} else {
40
    plan tests => 4;
37
    plan tests => 5;
41
}
38
}
42
39
43
$intranet =~ s#/$##;
40
$intranet =~ s#/$##;
(-)a/t/db_dependent/www/search_utf8.t (-4 / +4 lines)
Lines 19-27 use Modern::Perl; Link Here
19
19
20
use utf8;
20
use utf8;
21
use Test::More;    #See plan tests => \d+ below
21
use Test::More;    #See plan tests => \d+ below
22
use Test::NoWarnings;
22
use Test::WWW::Mechanize;
23
use Test::WWW::Mechanize;
23
use Data::Dumper;
24
use Data::Dumper;
24
use XML::Simple;
25
use File::Basename qw(dirname );
25
use File::Basename qw(dirname );
26
use POSIX;
26
use POSIX;
27
use Encode;
27
use Encode;
Lines 64-70 if ( not $intranet ) { Link Here
64
elsif ( not $opac ) {
64
elsif ( not $opac ) {
65
    plan skip_all => "Tests skip. You must set env. variable KOHA_OPAC_URL to do tests\n";
65
    plan skip_all => "Tests skip. You must set env. variable KOHA_OPAC_URL to do tests\n";
66
} else {
66
} else {
67
    plan tests => 80;
67
    plan tests => 81;
68
}
68
}
69
69
70
$intranet =~ s#/$##;
70
$intranet =~ s#/$##;
Lines 94-101 if ( not defined $mock_zebra->{indexer_pid} ) { Link Here
94
our $agent = Test::WWW::Mechanize->new( autocheck => 1 );
94
our $agent = Test::WWW::Mechanize->new( autocheck => 1 );
95
$agent->get_ok( "$intranet/cgi-bin/koha/mainpage.pl", 'connect to intranet' );
95
$agent->get_ok( "$intranet/cgi-bin/koha/mainpage.pl", 'connect to intranet' );
96
$agent->form_name('loginform');
96
$agent->form_name('loginform');
97
$agent->field( 'login_userid',   $ENV{KOHA_PASS} );
97
$agent->field( 'login_userid',   $ENV{KOHA_PASS} || 'koha' );
98
$agent->field( 'login_password', $ENV{KOHA_USER} );
98
$agent->field( 'login_password', $ENV{KOHA_USER} || 'koha' );
99
$agent->field( 'branch',         '' );
99
$agent->field( 'branch',         '' );
100
$agent->click_ok( '', 'login to staff interface' );
100
$agent->click_ok( '', 'login to staff interface' );
101
101
(-)a/t/dummy.t (-4 lines)
Lines 1-4 Link Here
1
# Dummy test until Test::Harness or similar
2
# is used by the other tests to check deps.
3
use Modern::Perl;
4
print "1..1\nok 1\n";
(-)a/t/lib/TestBuilder.pm (-3 / +4 lines)
Lines 658-666 sub _gen_default_values { Link Here
658
            reservefee   => 0,
658
            reservefee   => 0,
659
659
660
            # Not X, used for statistics
660
            # Not X, used for statistics
661
            category_type           => sub { return [qw( A C S I P )]->[ int( rand(5) ) ] },
661
            category_type                 => sub { return [qw( A C S I P )]->[ int( rand(5) ) ] },
662
            min_password_length     => undef,
662
            min_password_length           => undef,
663
            require_strong_password => undef,
663
            require_strong_password       => undef,
664
            BlockExpiredPatronOpacActions => q{follow_syspref_BlockExpiredPatronOpacActions},
664
        },
665
        },
665
        Branch => {
666
        Branch => {
666
            pickup_location => 0,
667
            pickup_location => 0,
(-)a/tools/batchMod.pl (-4 / +17 lines)
Lines 33-38 use Koha::Database; Link Here
33
use Koha::Exception;
33
use Koha::Exception;
34
use Koha::Biblios;
34
use Koha::Biblios;
35
use Koha::Items;
35
use Koha::Items;
36
use Koha::Object;
36
use Koha::Patrons;
37
use Koha::Patrons;
37
use Koha::Item::Attributes;
38
use Koha::Item::Attributes;
38
use Koha::BackgroundJob::BatchDeleteItem;
39
use Koha::BackgroundJob::BatchDeleteItem;
Lines 112-124 if ( $op eq "cud-action" ) { Link Here
112
113
113
    } else {    # modification
114
    } else {    # modification
114
115
115
        my @item_columns = Koha::Items->columns;
116
        my $items_info = Koha::Items->_resultset->result_source->columns_info;
116
117
117
        my $new_item_data;
118
        my $new_item_data;
118
        my ($columns_with_regex);
119
        my ($columns_with_regex);
119
        my @subfields_to_blank = $input->multi_param('disable_input');
120
        my @subfields_to_blank = $input->multi_param('disable_input');
120
        my @more_subfields     = $input->multi_param("items.more_subfields_xml");
121
        my @more_subfields     = $input->multi_param("items.more_subfields_xml");
121
        for my $item_column (@item_columns) {
122
        for my $item_column ( keys %$items_info ) {
122
            my @attributes       = ($item_column);
123
            my @attributes       = ($item_column);
123
            my $cgi_param_prefix = 'items.';
124
            my $cgi_param_prefix = 'items.';
124
            if ( $item_column eq 'more_subfields_xml' ) {
125
            if ( $item_column eq 'more_subfields_xml' ) {
Lines 136-143 if ( $op eq "cud-action" ) { Link Here
136
137
137
                if ( grep { $cgi_var_name eq $_ } @subfields_to_blank ) {
138
                if ( grep { $cgi_var_name eq $_ } @subfields_to_blank ) {
138
139
139
                    # Empty this column
140
                    # Empty this column, check nullable and data_type
140
                    $new_item_data->{$attr} = undef;
141
                    next if !$items_info->{$attr};           # skip this weird case
142
                    if ( $items_info->{$attr}->{is_nullable} ) {
143
                        $new_item_data->{$attr} = undef;
144
                    } elsif ( Koha::Object::_numeric_column_type( $items_info->{$attr}->{data_type} ) ) {
145
                        $new_item_data->{$attr} = 0;
146
                    } elsif ( Koha::Object::_date_or_datetime_column_type( $items_info->{$attr}->{data_type} ) ) {
147
148
                        # TODO Currently, we do not have NOT NULL date(time) columns in items
149
                        warn "batchMod: $attr must be blanked, but does not accept NULL values";
150
                        next;
151
                    } else {
152
                        $new_item_data->{$attr} = q{};
153
                    }
141
                } elsif ( my $regex_search = $input->param( $cgi_var_name . '_regex_search' ) ) {
154
                } elsif ( my $regex_search = $input->param( $cgi_var_name . '_regex_search' ) ) {
142
                    $columns_with_regex->{$attr} = {
155
                    $columns_with_regex->{$attr} = {
143
                        search    => $regex_search,
156
                        search    => $regex_search,
(-)a/xt/api.t (-1 / +50 lines)
Lines 14-28 Link Here
14
14
15
use Modern::Perl;
15
use Modern::Perl;
16
16
17
use Test::More tests => 6;
17
use Test::More tests => 8;
18
use Test::NoWarnings;
18
19
19
use Test::Mojo;
20
use Test::Mojo;
20
use Data::Dumper;
21
use Data::Dumper;
22
use Koha::Database;
21
23
22
use FindBin();
24
use FindBin();
23
use IPC::Cmd        qw(can_run);
25
use IPC::Cmd        qw(can_run);
24
use List::MoreUtils qw(any);
26
use List::MoreUtils qw(any);
25
use File::Slurp     qw(read_file);
27
use File::Slurp     qw(read_file);
28
use YAML::XS        qw(LoadFile);
26
29
27
my $t    = Test::Mojo->new('Koha::REST::V1');
30
my $t    = Test::Mojo->new('Koha::REST::V1');
28
my $spec = $t->get_ok( '/api/v1/', 'Correctly fetched the spec' )->tx->res->json;
31
my $spec = $t->get_ok( '/api/v1/', 'Correctly fetched the spec' )->tx->res->json;
Lines 194-196 subtest 'POST (201) have location header' => sub { Link Here
194
        }
197
        }
195
    }
198
    }
196
};
199
};
200
201
subtest 'maxlength + enum' => sub {
202
    my $def_map = {
203
204
        # api def => schema
205
        item            => 'Item',
206
        library         => 'Branch',
207
        patron          => 'Borrower',
208
        patron_category => 'Category',
209
    };
210
    plan tests => scalar keys %$def_map;
211
    my $schema = Koha::Database->new->schema;
212
    while ( my ( $def, $dbic_src ) = each %$def_map ) {
213
        my @failures;
214
        my $definition   = LoadFile("api/v1/swagger/definitions/$def.yaml");
215
        my $source       = $schema->source($dbic_src);
216
        my $object_class = Koha::Object::_get_object_class( $source->result_class );
217
        eval "require $object_class";
218
        my $koha_object = $object_class->new;
219
        my $api_mapping = $koha_object->to_api_mapping;
220
        my $reversed_api_mapping =
221
            { reverse map { defined $api_mapping->{$_} ? ( $_ => $api_mapping->{$_} ) : () } keys %$api_mapping };
222
223
        my $db_columns = $koha_object->_result->columns_info;
224
        while ( my ( $db_col, $column_info ) = each %{$db_columns} ) {
225
            my $api_attr   = $api_mapping->{$db_col} || $db_col;
226
            my $properties = $definition->{properties}->{$api_attr};
227
228
            next unless $properties;
229
230
            next unless $column_info->{size};
231
232
            next if ref( $column_info->{size} ) eq 'ARRAY';    # decimal # FIXME Could have a test for this as well
233
234
            next
235
                if $properties->{enum}
236
                ; # FIXME This is not fully correct, we might want to make sure enum is set for both DB and spec. eg. now checkprevcheckout is enum only for the api spec
237
238
            if ( !exists $properties->{maxLength} || $column_info->{size} != $properties->{maxLength} ) {
239
                push @failures, sprintf "%s.%s should have maxLength=%s in api spec", $def, $api_attr,
240
                    $column_info->{size};
241
            }
242
        }
243
        is( scalar(@failures), 0, "maxLength tests for $def" ) or diag Dumper @failures;
244
    }
245
};
(-)a/xt/author/codespell.t (-7 / +6 lines)
Lines 3-26 use Modern::Perl; Link Here
3
use Test::PerlTidy;
3
use Test::PerlTidy;
4
use Test::More;
4
use Test::More;
5
5
6
use Koha::Devel::Files;
7
6
my $codespell_version = qx{codespell --version};
8
my $codespell_version = qx{codespell --version};
7
chomp $codespell_version;
9
chomp $codespell_version;
8
$codespell_version =~ s/-.*$//;
10
$codespell_version =~ s/-.*$//;
9
if ( ( $codespell_version =~ s/\.//gr ) < 220 ) {    # if codespell < 2.2.0
11
if ( ( $codespell_version =~ s/\.//gr ) < 220 ) {    # if codespell < 2.2.0
10
    plan skip_all => "codespell version $codespell_version too low, need at least 2.2.0";
12
    plan skip_all => "codespell version $codespell_version too low, need at least 2.2.0";
11
}
13
}
14
my $dev_files = Koha::Devel::Files->new( { context => 'codespell' } );
12
my @files;
15
my @files;
13
push @files,
16
push @files, $dev_files->ls_perl_files;
14
    qx{git ls-files '*.pl' '*.PL' '*.pm' '*.t' ':(exclude)installer/data/mysql/updatedatabase.pl' ':(exclude)installer/data/mysql/update22to30.pl' ':(exclude)installer/data/mysql/db_revs/241200035.pl' ':(exclude)misc/cronjobs/build_browser_and_cloud.pl'};
17
push @files, $dev_files->ls_tt_files;
15
push @files, qx{git ls-files svc opac/svc};          # Files without extension
18
push @files, $dev_files->ls_js_files;
16
push @files, qx{git ls-files '*.tt' '*.inc'};
17
push @files,
18
    qx{git ls-files '*.js' '*.ts' '*.vue' ':(exclude)koha-tmpl/intranet-tmpl/lib' ':(exclude)koha-tmpl/intranet-tmpl/js/Gettext.js' ':(exclude)koha-tmpl/opac-tmpl/lib' ':(exclude)koha-tmpl/opac-tmpl/bootstrap/js/Gettext.js'};
19
19
20
plan tests => scalar @files;
20
plan tests => scalar @files;
21
21
22
for my $file (@files) {
22
for my $file (@files) {
23
    chomp $file;
24
    my $output = qx{codespell -d --ignore-words .codespell-ignore $file};
23
    my $output = qx{codespell -d --ignore-words .codespell-ignore $file};
25
    chomp $output;
24
    chomp $output;
26
    is( $output, q{} );
25
    is( $output, q{} );
(-)a/xt/author/pod_checker.t (-4 / +3 lines)
Lines 4-14 use Modern::Perl; Link Here
4
use Test::More;
4
use Test::More;
5
use Test::NoWarnings;
5
use Test::NoWarnings;
6
use Pod::Checker;
6
use Pod::Checker;
7
use Koha::Devel::Files;
7
8
8
my @files;
9
my $dev_files = Koha::Devel::Files->new;
9
push @files, qx{git ls-files '*.pl' '*.PL' '*.pm' '*.t'};
10
my @files     = $dev_files->ls_perl_files;
10
push @files, qx{git ls-files svc opac/svc};                 # Files without extension
11
chomp for @files;
12
11
13
plan tests => scalar @files + 1;
12
plan tests => scalar @files + 1;
14
13
(-)a/xt/author/podcorrectness.t (-4 / +4 lines)
Lines 15-23 use Modern::Perl; Link Here
15
use Test::More;
15
use Test::More;
16
use Test::Pod;
16
use Test::Pod;
17
17
18
my @files;
18
use Koha::Devel::Files;
19
push @files, qx{git ls-files '*.pl' '*.PL' '*.pm' '*.t'};
19
20
push @files, qx{git ls-files svc opac/svc};                 # Files without extension
20
my $dev_files = Koha::Devel::Files->new;
21
chomp for @files;
21
my @files     = $dev_files->ls_perl_files;
22
22
23
all_pod_files_ok(@files);
23
all_pod_files_ok(@files);
(-)a/xt/find-license-problems.t (-23 / +24 lines)
Lines 25-58 Link Here
25
25
26
use Modern::Perl;
26
use Modern::Perl;
27
use Test::More;
27
use Test::More;
28
use Test::NoWarnings;
28
29
29
use File::Spec;
30
my @files = map {
30
use File::Find;
31
    chomp;
32
    my $name = $_;
33
    !(     $name =~ m{^koha-tmpl/}
34
        || $name =~ m{\.(gif|jpg|odt|ogg|pdf|png|po|psd|svg|swf|zip)$}
35
        || $name =~ m{xt/find-license-problems|xt/fix-old-fsf-address|misc/translator/po2json}
36
        || $name =~ m[t/mock_templates/intranet-tmpl/prog]
37
        || !-f $name )
38
        ? $_
39
        : ()
40
} `git ls-tree -r HEAD --name-only`;    # only files part of git
31
41
32
my @files;
42
plan tests => scalar(@files) + 1;
33
34
sub wanted {
35
    my $name = $File::Find::name;
36
    push @files, $name
37
        unless $name =~ /\/(\.git|koha-tmpl|node_modules|swagger-ui)(\/.*)?$/
38
        || $name     =~ /\.(gif|jpg|odt|ogg|pdf|png|po|psd|svg|swf|zip|patch)$/
39
        || $name     =~ m[(xt/find-license-problems|xt/fix-old-fsf-address|misc/translator/po2json)]
40
        || !-f $name;
41
}
42
43
find( { wanted => \&wanted, no_chdir => 1 }, File::Spec->curdir() );
44
43
45
foreach my $name (@files) {
44
foreach my $name (@files) {
46
    open( my $fh, '<', $name ) || die "cannot open file $name $!";
45
    open( my $fh, '<', $name ) || die "cannot open file $name $!";
47
    my (
46
    my (
48
        $hascopyright,  $hasgpl, $hasv3, $hasorlater, $haslinktolicense,
47
        $hasgpl,        $hasv3, $hasorlater, $haslinktolicense,
49
        $hasfranklinst, $is_not_us
48
        $hasfranklinst, $is_not_us
50
    ) = (0) x 7;
49
    ) = (0) x 7;
51
    while ( my $line = <$fh> ) {
50
    while ( my $line = <$fh> ) {
52
        $hascopyright = 1 if ( $line =~ /^(#|--)?\s*Copyright.*\d\d/ );
51
        $hasgpl     = 1 if ( $line =~ /GNU General Public License/ );
53
        $hasgpl       = 1 if ( $line =~ /GNU General Public License/ );
52
        $hasv3      = 1 if ( $line =~ /either version 3/ );
54
        $hasv3        = 1 if ( $line =~ /either version 3/ );
53
        $hasorlater = 1
55
        $hasorlater   = 1
56
            if ( $line =~ /any later version/
54
            if ( $line =~ /any later version/
57
            || $line =~ /at your option/ );
55
            || $line =~ /at your option/ );
58
        $haslinktolicense = 1 if $line =~ m|http://www\.gnu\.org/licenses|;
56
        $haslinktolicense = 1 if $line =~ m|http://www\.gnu\.org/licenses|;
Lines 60-72 foreach my $name (@files) { Link Here
60
        $is_not_us        = 1 if $line =~ m|This file is part of the Zebra server|;
58
        $is_not_us        = 1 if $line =~ m|This file is part of the Zebra server|;
61
    }
59
    }
62
    close $fh;
60
    close $fh;
63
    next unless $hascopyright;
61
64
    next if $is_not_us;
62
    if ( $is_not_us || !$hasgpl ) {
65
    is( $hasgpl && $hasv3 && $hasorlater && $haslinktolicense && !$hasfranklinst, 1 )
63
        pass();
64
        next;
65
    }
66
67
    ok( $hasgpl && $hasv3 && $hasorlater && $haslinktolicense && !$hasfranklinst )
66
        or diag(
68
        or diag(
67
        sprintf
69
        sprintf
68
            "File %s has wrong copyright: hasgpl=%s, hasv3=%s, hasorlater=%s, haslinktolicense=%s, hasfranklinst=%s",
70
            "File %s has wrong copyright: hasgpl=%s, hasv3=%s, hasorlater=%s, haslinktolicense=%s, hasfranklinst=%s",
69
        $name, $hasgpl, $hasv3, $hasorlater, $haslinktolicense, $hasfranklinst
71
        $name, $hasgpl, $hasv3, $hasorlater, $haslinktolicense, $hasfranklinst
70
        );
72
        );
71
}
73
}
72
done_testing;
(-)a/xt/find-missing-csrf.t (-8 / +3 lines)
Lines 22-41 use Test::More tests => 2; Link Here
22
use File::Slurp;
22
use File::Slurp;
23
use Data::Dumper;
23
use Data::Dumper;
24
24
25
my @files;
25
use Koha::Devel::Files;
26
26
27
# OPAC
27
my $dev_files = Koha::Devel::Files->new;
28
push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.tt'`;
28
my @files     = $dev_files->ls_tt_files;
29
push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.inc'`;
30
29
31
# Staff
32
push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.tt'`;
33
push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.inc'`;
34
ok( @files > 0, 'We should test something' );
30
ok( @files > 0, 'We should test something' );
35
31
36
my @errors;
32
my @errors;
37
for my $file (@files) {
33
for my $file (@files) {
38
    chomp $file;
39
    my @e = check_csrf_in_forms($file);
34
    my @e = check_csrf_in_forms($file);
40
    push @errors, sprintf "%s:%s", $file, join( ",", @e ) if @e;
35
    push @errors, sprintf "%s:%s", $file, join( ",", @e ) if @e;
41
}
36
}
(-)a/xt/find-missing-filters.t (-9 / +3 lines)
Lines 20-40 use Test::More tests => 2; Link Here
20
use File::Slurp qw( read_file );
20
use File::Slurp qw( read_file );
21
use Data::Dumper;
21
use Data::Dumper;
22
use t::lib::QA::TemplateFilters;
22
use t::lib::QA::TemplateFilters;
23
use Koha::Devel::Files;
23
24
24
my @files;
25
my $dev_files = Koha::Devel::Files->new;
26
my @files     = $dev_files->ls_tt_files;
25
27
26
# OPAC
27
push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.tt'`;
28
push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.inc'`;
29
30
# Staff
31
push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.tt'`;
32
push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.inc'`;
33
ok( @files > 0, 'We should test something' );
28
ok( @files > 0, 'We should test something' );
34
29
35
my @errors;
30
my @errors;
36
for my $file (@files) {
31
for my $file (@files) {
37
    chomp $file;
38
    my $content = read_file($file);
32
    my $content = read_file($file);
39
    my @e       = t::lib::QA::TemplateFilters::missing_filters($content);
33
    my @e       = t::lib::QA::TemplateFilters::missing_filters($content);
40
    push @errors, { file => $file, errors => \@e } if @e;
34
    push @errors, { file => $file, errors => \@e } if @e;
(-)a/xt/find-missing-op-in-forms.t (-9 / +3 lines)
Lines 22-41 use Test::More tests => 2; Link Here
22
use File::Slurp;
22
use File::Slurp;
23
use Data::Dumper;
23
use Data::Dumper;
24
24
25
my @files;
25
use Koha::Devel::Files;
26
26
27
# OPAC
27
my $dev_files = Koha::Devel::Files->new;
28
push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.tt'`;
28
my @files     = $dev_files->ls_tt_files;
29
push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.inc'`;
30
31
# Staff
32
push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.tt'`;
33
push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.inc'`;
34
ok( @files > 0, 'We should test something' );
29
ok( @files > 0, 'We should test something' );
35
30
36
my @errors;
31
my @errors;
37
for my $file (@files) {
32
for my $file (@files) {
38
    chomp $file;
39
    my @e = catch_missing_op($file);
33
    my @e = catch_missing_op($file);
40
    push @errors, sprintf "%s:%s", $file, join( ",", @e ) if @e;
34
    push @errors, sprintf "%s:%s", $file, join( ",", @e ) if @e;
41
}
35
}
(-)a/xt/perltidy.t (-4 / +4 lines)
Lines 3-15 use Modern::Perl; Link Here
3
use Test::PerlTidy;
3
use Test::PerlTidy;
4
use Test::More;
4
use Test::More;
5
5
6
my @files;
6
use Koha::Devel::Files;
7
push @files, qx{git ls-files '*.pl' '*.PL' '*.pm' '*.t' ':(exclude)Koha/Schema/Result' ':(exclude)Koha/Schema.pm'};
7
8
push @files, qx{git ls-files svc opac/svc};    # Files without extension
8
my $dev_files = Koha::Devel::Files->new( { context => 'tidy' } );
9
my @files     = $dev_files->ls_perl_files;
9
10
10
plan tests => scalar @files;
11
plan tests => scalar @files;
11
12
12
for my $file (@files) {
13
for my $file (@files) {
13
    chomp $file;
14
    ok( Test::PerlTidy::is_file_tidy($file) );
14
    ok( Test::PerlTidy::is_file_tidy($file) );
15
}
15
}
(-)a/xt/pl_valid.t (-14 / +3 lines)
Lines 24-40 use Pod::Checker; Link Here
24
use Parallel::ForkManager;
24
use Parallel::ForkManager;
25
use Sys::CPU;
25
use Sys::CPU;
26
26
27
my @files;
27
use Koha::Devel::Files;
28
push @files, qx{git ls-files '*.pl' '*.PL' '*.pm' '*.t'};
29
push @files, qx{git ls-files svc opac/svc};                 # Files without extension
30
chomp for @files;
31
28
32
my @exceptions = qw(
29
my $dev_files = Koha::Devel::Files->new( { context => 'valid' } );
33
    Koha/Account/Credit.pm
30
my @files     = $dev_files->ls_perl_files;
34
    Koha/Account/Debit.pm
35
    Koha/Old/Hold.pm
36
    misc/translator/TmplTokenizer.pm
37
);
38
31
39
my $ncpu;
32
my $ncpu;
40
if ( $ENV{KOHA_PROVE_CPUS} ) {
33
if ( $ENV{KOHA_PROVE_CPUS} ) {
Lines 48-57 my $pm = Parallel::ForkManager->new($ncpu); Link Here
48
plan tests => scalar(@files) + 1;
41
plan tests => scalar(@files) + 1;
49
42
50
for my $file (@files) {
43
for my $file (@files) {
51
    if ( grep { $file eq $_ } @exceptions ) {
52
        pass("$file is skipped - exception");
53
        next;
54
    }
55
    $pm->start and next;
44
    $pm->start and next;
56
    my $output = `perl -cw '$file' 2>&1`;
45
    my $output = `perl -cw '$file' 2>&1`;
57
    chomp $output;
46
    chomp $output;
(-)a/xt/single_quotes.t (-8 / +3 lines)
Lines 21-40 use Modern::Perl; Link Here
21
use Test::More tests => 2;
21
use Test::More tests => 2;
22
use File::Slurp qw( read_file );
22
use File::Slurp qw( read_file );
23
23
24
my @files;
24
use Koha::Devel::Files;
25
25
26
# OPAC
26
my $dev_files = Koha::Devel::Files->new;
27
push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.tt'`;
27
my @files     = $dev_files->ls_tt_files;
28
push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.inc'`;
29
28
30
# Staff
31
push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.tt'`;
32
push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.inc'`;
33
ok( @files > 0, 'We should test something' );
29
ok( @files > 0, 'We should test something' );
34
30
35
my @errors;
31
my @errors;
36
for my $file (@files) {
32
for my $file (@files) {
37
    chomp $file;
38
    my @lines = sort grep /\_\(\'/, read_file($file);
33
    my @lines = sort grep /\_\(\'/, read_file($file);
39
    push @errors, { name => $file, lines => \@lines } if @lines;
34
    push @errors, { name => $file, lines => \@lines } if @lines;
40
}
35
}
(-)a/xt/tt_tidy.t (-2 / +4 lines)
Lines 23-29 use Test::Strict; Link Here
23
use Parallel::ForkManager;
23
use Parallel::ForkManager;
24
use Sys::CPU;
24
use Sys::CPU;
25
25
26
my @tt_files = qx{git ls-files '*.tt' '*.inc'};
26
use Koha::Devel::Files;
27
28
my $dev_files = Koha::Devel::Files->new( { context => 'tidy' } );
29
my @tt_files  = $dev_files->ls_tt_files;
27
30
28
$Test::Strict::TEST_STRICT = 0;
31
$Test::Strict::TEST_STRICT = 0;
29
32
Lines 39-45 my $pm = Parallel::ForkManager->new($ncpu); Link Here
39
foreach my $filepath (@tt_files) {
42
foreach my $filepath (@tt_files) {
40
    $pm->start and next;
43
    $pm->start and next;
41
44
42
    chomp $filepath;
43
    my $tidy    = qx{perl misc/devel/tidy.pl --silent --no-write $filepath};
45
    my $tidy    = qx{perl misc/devel/tidy.pl --silent --no-write $filepath};
44
    my $content = read_file $filepath;
46
    my $content = read_file $filepath;
45
    ok( $content eq $tidy, "$filepath should be kept tidy" );
47
    ok( $content eq $tidy, "$filepath should be kept tidy" );
(-)a/yarn.lock (-22 / +25 lines)
Lines 2688-2693 aws-sign2@~0.7.0: Link Here
2688
  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
2688
  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
2689
  integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
2689
  integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
2690
2690
2691
aws-ssl-profiles@^1.1.1:
2692
  version "1.1.2"
2693
  resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641"
2694
  integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==
2695
2691
aws4@^1.8.0:
2696
aws4@^1.8.0:
2692
  version "1.11.0"
2697
  version "1.11.0"
2693
  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
2698
  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
Lines 3975-3987 csstype@^3.1.3: Link Here
3975
  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
3980
  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
3976
  integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
3981
  integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
3977
3982
3978
cypress-mysql@^1.0.0:
3979
  version "1.0.0"
3980
  resolved "https://registry.yarnpkg.com/cypress-mysql/-/cypress-mysql-1.0.0.tgz#987dd693aa4ad6b3968c0ff24834b0ad1af00876"
3981
  integrity sha512-UEy/jwzKEtctyWSQbgGBPsNJH0yCGaKN/xFZsNHAdjeBGOvI1prnhrsTVnOxclPwRmESnlTmap5P52u2Vl4TjA==
3982
  dependencies:
3983
    mysql2 "^2.3.3"
3984
3985
cypress@^12.17.2:
3983
cypress@^12.17.2:
3986
  version "12.17.4"
3984
  version "12.17.4"
3987
  resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c"
3985
  resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c"
Lines 4308-4314 delayed-stream@~1.0.0: Link Here
4308
  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
4306
  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
4309
  integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
4307
  integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
4310
4308
4311
denque@^2.0.1:
4309
denque@^2.1.0:
4312
  version "2.1.0"
4310
  version "2.1.0"
4313
  resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
4311
  resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
4314
  integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
4312
  integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
Lines 8020-8029 log-update@^4.0.0: Link Here
8020
    slice-ansi "^4.0.0"
8018
    slice-ansi "^4.0.0"
8021
    wrap-ansi "^6.2.0"
8019
    wrap-ansi "^6.2.0"
8022
8020
8023
long@^4.0.0:
8021
long@^5.2.1:
8024
  version "4.0.0"
8022
  version "5.3.2"
8025
  resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
8023
  resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83"
8026
  integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
8024
  integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==
8027
8025
8028
loose-envify@^1.1.0, loose-envify@^1.4.0:
8026
loose-envify@^1.1.0, loose-envify@^1.4.0:
8029
  version "1.4.0"
8027
  version "1.4.0"
Lines 8070-8075 lru-queue@^0.1.0: Link Here
8070
  dependencies:
8068
  dependencies:
8071
    es5-ext "~0.10.2"
8069
    es5-ext "~0.10.2"
8072
8070
8071
lru.min@^1.0.0:
8072
  version "1.1.2"
8073
  resolved "https://registry.yarnpkg.com/lru.min/-/lru.min-1.1.2.tgz#01ce1d72cc50c7faf8bd1f809ebf05d4331021eb"
8074
  integrity sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==
8075
8073
lunr@^2.3.9:
8076
lunr@^2.3.9:
8074
  version "2.3.9"
8077
  version "2.3.9"
8075
  resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
8078
  resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
Lines 8474-8490 mute-stdout@^1.0.0: Link Here
8474
  resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz#acb0300eb4de23a7ddeec014e3e96044b3472331"
8477
  resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz#acb0300eb4de23a7ddeec014e3e96044b3472331"
8475
  integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==
8478
  integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==
8476
8479
8477
mysql2@^2.3.3:
8480
mysql2@^3.14.1:
8478
  version "2.3.3"
8481
  version "3.14.1"
8479
  resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-2.3.3.tgz#944f3deca4b16629052ff8614fbf89d5552545a0"
8482
  resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.14.1.tgz#7786160abf086fd279e0253e16e34c05b4ab3b3e"
8480
  integrity sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==
8483
  integrity sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==
8481
  dependencies:
8484
  dependencies:
8482
    denque "^2.0.1"
8485
    aws-ssl-profiles "^1.1.1"
8486
    denque "^2.1.0"
8483
    generate-function "^2.3.1"
8487
    generate-function "^2.3.1"
8484
    iconv-lite "^0.6.3"
8488
    iconv-lite "^0.6.3"
8485
    long "^4.0.0"
8489
    long "^5.2.1"
8486
    lru-cache "^6.0.0"
8490
    lru.min "^1.0.0"
8487
    named-placeholders "^1.1.2"
8491
    named-placeholders "^1.1.3"
8488
    seq-queue "^0.0.5"
8492
    seq-queue "^0.0.5"
8489
    sqlstring "^2.3.2"
8493
    sqlstring "^2.3.2"
8490
8494
Lines 8498-8504 mysql@^2.18.1: Link Here
8498
    safe-buffer "5.1.2"
8502
    safe-buffer "5.1.2"
8499
    sqlstring "2.3.1"
8503
    sqlstring "2.3.1"
8500
8504
8501
named-placeholders@^1.1.2:
8505
named-placeholders@^1.1.3:
8502
  version "1.1.3"
8506
  version "1.1.3"
8503
  resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351"
8507
  resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351"
8504
  integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==
8508
  integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==
8505
- 

Return to bug 16631