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

(-)a/C4/Biblio.pm (-10 / +9 lines)
Lines 99-104 use C4::Linker; Link Here
99
use C4::OAI::Sets;
99
use C4::OAI::Sets;
100
100
101
use Koha::Logger;
101
use Koha::Logger;
102
use Koha::Cache::Memory::Lite;
102
use Koha::Caches;
103
use Koha::Caches;
103
use Koha::ClassSources;
104
use Koha::ClassSources;
104
use Koha::Authority::Types;
105
use Koha::Authority::Types;
Lines 1473-1489 sub GetAuthorisedValueDesc { Link Here
1473
1474
1474
        #---- itemtypes
1475
        #---- itemtypes
1475
        if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "itemtypes" ) {
1476
        if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "itemtypes" ) {
1476
            my $lang = C4::Languages::getlanguage;
1477
            my $memory_cache = Koha::Cache::Memory::Lite->get_instance();
1477
            $lang //= 'en';
1478
            my $cache_key = 'GetAuthorisedValueDesc:itemtypes';
1478
            $cache_key = 'itemtype:description:' . $lang;
1479
            my $itemtypes = $memory_cache->get_from_cache($cache_key);
1479
            my $itypes = $cache->get_from_cache( $cache_key, { unsafe => 1 } );
1480
            unless ($itemtypes) {
1480
            if ( !$itypes ) {
1481
                $itemtypes = { map { $_->itemtype => $_ } Koha::ItemTypes->as_list };
1481
                $itypes =
1482
                $memory_cache->set_in_cache($cache_key, $itemtypes);
1482
                  { map { $_->itemtype => $_->translated_description }
1483
                      Koha::ItemTypes->search()->as_list };
1484
                $cache->set_in_cache( $cache_key, $itypes );
1485
            }
1483
            }
1486
            return $itypes->{$value};
1484
            my $itemtype = $itemtypes->{$value};
1485
            return $itemtype ? $itemtype->translated_description : undef;
1487
        }
1486
        }
1488
1487
1489
        if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "cn_source" ) {
1488
        if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "cn_source" ) {
(-)a/Koha/ItemType.pm (-73 / +18 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') } ) {
50
97
51
    my $localization = $self->localization('description', $lang || C4::Languages::getlanguage());
98
        # If the value has already been fetched (eg. from sarch_with_localization),
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
# TODO Remove after having updated all code using unblessed translated_description
176
sub unblessed {
177
    my ($self) = @_;
178
179
    my $unblessed = $self->SUPER::unblessed();
180
181
    $unblessed->{translated_description} = $self->translated_description;
182
183
    return $unblessed;
184
}
185
241
=head2 Internal methods
186
=head2 Internal methods
242
187
243
=head3 _type
188
=head3 _type
(-)a/Koha/ItemTypes.pm (-18 / +16 lines)
Lines 33-40 Koha::ItemTypes - Koha ItemType Object set class Link Here
33
33
34
=head2 Class methods
34
=head2 Class methods
35
35
36
=cut
37
38
=head3 search_with_localization
36
=head3 search_with_localization
39
37
40
my $itemtypes = Koha::ItemTypes->search_with_localization
38
my $itemtypes = Koha::ItemTypes->search_with_localization
Lines 44-65 my $itemtypes = Koha::ItemTypes->search_with_localization Link Here
44
sub search_with_localization {
42
sub search_with_localization {
45
    my ( $self, $params, $attributes ) = @_;
43
    my ( $self, $params, $attributes ) = @_;
46
44
47
    my $language = C4::Languages::getlanguage();
45
    return $self->search( $params, $attributes )->order_by_translated_description;
48
    $Koha::Schema::Result::Itemtype::LANGUAGE = $language;
46
}
49
    $attributes->{order_by} = 'translated_description' unless exists $attributes->{order_by};
47
50
    $attributes->{join} = 'localization';
48
=head3 order_by_translated_description
51
    $attributes->{'+select'} = [
49
52
        {
50
=cut
53
            coalesce => [qw( localization.translation me.description )],
51
54
            -as      => 'translated_description'
52
sub order_by_translated_description {
55
        }
53
    my ($self) = @_;
56
    ];
54
57
    if(defined $params->{branchcode}) {
55
    my $attributes = {
58
        my $branchcode = delete $params->{branchcode};
56
        join     => 'description_localization',
59
        $self->search_with_library_limits( $params, $attributes, $branchcode );
57
        order_by => \['COALESCE(description_localization.translation, me.description)'],
60
    } else {
58
    };
61
        $self->SUPER::search( $params, $attributes );
59
62
    }
60
    return $self->search( {}, $attributes );
63
}
61
}
64
62
65
=head2 Internal methods
63
=head2 Internal methods
(-)a/Koha/Localization.pm (-73 lines)
Lines 1-73 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 <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Koha::Database;
21
22
use base qw(Koha::Object);
23
24
my $cache = Koha::Caches->get_instance();
25
26
=head1 NAME
27
28
Koha::Localization - Koha Localization type Object class
29
30
=head1 API
31
32
=head2 Class methods
33
34
=cut
35
36
=head3 store
37
38
Localization specific store to ensure relevant caches are flushed on change
39
40
=cut
41
42
sub store {
43
    my ($self) = @_;
44
    $self = $self->SUPER::store;
45
46
    if ($self->entity eq 'itemtypes') {
47
        my $key = "itemtype:description:".$self->lang;
48
        $cache->clear_from_cache($key);
49
    }
50
51
    return $self;
52
}
53
54
=head2 delete
55
56
Localization specific C<delete> to clear relevant caches on delete.
57
58
=cut
59
60
sub delete {
61
    my $self = shift @_;
62
    if ($self->entity eq 'itemtypes') {
63
        my $key = "itemtype:description:".$self->lang;
64
        $cache->clear_from_cache($key);
65
    }
66
    $self->SUPER::delete(@_);
67
}
68
69
sub _type {
70
    return 'Localization';
71
}
72
73
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 <http://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 (+27 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
985
959
=head3 $object->_result();
986
=head3 $object->_result();
960
987
961
Returns the internal DBIC Row object
988
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 (-18 / +4 lines)
Lines 344-351 __PACKAGE__->has_many( Link Here
344
);
344
);
345
345
346
346
347
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2024-10-25 13:25:14
347
# Created by DBIx::Class::Schema::Loader v0.07052 @ 2024-12-03 14:37:10
348
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:jd0dYE700dpg1IiRnfbcEg
348
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:l3ycM1L5bmCeM0Pby8GceA
349
349
350
__PACKAGE__->add_columns(
350
__PACKAGE__->add_columns(
351
    '+automatic_checkin'            => { is_boolean => 1 },
351
    '+automatic_checkin'            => { is_boolean => 1 },
Lines 356-377 __PACKAGE__->add_columns( Link Here
356
    '+bookable'                     => { is_boolean => 1, is_nullable => 1 },
356
    '+bookable'                     => { is_boolean => 1, is_nullable => 1 },
357
);
357
);
358
358
359
# Use the ItemtypeLocalization view to create the join on localization
359
__PACKAGE__->load_components('+Koha::Schema::Component::Localization');
360
our $LANGUAGE;
360
__PACKAGE__->localization_add_relationships('itemtype', 'description');
361
__PACKAGE__->has_many(
362
  "localization" => "Koha::Schema::Result::ItemtypeLocalization",
363
    sub {
364
        my $args = shift;
365
366
        die "no lang specified!" unless $LANGUAGE;
367
368
        return ({
369
            "$args->{self_alias}.itemtype" => { -ident => "$args->{foreign_alias}.code" },
370
            "$args->{foreign_alias}.lang" => $LANGUAGE,
371
        });
372
373
    }
374
);
375
361
376
sub koha_object_class {
362
sub koha_object_class {
377
    'Koha::ItemType';
363
    'Koha::ItemType';
(-)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/Schema/Result/ItemtypesLocalization.pm (+124 lines)
Line 0 Link Here
1
use utf8;
2
package Koha::Schema::Result::ItemtypesLocalization;
3
4
# Created by DBIx::Class::Schema::Loader
5
# DO NOT MODIFY THE FIRST PART OF THIS FILE
6
7
=head1 NAME
8
9
Koha::Schema::Result::ItemtypesLocalization
10
11
=cut
12
13
use strict;
14
use warnings;
15
16
use base 'DBIx::Class::Core';
17
18
=head1 TABLE: C<itemtypes_localizations>
19
20
=cut
21
22
__PACKAGE__->table("itemtypes_localizations");
23
24
=head1 ACCESSORS
25
26
=head2 itemtypes_localizations_id
27
28
  data_type: 'integer'
29
  is_auto_increment: 1
30
  is_nullable: 0
31
32
=head2 itemtype
33
34
  data_type: 'varchar'
35
  is_foreign_key: 1
36
  is_nullable: 0
37
  size: 10
38
39
=head2 property
40
41
  data_type: 'varchar'
42
  is_nullable: 0
43
  size: 100
44
45
=head2 lang
46
47
  data_type: 'varchar'
48
  is_nullable: 0
49
  size: 25
50
51
=head2 translation
52
53
  data_type: 'mediumtext'
54
  is_nullable: 1
55
56
=cut
57
58
__PACKAGE__->add_columns(
59
  "itemtypes_localizations_id",
60
  { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
61
  "itemtype",
62
  { data_type => "varchar", is_foreign_key => 1, is_nullable => 0, size => 10 },
63
  "property",
64
  { data_type => "varchar", is_nullable => 0, size => 100 },
65
  "lang",
66
  { data_type => "varchar", is_nullable => 0, size => 25 },
67
  "translation",
68
  { data_type => "mediumtext", is_nullable => 1 },
69
);
70
71
=head1 PRIMARY KEY
72
73
=over 4
74
75
=item * L</itemtypes_localizations_id>
76
77
=back
78
79
=cut
80
81
__PACKAGE__->set_primary_key("itemtypes_localizations_id");
82
83
=head1 UNIQUE CONSTRAINTS
84
85
=head2 C<itemtype_property_lang>
86
87
=over 4
88
89
=item * L</itemtype>
90
91
=item * L</property>
92
93
=item * L</lang>
94
95
=back
96
97
=cut
98
99
__PACKAGE__->add_unique_constraint("itemtype_property_lang", ["itemtype", "property", "lang"]);
100
101
=head1 RELATIONS
102
103
=head2 itemtype
104
105
Type: belongs_to
106
107
Related object: L<Koha::Schema::Result::Itemtype>
108
109
=cut
110
111
__PACKAGE__->belongs_to(
112
  "itemtype",
113
  "Koha::Schema::Result::Itemtype",
114
  { itemtype => "itemtype" },
115
  { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
116
);
117
118
119
# Created by DBIx::Class::Schema::Loader v0.07052 @ 2024-10-08 14:32:00
120
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:9xi1V/qaD/R9u3KHcW7saA
121
122
123
# You can replace this text with custom code or comments, and it will be preserved on regeneration
124
1;
(-)a/Koha/Schema/Result/Localization.pm (-108 lines)
Lines 1-108 Link Here
1
use utf8;
2
package Koha::Schema::Result::Localization;
3
4
# Created by DBIx::Class::Schema::Loader
5
# DO NOT MODIFY THE FIRST PART OF THIS FILE
6
7
=head1 NAME
8
9
Koha::Schema::Result::Localization
10
11
=cut
12
13
use strict;
14
use warnings;
15
16
use base 'DBIx::Class::Core';
17
18
=head1 TABLE: C<localization>
19
20
=cut
21
22
__PACKAGE__->table("localization");
23
24
=head1 ACCESSORS
25
26
=head2 localization_id
27
28
  data_type: 'integer'
29
  is_auto_increment: 1
30
  is_nullable: 0
31
32
=head2 entity
33
34
  data_type: 'varchar'
35
  is_nullable: 0
36
  size: 16
37
38
=head2 code
39
40
  data_type: 'varchar'
41
  is_nullable: 0
42
  size: 64
43
44
=head2 lang
45
46
  data_type: 'varchar'
47
  is_nullable: 0
48
  size: 25
49
50
could be a foreign key
51
52
=head2 translation
53
54
  data_type: 'mediumtext'
55
  is_nullable: 1
56
57
=cut
58
59
__PACKAGE__->add_columns(
60
  "localization_id",
61
  { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
62
  "entity",
63
  { data_type => "varchar", is_nullable => 0, size => 16 },
64
  "code",
65
  { data_type => "varchar", is_nullable => 0, size => 64 },
66
  "lang",
67
  { data_type => "varchar", is_nullable => 0, size => 25 },
68
  "translation",
69
  { data_type => "mediumtext", is_nullable => 1 },
70
);
71
72
=head1 PRIMARY KEY
73
74
=over 4
75
76
=item * L</localization_id>
77
78
=back
79
80
=cut
81
82
__PACKAGE__->set_primary_key("localization_id");
83
84
=head1 UNIQUE CONSTRAINTS
85
86
=head2 C<entity_code_lang>
87
88
=over 4
89
90
=item * L</entity>
91
92
=item * L</code>
93
94
=item * L</lang>
95
96
=back
97
98
=cut
99
100
__PACKAGE__->add_unique_constraint("entity_code_lang", ["entity", "code", "lang"]);
101
102
103
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-01-21 13:39:29
104
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Elbup2i+1JON+xa38uzd6A
105
106
107
# You can replace this text with custom code or comments, and it will be preserved on regeneration
108
1;
(-)a/Koha/UI/Form/Builder/Item.pm (-6 / +3 lines)
Lines 213-226 sub generate_subfield_form { Link Here
213
        }
213
        }
214
        elsif ( $subfield->{authorised_value} eq "itemtypes" ) {
214
        elsif ( $subfield->{authorised_value} eq "itemtypes" ) {
215
            push @authorised_values, "";
215
            push @authorised_values, "";
216
            my $itemtypes;
216
            my $itemtypes = Koha::ItemTypes->new;
217
            if ($branch_limit) {
217
            if ($branch_limit) {
218
                $itemtypes = Koha::ItemTypes->search_with_localization(
218
                $itemtypes = $itemtypes->search_with_library_limits({}, {}, $branch_limit);
219
                    { branchcode => $branch_limit } );
220
            }
221
            else {
222
                $itemtypes = Koha::ItemTypes->search_with_localization;
223
            }
219
            }
220
            $itemtypes = $itemtypes->order_by_translated_description;
224
            while ( my $itemtype = $itemtypes->next ) {
221
            while ( my $itemtype = $itemtypes->next ) {
225
                push @authorised_values, $itemtype->itemtype;
222
                push @authorised_values, $itemtype->itemtype;
226
                $authorised_lib{ $itemtype->itemtype } =
223
                $authorised_lib{ $itemtype->itemtype } =
(-)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 (-15 / +19 lines)
Lines 21-28 use Modern::Perl; Link Here
21
use C4::Auth qw( get_template_and_user );
21
use C4::Auth qw( get_template_and_user );
22
use C4::Output qw( output_html_with_http_headers );
22
use C4::Output qw( output_html_with_http_headers );
23
23
24
use Koha::Localization;
24
use Koha::Database;
25
use Koha::Localizations;
25
26
my $schema = Koha::Database->schema;
26
27
27
use CGI qw( -utf8 );
28
use CGI qw( -utf8 );
28
29
Lines 36-53 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
      { id          => $s->localization_id,
49
    push @translations, {
46
        entity      => $s->entity,
50
        localization_id => $localization->id,
47
        code        => $s->code,
51
        lang            => $localization->lang,
48
        lang        => $s->lang,
52
        translation     => $localization->translation,
49
        translation => $s->translation,
53
    };
50
      };
51
}
54
}
52
55
53
my $translated_languages = C4::Languages::getTranslatedLanguages( 'intranet', C4::Context->preference('template') );
56
my $translated_languages = C4::Languages::getTranslatedLanguages( 'intranet', C4::Context->preference('template') );
Lines 55-62 my $translated_languages = C4::Languages::getTranslatedLanguages( 'intranet', C4 Link Here
55
$template->param(
58
$template->param(
56
    translations => \@translations,
59
    translations => \@translations,
57
    languages    => $translated_languages,
60
    languages    => $translated_languages,
58
    entity       => $entity,
61
    source       => $source,
59
    code         => $code,
62
    object_id    => $object_id,
63
    property     => $property,
60
);
64
);
61
65
62
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 (+22 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("alter table `localization` add unique key `entity_code_property_lang` (`entity`, `code`, `property`, `lang`)");
17
18
            say_success($out, 'Added column localization.property and updated localization.entity values');
19
        }
20
21
    },
22
};
(-)a/installer/data/mysql/kohastructure.sql (-1 / +2 lines)
Lines 4406-4415 CREATE TABLE `localization` ( Link Here
4406
  `localization_id` int(11) NOT NULL AUTO_INCREMENT,
4406
  `localization_id` int(11) NOT NULL AUTO_INCREMENT,
4407
  `entity` varchar(16) NOT NULL,
4407
  `entity` varchar(16) NOT NULL,
4408
  `code` varchar(64) NOT NULL,
4408
  `code` varchar(64) NOT NULL,
4409
  `property` varchar(100) NOT NULL,
4409
  `lang` varchar(25) NOT NULL COMMENT 'could be a foreign key',
4410
  `lang` varchar(25) NOT NULL COMMENT 'could be a foreign key',
4410
  `translation` mediumtext DEFAULT NULL,
4411
  `translation` mediumtext DEFAULT NULL,
4411
  PRIMARY KEY (`localization_id`),
4412
  PRIMARY KEY (`localization_id`),
4412
  UNIQUE KEY `entity_code_lang` (`entity`,`code`,`lang`)
4413
  UNIQUE KEY `entity_code_property_lang` (`entity`,`code`,`property`,`lang`)
4413
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4414
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4414
/*!40101 SET character_set_client = @saved_cs_client */;
4415
/*!40101 SET character_set_client = @saved_cs_client */;
4415
4416
(-)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 %]" rel="gb_page_center[600,500]"><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 192-198 Link Here
192
                    <label for="description" class="required">Description: </label>
192
                    <label for="description" class="required">Description: </label>
193
                    <input type="text" id="description" name="description" size="48" value="[% itemtype.description | html %]" required="required" /> <span class="required">Required</span>
193
                    <input type="text" id="description" name="description" size="48" value="[% itemtype.description | html %]" required="required" /> <span class="required">Required</span>
194
                    [% IF can_be_translated %]
194
                    [% IF can_be_translated %]
195
                        <a href="/cgi-bin/koha/admin/localization.pl?entity=itemtypes&code=[% itemtype.itemtype | uri %]" title="Translate item type [% itemtype.itemtype | html %]" rel="gb_page_center[600,500]"><i class="fa-solid fa-pencil" aria-hidden="true"></i> Translate into other languages</a>
195
                        [% INCLUDE 'localization-link.inc' source='Itemtype' object_id=itemtype.itemtype property='description' %]
196
                    [% END %]
196
                    [% END %]
197
                </li>
197
                </li>
198
                <li>
198
                <li>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/localization.tt (-34 / +45 lines)
Lines 18-31 Link Here
18
                <h1>Localization</h1>
18
                <h1>Localization</h1>
19
<form id="add_translation" method="get">
19
<form id="add_translation" method="get">
20
    [% INCLUDE 'csrf-token.inc' %]
20
    [% INCLUDE 'csrf-token.inc' %]
21
    <input type="hidden" name="entity" value="[% entity | html %]" />
21
    <input type="hidden" name="source" value="[% source | html %]" />
22
    <input type="hidden" name="code" value="[% code | html %]" />
22
    <input type="hidden" name="object_id" value="[% object_id | html %]" />
23
    <input type="hidden" name="interface" value="[% interface_side | html %]" />
23
    <input type="hidden" name="property" value="[% property | html %]" />
24
    <fieldset class="rows clearfix">
24
    <fieldset class="rows clearfix">
25
        <ol>
25
        <ol>
26
            <li>
26
            <li>
27
                <span class="label">Authorized value:</span>
27
                <span class="label">Authorized value:</span>
28
                [% code | html %]
28
                [% object_id | html %]
29
            </li>
29
            </li>
30
            <li>
30
            <li>
31
                <label for="lang">Language:</label>
31
                <label for="lang">Language:</label>
Lines 66-74 Link Here
66
<table id="localization">
66
<table id="localization">
67
    <thead>
67
    <thead>
68
        <tr>
68
        <tr>
69
            <th>Id</th>
70
            <th>Entity</th>
71
            <th>Code</th>
72
            <th>Language</th>
69
            <th>Language</th>
73
            <th>Translation</th>
70
            <th>Translation</th>
74
            <th class="NoSort">&nbsp;</th>
71
            <th class="NoSort">&nbsp;</th>
Lines 76-85 Link Here
76
    </thead>
73
    </thead>
77
    <tbody>
74
    <tbody>
78
        [% FOR t IN translations %]
75
        [% FOR t IN translations %]
79
        <tr id="row_id_[% t.id | html %]" data-id="[% t.id | html %]">
76
        <tr id="row_id_[% t.localization_id | html %]" data-id="[% t.localization_id | html %]">
80
            <td>[% t.id | html %]</td>
81
            <td>[% t.entity | html %]</td>
82
            <td>[% t.code | html %]</td>
83
            <td class="lang">[% t.lang | html %]</td>
77
            <td class="lang">[% t.lang | html %]</td>
84
            <td class="translation" contenteditable="true">[% t.translation | html %]</td>
78
            <td class="translation" contenteditable="true">[% t.translation | html %]</td>
85
            <td class="actions"><a href="#" class="delete"><i class="fa fa-trash-can"></i> Delete</a></td>
79
            <td class="actions"><a href="#" class="delete"><i class="fa fa-trash-can"></i> Delete</a></td>
Lines 102-108 Link Here
102
            var message;
96
            var message;
103
            if ( type == 'success_on_update' ) {
97
            if ( type == 'success_on_update' ) {
104
                message = $('<div class="alert alert-info"></div>');
98
                message = $('<div class="alert alert-info"></div>');
105
                message.text(_("Entity %s (code %s) for lang %s has correctly been updated with '%s'").format(data.entity, data.code, data.lang, data.translation));
99
                message.text(_("Translation for lang %s has correctly been updated with '%s'").format(data.lang, data.translation));
106
            } else if ( type == 'error_on_update' ) {
100
            } else if ( type == 'error_on_update' ) {
107
                message = $('<div class="alert alert-warning"></div>');
101
                message = $('<div class="alert alert-warning"></div>');
108
                if ( data.error_code == 'already_exists' ) {
102
                if ( data.error_code == 'already_exists' ) {
Lines 112-124 Link Here
112
                }
106
                }
113
            } else if ( type == 'success_on_delete' ) {
107
            } else if ( type == 'success_on_delete' ) {
114
                message = $('<div class="alert alert-info"></div>');
108
                message = $('<div class="alert alert-info"></div>');
115
                message.text(_("The translation (id %s) has been removed successfully").format(data.id));
109
                message.text(_("The translation has been removed successfully"));
116
            } else if ( type == 'error_on_delete' ) {
110
            } else if ( type == 'error_on_delete' ) {
117
                message = $('<div class="alert alert-warning"></div>');
111
                message = $('<div class="alert alert-warning"></div>');
118
                message.text(_("An error occurred when deleting this translation"));
112
                message.text(_("An error occurred when deleting this translation"));
119
            } else if ( type == 'success_on_insert' ) {
113
            } else if ( type == 'success_on_insert' ) {
120
                message = $('<div class="alert alert-info"></div>');
114
                message = $('<div class="alert alert-info"></div>');
121
                message.text(_("Translation (id %s) has been added successfully").format(data.id));
115
                message.text(_("Translation has been added successfully"));
122
            } else if ( type == 'error_on_insert' ) {
116
            } else if ( type == 'error_on_insert' ) {
123
                message = $('<div class="alert alert-warning"></div>');
117
                message = $('<div class="alert alert-warning"></div>');
124
                if ( data.error_code == 'already_exists' ) {
118
                if ( data.error_code == 'already_exists' ) {
Lines 135-148 Link Here
135
            }, 3000);
129
            }, 3000);
136
        }
130
        }
137
131
138
        function send_update_request( data, cell ) {
132
        function send_update_request( _data, cell ) {
133
            const form = document.forms.add_translation;
134
            const source = form.elements.source.value;
135
            const object_id = form.elements.object_id.value;
136
            const data = Object.assign({}, _data, { source, object_id });
139
            const client = APIClient.localization;
137
            const client = APIClient.localization;
140
            client.localizations.update(data).then(
138
            client.localizations.update(data).then(
141
                success => {
139
                success => {
142
                    if ( success.error ) {
140
                    if ( success.error ) {
143
                        $(cell).css('background-color', '#FF0000');
141
                        $(cell).css('background-color', '#FF0000');
144
                        show_message({ type: 'error_on_update', data: success });
142
                        show_message({ type: 'error_on_update', data: success });
145
                    } else if ( success.is_changed == 1 ) {
143
                    } else {
146
                        $(cell).css('background-color', '#00FF00');
144
                        $(cell).css('background-color', '#00FF00');
147
                        show_message({ type: 'success_on_update', data: success });
145
                        show_message({ type: 'success_on_update', data: success });
148
                    }
146
                    }
Lines 166-177 Link Here
166
            );
164
            );
167
        }
165
        }
168
166
169
        function send_delete_request( id, cell ) {
167
        function send_delete_request( localization_id, cell ) {
168
            const form = document.forms.add_translation;
169
            const source = form.elements.source.value;
170
            const object_id = form.elements.object_id.value;
171
            const property = form.elements.property.value;
172
            const data = { source, object_id, property, localization_id };
173
170
            const client = APIClient.localization;
174
            const client = APIClient.localization;
171
            client.localizations.delete(id).then(
175
            client.localizations.delete(data).then(
172
                success => {
176
                success => {
173
                    $("#localization").DataTable().row( '#row_id_' + id ).remove().draw();
177
                    $("#localization").DataTable().row( '#row_id_' + localization_id ).remove().draw();
174
                    show_message({ type: 'success_on_delete', data: {id} });
178
                    show_message({ type: 'success_on_delete', data: {localization_id} });
175
                },
179
                },
176
                error => {
180
                error => {
177
                    $(cell).css('background-color', '#FF9090');
181
                    $(cell).css('background-color', '#FF9090');
Lines 212-217 Link Here
212
            });
216
            });
213
            $("td.lang").on('click', function(){
217
            $("td.lang").on('click', function(){
214
                var td = this;
218
                var td = this;
219
                if (td.childElementCount > 0) {
220
                    // do nothing if there is already something there (like a select for instance)
221
                    return;
222
                }
223
215
                var lang = $(td).text();
224
                var lang = $(td).text();
216
                $(td).css('background-color', '');
225
                $(td).css('background-color', '');
217
                var my_select = $(languages_select).clone();
226
                var my_select = $(languages_select).clone();
Lines 221-230 Link Here
221
                });
230
                });
222
                $(my_select).on('change', function(){
231
                $(my_select).on('change', function(){
223
                    var tr = $(this).parent().parent();
232
                    var tr = $(this).parent().parent();
224
                    var id = $(tr).data('id');
233
                    var localization_id = $(tr).data('id');
225
                    var lang = $(this).find('option:selected').val();
234
                    var lang = $(this).find('option:selected').val();
226
                    var translation = $(this).text();
235
                    send_update_request( {localization_id, lang}, td );
227
                    send_update_request( {id, lang, translation}, td );
228
                });
236
                });
229
                $(my_select).on('blur', function(){
237
                $(my_select).on('blur', function(){
230
                    $(td).html(lang);
238
                    $(td).html(lang);
Lines 234-243 Link Here
234
242
235
            $("td.translation").on('blur', function(){
243
            $("td.translation").on('blur', function(){
236
                var tr = $(this).parent();
244
                var tr = $(this).parent();
237
                var id = $(tr).data('id');
245
                var localization_id = $(tr).data('id');
238
                var lang = $(tr).find('td.lang').text();
239
                var translation = $(this).text();
246
                var translation = $(this).text();
240
                send_update_request( {id, lang, translation}, this );
247
                send_update_request( {localization_id, translation}, this );
241
            });
248
            });
242
249
243
            $("body").on("click", "a.delete", function(e){
250
            $("body").on("click", "a.delete", function(e){
Lines 252-271 Link Here
252
259
253
            $("#add_translation").on('submit', function(e){
260
            $("#add_translation").on('submit', function(e){
254
                e.preventDefault();
261
                e.preventDefault();
255
                let localization = {
262
256
                    entity: $(this).find('input[name="entity"]').val(),
263
                const form = this;
257
                    code: $(this).find('input[name="code"]').val(),
264
                const source = form.elements.source.value;
258
                    lang: $(this).find('select[name="lang"] option:selected').val(),
265
                const object_id = form.elements.object_id.value;
259
                    translation: $(this).find('input[name="translation"]').val(),
266
                const property = form.elements.property.value;
260
                };
267
                const lang = form.elements.lang.value
268
                const translation = form.elements.translation.value
269
270
                let localization = { source, object_id, property, lang, translation };
261
                const client = APIClient.localization;
271
                const client = APIClient.localization;
262
                client.localizations.create(localization).then(
272
                client.localizations.create(localization).then(
263
                    success => {
273
                    success => {
264
                        if ( success.error ) {
274
                        if ( success.error ) {
265
                            show_message({ type: 'error_on_insert', data: success });
275
                            show_message({ type: 'error_on_insert', data: success });
266
                        } else {
276
                        } else {
267
                            var new_row = table.row.add( [ success.id, success.entity, success.code, success.lang, success.translation, "<a href=\"#\" class=\"delete\"><i class=\"fa fa-trash-can\"></i> Delete</a>" ] ).draw().node();
277
                            let delete_str = _("Delete");
268
                            $( new_row ).attr("id", "row_id_" + success.id ).data("id", success.id );
278
                            var new_row = table.row.add( [ success.lang, success.translation, "<a href=\"#\" class=\"delete\"><i class=\"fa fa-trash-can\"></i> %s</a>".format(escape_str(delete_str)) ] ).draw().node();
279
                            $( new_row ).attr("id", "row_id_" + success.localization_id ).data("id", success.localization_id );
269
                            show_message({ type: 'success_on_insert', data: success });
280
                            show_message({ type: 'success_on_insert', data: success });
270
                        }
281
                        }
271
                    },
282
                    },
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/localization-api-client.js (-17 / +4 lines)
Lines 13-24 export class LocalizationAPIClient extends HttpClient { Link Here
13
            create: localization =>
13
            create: localization =>
14
                this.post({
14
                this.post({
15
                    endpoint: "",
15
                    endpoint: "",
16
                    body: "entity=%s&code=%s&lang=%s&translation=%s".format(
16
                    body: (new URLSearchParams(localization)).toString(),
17
                        encodeURIComponent(localization.entity),
18
                        encodeURIComponent(localization.code),
19
                        encodeURIComponent(localization.lang),
20
                        encodeURIComponent(localization.translation)
21
                    ),
22
                    headers: {
17
                    headers: {
23
                        "Content-Type":
18
                        "Content-Type":
24
                            "application/x-www-form-urlencoded;charset=utf-8",
19
                            "application/x-www-form-urlencoded;charset=utf-8",
Lines 27-49 export class LocalizationAPIClient extends HttpClient { Link Here
27
            update: localization =>
22
            update: localization =>
28
                this.put({
23
                this.put({
29
                    endpoint: "",
24
                    endpoint: "",
30
                    body: "id=%s&lang=%s&translation=%s".format(
25
                    body: (new URLSearchParams(localization)).toString(),
31
                        encodeURIComponent(localization.id),
32
                        encodeURIComponent(localization.lang),
33
                        encodeURIComponent(localization.translation)
34
                    ),
35
                    headers: {
26
                    headers: {
36
                        "Content-Type":
27
                        "Content-Type":
37
                            "application/x-www-form-urlencoded;charset=utf-8",
28
                            "application/x-www-form-urlencoded;charset=utf-8",
38
                    },
29
                    },
39
                }),
30
                }),
40
            delete: id =>
31
            delete: localization =>
41
                this.delete({
32
                this.delete({
42
                    endpoint: "/?id=%s".format(id),
33
                    endpoint: "?" + (new URLSearchParams(localization)).toString(),
43
                    headers: {
44
                        "Content-Type":
45
                            "application/x-www-form-urlencoded;charset=utf-8",
46
                    },
47
                }),
34
                }),
48
        };
35
        };
49
    }
36
    }
(-)a/svc/localization (-66 / +77 lines)
Lines 2-104 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');
18
    my $object_id = $query->param('object_id');
30
    my $lang = $query->param('lang');
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 ( Koha::Localizations->search( { entity => $localization->entity, code => $localization->code, lang => $lang, localization_id => { '!=' => $localization->localization_id }, } )->count ) {
33
                Koha::Caches->get_instance('localization')->clear_from_cache("$source:$lang") if $lang;
44
            $localization->store;
34
            } catch {
45
        } else {
35
                $localization->discard_changes();
46
            $params{error} = 1;
36
                $response->param(error => 1, error_code => 'already_exists');
47
            $params{error_code} = 'already_exists';
37
            };
48
        }
38
        }
39
40
        $response->param(
41
            lang        => $localization->lang,
42
            translation => $localization->translation,
43
        );
49
    }
44
    }
50
    $response->param(
45
51
        %params,
52
        id          => $localization->localization_id,
53
        entity      => $localization->entity,
54
        code        => $localization->code,
55
        lang        => $localization->lang,
56
        translation => $localization->translation,
57
        is_changed  => $is_changed,
58
    );
59
    C4::Service->return_success( $response );
46
    C4::Service->return_success( $response );
60
}
47
}
61
48
62
sub add_translation {
49
sub add_translation {
63
    my $entity = $query->param('entity');
50
    my $source = $query->param('source');
64
    my $code = $query->param('code');
51
    my $localization_id = $query->param('localization_id');
52
    my $object_id = $query->param('object_id');
53
    my $property = $query->param('property');
65
    my $lang = $query->param('lang');
54
    my $lang = $query->param('lang');
66
    my $translation = $query->param('translation');
55
    my $translation = $query->param('translation');
67
56
68
    unless ( Koha::Localizations->search({entity => $entity, code => $code, lang => $lang, })->count ) {
57
    my $schema = Koha::Database->schema;
69
        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",
70
            {
62
            {
71
                entity => $entity,
63
                entity => $source,
72
                code => $code,
73
                lang => $lang,
64
                lang => $lang,
74
                translation => $translation,
65
                translation => $translation,
75
            }
66
            }
76
        );
67
        );
77
        $localization->store;
68
        Koha::Caches->get_instance('localization')->clear_from_cache("$source:$lang");
69
78
        $response->param(
70
        $response->param(
79
            id          => $localization->localization_id,
71
            lang            => $localization->lang,
80
            entity      => $localization->entity,
72
            translation     => $localization->translation,
81
            code        => $localization->code,
73
            localization_id => $localization->id,
82
            lang        => $localization->lang,
83
            translation => $localization->translation,
84
        );
74
        );
75
    } catch {
76
        $response->param(error => 1, error_code => 'already_exists');
77
    };
85
78
86
    } else {
87
        $response->param( error => 1, error_code => 'already_exists', );
88
    }
89
    C4::Service->return_success( $response );
79
    C4::Service->return_success( $response );
90
}
80
}
91
81
92
sub delete_translation {
82
sub delete_translation {
93
    my $id = $query->param('id');
83
    my $source          = $query->param('source');
94
    Koha::Localizations->find($id)->delete;
84
    my $object_id       = $query->param('object_id');
95
    $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
96
    C4::Service->return_success( $response );
108
    C4::Service->return_success( $response );
97
}
109
}
98
110
99
C4::Service->dispatch(
111
C4::Service->dispatch(
100
    [ 'GET /', [ 'id' ], \&get_translations ],
112
    [ 'PUT /', [], \&update_translation ],
101
    [ 'PUT /', [ 'id' ], \&update_translation ],
113
    [ 'POST /', [], \&add_translation ],
102
    [ 'POST /', [ 'entity', 'code', 'lang', 'translation' ], \&add_translation ],
114
    [ 'DELETE /', [],  \&delete_translation ],
103
    [ 'DELETE /', ['id'],  \&delete_translation ],
104
);
115
);
(-)a/t/db_dependent/Koha/Filter/ExpandCodedFields.t (-1 / +2 lines)
Lines 68-77 subtest 'ExpandCodedFields tests' => sub { Link Here
68
    $cache->clear_from_cache("MarcCodedFields-");
68
    $cache->clear_from_cache("MarcCodedFields-");
69
    # Clear GetAuthorisedValueDesc-generated cache
69
    # Clear GetAuthorisedValueDesc-generated cache
70
    $cache->clear_from_cache("libraries:name");
70
    $cache->clear_from_cache("libraries:name");
71
    $cache->clear_from_cache("itemtype:description:en");
72
    $cache->clear_from_cache("cn_sources:description");
71
    $cache->clear_from_cache("cn_sources:description");
73
    $cache->clear_from_cache("AV_descriptions:" . $av->category);
72
    $cache->clear_from_cache("AV_descriptions:" . $av->category);
74
73
74
    Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en');
75
75
    C4::Biblio::ModBiblio( $record, $biblio->biblionumber );
76
    C4::Biblio::ModBiblio( $record, $biblio->biblionumber );
76
    $biblio = Koha::Biblios->find( $biblio->biblionumber);
77
    $biblio = Koha::Biblios->find( $biblio->biblionumber);
77
    $record = $biblio->metadata->record;
78
    $record = $biblio->metadata->record;
(-)a/t/db_dependent/Koha/Item.t (-4 / +8 lines)
Lines 1950-1959 subtest 'columns_to_str' => sub { Link Here
1950
    $cache->clear_from_cache("MarcStructure-1-");
1950
    $cache->clear_from_cache("MarcStructure-1-");
1951
    $cache->clear_from_cache("MarcSubfieldStructure-");
1951
    $cache->clear_from_cache("MarcSubfieldStructure-");
1952
    $cache->clear_from_cache("libraries:name");
1952
    $cache->clear_from_cache("libraries:name");
1953
    $cache->clear_from_cache("itemtype:description:en");
1954
    $cache->clear_from_cache("cn_sources:description");
1953
    $cache->clear_from_cache("cn_sources:description");
1955
    $cache->clear_from_cache("AV_descriptions:LOST");
1954
    $cache->clear_from_cache("AV_descriptions:LOST");
1956
1955
1956
    Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en');
1957
1957
    # Creating subfields 'é', 'è' that are not linked with a kohafield
1958
    # Creating subfields 'é', 'è' that are not linked with a kohafield
1958
    Koha::MarcSubfieldStructures->search(
1959
    Koha::MarcSubfieldStructures->search(
1959
        {
1960
        {
Lines 2034-2043 subtest 'columns_to_str' => sub { Link Here
2034
    $cache->clear_from_cache("MarcStructure-1-");
2035
    $cache->clear_from_cache("MarcStructure-1-");
2035
    $cache->clear_from_cache("MarcSubfieldStructure-");
2036
    $cache->clear_from_cache("MarcSubfieldStructure-");
2036
    $cache->clear_from_cache("libraries:name");
2037
    $cache->clear_from_cache("libraries:name");
2037
    $cache->clear_from_cache("itemtype:description:en");
2038
    $cache->clear_from_cache("cn_sources:description");
2038
    $cache->clear_from_cache("cn_sources:description");
2039
    $cache->clear_from_cache("AV_descriptions:LOST");
2039
    $cache->clear_from_cache("AV_descriptions:LOST");
2040
2040
2041
    Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en');
2042
2041
    $schema->storage->txn_rollback;
2043
    $schema->storage->txn_rollback;
2042
};
2044
};
2043
2045
Lines 2054-2063 subtest 'strings_map() tests' => sub { Link Here
2054
    $cache->clear_from_cache("MarcStructure-1-");
2056
    $cache->clear_from_cache("MarcStructure-1-");
2055
    $cache->clear_from_cache("MarcSubfieldStructure-");
2057
    $cache->clear_from_cache("MarcSubfieldStructure-");
2056
    $cache->clear_from_cache("libraries:name");
2058
    $cache->clear_from_cache("libraries:name");
2057
    $cache->clear_from_cache("itemtype:description:en");
2058
    $cache->clear_from_cache("cn_sources:description");
2059
    $cache->clear_from_cache("cn_sources:description");
2059
    $cache->clear_from_cache("AV_descriptions:LOST");
2060
    $cache->clear_from_cache("AV_descriptions:LOST");
2060
2061
2062
    Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en');
2063
2061
    # Recreating subfields just to be sure tests will be ok
2064
    # Recreating subfields just to be sure tests will be ok
2062
    # 1 => av (LOST)
2065
    # 1 => av (LOST)
2063
    # 3 => no link
2066
    # 3 => no link
Lines 2240-2248 subtest 'strings_map() tests' => sub { Link Here
2240
    $cache->clear_from_cache("MarcStructure-1-");
2243
    $cache->clear_from_cache("MarcStructure-1-");
2241
    $cache->clear_from_cache("MarcSubfieldStructure-");
2244
    $cache->clear_from_cache("MarcSubfieldStructure-");
2242
    $cache->clear_from_cache("libraries:name");
2245
    $cache->clear_from_cache("libraries:name");
2243
    $cache->clear_from_cache("itemtype:description:en");
2244
    $cache->clear_from_cache("cn_sources:description");
2246
    $cache->clear_from_cache("cn_sources:description");
2245
2247
2248
    Koha::Caches->get_instance('localization')->clear_from_cache('Itemtype:en');
2249
2246
    $schema->storage->txn_rollback;
2250
    $schema->storage->txn_rollback;
2247
};
2251
};
2248
2252
(-)a/t/db_dependent/Koha/ItemTypes.t (-12 / +12 lines)
Lines 26-31 use t::lib::TestBuilder; Link Here
26
26
27
use C4::Calendar qw( new );
27
use C4::Calendar qw( new );
28
use Koha::Biblioitems;
28
use Koha::Biblioitems;
29
use Koha::Caches;
29
use Koha::Libraries;
30
use Koha::Libraries;
30
use Koha::Database;
31
use Koha::Database;
31
use Koha::DateUtils qw(dt_from_string);;
32
use Koha::DateUtils qw(dt_from_string);;
Lines 66-95 my $child3 = $builder->build_object({ Link Here
66
        }
67
        }
67
    });
68
    });
68
69
69
Koha::Localization->new(
70
$child1->_result->localizations->create(
70
    {
71
    {
71
        entity      => 'itemtypes',
72
        property    => 'description',
72
        code        => $child1->itemtype,
73
        lang        => 'en',
73
        lang        => 'en',
74
        translation => 'b translated itemtype desc'
74
        translation => 'b translated itemtype desc'
75
    }
75
    }
76
)->store;
76
);
77
Koha::Localization->new(
77
$child2->_result->localizations->create(
78
    {
78
    {
79
        entity      => 'itemtypes',
79
        property    => 'description',
80
        code        => $child2->itemtype,
81
        lang        => 'en',
80
        lang        => 'en',
82
        translation => 'a translated itemtype desc'
81
        translation => 'a translated itemtype desc'
83
    }
82
    }
84
)->store;
83
);
85
Koha::Localization->new(
84
$child3->_result->localizations->create(
86
    {
85
    {
87
        entity      => 'something_else',
86
        property    => 'description',
88
        code        => $child2->itemtype,
89
        lang        => 'en',
87
        lang        => 'en',
90
        translation => 'another thing'
88
        translation => 'another thing'
91
    }
89
    }
92
)->store;
90
);
91
92
Koha::Caches->get_instance('localization')->flush_all();
93
93
94
my $type = Koha::ItemTypes->find($child1->itemtype);
94
my $type = Koha::ItemTypes->find($child1->itemtype);
95
ok( defined($type), 'first result' );
95
ok( defined($type), 'first result' );
(-)a/t/db_dependent/Koha/Template/Plugin/ItemTypes.t (-8 / +9 lines)
Lines 19-24 use Modern::Perl; Link Here
19
use Test::More tests => 10;
19
use Test::More tests => 10;
20
20
21
use C4::Context;
21
use C4::Context;
22
use Koha::Caches;
22
use Koha::Database;
23
use Koha::Database;
23
use Koha::ItemTypes;
24
use Koha::ItemTypes;
24
25
Lines 52-65 my $itemtypeA = $builder->build_object( Link Here
52
        }
53
        }
53
    }
54
    }
54
);
55
);
55
Koha::Localization->new(
56
$itemtypeA->_result->localizations->create(
56
    {
57
    {
57
        entity      => 'itemtypes',
58
        property    => 'description',
58
        code        => $itemtypeA->itemtype,
59
        lang        => 'en',
59
        lang        => 'en',
60
        translation => 'Translated itemtype A'
60
        translation => 'Translated itemtype A'
61
    }
61
    }
62
)->store;
62
);
63
my $itemtypeB = $builder->build_object(
63
my $itemtypeB = $builder->build_object(
64
    {
64
    {
65
        class  => 'Koha::ItemTypes',
65
        class  => 'Koha::ItemTypes',
Lines 69-82 my $itemtypeB = $builder->build_object( Link Here
69
        }
69
        }
70
    }
70
    }
71
);
71
);
72
Koha::Localization->new(
72
$itemtypeB->_result->localizations->create(
73
    {
73
    {
74
        entity      => 'itemtypes',
74
        property    => 'description',
75
        code        => $itemtypeB->itemtype,
76
        lang        => 'en',
75
        lang        => 'en',
77
        translation => 'Translated itemtype B'
76
        translation => 'Translated itemtype B'
78
    }
77
    }
79
)->store;
78
);
80
my $itemtypeC = $builder->build_object(
79
my $itemtypeC = $builder->build_object(
81
    {
80
    {
82
        class => 'Koha::ItemTypes',
81
        class => 'Koha::ItemTypes',
Lines 87-92 my $itemtypeC = $builder->build_object( Link Here
87
    }
86
    }
88
);
87
);
89
88
89
Koha::Caches->get_instance('localization')->flush_all();
90
90
my $GetDescriptionA1 = $plugin->GetDescription($itemtypeA->itemtype);
91
my $GetDescriptionA1 = $plugin->GetDescription($itemtypeA->itemtype);
91
is($GetDescriptionA1, "Translated itemtype A", "ItemType without parent - GetDescription without want parent");
92
is($GetDescriptionA1, "Translated itemtype A", "ItemType without parent - GetDescription without want parent");
92
my $GetDescriptionA2 = $plugin->GetDescription($itemtypeA->itemtype, 1);
93
my $GetDescriptionA2 = $plugin->GetDescription($itemtypeA->itemtype, 1);
(-)a/t/db_dependent/api/v1/item_types.t (-17 / +11 lines)
Lines 25-30 use t::lib::Mocks; Link Here
25
25
26
use Mojo::JSON qw(encode_json);
26
use Mojo::JSON qw(encode_json);
27
27
28
use Koha::Caches;
28
use Koha::ItemTypes;
29
use Koha::ItemTypes;
29
use Koha::Database;
30
use Koha::Database;
30
31
Lines 66-94 subtest 'list() tests' => sub { Link Here
66
        }
67
        }
67
    );
68
    );
68
69
69
    my $en = $builder->build_object(
70
    $item_type->_result->localizations->create(
70
        {
71
        {
71
            class => 'Koha::Localizations',
72
            property    => 'description',
72
            value => {
73
            lang        => 'en',
73
                entity      => 'itemtypes',
74
            translation => 'English word "test"',
74
                code        => $item_type->id,
75
                lang        => 'en',
76
                translation => 'English word "test"',
77
            }
78
        }
75
        }
79
    );
76
    );
80
    my $sv = $builder->build_object(
77
    $item_type->_result->localizations->create(
81
        {
78
        {
82
            class => 'Koha::Localizations',
79
            property    => 'description',
83
            value => {
80
            lang        => 'sv_SE',
84
                entity      => 'itemtypes',
81
            translation => 'Swedish word "test"',
85
                code        => $item_type->id,
86
                lang        => 'sv_SE',
87
                translation => 'Swedish word "test"',
88
            }
89
        }
82
        }
90
    );
83
    );
91
84
85
    Koha::Caches->get_instance('localization')->flush_all();
86
92
    my $librarian = $builder->build_object(
87
    my $librarian = $builder->build_object(
93
        {
88
        {
94
            class => 'Koha::Patrons',
89
            class => 'Koha::Patrons',
95
- 

Return to bug 38136