diff --git a/C4/Biblio.pm b/C4/Biblio.pm index 6b20c87af0..0fefd336dd 100644 --- a/C4/Biblio.pm +++ b/C4/Biblio.pm @@ -95,6 +95,7 @@ use C4::Linker; use C4::OAI::Sets; use Koha::Logger; +use Koha::Cache::Memory::Lite; use Koha::Caches; use Koha::ClassSources; use Koha::Authority::Types; @@ -1570,15 +1571,15 @@ sub GetAuthorisedValueDesc { #---- itemtypes if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "itemtypes" ) { - my $lang = C4::Languages::getlanguage; - $lang //= 'en'; - $cache_key = 'itemtype:description:' . $lang; - my $itypes = $cache->get_from_cache( $cache_key, { unsafe => 1 } ); - if ( !$itypes ) { - $itypes = { map { $_->itemtype => $_->translated_description } Koha::ItemTypes->search()->as_list }; - $cache->set_in_cache( $cache_key, $itypes ); + my $memory_cache = Koha::Cache::Memory::Lite->get_instance(); + my $cache_key = 'GetAuthorisedValueDesc:itemtypes'; + my $itemtypes = $memory_cache->get_from_cache($cache_key); + unless ($itemtypes) { + $itemtypes = { map { $_->itemtype => $_ } Koha::ItemTypes->as_list }; + $memory_cache->set_in_cache( $cache_key, $itemtypes ); } - return $itypes->{$value}; + my $itemtype = $itemtypes->{$value}; + return $itemtype ? $itemtype->translated_description : undef; } if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "cn_source" ) { diff --git a/Koha/ItemType.pm b/Koha/ItemType.pm index a59af59a78..f15e49d9fd 100644 --- a/Koha/ItemType.pm +++ b/Koha/ItemType.pm @@ -19,15 +19,11 @@ use Modern::Perl; use C4::Koha qw( getitemtypeimagelocation ); use C4::Languages; -use Koha::Caches; use Koha::Database; use Koha::CirculationRules; -use Koha::Localizations; use base qw(Koha::Object Koha::Object::Limit::Library); -my $cache = Koha::Caches->get_instance(); - =head1 NAME Koha::ItemType - Koha Item type Object class @@ -36,48 +32,6 @@ Koha::ItemType - Koha Item type Object class =head2 Class methods -=cut - -=head3 store - -ItemType specific store to ensure relevant caches are flushed on change - -=cut - -sub store { - my ($self) = @_; - - my $flush = 0; - - if ( !$self->in_storage ) { - $flush = 1; - } else { - my $self_from_storage = $self->get_from_storage; - $flush = 1 if ( $self_from_storage->description ne $self->description ); - } - - $self = $self->SUPER::store; - - if ($flush) { - my $key = "itemtype:description:en"; - $cache->clear_from_cache($key); - } - - return $self; -} - -=head2 delete - -ItemType specific C to clear relevant caches on delete. - -=cut - -sub delete { - my $self = shift @_; - $cache->clear_from_cache('itemtype:description:en'); - $self->SUPER::delete(@_); -} - =head3 image_location =cut @@ -93,26 +47,8 @@ sub image_location { sub translated_description { my ( $self, $lang ) = @_; - if ( my $translated_description = eval { $self->get_column('translated_description') } ) { - # If the value has already been fetched (eg. from sarch_with_localization), - # do not search for it again - # Note: This is a bit hacky but should be fast - return $translated_description - ? $translated_description - : $self->description; - } - $lang ||= C4::Languages::getlanguage; - my $translated_description = Koha::Localizations->search( - { - code => $self->itemtype, - entity => 'itemtypes', - lang => $lang - } - )->next; - return $translated_description - ? $translated_description->translation - : $self->description; + my $localization = $self->localization( 'description', $lang || C4::Languages::getlanguage() ); } =head3 translated_descriptions @@ -121,19 +57,14 @@ sub translated_description { sub translated_descriptions { my ($self) = @_; - my @translated_descriptions = Koha::Localizations->search( - { - entity => 'itemtypes', - code => $self->itemtype, - } - )->as_list; + return [ map { { lang => $_->lang, translation => $_->translation, } - } @translated_descriptions + } $self->_result->description_localizations ]; } @@ -234,9 +165,32 @@ sub to_api_mapping { rentalcharge_hourly => 'hourly_rental_charge', rentalcharge_hourly_calendar => 'hourly_rental_charge_calendar', bookable_itemtype => 'bookable_itemtype', + + # TODO Remove after having updated all code using unblessed translated_description + translated_description => undef, }; } +=head3 unblessed + +See L + +Overridden to add a C key for backward compatibility. +This should not be relied on as it may be removed in the future. + +=cut + +# TODO Remove after having updated all code using unblessed translated_description +sub unblessed { + my ($self) = @_; + + my $unblessed = $self->SUPER::unblessed(); + + $unblessed->{translated_description} = $self->translated_description; + + return $unblessed; +} + =head2 Internal methods =head3 _type diff --git a/Koha/ItemTypes.pm b/Koha/ItemTypes.pm index a387df25c4..afb7bb6186 100644 --- a/Koha/ItemTypes.pm +++ b/Koha/ItemTypes.pm @@ -32,8 +32,6 @@ Koha::ItemTypes - Koha ItemType Object set class =head2 Class methods -=cut - =head3 search_with_localization my $itemtypes = Koha::ItemTypes->search_with_localization @@ -43,22 +41,22 @@ my $itemtypes = Koha::ItemTypes->search_with_localization sub search_with_localization { my ( $self, $params, $attributes ) = @_; - my $language = C4::Languages::getlanguage(); - $Koha::Schema::Result::Itemtype::LANGUAGE = $language; - $attributes->{order_by} = 'translated_description' unless exists $attributes->{order_by}; - $attributes->{join} = 'localization'; - $attributes->{'+select'} = [ - { - coalesce => [qw( localization.translation me.description )], - -as => 'translated_description' - } - ]; - if ( defined $params->{branchcode} ) { - my $branchcode = delete $params->{branchcode}; - $self->search_with_library_limits( $params, $attributes, $branchcode ); - } else { - $self->SUPER::search( $params, $attributes ); - } + return $self->search( $params, $attributes )->order_by_translated_description; +} + +=head3 order_by_translated_description + +=cut + +sub order_by_translated_description { + my ($self) = @_; + + my $attributes = { + join => 'description_localization', + order_by => \['COALESCE(description_localization.translation, me.description)'], + }; + + return $self->search( {}, $attributes ); } =head2 Internal methods diff --git a/Koha/Localization.pm b/Koha/Localization.pm deleted file mode 100644 index 269e1d3b36..0000000000 --- a/Koha/Localization.pm +++ /dev/null @@ -1,74 +0,0 @@ -package Koha::Localization; - -# 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 . - -use Modern::Perl; - -use Koha::Database; - -use base qw(Koha::Object); - -use Koha::Caches; -my $cache = Koha::Caches->get_instance(); - -=head1 NAME - -Koha::Localization - Koha Localization type Object class - -=head1 API - -=head2 Class methods - -=cut - -=head3 store - -Localization specific store to ensure relevant caches are flushed on change - -=cut - -sub store { - my ($self) = @_; - $self = $self->SUPER::store; - - if ( $self->entity eq 'itemtypes' ) { - my $key = "itemtype:description:" . $self->lang; - $cache->clear_from_cache($key); - } - - return $self; -} - -=head2 delete - -Localization specific C to clear relevant caches on delete. - -=cut - -sub delete { - my $self = shift @_; - if ( $self->entity eq 'itemtypes' ) { - my $key = "itemtype:description:" . $self->lang; - $cache->clear_from_cache($key); - } - $self->SUPER::delete(@_); -} - -sub _type { - return 'Localization'; -} - -1; diff --git a/Koha/Localizations.pm b/Koha/Localizations.pm deleted file mode 100644 index b61d04580f..0000000000 --- a/Koha/Localizations.pm +++ /dev/null @@ -1,34 +0,0 @@ -package Koha::Localizations; - -# 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 . - -use Modern::Perl; - -use Koha::Database; - -use Koha::Localization; - -use base qw(Koha::Objects); - -sub _type { - return 'Localization'; -} - -sub object_class { - return 'Koha::Localization'; -} - -1; diff --git a/Koha/Object.pm b/Koha/Object.pm index 55c7e489a0..884478f8f0 100644 --- a/Koha/Object.pm +++ b/Koha/Object.pm @@ -956,6 +956,32 @@ sub unblessed_all_relateds { return \%data; } +=head3 localization + +Returns a localized (translated) value of the property, or the original +property value if no translation exist + + $localization = $object->localization($property, $lang); + +C<$property> is the property name. Often this will correspond to an SQL column name + +C<$lang> is the language code (for instance: 'en-GB') + +=cut + +sub localization { + my ( $self, $property, $lang ) = @_; + + my $result = $self->_result; + if ( $result->can('localization') ) { + if ( my $localization = $result->localization( $property, $lang ) ) { + return $localization; + } + } + + return $result->get_column($property); +} + =head3 $object->_result(); Returns the internal DBIC Row object diff --git a/Koha/Schema/Component/Localization.pm b/Koha/Schema/Component/Localization.pm new file mode 100644 index 0000000000..1f0423a755 --- /dev/null +++ b/Koha/Schema/Component/Localization.pm @@ -0,0 +1,214 @@ +package Koha::Schema::Component::Localization; + +=head1 NAME + +Koha::Schema::Component::Localization + +=head1 SYNOPSIS + + package Koha::Schema::Result::SomeTable; + + # ... generated code ... + + __PACKAGE__->load_components('+Koha::Schema::Component::Localization'); + + __PACKAGE__->localization_add_relationships( + 'some_table_localizations', + 'some_table_id' => 'some_table_id', + 'first_column', + 'second_column', + # ... + ); + + package main; + + my $rows = $schema->resultset('SomeTable'); + my $row = $rows->first; + $row->localizations->search($cond); + $row->create_related('first_column_localizations', { lang => $lang, translation => $translation }) + + while (my $row = $rows->next) + # first call will fetch all localizations for the current language and + # the result will be cached, next calls will not execute a query + $row->localization('first_column', $lang); + + # no query executed + $row->localization('second_column', $lang); + } + +=head1 DESCRIPTION + +This is a DBIx::Class component that helps to manage database localizations by +adding several relationships and methods to a "Result Class" + +This can handle several localizable columns (also referred as "properties") per +table + +To add database localizations to an existing database table, you need to: + +=over + +=item * Create a new table with: + +=over + +=item * An auto incremented column as primary key + +=item * A foreign key column referencing the existing table + +=item * 3 string (varchar or text) columns named 'property', 'lang', +'translation' + +=item * A unique key comprising the foreign key column, 'property' and 'lang' + +=back + +=item * Regenerate the DBIx::Class schema with +misc/devel/update_dbix_class_files.pl + +=item * Add calls to load_components and localization_add_relationships at the +end of the result class + +=back + +This will give you a relationship named 'localizations' through which you can +access all localizations of a particular table row. + +And for every property, you will have: + +=over + +=item * a "has_many" relationship named _localizations, giving access +to all localizations of a particular table row for this particular property + +=item * a "might_have" relationship named _localization, giving +access to the localization of a particular table row for this particular +property and for the current language (uses C4::Languages::getlanguage) + +=back + +The "row" object will also gain a method C +which returns a specific translation and uses cache to avoid executing lots of +queries + +=cut + +use Modern::Perl; +use Carp; + +use base qw(DBIx::Class); + +=head2 localization_add_relationships + +Add relationships to the localization table + +=cut + +sub localization_add_relationships { + my ( $class, $pk_column, @properties ) = @_; + + my $rel_class = 'Koha::Schema::Result::Localization'; + my $source_name = $class =~ s/.*:://r; + + $class->has_many( + 'localizations', + $rel_class, + sub { + my ($args) = @_; + + return ( + { + "$args->{foreign_alias}.code" => { -ident => "$args->{self_alias}.$pk_column" }, + "$args->{foreign_alias}.entity" => $source_name, + }, + !$args->{self_result_object} ? () : { + "$args->{foreign_alias}.code" => $args->{self_result_object}->get_column($pk_column), + "$args->{foreign_alias}.entity" => $source_name, + }, + ); + }, + { cascade_copy => 0, cascade_delete => 1, cascade_update => 0 }, + ); + + foreach my $property (@properties) { + $class->might_have( + $property . '_localization', + $rel_class, + sub { + my ($args) = @_; + + # Not a 'use' because we don't want to load C4::Languages (and + # thus C4::Context) while loading the schema + require C4::Languages; + my $lang = C4::Languages::getlanguage(); + + return ( + { + "$args->{foreign_alias}.code" => { -ident => "$args->{self_alias}.$pk_column" }, + "$args->{foreign_alias}.entity" => $source_name, + "$args->{foreign_alias}.property" => $property, + "$args->{foreign_alias}.lang" => $lang, + }, + !$args->{self_result_object} ? () : { + "$args->{foreign_alias}.code" => $args->{self_result_object}->get_column($pk_column), + "$args->{foreign_alias}.entity" => $source_name, + "$args->{foreign_alias}.property" => $property, + "$args->{foreign_alias}.lang" => $lang, + }, + ); + }, + { cascade_copy => 0, cascade_delete => 0, cascade_update => 0 }, + ); + + $class->has_many( + $property . '_localizations', + $rel_class, + sub { + my ($args) = @_; + + return ( + { + "$args->{foreign_alias}.code" => { -ident => "$args->{self_alias}.$pk_column" }, + "$args->{foreign_alias}.entity" => $source_name, + "$args->{foreign_alias}.property" => $property, + }, + !$args->{self_result_object} ? () : { + "$args->{foreign_alias}.code" => $args->{self_result_object}->get_column($pk_column), + "$args->{foreign_alias}.entity" => $source_name, + "$args->{foreign_alias}.property" => $property, + }, + ); + }, + { cascade_copy => 0, cascade_delete => 0, cascade_update => 0 }, + ); + } +} + +sub localization { + my ( $self, $property, $lang ) = @_; + + my $result_source = $self->result_source; + + my $cache = Koha::Caches->get_instance('localization'); + my $cache_key = sprintf( '%s:%s', $result_source->source_name, $lang ); + my $localizations_map = $cache->get_from_cache($cache_key); + unless ($localizations_map) { + $localizations_map = {}; + + my $localizations = $result_source->schema->resultset('Localization')->search( { lang => $lang } ); + while ( my $localization = $localizations->next ) { + my $fk = $localization->get_column('code'); + my $localization_key = sprintf( '%s:%s', $fk, $localization->property ); + $localizations_map->{$localization_key} = $localization->translation; + } + + $cache->set_in_cache( $cache_key, $localizations_map ); + } + + my ($pk) = $self->id; + my $localization_key = sprintf( '%s:%s', $pk, $property ); + + return $localizations_map->{$localization_key}; +} + +1; diff --git a/Koha/Schema/Result/Itemtype.pm b/Koha/Schema/Result/Itemtype.pm index c29305bafe..ce784cd3c3 100644 --- a/Koha/Schema/Result/Itemtype.pm +++ b/Koha/Schema/Result/Itemtype.pm @@ -377,22 +377,8 @@ __PACKAGE__->add_columns( '+bookable' => { is_boolean => 1, is_nullable => 1 }, ); -# Use the ItemtypeLocalization view to create the join on localization -our $LANGUAGE; -__PACKAGE__->has_many( - "localization" => "Koha::Schema::Result::ItemtypeLocalization", - sub { - my $args = shift; - - die "no lang specified!" unless $LANGUAGE; - - return ({ - "$args->{self_alias}.itemtype" => { -ident => "$args->{foreign_alias}.code" }, - "$args->{foreign_alias}.lang" => $LANGUAGE, - }); - - } -); +__PACKAGE__->load_components('+Koha::Schema::Component::Localization'); +__PACKAGE__->localization_add_relationships('itemtype', 'description'); =head2 koha_object_class diff --git a/Koha/Schema/Result/ItemtypeLocalization.pm b/Koha/Schema/Result/ItemtypeLocalization.pm deleted file mode 100644 index d5966128c3..0000000000 --- a/Koha/Schema/Result/ItemtypeLocalization.pm +++ /dev/null @@ -1,32 +0,0 @@ -package Koha::Schema::Result::ItemtypeLocalization; - -use base 'DBIx::Class::Core'; - -use Modern::Perl; - -__PACKAGE__->table_class('DBIx::Class::ResultSource::View'); - -__PACKAGE__->table('itemtype_localizations'); -__PACKAGE__->result_source_instance->is_virtual(1); -__PACKAGE__->result_source_instance->view_definition( - "SELECT localization_id, code, lang, translation FROM localization WHERE entity='itemtypes'" -); - -__PACKAGE__->add_columns( - "localization_id", - { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, - "code", - { data_type => "varchar", is_nullable => 0, size => 64 }, - "lang", - { data_type => "varchar", is_nullable => 0, size => 25 }, - "translation", - { data_type => "text", is_nullable => 1 }, -); - -__PACKAGE__->belongs_to( - "itemtype", - "Koha::Schema::Result::Itemtype", - { code => 'itemtype' } -); - -1; diff --git a/Koha/UI/Form/Builder/Item.pm b/Koha/UI/Form/Builder/Item.pm index 033037d43d..907a8ebb2c 100644 --- a/Koha/UI/Form/Builder/Item.pm +++ b/Koha/UI/Form/Builder/Item.pm @@ -202,12 +202,11 @@ sub generate_subfield_form { } } elsif ( $subfield->{authorised_value} eq "itemtypes" ) { push @authorised_values, ""; - my $itemtypes; + my $itemtypes = Koha::ItemTypes->new; if ($branch_limit) { - $itemtypes = Koha::ItemTypes->search_with_localization( { branchcode => $branch_limit } ); - } else { - $itemtypes = Koha::ItemTypes->search_with_localization; + $itemtypes = $itemtypes->search_with_library_limits( {}, {}, $branch_limit ); } + $itemtypes = $itemtypes->order_by_translated_description; while ( my $itemtype = $itemtypes->next ) { push @authorised_values, $itemtype->itemtype; $authorised_lib{ $itemtype->itemtype } = $itemtype->translated_description; diff --git a/admin/itemtypes.pl b/admin/itemtypes.pl index eea77d2bb8..7b5adee618 100755 --- a/admin/itemtypes.pl +++ b/admin/itemtypes.pl @@ -31,7 +31,6 @@ use C4::Auth qw( get_template_and_user ); use C4::Output qw( output_html_with_http_headers ); use Koha::ItemTypes; use Koha::ItemType; -use Koha::Localizations; my $input = CGI->new; my $searchfield = $input->param('description'); diff --git a/admin/localization.pl b/admin/localization.pl index 3bb7440daf..04508e51ad 100755 --- a/admin/localization.pl +++ b/admin/localization.pl @@ -20,8 +20,9 @@ use Modern::Perl; use C4::Auth qw( get_template_and_user ); use C4::Output qw( output_html_with_http_headers ); -use Koha::Localization; -use Koha::Localizations; +use Koha::Database; + +my $schema = Koha::Database->schema; use CGI qw( -utf8 ); @@ -36,19 +37,20 @@ my ( $template, $borrowernumber, $cookie ) = get_template_and_user( } ); -my $entity = $query->param('entity'); -my $code = $query->param('code'); -my $rs = Koha::Localizations->search( { entity => $entity, code => $code } ); +my $source = $query->param('source'); +my $object_id = $query->param('object_id'); +my $property = $query->param('property'); + +my $row = $schema->resultset($source)->find($object_id); + my @translations; -while ( my $s = $rs->next ) { - push @translations, - { - id => $s->localization_id, - entity => $s->entity, - code => $s->code, - lang => $s->lang, - translation => $s->translation, - }; +my $localizations = $row->localizations->search( { property => $property } ); +while ( my $localization = $localizations->next ) { + push @translations, { + localization_id => $localization->id, + lang => $localization->lang, + translation => $localization->translation, + }; } my $translated_languages = C4::Languages::getTranslatedLanguages(); # opac and intranet @@ -56,8 +58,9 @@ my $translated_languages = C4::Languages::getTranslatedLanguages(); # opac an $template->param( translations => \@translations, languages => $translated_languages, - entity => $entity, - code => $code, + source => $source, + object_id => $object_id, + property => $property, ); output_html_with_http_headers $query, $cookie, $template->output; diff --git a/installer/data/mysql/atomicupdate/bug-38136.pl b/installer/data/mysql/atomicupdate/bug-38136.pl new file mode 100755 index 0000000000..f357e4d11c --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug-38136.pl @@ -0,0 +1,24 @@ +use Modern::Perl; +use Koha::Installer::Output qw(say_warning say_failure say_success say_info); + +return { + bug_number => '38136', + description => 'Add localization.property', + up => sub { + my ($args) = @_; + my ( $dbh, $out ) = @$args{qw(dbh out)}; + + unless ( column_exists( 'localization', 'property' ) ) { + $dbh->do("alter table `localization` add `property` varchar(100) null after `code`"); + $dbh->do("update `localization` set `property` = 'description', entity = 'Itemtype'"); + $dbh->do("alter table `localization` modify `property` varchar(100) not null"); + $dbh->do("alter table `localization` drop key `entity_code_lang`"); + $dbh->do( + "alter table `localization` add unique key `entity_code_property_lang` (`entity`, `code`, `property`, `lang`)" + ); + + say_success( $out, 'Added column localization.property and updated localization.entity values' ); + } + + }, +}; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index faa27cf7ea..e4f7b80cb0 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -4583,10 +4583,11 @@ CREATE TABLE `localization` ( `localization_id` int(11) NOT NULL AUTO_INCREMENT, `entity` varchar(16) NOT NULL, `code` varchar(64) NOT NULL, + `property` varchar(100) NOT NULL, `lang` varchar(25) NOT NULL COMMENT 'could be a foreign key', `translation` mediumtext DEFAULT NULL, PRIMARY KEY (`localization_id`), - UNIQUE KEY `entity_code_lang` (`entity`,`code`,`lang`) + UNIQUE KEY `entity_code_property_lang` (`entity`,`code`,`property`,`lang`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/localization-link.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/localization-link.inc new file mode 100644 index 0000000000..7bbce45b5c --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/localization-link.inc @@ -0,0 +1 @@ + Translate into other languages diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt index 7a99028cf4..ca70b6928f 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt @@ -194,7 +194,7 @@ Required [% IF can_be_translated %] - Translate into other languages + [% INCLUDE 'localization-link.inc' source='Itemtype' object_id=itemtype.itemtype property='description' %] [% END %]
  • diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/localization.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/localization.tt index eb593d31be..df26ca0550 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/localization.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/localization.tt @@ -5,12 +5,12 @@ [% INCLUDE 'doc-head-open.inc' %] [% FILTER collapse %] - [% SWITCH entity %] - [% CASE "itemtypes" %] - [% tx("Add translations for item type '{code}'", { code = code }) | html %] + [% SWITCH source %] + [% CASE "Itemtype" %] + [% tx("Add translations for item type '{code}'", { code = object_id }) | html %] › [% t("Item types") | html %] › [% t("Administration") | html %] [% CASE %] - [% tx("Add translations for '{code}'", { code = code }) | html %] + [% tx("Add translations for '{code}'", { code = object_id }) | html %] [% END %] › [% t("Koha") | html %] [% END %] @@ -29,8 +29,8 @@ [% END %] [% WRAPPER 'sub-header.inc' %] [% WRAPPER breadcrumbs %] - [% SWITCH entity %] - [% CASE "itemtypes" %] + [% SWITCH source %] + [% CASE "Itemtype" %] [% WRAPPER breadcrumb_item %] <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a> [% END %] @@ -38,37 +38,37 @@ <a href="/cgi-bin/koha/admin/itemtypes.pl">Item types</a> [% END %] [% WRAPPER breadcrumb_item %] - <a href="/cgi-bin/koha/admin/itemtypes.pl?op=add_form&itemtype=[% code | uri %]"> [% tx("Modify item type '{code}'", { code = code }) | html %] </a> + <a href="/cgi-bin/koha/admin/itemtypes.pl?op=add_form&itemtype=[% object_id | uri %]"> [% tx("Modify item type '{code}'", { code = object_id }) | html %] </a> [% END %] [% WRAPPER breadcrumb_item bc_active= 1 %] <span>Add translations</span> [% END %] [% CASE %] [% WRAPPER breadcrumb_item bc_active= 1 %] - [% tx("Add translations for '{code}'", { code = code }) | html %] + [% tx("Add translations for '{code}'", { code = object_id }) | html %] [% END %] [% END %] [% END #/ WRAPPER breadcrumbs %] [% END #/ WRAPPER sub-header.inc %] [% WRAPPER 'main-container.inc' %] - [% SWITCH entity %] - [% CASE "itemtypes" %] - <h1>[% tx("Add translations for item type '{code}'", { code = code }) | html %]</h1> + [% SWITCH source %] + [% CASE "Itemtype" %] + <h1>[% tx("Add translations for item type '{code}'", { code = object_id }) | html %]</h1> [% CASE %] <h1>[% tx("Add translations for '{code}'", { code = code }) | html %]</h1> [% END %] <form id="add_translation" method="get"> [% INCLUDE 'csrf-token.inc' %] - <input type="hidden" name="entity" value="[% entity | html %]" /> - <input type="hidden" name="code" value="[% code | html %]" /> - <input type="hidden" name="interface" value="[% interface_side | html %]" /> + <input type="hidden" name="source" value="[% source | html %]" /> + <input type="hidden" name="object_id" value="[% object_id | html %]" /> + <input type="hidden" name="property" value="[% property | html %]" /> <fieldset class="rows clearfix"> <ol> <li> - <span class="label">Authorized value:</span> - [% code | html %] + <span class="label">Code:</span> + [% object_id | html %] </li> <li> <label for="lang">Language:</label> @@ -100,9 +100,6 @@ <table id="localization"> <thead> <tr> - <th>Id</th> - <th>Entity</th> - <th>Code</th> <th class="lang">Language</th> <th class="translation">Translation</th> <th class="no-sort"> </th> @@ -110,17 +107,7 @@ </thead> <tbody> [% FOR t IN translations %] - <tr id="row_id_[% t.id | html %]" data-id="[% t.id | html %]"> - <td>[% t.id | html %]</td> - <td> - [% SWITCH entity %] - [% CASE "itemtypes" %] - Item type - [% CASE %] - [% t.entity | html %] - [% END %] - </td> - <td>[% t.code | html %]</td> + <tr id="row_id_[% t.localization_id | html %]" data-id="[% t.localization_id | html %]"> <td class="lang">[% t.lang | html %]</td> <td class="translation" contenteditable="true">[% t.translation | html %]</td> <td class="actions" @@ -149,7 +136,7 @@ var message; if (type == "success_on_update") { message = $('<div class="alert alert-info"></div>'); - message.text(_("Entity %s (code %s) for lang %s has been updated with '%s'").format(data.entity, data.code, data.lang, data.translation)); + message.text(_("Translation for lang %s has correctly been updated with '%s'").format(data.lang, data.translation)); } else if (type == "error_on_update") { message = $('<div class="alert alert-warning"></div>'); if (data.error_code == "already_exists") { @@ -159,13 +146,13 @@ } } else if (type == "success_on_delete") { message = $('<div class="alert alert-info"></div>'); - message.text(_("The translation (id %s) has been removed successfully").format(data.id)); + message.text(_("The translation has been removed successfully")); } else if (type == "error_on_delete") { message = $('<div class="alert alert-warning"></div>'); message.text(_("An error occurred when deleting this translation")); } else if (type == "success_on_insert") { message = $('<div class="alert alert-info"></div>'); - message.text(_("Translation (id %s) has been added successfully").format(data.id)); + message.text(_("Translation has been added successfully")); } else if (type == "error_on_insert") { message = $('<div class="alert alert-warning"></div>'); if (data.error_code == "already_exists") { @@ -182,14 +169,18 @@ }, 3000); } - function send_update_request(data, cell) { + function send_update_request(_data, cell) { + const form = document.forms.add_translation; + const source = form.elements.source.value; + const object_id = form.elements.object_id.value; + const data = Object.assign({}, _data, { source, object_id }); const client = APIClient.localization; client.localizations.update(data).then( success => { if (success.error) { $(cell).css("background-color", "#FF0000"); show_message({ type: "error_on_update", data: success }); - } else if (success.is_changed == 1) { + } else { $(cell).css("background-color", "#00FF00"); show_message({ type: "success_on_update", data: success }); } @@ -213,16 +204,22 @@ ); } - function send_delete_request(id, cell) { + function send_delete_request(localization_id, cell) { + const form = document.forms.add_translation; + const source = form.elements.source.value; + const object_id = form.elements.object_id.value; + const property = form.elements.property.value; + const data = { source, object_id, property, localization_id }; + const client = APIClient.localization; - client.localizations.delete(id).then( + client.localizations.delete(data).then( success => { $("#localization") .DataTable() - .row("#row_id_" + id) + .row("#row_id_" + localization_id) .remove() .draw(); - show_message({ type: "success_on_delete", data: { id } }); + show_message({ type: "success_on_delete", data: { localization_id } }); }, error => { $(cell).css("background-color", "#FF9090"); @@ -247,6 +244,10 @@ }); $("body").on("click", "td.lang", function () { var td = $(this); + if (td[0].childElementCount > 0) { + // do nothing if there is already something there (like a select for instance) + return; + } var tr = td.parent(); var id = tr.data("id"); var lang = td.text(); @@ -258,9 +259,9 @@ }); var lang; my_select.on("change", function () { - lang = td.find("option:selected").val(); - var translation = tr.find("td.translation").text(); - send_update_request({ id, lang, translation }, td[0]); + var localization_id = $(tr).data('id'); + lang = td.find('option:selected').val(); + send_update_request( {localization_id, lang}, td[0] ); }); my_select.on("blur", function () { td.html(lang); @@ -270,10 +271,9 @@ $("body").on("blur", "td.translation", function () { var tr = $(this).parent(); - var id = $(tr).data("id"); - var lang = $(tr).find("td.lang").text(); + var localization_id = $(tr).data('id'); var translation = $(this).text(); - send_update_request({ id, lang, translation }, this); + send_update_request( {localization_id, translation}, this ); }); $("body").on("click", "a.delete", function (e) { @@ -291,25 +291,28 @@ $("#add_translation").on("submit", function (e) { e.preventDefault(); - let localization = { - entity: $(this).find('input[name="entity"]').val(), - code: $(this).find('input[name="code"]').val(), - lang: $(this).find('select[name="lang"] option:selected').val(), - translation: $(this).find('input[name="translation"]').val(), - }; + + const form = this; + const source = form.elements.source.value; + const object_id = form.elements.object_id.value; + const property = form.elements.property.value; + const lang = form.elements.lang.value + const translation = form.elements.translation.value + + let localization = { source, object_id, property, lang, translation }; const client = APIClient.localization; client.localizations.create(localization).then( success => { if (success.error) { show_message({ type: "error_on_insert", data: success }); } else { - var new_row = table_dt.row - .add([success.id, success.entity, success.code, success.lang, success.translation, '<a href="#" class="btn btn-default btn-xs delete"><i class="fa fa-trash-can"></i> Delete</a>']) + var new_row = table.api().row + .add([success.lang, success.translation, '<a href="#" class="btn btn-xs btn-default delete"><i class="fa fa-trash-can"></i> %s</a>'.format(escape_str(_("Delete")))]) .draw() .node(); $(new_row) - .attr("id", "row_id_" + success.id) - .data("id", success.id); + .attr("id", "row_id_" + success.localization_id) + .data("id", success.localization_id); $(new_row.children[th_lang_index]).prop("contenteditable", true).addClass("lang"); $(new_row.children[th_translation_index]).prop("contenteditable", true).addClass("translation"); show_message({ type: "success_on_insert", data: success }); diff --git a/koha-tmpl/intranet-tmpl/prog/js/fetch/localization-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/fetch/localization-api-client.js index 36d4b521d9..f27b0427b3 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/fetch/localization-api-client.js +++ b/koha-tmpl/intranet-tmpl/prog/js/fetch/localization-api-client.js @@ -10,12 +10,7 @@ export class LocalizationAPIClient { create: localization => this.httpClient.post({ endpoint: "", - body: "entity=%s&code=%s&lang=%s&translation=%s".format( - encodeURIComponent(localization.entity), - encodeURIComponent(localization.code), - encodeURIComponent(localization.lang), - encodeURIComponent(localization.translation) - ), + body: new URLSearchParams(localization).toString(), headers: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", @@ -24,23 +19,16 @@ export class LocalizationAPIClient { update: localization => this.httpClient.put({ endpoint: "", - body: "id=%s&lang=%s&translation=%s".format( - encodeURIComponent(localization.id), - encodeURIComponent(localization.lang), - encodeURIComponent(localization.translation) - ), + body: new URLSearchParams(localization).toString(), headers: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", }, }), - delete: id => + delete: localization => this.httpClient.delete({ - endpoint: "/?id=%s".format(id), - headers: { - "Content-Type": - "application/x-www-form-urlencoded;charset=utf-8", - }, + endpoint: + "?" + new URLSearchParams(localization).toString(), }), }; } diff --git a/svc/localization b/svc/localization index f44d1a6c04..ffd1a0315f 100755 --- a/svc/localization +++ b/svc/localization @@ -2,111 +2,114 @@ use Modern::Perl; use Encode qw( encode ); +use Try::Tiny; +use JSON qw( to_json ); use C4::Service; -use Koha::Localizations; +use Koha::Caches; +use Koha::Database; +use C4::Output qw( output_with_http_headers ); our ( $query, $response ) = C4::Service->init( parameters => 'manage_itemtypes' ); -sub get_translations { - my $rs = Koha::Localizations->search( { entity => $query->param('entity'), code => $query->param('code') } ); - my @translations; - while ( my $s = $rs->next ) { - push @translations, { - id => $s->localization_id, - entity => $s->entity, - code => $s->code, - lang => $s->lang, - translation => $s->translation, - }; - } - $response->param( translations => \@translations ); - C4::Service->return_success($response); -} - sub update_translation { - my $id = $query->param('id'); - my $translation = $query->param('translation'); - my $lang = $query->param('lang'); + my $source = $query->param('source'); + my $localization_id = $query->param('localization_id'); + my $object_id = $query->param('object_id'); + my $lang = $query->param('lang'); + my $translation = $query->param('translation'); - my $localization = Koha::Localizations->find($id); - if ( defined $lang and $localization->lang ne $lang ) { - $localization->lang($lang); - } - if ( defined $translation and $localization->translation ne $translation ) { - $localization->translation($translation); - } - my %params; - my $is_changed; - if ( $localization->is_changed ) { - $is_changed = 1; - unless ( - Koha::Localizations->search( - { - entity => $localization->entity, code => $localization->code, lang => $lang, - localization_id => { '!=' => $localization->localization_id }, - } - )->count - ) - { - $localization->store; - } else { - $params{error} = 1; - $params{error_code} = 'already_exists'; + my $schema = Koha::Database->schema; + my $row = $schema->resultset($source)->find($object_id); + if ($row) { + my $localization = $row->localizations->find($localization_id); + if ($localization) { + try { + my $original_lang = $localization->lang; + $localization->lang($lang) if $lang; + $localization->translation($translation) if $translation; + $localization->update(); + Koha::Caches->get_instance('localization')->clear_from_cache("$source:$original_lang"); + Koha::Caches->get_instance('localization')->clear_from_cache("$source:$lang") if $lang; + } catch { + $localization->discard_changes(); + $response->param( error => 1, error_code => 'already_exists' ); + }; } + + $response->param( + lang => $localization->lang, + translation => $localization->translation, + ); } - $response->param( - %params, - id => $localization->localization_id, - entity => $localization->entity, - code => $localization->code, - lang => $localization->lang, - translation => $localization->translation, - is_changed => $is_changed, - ); + C4::Service->return_success($response); } sub add_translation { - my $entity = $query->param('entity'); - my $code = $query->param('code'); - my $lang = $query->param('lang'); - my $translation = $query->param('translation'); + my $source = $query->param('source'); + my $localization_id = $query->param('localization_id'); + my $object_id = $query->param('object_id'); + my $property = $query->param('property'); + my $lang = $query->param('lang'); + my $translation = $query->param('translation'); - unless ( Koha::Localizations->search( { entity => $entity, code => $code, lang => $lang, } )->count ) { - my $localization = Koha::Localization->new( + my $schema = Koha::Database->schema; + my $row = $schema->resultset($source)->find($object_id); + try { + my $localization = $row->create_related( + "${property}_localizations", { - entity => $entity, - code => $code, + entity => $source, lang => $lang, translation => $translation, } ); - $localization->store; - $response->param( - id => $localization->localization_id, - entity => $localization->entity, - code => $localization->code, - lang => $localization->lang, - translation => $localization->translation, - ); + Koha::Caches->get_instance('localization')->clear_from_cache("$source:$lang"); + + $response->param( + lang => $localization->lang, + translation => $localization->translation, + localization_id => $localization->id, + ); + } catch { + $response->param( error => 1, error_code => 'already_exists' ); + }; - } else { - $response->param( error => 1, error_code => 'already_exists', ); - } C4::Service->return_success($response); } sub delete_translation { - my $id = $query->param('id'); - Koha::Localizations->find($id)->delete; - $response->param( id => $id ); + my $source = $query->param('source'); + my $object_id = $query->param('object_id'); + my $localization_id = $query->param('localization_id'); + + my $schema = Koha::Database->schema; + my $row = $schema->resultset($source)->find($object_id); + if ( $row && $row->can('localizations') ) { + my $localization = $row->localizations->find($localization_id); + + unless ($localization) { + my $json = to_json( { errors => 'Not found' } ); + output_with_http_headers $query, undef, $json, 'js', '404 Not Found'; + exit; + } + + if ($localization) { + $localization->delete(); + Koha::Caches->get_instance('localization')->clear_from_cache( "$source:" . $localization->lang ); + } + + $response->param( + localization_id => $localization_id, + ); + } + C4::Service->return_success($response); } C4::Service->dispatch( - [ 'GET /', ['id'], \&get_translations ], - [ 'PUT /', ['id'], \&update_translation ], - [ 'POST /', [ 'entity', 'code', 'lang', 'translation' ], \&add_translation ], - [ 'DELETE /', ['id'], \&delete_translation ], + [ 'PUT /', [], \&update_translation ], + [ 'POST /', [], \&add_translation ], + [ 'DELETE /', [], \&delete_translation ], ); diff --git a/t/db_dependent/Koha/Filter/ExpandCodedFields.t b/t/db_dependent/Koha/Filter/ExpandCodedFields.t index e2ed0524a5..56c989fe18 100755 --- a/t/db_dependent/Koha/Filter/ExpandCodedFields.t +++ b/t/db_dependent/Koha/Filter/ExpandCodedFields.t @@ -71,10 +71,11 @@ subtest 'ExpandCodedFields tests' => sub { # Clear GetAuthorisedValueDesc-generated cache $cache->clear_from_cache("libraries:name"); - $cache->clear_from_cache("itemtype:description:en"); $cache->clear_from_cache("cn_sources:description"); $cache->clear_from_cache( "AV_descriptions:" . $av->category ); + Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en'); + C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); $record = $biblio->metadata->record; diff --git a/t/db_dependent/Koha/Item.t b/t/db_dependent/Koha/Item.t index 920c3c01da..3731c6f66e 100755 --- a/t/db_dependent/Koha/Item.t +++ b/t/db_dependent/Koha/Item.t @@ -30,6 +30,7 @@ use C4::Biblio qw( GetMarcSubfieldStructure ); use C4::Circulation qw( AddIssue AddReturn ); use Koha::Caches; +use Koha::Cache::Memory::Lite; use Koha::Items; use Koha::Database; use Koha::DateUtils qw( dt_from_string ); @@ -2455,10 +2456,11 @@ subtest 'columns_to_str' => sub { $cache->clear_from_cache("MarcStructure-1-"); $cache->clear_from_cache("MarcSubfieldStructure-"); $cache->clear_from_cache("libraries:name"); - $cache->clear_from_cache("itemtype:description:en"); $cache->clear_from_cache("cn_sources:description"); $cache->clear_from_cache("AV_descriptions:LOST"); + Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en'); + # Creating subfields 'é', 'è' that are not linked with a kohafield Koha::MarcSubfieldStructures->search( { @@ -2540,10 +2542,11 @@ subtest 'columns_to_str' => sub { $cache->clear_from_cache("MarcStructure-1-"); $cache->clear_from_cache("MarcSubfieldStructure-"); $cache->clear_from_cache("libraries:name"); - $cache->clear_from_cache("itemtype:description:en"); $cache->clear_from_cache("cn_sources:description"); $cache->clear_from_cache("AV_descriptions:LOST"); + Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en'); + $schema->storage->txn_rollback; }; @@ -2560,10 +2563,11 @@ subtest 'strings_map() tests' => sub { $cache->clear_from_cache("MarcStructure-1-"); $cache->clear_from_cache("MarcSubfieldStructure-"); $cache->clear_from_cache("libraries:name"); - $cache->clear_from_cache("itemtype:description:en"); $cache->clear_from_cache("cn_sources:description"); $cache->clear_from_cache("AV_descriptions:LOST"); + Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en'); + # Recreating subfields just to be sure tests will be ok # 1 => av (LOST) # 3 => no link @@ -2675,6 +2679,7 @@ subtest 'strings_map() tests' => sub { )->store(); Koha::Caches->get_instance->flush_all; + Koha::Cache::Memory::Lite->get_instance->flush; $item->set( { @@ -2747,9 +2752,10 @@ subtest 'strings_map() tests' => sub { $cache->clear_from_cache("MarcStructure-1-"); $cache->clear_from_cache("MarcSubfieldStructure-"); $cache->clear_from_cache("libraries:name"); - $cache->clear_from_cache("itemtype:description:en"); $cache->clear_from_cache("cn_sources:description"); + Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en'); + $schema->storage->txn_rollback; }; diff --git a/t/db_dependent/Koha/ItemTypes.t b/t/db_dependent/Koha/ItemTypes.t index 0970ab6200..e54dccf5d1 100755 --- a/t/db_dependent/Koha/ItemTypes.t +++ b/t/db_dependent/Koha/ItemTypes.t @@ -27,6 +27,7 @@ use t::lib::TestBuilder; use C4::Calendar; use Koha::Biblioitems; +use Koha::Caches; use Koha::Libraries; use Koha::Database; use Koha::DateUtils qw(dt_from_string); @@ -73,30 +74,29 @@ my $child3 = $builder->build_object( } ); -Koha::Localization->new( +$child1->_result->localizations->create( { - entity => 'itemtypes', - code => $child1->itemtype, + property => 'description', lang => 'en', translation => 'b translated itemtype desc' } -)->store; -Koha::Localization->new( +); +$child2->_result->localizations->create( { - entity => 'itemtypes', - code => $child2->itemtype, + property => 'description', lang => 'en', translation => 'a translated itemtype desc' } -)->store; -Koha::Localization->new( +); +$child3->_result->localizations->create( { - entity => 'something_else', - code => $child2->itemtype, + property => 'description', lang => 'en', translation => 'another thing' } -)->store; +); + +Koha::Caches->get_instance('localization')->flush_all(); my $type = Koha::ItemTypes->find( $child1->itemtype ); ok( defined($type), 'first result' ); diff --git a/t/db_dependent/Koha/Template/Plugin/ItemTypes.t b/t/db_dependent/Koha/Template/Plugin/ItemTypes.t index 073a93d784..a4419bac30 100755 --- a/t/db_dependent/Koha/Template/Plugin/ItemTypes.t +++ b/t/db_dependent/Koha/Template/Plugin/ItemTypes.t @@ -20,6 +20,7 @@ use Test::NoWarnings; use Test::More tests => 11; use C4::Context; +use Koha::Caches; use Koha::Database; use Koha::ItemTypes; @@ -53,14 +54,13 @@ my $itemtypeA = $builder->build_object( } } ); -Koha::Localization->new( +$itemtypeA->_result->localizations->create( { - entity => 'itemtypes', - code => $itemtypeA->itemtype, + property => 'description', lang => 'en', translation => 'Translated itemtype A' } -)->store; +); my $itemtypeB = $builder->build_object( { class => 'Koha::ItemTypes', @@ -70,14 +70,13 @@ my $itemtypeB = $builder->build_object( } } ); -Koha::Localization->new( +$itemtypeB->_result->localizations->create( { - entity => 'itemtypes', - code => $itemtypeB->itemtype, + property => 'description', lang => 'en', translation => 'Translated itemtype B' } -)->store; +); my $itemtypeC = $builder->build_object( { class => 'Koha::ItemTypes', @@ -88,6 +87,8 @@ my $itemtypeC = $builder->build_object( } ); +Koha::Caches->get_instance('localization')->flush_all(); + my $GetDescriptionA1 = $plugin->GetDescription( $itemtypeA->itemtype ); is( $GetDescriptionA1, "Translated itemtype A", "ItemType without parent - GetDescription without want parent" ); my $GetDescriptionA2 = $plugin->GetDescription( $itemtypeA->itemtype, 1 ); diff --git a/t/db_dependent/api/v1/item_types.t b/t/db_dependent/api/v1/item_types.t index dfab04d75a..709914e201 100755 --- a/t/db_dependent/api/v1/item_types.t +++ b/t/db_dependent/api/v1/item_types.t @@ -26,6 +26,7 @@ use t::lib::Mocks; use Mojo::JSON qw(encode_json); +use Koha::Caches; use Koha::ItemTypes; use Koha::Database; @@ -66,29 +67,23 @@ subtest 'list() tests' => sub { } ); - my $en = $builder->build_object( + $item_type->_result->localizations->create( { - class => 'Koha::Localizations', - value => { - entity => 'itemtypes', - code => $item_type->id, - lang => 'en', - translation => 'English word "test"', - } + property => 'description', + lang => 'en', + translation => 'English word "test"', } ); - my $sv = $builder->build_object( + $item_type->_result->localizations->create( { - class => 'Koha::Localizations', - value => { - entity => 'itemtypes', - code => $item_type->id, - lang => 'sv_SE', - translation => 'Swedish word "test"', - } + property => 'description', + lang => 'sv_SE', + translation => 'Swedish word "test"', } ); + Koha::Caches->get_instance('localization')->flush_all(); + my $librarian = $builder->build_object( { class => 'Koha::Patrons',