mobility 0.8.13 → 1.0.0.alpha

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 (93) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +26 -0
  5. data/Gemfile +5 -2
  6. data/Gemfile.lock +79 -8
  7. data/README.md +183 -91
  8. data/lib/mobility.rb +40 -166
  9. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  10. data/lib/mobility/backend.rb +19 -41
  11. data/lib/mobility/backends.rb +20 -0
  12. data/lib/mobility/backends/active_record.rb +4 -0
  13. data/lib/mobility/backends/active_record/column.rb +2 -0
  14. data/lib/mobility/backends/active_record/container.rb +4 -2
  15. data/lib/mobility/backends/active_record/hstore.rb +2 -0
  16. data/lib/mobility/backends/active_record/json.rb +2 -0
  17. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  18. data/lib/mobility/backends/active_record/key_value.rb +5 -3
  19. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  20. data/lib/mobility/backends/active_record/serialized.rb +2 -0
  21. data/lib/mobility/backends/active_record/table.rb +5 -3
  22. data/lib/mobility/backends/column.rb +0 -6
  23. data/lib/mobility/backends/container.rb +2 -1
  24. data/lib/mobility/backends/hash.rb +39 -0
  25. data/lib/mobility/backends/hstore.rb +0 -1
  26. data/lib/mobility/backends/json.rb +0 -1
  27. data/lib/mobility/backends/jsonb.rb +0 -1
  28. data/lib/mobility/backends/key_value.rb +22 -14
  29. data/lib/mobility/backends/null.rb +2 -0
  30. data/lib/mobility/backends/sequel.rb +3 -0
  31. data/lib/mobility/backends/sequel/column.rb +2 -0
  32. data/lib/mobility/backends/sequel/container.rb +3 -1
  33. data/lib/mobility/backends/sequel/hstore.rb +2 -0
  34. data/lib/mobility/backends/sequel/json.rb +2 -0
  35. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  36. data/lib/mobility/backends/sequel/key_value.rb +8 -6
  37. data/lib/mobility/backends/sequel/serialized.rb +2 -0
  38. data/lib/mobility/backends/sequel/table.rb +5 -2
  39. data/lib/mobility/backends/serialized.rb +1 -3
  40. data/lib/mobility/backends/table.rb +14 -6
  41. data/lib/mobility/pluggable.rb +36 -0
  42. data/lib/mobility/plugin.rb +260 -0
  43. data/lib/mobility/plugins.rb +26 -25
  44. data/lib/mobility/plugins/active_model.rb +17 -0
  45. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  46. data/lib/mobility/plugins/active_model/dirty.rb +112 -77
  47. data/lib/mobility/plugins/active_record.rb +34 -0
  48. data/lib/mobility/plugins/active_record/backend.rb +25 -0
  49. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  50. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  51. data/lib/mobility/plugins/active_record/query.rb +43 -31
  52. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
  53. data/lib/mobility/plugins/attribute_methods.rb +28 -20
  54. data/lib/mobility/plugins/attributes.rb +70 -0
  55. data/lib/mobility/plugins/backend.rb +138 -0
  56. data/lib/mobility/plugins/backend_reader.rb +34 -0
  57. data/lib/mobility/plugins/cache.rb +59 -24
  58. data/lib/mobility/plugins/default.rb +22 -17
  59. data/lib/mobility/plugins/dirty.rb +12 -33
  60. data/lib/mobility/plugins/fallbacks.rb +51 -43
  61. data/lib/mobility/plugins/fallthrough_accessors.rb +20 -23
  62. data/lib/mobility/plugins/locale_accessors.rb +25 -35
  63. data/lib/mobility/plugins/presence.rb +28 -21
  64. data/lib/mobility/plugins/query.rb +8 -17
  65. data/lib/mobility/plugins/reader.rb +50 -0
  66. data/lib/mobility/plugins/sequel.rb +34 -0
  67. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  68. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  69. data/lib/mobility/plugins/sequel/dirty.rb +32 -21
  70. data/lib/mobility/plugins/sequel/query.rb +21 -6
  71. data/lib/mobility/plugins/writer.rb +44 -0
  72. data/lib/mobility/translations.rb +95 -0
  73. data/lib/mobility/version.rb +12 -1
  74. data/lib/rails/generators/mobility/templates/initializer.rb +95 -77
  75. metadata +28 -27
  76. metadata.gz.sig +0 -0
  77. data/lib/mobility/active_model.rb +0 -4
  78. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  79. data/lib/mobility/active_record.rb +0 -23
  80. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  81. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  82. data/lib/mobility/attributes.rb +0 -324
  83. data/lib/mobility/backend/orm_delegator.rb +0 -44
  84. data/lib/mobility/backend_resetter.rb +0 -50
  85. data/lib/mobility/configuration.rb +0 -138
  86. data/lib/mobility/fallbacks.rb +0 -28
  87. data/lib/mobility/interface.rb +0 -0
  88. data/lib/mobility/loaded.rb +0 -4
  89. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  90. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  91. data/lib/mobility/sequel.rb +0 -9
  92. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  93. data/lib/mobility/translates.rb +0 -73
@@ -42,106 +42,132 @@ the ActiveRecord dirty plugin for more information.
42
42
 
43
43
  =end
44
44
  module Dirty
45
- # Builds module which adds suffix/prefix methods for translated
46
- # attributes so they act like normal dirty-tracked attributes.
47
- class MethodsBuilder < Module
48
- delegate :dirty_class, :handler_methods_module, :method_patterns, to: :class
45
+ extend Plugin
49
46
 
50
- def initialize(*attribute_names)
51
- define_dirty_methods(attribute_names)
47
+ requires :dirty, include: false
48
+
49
+ initialize_hook do
50
+ if options[:dirty]
51
+ define_dirty_methods(names)
52
+ include dirty_handler_methods
52
53
  end
54
+ end
53
55
 
54
- def included(model_class)
55
- model_class.include InstanceMethods
56
- model_class.include handler_methods_module
56
+ included_hook do |klass, backend_class|
57
+ raise TypeError, "#{name} should include ActiveModel::Dirty to use the active_model plugin" unless active_model_dirty_class?(klass)
57
58
 
58
- # In earlier versions of Rails, these methods are private
59
- %i[clear_attribute_changes clear_changes_information changes_applied].each do |method_name|
60
- if dirty_class.private_instance_methods.include?(method_name)
61
- model_class.class_eval { private method_name }
62
- end
63
- end
64
- end
59
+ if options[:dirty]
60
+ private_methods = InstanceMethods.instance_methods & klass.private_instance_methods
61
+ klass.include InstanceMethods
62
+ klass.class_eval { private(*private_methods) }
65
63
 
66
- def append_locale(attr_name)
67
- Mobility.normalize_locale_accessor(attr_name)
64
+ backend_class.include BackendMethods
68
65
  end
66
+ end
69
67
 
70
- private
68
+ private
71
69
 
72
- def define_dirty_methods(attribute_names)
73
- m = self
70
+ # Overridden in AR::Dirty plugin to define a different HandlerMethods module
71
+ def dirty_handler_methods
72
+ HandlerMethods
73
+ end
74
74
 
75
- attribute_names.each do |name|
76
- method_patterns.each do |pattern|
77
- define_method(pattern % name) do |*args|
78
- mutations_from_mobility.send(pattern % 'attribute', m.append_locale(name), *args)
79
- end
80
- end
75
+ def active_model_dirty_class?(klass)
76
+ klass.ancestors.include?(::ActiveModel::Dirty)
77
+ end
81
78
 
82
- define_method "restore_#{name}!" do
83
- locale_accessor = m.append_locale(name)
84
- if mutations_from_mobility.attribute_changed?(locale_accessor)
85
- __send__("#{name}=", mutations_from_mobility.attribute_was(locale_accessor))
86
- mutations_from_mobility.restore_attribute!(locale_accessor)
87
- end
79
+ def define_dirty_methods(attribute_names)
80
+ attribute_names.each do |name|
81
+ dirty_handler_methods.each_pattern(name) do |method_name, attribute_method|
82
+ define_method(method_name) do |*args|
83
+ mutations_from_mobility.send(attribute_method, Dirty.append_locale(name), *args)
88
84
  end
89
85
  end
90
86
 
91
- # This private method override is necessary to make
92
- # +restore_attributes+ (which is public) work with translated
93
- # attributes.
94
- define_method :restore_attribute! do |attr|
95
- attribute_names.include?(attr.to_s) ? send("restore_#{attr}!") : super(attr)
96
- end
97
- private :restore_attribute!
98
- end
99
-
100
- class << self
101
- def handler_methods_module
102
- @handler_methods_module ||= (AttributeHandlerMethods.new.tap do |mod|
103
- public_method_patterns.each do |pattern|
104
- method_name = pattern % 'attribute'
105
-
106
- mod.module_eval <<-EOM, __FILE__, __LINE__ + 1
107
- def #{method_name}(attr_name, *rest)
108
- if (mutations_from_mobility.attribute_changed?(attr_name) ||
109
- mutations_from_mobility.attribute_previously_changed?(attr_name))
110
- mutations_from_mobility.send(#{method_name.inspect}, attr_name, *rest)
111
- else
112
- super
113
- end
114
- end
115
- EOM
116
- end
117
- end)
87
+ define_method "restore_#{name}!" do
88
+ locale_accessor = Dirty.append_locale(name)
89
+ if mutations_from_mobility.attribute_changed?(locale_accessor)
90
+ __send__("#{name}=", mutations_from_mobility.attribute_was(locale_accessor))
91
+ mutations_from_mobility.restore_attribute!(locale_accessor)
92
+ end
118
93
  end
94
+ end
95
+
96
+ # This private method override is necessary to make
97
+ # +restore_attributes+ (which is public) work with translated
98
+ # attributes.
99
+ define_method :restore_attribute! do |attr|
100
+ attribute_names.include?(attr.to_s) ? send("restore_#{attr}!") : super(attr)
101
+ end
102
+ private :restore_attribute!
103
+ end
104
+
105
+ def self.append_locale(attr_name)
106
+ Mobility.normalize_locale_accessor(attr_name)
107
+ end
119
108
 
120
- # Get method suffixes. Creating an object just to get the list of
121
- # suffixes is simplest given they change from Rails version to version.
122
- def method_patterns
123
- @method_patterns ||=
124
- (dirty_class.attribute_method_matchers.map { |p| "#{p.prefix}%s#{p.suffix}" } - excluded_method_patterns)
109
+ # Module builder which mimics dirty method handlers on a given dirty class.
110
+ # Used to mimic ActiveModel::Dirty and ActiveRecord::Dirty, which have
111
+ # similar but slightly different sets of handler methods. Doing it this
112
+ # way with introspection allows us to support basically all AR/AM
113
+ # versions without changes here.
114
+ class HandlerMethodsBuilder < Module
115
+ attr_reader :klass
116
+
117
+ # @param [Class] klass Dirty class to mimic
118
+ def initialize(klass)
119
+ @klass = klass
120
+ define_handler_methods
121
+ end
122
+
123
+ def each_pattern(attr_name)
124
+ patterns.each do |pattern|
125
+ yield pattern % attr_name, pattern % 'attribute'
125
126
  end
127
+ end
126
128
 
127
- def public_method_patterns
128
- @public_method_patterns ||= method_patterns.select do |p|
129
- !dirty_class.private_instance_methods.include?(:"#{p % 'attribute'}")
129
+ def define_handler_methods
130
+ public_patterns.each do |pattern|
131
+ method_name = pattern % 'attribute'
132
+
133
+ module_eval <<-EOM, __FILE__, __LINE__ + 1
134
+ def #{method_name}(attr_name, *rest)
135
+ if (mutations_from_mobility.attribute_changed?(attr_name) ||
136
+ mutations_from_mobility.attribute_previously_changed?(attr_name))
137
+ mutations_from_mobility.send(#{method_name.inspect}, attr_name, *rest)
138
+ else
139
+ super
140
+ end
130
141
  end
142
+ EOM
131
143
  end
144
+ end
132
145
 
133
- def dirty_class
134
- @dirty_class ||= Class.new { include ::ActiveModel::Dirty }
135
- end
146
+ # Get method suffixes. Creating an object just to get the list of
147
+ # suffixes is simplest given they change from Rails version to version.
148
+ def patterns
149
+ @patterns ||=
150
+ (klass.attribute_method_matchers.map { |p| "#{p.prefix}%s#{p.suffix}" } - excluded_patterns)
151
+ end
136
152
 
137
- private
153
+ private
138
154
 
139
- def excluded_method_patterns
140
- ['%s', 'restore_%s!']
155
+ def public_patterns
156
+ @public_patterns ||= patterns.select do |p|
157
+ klass.public_method_defined?(p % 'attribute')
141
158
  end
142
159
  end
160
+
161
+ def excluded_patterns
162
+ ['%s', 'restore_%s!']
163
+ end
143
164
  end
144
165
 
166
+ # Module which defines generic handler methods like
167
+ # +attribute_changed?+ that are patched to work with translated
168
+ # attributes.
169
+ HandlerMethods = HandlerMethodsBuilder.new(Class.new { include ::ActiveModel::Dirty })
170
+
145
171
  module InstanceMethods
146
172
  def changed_attributes
147
173
  super.merge(mutations_from_mobility.changed_attributes)
@@ -186,9 +212,16 @@ the ActiveRecord dirty plugin for more information.
186
212
  end
187
213
  end
188
214
 
189
- # Give the module builder a name so it's easier to see in the model's ancestors
190
- class AttributeHandlerMethods < Module; end
191
-
215
+ # @note Seriously, I really don't want to reproduce all of
216
+ # ActiveModel::Dirty here, but having fought with upstream changes
217
+ # many many times I finally decided it's more future-proof to just
218
+ # re-implement the stuff we need here, to avoid weird breakage.
219
+ #
220
+ # Although this is somewhat ugly, at least it's explicit and since
221
+ # it's self-defined (rather than hooking into fickle private methods
222
+ # in Rails), it won't break all of a sudden. We just need to ensure
223
+ # that specs are up-to-date with the latest weird dirty method
224
+ # pattern Rails has decided to support.
192
225
  class MobilityMutationTracker
193
226
  OPTION_NOT_GIVEN = Object.new
194
227
 
@@ -316,5 +349,7 @@ the ActiveRecord dirty plugin for more information.
316
349
  end
317
350
  end
318
351
  end
352
+
353
+ register_plugin(:active_model_dirty, ActiveModel::Dirty)
319
354
  end
320
355
  end
@@ -1,6 +1,40 @@
1
+ # frozen_string_literal: true
2
+ require_relative "./active_record/backend"
3
+ require_relative "./active_record/dirty"
4
+ require_relative "./active_record/cache"
5
+ require_relative "./active_record/query"
6
+ require_relative "./active_record/uniqueness_validation"
7
+
1
8
  module Mobility
9
+ =begin
10
+
11
+ Plugin for ActiveRecord models.
12
+
13
+ =end
2
14
  module Plugins
3
15
  module ActiveRecord
16
+ extend Plugin
17
+
18
+ requires :active_record_backend, include: :after
19
+ requires :active_record_dirty
20
+ requires :active_record_cache
21
+ requires :active_record_query
22
+ requires :active_record_uniqueness_validation
23
+
24
+ included_hook do |klass|
25
+ unless active_record_class?(klass)
26
+ name = klass.name || klass.to_s
27
+ raise TypeError, "#{name} should be a subclass of ActiveRecord::Base to use the active_record plugin"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def active_record_class?(klass)
34
+ klass < ::ActiveRecord::Base
35
+ end
4
36
  end
37
+
38
+ register_plugin(:active_record, ActiveRecord)
5
39
  end
6
40
  end
@@ -0,0 +1,25 @@
1
+ module Mobility
2
+ module Plugins
3
+ module ActiveRecord
4
+ module Backend
5
+ extend Plugin
6
+
7
+ requires :backend, include: :before
8
+
9
+ def load_backend(backend)
10
+ if Symbol === backend
11
+ require "mobility/backends/active_record/#{backend}"
12
+ Backends.load_backend("active_record_#{backend}".to_sym)
13
+ else
14
+ super
15
+ end
16
+ rescue LoadError => e
17
+ raise unless e.message =~ /active_record\/#{backend}/
18
+ super
19
+ end
20
+ end
21
+ end
22
+
23
+ register_plugin(:active_record_backend, ActiveRecord::Backend)
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen-string-literal: true
2
+ require "mobility/plugins/active_model/cache"
3
+
4
+ module Mobility
5
+ module Plugins
6
+ module ActiveRecord
7
+ =begin
8
+
9
+ Resets cache on calls to +reload+, in addition to other AM dirty reset
10
+ methods.
11
+
12
+ =end
13
+ module Cache
14
+ extend Plugin
15
+
16
+ requires :cache, include: false
17
+
18
+ included_hook do |klass, _|
19
+ if options[:cache]
20
+ define_cache_hooks(klass, :changes_applied, :clear_changes_information, :reload)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ register_plugin(:active_record_cache, ActiveRecord::Cache)
27
+ end
28
+ end
@@ -40,29 +40,44 @@ locale suffix, so +title_en+, +title_pt_br+, etc.)
40
40
 
41
41
  =end
42
42
  module Dirty
43
- class MethodsBuilder < ActiveModel::Dirty::MethodsBuilder
44
- # @param [Attributes] attributes
45
- def included(model_class)
46
- super
43
+ extend Plugin
47
44
 
48
- model_class.include InstanceMethods
45
+ requires :dirty, include: false
46
+ requires :active_model_dirty, include: :before
47
+
48
+ initialize_hook do
49
+ if options[:dirty]
50
+ include InstanceMethods
49
51
  end
52
+ end
50
53
 
51
- class << self
52
- def dirty_class
53
- @dirty_class ||= (Class.new do
54
- # In earlier versions of Rails, these are needed to avoid an
55
- # exception when including the AR Dirty module outside of an
56
- # AR::Base class. Eventually we should be able to drop them.
57
- def self.after_create; end
58
- def self.after_update; end
59
-
60
- include ::ActiveRecord::AttributeMethods::Dirty
61
- end)
62
- end
54
+ included_hook do |_, backend_class|
55
+ if options[:dirty]
56
+ backend_class.include BackendMethods
63
57
  end
64
58
  end
65
59
 
60
+ private
61
+
62
+ def dirty_handler_methods
63
+ HandlerMethods
64
+ end
65
+
66
+ # Module which defines generic ActiveRecord::Dirty handler methods like
67
+ # +attribute_before_last_save+ that are patched to work with translated
68
+ # attributes.
69
+ HandlerMethods = ActiveModel::Dirty::HandlerMethodsBuilder.new(
70
+ Class.new do
71
+ # In earlier versions of Rails, these are needed to avoid an
72
+ # exception when including the AR Dirty module outside of an
73
+ # AR::Base class. Eventually we should be able to drop them.
74
+ def self.after_create; end
75
+ def self.after_update; end
76
+
77
+ include ::ActiveRecord::AttributeMethods::Dirty
78
+ end
79
+ )
80
+
66
81
  module InstanceMethods
67
82
  if ::ActiveRecord::VERSION::STRING >= '5.1' # define patterns added in 5.1
68
83
  def saved_changes
@@ -98,5 +113,7 @@ locale suffix, so +title_en+, +title_pt_br+, etc.)
98
113
  BackendMethods = ActiveModel::Dirty::BackendMethods
99
114
  end
100
115
  end
116
+
117
+ register_plugin(:active_record_dirty, ActiveRecord::Dirty)
101
118
  end
102
119
  end
@@ -1,4 +1,6 @@
1
1
  # frozen-string-literal: true
2
+ require "active_record/relation"
3
+
2
4
  module Mobility
3
5
  module Plugins
4
6
  =begin
@@ -15,45 +17,47 @@ enabled for any one attribute on the model.
15
17
  =end
16
18
  module ActiveRecord
17
19
  module Query
18
- class << self
19
- def apply(attributes)
20
- attributes.model_class.class_eval do
20
+ extend Plugin
21
+
22
+ requires :query, include: false
23
+
24
+ included_hook do |klass, backend_class|
25
+ plugin = self
26
+ if options[:query]
27
+ raise MissingBackend, "backend required for Query plugin" unless backend_class
28
+
29
+ klass.class_eval do
21
30
  extend QueryMethod
22
- extend FindByMethods.new(*attributes.names)
23
- singleton_class.send :alias_method, Mobility.query_method, :__mobility_query_scope__
31
+ extend FindByMethods.new(*plugin.names)
32
+ singleton_class.send :alias_method, plugin.query_method, :__mobility_query_scope__
24
33
  end
25
- attributes.backend_class.include self
34
+ backend_class.include BackendMethods
26
35
  end
36
+ end
27
37
 
38
+ class << self
28
39
  def attribute_alias(attribute, locale = Mobility.locale)
29
40
  "__mobility_%s_%s__" % [attribute, ::Mobility.normalize_locale(locale)]
30
41
  end
31
42
  end
32
43
 
33
- # @note We use +instance_variable_get+ here to get the +AttributeSet+
34
- # rather than the hash of attributes. Getting the full hash of
35
- # attributes is a performance hit and better to avoid if unnecessary.
36
- # TODO: Improve this.
37
- def read(locale, **)
38
- if (model_attributes_defined? &&
39
- model_attributes.key?(alias_ = Query.attribute_alias(attribute, locale)))
40
- model_attributes[alias_].value
41
- else
42
- super
44
+ module BackendMethods
45
+ # @note We use +instance_variable_get+ here to get the +AttributeSet+
46
+ # rather than the hash of attributes. Getting the full hash of
47
+ # attributes is a performance hit and better to avoid if unnecessary.
48
+ # TODO: Improve this.
49
+ def read(locale, **)
50
+ if model.instance_variable_defined?(:@attributes) &&
51
+ (model_attributes = model.instance_variable_get(:@attributes)).key?(alias_ = Query.attribute_alias(attribute, locale))
52
+ model_attributes[alias_].value
53
+ else
54
+ super
55
+ end
43
56
  end
44
57
  end
45
58
 
46
- private
47
-
48
- def model_attributes_defined?
49
- model.instance_variable_defined?(:@attributes)
50
- end
51
-
52
- def model_attributes
53
- model.instance_variable_get(:@attributes)
54
- end
55
-
56
59
  module QueryMethod
60
+ # This is required for UniquenessValidator.
57
61
  def __mobility_query_scope__(locale: Mobility.locale, &block)
58
62
  if block_given?
59
63
  VirtualRow.build_query(self, locale, &block)
@@ -121,7 +125,7 @@ enabled for any one attribute on the model.
121
125
  case opts
122
126
  when Symbol, String
123
127
  @klass.mobility_attribute?(opts) ? order({ opts => :asc }, *rest) : super
124
- when Hash
128
+ when ::Hash
125
129
  i18n_keys, keys = opts.keys.partition(&@klass.method(:mobility_attribute?))
126
130
  return super if i18n_keys.empty?
127
131
 
@@ -182,7 +186,7 @@ enabled for any one attribute on the model.
182
186
 
183
187
  class << self
184
188
  def build(scope, where_opts, invert: false, &block)
185
- return yield unless Hash === where_opts
189
+ return yield unless ::Hash === where_opts
186
190
 
187
191
  opts = where_opts.with_indifferent_access
188
192
  locale = opts.delete(:locale) || Mobility.locale
@@ -195,11 +199,11 @@ enabled for any one attribute on the model.
195
199
  # Builds a translated relation for a given opts hash and optional
196
200
  # invert boolean.
197
201
  def _build(scope, opts, locale, invert)
198
- return yield unless scope.respond_to?(:mobility_modules)
202
+ return yield if (mods = attribute_modules(scope)).empty?
199
203
 
200
204
  keys, predicates = opts.keys.map(&:to_s), []
201
205
 
202
- query_map = scope.mobility_modules.inject(IDENTITY) do |qm, mod|
206
+ query_map = mods.inject(IDENTITY) do |qm, mod|
203
207
  i18n_keys = mod.names & keys
204
208
  next qm if i18n_keys.empty?
205
209
 
@@ -215,7 +219,11 @@ enabled for any one attribute on the model.
215
219
  return yield if query_map == IDENTITY
216
220
 
217
221
  relation = opts.empty? ? scope : yield(opts)
218
- query_map[relation.where(predicates.inject(&:and))]
222
+ query_map[relation.where(predicates.inject(:and))]
223
+ end
224
+
225
+ def attribute_modules(scope)
226
+ scope.model.ancestors.grep(::Mobility::Translations)
219
227
  end
220
228
 
221
229
  def build_predicate(node, values)
@@ -267,6 +275,10 @@ enabled for any one attribute on the model.
267
275
 
268
276
  private_constant :QueryExtension, :FindByMethods
269
277
  end
278
+
279
+ class MissingBackend < Mobility::Error; end
270
280
  end
281
+
282
+ register_plugin(:active_record_query, ActiveRecord::Query)
271
283
  end
272
284
  end