Bug 32350: Use array_minus and ignore nesting
[koha.git] / t / lib / TestBuilder.pm
1 package t::lib::TestBuilder;
2
3 use Modern::Perl;
4
5 use Koha::Database qw( schema );
6 use C4::Biblio qw( AddBiblio );
7 use Koha::Biblios qw( _type );
8 use Koha::Items qw( _type );
9 use Koha::DateUtils qw( dt_from_string );
10
11 use Bytes::Random::Secure;
12 use Carp qw( carp );
13 use Module::Load qw( load );
14 use String::Random;
15 use Array::Utils qw( array_minus );
16
17 use constant {
18     SIZE_BARCODE => 20, # Not perfect but avoid to fetch the value when creating a new item
19 };
20
21 sub new {
22     my ($class) = @_;
23     my $self = {};
24     bless( $self, $class );
25
26     $self->schema( Koha::Database->new()->schema );
27     $self->schema->storage->sql_maker->quote_char('`');
28
29     $self->{gen_type} = _gen_type();
30     $self->{default_values} = _gen_default_values();
31     return $self;
32 }
33
34 sub schema {
35     my ($self, $schema) = @_;
36
37     if( defined( $schema ) ) {
38         $self->{schema} = $schema;
39     }
40     return $self->{schema};
41 }
42
43 # sub clear has been obsoleted; use delete_all from the schema resultset
44
45 sub delete {
46     my ( $self, $params ) = @_;
47     my $source = $params->{source} || return;
48     my @recs = ref( $params->{records} ) eq 'ARRAY'?
49         @{$params->{records}}: ( $params->{records} // () );
50     # tables without PK are not supported
51     my @pk = $self->schema->source( $source )->primary_columns;
52     return if !@pk;
53     my $rv = 0;
54     foreach my $rec ( @recs ) {
55     # delete only works when you supply full primary key values
56     # $cond does not include searches for undef (not allowed in PK)
57         my $cond = { map { defined $rec->{$_}? ($_, $rec->{$_}): (); } @pk };
58         next if keys %$cond < @pk;
59         $self->schema->resultset( $source )->search( $cond )->delete;
60         # we clear the pk columns in the supplied hash
61         # this indirectly signals at least an attempt to delete
62         map { delete $rec->{$_}; } @pk;
63         $rv++;
64     }
65     return $rv;
66 }
67
68 sub build_object {
69     my ( $self, $params ) = @_;
70
71     my $class = $params->{class};
72     my $value = $params->{value};
73
74     if ( not defined $class ) {
75         carp "Missing class param";
76         return;
77     }
78
79     my @unknowns = grep( !/^(class|value)$/, keys %{ $params });
80     carp "Unknown parameter(s): ", join( ', ', @unknowns ) if scalar @unknowns;
81
82     load $class;
83     my $source = $class->_type;
84
85     my $hashref = $self->build({ source => $source, value => $value });
86     my $object;
87     if ( $class eq 'Koha::Old::Patrons' ) {
88         $object = $class->search({ borrowernumber => $hashref->{borrowernumber} })->next;
89     } elsif ( $class eq 'Koha::Statistics' ) {
90         $object = $class->search({ datetime => $hashref->{datetime} })->next;
91     } else {
92         my @ids;
93         my @pks = $self->schema->source( $class->_type )->primary_columns;
94         foreach my $pk ( @pks ) {
95             push @ids, $hashref->{ $pk };
96         }
97
98         $object = $class->find( @ids );
99     }
100
101     return $object;
102 }
103
104 sub build {
105 # build returns a hash of column values for a created record, or undef
106 # build does NOT update a record, or pass back values of an existing record
107     my ($self, $params) = @_;
108     my $source  = $params->{source};
109     if( !$source ) {
110         carp "Source parameter not specified!";
111         return;
112     }
113     my $value   = $params->{value};
114
115     my @unknowns = grep( !/^(source|value)$/, keys %{ $params });
116     carp "Unknown parameter(s): ", join( ', ', @unknowns ) if scalar @unknowns;
117
118     my $col_values = $self->_buildColumnValues({
119         source  => $source,
120         value   => $value,
121     });
122     return if !$col_values; # did not meet unique constraints?
123
124     # loop thru all fk and create linked records if needed
125     # fills remaining entries in $col_values
126     my $foreign_keys = $self->_getForeignKeys( { source => $source } );
127     my $col_names = {};
128     for my $fk ( @$foreign_keys ) {
129         # skip when FK points to itself: e.g. borrowers:guarantorid
130         next if $fk->{source} eq $source;
131
132         # If we have more than one FK on the same column, we only generate values for the first one
133         next
134           if scalar @{ $fk->{keys} } == 1
135           && exists $col_names->{ $fk->{keys}->[0]->{col_name} };
136
137         my $keys = $fk->{keys};
138         my $tbl = $fk->{source};
139         my $res = $self->_create_links( $tbl, $keys, $col_values, $value );
140         return if !$res; # failed: no need to go further
141         foreach( keys %$res ) { # save new values
142             $col_values->{$_} = $res->{$_};
143         }
144
145         $col_names->{ $fk->{keys}->[0]->{col_name} } = 1
146           if scalar @{ $fk->{keys} } == 1
147     }
148
149     # store this record and return hashref
150     return $self->_storeColumnValues({
151         source => $source,
152         values => $col_values,
153     });
154 }
155
156 sub build_sample_biblio {
157     my ( $self, $args ) = @_;
158
159     my $title  = $args->{title}  || 'Some boring read';
160     my $author = $args->{author} || 'Some boring author';
161     my $frameworkcode = $args->{frameworkcode} || '';
162     my $itemtype = $args->{itemtype}
163       || $self->build_object( { class => 'Koha::ItemTypes' } )->itemtype;
164
165     my $marcflavour = C4::Context->preference('marcflavour');
166
167     my $record = MARC::Record->new();
168     $record->encoding( 'UTF-8' );
169
170     my ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 200, 'a' ) : ( 245, 'a' );
171     $record->append_fields(
172         MARC::Field->new( $tag, ' ', ' ', $subfield => $title ),
173     );
174
175     ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 200, 'f' ) : ( 100, 'a' );
176     $record->append_fields(
177         MARC::Field->new( $tag, ' ', ' ', $subfield => $author ),
178     );
179
180     ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 995, 'r' ) : ( 942, 'c' );
181     $record->append_fields(
182         MARC::Field->new( $tag, ' ', ' ', $subfield => $itemtype )
183     );
184
185     my ($biblio_id) = C4::Biblio::AddBiblio( $record, $frameworkcode );
186     return Koha::Biblios->find($biblio_id);
187 }
188
189 sub build_sample_item {
190     my ( $self, $args ) = @_;
191
192     my $biblionumber =
193       delete $args->{biblionumber} || $self->build_sample_biblio->biblionumber;
194     my $library = delete $args->{library}
195       || $self->build_object( { class => 'Koha::Libraries' } )->branchcode;
196
197     # If itype is not passed it will be picked from the biblio (see Koha::Item->store)
198
199     my $barcode = delete $args->{barcode}
200       || $self->_gen_text( { info => { size => SIZE_BARCODE } } );
201
202     return Koha::Item->new(
203         {
204             biblionumber  => $biblionumber,
205             homebranch    => $library,
206             holdingbranch => $library,
207             barcode       => $barcode,
208             %$args,
209         }
210     )->store->get_from_storage;
211 }
212
213 # ------------------------------------------------------------------------------
214 # Internal helper routines
215
216 sub _create_links {
217 # returns undef for failure to create linked records
218 # otherwise returns hashref containing new column values for parent record
219     my ( $self, $linked_tbl, $keys, $col_values, $value ) = @_;
220
221     my $fk_value = {};
222     my ( $cnt_scalar, $cnt_null ) = ( 0, 0 );
223
224     # First, collect all values for creating a linked record (if needed)
225     foreach my $fk ( @$keys ) {
226         my ( $col, $destcol ) = ( $fk->{col_name}, $fk->{col_fk_name} );
227         if( ref( $value->{$col} ) eq 'HASH' ) {
228             # add all keys from the FK hash
229             $fk_value = { %{ $value->{$col} }, %$fk_value };
230         }
231         if( exists $col_values->{$col} ) {
232             # add specific value (this does not necessarily exclude some
233             # values from the hash in the preceding if)
234             $fk_value->{ $destcol } = $col_values->{ $col };
235             $cnt_scalar++;
236             $cnt_null++ if !defined( $col_values->{$col} );
237         }
238     }
239
240     # If we saw all FK columns, first run the following checks
241     if( $cnt_scalar == @$keys ) {
242         # if one or more fk cols are null, the FK constraint will not be forced
243         return {} if $cnt_null > 0;
244
245         # does the record exist already?
246         my @pks = $self->schema->source( $linked_tbl )->primary_columns;
247         my %fk_pk_value;
248         for (@pks) {
249             $fk_pk_value{$_} = $fk_value->{$_} if defined $fk_value->{$_};
250         }
251         return {} if !(keys %fk_pk_value);
252         return {} if $self->schema->resultset($linked_tbl)->find( \%fk_pk_value );
253     }
254     # create record with a recursive build call
255     my $row = $self->build({ source => $linked_tbl, value => $fk_value });
256     return if !$row; # failure
257
258     # Finally, only return the new values
259     my $rv = {};
260     foreach my $fk ( @$keys ) {
261         my ( $col, $destcol ) = ( $fk->{col_name}, $fk->{col_fk_name} );
262         next if exists $col_values->{ $col };
263         $rv->{ $col } = $row->{ $destcol };
264     }
265     return $rv; # success
266 }
267
268 sub _formatSource {
269     my ($params) = @_;
270     my $source = $params->{source} || return;
271     $source =~ s|(\w+)$|$1|;
272     return $source;
273 }
274
275 sub _buildColumnValues {
276     my ($self, $params) = @_;
277     my $source = _formatSource( $params ) || return;
278     my $original_value = $params->{value};
279
280     my $col_values = {};
281     my @columns = $self->schema->source($source)->columns;
282     my %unique_constraints = $self->schema->source($source)->unique_constraints();
283
284     my @passed_keys = grep { ref($original_value->{$_}) ne 'HASH' } keys %$original_value;
285     my @minus = array_minus( @passed_keys, @columns );
286     die "Error: value hash contains unrecognized columns: ". (join ',', @minus) if @minus;
287
288     my $build_value = 5;
289     # we try max $build_value times if there are unique constraints
290     BUILD_VALUE: while ( $build_value ) {
291         # generate random values for all columns
292         for my $col_name( @columns ) {
293             my $valref = $self->_buildColumnValue({
294                 source      => $source,
295                 column_name => $col_name,
296                 value       => $original_value,
297             });
298             return if !$valref; # failure
299             if( @$valref ) { # could be empty
300                 # there will be only one value, but it could be undef
301                 $col_values->{$col_name} = $valref->[0];
302             }
303         }
304
305         # verify the data would respect each unique constraint
306         # note that this is INCOMPLETE since not all col_values are filled
307         CONSTRAINTS: foreach my $constraint (keys %unique_constraints) {
308
309                 my $condition;
310                 my $constraint_columns = $unique_constraints{$constraint};
311                 # loop through all constraint columns and build the condition
312                 foreach my $constraint_column ( @$constraint_columns ) {
313                     # build the filter
314                     # if one column does not exist or is undef, skip it
315                     # an insert with a null will not trigger the constraint
316                     next CONSTRAINTS
317                         if !exists $col_values->{ $constraint_column } ||
318                         !defined $col_values->{ $constraint_column };
319                     $condition->{ $constraint_column } =
320                             $col_values->{ $constraint_column };
321                 }
322                 my $count = $self->schema
323                                  ->resultset( $source )
324                                  ->search( $condition )
325                                  ->count();
326                 if ( $count > 0 ) {
327                     # no point checking more stuff, exit the loop
328                     $build_value--;
329                     next BUILD_VALUE;
330                 }
331         }
332         last; # you passed all tests
333     }
334     return $col_values if $build_value > 0;
335
336     # if you get here, we have a problem
337     warn "Violation of unique constraint in $source";
338     return;
339 }
340
341 sub _getForeignKeys {
342
343 # Returns the following arrayref
344 #   [ [ source => name, keys => [ col_name => A, col_fk_name => B ] ], ... ]
345 # The array gives source name and keys for each FK constraint
346
347     my ($self, $params) = @_;
348     my $source = $self->schema->source( $params->{source} );
349
350     my ( @foreign_keys, $check_dupl );
351     my @relationships = $source->relationships;
352     for my $rel_name( @relationships ) {
353         my $rel_info = $source->relationship_info($rel_name);
354         if( $rel_info->{attrs}->{is_foreign_key_constraint} ) {
355             $rel_info->{source} =~ s/^.*:://g;
356             my $rel = { source => $rel_info->{source} };
357
358             my @keys;
359             while( my ($col_fk_name, $col_name) = each(%{$rel_info->{cond}}) ) {
360                 $col_name    =~ s|self.(\w+)|$1|;
361                 $col_fk_name =~ s|foreign.(\w+)|$1|;
362                 push @keys, {
363                     col_name    => $col_name,
364                     col_fk_name => $col_fk_name,
365                 };
366             }
367             # check if the combination table and keys is unique
368             # so skip double belongs_to relations (as in Biblioitem)
369             my $tag = $rel->{source}. ':'.
370                 join ',', sort map { $_->{col_name} } @keys;
371             next if $check_dupl->{$tag};
372             $check_dupl->{$tag} = 1;
373             $rel->{keys} = \@keys;
374             push @foreign_keys, $rel;
375         }
376     }
377     return \@foreign_keys;
378 }
379
380 sub _storeColumnValues {
381     my ($self, $params) = @_;
382     my $source      = $params->{source};
383     my $col_values  = $params->{values};
384     my $new_row = $self->schema->resultset( $source )->create( $col_values );
385     return $new_row? { $new_row->get_columns }: {};
386 }
387
388 sub _buildColumnValue {
389 # returns an arrayref if all goes well
390 # an empty arrayref typically means: auto_incr column or fk column
391 # undef means failure
392     my ($self, $params) = @_;
393     my $source    = $params->{source};
394     my $value     = $params->{value};
395     my $col_name  = $params->{column_name};
396
397     my $col_info  = $self->schema->source($source)->column_info($col_name);
398
399     my $retvalue = [];
400     if( $col_info->{is_auto_increment} ) {
401         if( exists $value->{$col_name} ) {
402             warn "Value not allowed for auto_incr $col_name in $source";
403             return;
404         }
405         # otherwise: no need to assign a value
406     } elsif( $col_info->{is_foreign_key} || _should_be_fk($source,$col_name) ) {
407         if( exists $value->{$col_name} ) {
408             if( !defined $value->{$col_name} && !$col_info->{is_nullable} ) {
409                 # This explicit undef is not allowed
410                 warn "Null value for $col_name in $source not allowed";
411                 return;
412             }
413             if( ref( $value->{$col_name} ) ne 'HASH' ) {
414                 push @$retvalue, $value->{$col_name};
415             }
416             # sub build will handle a passed hash value later on
417         }
418     } elsif( ref( $value->{$col_name} ) eq 'HASH' ) {
419         # this is not allowed for a column that is not a FK
420         warn "Hash not allowed for $col_name in $source";
421         return;
422     } elsif( exists $value->{$col_name} ) {
423         if( !defined $value->{$col_name} && !$col_info->{is_nullable} ) {
424             # This explicit undef is not allowed
425             warn "Null value for $col_name in $source not allowed";
426             return;
427         }
428         push @$retvalue, $value->{$col_name};
429     } elsif( exists $self->{default_values}{$source}{$col_name} ) {
430         my $v = $self->{default_values}{$source}{$col_name};
431         $v = &$v() if ref($v) eq 'CODE';
432         push @$retvalue, $v;
433     } else {
434         my $data_type = $col_info->{data_type};
435         $data_type =~ s| |_|;
436         if( my $hdlr = $self->{gen_type}->{$data_type} ) {
437             push @$retvalue, &$hdlr( $self, { info => $col_info } );
438         } else {
439             warn "Unknown type $data_type for $col_name in $source";
440             return;
441         }
442     }
443     return $retvalue;
444 }
445
446 sub _should_be_fk {
447 # This sub is only needed for inconsistencies in the schema
448 # A column is not marked as FK, but a belongs_to relation is defined
449     my ( $source, $column ) = @_;
450     my $inconsistencies = {
451         'Item.biblionumber'           => 1, #FIXME: Please remove me when I become FK
452         'CheckoutRenewal.checkout_id' => 1, #FIXME: Please remove when issues and old_issues are merged
453     };
454     return $inconsistencies->{ "$source.$column" };
455 }
456
457 sub _gen_type {
458     return {
459         tinyint   => \&_gen_bool,
460         smallint  => \&_gen_int,
461         mediumint => \&_gen_int,
462         integer   => \&_gen_int,
463         bigint    => \&_gen_int,
464
465         float            => \&_gen_real,
466         decimal          => \&_gen_real,
467         double_precision => \&_gen_real,
468
469         timestamp => \&_gen_datetime,
470         datetime  => \&_gen_datetime,
471         date      => \&_gen_date,
472
473         char       => \&_gen_text,
474         varchar    => \&_gen_text,
475         tinytext   => \&_gen_text,
476         text       => \&_gen_text,
477         mediumtext => \&_gen_text,
478         longtext   => \&_gen_text,
479
480         set  => \&_gen_set_enum,
481         enum => \&_gen_set_enum,
482
483         tinyblob   => \&_gen_blob,
484         mediumblob => \&_gen_blob,
485         blob       => \&_gen_blob,
486         longblob   => \&_gen_blob,
487     };
488 };
489
490 sub _gen_bool {
491     my ($self, $params) = @_;
492     return int( rand(2) );
493 }
494
495 sub _gen_int {
496     my ($self, $params) = @_;
497     my $data_type = $params->{info}->{data_type};
498
499     my $max = 1;
500     if( $data_type eq 'tinyint' ) {
501         $max = 127;
502     }
503     elsif( $data_type eq 'smallint' ) {
504         $max = 32767;
505     }
506     elsif( $data_type eq 'mediumint' ) {
507         $max = 8388607;
508     }
509     elsif( $data_type eq 'integer' ) {
510         $max = 2147483647;
511     }
512     elsif( $data_type eq 'bigint' ) {
513         $max = 9223372036854775807;
514     }
515     return int( rand($max+1) );
516 }
517
518 sub _gen_real {
519     my ($self, $params) = @_;
520     my $max = 10 ** 38;
521     if( defined( $params->{info}->{size} ) ) {
522         $max = 10 ** ($params->{info}->{size}->[0] - $params->{info}->{size}->[1]);
523     }
524     $max = 10 ** 5 if $max > 10 ** 5;
525     return sprintf("%.2f", rand($max-0.1));
526 }
527
528 sub _gen_date {
529     my ($self, $params) = @_;
530     return $self->schema->storage->datetime_parser->format_date(dt_from_string)
531 }
532
533 sub _gen_datetime {
534     my ($self, $params) = @_;
535     return $self->schema->storage->datetime_parser->format_datetime(dt_from_string);
536 }
537
538 sub _gen_text {
539     my ($self, $params) = @_;
540     # From perldoc String::Random
541     my $size = $params->{info}{size} // 10;
542     $size -= alt_rand(0.5 * $size);
543     my $regex = $size > 1
544         ? '[A-Za-z][A-Za-z0-9_]{'.($size-1).'}'
545         : '[A-Za-z]';
546     my $random = String::Random->new( rand_gen => \&alt_rand );
547     # rand_gen is only supported from 0.27 onward
548     return $random->randregex($regex);
549 }
550
551 sub alt_rand { #Alternative randomizer
552     my ($max) = @_;
553     my $random = Bytes::Random::Secure->new( NonBlocking => 1 );
554     my $r = $random->irand / 2**32;
555     return int( $r * $max );
556 }
557
558 sub _gen_set_enum {
559     my ($self, $params) = @_;
560     return $params->{info}->{extra}->{list}->[0];
561 }
562
563 sub _gen_blob {
564     my ($self, $params) = @_;;
565     return 'b';
566 }
567
568 sub _gen_default_values {
569     my ($self) = @_;
570     return {
571         BackgroundJob => {
572             context => '{}'
573         },
574         Borrower => {
575             login_attempts => 0,
576             gonenoaddress  => undef,
577             lost           => undef,
578             debarred       => undef,
579             borrowernotes  => '',
580             secret         => undef,
581             password_expiration_date => undef,
582         },
583         Item => {
584             notforloan         => 0,
585             itemlost           => 0,
586             withdrawn          => 0,
587             restricted         => 0,
588             damaged            => 0,
589             materials          => undef,
590             more_subfields_xml => undef,
591         },
592         Category => {
593             enrolmentfee => 0,
594             reservefee   => 0,
595             # Not X, used for statistics
596             category_type => sub { return [ qw( A C S I P ) ]->[int(rand(5))] },
597             min_password_length => undef,
598             require_strong_password => undef,
599         },
600         Branch => {
601             pickup_location => 0,
602         },
603         Reserve => {
604             non_priority => 0,
605         },
606         Itemtype => {
607             rentalcharge => 0,
608             rentalcharge_daily => 0,
609             rentalcharge_hourly => 0,
610             defaultreplacecost => 0,
611             processfee => 0,
612             notforloan => 0,
613         },
614         Aqbookseller => {
615             tax_rate => 0,
616             discount => 0,
617             url  => undef,
618         },
619         Aqbudget => {
620             sort1_authcat => undef,
621             sort2_authcat => undef,
622         },
623         AuthHeader => {
624             marcxml => '',
625         },
626         BorrowerAttributeType => {
627             mandatory => 0,
628         },
629         Suggestion => {
630             suggesteddate => dt_from_string()->ymd,
631             STATUS        => 'ASKED'
632         },
633         ReturnClaim => {
634             issue_id => undef, # It should be a FK but we removed it
635                                # We don't want to generate a random value
636         },
637         ImportItem => {
638             status => 'staged',
639             import_error => undef
640         },
641         SearchFilter => {
642             opac => 1,
643             staff_client => 1
644         },
645         ErmAgreement => {
646             status           => 'active',
647             closure_reason   => undef,
648             renewal_priority => undef,
649             vendor_id        => undef,
650           },
651     };
652 }
653
654 =head1 NAME
655
656 t::lib::TestBuilder.pm - Koha module to create test records
657
658 =head1 SYNOPSIS
659
660     use t::lib::TestBuilder;
661     my $builder = t::lib::TestBuilder->new;
662
663     # The following call creates a patron, linked to branch CPL.
664     # Surname is provided, other columns are randomly generated.
665     # Branch CPL is created if it does not exist.
666     my $patron = $builder->build({
667         source => 'Borrower',
668         value  => { surname => 'Jansen', branchcode => 'CPL' },
669     });
670
671 =head1 DESCRIPTION
672
673 This module automatically creates database records for you.
674 If needed, records for foreign keys are created too.
675 Values will be randomly generated if not passed to TestBuilder.
676 Note that you should wrap these actions in a transaction yourself.
677
678 =head1 METHODS
679
680 =head2 new
681
682     my $builder = t::lib::TestBuilder->new;
683
684     Constructor - Returns the object TestBuilder
685
686 =head2 schema
687
688     my $schema = $builder->schema;
689
690     Getter - Returns the schema of DBIx::Class
691
692 =head2 delete
693
694     $builder->delete({
695         source => $source,
696         records => $patron, # OR: records => [ $patron, ... ],
697     });
698
699     Delete individual records, created by builder.
700     Returns the number of delete attempts, or undef.
701
702 =head2 build
703
704     $builder->build({ source  => $source_name, value => $value });
705
706     Create a test record in the table, represented by $source_name.
707     The name is required and must conform to the DBIx::Class schema.
708     Values may be specified by the optional $value hashref. Will be
709     randomized otherwise.
710     If needed, TestBuilder creates linked records for foreign keys.
711     Returns the values of the new record as a hashref, or undef if
712     the record could not be created.
713
714     Note that build also supports recursive hash references inside the
715     value hash for foreign key columns, like:
716         value => {
717             column1 => 'some_value',
718             fk_col2 => {
719                 columnA => 'another_value',
720             }
721         }
722     The hash for fk_col2 here means: create a linked record with build
723     where columnA has this value. In case of a composite FK the hashes
724     are merged.
725
726     Realize that passing primary key values to build may result in undef
727     if a record with that primary key already exists.
728
729 =head2 build_object
730
731 Given a plural Koha::Object-derived class, it creates a random element, and
732 returns the corresponding Koha::Object.
733
734     my $patron = $builder->build_object({ class => 'Koha::Patrons' [, value => { ... }] });
735
736 =head1 AUTHOR
737
738 Yohann Dufour <yohann.dufour@biblibre.com>
739
740 Koha Development Team
741
742 =head1 COPYRIGHT
743
744 Copyright 2014 - Biblibre SARL
745
746 =head1 LICENSE
747
748 This file is part of Koha.
749
750 Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
751 the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
752
753 Koha is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
754
755 You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>.
756
757 =cut
758
759 1;