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

(-)a/C4/Biblio.pm (-8 / +9 lines)
Lines 95-100 use C4::Linker; Link Here
95
use C4::OAI::Sets;
95
use C4::OAI::Sets;
96
96
97
use Koha::Logger;
97
use Koha::Logger;
98
use Koha::Cache::Memory::Lite;
98
use Koha::Caches;
99
use Koha::Caches;
99
use Koha::ClassSources;
100
use Koha::ClassSources;
100
use Koha::Authority::Types;
101
use Koha::Authority::Types;
Lines 1522-1536 sub GetAuthorisedValueDesc { Link Here
1522
1523
1523
        #---- itemtypes
1524
        #---- itemtypes
1524
        if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "itemtypes" ) {
1525
        if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "itemtypes" ) {
1525
            my $lang = C4::Languages::getlanguage;
1526
            my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
1526
            $lang //= 'en';
1527
            my $cache_key    = 'GetAuthorisedValueDesc:itemtypes';
1527
            $cache_key = 'itemtype:description:' . $lang;
1528
            my $itemtypes    = $memory_cache->get_from_cache($cache_key);
1528
            my $itypes = $cache->get_from_cache( $cache_key, { unsafe => 1 } );
1529
            unless ($itemtypes) {
1529
            if ( !$itypes ) {
1530
                $itemtypes = { map { $_->itemtype => $_ } Koha::ItemTypes->as_list };
1530
                $itypes = { map { $_->itemtype => $_->translated_description } Koha::ItemTypes->search()->as_list };
1531
                $memory_cache->set_in_cache( $cache_key, $itemtypes );
1531
                $cache->set_in_cache( $cache_key, $itypes );
1532
            }
1532
            }
1533
            return $itypes->{$value};
1533
            my $itemtype = $itemtypes->{$value};
1534
            return $itemtype ? $itemtype->translated_description : undef;
1534
        }
1535
        }
1535
1536
1536
        if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "cn_source" ) {
1537
        if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "cn_source" ) {
(-)a/Koha/ItemType.pm (-72 / +26 lines)
Lines 19-33 use Modern::Perl; Link Here
19
19
20
use C4::Koha qw( getitemtypeimagelocation );
20
use C4::Koha qw( getitemtypeimagelocation );
21
use C4::Languages;
21
use C4::Languages;
22
use Koha::Caches;
23
use Koha::Database;
22
use Koha::Database;
24
use Koha::CirculationRules;
23
use Koha::CirculationRules;
25
use Koha::Localizations;
26
24
27
use base qw(Koha::Object Koha::Object::Limit::Library);
25
use base qw(Koha::Object Koha::Object::Limit::Library);
28
26
29
my $cache = Koha::Caches->get_instance();
30
31
=head1 NAME
27
=head1 NAME
32
28
33
Koha::ItemType - Koha Item type Object class
29
Koha::ItemType - Koha Item type Object class
Lines 36-83 Koha::ItemType - Koha Item type Object class Link Here
36
32
37
=head2 Class methods
33
=head2 Class methods
38
34
39
=cut
40
41
=head3 store
42
43
ItemType specific store to ensure relevant caches are flushed on change
44
45
=cut
46
47
sub store {
48
    my ($self) = @_;
49
50
    my $flush = 0;
51
52
    if ( !$self->in_storage ) {
53
        $flush = 1;
54
    } else {
55
        my $self_from_storage = $self->get_from_storage;
56
        $flush = 1 if ( $self_from_storage->description ne $self->description );
57
    }
58
59
    $self = $self->SUPER::store;
60
61
    if ($flush) {
62
        my $key = "itemtype:description:en";
63
        $cache->clear_from_cache($key);
64
    }
65
66
    return $self;
67
}
68
69
=head2 delete
70
71
ItemType specific C<delete> to clear relevant caches on delete.
72
73
=cut
74
75
sub delete {
76
    my $self = shift @_;
77
    $cache->clear_from_cache('itemtype:description:en');
78
    $self->SUPER::delete(@_);
79
}
80
81
=head3 image_location
35
=head3 image_location
82
36
83
=cut
37
=cut
Lines 93-118 sub image_location { Link Here
93
47
94
sub translated_description {
48
sub translated_description {
95
    my ( $self, $lang ) = @_;
49
    my ( $self, $lang ) = @_;
96
    if ( my $translated_description = eval { $self->get_column('translated_description') } ) {
97
50
98
        # If the value has already been fetched (eg. from sarch_with_localization),
51
    my $localization = $self->localization( 'description', $lang || C4::Languages::getlanguage() );
99
        # do not search for it again
100
        # Note: This is a bit hacky but should be fast
101
        return $translated_description
102
            ? $translated_description
103
            : $self->description;
104
    }
105
    $lang ||= C4::Languages::getlanguage;
106
    my $translated_description = Koha::Localizations->search(
107
        {
108
            code   => $self->itemtype,
109
            entity => 'itemtypes',
110
            lang   => $lang
111
        }
112
    )->next;
113
    return $translated_description
114
        ? $translated_description->translation
115
        : $self->description;
116
}
52
}
117
53
118
=head3 translated_descriptions
54
=head3 translated_descriptions
Lines 121-139 sub translated_description { Link Here
121
57
122
sub translated_descriptions {
58
sub translated_descriptions {
123
    my ($self) = @_;
59
    my ($self) = @_;
124
    my @translated_descriptions = Koha::Localizations->search(
60
125
        {
126
            entity => 'itemtypes',
127
            code   => $self->itemtype,
128
        }
129
    )->as_list;
130
    return [
61
    return [
131
        map {
62
        map {
132
            {
63
            {
133
                lang        => $_->lang,
64
                lang        => $_->lang,
134
                translation => $_->translation,
65
                translation => $_->translation,
135
            }
66
            }
136
        } @translated_descriptions
67
        } $self->_result->description_localizations
137
    ];
68
    ];
138
}
69
}
139
70
Lines 235-243 sub to_api_mapping { Link Here
235
        rentalcharge_hourly          => 'hourly_rental_charge',
166
        rentalcharge_hourly          => 'hourly_rental_charge',
236
        rentalcharge_hourly_calendar => 'hourly_rental_charge_calendar',
167
        rentalcharge_hourly_calendar => 'hourly_rental_charge_calendar',
237
        bookable_itemtype            => 'bookable_itemtype',
168
        bookable_itemtype            => 'bookable_itemtype',
169
170
        # TODO Remove after having updated all code using unblessed translated_description
171
        translated_description => undef,
238
    };
172
    };
239
}
173
}
240
174
175
=head3 unblessed
176
177
See L<Koha::Object/unblessed>
178
179
Overridden to add a C<translated_description> key for backward compatibility.
180
This should not be relied on as it may be removed in the future.
181
182
=cut
183
184
# TODO Remove after having updated all code using unblessed translated_description
185
sub unblessed {
186
    my ($self) = @_;
187
188
    my $unblessed = $self->SUPER::unblessed();
189
190
    $unblessed->{translated_description} = $self->translated_description;
191
192
    return $unblessed;
193
}
194
241
=head2 Internal methods
195
=head2 Internal methods
242
196
243
=head3 _type
197
=head3 _type
(-)a/Koha/ItemTypes.pm (-18 / +16 lines)
Lines 32-39 Koha::ItemTypes - Koha ItemType Object set class Link Here
32
32
33
=head2 Class methods
33
=head2 Class methods
34
34
35
=cut
36
37
=head3 search_with_localization
35
=head3 search_with_localization
38
36
39
my $itemtypes = Koha::ItemTypes->search_with_localization
37
my $itemtypes = Koha::ItemTypes->search_with_localization
Lines 43-64 my $itemtypes = Koha::ItemTypes->search_with_localization Link Here
43
sub search_with_localization {
41
sub search_with_localization {
44
    my ( $self, $params, $attributes ) = @_;
42
    my ( $self, $params, $attributes ) = @_;
45
43
46
    my $language = C4::Languages::getlanguage();
44
    return $self->search( $params, $attributes )->order_by_translated_description;
47
    $Koha::Schema::Result::Itemtype::LANGUAGE = $language;
45
}
48
    $attributes->{order_by}                   = 'translated_description' unless exists $attributes->{order_by};
46
49
    $attributes->{join}                       = 'localization';
47
=head3 order_by_translated_description
50
    $attributes->{'+select'}                  = [
48
51
        {
49
=cut
52
            coalesce => [qw( localization.translation me.description )],
50
53
            -as      => 'translated_description'
51
sub order_by_translated_description {
54
        }
52
    my ($self) = @_;
55
    ];
53
56
    if ( defined $params->{branchcode} ) {
54
    my $attributes = {
57
        my $branchcode = delete $params->{branchcode};
55
        join     => 'description_localization',
58
        $self->search_with_library_limits( $params, $attributes, $branchcode );
56
        order_by => \['COALESCE(description_localization.translation, me.description)'],
59
    } else {
57
    };
60
        $self->SUPER::search( $params, $attributes );
58
61
    }
59
    return $self->search( {}, $attributes );
62
}
60
}
63
61
64
=head2 Internal methods
62
=head2 Internal methods
(-)a/Koha/Localization.pm (-74 lines)
Lines 1-74 Link Here
1
package Koha::Localization;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <https://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Koha::Database;
21
22
use base qw(Koha::Object);
23
24
use Koha::Caches;
25
my $cache = Koha::Caches->get_instance();
26
27
=head1 NAME
28
29
Koha::Localization - Koha Localization type Object class
30
31
=head1 API
32
33
=head2 Class methods
34
35
=cut
36
37
=head3 store
38
39
Localization specific store to ensure relevant caches are flushed on change
40
41
=cut
42
43
sub store {
44
    my ($self) = @_;
45
    $self = $self->SUPER::store;
46
47
    if ( $self->entity eq 'itemtypes' ) {
48
        my $key = "itemtype:description:" . $self->lang;
49
        $cache->clear_from_cache($key);
50
    }
51
52
    return $self;
53
}
54
55
=head2 delete
56
57
Localization specific C<delete> to clear relevant caches on delete.
58
59
=cut
60
61
sub delete {
62
    my $self = shift @_;
63
    if ( $self->entity eq 'itemtypes' ) {
64
        my $key = "itemtype:description:" . $self->lang;
65
        $cache->clear_from_cache($key);
66
    }
67
    $self->SUPER::delete(@_);
68
}
69
70
sub _type {
71
    return 'Localization';
72
}
73
74
1;
(-)a/Koha/Localizations.pm (-34 lines)
Lines 1-34 Link Here
1
package Koha::Localizations;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <https://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Koha::Database;
21
22
use Koha::Localization;
23
24
use base qw(Koha::Objects);
25
26
sub _type {
27
    return 'Localization';
28
}
29
30
sub object_class {
31
    return 'Koha::Localization';
32
}
33
34
1;
(-)a/Koha/Object.pm (+26 lines)
Lines 956-961 sub unblessed_all_relateds { Link Here
956
    return \%data;
956
    return \%data;
957
}
957
}
958
958
959
=head3 localization
960
961
Returns a localized (translated) value of the property, or the original
962
property value if no translation exist
963
964
    $localization = $object->localization($property, $lang);
965
966
C<$property> is the property name. Often this will correspond to an SQL column name
967
968
C<$lang> is the language code (for instance: 'en-GB')
969
970
=cut
971
972
sub localization {
973
    my ( $self, $property, $lang ) = @_;
974
975
    my $result = $self->_result;
976
    if ( $result->can('localization') ) {
977
        if ( my $localization = $result->localization( $property, $lang ) ) {
978
            return $localization;
979
        }
980
    }
981
982
    return $result->get_column($property);
983
}
984
959
=head3 $object->_result();
985
=head3 $object->_result();
960
986
961
Returns the internal DBIC Row object
987
Returns the internal DBIC Row object
(-)a/Koha/Schema/Component/Localization.pm (+214 lines)
Line 0 Link Here
1
package Koha::Schema::Component::Localization;
2
3
=head1 NAME
4
5
Koha::Schema::Component::Localization
6
7
=head1 SYNOPSIS
8
9
    package Koha::Schema::Result::SomeTable;
10
11
    # ... generated code ...
12
13
    __PACKAGE__->load_components('+Koha::Schema::Component::Localization');
14
15
    __PACKAGE__->localization_add_relationships(
16
        'some_table_localizations',
17
        'some_table_id' => 'some_table_id',
18
        'first_column',
19
        'second_column',
20
        # ...
21
    );
22
23
    package main;
24
25
    my $rows = $schema->resultset('SomeTable');
26
    my $row = $rows->first;
27
    $row->localizations->search($cond);
28
    $row->create_related('first_column_localizations', { lang => $lang, translation => $translation })
29
30
    while (my $row = $rows->next)
31
        # first call will fetch all localizations for the current language and
32
        # the result will be cached, next calls will not execute a query
33
        $row->localization('first_column', $lang);
34
35
        # no query executed
36
        $row->localization('second_column', $lang);
37
    }
38
39
=head1 DESCRIPTION
40
41
This is a DBIx::Class component that helps to manage database localizations by
42
adding several relationships and methods to a "Result Class"
43
44
This can handle several localizable columns (also referred as "properties") per
45
table
46
47
To add database localizations to an existing database table, you need to:
48
49
=over
50
51
=item * Create a new table with:
52
53
=over
54
55
=item * An auto incremented column as primary key
56
57
=item * A foreign key column referencing the existing table
58
59
=item * 3 string (varchar or text) columns named 'property', 'lang',
60
'translation'
61
62
=item * A unique key comprising the foreign key column, 'property' and 'lang'
63
64
=back
65
66
=item * Regenerate the DBIx::Class schema with
67
misc/devel/update_dbix_class_files.pl
68
69
=item * Add calls to load_components and localization_add_relationships at the
70
end of the result class
71
72
=back
73
74
This will give you a relationship named 'localizations' through which you can
75
access all localizations of a particular table row.
76
77
And for every property, you will have:
78
79
=over
80
81
=item * a "has_many" relationship named <property>_localizations, giving access
82
to all localizations of a particular table row for this particular property
83
84
=item * a "might_have" relationship named <property>_localization, giving
85
access to the localization of a particular table row for this particular
86
property and for the current language (uses C4::Languages::getlanguage)
87
88
=back
89
90
The "row" object will also gain a method C<localization($property, $lang)>
91
which returns a specific translation and uses cache to avoid executing lots of
92
queries
93
94
=cut
95
96
use Modern::Perl;
97
use Carp;
98
99
use base qw(DBIx::Class);
100
101
=head2 localization_add_relationships
102
103
Add relationships to the localization table
104
105
=cut
106
107
sub localization_add_relationships {
108
    my ( $class, $pk_column, @properties ) = @_;
109
110
    my $rel_class   = 'Koha::Schema::Result::Localization';
111
    my $source_name = $class =~ s/.*:://r;
112
113
    $class->has_many(
114
        'localizations',
115
        $rel_class,
116
        sub {
117
            my ($args) = @_;
118
119
            return (
120
                {
121
                    "$args->{foreign_alias}.code"   => { -ident => "$args->{self_alias}.$pk_column" },
122
                    "$args->{foreign_alias}.entity" => $source_name,
123
                },
124
                !$args->{self_result_object} ? () : {
125
                    "$args->{foreign_alias}.code"   => $args->{self_result_object}->get_column($pk_column),
126
                    "$args->{foreign_alias}.entity" => $source_name,
127
                },
128
            );
129
        },
130
        { cascade_copy => 0, cascade_delete => 1, cascade_update => 0 },
131
    );
132
133
    foreach my $property (@properties) {
134
        $class->might_have(
135
            $property . '_localization',
136
            $rel_class,
137
            sub {
138
                my ($args) = @_;
139
140
                # Not a 'use' because we don't want to load C4::Languages (and
141
                # thus C4::Context) while loading the schema
142
                require C4::Languages;
143
                my $lang = C4::Languages::getlanguage();
144
145
                return (
146
                    {
147
                        "$args->{foreign_alias}.code"     => { -ident => "$args->{self_alias}.$pk_column" },
148
                        "$args->{foreign_alias}.entity"   => $source_name,
149
                        "$args->{foreign_alias}.property" => $property,
150
                        "$args->{foreign_alias}.lang"     => $lang,
151
                    },
152
                    !$args->{self_result_object} ? () : {
153
                        "$args->{foreign_alias}.code"     => $args->{self_result_object}->get_column($pk_column),
154
                        "$args->{foreign_alias}.entity"   => $source_name,
155
                        "$args->{foreign_alias}.property" => $property,
156
                        "$args->{foreign_alias}.lang"     => $lang,
157
                    },
158
                );
159
            },
160
            { cascade_copy => 0, cascade_delete => 0, cascade_update => 0 },
161
        );
162
163
        $class->has_many(
164
            $property . '_localizations',
165
            $rel_class,
166
            sub {
167
                my ($args) = @_;
168
169
                return (
170
                    {
171
                        "$args->{foreign_alias}.code"     => { -ident => "$args->{self_alias}.$pk_column" },
172
                        "$args->{foreign_alias}.entity"   => $source_name,
173
                        "$args->{foreign_alias}.property" => $property,
174
                    },
175
                    !$args->{self_result_object} ? () : {
176
                        "$args->{foreign_alias}.code"     => $args->{self_result_object}->get_column($pk_column),
177
                        "$args->{foreign_alias}.entity"   => $source_name,
178
                        "$args->{foreign_alias}.property" => $property,
179
                    },
180
                );
181
            },
182
            { cascade_copy => 0, cascade_delete => 0, cascade_update => 0 },
183
        );
184
    }
185
}
186
187
sub localization {
188
    my ( $self, $property, $lang ) = @_;
189
190
    my $result_source = $self->result_source;
191
192
    my $cache             = Koha::Caches->get_instance('localization');
193
    my $cache_key         = sprintf( '%s:%s', $result_source->source_name, $lang );
194
    my $localizations_map = $cache->get_from_cache($cache_key);
195
    unless ($localizations_map) {
196
        $localizations_map = {};
197
198
        my $localizations = $result_source->schema->resultset('Localization')->search( { lang => $lang } );
199
        while ( my $localization = $localizations->next ) {
200
            my $fk               = $localization->get_column('code');
201
            my $localization_key = sprintf( '%s:%s', $fk, $localization->property );
202
            $localizations_map->{$localization_key} = $localization->translation;
203
        }
204
205
        $cache->set_in_cache( $cache_key, $localizations_map );
206
    }
207
208
    my ($pk) = $self->id;
209
    my $localization_key = sprintf( '%s:%s', $pk, $property );
210
211
    return $localizations_map->{$localization_key};
212
}
213
214
1;
(-)a/Koha/Schema/Result/Itemtype.pm (-16 / +2 lines)
Lines 372-393 __PACKAGE__->add_columns( Link Here
372
    '+bookable'                     => { is_boolean => 1, is_nullable => 1 },
372
    '+bookable'                     => { is_boolean => 1, is_nullable => 1 },
373
);
373
);
374
374
375
# Use the ItemtypeLocalization view to create the join on localization
375
__PACKAGE__->load_components('+Koha::Schema::Component::Localization');
376
our $LANGUAGE;
376
__PACKAGE__->localization_add_relationships('itemtype', 'description');
377
__PACKAGE__->has_many(
378
  "localization" => "Koha::Schema::Result::ItemtypeLocalization",
379
    sub {
380
        my $args = shift;
381
382
        die "no lang specified!" unless $LANGUAGE;
383
384
        return ({
385
            "$args->{self_alias}.itemtype" => { -ident => "$args->{foreign_alias}.code" },
386
            "$args->{foreign_alias}.lang" => $LANGUAGE,
387
        });
388
389
    }
390
);
391
377
392
=head2 koha_object_class
378
=head2 koha_object_class
393
379
(-)a/Koha/Schema/Result/ItemtypeLocalization.pm (-32 lines)
Lines 1-32 Link Here
1
package Koha::Schema::Result::ItemtypeLocalization;
2
3
use base 'DBIx::Class::Core';
4
5
use Modern::Perl;
6
7
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
8
9
__PACKAGE__->table('itemtype_localizations');
10
__PACKAGE__->result_source_instance->is_virtual(1);
11
__PACKAGE__->result_source_instance->view_definition(
12
    "SELECT localization_id, code, lang, translation FROM localization WHERE entity='itemtypes'"
13
);
14
15
__PACKAGE__->add_columns(
16
  "localization_id",
17
  { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
18
  "code",
19
  { data_type => "varchar", is_nullable => 0, size => 64 },
20
  "lang",
21
  { data_type => "varchar", is_nullable => 0, size => 25 },
22
  "translation",
23
  { data_type => "text", is_nullable => 1 },
24
);
25
26
__PACKAGE__->belongs_to(
27
    "itemtype",
28
    "Koha::Schema::Result::Itemtype",
29
    { code => 'itemtype' }
30
);
31
32
1;
(-)a/Koha/UI/Form/Builder/Item.pm (-4 / +3 lines)
Lines 202-213 sub generate_subfield_form { Link Here
202
            }
202
            }
203
        } elsif ( $subfield->{authorised_value} eq "itemtypes" ) {
203
        } elsif ( $subfield->{authorised_value} eq "itemtypes" ) {
204
            push @authorised_values, "";
204
            push @authorised_values, "";
205
            my $itemtypes;
205
            my $itemtypes = Koha::ItemTypes->new;
206
            if ($branch_limit) {
206
            if ($branch_limit) {
207
                $itemtypes = Koha::ItemTypes->search_with_localization( { branchcode => $branch_limit } );
207
                $itemtypes = $itemtypes->search_with_library_limits( {}, {}, $branch_limit );
208
            } else {
209
                $itemtypes = Koha::ItemTypes->search_with_localization;
210
            }
208
            }
209
            $itemtypes = $itemtypes->order_by_translated_description;
211
            while ( my $itemtype = $itemtypes->next ) {
210
            while ( my $itemtype = $itemtypes->next ) {
212
                push @authorised_values, $itemtype->itemtype;
211
                push @authorised_values, $itemtype->itemtype;
213
                $authorised_lib{ $itemtype->itemtype } = $itemtype->translated_description;
212
                $authorised_lib{ $itemtype->itemtype } = $itemtype->translated_description;
(-)a/admin/itemtypes.pl (-1 lines)
Lines 31-37 use C4::Auth qw( get_template_and_user ); Link Here
31
use C4::Output qw( output_html_with_http_headers );
31
use C4::Output qw( output_html_with_http_headers );
32
use Koha::ItemTypes;
32
use Koha::ItemTypes;
33
use Koha::ItemType;
33
use Koha::ItemType;
34
use Koha::Localizations;
35
34
36
my $input         = CGI->new;
35
my $input         = CGI->new;
37
my $searchfield   = $input->param('description');
36
my $searchfield   = $input->param('description');
(-)a/admin/localization.pl (-16 / +19 lines)
Lines 20-27 use Modern::Perl; Link Here
20
use C4::Auth   qw( get_template_and_user );
20
use C4::Auth   qw( get_template_and_user );
21
use C4::Output qw( output_html_with_http_headers );
21
use C4::Output qw( output_html_with_http_headers );
22
22
23
use Koha::Localization;
23
use Koha::Database;
24
use Koha::Localizations;
24
25
my $schema = Koha::Database->schema;
25
26
26
use CGI qw( -utf8 );
27
use CGI qw( -utf8 );
27
28
Lines 36-54 my ( $template, $borrowernumber, $cookie ) = get_template_and_user( Link Here
36
    }
37
    }
37
);
38
);
38
39
39
my $entity = $query->param('entity');
40
my $source    = $query->param('source');
40
my $code   = $query->param('code');
41
my $object_id = $query->param('object_id');
41
my $rs     = Koha::Localizations->search( { entity => $entity, code => $code } );
42
my $property  = $query->param('property');
43
44
my $row = $schema->resultset($source)->find($object_id);
45
42
my @translations;
46
my @translations;
43
while ( my $s = $rs->next ) {
47
my $localizations = $row->localizations->search( { property => $property } );
44
    push @translations,
48
while ( my $localization = $localizations->next ) {
45
        {
49
    push @translations, {
46
        id          => $s->localization_id,
50
        localization_id => $localization->id,
47
        entity      => $s->entity,
51
        lang            => $localization->lang,
48
        code        => $s->code,
52
        translation     => $localization->translation,
49
        lang        => $s->lang,
53
    };
50
        translation => $s->translation,
51
        };
52
}
54
}
53
55
54
my $translated_languages = C4::Languages::getTranslatedLanguages();    # opac and intranet
56
my $translated_languages = C4::Languages::getTranslatedLanguages();    # opac and intranet
Lines 56-63 my $translated_languages = C4::Languages::getTranslatedLanguages(); # opac an Link Here
56
$template->param(
58
$template->param(
57
    translations => \@translations,
59
    translations => \@translations,
58
    languages    => $translated_languages,
60
    languages    => $translated_languages,
59
    entity       => $entity,
61
    source       => $source,
60
    code         => $code,
62
    object_id    => $object_id,
63
    property     => $property,
61
);
64
);
62
65
63
output_html_with_http_headers $query, $cookie, $template->output;
66
output_html_with_http_headers $query, $cookie, $template->output;
(-)a/installer/data/mysql/atomicupdate/bug-38136.pl (+24 lines)
Line 0 Link Here
1
use Modern::Perl;
2
use Koha::Installer::Output qw(say_warning say_failure say_success say_info);
3
4
return {
5
    bug_number  => '38136',
6
    description => 'Add localization.property',
7
    up          => sub {
8
        my ($args) = @_;
9
        my ( $dbh, $out ) = @$args{qw(dbh out)};
10
11
        unless ( column_exists( 'localization', 'property' ) ) {
12
            $dbh->do("alter table `localization` add `property` varchar(100) null after `code`");
13
            $dbh->do("update `localization` set `property` = 'description', entity = 'Itemtype'");
14
            $dbh->do("alter table `localization` modify `property` varchar(100) not null");
15
            $dbh->do("alter table `localization` drop key `entity_code_lang`");
16
            $dbh->do(
17
                "alter table `localization` add unique key `entity_code_property_lang` (`entity`, `code`, `property`, `lang`)"
18
            );
19
20
            say_success( $out, 'Added column localization.property and updated localization.entity values' );
21
        }
22
23
    },
24
};
(-)a/installer/data/mysql/kohastructure.sql (-1 / +2 lines)
Lines 4563-4572 CREATE TABLE `localization` ( Link Here
4563
  `localization_id` int(11) NOT NULL AUTO_INCREMENT,
4563
  `localization_id` int(11) NOT NULL AUTO_INCREMENT,
4564
  `entity` varchar(16) NOT NULL,
4564
  `entity` varchar(16) NOT NULL,
4565
  `code` varchar(64) NOT NULL,
4565
  `code` varchar(64) NOT NULL,
4566
  `property` varchar(100) NOT NULL,
4566
  `lang` varchar(25) NOT NULL COMMENT 'could be a foreign key',
4567
  `lang` varchar(25) NOT NULL COMMENT 'could be a foreign key',
4567
  `translation` mediumtext DEFAULT NULL,
4568
  `translation` mediumtext DEFAULT NULL,
4568
  PRIMARY KEY (`localization_id`),
4569
  PRIMARY KEY (`localization_id`),
4569
  UNIQUE KEY `entity_code_lang` (`entity`,`code`,`lang`)
4570
  UNIQUE KEY `entity_code_property_lang` (`entity`,`code`,`property`,`lang`)
4570
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4571
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4571
/*!40101 SET character_set_client = @saved_cs_client */;
4572
/*!40101 SET character_set_client = @saved_cs_client */;
4572
4573
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/localization-link.inc (+1 lines)
Line 0 Link Here
1
<a href="/cgi-bin/koha/admin/localization.pl?source=[% source | uri %]&object_id=[% object_id | uri %]&property=[% property | uri %]"><i class="fa-solid fa-pencil" aria-hidden="true"></i> Translate into other languages</a>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt (-1 / +1 lines)
Lines 194-200 Link Here
194
                        <label for="description" class="required">Description: </label>
194
                        <label for="description" class="required">Description: </label>
195
                        <input type="text" id="description" name="description" size="48" value="[% itemtype.description | html %]" required="required" /> <span class="required">Required</span>
195
                        <input type="text" id="description" name="description" size="48" value="[% itemtype.description | html %]" required="required" /> <span class="required">Required</span>
196
                        [% IF can_be_translated %]
196
                        [% IF can_be_translated %]
197
                            <a href="/cgi-bin/koha/admin/localization.pl?entity=itemtypes&code=[% itemtype.itemtype | uri %]"> <i class="fa-solid fa-pencil" aria-hidden="true"></i> Translate into other languages </a>
197
                            [% INCLUDE 'localization-link.inc' source='Itemtype' object_id=itemtype.itemtype property='description' %]
198
                        [% END %]
198
                        [% END %]
199
                    </li>
199
                    </li>
200
                    <li>
200
                    <li>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/localization.tt (-52 / +55 lines)
Lines 5-16 Link Here
5
[% INCLUDE 'doc-head-open.inc' %]
5
[% INCLUDE 'doc-head-open.inc' %]
6
<title>
6
<title>
7
    [% FILTER collapse %]
7
    [% FILTER collapse %]
8
        [% SWITCH entity %]
8
        [% SWITCH source %]
9
        [% CASE "itemtypes" %]
9
        [% CASE "Itemtype" %]
10
            [% tx("Add translations for item type '{code}'", { code = code }) | html %]
10
            [% tx("Add translations for item type '{code}'", { code = object_id }) | html %]
11
            &rsaquo; [% t("Item types") | html %] &rsaquo; [% t("Administration") | html %]
11
            &rsaquo; [% t("Item types") | html %] &rsaquo; [% t("Administration") | html %]
12
        [% CASE %]
12
        [% CASE %]
13
            [% tx("Add translations for '{code}'", { code = code }) | html %]
13
            [% tx("Add translations for '{code}'", { code = object_id }) | html %]
14
        [% END %]
14
        [% END %]
15
        &rsaquo; [% t("Koha") | html %]
15
        &rsaquo; [% t("Koha") | html %]
16
    [% END %]
16
    [% END %]
Lines 29-36 Link Here
29
[% END %]
29
[% END %]
30
[% WRAPPER 'sub-header.inc' %]
30
[% WRAPPER 'sub-header.inc' %]
31
    [% WRAPPER breadcrumbs %]
31
    [% WRAPPER breadcrumbs %]
32
        [% SWITCH entity %]
32
        [% SWITCH source %]
33
        [% CASE "itemtypes" %]
33
        [% CASE "Itemtype" %]
34
            [% WRAPPER breadcrumb_item %]
34
            [% WRAPPER breadcrumb_item %]
35
                <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
35
                <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
36
            [% END %]
36
            [% END %]
Lines 38-74 Link Here
38
                <a href="/cgi-bin/koha/admin/itemtypes.pl">Item types</a>
38
                <a href="/cgi-bin/koha/admin/itemtypes.pl">Item types</a>
39
            [% END %]
39
            [% END %]
40
            [% WRAPPER breadcrumb_item %]
40
            [% WRAPPER breadcrumb_item %]
41
                <a href="/cgi-bin/koha/admin/itemtypes.pl?op=add_form&itemtype=[% code | uri %]"> [% tx("Modify item type '{code}'", { code = code }) | html %] </a>
41
                <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>
42
            [% END %]
42
            [% END %]
43
            [% WRAPPER breadcrumb_item bc_active= 1 %]
43
            [% WRAPPER breadcrumb_item bc_active= 1 %]
44
                <span>Add translations</span>
44
                <span>Add translations</span>
45
            [% END %]
45
            [% END %]
46
        [% CASE %]
46
        [% CASE %]
47
            [% WRAPPER breadcrumb_item bc_active= 1 %]
47
            [% WRAPPER breadcrumb_item bc_active= 1 %]
48
                [% tx("Add translations for '{code}'", { code = code }) | html %]
48
                [% tx("Add translations for '{code}'", { code = object_id }) | html %]
49
            [% END %]
49
            [% END %]
50
        [% END %]
50
        [% END %]
51
    [% END #/ WRAPPER breadcrumbs %]
51
    [% END #/ WRAPPER breadcrumbs %]
52
[% END #/ WRAPPER sub-header.inc %]
52
[% END #/ WRAPPER sub-header.inc %]
53
53
54
[% WRAPPER 'main-container.inc' %]
54
[% WRAPPER 'main-container.inc' %]
55
    [% SWITCH entity %]
55
    [% SWITCH source %]
56
    [% CASE "itemtypes" %]
56
    [% CASE "Itemtype" %]
57
        <h1>[% tx("Add translations for item type '{code}'", { code = code }) | html %]</h1>
57
        <h1>[% tx("Add translations for item type '{code}'", { code = object_id }) | html %]</h1>
58
    [% CASE %]
58
    [% CASE %]
59
        <h1>[% tx("Add translations for '{code}'", { code = code }) | html %]</h1>
59
        <h1>[% tx("Add translations for '{code}'", { code = code }) | html %]</h1>
60
    [% END %]
60
    [% END %]
61
61
62
    <form id="add_translation" method="get">
62
    <form id="add_translation" method="get">
63
        [% INCLUDE 'csrf-token.inc' %]
63
        [% INCLUDE 'csrf-token.inc' %]
64
        <input type="hidden" name="entity" value="[% entity | html %]" />
64
        <input type="hidden" name="source" value="[% source | html %]" />
65
        <input type="hidden" name="code" value="[% code | html %]" />
65
        <input type="hidden" name="object_id" value="[% object_id | html %]" />
66
        <input type="hidden" name="interface" value="[% interface_side | html %]" />
66
        <input type="hidden" name="property" value="[% property | html %]" />
67
        <fieldset class="rows clearfix">
67
        <fieldset class="rows clearfix">
68
            <ol>
68
            <ol>
69
                <li>
69
                <li>
70
                    <span class="label">Authorized value:</span>
70
                    <span class="label">Code:</span>
71
                    [% code | html %]
71
                    [% object_id | html %]
72
                </li>
72
                </li>
73
                <li>
73
                <li>
74
                    <label for="lang">Language:</label>
74
                    <label for="lang">Language:</label>
Lines 100-108 Link Here
100
        <table id="localization">
100
        <table id="localization">
101
            <thead>
101
            <thead>
102
                <tr>
102
                <tr>
103
                    <th>Id</th>
104
                    <th>Entity</th>
105
                    <th>Code</th>
106
                    <th class="lang">Language</th>
103
                    <th class="lang">Language</th>
107
                    <th class="translation">Translation</th>
104
                    <th class="translation">Translation</th>
108
                    <th class="no-sort">&nbsp;</th>
105
                    <th class="no-sort">&nbsp;</th>
Lines 110-126 Link Here
110
            </thead>
107
            </thead>
111
            <tbody>
108
            <tbody>
112
                [% FOR t IN translations %]
109
                [% FOR t IN translations %]
113
                    <tr id="row_id_[% t.id | html %]" data-id="[% t.id | html %]">
110
                    <tr id="row_id_[% t.localization_id | html %]" data-id="[% t.localization_id | html %]">
114
                        <td>[% t.id | html %]</td>
115
                        <td>
116
                            [% SWITCH entity %]
117
                            [% CASE "itemtypes" %]
118
                                Item type
119
                            [% CASE %]
120
                                [% t.entity | html %]
121
                            [% END %]
122
                        </td>
123
                        <td>[% t.code | html %]</td>
124
                        <td class="lang">[% t.lang | html %]</td>
111
                        <td class="lang">[% t.lang | html %]</td>
125
                        <td class="translation" contenteditable="true">[% t.translation | html %]</td>
112
                        <td class="translation" contenteditable="true">[% t.translation | html %]</td>
126
                        <td class="actions"
113
                        <td class="actions"
Lines 149-155 Link Here
149
            var message;
136
            var message;
150
            if ( type == 'success_on_update' ) {
137
            if ( type == 'success_on_update' ) {
151
                message = $('<div class="alert alert-info"></div>');
138
                message = $('<div class="alert alert-info"></div>');
152
                message.text(_("Entity %s (code %s) for lang %s has been updated with '%s'").format(data.entity, data.code, data.lang, data.translation));
139
                message.text(_("Translation for lang %s has correctly been updated with '%s'").format(data.lang, data.translation));
153
            } else if ( type == 'error_on_update' ) {
140
            } else if ( type == 'error_on_update' ) {
154
                message = $('<div class="alert alert-warning"></div>');
141
                message = $('<div class="alert alert-warning"></div>');
155
                if ( data.error_code == 'already_exists' ) {
142
                if ( data.error_code == 'already_exists' ) {
Lines 159-171 Link Here
159
                }
146
                }
160
            } else if ( type == 'success_on_delete' ) {
147
            } else if ( type == 'success_on_delete' ) {
161
                message = $('<div class="alert alert-info"></div>');
148
                message = $('<div class="alert alert-info"></div>');
162
                message.text(_("The translation (id %s) has been removed successfully").format(data.id));
149
                message.text(_("The translation has been removed successfully"));
163
            } else if ( type == 'error_on_delete' ) {
150
            } else if ( type == 'error_on_delete' ) {
164
                message = $('<div class="alert alert-warning"></div>');
151
                message = $('<div class="alert alert-warning"></div>');
165
                message.text(_("An error occurred when deleting this translation"));
152
                message.text(_("An error occurred when deleting this translation"));
166
            } else if ( type == 'success_on_insert' ) {
153
            } else if ( type == 'success_on_insert' ) {
167
                message = $('<div class="alert alert-info"></div>');
154
                message = $('<div class="alert alert-info"></div>');
168
                message.text(_("Translation (id %s) has been added successfully").format(data.id));
155
                message.text(_("Translation has been added successfully"));
169
            } else if ( type == 'error_on_insert' ) {
156
            } else if ( type == 'error_on_insert' ) {
170
                message = $('<div class="alert alert-warning"></div>');
157
                message = $('<div class="alert alert-warning"></div>');
171
                if ( data.error_code == 'already_exists' ) {
158
                if ( data.error_code == 'already_exists' ) {
Lines 182-195 Link Here
182
            }, 3000);
169
            }, 3000);
183
        }
170
        }
184
171
185
        function send_update_request( data, cell ) {
172
        function send_update_request( _data, cell ) {
173
            const form = document.forms.add_translation;
174
            const source = form.elements.source.value;
175
            const object_id = form.elements.object_id.value;
176
            const data = Object.assign({}, _data, { source, object_id });
186
            const client = APIClient.localization;
177
            const client = APIClient.localization;
187
            client.localizations.update(data).then(
178
            client.localizations.update(data).then(
188
                success => {
179
                success => {
189
                    if ( success.error ) {
180
                    if ( success.error ) {
190
                        $(cell).css('background-color', '#FF0000');
181
                        $(cell).css('background-color', '#FF0000');
191
                        show_message({ type: 'error_on_update', data: success });
182
                        show_message({ type: 'error_on_update', data: success });
192
                    } else if ( success.is_changed == 1 ) {
183
                    } else {
193
                        $(cell).css('background-color', '#00FF00');
184
                        $(cell).css('background-color', '#00FF00');
194
                        show_message({ type: 'success_on_update', data: success });
185
                        show_message({ type: 'success_on_update', data: success });
195
                    }
186
                    }
Lines 213-224 Link Here
213
            );
204
            );
214
        }
205
        }
215
206
216
        function send_delete_request( id, cell ) {
207
        function send_delete_request( localization_id, cell ) {
208
            const form = document.forms.add_translation;
209
            const source = form.elements.source.value;
210
            const object_id = form.elements.object_id.value;
211
            const property = form.elements.property.value;
212
            const data = { source, object_id, property, localization_id };
213
217
            const client = APIClient.localization;
214
            const client = APIClient.localization;
218
            client.localizations.delete(id).then(
215
            client.localizations.delete(data).then(
219
                success => {
216
                success => {
220
                    $("#localization").DataTable().row( '#row_id_' + id ).remove().draw();
217
                    $("#localization").DataTable().row( '#row_id_' + localization_id ).remove().draw();
221
                    show_message({ type: 'success_on_delete', data: {id} });
218
                    show_message({ type: 'success_on_delete', data: {localization_id} });
222
                },
219
                },
223
                error => {
220
                error => {
224
                    $(cell).css('background-color', '#FF9090');
221
                    $(cell).css('background-color', '#FF9090');
Lines 257-262 Link Here
257
            });
254
            });
258
            $("body").on('click', "td.lang", function(){
255
            $("body").on('click', "td.lang", function(){
259
                var td = $(this);
256
                var td = $(this);
257
                if (td[0].childElementCount > 0) {
258
                    // do nothing if there is already something there (like a select for instance)
259
                    return;
260
                }
260
                var tr = td.parent();
261
                var tr = td.parent();
261
                var id = tr.data('id');
262
                var id = tr.data('id');
262
                var lang = td.text();
263
                var lang = td.text();
Lines 268-276 Link Here
268
                });
269
                });
269
                var lang;
270
                var lang;
270
                my_select.on('change', function(){
271
                my_select.on('change', function(){
272
                    var localization_id = $(tr).data('id');
271
                    lang = td.find('option:selected').val();
273
                    lang = td.find('option:selected').val();
272
                    var translation = tr.find("td.translation").text();
274
                    send_update_request( {localization_id, lang}, td[0] );
273
                    send_update_request( {id, lang, translation}, td[0] );
274
                });
275
                });
275
                my_select.on('blur', function(){
276
                my_select.on('blur', function(){
276
                    td.html(lang);
277
                    td.html(lang);
Lines 280-289 Link Here
280
281
281
            $("body").on('blur', "td.translation", function(){
282
            $("body").on('blur', "td.translation", function(){
282
                var tr = $(this).parent();
283
                var tr = $(this).parent();
283
                var id = $(tr).data('id');
284
                var localization_id = $(tr).data('id');
284
                var lang = $(tr).find('td.lang').text();
285
                var translation = $(this).text();
285
                var translation = $(this).text();
286
                send_update_request( {id, lang, translation}, this );
286
                send_update_request( {localization_id, translation}, this );
287
            });
287
            });
288
288
289
            $("body").on("click", "a.delete", function(e){
289
            $("body").on("click", "a.delete", function(e){
Lines 301-320 Link Here
301
301
302
            $("#add_translation").on('submit', function(e){
302
            $("#add_translation").on('submit', function(e){
303
                e.preventDefault();
303
                e.preventDefault();
304
                let localization = {
304
305
                    entity: $(this).find('input[name="entity"]').val(),
305
                const form = this;
306
                    code: $(this).find('input[name="code"]').val(),
306
                const source = form.elements.source.value;
307
                    lang: $(this).find('select[name="lang"] option:selected').val(),
307
                const object_id = form.elements.object_id.value;
308
                    translation: $(this).find('input[name="translation"]').val(),
308
                const property = form.elements.property.value;
309
                };
309
                const lang = form.elements.lang.value
310
                const translation = form.elements.translation.value
311
312
                let localization = { source, object_id, property, lang, translation };
310
                const client = APIClient.localization;
313
                const client = APIClient.localization;
311
                client.localizations.create(localization).then(
314
                client.localizations.create(localization).then(
312
                    success => {
315
                    success => {
313
                        if ( success.error ) {
316
                        if ( success.error ) {
314
                            show_message({ type: 'error_on_insert', data: success });
317
                            show_message({ type: 'error_on_insert', data: success });
315
                        } else {
318
                        } else {
316
                            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>" ] ).draw().node();
319
                            var new_row = table.api().row.add( [ success.lang, success.translation, "<a href=\"#\" class=\"delete\"><i class=\"fa fa-trash-can\"></i> %s</a>".format(escape_str(_("Delete"))) ] ).draw().node();
317
                            $( new_row ).attr("id", "row_id_" + success.id ).data("id", success.id );
320
                            $( new_row ).attr("id", "row_id_" + success.localization_id ).data("id", success.localization_id );
318
                            $( new_row.children[th_lang_index] ).prop("contenteditable", true).addClass("lang")
321
                            $( new_row.children[th_lang_index] ).prop("contenteditable", true).addClass("lang")
319
                            $( new_row.children[th_translation_index] ).prop("contenteditable", true).addClass("translation")
322
                            $( new_row.children[th_translation_index] ).prop("contenteditable", true).addClass("translation")
320
                            show_message({ type: 'success_on_insert', data: success });
323
                            show_message({ type: 'success_on_insert', data: success });
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/localization-api-client.js (-17 / +5 lines)
Lines 10-21 export class LocalizationAPIClient { Link Here
10
            create: localization =>
10
            create: localization =>
11
                this.httpClient.post({
11
                this.httpClient.post({
12
                    endpoint: "",
12
                    endpoint: "",
13
                    body: "entity=%s&code=%s&lang=%s&translation=%s".format(
13
                    body: new URLSearchParams(localization).toString(),
14
                        encodeURIComponent(localization.entity),
15
                        encodeURIComponent(localization.code),
16
                        encodeURIComponent(localization.lang),
17
                        encodeURIComponent(localization.translation)
18
                    ),
19
                    headers: {
14
                    headers: {
20
                        "Content-Type":
15
                        "Content-Type":
21
                            "application/x-www-form-urlencoded;charset=utf-8",
16
                            "application/x-www-form-urlencoded;charset=utf-8",
Lines 24-46 export class LocalizationAPIClient { Link Here
24
            update: localization =>
19
            update: localization =>
25
                this.httpClient.put({
20
                this.httpClient.put({
26
                    endpoint: "",
21
                    endpoint: "",
27
                    body: "id=%s&lang=%s&translation=%s".format(
22
                    body: new URLSearchParams(localization).toString(),
28
                        encodeURIComponent(localization.id),
29
                        encodeURIComponent(localization.lang),
30
                        encodeURIComponent(localization.translation)
31
                    ),
32
                    headers: {
23
                    headers: {
33
                        "Content-Type":
24
                        "Content-Type":
34
                            "application/x-www-form-urlencoded;charset=utf-8",
25
                            "application/x-www-form-urlencoded;charset=utf-8",
35
                    },
26
                    },
36
                }),
27
                }),
37
            delete: id =>
28
            delete: localization =>
38
                this.httpClient.delete({
29
                this.httpClient.delete({
39
                    endpoint: "/?id=%s".format(id),
30
                    endpoint:
40
                    headers: {
31
                        "?" + new URLSearchParams(localization).toString(),
41
                        "Content-Type":
42
                            "application/x-www-form-urlencoded;charset=utf-8",
43
                    },
44
                }),
32
                }),
45
        };
33
        };
46
    }
34
    }
(-)a/svc/localization (-79 / +82 lines)
Lines 2-112 Link Here
2
2
3
use Modern::Perl;
3
use Modern::Perl;
4
use Encode qw( encode );
4
use Encode qw( encode );
5
use Try::Tiny;
6
use JSON qw( to_json );
5
7
6
use C4::Service;
8
use C4::Service;
7
use Koha::Localizations;
9
use Koha::Caches;
10
use Koha::Database;
11
use C4::Output qw( output_with_http_headers );
8
12
9
our ( $query, $response ) = C4::Service->init( parameters => 'manage_itemtypes' );
13
our ( $query, $response ) = C4::Service->init( parameters => 'manage_itemtypes' );
10
14
11
sub get_translations {
12
    my $rs = Koha::Localizations->search( { entity => $query->param('entity'), code => $query->param('code') } );
13
    my @translations;
14
    while ( my $s = $rs->next ) {
15
        push @translations, {
16
            id          => $s->localization_id,
17
            entity      => $s->entity,
18
            code        => $s->code,
19
            lang        => $s->lang,
20
            translation => $s->translation,
21
        };
22
    }
23
    $response->param( translations => \@translations );
24
    C4::Service->return_success($response);
25
}
26
27
sub update_translation {
15
sub update_translation {
28
    my $id          = $query->param('id');
16
    my $source          = $query->param('source');
29
    my $translation = $query->param('translation');
17
    my $localization_id = $query->param('localization_id');
30
    my $lang        = $query->param('lang');
18
    my $object_id       = $query->param('object_id');
19
    my $lang            = $query->param('lang');
20
    my $translation     = $query->param('translation');
31
21
32
    my $localization = Koha::Localizations->find($id);
22
    my $schema = Koha::Database->schema;
33
    if ( defined $lang and $localization->lang ne $lang ) {
23
    my $row    = $schema->resultset($source)->find($object_id);
34
        $localization->lang($lang);
24
    if ($row) {
35
    }
25
        my $localization = $row->localizations->find($localization_id);
36
    if ( defined $translation and $localization->translation ne $translation ) {
26
        if ($localization) {
37
        $localization->translation($translation);
27
            try {
38
    }
28
                my $original_lang = $localization->lang;
39
    my %params;
29
                $localization->lang($lang)               if $lang;
40
    my $is_changed;
30
                $localization->translation($translation) if $translation;
41
    if ( $localization->is_changed ) {
31
                $localization->update();
42
        $is_changed = 1;
32
                Koha::Caches->get_instance('localization')->clear_from_cache("$source:$original_lang");
43
        unless (
33
                Koha::Caches->get_instance('localization')->clear_from_cache("$source:$lang") if $lang;
44
            Koha::Localizations->search(
34
            } catch {
45
                {
35
                $localization->discard_changes();
46
                    entity          => $localization->entity, code => $localization->code, lang => $lang,
36
                $response->param( error => 1, error_code => 'already_exists' );
47
                    localization_id => { '!=' => $localization->localization_id },
37
            };
48
                }
49
            )->count
50
            )
51
        {
52
            $localization->store;
53
        } else {
54
            $params{error}      = 1;
55
            $params{error_code} = 'already_exists';
56
        }
38
        }
39
40
        $response->param(
41
            lang        => $localization->lang,
42
            translation => $localization->translation,
43
        );
57
    }
44
    }
58
    $response->param(
45
59
        %params,
60
        id          => $localization->localization_id,
61
        entity      => $localization->entity,
62
        code        => $localization->code,
63
        lang        => $localization->lang,
64
        translation => $localization->translation,
65
        is_changed  => $is_changed,
66
    );
67
    C4::Service->return_success($response);
46
    C4::Service->return_success($response);
68
}
47
}
69
48
70
sub add_translation {
49
sub add_translation {
71
    my $entity      = $query->param('entity');
50
    my $source          = $query->param('source');
72
    my $code        = $query->param('code');
51
    my $localization_id = $query->param('localization_id');
73
    my $lang        = $query->param('lang');
52
    my $object_id       = $query->param('object_id');
74
    my $translation = $query->param('translation');
53
    my $property        = $query->param('property');
54
    my $lang            = $query->param('lang');
55
    my $translation     = $query->param('translation');
75
56
76
    unless ( Koha::Localizations->search( { entity => $entity, code => $code, lang => $lang, } )->count ) {
57
    my $schema = Koha::Database->schema;
77
        my $localization = Koha::Localization->new(
58
    my $row    = $schema->resultset($source)->find($object_id);
59
    try {
60
        my $localization = $row->create_related(
61
            "${property}_localizations",
78
            {
62
            {
79
                entity      => $entity,
63
                entity      => $source,
80
                code        => $code,
81
                lang        => $lang,
64
                lang        => $lang,
82
                translation => $translation,
65
                translation => $translation,
83
            }
66
            }
84
        );
67
        );
85
        $localization->store;
68
        Koha::Caches->get_instance('localization')->clear_from_cache("$source:$lang");
86
        $response->param(
69
87
            id          => $localization->localization_id,
70
        $response->param(
88
            entity      => $localization->entity,
71
            lang            => $localization->lang,
89
            code        => $localization->code,
72
            translation     => $localization->translation,
90
            lang        => $localization->lang,
73
            localization_id => $localization->id,
91
            translation => $localization->translation,
74
        );
92
        );
75
    } catch {
76
        $response->param( error => 1, error_code => 'already_exists' );
77
    };
93
78
94
    } else {
95
        $response->param( error => 1, error_code => 'already_exists', );
96
    }
97
    C4::Service->return_success($response);
79
    C4::Service->return_success($response);
98
}
80
}
99
81
100
sub delete_translation {
82
sub delete_translation {
101
    my $id = $query->param('id');
83
    my $source          = $query->param('source');
102
    Koha::Localizations->find($id)->delete;
84
    my $object_id       = $query->param('object_id');
103
    $response->param( id => $id );
85
    my $localization_id = $query->param('localization_id');
86
87
    my $schema = Koha::Database->schema;
88
    my $row    = $schema->resultset($source)->find($object_id);
89
    if ( $row && $row->can('localizations') ) {
90
        my $localization = $row->localizations->find($localization_id);
91
92
        unless ($localization) {
93
            my $json = to_json( { errors => 'Not found' } );
94
            output_with_http_headers $query, undef, $json, 'js', '404 Not Found';
95
            exit;
96
        }
97
98
        if ($localization) {
99
            $localization->delete();
100
            Koha::Caches->get_instance('localization')->clear_from_cache( "$source:" . $localization->lang );
101
        }
102
103
        $response->param(
104
            localization_id => $localization_id,
105
        );
106
    }
107
104
    C4::Service->return_success($response);
108
    C4::Service->return_success($response);
105
}
109
}
106
110
107
C4::Service->dispatch(
111
C4::Service->dispatch(
108
    [ 'GET /',    ['id'],                                      \&get_translations ],
112
    [ 'PUT /',    [], \&update_translation ],
109
    [ 'PUT /',    ['id'],                                      \&update_translation ],
113
    [ 'POST /',   [], \&add_translation ],
110
    [ 'POST /',   [ 'entity', 'code', 'lang', 'translation' ], \&add_translation ],
114
    [ 'DELETE /', [], \&delete_translation ],
111
    [ 'DELETE /', ['id'],                                      \&delete_translation ],
112
);
115
);
(-)a/t/db_dependent/Koha/Filter/ExpandCodedFields.t (-1 / +2 lines)
Lines 71-80 subtest 'ExpandCodedFields tests' => sub { Link Here
71
71
72
    # Clear GetAuthorisedValueDesc-generated cache
72
    # Clear GetAuthorisedValueDesc-generated cache
73
    $cache->clear_from_cache("libraries:name");
73
    $cache->clear_from_cache("libraries:name");
74
    $cache->clear_from_cache("itemtype:description:en");
75
    $cache->clear_from_cache("cn_sources:description");
74
    $cache->clear_from_cache("cn_sources:description");
76
    $cache->clear_from_cache( "AV_descriptions:" . $av->category );
75
    $cache->clear_from_cache( "AV_descriptions:" . $av->category );
77
76
77
    Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en');
78
78
    C4::Biblio::ModBiblio( $record, $biblio->biblionumber );
79
    C4::Biblio::ModBiblio( $record, $biblio->biblionumber );
79
    $biblio = Koha::Biblios->find( $biblio->biblionumber );
80
    $biblio = Koha::Biblios->find( $biblio->biblionumber );
80
    $record = $biblio->metadata->record;
81
    $record = $biblio->metadata->record;
(-)a/t/db_dependent/Koha/Item.t (-5 / +11 lines)
Lines 30-35 use C4::Biblio qw( GetMarcSubfieldStructure ); Link Here
30
use C4::Circulation qw( AddIssue AddReturn );
30
use C4::Circulation qw( AddIssue AddReturn );
31
31
32
use Koha::Caches;
32
use Koha::Caches;
33
use Koha::Cache::Memory::Lite;
33
use Koha::Items;
34
use Koha::Items;
34
use Koha::Database;
35
use Koha::Database;
35
use Koha::DateUtils qw( dt_from_string );
36
use Koha::DateUtils qw( dt_from_string );
Lines 2432-2441 subtest 'columns_to_str' => sub { Link Here
2432
    $cache->clear_from_cache("MarcStructure-1-");
2433
    $cache->clear_from_cache("MarcStructure-1-");
2433
    $cache->clear_from_cache("MarcSubfieldStructure-");
2434
    $cache->clear_from_cache("MarcSubfieldStructure-");
2434
    $cache->clear_from_cache("libraries:name");
2435
    $cache->clear_from_cache("libraries:name");
2435
    $cache->clear_from_cache("itemtype:description:en");
2436
    $cache->clear_from_cache("cn_sources:description");
2436
    $cache->clear_from_cache("cn_sources:description");
2437
    $cache->clear_from_cache("AV_descriptions:LOST");
2437
    $cache->clear_from_cache("AV_descriptions:LOST");
2438
2438
2439
    Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en');
2440
2439
    # Creating subfields 'é', 'è' that are not linked with a kohafield
2441
    # Creating subfields 'é', 'è' that are not linked with a kohafield
2440
    Koha::MarcSubfieldStructures->search(
2442
    Koha::MarcSubfieldStructures->search(
2441
        {
2443
        {
Lines 2517-2526 subtest 'columns_to_str' => sub { Link Here
2517
    $cache->clear_from_cache("MarcStructure-1-");
2519
    $cache->clear_from_cache("MarcStructure-1-");
2518
    $cache->clear_from_cache("MarcSubfieldStructure-");
2520
    $cache->clear_from_cache("MarcSubfieldStructure-");
2519
    $cache->clear_from_cache("libraries:name");
2521
    $cache->clear_from_cache("libraries:name");
2520
    $cache->clear_from_cache("itemtype:description:en");
2521
    $cache->clear_from_cache("cn_sources:description");
2522
    $cache->clear_from_cache("cn_sources:description");
2522
    $cache->clear_from_cache("AV_descriptions:LOST");
2523
    $cache->clear_from_cache("AV_descriptions:LOST");
2523
2524
2525
    Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en');
2526
2524
    $schema->storage->txn_rollback;
2527
    $schema->storage->txn_rollback;
2525
};
2528
};
2526
2529
Lines 2537-2546 subtest 'strings_map() tests' => sub { Link Here
2537
    $cache->clear_from_cache("MarcStructure-1-");
2540
    $cache->clear_from_cache("MarcStructure-1-");
2538
    $cache->clear_from_cache("MarcSubfieldStructure-");
2541
    $cache->clear_from_cache("MarcSubfieldStructure-");
2539
    $cache->clear_from_cache("libraries:name");
2542
    $cache->clear_from_cache("libraries:name");
2540
    $cache->clear_from_cache("itemtype:description:en");
2541
    $cache->clear_from_cache("cn_sources:description");
2543
    $cache->clear_from_cache("cn_sources:description");
2542
    $cache->clear_from_cache("AV_descriptions:LOST");
2544
    $cache->clear_from_cache("AV_descriptions:LOST");
2543
2545
2546
    Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en');
2547
2544
    # Recreating subfields just to be sure tests will be ok
2548
    # Recreating subfields just to be sure tests will be ok
2545
    # 1 => av (LOST)
2549
    # 1 => av (LOST)
2546
    # 3 => no link
2550
    # 3 => no link
Lines 2652-2657 subtest 'strings_map() tests' => sub { Link Here
2652
    )->store();
2656
    )->store();
2653
2657
2654
    Koha::Caches->get_instance->flush_all;
2658
    Koha::Caches->get_instance->flush_all;
2659
    Koha::Cache::Memory::Lite->get_instance->flush;
2655
2660
2656
    $item->set(
2661
    $item->set(
2657
        {
2662
        {
Lines 2724-2732 subtest 'strings_map() tests' => sub { Link Here
2724
    $cache->clear_from_cache("MarcStructure-1-");
2729
    $cache->clear_from_cache("MarcStructure-1-");
2725
    $cache->clear_from_cache("MarcSubfieldStructure-");
2730
    $cache->clear_from_cache("MarcSubfieldStructure-");
2726
    $cache->clear_from_cache("libraries:name");
2731
    $cache->clear_from_cache("libraries:name");
2727
    $cache->clear_from_cache("itemtype:description:en");
2728
    $cache->clear_from_cache("cn_sources:description");
2732
    $cache->clear_from_cache("cn_sources:description");
2729
2733
2734
    Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en');
2735
2730
    $schema->storage->txn_rollback;
2736
    $schema->storage->txn_rollback;
2731
};
2737
};
2732
2738
Lines 2863-2869 subtest 'store() tests' => sub { Link Here
2863
            {
2869
            {
2864
                borrowernumber    => $patron->id,
2870
                borrowernumber    => $patron->id,
2865
                date              => '1970-01-01 14:00:01',
2871
                date              => '1970-01-01 14:00:01',
2866
                amountoutstanding => 0,
2872
                amountoutstanding =>  0,
2867
                amount            => -5,
2873
                amount            => -5,
2868
                interface         => 'commandline',
2874
                interface         => 'commandline',
2869
                credit_type_code  => 'PAYMENT'
2875
                credit_type_code  => 'PAYMENT'
(-)a/t/db_dependent/Koha/ItemTypes.t (-12 / +12 lines)
Lines 27-32 use t::lib::TestBuilder; Link Here
27
27
28
use C4::Calendar;
28
use C4::Calendar;
29
use Koha::Biblioitems;
29
use Koha::Biblioitems;
30
use Koha::Caches;
30
use Koha::Libraries;
31
use Koha::Libraries;
31
use Koha::Database;
32
use Koha::Database;
32
use Koha::DateUtils qw(dt_from_string);
33
use Koha::DateUtils qw(dt_from_string);
Lines 73-102 my $child3 = $builder->build_object( Link Here
73
    }
74
    }
74
);
75
);
75
76
76
Koha::Localization->new(
77
$child1->_result->localizations->create(
77
    {
78
    {
78
        entity      => 'itemtypes',
79
        property    => 'description',
79
        code        => $child1->itemtype,
80
        lang        => 'en',
80
        lang        => 'en',
81
        translation => 'b translated itemtype desc'
81
        translation => 'b translated itemtype desc'
82
    }
82
    }
83
)->store;
83
);
84
Koha::Localization->new(
84
$child2->_result->localizations->create(
85
    {
85
    {
86
        entity      => 'itemtypes',
86
        property    => 'description',
87
        code        => $child2->itemtype,
88
        lang        => 'en',
87
        lang        => 'en',
89
        translation => 'a translated itemtype desc'
88
        translation => 'a translated itemtype desc'
90
    }
89
    }
91
)->store;
90
);
92
Koha::Localization->new(
91
$child3->_result->localizations->create(
93
    {
92
    {
94
        entity      => 'something_else',
93
        property    => 'description',
95
        code        => $child2->itemtype,
96
        lang        => 'en',
94
        lang        => 'en',
97
        translation => 'another thing'
95
        translation => 'another thing'
98
    }
96
    }
99
)->store;
97
);
98
99
Koha::Caches->get_instance('localization')->flush_all();
100
100
101
my $type = Koha::ItemTypes->find( $child1->itemtype );
101
my $type = Koha::ItemTypes->find( $child1->itemtype );
102
ok( defined($type), 'first result' );
102
ok( defined($type), 'first result' );
(-)a/t/db_dependent/Koha/Template/Plugin/ItemTypes.t (-8 / +9 lines)
Lines 20-25 use Test::NoWarnings; Link Here
20
use Test::More tests => 11;
20
use Test::More tests => 11;
21
21
22
use C4::Context;
22
use C4::Context;
23
use Koha::Caches;
23
use Koha::Database;
24
use Koha::Database;
24
use Koha::ItemTypes;
25
use Koha::ItemTypes;
25
26
Lines 53-66 my $itemtypeA = $builder->build_object( Link Here
53
        }
54
        }
54
    }
55
    }
55
);
56
);
56
Koha::Localization->new(
57
$itemtypeA->_result->localizations->create(
57
    {
58
    {
58
        entity      => 'itemtypes',
59
        property    => 'description',
59
        code        => $itemtypeA->itemtype,
60
        lang        => 'en',
60
        lang        => 'en',
61
        translation => 'Translated itemtype A'
61
        translation => 'Translated itemtype A'
62
    }
62
    }
63
)->store;
63
);
64
my $itemtypeB = $builder->build_object(
64
my $itemtypeB = $builder->build_object(
65
    {
65
    {
66
        class => 'Koha::ItemTypes',
66
        class => 'Koha::ItemTypes',
Lines 70-83 my $itemtypeB = $builder->build_object( Link Here
70
        }
70
        }
71
    }
71
    }
72
);
72
);
73
Koha::Localization->new(
73
$itemtypeB->_result->localizations->create(
74
    {
74
    {
75
        entity      => 'itemtypes',
75
        property    => 'description',
76
        code        => $itemtypeB->itemtype,
77
        lang        => 'en',
76
        lang        => 'en',
78
        translation => 'Translated itemtype B'
77
        translation => 'Translated itemtype B'
79
    }
78
    }
80
)->store;
79
);
81
my $itemtypeC = $builder->build_object(
80
my $itemtypeC = $builder->build_object(
82
    {
81
    {
83
        class => 'Koha::ItemTypes',
82
        class => 'Koha::ItemTypes',
Lines 88-93 my $itemtypeC = $builder->build_object( Link Here
88
    }
87
    }
89
);
88
);
90
89
90
Koha::Caches->get_instance('localization')->flush_all();
91
91
my $GetDescriptionA1 = $plugin->GetDescription( $itemtypeA->itemtype );
92
my $GetDescriptionA1 = $plugin->GetDescription( $itemtypeA->itemtype );
92
is( $GetDescriptionA1, "Translated itemtype A", "ItemType without parent - GetDescription without want parent" );
93
is( $GetDescriptionA1, "Translated itemtype A", "ItemType without parent - GetDescription without want parent" );
93
my $GetDescriptionA2 = $plugin->GetDescription( $itemtypeA->itemtype, 1 );
94
my $GetDescriptionA2 = $plugin->GetDescription( $itemtypeA->itemtype, 1 );
(-)a/t/db_dependent/api/v1/item_types.t (-17 / +11 lines)
Lines 26-31 use t::lib::Mocks; Link Here
26
26
27
use Mojo::JSON qw(encode_json);
27
use Mojo::JSON qw(encode_json);
28
28
29
use Koha::Caches;
29
use Koha::ItemTypes;
30
use Koha::ItemTypes;
30
use Koha::Database;
31
use Koha::Database;
31
32
Lines 67-95 subtest 'list() tests' => sub { Link Here
67
        }
68
        }
68
    );
69
    );
69
70
70
    my $en = $builder->build_object(
71
    $item_type->_result->localizations->create(
71
        {
72
        {
72
            class => 'Koha::Localizations',
73
            property    => 'description',
73
            value => {
74
            lang        => 'en',
74
                entity      => 'itemtypes',
75
            translation => 'English word "test"',
75
                code        => $item_type->id,
76
                lang        => 'en',
77
                translation => 'English word "test"',
78
            }
79
        }
76
        }
80
    );
77
    );
81
    my $sv = $builder->build_object(
78
    $item_type->_result->localizations->create(
82
        {
79
        {
83
            class => 'Koha::Localizations',
80
            property    => 'description',
84
            value => {
81
            lang        => 'sv_SE',
85
                entity      => 'itemtypes',
82
            translation => 'Swedish word "test"',
86
                code        => $item_type->id,
87
                lang        => 'sv_SE',
88
                translation => 'Swedish word "test"',
89
            }
90
        }
83
        }
91
    );
84
    );
92
85
86
    Koha::Caches->get_instance('localization')->flush_all();
87
93
    my $librarian = $builder->build_object(
88
    my $librarian = $builder->build_object(
94
        {
89
        {
95
            class => 'Koha::Patrons',
90
            class => 'Koha::Patrons',
96
- 

Return to bug 38136