globalize 5.0.1 → 5.2.0

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.
Files changed (34) hide show
  1. checksums.yaml +5 -5
  2. data/Appraisals +33 -0
  3. data/CHANGELOG.md +12 -0
  4. data/CONTRIBUTING.md +15 -0
  5. data/Gemfile +0 -9
  6. data/Gemfile.lock +59 -241
  7. data/{readme.md → README.md} +124 -36
  8. data/Rakefile +33 -0
  9. data/lib/globalize/active_record/act_macro.rb +27 -1
  10. data/lib/globalize/active_record/adapter.rb +13 -5
  11. data/lib/globalize/active_record/adapter_dirty.rb +56 -0
  12. data/lib/globalize/active_record/class_methods.rb +23 -9
  13. data/lib/globalize/active_record/exceptions.rb +1 -7
  14. data/lib/globalize/active_record/instance_methods.rb +89 -41
  15. data/lib/globalize/active_record/migration.rb +58 -31
  16. data/lib/globalize/active_record/translated_attributes_query.rb +181 -0
  17. data/lib/globalize/active_record.rb +10 -9
  18. data/lib/globalize/version.rb +1 -1
  19. data/lib/globalize.rb +20 -6
  20. data/lib/patches/active_record/persistence.rb +6 -15
  21. data/lib/patches/active_record/query_method.rb +2 -34
  22. data/lib/patches/active_record/rails4/query_method.rb +35 -0
  23. data/lib/patches/active_record/rails4/serialization.rb +22 -0
  24. data/lib/patches/active_record/rails4/uniqueness_validator.rb +42 -0
  25. data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
  26. data/lib/patches/active_record/rails5_1/serialization.rb +22 -0
  27. data/lib/patches/active_record/rails5_1/uniqueness_validator.rb +45 -0
  28. data/lib/patches/active_record/relation.rb +12 -0
  29. data/lib/patches/active_record/serialization.rb +5 -24
  30. data/lib/patches/active_record/uniqueness_validator.rb +7 -39
  31. data/lib/patches/active_record/xml_attribute_serializer.rb +19 -9
  32. metadata +85 -22
  33. data/globalize.gemspec +0 -29
  34. data/lib/globalize/active_record/query_methods.rb +0 -98
data/Rakefile CHANGED
@@ -20,3 +20,36 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
20
20
  rdoc.rdoc_files.include('README')
21
21
  rdoc.rdoc_files.include('lib/**/*.rb')
22
22
  end
23
+
24
+ task :load_path do
25
+ %w(lib test).each do |path|
26
+ $LOAD_PATH.unshift(File.expand_path("../#{path}", __FILE__))
27
+ end
28
+ end
29
+
30
+ namespace :db do
31
+ desc 'Create the database'
32
+ task :create => :load_path do
33
+ require 'support/database'
34
+
35
+ Globalize::Test::Database.create!
36
+ end
37
+
38
+ desc "Drop the database"
39
+ task :drop => :load_path do
40
+ require 'support/database'
41
+
42
+ Globalize::Test::Database.drop!
43
+ end
44
+
45
+ desc "Set up the database schema"
46
+ task :migrate => :load_path do
47
+ require 'support/database'
48
+
49
+ Globalize::Test::Database.migrate!
50
+ # ActiveRecord::Schema.migrate :up
51
+ end
52
+
53
+ desc "Drop and recreate the database schema"
54
+ task :reset => [:drop, :create]
55
+ end
@@ -5,6 +5,7 @@ module Globalize
5
5
  options = attr_names.extract_options!
6
6
  # Bypass setup_translates! if the initial bootstrapping is done already.
7
7
  setup_translates!(options) unless translates?
8
+ check_columns!(attr_names)
8
9
 
9
10
  # Add any extra translatable attributes.
10
11
  attr_names = attr_names.map(&:to_sym)
@@ -38,6 +39,30 @@ module Globalize
38
39
  # Add attribute to the list.
39
40
  self.translated_attribute_names << attr_name
40
41
  end
42
+
43
+ begin
44
+ if ::ActiveRecord::VERSION::STRING > "5.0" && table_exists? && translation_class.table_exists?
45
+ self.ignored_columns += translated_attribute_names.map(&:to_s)
46
+ reset_column_information
47
+ end
48
+ rescue ::ActiveRecord::NoDatabaseError
49
+ warn 'Unable to connect to a database. Globalize skipped ignoring columns of translated attributes.'
50
+ end
51
+ end
52
+
53
+ def check_columns!(attr_names)
54
+ # If tables do not exist or Rails version is greater than 5, do not warn about conflicting columns
55
+ return unless ::ActiveRecord::VERSION::STRING < "5.0" && table_exists? && translation_class.table_exists?
56
+ if (overlap = attr_names.map(&:to_s) & column_names).present?
57
+ ActiveSupport::Deprecation.warn(
58
+ ["You have defined one or more translated attributes with names that conflict with column(s) on the model table. ",
59
+ "Globalize does not support this configuration anymore, remove or rename column(s) on the model table.\n",
60
+ "Model name (table name): #{model_name} (#{table_name})\n",
61
+ "Attribute name(s): #{overlap.join(', ')}\n"].join
62
+ )
63
+ end
64
+ rescue ::ActiveRecord::NoDatabaseError
65
+ warn 'Unable to connect to a database. Globalize skipped checking attributes with conflicting column names.'
41
66
  end
42
67
 
43
68
  def apply_globalize_options(options)
@@ -74,7 +99,8 @@ module Globalize
74
99
  :foreign_key => options[:foreign_key],
75
100
  :dependent => :destroy,
76
101
  :extend => HasManyExtensions,
77
- :autosave => false
102
+ :autosave => false,
103
+ :inverse_of => :globalized_model
78
104
 
79
105
  after_create :save_translations!
80
106
  after_update :save_translations!
@@ -3,7 +3,7 @@ module Globalize
3
3
  class Adapter
4
4
  # The cache caches attributes that already were looked up for read access.
5
5
  # The stash keeps track of new or changed values that need to be saved.
6
- attr_accessor :record, :stash, :translations
6
+ attr_accessor :record, :stash
7
7
  private :record=, :stash=
8
8
 
9
9
  delegate :translation_class, :to => :'record.class'
@@ -34,14 +34,16 @@ module Globalize
34
34
  end
35
35
 
36
36
  def save_translations!
37
- stash.reject {|locale, attrs| attrs.empty?}.each do |locale, attrs|
37
+ stash.each do |locale, attrs|
38
+ next if attrs.empty?
39
+
38
40
  translation = record.translations_by_locale[locale] ||
39
41
  record.translations.build(locale: locale.to_s)
40
-
41
42
  attrs.each do |name, value|
42
43
  value = value.val if value.is_a?(Arel::Nodes::Casted)
43
44
  translation[name] = value
44
45
  end
46
+
45
47
  ensure_foreign_key_for(translation)
46
48
  translation.save!
47
49
  end
@@ -57,7 +59,8 @@ module Globalize
57
59
 
58
60
  # Sometimes the translation is initialised before a foreign key can be set.
59
61
  def ensure_foreign_key_for(translation)
60
- translation[translation.class.reflections["globalized_model"].foreign_key] = record.id
62
+ # AR >= 4.1 reflections renamed to _reflections
63
+ translation[translation.class.reflections.stringify_keys["globalized_model"].foreign_key] = record.id
61
64
  end
62
65
 
63
66
  def type_cast(name, value)
@@ -76,7 +79,11 @@ module Globalize
76
79
 
77
80
  def fetch_attribute(locale, name)
78
81
  translation = record.translation_for(locale, false)
79
- return translation && translation.send(name)
82
+ if translation
83
+ translation.send(name)
84
+ else
85
+ record.class.translation_class.new.send(name)
86
+ end
80
87
  end
81
88
 
82
89
  def set_metadata(object, metadata)
@@ -95,6 +102,7 @@ module Globalize
95
102
  end
96
103
 
97
104
  delegate :fallbacks_for_empty_translations?, :to => :record, :prefix => false
105
+ prepend AdapterDirty
98
106
  end
99
107
  end
100
108
  end
@@ -0,0 +1,56 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module AdapterDirty
4
+ def write locale, name, value
5
+ # Dirty tracking, paraphrased from
6
+ # ActiveRecord::AttributeMethods::Dirty#write_attribute.
7
+ name = name.to_s
8
+ store_old_value name, locale
9
+ old_values = dirty[name]
10
+ old_value = old_values[locale]
11
+ is_changed = record.send :attribute_changed?, name
12
+ if is_changed && value == old_value
13
+ # If there's already a change, delete it if this undoes the change.
14
+ old_values.delete locale
15
+ if old_values.empty?
16
+ _reset_attribute name
17
+ end
18
+ elsif !is_changed
19
+ # If there's not a change yet, record it.
20
+ record.send(:attribute_will_change!, name) if old_value != value
21
+ end
22
+
23
+ super locale, name, value
24
+ end
25
+
26
+ attr_writer :dirty
27
+ def dirty
28
+ @dirty ||= {}
29
+ end
30
+
31
+ def store_old_value name, locale
32
+ dirty[name] ||= {}
33
+ unless dirty[name].key? locale
34
+ old = fetch(locale, name)
35
+ old = old.dup if old.duplicable?
36
+ dirty[name][locale] = old
37
+ end
38
+ end
39
+
40
+ def clear_dirty
41
+ self.dirty = {}
42
+ end
43
+
44
+ def _reset_attribute name
45
+ record.send("#{name}=", record.changed_attributes[name])
46
+ record.send(:clear_attribute_changes, [name])
47
+ end
48
+
49
+ def reset
50
+ clear_dirty
51
+ super
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -3,13 +3,21 @@ module Globalize
3
3
  module ClassMethods
4
4
  delegate :translated_locales, :set_translations_table_name, :to => :translation_class
5
5
 
6
+ if ::ActiveRecord::VERSION::STRING < "5.0.0"
7
+ def columns_hash
8
+ super.except(*translated_attribute_names.map(&:to_s))
9
+ end
10
+ end
11
+
6
12
  def with_locales(*locales)
7
13
  all.merge translation_class.with_locales(*locales)
8
14
  end
9
15
 
10
16
  def with_translations(*locales)
11
17
  locales = translated_locales if locales.empty?
12
- preload(:translations).joins(:translations).readonly(false).with_locales(locales)
18
+ preload(:translations).joins(:translations).readonly(false).with_locales(locales).tap do |query|
19
+ query.distinct! unless locales.flatten.one?
20
+ end
13
21
  end
14
22
 
15
23
  def with_required_attributes
@@ -42,12 +50,17 @@ module Globalize
42
50
 
43
51
  def translation_class
44
52
  @translation_class ||= begin
45
- klass = self.const_get(:Translation) rescue nil
46
- if klass.nil? || klass.class_name != (self.class_name + "Translation")
53
+ if self.const_defined?(:Translation, false)
54
+ klass = self.const_get(:Translation, false)
55
+ else
47
56
  klass = self.const_set(:Translation, Class.new(Globalize::ActiveRecord::Translation))
48
57
  end
49
58
 
50
- klass.belongs_to :globalized_model, :class_name => self.name, :foreign_key => translation_options[:foreign_key]
59
+ klass.belongs_to :globalized_model,
60
+ class_name: self.name,
61
+ foreign_key: translation_options[:foreign_key],
62
+ inverse_of: :translations,
63
+ touch: translation_options.fetch(:touch, false)
51
64
  klass
52
65
  end
53
66
  end
@@ -63,10 +76,10 @@ module Globalize
63
76
  private
64
77
 
65
78
  # Override the default relation method in order to return a subclass
66
- # of ActiveRecord::Relation with custom finder methods for translated
67
- # attributes.
79
+ # of ActiveRecord::Relation with custom finder and calculation methods
80
+ # for translated attributes.
68
81
  def relation
69
- super.extending!(QueryMethods)
82
+ super.extending!(TranslatedAttributesQuery)
70
83
  end
71
84
 
72
85
  protected
@@ -85,6 +98,7 @@ module Globalize
85
98
  end
86
99
 
87
100
  def define_translated_attr_accessor(name)
101
+ attribute(name, ::ActiveRecord::Type::Value.new)
88
102
  define_translated_attr_reader(name)
89
103
  define_translated_attr_writer(name)
90
104
  end
@@ -100,8 +114,8 @@ module Globalize
100
114
 
101
115
  def define_translations_writer(name)
102
116
  define_method(:"#{name}_translations=") do |value|
103
- value.each do |(locale, value)|
104
- write_attribute name, value, :locale => locale
117
+ value.each do |(locale, _value)|
118
+ write_attribute name, _value, :locale => locale
105
119
  end
106
120
  end
107
121
  end
@@ -8,12 +8,6 @@ module Globalize
8
8
  super("Missing translated field #{field.inspect}")
9
9
  end
10
10
  end
11
-
12
- class BadFieldType < MigrationError
13
- def initialize(name, type)
14
- super("Bad field type for field #{name.inspect} (#{type.inspect}), should be :string or :text")
15
- end
16
- end
17
11
  end
18
12
  end
19
- end
13
+ end
@@ -11,51 +11,65 @@ module Globalize
11
11
  super.merge(translated_attributes)
12
12
  end
13
13
 
14
- def attributes=(attributes, *args)
15
- with_given_locale(attributes) { super }
14
+ def attributes=(new_attributes, *options)
15
+ super unless new_attributes.respond_to?(:stringify_keys) && new_attributes.present?
16
+ attributes = new_attributes.stringify_keys
17
+ with_given_locale(attributes) { super(attributes.except("locale"), *options) }
16
18
  end
17
19
 
18
- def assign_attributes(attributes, *args)
19
- with_given_locale(attributes) { super }
20
- end
20
+ if Globalize.rails_52?
21
21
 
22
- def write_attribute(name, value, options = {})
23
- return super(name, value) unless translated?(name)
22
+ # In Rails 5.2 we need to override *_assign_attributes* as it's called earlier
23
+ # in the stack (before *assign_attributes*)
24
+ # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_assignment.rb#L11
25
+ def _assign_attributes(new_attributes)
26
+ attributes = new_attributes.stringify_keys
27
+ with_given_locale(attributes) { super(attributes.except("locale")) }
28
+ end
24
29
 
25
- options = {:locale => Globalize.locale}.merge(options)
30
+ else
26
31
 
27
- # Dirty tracking, paraphrased from
28
- # ActiveRecord::AttributeMethods::Dirty#write_attribute.
29
- name_str = name.to_s
30
- if attribute_changed?(name_str)
31
- # If there's already a change, delete it if this undoes the change.
32
- old = changed_attributes[name_str]
33
- @changed_attributes.delete(name_str) if value == old
34
- else
35
- # If there's not a change yet, record it.
36
- old = globalize.fetch(options[:locale], name)
37
- old = old.dup if old.duplicable?
38
- @changed_attributes[name_str] = old if value != old
32
+ def assign_attributes(new_attributes, *options)
33
+ super unless new_attributes.respond_to?(:stringify_keys) && new_attributes.present?
34
+ attributes = new_attributes.stringify_keys
35
+ with_given_locale(attributes) { super(attributes.except("locale"), *options) }
39
36
  end
40
37
 
41
- globalize.write(options[:locale], name, value)
42
38
  end
43
39
 
44
- def read_attribute(name, options = {})
45
- options = {:translated => true, :locale => nil}.merge(options)
46
- return super(name) unless options[:translated]
40
+ def write_attribute(name, value, *args, &block)
41
+ return super(name, value, *args, &block) unless translated?(name)
47
42
 
48
- if translated?(name)
49
- if (value = globalize.fetch(options[:locale] || Globalize.locale, name))
50
- value
51
- else
52
- super(name)
53
- end
43
+ options = {:locale => Globalize.locale}.merge(args.first || {})
44
+
45
+ globalize.write(options[:locale], name, value)
46
+ end
47
+
48
+ def [](attr_name)
49
+ if translated?(attr_name)
50
+ read_attribute(attr_name)
54
51
  else
55
- super(name)
52
+ read_attribute(attr_name) { |n| missing_attribute(n, caller) }
56
53
  end
57
54
  end
58
55
 
56
+ def read_attribute(attr_name, options = {}, &block)
57
+ name = if self.class.attribute_alias?(attr_name)
58
+ self.class.attribute_alias(attr_name).to_s
59
+ else
60
+ attr_name.to_s
61
+ end
62
+
63
+ name = self.class.primary_key if name == "id".freeze && self.class.primary_key
64
+
65
+ _read_attribute(name, options, &block)
66
+ end
67
+
68
+ def _read_attribute(attr_name, options = {}, &block)
69
+ translated_value = read_translated_attribute(attr_name, options, &block)
70
+ translated_value.nil? ? super(attr_name, &block) : translated_value
71
+ end
72
+
59
73
  def attribute_names
60
74
  translated_attribute_names.map(&:to_s) + super
61
75
  end
@@ -83,8 +97,9 @@ module Globalize
83
97
 
84
98
  options[locale].each do |key, value|
85
99
  translation.send :"#{key}=", value
100
+ translation.globalized_model.send :"#{key}=", value
86
101
  end
87
- translation.save
102
+ translation.save if persisted?
88
103
  end
89
104
  globalize.reset
90
105
  end
@@ -143,14 +158,17 @@ module Globalize
143
158
  Globalize.fallbacks(locale)
144
159
  end
145
160
 
146
- def rollback
147
- translation_caches[::Globalize.locale] = translation.previous_version
148
- end
149
-
150
161
  def save(*)
151
- Globalize.with_locale(translation.locale || I18n.default_locale) do
152
- super
162
+ result = Globalize.with_locale(translation.locale || I18n.default_locale) do
163
+ without_fallbacks do
164
+ super
165
+ end
166
+ end
167
+ if result
168
+ globalize.clear_dirty
153
169
  end
170
+
171
+ result
154
172
  end
155
173
 
156
174
  def column_for_attribute name
@@ -163,6 +181,16 @@ module Globalize
163
181
  [super, translation.cache_key].join("/")
164
182
  end
165
183
 
184
+ def changed?
185
+ changed_attributes.present? || translations.any?(&:changed?)
186
+ end
187
+
188
+ # need to access instance variable directly since changed_attributes
189
+ # is frozen as of Rails 4.2
190
+ def original_changed_attributes
191
+ @changed_attributes
192
+ end
193
+
166
194
  protected
167
195
 
168
196
  def each_locale_and_translated_attribute
@@ -184,15 +212,35 @@ module Globalize
184
212
  translation_caches.clear
185
213
  end
186
214
 
187
- def with_given_locale(attributes, &block)
188
- attributes.symbolize_keys! if attributes.respond_to?(:symbolize_keys!)
215
+ def with_given_locale(_attributes, &block)
216
+ attributes = _attributes.stringify_keys
189
217
 
190
- if locale = attributes.try(:delete, :locale)
218
+ if locale = attributes.try(:delete, "locale")
191
219
  Globalize.with_locale(locale, &block)
192
220
  else
193
221
  yield
194
222
  end
195
223
  end
224
+
225
+ def without_fallbacks
226
+ before = self.fallbacks_for_empty_translations
227
+ self.fallbacks_for_empty_translations = false
228
+ yield
229
+ ensure
230
+ self.fallbacks_for_empty_translations = before
231
+ end
232
+
233
+ # nil or value
234
+ def read_translated_attribute(name, options)
235
+ options = {:translated => true, :locale => nil}.merge(options)
236
+ return nil unless options[:translated]
237
+ return nil unless translated?(name)
238
+
239
+ value = globalize.fetch(options[:locale] || Globalize.locale, name)
240
+ return nil if value.nil?
241
+
242
+ block_given? ? yield(value) : value
243
+ end
196
244
  end
197
245
  end
198
246
  end