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.
@@ -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!, :drop_translation_table!,
13
- :translation_index_name, :translation_locale_index_name,
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, :fields
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
- "#{table_name.sub(/^#{table_name_prefix}/, "").singularize}_id",
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.remove_index(translations_table_name, :name => translation_index_name)
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.default_locale) || record.translations.build(:locale => I18n.default_locale)
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 = @model.all.collect{|m| m.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
- @model.where(:id => translated_record['id']).update_all(fields_to_update)
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
- index_name = "index_#{translations_table_name}_on_#{table_name.singularize}_id"
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
- index_name = "index_#{translations_table_name}_on_locale"
165
- index_name.size < connection.index_name_length ? index_name :
166
- "index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length]
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
- 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
- }
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
- binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
60
+ binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
61
61
 
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
- }])
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
- klass = translated_column?(column) ? translation_class : self
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
- translated_column_name(opts) if translated_attribute_names.include?(opts)
112
+ parse_translated_order({ opts => :asc })
88
113
  else # failsafe returns nothing
89
114
  nil
90
115
  end
@@ -1,3 +1,3 @@
1
1
  module Globalize
2
- Version = '5.0.1'
2
+ Version = '5.1.0.beta1'
3
3
  end
@@ -1,26 +1,17 @@
1
- module ActiveRecord
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
- attributes_values = arel_attributes_with_values_for_update(attribute_names_without_translated)
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
- attributes_values = arel_attributes_with_values_for_create(attribute_names_without_translated)
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
- 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
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