Browse Source
This patch contains a new module t::lib::TestBuilder which allows to write tests easier and it contains the unit tests of this module. For more information, see the documentation of the module. This module uses the DBIx::Class schema and works with a clean DBIx::Class schema. In order to use it, you have to remove the current circular dependence (existing in the DBIx::Class) by applying the last patch of the bug 11518. Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com> Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> Signed-off-by: Tomas Cohen Arazi <tomascohen@gmail.com>3.20.x
2 changed files with 646 additions and 0 deletions
@ -0,0 +1,249 @@ |
|||
#!/usr/bin/perl |
|||
|
|||
# This file is part of Koha. |
|||
# |
|||
# Copyright 2014 - Biblibre SARL |
|||
# |
|||
# Koha is free software; you can redistribute it and/or modify it |
|||
# under the terms of the GNU General Public License as published by |
|||
# the Free Software Foundation; either version 3 of the License, or |
|||
# (at your option) any later version. |
|||
# |
|||
# 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. |
|||
# |
|||
# You should have received a copy of the GNU General Public License |
|||
# along with Koha; if not, see <http://www.gnu.org/licenses>. |
|||
|
|||
use Modern::Perl; |
|||
use Test::More tests => 40; |
|||
|
|||
BEGIN { |
|||
use_ok('t::lib::TestBuilder'); |
|||
} |
|||
|
|||
|
|||
my $builder = t::lib::TestBuilder->new(); |
|||
|
|||
is( $builder->build(), undef, 'build without arguments returns undef' ); |
|||
|
|||
my @sources = $builder->schema->sources; |
|||
my $nb_failure = 0; |
|||
for my $source (@sources) { |
|||
eval { $builder->build( { source => $source } ); }; |
|||
$nb_failure++ if ($@); |
|||
} |
|||
is( $nb_failure, 0, 'TestBuilder can create a entry for every sources' ); |
|||
|
|||
my $my_overduerules_transport_type = { |
|||
message_transport_type => { |
|||
message_transport_type => 'my msg_t_t', |
|||
}, |
|||
letternumber => 1, |
|||
branchcode => { |
|||
branchcode => 'codeB', |
|||
categorycode => 'codeC', |
|||
}, |
|||
categorycode => undef, |
|||
}; |
|||
$my_overduerules_transport_type->{categorycode} = $my_overduerules_transport_type->{branchcode}; |
|||
my $overduerules_transport_type = $builder->build({ |
|||
source => 'OverduerulesTransportType', |
|||
value => $my_overduerules_transport_type, |
|||
}); |
|||
is( |
|||
$overduerules_transport_type->{message_transport_type}, |
|||
$my_overduerules_transport_type->{message_transport_type}->{message_transport_type}, |
|||
'build stores the message_transport_type correctly' |
|||
); |
|||
is( |
|||
$overduerules_transport_type->{letternumber}, |
|||
$my_overduerules_transport_type->{letternumber}, |
|||
'build stores the letternumber correctly' |
|||
); |
|||
is( |
|||
$overduerules_transport_type->{branchcode}, |
|||
$my_overduerules_transport_type->{branchcode}->{branchcode}, |
|||
'build stores the branchcode correctly' |
|||
); |
|||
is( |
|||
$overduerules_transport_type->{categorycode}, |
|||
$my_overduerules_transport_type->{categorycode}->{categorycode}, |
|||
'build stores the categorycode correctly' |
|||
); |
|||
is( |
|||
$overduerules_transport_type->{_fk}->{message_transport_type}->{message_transport_type}, |
|||
$my_overduerules_transport_type->{message_transport_type}->{message_transport_type}, |
|||
'build stores the foreign key message_transport_type correctly' |
|||
); |
|||
is( |
|||
$overduerules_transport_type->{_fk}->{branchcode}->{branchcode}, |
|||
$my_overduerules_transport_type->{branchcode}->{branchcode}, |
|||
'build stores the foreign key branchcode correctly' |
|||
); |
|||
is( |
|||
$overduerules_transport_type->{_fk}->{categorycode}->{categorycode}, |
|||
$my_overduerules_transport_type->{categorycode}->{categorycode}, |
|||
'build stores the foreign key categorycode correctly' |
|||
); |
|||
is_deeply( |
|||
$overduerules_transport_type->{_fk}->{branchcode}, |
|||
$overduerules_transport_type->{_fk}->{categorycode}, |
|||
'build links the branchcode and the categorycode correctly' |
|||
); |
|||
isnt( |
|||
$overduerules_transport_type->{_fk}->{branchcode}->{letter2}, |
|||
undef, |
|||
'build generates values if they are not given' |
|||
); |
|||
|
|||
my $my_user_permission = $t::lib::TestBuilder::default_value->{UserPermission}; |
|||
my $user_permission = $builder->build({ |
|||
source => 'UserPermission', |
|||
}); |
|||
isnt( |
|||
$user_permission->{borrowernumber}, |
|||
undef, |
|||
'build generates a borrowernumber correctly' |
|||
); |
|||
is( |
|||
$user_permission->{module_bit}, |
|||
$my_user_permission->{module_bit}->{module_bit}->{bit}, |
|||
'build stores the default value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{code}, |
|||
$my_user_permission->{module_bit}->{code}, |
|||
'build stores the default value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{borrowernumber}, |
|||
$user_permission->{_fk}->{borrowernumber}->{borrowernumber}, |
|||
'build links the foreign key correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{borrowernumber}->{surname}, |
|||
$my_user_permission->{borrowernumber}->{surname}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{borrowernumber}->{address}, |
|||
$my_user_permission->{borrowernumber}->{address}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{borrowernumber}->{city}, |
|||
$my_user_permission->{borrowernumber}->{city}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{borrowernumber}->{_fk}->{branchcode}->{branchcode}, |
|||
$my_user_permission->{borrowernumber}->{branchcode}->{branchcode}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{borrowernumber}->{_fk}->{branchcode}->{branchname}, |
|||
$my_user_permission->{borrowernumber}->{branchcode}->{branchname}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{borrowernumber}->{_fk}->{categorycode}->{categorycode}, |
|||
$my_user_permission->{borrowernumber}->{categorycode}->{categorycode}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{borrowernumber}->{_fk}->{categorycode}->{hidelostitems}, |
|||
$my_user_permission->{borrowernumber}->{categorycode}->{hidelostitems}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{borrowernumber}->{_fk}->{categorycode}->{category_type}, |
|||
$my_user_permission->{borrowernumber}->{categorycode}->{category_type}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{borrowernumber}->{_fk}->{categorycode}->{defaultprivacy}, |
|||
$my_user_permission->{borrowernumber}->{categorycode}->{defaultprivacy}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{borrowernumber}->{privacy}, |
|||
$my_user_permission->{borrowernumber}->{privacy}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{module_bit}->{_fk}->{module_bit}->{bit}, |
|||
$my_user_permission->{module_bit}->{module_bit}->{bit}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is( |
|||
$user_permission->{_fk}->{module_bit}->{code}, |
|||
$my_user_permission->{module_bit}->{code}, |
|||
'build stores the foreign key value correctly' |
|||
); |
|||
is_deeply( |
|||
$user_permission->{_fk}->{module_bit}, |
|||
$user_permission->{_fk}->{code}, |
|||
'build links the codes correctly' |
|||
); |
|||
isnt( |
|||
$user_permission->{_fk}->{borrowernumber}->{cardnumber}, |
|||
undef, |
|||
'build generates values if they are not given' |
|||
); |
|||
isnt( |
|||
$user_permission->{_fk}->{borrowernumber}->{_fk}->{branchcode}->{branchaddress1}, |
|||
undef, |
|||
'build generates values if they are not given' |
|||
); |
|||
isnt( |
|||
$user_permission->{_fk}->{borrowernumber}->{_fk}->{categorycode}->{description}, |
|||
undef, |
|||
'build generates values if they are not given' |
|||
); |
|||
isnt( |
|||
$user_permission->{_fk}->{module_bit}->{description}, |
|||
undef, |
|||
'build generates values if they are not given' |
|||
); |
|||
isnt( |
|||
$user_permission->{_fk}->{module_bit}->{_fk}->{module_bit}->{flag}, |
|||
undef, |
|||
'build generates values if they are not given' |
|||
); |
|||
|
|||
|
|||
my $nb_basket = $builder->schema->resultset('Aqbasket')->search(); |
|||
isnt( $nb_basket, 0, 'add stores the generated entries correctly' ); |
|||
$builder->clear( { source => 'Aqbasket' } ); |
|||
$nb_basket = $builder->schema->resultset('Aqbasket')->search(); |
|||
is( $nb_basket, 0, 'clear removes all the entries correctly' ); |
|||
|
|||
|
|||
my $rs_aqbookseller = $builder->schema->resultset('Aqbookseller'); |
|||
my $bookseller = $builder->build({ |
|||
source => 'Aqbookseller', |
|||
only_fk => 1, |
|||
}); |
|||
delete $bookseller->{_fk}; |
|||
my $bookseller_from_db = $rs_aqbookseller->find($bookseller); |
|||
is( $bookseller_from_db, undef, 'build with only_fk = 1 does not store the entry' ); |
|||
my $bookseller_result = $rs_aqbookseller->create($bookseller); |
|||
is( $bookseller_result->in_storage, 1, 'build with only_fk = 1 creates the foreign keys correctly' ); |
|||
|
|||
$bookseller = $builder->build({ |
|||
source => 'Aqbookseller', |
|||
}); |
|||
delete $bookseller->{_fk}; |
|||
$bookseller_from_db = $rs_aqbookseller->find($bookseller); |
|||
is( $bookseller_from_db->in_storage, 1, 'build without the parameter only_sk stores the entry correctly' ); |
|||
|
|||
$bookseller = $builder->build({ |
|||
source => 'Aqbookseller', |
|||
only_fk => 0, |
|||
}); |
|||
delete $bookseller->{_fk}; |
|||
$bookseller_from_db = $rs_aqbookseller->find($bookseller); |
|||
is( $bookseller_from_db->in_storage, 1, 'build with only_fk = 0 stores the entry correctly' ); |
@ -0,0 +1,397 @@ |
|||
package t::lib::TestBuilder; |
|||
|
|||
use Modern::Perl; |
|||
use Koha::Database; |
|||
use String::Random; |
|||
|
|||
|
|||
my $gen_type = { |
|||
tinyint => \&_gen_int, |
|||
smallint => \&_gen_int, |
|||
mediumint => \&_gen_int, |
|||
integer => \&_gen_int, |
|||
bigint => \&_gen_int, |
|||
|
|||
float => \&_gen_real, |
|||
decimal => \&_gen_real, |
|||
double_precision => \&_gen_real, |
|||
|
|||
timestamp => \&_gen_date, |
|||
datetime => \&_gen_date, |
|||
date => \&_gen_date, |
|||
|
|||
char => \&_gen_text, |
|||
varchar => \&_gen_text, |
|||
tinytext => \&_gen_text, |
|||
text => \&_gen_text, |
|||
mediumtext => \&_gen_text, |
|||
longtext => \&_gen_text, |
|||
|
|||
set => \&_gen_set_enum, |
|||
enum => \&_gen_set_enum, |
|||
|
|||
tinyblob => \&_gen_blob, |
|||
mediumblob => \&_gen_blob, |
|||
blob => \&_gen_blob, |
|||
longblob => \&_gen_blob, |
|||
}; |
|||
|
|||
our $default_value = { |
|||
UserPermission => { |
|||
borrowernumber => { |
|||
surname => 'my surname', |
|||
address => 'my adress', |
|||
city => 'my city', |
|||
branchcode => { |
|||
branchcode => 'cB', |
|||
branchname => 'my branchname', |
|||
}, |
|||
categorycode => { |
|||
categorycode => 'cC', |
|||
hidelostitems => 0, |
|||
category_type => 'A', |
|||
default_privacy => 'default', |
|||
}, |
|||
privacy => 1, |
|||
}, |
|||
module_bit => { |
|||
module_bit => { |
|||
bit => '10', |
|||
}, |
|||
code => 'my code', |
|||
}, |
|||
code => undef, |
|||
}, |
|||
}; |
|||
$default_value->{UserPermission}->{code} = $default_value->{UserPermission}->{module_bit}; |
|||
|
|||
|
|||
sub new { |
|||
my ($class) = @_; |
|||
my $self = {}; |
|||
bless( $self, $class ); |
|||
|
|||
$self->schema( Koha::Database->new()->schema ); |
|||
$self->schema->txn_begin(); |
|||
$self->schema->storage->sql_maker->quote_char('`'); |
|||
return $self; |
|||
} |
|||
|
|||
sub schema { |
|||
my ($self, $schema) = @_; |
|||
|
|||
if( defined( $schema ) ) { |
|||
$self->{schema} = $schema; |
|||
} |
|||
return $self->{schema}; |
|||
} |
|||
|
|||
sub clear { |
|||
my ($self, $params) = @_; |
|||
my $source = $self->schema->resultset( $params->{source} ); |
|||
return $source->delete_all(); |
|||
} |
|||
|
|||
sub build { |
|||
my ($self, $params) = @_; |
|||
my $source = $params->{source} || return; |
|||
my $value = $params->{value}; |
|||
my $only_fk = $params->{only_fk} || 0; |
|||
|
|||
my $col_values = $self->_buildColumnValues({ |
|||
source => $source, |
|||
value => $value, |
|||
}); |
|||
|
|||
my $data; |
|||
my $foreign_keys = $self->_getForeignKeys( { source => $source } ); |
|||
for my $fk ( @$foreign_keys ) { |
|||
my $fk_value; |
|||
my $col_name = $fk->{keys}->[0]->{col_name}; |
|||
if( ref( $col_values->{$col_name} ) eq 'HASH' ) { |
|||
$fk_value = $col_values->{$col_name}; |
|||
} |
|||
elsif( defined( $col_values->{$col_name} ) ) { |
|||
next; |
|||
} |
|||
|
|||
my $fk_row = $self->build({ |
|||
source => $fk->{source}, |
|||
value => $fk_value, |
|||
}); |
|||
|
|||
my $keys = $fk->{keys}; |
|||
for my $key( @$keys ) { |
|||
$col_values->{ $key->{col_name} } = $fk_row->{ $key->{col_fk_name} }; |
|||
$data->{ $key->{col_name} } = $fk_row; |
|||
} |
|||
} |
|||
|
|||
my $new_row; |
|||
if( $only_fk ) { |
|||
$new_row = $col_values; |
|||
} |
|||
else { |
|||
$new_row = $self->_storeColumnValues({ |
|||
source => $source, |
|||
values => $col_values, |
|||
}); |
|||
} |
|||
$new_row->{_fk} = $data if( defined( $data ) ); |
|||
return $new_row; |
|||
} |
|||
|
|||
sub _formatSource { |
|||
my ($params) = @_; |
|||
my $source = $params->{source}; |
|||
$source =~ s|(\w+)$|$1|; |
|||
return $source; |
|||
} |
|||
|
|||
sub _buildColumnValues { |
|||
my ($self, $params) = @_; |
|||
my $source = _formatSource( { source => $params->{source} } ); |
|||
my $value = $params->{value}; |
|||
|
|||
my $col_values; |
|||
my @columns = $self->schema->source($source)->columns; |
|||
for my $col_name( @columns ) { |
|||
my $col_value = $self->_buildColumnValue({ |
|||
source => $source, |
|||
column_name => $col_name, |
|||
value => $value, |
|||
}); |
|||
$col_values->{$col_name} = $col_value if( defined( $col_value ) ); |
|||
} |
|||
return $col_values; |
|||
} |
|||
|
|||
# Returns [ { |
|||
# rel_name => $rel_name, |
|||
# source => $table_name, |
|||
# keys => [ { |
|||
# col_name => $col_name, |
|||
# col_fk_name => $col_fk_name, |
|||
# }, ... ] |
|||
# }, ... ] |
|||
sub _getForeignKeys { |
|||
my ($self, $params) = @_; |
|||
my $source = $self->schema->source( $params->{source} ); |
|||
|
|||
my @foreign_keys = (); |
|||
my @relationships = $source->relationships; |
|||
for my $rel_name( @relationships ) { |
|||
my $rel_info = $source->relationship_info($rel_name); |
|||
if( $rel_info->{attrs}->{is_foreign_key_constraint} ) { |
|||
my $rel = { |
|||
rel_name => $rel_name, |
|||
source => $rel_info->{source}, |
|||
}; |
|||
|
|||
my @keys = (); |
|||
while( my ($col_fk_name, $col_name) = each($rel_info->{cond}) ) { |
|||
$col_name =~ s|self.(\w+)|$1|; |
|||
$col_fk_name =~ s|foreign.(\w+)|$1|; |
|||
push @keys, { |
|||
col_name => $col_name, |
|||
col_fk_name => $col_fk_name, |
|||
}; |
|||
} |
|||
$rel->{keys} = \@keys; |
|||
|
|||
push @foreign_keys, $rel; |
|||
} |
|||
} |
|||
return \@foreign_keys; |
|||
} |
|||
|
|||
sub _storeColumnValues { |
|||
my ($self, $params) = @_; |
|||
my $source = $params->{source}; |
|||
my $col_values = $params->{values}; |
|||
|
|||
my $new_row; |
|||
eval { |
|||
$new_row = $self->schema->resultset($source)->update_or_create($col_values); |
|||
}; |
|||
die "$source - $@\n" if ($@); |
|||
|
|||
eval { |
|||
$new_row = { $new_row->get_columns }; |
|||
}; |
|||
warn "$source - $@\n" if ($@); |
|||
return $new_row; |
|||
} |
|||
|
|||
sub _buildColumnValue { |
|||
my ($self, $params) = @_; |
|||
my $source = $params->{source}; |
|||
my $value = $params->{value}; |
|||
my $col_name = $params->{column_name}; |
|||
my $col_info = $self->schema->source($source)->column_info($col_name); |
|||
|
|||
my $col_value; |
|||
if( exists( $value->{$col_name} ) ) { |
|||
$col_value = $value->{$col_name}; |
|||
} |
|||
elsif( exists( $default_value->{$source}->{$col_name} ) ) { |
|||
$col_value = $default_value->{$source}->{$col_name}; |
|||
} |
|||
elsif( not $col_info->{default_value} and not $col_info->{is_auto_increment} and not $col_info->{is_foreign_key} ) { |
|||
eval { |
|||
my $data_type = $col_info->{data_type}; |
|||
$data_type =~ s| |_|; |
|||
$col_value = $gen_type->{$data_type}->( $self, { info => $col_info } ); |
|||
}; |
|||
die "The type $col_info->{data_type} is not defined\n" if ($@); |
|||
} |
|||
return $col_value; |
|||
} |
|||
|
|||
|
|||
sub _gen_int { |
|||
my ($self, $params) = @_; |
|||
my $data_type = $params->{info}->{data_type}; |
|||
|
|||
my $max = 1; |
|||
if( $data_type eq 'tinyint' ) { |
|||
$max = 127; |
|||
} |
|||
elsif( $data_type eq 'smallint' ) { |
|||
$max = 32767; |
|||
} |
|||
elsif( $data_type eq 'mediumint' ) { |
|||
$max = 8388607; |
|||
} |
|||
elsif( $data_type eq 'integer' ) { |
|||
$max = 2147483647; |
|||
} |
|||
elsif( $data_type eq 'bigint' ) { |
|||
$max = 9223372036854775807; |
|||
} |
|||
return int( rand($max+1) ); |
|||
} |
|||
|
|||
sub _gen_real { |
|||
my ($self, $params) = @_; |
|||
my $max = 10 ** 38; |
|||
if( defined( $params->{info}->{size} ) ) { |
|||
$max = 10 ** ($params->{info}->{size}->[0] - $params->{info}->{size}->[1]); |
|||
} |
|||
return rand($max) + 1; |
|||
} |
|||
|
|||
sub _gen_date { |
|||
my ($self, $params) = @_; |
|||
return $self->schema->storage->datetime_parser->format_datetime(DateTime->now()); |
|||
} |
|||
|
|||
sub _gen_text { |
|||
my ($self, $params) = @_; |
|||
my $random = String::Random->new( max => $params->{info}->{size} ); |
|||
return $random->randregex('[A-Za-z]+[A-Za-z0-9_]*'); |
|||
} |
|||
|
|||
sub _gen_set_enum { |
|||
my ($self, $params) = @_; |
|||
return $params->{info}->{extra}->{list}->[0]; |
|||
} |
|||
|
|||
sub _gen_blob { |
|||
my ($self, $params) = @_;; |
|||
return 'b'; |
|||
} |
|||
|
|||
|
|||
sub DESTROY { |
|||
my $self = shift; |
|||
$self->schema->txn_rollback(); |
|||
} |
|||
|
|||
|
|||
=head1 NAME |
|||
|
|||
t::lib::TestBuilder.pm - Koha module to simplify the writing of tests |
|||
|
|||
=head1 SYNOPSIS |
|||
|
|||
use t::lib::TestBuilder; |
|||
|
|||
Koha module to insert the foreign keys automatically for the tests |
|||
|
|||
=head1 DESCRIPTION |
|||
|
|||
This module allows to insert automatically an entry in the database. All the database changes are wrapped in a transaction. |
|||
The foreign keys are created according to the DBIx::Class schema. |
|||
The taken values are the values by default if it is possible or randomly generated. |
|||
|
|||
=head1 FUNCTIONS |
|||
|
|||
=head2 new |
|||
|
|||
$builder = t::lib::TestBuilder->new() |
|||
|
|||
Constructor - Begins a transaction and returns the object TestBuilder |
|||
|
|||
=head2 schema |
|||
|
|||
$schema = $builder->schema |
|||
|
|||
Getter - Returns the schema of DBIx::Class |
|||
|
|||
=head2 clear |
|||
|
|||
$builder->clear({ source => $source_name }) |
|||
|
|||
=over |
|||
|
|||
=item C<$source_name> is the name of the source in the DBIx::Class schema (required) |
|||
|
|||
=back |
|||
|
|||
Clears all the data of this source (database table) |
|||
|
|||
=head2 build |
|||
|
|||
$builder->build({ |
|||
source => $source_name, |
|||
value => $value, |
|||
only_fk => $only_fk, |
|||
}) |
|||
|
|||
=over |
|||
|
|||
=item C<$source_name> is the name of the source in the DBIx::Class schema (required) |
|||
|
|||
=item C<$value> is the values for the entry (optional) |
|||
|
|||
=item C<$only_fk> is a boolean to indicate if only the foreign keys are created (optional) |
|||
|
|||
=back |
|||
|
|||
Inserts an entry in the database by instanciating all the foreign keys. |
|||
The values can be specified, the values which are not given are default values if they exists or generated randomly. |
|||
Returns the values of the entry as a hashref with an extra key : _fk which contains all the values of the generated foreign keys. |
|||
|
|||
=head1 AUTHOR |
|||
|
|||
Yohann Dufour <yohann.dufour@biblibre.com> |
|||
|
|||
=head1 COPYRIGHT |
|||
|
|||
Copyright 2014 - Biblibre SARL |
|||
|
|||
=head1 LICENSE |
|||
|
|||
This file is part of Koha. |
|||
|
|||
Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by |
|||
the Free Software Foundation; either version 3 of the License, or (at your option) any later version. |
|||
|
|||
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. |
|||
|
|||
You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>. |
|||
|
|||
=cut |
|||
|
|||
1; |
Loading…
Reference in new issue