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.
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
@@ -1,11 +1,12 @@
1
+ require 'request_store'
1
2
  require 'active_record'
2
3
  require 'patches/active_record/xml_attribute_serializer'
3
4
  require 'patches/active_record/query_method'
5
+ require 'patches/active_record/relation'
4
6
  require 'patches/active_record/serialization'
5
7
  require 'patches/active_record/uniqueness_validator'
6
8
  require 'patches/active_record/persistence'
7
9
 
8
-
9
10
  module Globalize
10
11
  autoload :ActiveRecord, 'globalize/active_record'
11
12
  autoload :Interpolation, 'globalize/interpolation'
@@ -52,18 +53,23 @@ module Globalize
52
53
  i18n_fallbacks? ? I18n.fallbacks[for_locale] : [for_locale.to_sym]
53
54
  end
54
55
 
56
+ # Thread-safe global storage
57
+ def storage
58
+ RequestStore.store
59
+ end
60
+
55
61
  protected
56
62
 
57
63
  def read_locale
58
- @globalize_locale
64
+ storage[:globalize_locale]
59
65
  end
60
66
 
61
67
  def set_locale(locale)
62
- @globalize_locale = locale.try(:to_sym)
68
+ storage[:globalize_locale] = locale.try(:to_sym)
63
69
  end
64
70
 
65
71
  def read_fallbacks
66
- @fallbacks || HashWithIndifferentAccess.new
72
+ storage[:globalize_fallbacks] || HashWithIndifferentAccess.new
67
73
  end
68
74
 
69
75
  def set_fallbacks(locales)
@@ -73,7 +79,7 @@ module Globalize
73
79
  fallback_hash[key] = value.presence || [key]
74
80
  end if locales.present?
75
81
 
76
- @fallbacks = fallback_hash
82
+ storage[:globalize_fallbacks] = fallback_hash
77
83
  end
78
84
  end
79
85
  end
@@ -2,6 +2,7 @@ module Globalize
2
2
  module ActiveRecord
3
3
  autoload :ActMacro, 'globalize/active_record/act_macro'
4
4
  autoload :Adapter, 'globalize/active_record/adapter'
5
+ autoload :AdapterDirty, 'globalize/active_record/adapter_dirty'
5
6
  autoload :Attributes, 'globalize/active_record/attributes'
6
7
  autoload :ClassMethods, 'globalize/active_record/class_methods'
7
8
  autoload :Exceptions, 'globalize/active_record/exceptions'
@@ -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,25 @@ module Globalize
38
39
  # Add attribute to the list.
39
40
  self.translated_attribute_names << attr_name
40
41
  end
42
+
43
+ if ::ActiveRecord::VERSION::STRING > "5.0" && table_exists? && translation_class.table_exists?
44
+ self.ignored_columns += translated_attribute_names.map(&:to_s)
45
+ reset_column_information
46
+ end
47
+ end
48
+
49
+ def check_columns!(attr_names)
50
+ # If tables do not exist, do not warn about conflicting columns
51
+ return unless table_exists? && translation_class.table_exists?
52
+
53
+ if (overlap = attr_names.map(&:to_s) & column_names).present?
54
+ ActiveSupport::Deprecation.warn(
55
+ ["You have defined one or more translated attributes with names that conflict with column(s) on the model table. ",
56
+ "Globalize does not support this configuration anymore, remove or rename column(s) on the model table.\n",
57
+ "Model name (table name): #{model_name} (#{table_name})\n",
58
+ "Attribute name(s): #{overlap.join(', ')}\n"].join
59
+ )
60
+ end
41
61
  end
42
62
 
43
63
  def apply_globalize_options(options)
@@ -74,10 +94,11 @@ module Globalize
74
94
  :foreign_key => options[:foreign_key],
75
95
  :dependent => :destroy,
76
96
  :extend => HasManyExtensions,
77
- :autosave => false
97
+ :autosave => true,
98
+ :inverse_of => :globalized_model
78
99
 
79
- after_create :save_translations!
80
- after_update :save_translations!
100
+ before_create :save_translations!
101
+ before_update :save_translations!
81
102
  end
82
103
  end
83
104
 
@@ -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,16 +34,15 @@ 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
45
- ensure_foreign_key_for(translation)
46
- translation.save!
47
46
  end
48
47
 
49
48
  reset
@@ -55,11 +54,6 @@ module Globalize
55
54
 
56
55
  protected
57
56
 
58
- # Sometimes the translation is initialised before a foreign key can be set.
59
- def ensure_foreign_key_for(translation)
60
- translation[translation.class.reflections["globalized_model"].foreign_key] = record.id
61
- end
62
-
63
57
  def type_cast(name, value)
64
58
  return value.presence unless column = column_for_attribute(name)
65
59
 
@@ -76,7 +70,11 @@ module Globalize
76
70
 
77
71
  def fetch_attribute(locale, name)
78
72
  translation = record.translation_for(locale, false)
79
- return translation && translation.send(name)
73
+ if translation
74
+ translation.send(name)
75
+ else
76
+ record.class.translation_class.new.send(name)
77
+ end
80
78
  end
81
79
 
82
80
  def set_metadata(object, metadata)
@@ -95,6 +93,7 @@ module Globalize
95
93
  end
96
94
 
97
95
  delegate :fallbacks_for_empty_translations?, :to => :record, :prefix => false
96
+ prepend AdapterDirty
98
97
  end
99
98
  end
100
99
  end
@@ -0,0 +1,54 @@
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
+ def clear_dirty
40
+ self.dirty = {}
41
+ end
42
+
43
+ def _reset_attribute name
44
+ record.send("#{name}=", record.changed_attributes[name])
45
+ record.original_changed_attributes.except!(name)
46
+ end
47
+
48
+ def reset
49
+ clear_dirty
50
+ super
51
+ end
52
+ end
53
+ end
54
+ 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,13 @@ 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, :class_name => self.name, :foreign_key => translation_options[:foreign_key], inverse_of: :translations
51
60
  klass
52
61
  end
53
62
  end
@@ -100,8 +109,8 @@ module Globalize
100
109
 
101
110
  def define_translations_writer(name)
102
111
  define_method(:"#{name}_translations=") do |value|
103
- value.each do |(locale, value)|
104
- write_attribute name, value, :locale => locale
112
+ value.each do |(locale, _value)|
113
+ write_attribute name, _value, :locale => locale
105
114
  end
106
115
  end
107
116
  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,48 +11,46 @@ 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
+ def assign_attributes(new_attributes, *options)
21
+ super unless new_attributes.respond_to?(:stringify_keys) && new_attributes.present?
22
+ attributes = new_attributes.stringify_keys
23
+ with_given_locale(attributes) { super(attributes.except("locale"), *options) }
20
24
  end
21
25
 
22
- def write_attribute(name, value, options = {})
23
- return super(name, value) unless translated?(name)
26
+ def write_attribute(name, value, *args, &block)
27
+ return super(name, value, *args, &block) unless translated?(name)
24
28
 
25
- options = {:locale => Globalize.locale}.merge(options)
29
+ options = {:locale => Globalize.locale}.merge(args.first || {})
26
30
 
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
31
+ globalize.write(options[:locale], name, value)
32
+ end
33
+
34
+ def [](attr_name)
35
+ if translated?(attr_name)
36
+ read_attribute(attr_name)
34
37
  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
38
+ read_attribute(attr_name) { |n| missing_attribute(n, caller) }
39
39
  end
40
-
41
- globalize.write(options[:locale], name, value)
42
40
  end
43
41
 
44
- def read_attribute(name, options = {})
42
+ def read_attribute(name, options = {}, &block)
45
43
  options = {:translated => true, :locale => nil}.merge(options)
46
- return super(name) unless options[:translated]
44
+ return super(name, &block) unless options[:translated]
47
45
 
48
46
  if translated?(name)
49
- if (value = globalize.fetch(options[:locale] || Globalize.locale, name))
47
+ if !(value = globalize.fetch(options[:locale] || Globalize.locale, name)).nil?
50
48
  value
51
49
  else
52
- super(name)
50
+ super(name, &block)
53
51
  end
54
52
  else
55
- super(name)
53
+ super(name, &block)
56
54
  end
57
55
  end
58
56
 
@@ -83,8 +81,9 @@ module Globalize
83
81
 
84
82
  options[locale].each do |key, value|
85
83
  translation.send :"#{key}=", value
84
+ translation.globalized_model.send :"#{key}=", value
86
85
  end
87
- translation.save
86
+ translation.save if persisted?
88
87
  end
89
88
  globalize.reset
90
89
  end
@@ -143,14 +142,17 @@ module Globalize
143
142
  Globalize.fallbacks(locale)
144
143
  end
145
144
 
146
- def rollback
147
- translation_caches[::Globalize.locale] = translation.previous_version
148
- end
149
-
150
145
  def save(*)
151
- Globalize.with_locale(translation.locale || I18n.default_locale) do
152
- super
146
+ result = Globalize.with_locale(translation.locale || I18n.default_locale) do
147
+ without_fallbacks do
148
+ super
149
+ end
153
150
  end
151
+ if result
152
+ globalize.clear_dirty
153
+ end
154
+
155
+ result
154
156
  end
155
157
 
156
158
  def column_for_attribute name
@@ -163,6 +165,16 @@ module Globalize
163
165
  [super, translation.cache_key].join("/")
164
166
  end
165
167
 
168
+ def changed?
169
+ changed_attributes.present? || translations.any?(&:changed?)
170
+ end
171
+
172
+ # need to access instance variable directly since changed_attributes
173
+ # is frozen as of Rails 4.2
174
+ def original_changed_attributes
175
+ @changed_attributes
176
+ end
177
+
166
178
  protected
167
179
 
168
180
  def each_locale_and_translated_attribute
@@ -184,15 +196,23 @@ module Globalize
184
196
  translation_caches.clear
185
197
  end
186
198
 
187
- def with_given_locale(attributes, &block)
188
- attributes.symbolize_keys! if attributes.respond_to?(:symbolize_keys!)
199
+ def with_given_locale(_attributes, &block)
200
+ attributes = _attributes.stringify_keys
189
201
 
190
- if locale = attributes.try(:delete, :locale)
202
+ if locale = attributes.try(:delete, "locale")
191
203
  Globalize.with_locale(locale, &block)
192
204
  else
193
205
  yield
194
206
  end
195
207
  end
208
+
209
+ def without_fallbacks
210
+ before = self.fallbacks_for_empty_translations
211
+ self.fallbacks_for_empty_translations = false
212
+ yield
213
+ ensure
214
+ self.fallbacks_for_empty_translations = before
215
+ end
196
216
  end
197
217
  end
198
218
  end