globalize 5.0.1 → 5.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|