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