mobility 0.0.1 → 0.1.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.
- checksums.yaml +4 -4
- data/Gemfile +19 -0
- data/Gemfile.lock +153 -0
- data/Guardfile +70 -0
- data/README.md +603 -13
- data/Rakefile +42 -0
- data/lib/generators/mobility/install_generator.rb +45 -0
- data/lib/generators/mobility/templates/create_string_translations.rb +15 -0
- data/lib/generators/mobility/templates/create_text_translations.rb +15 -0
- data/lib/mobility.rb +203 -2
- data/lib/mobility/active_model.rb +6 -0
- data/lib/mobility/active_model/attribute_methods.rb +27 -0
- data/lib/mobility/active_model/backend_resetter.rb +26 -0
- data/lib/mobility/active_record.rb +39 -0
- data/lib/mobility/active_record/backend_resetter.rb +26 -0
- data/lib/mobility/active_record/model_translation.rb +14 -0
- data/lib/mobility/active_record/string_translation.rb +7 -0
- data/lib/mobility/active_record/text_translation.rb +7 -0
- data/lib/mobility/active_record/translation.rb +14 -0
- data/lib/mobility/attributes.rb +210 -0
- data/lib/mobility/backend.rb +152 -0
- data/lib/mobility/backend/active_model.rb +7 -0
- data/lib/mobility/backend/active_model/dirty.rb +84 -0
- data/lib/mobility/backend/active_record.rb +13 -0
- data/lib/mobility/backend/active_record/column.rb +52 -0
- data/lib/mobility/backend/active_record/column/query_methods.rb +40 -0
- data/lib/mobility/backend/active_record/hash_valued.rb +58 -0
- data/lib/mobility/backend/active_record/hstore.rb +36 -0
- data/lib/mobility/backend/active_record/hstore/query_methods.rb +53 -0
- data/lib/mobility/backend/active_record/jsonb.rb +43 -0
- data/lib/mobility/backend/active_record/jsonb/query_methods.rb +53 -0
- data/lib/mobility/backend/active_record/key_value.rb +126 -0
- data/lib/mobility/backend/active_record/key_value/query_methods.rb +63 -0
- data/lib/mobility/backend/active_record/query_methods.rb +36 -0
- data/lib/mobility/backend/active_record/serialized.rb +93 -0
- data/lib/mobility/backend/active_record/serialized/query_methods.rb +32 -0
- data/lib/mobility/backend/active_record/table.rb +197 -0
- data/lib/mobility/backend/active_record/table/query_methods.rb +91 -0
- data/lib/mobility/backend/cache.rb +110 -0
- data/lib/mobility/backend/column.rb +52 -0
- data/lib/mobility/backend/dirty.rb +28 -0
- data/lib/mobility/backend/fallbacks.rb +89 -0
- data/lib/mobility/backend/hstore.rb +21 -0
- data/lib/mobility/backend/jsonb.rb +21 -0
- data/lib/mobility/backend/key_value.rb +71 -0
- data/lib/mobility/backend/null.rb +24 -0
- data/lib/mobility/backend/orm_delegator.rb +33 -0
- data/lib/mobility/backend/sequel.rb +14 -0
- data/lib/mobility/backend/sequel/column.rb +40 -0
- data/lib/mobility/backend/sequel/column/query_methods.rb +24 -0
- data/lib/mobility/backend/sequel/dirty.rb +54 -0
- data/lib/mobility/backend/sequel/hash_valued.rb +51 -0
- data/lib/mobility/backend/sequel/hstore.rb +36 -0
- data/lib/mobility/backend/sequel/hstore/query_methods.rb +42 -0
- data/lib/mobility/backend/sequel/jsonb.rb +43 -0
- data/lib/mobility/backend/sequel/jsonb/query_methods.rb +42 -0
- data/lib/mobility/backend/sequel/key_value.rb +139 -0
- data/lib/mobility/backend/sequel/key_value/query_methods.rb +48 -0
- data/lib/mobility/backend/sequel/query_methods.rb +22 -0
- data/lib/mobility/backend/sequel/serialized.rb +133 -0
- data/lib/mobility/backend/sequel/serialized/query_methods.rb +20 -0
- data/lib/mobility/backend/sequel/table.rb +149 -0
- data/lib/mobility/backend/sequel/table/query_methods.rb +48 -0
- data/lib/mobility/backend/serialized.rb +53 -0
- data/lib/mobility/backend/table.rb +93 -0
- data/lib/mobility/backend_resetter.rb +44 -0
- data/lib/mobility/configuration.rb +31 -0
- data/lib/mobility/core_ext/nil.rb +10 -0
- data/lib/mobility/core_ext/object.rb +19 -0
- data/lib/mobility/core_ext/string.rb +16 -0
- data/lib/mobility/instance_methods.rb +34 -0
- data/lib/mobility/orm.rb +4 -0
- data/lib/mobility/sequel.rb +26 -0
- data/lib/mobility/sequel/backend_resetter.rb +26 -0
- data/lib/mobility/sequel/column_changes.rb +29 -0
- data/lib/mobility/sequel/model_translation.rb +20 -0
- data/lib/mobility/sequel/string_translation.rb +7 -0
- data/lib/mobility/sequel/text_translation.rb +7 -0
- data/lib/mobility/sequel/translation.rb +53 -0
- data/lib/mobility/translates.rb +75 -0
- data/lib/mobility/wrapper.rb +31 -0
- metadata +152 -12
- data/.gitignore +0 -9
- data/.rspec +0 -2
- data/.travis.yml +0 -5
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/mobility.gemspec +0 -32
@@ -0,0 +1,63 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Backend
|
3
|
+
class ActiveRecord::KeyValue::QueryMethods < ActiveRecord::QueryMethods
|
4
|
+
def initialize(attributes, **options)
|
5
|
+
super
|
6
|
+
association_name, translations_class = options[:association_name], options[:class_name]
|
7
|
+
@association_name = association_name
|
8
|
+
attributes_extractor = @attributes_extractor
|
9
|
+
|
10
|
+
define_method :"join_#{association_name}" do |*attributes, **options|
|
11
|
+
attributes.inject(self) do |relation, attribute|
|
12
|
+
t = translations_class.arel_table.alias(:"#{attribute}_#{association_name}")
|
13
|
+
m = arel_table
|
14
|
+
join_type = options[:outer_join] ? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
|
15
|
+
relation.joins(m.join(t, join_type).
|
16
|
+
on(t[:key].eq(attribute).
|
17
|
+
and(t[:locale].eq(Mobility.locale).
|
18
|
+
and(t[:translatable_type].eq(name).
|
19
|
+
and(t[:translatable_id].eq(m[:id]))))).join_sources)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method :where! do |opts, *rest|
|
24
|
+
if i18n_keys = attributes_extractor.call(opts)
|
25
|
+
opts = opts.with_indifferent_access
|
26
|
+
i18n_nulls = i18n_keys.select { |key| opts[key].nil? }
|
27
|
+
i18n_keys.each { |attr| opts["#{attr}_#{association_name}"] = { value: opts.delete(attr) }}
|
28
|
+
super(opts, *rest).
|
29
|
+
send("join_#{association_name}", *(i18n_keys - i18n_nulls)).
|
30
|
+
send("join_#{association_name}", *i18n_nulls, outer_join: true)
|
31
|
+
else
|
32
|
+
super(opts, *rest)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
attributes.each do |attribute|
|
37
|
+
define_method :"find_by_#{attribute}" do |value|
|
38
|
+
find_by(attribute.to_sym => value)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def extended(relation)
|
44
|
+
super
|
45
|
+
association_name = @association_name
|
46
|
+
attributes_extractor = @attributes_extractor
|
47
|
+
|
48
|
+
mod = Module.new do
|
49
|
+
define_method :not do |opts, *rest|
|
50
|
+
if i18n_keys = attributes_extractor.call(opts)
|
51
|
+
opts = opts.with_indifferent_access
|
52
|
+
i18n_keys.each { |attr| opts["#{attr}_#{association_name}"] = { value: opts.delete(attr) }}
|
53
|
+
super(opts, *rest).send(:"join_#{association_name}", *i18n_keys)
|
54
|
+
else
|
55
|
+
super(opts, *rest)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
relation.model.mobility_where_chain.prepend(mod)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Backend
|
3
|
+
module ActiveRecord
|
4
|
+
=begin
|
5
|
+
|
6
|
+
Defines query method overrides to handle translated attributes for ActiveRecord
|
7
|
+
models. For details see backend-specific subclasses.
|
8
|
+
|
9
|
+
=end
|
10
|
+
class QueryMethods < Module
|
11
|
+
# @param [Array<String>] attributes Translated attributes
|
12
|
+
# @param [Hash] options Backend options
|
13
|
+
def initialize(attributes, **options)
|
14
|
+
@attributes = attributes
|
15
|
+
@attributes_extractor = lambda do |opts|
|
16
|
+
opts.is_a?(Hash) && (opts.keys.map(&:to_s) & attributes).presence
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [ActiveRecord::Relation] relation Relation being extended
|
21
|
+
def extended(relation)
|
22
|
+
model_class = relation.model
|
23
|
+
unless model_class.respond_to?(:mobility_where_chain)
|
24
|
+
model_class.define_singleton_method(:mobility_where_chain) do
|
25
|
+
@mobility_where_chain ||= Class.new(::ActiveRecord::QueryMethods::WhereChain)
|
26
|
+
end
|
27
|
+
|
28
|
+
relation.define_singleton_method :where do |opts = :chain, *rest|
|
29
|
+
opts == :chain ? mobility_where_chain.new(spawn) : super(opts, *rest)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Backend
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Implements {Mobility::Backend::Serialized} backend for ActiveRecord models.
|
6
|
+
|
7
|
+
@example Define attribute with serialized backend
|
8
|
+
class Post < ActiveRecord::Base
|
9
|
+
translates :title, backend: :serialized, format: :yaml
|
10
|
+
end
|
11
|
+
|
12
|
+
@example Read and write attribute translations
|
13
|
+
post = Post.create(title: "foo")
|
14
|
+
post.title
|
15
|
+
#=> "foo"
|
16
|
+
Mobility.locale = :ja
|
17
|
+
post.title = "あああ"
|
18
|
+
post.save
|
19
|
+
post.read_attribute(:title) # get serialized value
|
20
|
+
#=> {:en=>"foo", :ja=>"あああ"}
|
21
|
+
|
22
|
+
=end
|
23
|
+
class ActiveRecord::Serialized
|
24
|
+
include Backend
|
25
|
+
|
26
|
+
autoload :QueryMethods, 'mobility/backend/active_record/serialized/query_methods'
|
27
|
+
|
28
|
+
# @!group Backend Accessors
|
29
|
+
#
|
30
|
+
# @!macro backend_reader
|
31
|
+
def read(locale, **options)
|
32
|
+
translations[locale]
|
33
|
+
end
|
34
|
+
|
35
|
+
# @!macro backend_reader
|
36
|
+
def write(locale, value, **options)
|
37
|
+
translations[locale] = value
|
38
|
+
end
|
39
|
+
# @!endgroup
|
40
|
+
|
41
|
+
# @!group Backend Configuration
|
42
|
+
# @option options [Symbol] format (:yaml) Serialization format
|
43
|
+
# @raise [ArgumentError] if a format other than +:yaml+ or +:json+ is passed in
|
44
|
+
def self.configure!(options)
|
45
|
+
options[:format] ||= :yaml
|
46
|
+
options[:format] = options[:format].downcase.to_sym
|
47
|
+
raise ArgumentError, "Serialized backend only supports yaml or json formats." unless [:yaml, :json].include?(options[:format])
|
48
|
+
end
|
49
|
+
# @!endgroup
|
50
|
+
|
51
|
+
setup do |attributes, options|
|
52
|
+
coder = { yaml: YAMLCoder, json: JSONCoder }[options[:format]]
|
53
|
+
attributes.each { |attribute| serialize attribute, coder }
|
54
|
+
|
55
|
+
extension = Module.new do
|
56
|
+
define_method :i18n do
|
57
|
+
@mobility_scope ||= super().extending(QueryMethods.new(attributes, options))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
extend extension
|
61
|
+
end
|
62
|
+
|
63
|
+
# @!group Cache Methods
|
64
|
+
# Returns column value as a hash
|
65
|
+
# @return [Hash]
|
66
|
+
def translations
|
67
|
+
model.read_attribute(attribute)
|
68
|
+
end
|
69
|
+
alias_method :new_cache, :translations
|
70
|
+
|
71
|
+
# @return [Boolean]
|
72
|
+
def write_to_cache?
|
73
|
+
true
|
74
|
+
end
|
75
|
+
# @!endgroup
|
76
|
+
|
77
|
+
%w[yaml json].each do |format|
|
78
|
+
class_eval <<-EOM, __FILE__, __LINE__ + 1
|
79
|
+
class #{format.upcase}Coder
|
80
|
+
def self.dump(obj)
|
81
|
+
Serialized.serializer_for(:#{format}).call(obj)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.load(obj)
|
85
|
+
return {} if obj.nil?
|
86
|
+
Serialized.deserializer_for(:#{format}).call(obj)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
EOM
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Backend
|
3
|
+
class ActiveRecord::Serialized::QueryMethods < ActiveRecord::QueryMethods
|
4
|
+
def initialize(attributes, **options)
|
5
|
+
super
|
6
|
+
attributes_extractor = @attributes_extractor
|
7
|
+
opts_checker = @opts_checker = lambda do |opts|
|
8
|
+
if keys = attributes_extractor.call(opts)
|
9
|
+
raise ArgumentError,
|
10
|
+
"You cannot query on mobility attributes translated with the Serialized backend (#{keys.join(", ")})."
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
define_method :where! do |opts, *rest|
|
15
|
+
opts_checker.call(opts) || super(opts, *rest)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def extended(relation)
|
20
|
+
super
|
21
|
+
opts_checker = @opts_checker
|
22
|
+
|
23
|
+
mod = Module.new do
|
24
|
+
define_method :not do |opts, *rest|
|
25
|
+
opts_checker.call(opts) || super(opts, *rest)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
relation.model.mobility_where_chain.prepend(mod)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Backend
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Implements the {Mobility::Backend::Table} backend for ActiveRecord models.
|
6
|
+
|
7
|
+
@example Model with table backend
|
8
|
+
class Post < ActiveRecord::Base
|
9
|
+
translates :title, backend: :table, association_name: :translations
|
10
|
+
end
|
11
|
+
|
12
|
+
post = Post.create(title: "foo")
|
13
|
+
#<Post:0x00... id: 1>
|
14
|
+
|
15
|
+
post.title
|
16
|
+
#=> "foo"
|
17
|
+
|
18
|
+
post.translations
|
19
|
+
#=> [#<Post::Translation:0x00...
|
20
|
+
# id: 1,
|
21
|
+
# locale: "en",
|
22
|
+
# post_id: 1,
|
23
|
+
# title: "foo">]
|
24
|
+
|
25
|
+
Post::Translation.first
|
26
|
+
#=> #<Post::Translation:0x00...
|
27
|
+
# id: 1,
|
28
|
+
# locale: "en",
|
29
|
+
# post_id: 1,
|
30
|
+
# title: "foo">
|
31
|
+
|
32
|
+
@example Model with multiple translation tables
|
33
|
+
class Post < ActiveRecord::Base
|
34
|
+
translates :title, backend: :table, table_name: :post_title_translations, association_name: :title_translations
|
35
|
+
translates :content, backend: :table, table_name: :post_content_translations, association_name: :content_translations
|
36
|
+
end
|
37
|
+
|
38
|
+
post = Post.create(title: "foo", content: "bar")
|
39
|
+
#<Post:0x00... id: 1>
|
40
|
+
|
41
|
+
post.title
|
42
|
+
#=> "foo"
|
43
|
+
|
44
|
+
post.content
|
45
|
+
#=> "bar"
|
46
|
+
|
47
|
+
post.title_translations
|
48
|
+
#=> [#<Post::TitleTranslation:0x00...
|
49
|
+
# id: 1,
|
50
|
+
# locale: "en",
|
51
|
+
# post_id: 1,
|
52
|
+
# title: "foo">]
|
53
|
+
|
54
|
+
post.content_translations
|
55
|
+
#=> [#<Post::ContentTranslation:0x00...
|
56
|
+
# id: 1,
|
57
|
+
# locale: "en",
|
58
|
+
# post_id: 1,
|
59
|
+
# content: "bar">]
|
60
|
+
|
61
|
+
Post::TitleTranslation.first
|
62
|
+
#=> #<Post::TitleTranslation:0x00...
|
63
|
+
# id: 1,
|
64
|
+
# locale: "en",
|
65
|
+
# post_id: 1,
|
66
|
+
# title: "foo">
|
67
|
+
|
68
|
+
Post::ContentTranslation.first
|
69
|
+
#=> #<Post::ContentTranslation:0x00...
|
70
|
+
# id: 1,
|
71
|
+
# locale: "en",
|
72
|
+
# post_id: 1,
|
73
|
+
# title: "bar">
|
74
|
+
=end
|
75
|
+
class ActiveRecord::Table
|
76
|
+
include Backend
|
77
|
+
|
78
|
+
autoload :QueryMethods, 'mobility/backend/active_record/table/query_methods'
|
79
|
+
|
80
|
+
# @return [Symbol] name of the association method
|
81
|
+
attr_reader :association_name
|
82
|
+
|
83
|
+
# @!macro backend_constructor
|
84
|
+
# @option options [Symbol] association_name Name of association
|
85
|
+
def initialize(model, attribute, **options)
|
86
|
+
super
|
87
|
+
@association_name = options[:association_name]
|
88
|
+
end
|
89
|
+
|
90
|
+
# @!group Backend Accessors
|
91
|
+
# @!macro backend_reader
|
92
|
+
def read(locale, **options)
|
93
|
+
translation_for(locale).send(attribute)
|
94
|
+
end
|
95
|
+
|
96
|
+
# @!macro backend_reader
|
97
|
+
def write(locale, value, **options)
|
98
|
+
translation_for(locale).tap { |t| t.send("#{attribute}=", value) }.send(attribute)
|
99
|
+
end
|
100
|
+
# @!endgroup
|
101
|
+
|
102
|
+
# @!group Backend Configuration
|
103
|
+
# @option options [Symbol] association_name (:mobility_model_translations)
|
104
|
+
# Name of association method
|
105
|
+
# @option options [Symbol] table_name Name of translation table
|
106
|
+
# @option options [Symbol] foreign_key Name of foreign key
|
107
|
+
# @option options [Symbol] subclass_name (:Translation) Name of subclass
|
108
|
+
# to append to model class to generate translation class
|
109
|
+
def self.configure!(options)
|
110
|
+
table_name = options[:model_class].table_name
|
111
|
+
options[:table_name] ||= "#{table_name.singularize}_translations"
|
112
|
+
options[:foreign_key] ||= table_name.downcase.singularize.camelize.foreign_key
|
113
|
+
if (association_name = options[:association_name]).present?
|
114
|
+
options[:subclass_name] ||= association_name.to_s.singularize.camelize
|
115
|
+
else
|
116
|
+
options[:association_name] = :mobility_model_translations
|
117
|
+
options[:subclass_name] ||= :Translation
|
118
|
+
end
|
119
|
+
%i[foreign_key association_name subclass_name].each { |key| options[key] = options[key].to_sym }
|
120
|
+
end
|
121
|
+
# @!endgroup
|
122
|
+
|
123
|
+
setup do |attributes, options|
|
124
|
+
association_name = options[:association_name]
|
125
|
+
subclass_name = options[:subclass_name]
|
126
|
+
|
127
|
+
attr_accessor :"__#{association_name}_cache"
|
128
|
+
|
129
|
+
translation_class =
|
130
|
+
if self.const_defined?(subclass_name, false)
|
131
|
+
const_get(subclass_name, false)
|
132
|
+
else
|
133
|
+
const_set(subclass_name, Class.new(Mobility::ActiveRecord::ModelTranslation))
|
134
|
+
end
|
135
|
+
|
136
|
+
translation_class.table_name = options[:table_name]
|
137
|
+
|
138
|
+
has_many association_name,
|
139
|
+
class_name: translation_class.name,
|
140
|
+
foreign_key: options[:foreign_key],
|
141
|
+
dependent: :destroy,
|
142
|
+
autosave: true,
|
143
|
+
inverse_of: :translated_model
|
144
|
+
|
145
|
+
translation_class.belongs_to :translated_model,
|
146
|
+
class_name: name,
|
147
|
+
foreign_key: options[:foreign_key],
|
148
|
+
inverse_of: association_name
|
149
|
+
|
150
|
+
query_methods = Module.new do
|
151
|
+
define_method :i18n do
|
152
|
+
@mobility_scope ||= super().extending(QueryMethods.new(attributes, options))
|
153
|
+
end
|
154
|
+
end
|
155
|
+
extend query_methods
|
156
|
+
end
|
157
|
+
|
158
|
+
# @!group Cache Methods
|
159
|
+
# @return [Table::TranslationsCache]
|
160
|
+
def new_cache
|
161
|
+
reset_model_cache unless model_cache
|
162
|
+
model_cache.for(attribute)
|
163
|
+
end
|
164
|
+
|
165
|
+
# @return [Boolean]
|
166
|
+
def write_to_cache?
|
167
|
+
true
|
168
|
+
end
|
169
|
+
|
170
|
+
def clear_cache
|
171
|
+
model_cache.try(:clear)
|
172
|
+
end
|
173
|
+
# @!endgroup
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
def translation_for(locale)
|
178
|
+
translation = translations.find { |t| t.locale == locale.to_s }
|
179
|
+
translation ||= translations.build(locale: locale)
|
180
|
+
translation
|
181
|
+
end
|
182
|
+
|
183
|
+
def translations
|
184
|
+
model.send(association_name)
|
185
|
+
end
|
186
|
+
|
187
|
+
def model_cache
|
188
|
+
model.send(:"__#{association_name}_cache")
|
189
|
+
end
|
190
|
+
|
191
|
+
def reset_model_cache
|
192
|
+
model.send(:"__#{association_name}_cache=",
|
193
|
+
Table::TranslationsCache.new { |locale| translation_for(locale) })
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Backend
|
3
|
+
class ActiveRecord::Table::QueryMethods < ActiveRecord::QueryMethods
|
4
|
+
def initialize(attributes, **options)
|
5
|
+
super
|
6
|
+
association_name = options[:association_name]
|
7
|
+
foreign_key = options[:foreign_key]
|
8
|
+
@association_name = association_name
|
9
|
+
attributes_extractor = @attributes_extractor
|
10
|
+
translation_class = options[:model_class].const_get(options[:subclass_name])
|
11
|
+
@translation_class = translation_class
|
12
|
+
table_name = options[:table_name]
|
13
|
+
|
14
|
+
define_method :"join_#{association_name}" do |**options|
|
15
|
+
return self if (@__mobility_table_joined || []).include?(table_name)
|
16
|
+
(@__mobility_table_joined ||= []) << table_name
|
17
|
+
t = translation_class.arel_table
|
18
|
+
m = arel_table
|
19
|
+
join_type = options[:outer_join] ? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
|
20
|
+
joins(m.join(t, join_type).
|
21
|
+
on(t[foreign_key].eq(m[:id]).
|
22
|
+
and(t[:locale].eq(Mobility.locale))).join_sources)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Note that Mobility will try to use inner/outer joins appropriate to the query,
|
26
|
+
# so for example:
|
27
|
+
#
|
28
|
+
# Article.where(title: nil, content: nil) #=> OUTER JOIN (all nils)
|
29
|
+
# Article.where(title: "foo", content: nil) #=> INNER JOIN (one non-nil)
|
30
|
+
#
|
31
|
+
# In the first case, if we are in (say) the "en" locale, then we should match articles
|
32
|
+
# that have *no* article_translations with English locales (since no translation is
|
33
|
+
# equivalent to a nil value). If we used an inner join in the first case, an article
|
34
|
+
# with no English translations would be filtered out, so we use an outer join.
|
35
|
+
#
|
36
|
+
# However, if you call `where` multiple times, you may end up with an outer join
|
37
|
+
# when a (faster) inner join would have worked fine:
|
38
|
+
#
|
39
|
+
# Article.where(title: nil).where(content: "foo") #=> OUTER JOIN
|
40
|
+
#
|
41
|
+
# In this case, we are searching for a match on the article_translations table
|
42
|
+
# which has a NULL title and a content equal to "foo". Since we need a positive
|
43
|
+
# match for content, there must be an English translation on the article, thus
|
44
|
+
# we can use an inner join. However, Mobility will use an outer join since we don't
|
45
|
+
# want to modify the existing relation which has already been joined.
|
46
|
+
#
|
47
|
+
# To avoid this problem, simply make sure to either order your queries to place nil
|
48
|
+
# values last, or include all queried attributes in a single `where`:
|
49
|
+
#
|
50
|
+
# Article.where(title: nil, content: "foo") #=> INNER JOIN
|
51
|
+
#
|
52
|
+
define_method :where! do |opts, *rest|
|
53
|
+
if i18n_keys = attributes_extractor.call(opts)
|
54
|
+
opts = opts.with_indifferent_access
|
55
|
+
options = { outer_join: i18n_keys.all? { |attr| opts[attr].nil? } }
|
56
|
+
i18n_keys.each { |attr| opts["#{translation_class.table_name}.#{attr}"] = opts.delete(attr) }
|
57
|
+
super(opts, *rest).send("join_#{association_name}", options)
|
58
|
+
else
|
59
|
+
super(opts, *rest)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
attributes.each do |attribute|
|
64
|
+
define_method :"find_by_#{attribute}" do |value|
|
65
|
+
find_by(attribute.to_sym => value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def extended(relation)
|
71
|
+
super
|
72
|
+
association_name = @association_name
|
73
|
+
attributes_extractor = @attributes_extractor
|
74
|
+
translation_class = @translation_class
|
75
|
+
|
76
|
+
mod = Module.new do
|
77
|
+
define_method :not do |opts, *rest|
|
78
|
+
if i18n_keys = attributes_extractor.call(opts)
|
79
|
+
opts = opts.with_indifferent_access
|
80
|
+
i18n_keys.each { |attr| opts["#{translation_class.table_name}.#{attr}"] = opts.delete(attr) }
|
81
|
+
super(opts, *rest).send("join_#{association_name}")
|
82
|
+
else
|
83
|
+
super(opts, *rest)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
relation.model.mobility_where_chain.prepend(mod)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|