globalize 5.0.1 → 5.1.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/Gemfile +19 -5
- data/Gemfile.lock +97 -47
- data/{readme.md → README.md} +116 -35
- data/Rakefile +33 -0
- data/lib/globalize.rb +11 -5
- data/lib/globalize/active_record.rb +1 -0
- data/lib/globalize/active_record/act_macro.rb +24 -3
- data/lib/globalize/active_record/adapter.rb +10 -11
- data/lib/globalize/active_record/adapter_dirty.rb +54 -0
- data/lib/globalize/active_record/class_methods.rb +15 -6
- data/lib/globalize/active_record/exceptions.rb +1 -7
- data/lib/globalize/active_record/instance_methods.rb +55 -35
- data/lib/globalize/active_record/migration.rb +51 -29
- data/lib/globalize/active_record/query_methods.rb +42 -17
- data/lib/globalize/version.rb +1 -1
- data/lib/patches/active_record/persistence.rb +6 -15
- data/lib/patches/active_record/query_method.rb +2 -34
- data/lib/patches/active_record/rails4/query_method.rb +35 -0
- data/lib/patches/active_record/rails4/uniqueness_validator.rb +42 -0
- data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
- data/lib/patches/active_record/relation.rb +12 -0
- data/lib/patches/active_record/serialization.rb +13 -16
- data/lib/patches/active_record/uniqueness_validator.rb +5 -39
- data/lib/patches/active_record/xml_attribute_serializer.rb +19 -9
- metadata +27 -24
- data/globalize.gemspec +0 -29
@@ -3,20 +3,18 @@ require 'digest/sha1'
|
|
3
3
|
module Globalize
|
4
4
|
module ActiveRecord
|
5
5
|
module Migration
|
6
|
-
attr_reader :globalize_migrator
|
7
|
-
|
8
6
|
def globalize_migrator
|
9
7
|
@globalize_migrator ||= Migrator.new(self)
|
10
8
|
end
|
11
9
|
|
12
|
-
delegate :create_translation_table!, :add_translation_fields!,
|
13
|
-
:
|
14
|
-
:to => :globalize_migrator
|
10
|
+
delegate :create_translation_table!, :add_translation_fields!,
|
11
|
+
:drop_translation_table!, :translation_index_name,
|
12
|
+
:translation_locale_index_name, :to => :globalize_migrator
|
15
13
|
|
16
14
|
class Migrator
|
17
15
|
include Globalize::ActiveRecord::Exceptions
|
18
16
|
|
19
|
-
attr_reader :model
|
17
|
+
attr_reader :model
|
20
18
|
delegate :translated_attribute_names, :connection, :table_name,
|
21
19
|
:table_name_prefix, :translations_table_name, :columns, :to => :model
|
22
20
|
|
@@ -24,7 +22,15 @@ module Globalize
|
|
24
22
|
@model = model
|
25
23
|
end
|
26
24
|
|
25
|
+
def fields
|
26
|
+
@fields ||= complete_translated_fields
|
27
|
+
end
|
28
|
+
|
27
29
|
def create_translation_table!(fields = {}, options = {})
|
30
|
+
extra = options.keys - [:migrate_data, :remove_source_columns, :unique_index]
|
31
|
+
if extra.any?
|
32
|
+
raise ArgumentError, "Unknown migration #{'option'.pluralize(extra.size)}: #{extra}"
|
33
|
+
end
|
28
34
|
@fields = fields
|
29
35
|
# If we have fields we only want to create the translation table with those fields
|
30
36
|
complete_translated_fields if fields.blank?
|
@@ -32,14 +38,13 @@ module Globalize
|
|
32
38
|
|
33
39
|
create_translation_table
|
34
40
|
add_translation_fields!(fields, options)
|
35
|
-
create_translations_index
|
41
|
+
create_translations_index(options)
|
36
42
|
clear_schema_cache!
|
37
43
|
end
|
38
44
|
|
39
45
|
def add_translation_fields!(fields, options = {})
|
40
46
|
@fields = fields
|
41
47
|
validate_translated_fields
|
42
|
-
|
43
48
|
add_translation_fields
|
44
49
|
clear_schema_cache!
|
45
50
|
move_data_to_translation_table if options[:migrate_data]
|
@@ -62,13 +67,13 @@ module Globalize
|
|
62
67
|
# It's a problem because in early migrations would add all the translated attributes
|
63
68
|
def complete_translated_fields
|
64
69
|
translated_attribute_names.each do |name|
|
65
|
-
fields[name] ||= column_type(name)
|
70
|
+
@fields[name] ||= column_type(name)
|
66
71
|
end
|
67
72
|
end
|
68
73
|
|
69
74
|
def create_translation_table
|
70
75
|
connection.create_table(translations_table_name) do |t|
|
71
|
-
t.references table_name.sub(/^#{table_name_prefix}/, '').singularize, :null => false
|
76
|
+
t.references table_name.sub(/^#{table_name_prefix}/, '').singularize, :null => false, :index => false, :type => column_type(model.primary_key).to_sym
|
72
77
|
t.string :locale, :null => false
|
73
78
|
t.timestamps :null => false
|
74
79
|
end
|
@@ -86,10 +91,11 @@ module Globalize
|
|
86
91
|
end
|
87
92
|
end
|
88
93
|
|
89
|
-
def create_translations_index
|
94
|
+
def create_translations_index(options)
|
95
|
+
foreign_key = "#{table_name.sub(/^#{table_name_prefix}/, "").singularize}_id".to_sym
|
90
96
|
connection.add_index(
|
91
97
|
translations_table_name,
|
92
|
-
|
98
|
+
foreign_key,
|
93
99
|
:name => translation_index_name
|
94
100
|
)
|
95
101
|
# index for select('DISTINCT locale') call in translation.rb
|
@@ -98,6 +104,15 @@ module Globalize
|
|
98
104
|
:locale,
|
99
105
|
:name => translation_locale_index_name
|
100
106
|
)
|
107
|
+
|
108
|
+
if options[:unique_index]
|
109
|
+
connection.add_index(
|
110
|
+
translations_table_name,
|
111
|
+
[foreign_key, :locale],
|
112
|
+
:name => translation_unique_index_name,
|
113
|
+
unique: true
|
114
|
+
)
|
115
|
+
end
|
101
116
|
end
|
102
117
|
|
103
118
|
def drop_translation_table
|
@@ -105,12 +120,17 @@ module Globalize
|
|
105
120
|
end
|
106
121
|
|
107
122
|
def drop_translations_index
|
108
|
-
connection.
|
123
|
+
if connection.indexes(translations_table_name).map(&:name).include?(translation_index_name)
|
124
|
+
connection.remove_index(translations_table_name, :name => translation_index_name)
|
125
|
+
end
|
126
|
+
if connection.indexes(translations_table_name).map(&:name).include?(translation_locale_index_name)
|
127
|
+
connection.remove_index(translations_table_name, :name => translation_locale_index_name)
|
128
|
+
end
|
109
129
|
end
|
110
130
|
|
111
131
|
def move_data_to_translation_table
|
112
132
|
model.find_each do |record|
|
113
|
-
translation = record.translation_for(I18n.
|
133
|
+
translation = record.translation_for(I18n.locale) || record.translations.build(:locale => I18n.locale)
|
114
134
|
fields.each do |attribute_name, attribute_type|
|
115
135
|
translation[attribute_name] = record.read_attribute(attribute_name, {:translated => false})
|
116
136
|
end
|
@@ -122,7 +142,7 @@ module Globalize
|
|
122
142
|
add_missing_columns
|
123
143
|
|
124
144
|
# Find all of the translated attributes for all records in the model.
|
125
|
-
all_translated_attributes =
|
145
|
+
all_translated_attributes = model.all.collect{|m| m.attributes}
|
126
146
|
all_translated_attributes.each do |translated_record|
|
127
147
|
# Create a hash containing the translated column names and their values.
|
128
148
|
translated_attribute_names.inject(fields_to_update={}) do |f, name|
|
@@ -130,15 +150,13 @@ module Globalize
|
|
130
150
|
end
|
131
151
|
|
132
152
|
# Now, update the actual model's record with the hash.
|
133
|
-
|
153
|
+
model.where(model.primary_key.to_sym => translated_record[model.primary_key]).update_all(fields_to_update)
|
134
154
|
end
|
135
155
|
end
|
136
156
|
|
137
157
|
def validate_translated_fields
|
138
158
|
fields.each do |name, options|
|
139
159
|
raise BadFieldName.new(name) unless valid_field_name?(name)
|
140
|
-
type = (options.is_a? Hash) ? options[:type] : options
|
141
|
-
raise BadFieldType.new(name, type) unless valid_field_type?(name, type)
|
142
160
|
end
|
143
161
|
end
|
144
162
|
|
@@ -150,20 +168,16 @@ module Globalize
|
|
150
168
|
translated_attribute_names.include?(name)
|
151
169
|
end
|
152
170
|
|
153
|
-
def valid_field_type?(name, type)
|
154
|
-
!translated_attribute_names.include?(name) || [:string, :text].include?(type)
|
155
|
-
end
|
156
|
-
|
157
171
|
def translation_index_name
|
158
|
-
|
159
|
-
index_name.size < connection.index_name_length ? index_name :
|
160
|
-
"index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length]
|
172
|
+
truncate_index_name "index_#{translations_table_name}_on_#{table_name.singularize}_id"
|
161
173
|
end
|
162
174
|
|
163
175
|
def translation_locale_index_name
|
164
|
-
|
165
|
-
|
166
|
-
|
176
|
+
truncate_index_name "index_#{translations_table_name}_on_locale"
|
177
|
+
end
|
178
|
+
|
179
|
+
def translation_unique_index_name
|
180
|
+
truncate_index_name "index_#{translations_table_name}_on_#{table_name.singularize}_id_and_locale"
|
167
181
|
end
|
168
182
|
|
169
183
|
def clear_schema_cache!
|
@@ -174,14 +188,22 @@ module Globalize
|
|
174
188
|
|
175
189
|
private
|
176
190
|
|
191
|
+
def truncate_index_name(index_name)
|
192
|
+
if index_name.size < connection.index_name_length
|
193
|
+
index_name
|
194
|
+
else
|
195
|
+
"index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length]
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
177
199
|
def add_missing_columns
|
200
|
+
clear_schema_cache!
|
178
201
|
translated_attribute_names.map(&:to_s).each do |attribute|
|
179
202
|
unless model.column_names.include?(attribute)
|
180
203
|
connection.add_column(table_name, attribute, model::Translation.columns_hash[attribute].type)
|
181
204
|
end
|
182
205
|
end
|
183
206
|
end
|
184
|
-
|
185
207
|
end
|
186
208
|
end
|
187
209
|
end
|
@@ -1,10 +1,9 @@
|
|
1
1
|
module Globalize
|
2
2
|
module ActiveRecord
|
3
3
|
module QueryMethods
|
4
|
-
|
5
4
|
class WhereChain < ::ActiveRecord::QueryMethods::WhereChain
|
6
5
|
def not(opts, *rest)
|
7
|
-
if parsed = @scope.parse_translated_conditions(opts)
|
6
|
+
if parsed = @scope.clone.parse_translated_conditions(opts)
|
8
7
|
@scope.join_translations.where.not(parsed, *rest)
|
9
8
|
else
|
10
9
|
super
|
@@ -24,7 +23,7 @@ module Globalize
|
|
24
23
|
|
25
24
|
def order(opts, *rest)
|
26
25
|
if respond_to?(:translated_attribute_names) && parsed = parse_translated_order(opts)
|
27
|
-
super(parsed)
|
26
|
+
join_translations super(parsed)
|
28
27
|
else
|
29
28
|
super
|
30
29
|
end
|
@@ -50,19 +49,21 @@ module Globalize
|
|
50
49
|
end
|
51
50
|
end
|
52
51
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
52
|
+
if ::ActiveRecord::VERSION::STRING < "5.0.0"
|
53
|
+
def where_values_hash(*args)
|
54
|
+
return super unless respond_to?(:translations_table_name)
|
55
|
+
equalities = respond_to?(:with_default_scope) ? with_default_scope.where_values : where_values
|
56
|
+
equalities = equalities.grep(Arel::Nodes::Equality).find_all { |node|
|
57
|
+
node.left.relation.name == translations_table_name
|
58
|
+
}
|
59
59
|
|
60
|
-
|
60
|
+
binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
62
|
+
super.merge(Hash[equalities.map { |where|
|
63
|
+
name = where.left.name
|
64
|
+
[name, binds.fetch(name.to_s) { right = where.right; right.is_a?(Arel::Nodes::Casted) ? right.val : right }]
|
65
|
+
}])
|
66
|
+
end
|
66
67
|
end
|
67
68
|
|
68
69
|
def join_translations(relation = self)
|
@@ -75,16 +76,40 @@ module Globalize
|
|
75
76
|
|
76
77
|
private
|
77
78
|
|
79
|
+
def arel_translated_order_node(column, direction)
|
80
|
+
unless translated_column?(column)
|
81
|
+
return self.arel_table[column].send(direction)
|
82
|
+
end
|
83
|
+
|
84
|
+
full_column = translated_column_name(column)
|
85
|
+
|
86
|
+
# Inject `full_column` to the select values to avoid
|
87
|
+
# PG::InvalidColumnReference errors with distinct queries on Postgres
|
88
|
+
if select_values.empty?
|
89
|
+
self.select_values = [Arel.star, full_column]
|
90
|
+
else
|
91
|
+
self.select_values << full_column
|
92
|
+
end
|
93
|
+
|
94
|
+
translation_class.arel_table[column].send(direction)
|
95
|
+
end
|
96
|
+
|
78
97
|
def parse_translated_order(opts)
|
79
98
|
case opts
|
80
99
|
when Hash
|
100
|
+
# Do not process nothing unless there is at least a translated column
|
101
|
+
# so that the `order` statement will be processed by the original
|
102
|
+
# ActiveRecord method
|
103
|
+
return nil unless opts.find { |col, dir| translated_column?(col) }
|
104
|
+
|
105
|
+
# Build order arel nodes for translateds and untranslateds statements
|
81
106
|
ordering = opts.map do |column, direction|
|
82
|
-
|
83
|
-
klass.arel_table[column].send(direction)
|
107
|
+
arel_translated_order_node(column, direction)
|
84
108
|
end
|
109
|
+
|
85
110
|
order(ordering).order_values
|
86
111
|
when Symbol
|
87
|
-
|
112
|
+
parse_translated_order({ opts => :asc })
|
88
113
|
else # failsafe returns nothing
|
89
114
|
nil
|
90
115
|
end
|
data/lib/globalize/version.rb
CHANGED
@@ -1,26 +1,17 @@
|
|
1
|
-
module
|
1
|
+
module Globalize
|
2
2
|
module Persistence
|
3
3
|
# Updates the associated record with values matching those of the instance attributes.
|
4
4
|
# Returns the number of affected rows.
|
5
5
|
def _update_record(attribute_names = self.attribute_names)
|
6
6
|
attribute_names_without_translated = attribute_names.select{ |k| not respond_to?('translated?') or not translated?(k) }
|
7
|
-
|
8
|
-
if attributes_values.empty?
|
9
|
-
0
|
10
|
-
else
|
11
|
-
self.class.unscoped._update_record attributes_values, id, id_was
|
12
|
-
end
|
7
|
+
super(attribute_names_without_translated)
|
13
8
|
end
|
14
9
|
|
15
10
|
def _create_record(attribute_names = self.attribute_names)
|
16
11
|
attribute_names_without_translated = attribute_names.select{ |k| not respond_to?('translated?') or not translated?(k) }
|
17
|
-
|
18
|
-
|
19
|
-
new_id = self.class.unscoped.insert attributes_values
|
20
|
-
self.id ||= new_id if self.class.primary_key
|
21
|
-
|
22
|
-
@new_record = false
|
23
|
-
id
|
12
|
+
super(attribute_names_without_translated)
|
24
13
|
end
|
25
14
|
end
|
26
|
-
end
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveRecord::Persistence.send(:prepend, Globalize::Persistence)
|
@@ -1,35 +1,3 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
module ActiveRecord
|
4
|
-
module AttributeMethods
|
5
|
-
module Query
|
6
|
-
def query_attribute(attr_name)
|
7
|
-
unless value = read_attribute(attr_name)
|
8
|
-
false
|
9
|
-
else
|
10
|
-
column = self.class.columns_hash[attr_name]
|
11
|
-
if column.nil?
|
12
|
-
|
13
|
-
# TODO submit a rails patch
|
14
|
-
|
15
|
-
# not sure what active_record tests say but i guess this should mean:
|
16
|
-
# call to_i and check zero? if the value is a Numeric or starts with
|
17
|
-
# a digit, so it can meaningfully be typecasted by to_i
|
18
|
-
|
19
|
-
# if Numeric === value || value !~ /[^0-9]/
|
20
|
-
if Numeric === value || value.to_s =~ /^[0-9]/
|
21
|
-
!value.to_i.zero?
|
22
|
-
else
|
23
|
-
return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
|
24
|
-
!value.blank?
|
25
|
-
end
|
26
|
-
elsif column.number?
|
27
|
-
!value.zero?
|
28
|
-
else
|
29
|
-
!value.blank?
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
1
|
+
if ::ActiveRecord::VERSION::STRING < "5.0.0"
|
2
|
+
require_relative 'rails4/query_method'
|
35
3
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'active_record/attribute_methods/query'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module AttributeMethods
|
5
|
+
module Query
|
6
|
+
def query_attribute(attr_name)
|
7
|
+
unless value = read_attribute(attr_name)
|
8
|
+
false
|
9
|
+
else
|
10
|
+
column = self.class.columns_hash[attr_name]
|
11
|
+
if column.nil?
|
12
|
+
|
13
|
+
# TODO submit a rails patch
|
14
|
+
|
15
|
+
# not sure what active_record tests say but i guess this should mean:
|
16
|
+
# call to_i and check zero? if the value is a Numeric or starts with
|
17
|
+
# a digit, so it can meaningfully be typecasted by to_i
|
18
|
+
|
19
|
+
# if Numeric === value || value !~ /[^0-9]/
|
20
|
+
if Numeric === value || value.to_s =~ /^[0-9]/
|
21
|
+
!value.to_i.zero?
|
22
|
+
else
|
23
|
+
return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
|
24
|
+
!value.blank?
|
25
|
+
end
|
26
|
+
elsif column.number?
|
27
|
+
!value.zero?
|
28
|
+
else
|
29
|
+
!value.blank?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'active_record/validations/uniqueness.rb'
|
2
|
+
|
3
|
+
module Globalize
|
4
|
+
module UniquenessValidatorOverride
|
5
|
+
def validate_each(record, attribute, value)
|
6
|
+
klass = record.class
|
7
|
+
if klass.translates? && klass.translated?(attribute)
|
8
|
+
finder_class = klass.translation_class
|
9
|
+
table = finder_class.arel_table
|
10
|
+
|
11
|
+
relation = build_relation(finder_class, table, attribute, value).and(table[:locale].eq(Globalize.locale))
|
12
|
+
relation = relation.and(table[klass.reflect_on_association(:translations).foreign_key].not_eq(record.send(:id))) if record.persisted?
|
13
|
+
|
14
|
+
translated_scopes = Array(options[:scope]) & klass.translated_attribute_names
|
15
|
+
untranslated_scopes = Array(options[:scope]) - translated_scopes
|
16
|
+
|
17
|
+
untranslated_scopes.each do |scope_item|
|
18
|
+
scope_value = record.send(scope_item)
|
19
|
+
reflection = klass.reflect_on_association(scope_item)
|
20
|
+
if reflection
|
21
|
+
scope_value = record.send(reflection.foreign_key)
|
22
|
+
scope_item = reflection.foreign_key
|
23
|
+
end
|
24
|
+
relation = relation.and(find_finder_class_for(record).arel_table[scope_item].eq(scope_value))
|
25
|
+
end
|
26
|
+
|
27
|
+
translated_scopes.each do |scope_item|
|
28
|
+
scope_value = record.send(scope_item)
|
29
|
+
relation = relation.and(table[scope_item].eq(scope_value))
|
30
|
+
end
|
31
|
+
|
32
|
+
if klass.unscoped.with_translations.where(relation).exists?
|
33
|
+
record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
|
34
|
+
end
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
ActiveRecord::Validations::UniquenessValidator.send :prepend, Globalize::UniquenessValidatorOverride
|