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.
@@ -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