mobility 0.8.13 → 1.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
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