mobility 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -2
- data.tar.gz.sig +0 -0
- data/CHANGELOG.md +41 -1
- data/Gemfile.lock +3 -58
- data/README.md +22 -21
- data/lib/mobility.rb +3 -2
- data/lib/mobility/accumulator.rb +1 -2
- data/lib/mobility/active_model/backend_resetter.rb +1 -1
- data/lib/mobility/active_record.rb +12 -9
- data/lib/mobility/active_record/backend_resetter.rb +6 -7
- data/lib/mobility/active_record/uniqueness_validator.rb +12 -2
- data/lib/mobility/adapter.rb +1 -0
- data/lib/mobility/attributes.rb +3 -13
- data/lib/mobility/backends/active_record/column.rb +1 -0
- data/lib/mobility/backends/active_record/column/query_methods.rb +25 -20
- data/lib/mobility/backends/active_record/container/json_query_methods.rb +22 -16
- data/lib/mobility/backends/active_record/container/jsonb_query_methods.rb +19 -19
- data/lib/mobility/backends/active_record/hstore.rb +14 -12
- data/lib/mobility/backends/active_record/hstore/query_methods.rb +14 -5
- data/lib/mobility/backends/active_record/json.rb +21 -19
- data/lib/mobility/backends/active_record/json/query_methods.rb +16 -11
- data/lib/mobility/backends/active_record/jsonb.rb +21 -19
- data/lib/mobility/backends/active_record/jsonb/query_methods.rb +14 -5
- data/lib/mobility/backends/active_record/key_value.rb +9 -9
- data/lib/mobility/backends/active_record/key_value/query_methods.rb +53 -46
- data/lib/mobility/backends/active_record/pg_hash.rb +29 -25
- data/lib/mobility/backends/active_record/pg_query_methods.rb +76 -40
- data/lib/mobility/backends/active_record/query_methods.rb +17 -10
- data/lib/mobility/backends/active_record/serialized.rb +4 -2
- data/lib/mobility/backends/active_record/serialized/query_methods.rb +18 -15
- data/lib/mobility/backends/active_record/table.rb +21 -12
- data/lib/mobility/backends/active_record/table/query_methods.rb +82 -83
- data/lib/mobility/backends/hash_valued.rb +19 -0
- data/lib/mobility/backends/hstore.rb +3 -1
- data/lib/mobility/backends/json.rb +3 -1
- data/lib/mobility/backends/jsonb.rb +3 -1
- data/lib/mobility/backends/key_value.rb +32 -15
- data/lib/mobility/backends/sequel/column/query_methods.rb +16 -12
- data/lib/mobility/backends/sequel/container/json_query_methods.rb +25 -18
- data/lib/mobility/backends/sequel/container/jsonb_query_methods.rb +25 -18
- data/lib/mobility/backends/sequel/hstore.rb +14 -12
- data/lib/mobility/backends/sequel/hstore/query_methods.rb +18 -11
- data/lib/mobility/backends/sequel/json.rb +21 -19
- data/lib/mobility/backends/sequel/json/query_methods.rb +18 -11
- data/lib/mobility/backends/sequel/jsonb.rb +21 -19
- data/lib/mobility/backends/sequel/jsonb/query_methods.rb +18 -11
- data/lib/mobility/backends/sequel/key_value.rb +10 -11
- data/lib/mobility/backends/sequel/key_value/query_methods.rb +39 -34
- data/lib/mobility/backends/sequel/pg_hash.rb +37 -25
- data/lib/mobility/backends/sequel/pg_query_methods.rb +45 -20
- data/lib/mobility/backends/sequel/query_methods.rb +5 -0
- data/lib/mobility/backends/sequel/serialized.rb +18 -13
- data/lib/mobility/backends/sequel/serialized/query_methods.rb +10 -7
- data/lib/mobility/backends/sequel/table.rb +1 -1
- data/lib/mobility/backends/sequel/table/query_methods.rb +40 -35
- data/lib/mobility/plugins/cache/translation_cacher.rb +15 -15
- data/lib/mobility/plugins/default.rb +0 -7
- data/lib/mobility/plugins/fallbacks.rb +4 -0
- data/lib/mobility/sequel.rb +11 -5
- data/lib/mobility/sequel/backend_resetter.rb +6 -7
- data/lib/mobility/sequel/column_changes.rb +4 -4
- data/lib/mobility/version.rb +1 -1
- data/lib/rails/generators/mobility/backend_generators/base.rb +4 -0
- data/lib/rails/generators/mobility/backend_generators/table_backend.rb +0 -12
- data/lib/rails/generators/mobility/templates/column_translations.rb +2 -2
- data/lib/rails/generators/mobility/templates/create_string_translations.rb +5 -5
- data/lib/rails/generators/mobility/templates/create_text_translations.rb +5 -5
- data/lib/rails/generators/mobility/templates/initializer.rb +8 -0
- data/lib/rails/generators/mobility/templates/table_migration.rb +2 -3
- data/lib/rails/generators/mobility/templates/table_translations.rb +3 -4
- data/lib/rails/generators/mobility/translations_generator.rb +6 -5
- metadata +2 -3
- metadata.gz.sig +0 -0
- data/lib/mobility/backend/stringify_locale.rb +0 -18
@@ -3,29 +3,32 @@ require "mobility/backends/active_record/query_methods"
|
|
3
3
|
|
4
4
|
module Mobility
|
5
5
|
module Backends
|
6
|
-
|
7
|
-
|
6
|
+
module ActiveRecord
|
7
|
+
class Serialized::QueryMethods < QueryMethods
|
8
|
+
include Backends::Serialized
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
def initialize(attributes, _)
|
11
|
+
super
|
12
|
+
q = self
|
12
13
|
|
13
|
-
|
14
|
-
|
14
|
+
define_method :where! do |opts, *rest|
|
15
|
+
q.check_opts(opts) || super(opts, *rest)
|
16
|
+
end
|
15
17
|
end
|
16
|
-
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
def extended(relation)
|
20
|
+
super
|
21
|
+
q = self
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
mod = Module.new do
|
24
|
+
define_method :not do |opts, *rest|
|
25
|
+
q.check_opts(opts) || super(opts, *rest)
|
26
|
+
end
|
25
27
|
end
|
28
|
+
relation.mobility_where_chain.include(mod)
|
26
29
|
end
|
27
|
-
relation.mobility_where_chain.include(mod)
|
28
30
|
end
|
31
|
+
Serialized.private_constant :QueryMethods
|
29
32
|
end
|
30
33
|
end
|
31
34
|
end
|
@@ -20,6 +20,7 @@ columns to that table.
|
|
20
20
|
|
21
21
|
@example Model with table backend
|
22
22
|
class Post < ActiveRecord::Base
|
23
|
+
extend Mobility
|
23
24
|
translates :title, backend: :table
|
24
25
|
end
|
25
26
|
|
@@ -45,6 +46,7 @@ columns to that table.
|
|
45
46
|
|
46
47
|
@example Model with multiple translation tables
|
47
48
|
class Post < ActiveRecord::Base
|
49
|
+
extend Mobility
|
48
50
|
translates :title, backend: :table, table_name: :post_title_translations, association_name: :title_translations
|
49
51
|
translates :content, backend: :table, table_name: :post_content_translations, association_name: :content_translations
|
50
52
|
end
|
@@ -131,7 +133,8 @@ columns to that table.
|
|
131
133
|
foreign_key: options[:foreign_key],
|
132
134
|
dependent: :destroy,
|
133
135
|
autosave: true,
|
134
|
-
inverse_of: :translated_model
|
136
|
+
inverse_of: :translated_model,
|
137
|
+
extend: TranslationsHasManyExtension
|
135
138
|
|
136
139
|
translation_class.belongs_to :translated_model,
|
137
140
|
class_name: name,
|
@@ -139,7 +142,10 @@ columns to that table.
|
|
139
142
|
inverse_of: association_name,
|
140
143
|
touch: true
|
141
144
|
|
142
|
-
before_save
|
145
|
+
before_save do
|
146
|
+
required_attributes = self.class.translated_attribute_names & translation_class.attribute_names
|
147
|
+
send(association_name).destroy_empty_translations(required_attributes)
|
148
|
+
end
|
143
149
|
|
144
150
|
module_name = "MobilityArTable#{association_name.to_s.camelcase}"
|
145
151
|
unless const_defined?(module_name)
|
@@ -151,26 +157,29 @@ columns to that table.
|
|
151
157
|
end
|
152
158
|
include const_set(module_name, dupable)
|
153
159
|
end
|
154
|
-
|
155
|
-
include DestroyEmptyTranslations
|
156
160
|
end
|
157
161
|
|
158
162
|
setup_query_methods(QueryMethods)
|
159
163
|
|
164
|
+
# Returns translation for a given locale, or builds one if none is present.
|
165
|
+
# @param [Symbol] locale
|
160
166
|
def translation_for(locale, _)
|
161
|
-
translation = translations.
|
167
|
+
translation = translations.in_locale(locale)
|
162
168
|
translation ||= translations.build(locale: locale)
|
163
169
|
translation
|
164
170
|
end
|
165
171
|
|
166
|
-
module
|
167
|
-
|
172
|
+
module TranslationsHasManyExtension
|
173
|
+
# Returns translation in a given locale, or nil if none exist
|
174
|
+
# @param [Symbol, String] locale
|
175
|
+
def in_locale(locale)
|
176
|
+
locale = locale.to_s
|
177
|
+
find { |t| t.locale == locale }
|
178
|
+
end
|
168
179
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
send(association_name).destroy(t) if attrs.map(&t.method(:send)).none?
|
173
|
-
end
|
180
|
+
# Destroys translations with all empty values
|
181
|
+
def destroy_empty_translations(required_attributes)
|
182
|
+
each { |t| destroy(t) if required_attributes.map(&t.method(:send)).none? }
|
174
183
|
end
|
175
184
|
end
|
176
185
|
end
|
@@ -3,104 +3,103 @@ require "mobility/backends/active_record/query_methods"
|
|
3
3
|
|
4
4
|
module Mobility
|
5
5
|
module Backends
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
module ActiveRecord
|
7
|
+
class Table::QueryMethods < QueryMethods
|
8
|
+
def initialize(attributes, association_name: nil, model_class: nil, subclass_name: nil, **options)
|
9
|
+
super
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
@association_name = association_name
|
12
|
+
@translation_class = translation_class = model_class.const_get(subclass_name)
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
define_join_method(association_name, translation_class, **options)
|
15
|
+
define_query_methods(association_name, translation_class, **options)
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
def extended(relation)
|
19
|
+
super
|
20
|
+
association_name = @association_name
|
21
|
+
translation_class = @translation_class
|
22
|
+
q = self
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
24
|
+
mod = Module.new do
|
25
|
+
define_method :not do |opts, *rest|
|
26
|
+
if i18n_keys = q.extract_attributes(opts)
|
27
|
+
opts = opts.with_indifferent_access
|
28
|
+
i18n_keys.each do |attr|
|
29
|
+
opts["#{translation_class.table_name}.#{attr}"] = q.collapse opts.delete(attr)
|
30
|
+
end
|
31
|
+
super(opts, *rest).send("join_#{association_name}")
|
32
|
+
else
|
33
|
+
super(opts, *rest)
|
34
|
+
end
|
31
35
|
end
|
32
36
|
end
|
37
|
+
relation.mobility_where_chain.include(mod)
|
33
38
|
end
|
34
|
-
relation.mobility_where_chain.include(mod)
|
35
|
-
end
|
36
39
|
|
37
|
-
|
40
|
+
private
|
38
41
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
42
|
+
def define_join_method(association_name, translation_class, foreign_key: nil, table_name: nil, **)
|
43
|
+
define_method :"join_#{association_name}" do |**options|
|
44
|
+
if join = joins_values.find { |v| (Arel::Nodes::Join === v) && (v.left.name == table_name.to_s) }
|
45
|
+
return self if (options[:outer_join] || Arel::Nodes::InnerJoin === join)
|
46
|
+
self.joins_values = joins_values - [join]
|
47
|
+
end
|
48
|
+
t = translation_class.arel_table
|
49
|
+
m = arel_table
|
50
|
+
join_type = options[:outer_join] ? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
|
51
|
+
joins(m.join(t, join_type).
|
52
|
+
on(t[foreign_key].eq(m[:id]).
|
53
|
+
and(t[:locale].eq(Mobility.locale))).join_sources)
|
54
|
+
end
|
48
55
|
end
|
49
|
-
end
|
50
56
|
|
51
|
-
|
52
|
-
|
57
|
+
def define_query_methods(association_name, translation_class, **)
|
58
|
+
q = self
|
53
59
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
# We only need an OUTER JOIN if every value is either nil, or an
|
94
|
-
# array with at least one nil value.
|
95
|
-
outer_join: opts.values_at(*i18n_keys).compact.all? { |v| !Array(v).all? }
|
96
|
-
}
|
97
|
-
i18n_keys.each { |attr| opts["#{translation_class.table_name}.#{attr}"] = opts.delete(attr) }
|
98
|
-
super(opts, *rest).send("join_#{association_name}", options)
|
99
|
-
else
|
100
|
-
super(opts, *rest)
|
60
|
+
# Note that Mobility will try to use inner/outer joins appropriate to the query,
|
61
|
+
# so for example:
|
62
|
+
#
|
63
|
+
# Article.where(title: nil, content: nil) #=> OUTER JOIN (all nils)
|
64
|
+
# Article.where(title: "foo", content: nil) #=> INNER JOIN (one non-nil)
|
65
|
+
#
|
66
|
+
# In the first case, if we are in (say) the "en" locale, then we should match articles
|
67
|
+
# that have *no* article_translations with English locales (since no translation is
|
68
|
+
# equivalent to a nil value). If we used an inner join in the first case, an article
|
69
|
+
# with no English translations would be filtered out, so we use an outer join.
|
70
|
+
#
|
71
|
+
# When deciding whether to use an outer or inner join, array-valued
|
72
|
+
# conditions are treated as nil if they have any values.
|
73
|
+
#
|
74
|
+
# Article.where(title: nil, content: ["foo", nil]) #=> OUTER JOIN (all nil or array with nil)
|
75
|
+
# Article.where(title: "foo", content: ["foo", nil]) #=> INNER JOIN (one non-nil)
|
76
|
+
# Article.where(title: ["foo", "bar"], content: ["foo", nil]) #=> INNER JOIN (one non-nil array)
|
77
|
+
#
|
78
|
+
# The logic also applies when a query has more than one where clause.
|
79
|
+
#
|
80
|
+
# Article.where(title: nil).where(content: nil) #=> OUTER JOIN (all nils)
|
81
|
+
# Article.where(title: nil).where(content: "foo") #=> INNER JOIN (one non-nil)
|
82
|
+
# Article.where(title: "foo").where(content: nil) #=> INNER JOIN (one non-nil)
|
83
|
+
#
|
84
|
+
define_method :where! do |opts, *rest|
|
85
|
+
if i18n_keys = q.extract_attributes(opts)
|
86
|
+
opts = opts.with_indifferent_access
|
87
|
+
options = {
|
88
|
+
# We only need an OUTER JOIN if every value is either nil, or an
|
89
|
+
# array with at least one nil value.
|
90
|
+
outer_join: opts.values_at(*i18n_keys).compact.all? { |v| ![*v].all? }
|
91
|
+
}
|
92
|
+
i18n_keys.each do |attr|
|
93
|
+
opts["#{translation_class.table_name}.#{attr}"] = q.collapse opts.delete(attr)
|
94
|
+
end
|
95
|
+
super(opts, *rest).send("join_#{association_name}", options)
|
96
|
+
else
|
97
|
+
super(opts, *rest)
|
98
|
+
end
|
101
99
|
end
|
102
100
|
end
|
103
101
|
end
|
102
|
+
Table.private_constant :QueryMethods
|
104
103
|
end
|
105
104
|
end
|
106
105
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Mobility
|
2
3
|
module Backends
|
3
4
|
=begin
|
@@ -7,6 +8,16 @@ Defines read and write methods that access the value at a key with value
|
|
7
8
|
|
8
9
|
=end
|
9
10
|
module HashValued
|
11
|
+
# @!macro backend_constructor
|
12
|
+
# @option options [Symbol] column_prefix Prefix added to generate column
|
13
|
+
# name from attribute name
|
14
|
+
# @option options [Symbol] column_suffix Suffix added to generate column
|
15
|
+
# name from attribute name
|
16
|
+
def initialize(_model, _attribute, options = {})
|
17
|
+
super
|
18
|
+
@column_affix = "#{options[:column_prefix]}%s#{options[:column_suffix]}"
|
19
|
+
end
|
20
|
+
|
10
21
|
# @!group Backend Accessors
|
11
22
|
#
|
12
23
|
# @!macro backend_reader
|
@@ -24,6 +35,14 @@ Defines read and write methods that access the value at a key with value
|
|
24
35
|
def each_locale
|
25
36
|
translations.each { |l, _| yield l }
|
26
37
|
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def column_name
|
42
|
+
@column_name ||= (@column_affix % attribute)
|
43
|
+
end
|
27
44
|
end
|
45
|
+
|
46
|
+
private_constant :HashValued
|
28
47
|
end
|
29
48
|
end
|
@@ -7,7 +7,9 @@ Stores translations as hash on Postgres hstore column.
|
|
7
7
|
|
8
8
|
==Backend Options
|
9
9
|
|
10
|
-
|
10
|
+
===+column_prefix+ and +column_suffix+
|
11
|
+
|
12
|
+
Prefix and suffix to add to attribute name to generate hstore column name.
|
11
13
|
|
12
14
|
@see Mobility::Backends::ActiveRecord::Hstore
|
13
15
|
@see Mobility::Backends::Sequel::Hstore
|
@@ -6,7 +6,9 @@ Stores translations as hash on Postgres json column.
|
|
6
6
|
|
7
7
|
==Backend Options
|
8
8
|
|
9
|
-
|
9
|
+
===+column_prefix+ and +column_suffix+
|
10
|
+
|
11
|
+
Prefix and suffix to add to attribute name to generate json column name.
|
10
12
|
|
11
13
|
@see Mobility::Backends::ActiveRecord::Json
|
12
14
|
@see Mobility::Backends::Sequel::Json
|
@@ -6,7 +6,9 @@ Stores translations as hash on Postgres jsonb column.
|
|
6
6
|
|
7
7
|
==Backend Options
|
8
8
|
|
9
|
-
|
9
|
+
===+prefix+ and +suffix+
|
10
|
+
|
11
|
+
Prefix and suffix to add to attribute name to generate jsonb column name.
|
10
12
|
|
11
13
|
@see Mobility::Backends::ActiveRecord::Jsonb
|
12
14
|
@see Mobility::Backends::Sequel::Jsonb
|
@@ -15,19 +15,12 @@ string-valued translations (the only difference being the column type of the
|
|
15
15
|
|
16
16
|
==Backend Options
|
17
17
|
|
18
|
-
===+association_name+
|
19
|
-
|
20
|
-
Name of association on model. Defaults to +text_translations+ (if +type+ is
|
21
|
-
+:text+) or +string_translations+ (if +type+ is +:string+). If specified,
|
22
|
-
ensure name does not overlap with other methods on model or with the
|
23
|
-
association name used by other backends on model (otherwise one will overwrite
|
24
|
-
the other).
|
25
|
-
|
26
18
|
===+type+
|
27
19
|
|
28
|
-
Currently, either +:text+ or +:string+ is supported
|
29
|
-
|
30
|
-
|
20
|
+
Currently, either +:text+ or +:string+ is supported, but any value is allowed
|
21
|
+
as long as a corresponding +class_name+ can be found (see below). Determines
|
22
|
+
which class to use for translations, which in turn determines which table to
|
23
|
+
use to store translations (by default +text_translations+ for text type,
|
31
24
|
+string_translations+ for string type).
|
32
25
|
|
33
26
|
===+class_name+
|
@@ -38,6 +31,14 @@ Class to use for translations when defining association. By default,
|
|
38
31
|
for Sequel models). If string is passed in, it will be constantized to get the
|
39
32
|
class.
|
40
33
|
|
34
|
+
===+association_name+
|
35
|
+
|
36
|
+
Name of association on model. Defaults to +<type>_translations+, which will
|
37
|
+
typically be either +:text_translations+ (if +type+ is +:text+) or
|
38
|
+
+:string_translations (if +type+ is +:string+). If specified, ensure name does
|
39
|
+
not overlap with other methods on model or with the association name used by
|
40
|
+
other backends on model (otherwise one will overwrite the other).
|
41
|
+
|
41
42
|
@see Mobility::Backends::ActiveRecord::KeyValue
|
42
43
|
@see Mobility::Backends::Sequel::KeyValue
|
43
44
|
|
@@ -84,11 +85,27 @@ class.
|
|
84
85
|
|
85
86
|
module ClassMethods
|
86
87
|
# @!group Backend Configuration
|
87
|
-
# @option options [Symbol,String] type
|
88
|
-
# @
|
88
|
+
# @option options [Symbol,String] type Column type to use
|
89
|
+
# @option options [Symbol] associaiton_name (:<type>_translations) Name
|
90
|
+
# of association method, defaults to +<type>_translations+
|
91
|
+
# @option options [Symbol] class_name Translation class, defaults to
|
92
|
+
# +Mobility::<ORM>::<type>Translation+
|
93
|
+
# @raise [ArgumentError] if +type+ is not set, and both +class_name+
|
94
|
+
# and +association_name+ are also not set
|
89
95
|
def configure(options)
|
90
|
-
options[:type]
|
91
|
-
|
96
|
+
options[:type] &&= options[:type].to_sym
|
97
|
+
options[:association_name] &&= options[:association_name].to_sym
|
98
|
+
options[:class_name] &&= Util.constantize(options[:class_name])
|
99
|
+
if !(options[:type] || (options[:class_name] && options[:association_name]))
|
100
|
+
# TODO: Remove warning and raise ArgumentError in v1.0
|
101
|
+
warn %{
|
102
|
+
WARNING: In previous versions, the Mobility KeyValue backend defaulted to a
|
103
|
+
text type column, but this behavior is now deprecated and will be removed in
|
104
|
+
the next release. Either explicitly specify the type by passing type: :text in
|
105
|
+
each translated model, or set a default option in your configuration.
|
106
|
+
}
|
107
|
+
options[:type] = :text
|
108
|
+
end
|
92
109
|
end
|
93
110
|
|
94
111
|
# Apply custom processing for plugin
|