mobility 0.3.6 → 0.4.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 +0 -0
- data.tar.gz.sig +0 -0
- data/CHANGELOG.md +21 -0
- data/Gemfile +11 -9
- data/Gemfile.lock +9 -6
- data/README.md +50 -16
- data/lib/mobility.rb +18 -4
- data/lib/mobility/active_record.rb +14 -7
- data/lib/mobility/active_record/uniqueness_validator.rb +17 -3
- data/lib/mobility/attributes.rb +39 -16
- data/lib/mobility/backends/active_record/column/query_methods.rb +12 -11
- data/lib/mobility/backends/active_record/container.rb +116 -0
- data/lib/mobility/backends/active_record/container/query_methods.rb +31 -0
- data/lib/mobility/backends/active_record/hstore/query_methods.rb +7 -54
- data/lib/mobility/backends/active_record/jsonb/query_methods.rb +5 -55
- data/lib/mobility/backends/active_record/key_value.rb +13 -9
- data/lib/mobility/backends/active_record/key_value/query_methods.rb +6 -6
- data/lib/mobility/backends/active_record/pg_query_methods.rb +115 -0
- data/lib/mobility/backends/active_record/query_methods.rb +4 -3
- data/lib/mobility/backends/active_record/serialized/query_methods.rb +6 -5
- data/lib/mobility/backends/active_record/table.rb +18 -3
- data/lib/mobility/backends/active_record/table/query_methods.rb +25 -14
- data/lib/mobility/backends/container.rb +25 -0
- data/lib/mobility/backends/hash_valued.rb +2 -2
- data/lib/mobility/backends/null.rb +2 -2
- data/lib/mobility/backends/sequel/column.rb +2 -2
- data/lib/mobility/backends/sequel/column/query_methods.rb +2 -2
- data/lib/mobility/backends/sequel/container.rb +99 -0
- data/lib/mobility/backends/sequel/container/query_methods.rb +41 -0
- data/lib/mobility/backends/sequel/hstore/query_methods.rb +17 -3
- data/lib/mobility/backends/sequel/jsonb/query_methods.rb +17 -3
- data/lib/mobility/backends/sequel/key_value.rb +2 -1
- data/lib/mobility/backends/sequel/key_value/query_methods.rb +2 -2
- data/lib/mobility/backends/sequel/pg_hash.rb +4 -7
- data/lib/mobility/backends/sequel/pg_query_methods.rb +85 -0
- data/lib/mobility/backends/sequel/query_methods.rb +4 -3
- data/lib/mobility/backends/sequel/serialized/query_methods.rb +4 -3
- data/lib/mobility/backends/sequel/table.rb +1 -1
- data/lib/mobility/backends/sequel/table/query_methods.rb +2 -2
- data/lib/mobility/backends/serialized.rb +5 -7
- data/lib/mobility/configuration.rb +34 -4
- data/lib/mobility/plugins/active_record/dirty.rb +1 -1
- data/lib/mobility/plugins/fallbacks.rb +20 -6
- data/lib/mobility/sequel/column_changes.rb +2 -5
- data/lib/mobility/sequel/hash_initializer.rb +19 -0
- data/lib/mobility/util.rb +0 -2
- data/lib/mobility/version.rb +1 -1
- metadata +11 -4
- metadata.gz.sig +0 -0
- data/lib/mobility/backends/sequel/postgres_query_methods.rb +0 -41
@@ -11,9 +11,6 @@ models. For details see backend-specific subclasses.
|
|
11
11
|
# @param [Array<String>] attributes Translated attributes
|
12
12
|
def initialize(attributes, _)
|
13
13
|
@attributes = attributes
|
14
|
-
@attributes_extractor = lambda do |opts|
|
15
|
-
opts.is_a?(Hash) && (opts.keys.map(&:to_s) & attributes).presence
|
16
|
-
end
|
17
14
|
end
|
18
15
|
|
19
16
|
# @param [ActiveRecord::Relation] relation Relation being extended
|
@@ -30,6 +27,10 @@ models. For details see backend-specific subclasses.
|
|
30
27
|
end
|
31
28
|
end
|
32
29
|
end
|
30
|
+
|
31
|
+
def extract_attributes(opts)
|
32
|
+
opts.is_a?(Hash) && (opts.keys.map(&:to_s) & @attributes).presence
|
33
|
+
end
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
@@ -3,23 +3,24 @@ require "mobility/backends/active_record/query_methods"
|
|
3
3
|
module Mobility
|
4
4
|
module Backends
|
5
5
|
class ActiveRecord::Serialized::QueryMethods < ActiveRecord::QueryMethods
|
6
|
+
include Serialized
|
7
|
+
|
6
8
|
def initialize(attributes, _)
|
7
9
|
super
|
8
|
-
|
9
|
-
opts_checker = @opts_checker = Backends::Serialized.attr_checker(attributes_extractor)
|
10
|
+
q = self
|
10
11
|
|
11
12
|
define_method :where! do |opts, *rest|
|
12
|
-
|
13
|
+
q.check_opts(opts) || super(opts, *rest)
|
13
14
|
end
|
14
15
|
end
|
15
16
|
|
16
17
|
def extended(relation)
|
17
18
|
super
|
18
|
-
|
19
|
+
q = self
|
19
20
|
|
20
21
|
mod = Module.new do
|
21
22
|
define_method :not do |opts, *rest|
|
22
|
-
|
23
|
+
q.check_opts(opts) || super(opts, *rest)
|
23
24
|
end
|
24
25
|
end
|
25
26
|
relation.mobility_where_chain.include(mod)
|
@@ -20,7 +20,7 @@ columns to that table.
|
|
20
20
|
|
21
21
|
@example Model with table backend
|
22
22
|
class Post < ActiveRecord::Base
|
23
|
-
translates :title, backend: :table
|
23
|
+
translates :title, backend: :table
|
24
24
|
end
|
25
25
|
|
26
26
|
post = Post.create(title: "foo")
|
@@ -139,16 +139,20 @@ columns to that table.
|
|
139
139
|
inverse_of: association_name,
|
140
140
|
touch: true
|
141
141
|
|
142
|
+
before_save { mobility_destroy_empty_table_translations(association_name) }
|
143
|
+
|
142
144
|
module_name = "MobilityArTable#{association_name.to_s.camelcase}"
|
143
145
|
unless const_defined?(module_name)
|
144
|
-
|
146
|
+
dupable = Module.new do
|
145
147
|
define_method :initialize_dup do |source|
|
146
148
|
super(source)
|
147
149
|
self.send("#{association_name}=", source.send(association_name).map(&:dup))
|
148
150
|
end
|
149
151
|
end
|
150
|
-
include const_set(module_name,
|
152
|
+
include const_set(module_name, dupable)
|
151
153
|
end
|
154
|
+
|
155
|
+
include DestroyEmptyTranslations
|
152
156
|
end
|
153
157
|
|
154
158
|
setup_query_methods(QueryMethods)
|
@@ -158,6 +162,17 @@ columns to that table.
|
|
158
162
|
translation ||= translations.build(locale: locale)
|
159
163
|
translation
|
160
164
|
end
|
165
|
+
|
166
|
+
module DestroyEmptyTranslations
|
167
|
+
private
|
168
|
+
|
169
|
+
def mobility_destroy_empty_table_translations(association_name)
|
170
|
+
send(association_name).each do |t|
|
171
|
+
attrs = t.attribute_names & self.class.translated_attribute_names
|
172
|
+
send(association_name).destroy(t) if attrs.map(&t.method(:send)).none?
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
161
176
|
end
|
162
177
|
end
|
163
178
|
end
|
@@ -21,13 +21,13 @@ module Mobility
|
|
21
21
|
|
22
22
|
def extended(relation)
|
23
23
|
super
|
24
|
-
association_name
|
25
|
-
|
26
|
-
|
24
|
+
association_name = @association_name
|
25
|
+
translation_class = @translation_class
|
26
|
+
q = self
|
27
27
|
|
28
28
|
mod = Module.new do
|
29
29
|
define_method :not do |opts, *rest|
|
30
|
-
if i18n_keys =
|
30
|
+
if i18n_keys = q.extract_attributes(opts)
|
31
31
|
opts = opts.with_indifferent_access
|
32
32
|
i18n_keys.each { |attr| opts["#{translation_class.table_name}.#{attr}"] = opts.delete(attr) }
|
33
33
|
super(opts, *rest).send("join_#{association_name}")
|
@@ -43,8 +43,7 @@ module Mobility
|
|
43
43
|
|
44
44
|
def define_join_method(association_name, translation_class, foreign_key: nil, table_name: nil, **)
|
45
45
|
define_method :"join_#{association_name}" do |**options|
|
46
|
-
return self if
|
47
|
-
(@__mobility_table_joined ||= []) << table_name
|
46
|
+
return self if joins_values.any? { |v| v.left.name == table_name.to_s }
|
48
47
|
t = translation_class.arel_table
|
49
48
|
m = arel_table
|
50
49
|
join_type = options[:outer_join] ? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
|
@@ -55,23 +54,31 @@ module Mobility
|
|
55
54
|
end
|
56
55
|
|
57
56
|
def define_query_methods(association_name, translation_class, **)
|
58
|
-
|
57
|
+
q = self
|
59
58
|
|
60
59
|
# Note that Mobility will try to use inner/outer joins appropriate to the query,
|
61
60
|
# so for example:
|
62
61
|
#
|
63
|
-
# Article.where(title: nil, content: nil)
|
64
|
-
# Article.where(title: "foo", content: nil)
|
62
|
+
# Article.where(title: nil, content: nil) #=> OUTER JOIN (all nils)
|
63
|
+
# Article.where(title: "foo", content: nil) #=> INNER JOIN (one non-nil)
|
65
64
|
#
|
66
65
|
# In the first case, if we are in (say) the "en" locale, then we should match articles
|
67
66
|
# that have *no* article_translations with English locales (since no translation is
|
68
67
|
# equivalent to a nil value). If we used an inner join in the first case, an article
|
69
68
|
# with no English translations would be filtered out, so we use an outer join.
|
70
69
|
#
|
71
|
-
#
|
72
|
-
#
|
70
|
+
# When deciding whether to use an outer or inner join, array-valued
|
71
|
+
# conditions are treated as nil if they have any values.
|
73
72
|
#
|
74
|
-
# Article.where(title: nil
|
73
|
+
# Article.where(title: nil, content: ["foo", nil]) #=> OUTER JOIN (all nil or array with nil)
|
74
|
+
# Article.where(title: "foo", content: ["foo", nil]) #=> INNER JOIN (one non-nil)
|
75
|
+
# Article.where(title: ["foo", "bar"], content: ["foo", nil]) #=> INNER JOIN (one non-nil array)
|
76
|
+
#
|
77
|
+
# Note that if you call `where` multiple times, you may end up with an
|
78
|
+
# outer join when a (faster) inner join would have worked fine:
|
79
|
+
#
|
80
|
+
# Article.where(title: nil).where(content: "foo") #=> OUTER JOIN
|
81
|
+
# Article.where(title: [nil, "foo"]).where(content: "foo") #=> OUTER JOIN
|
75
82
|
#
|
76
83
|
# In this case, we are searching for a match on the article_translations table
|
77
84
|
# which has a NULL title and a content equal to "foo". Since we need a positive
|
@@ -85,9 +92,13 @@ module Mobility
|
|
85
92
|
# Article.where(title: nil, content: "foo") #=> INNER JOIN
|
86
93
|
#
|
87
94
|
define_method :where! do |opts, *rest|
|
88
|
-
if i18n_keys =
|
95
|
+
if i18n_keys = q.extract_attributes(opts)
|
89
96
|
opts = opts.with_indifferent_access
|
90
|
-
options = {
|
97
|
+
options = {
|
98
|
+
# We only need an OUTER JOIN if every value is either nil, or an
|
99
|
+
# array with at least one nil value.
|
100
|
+
outer_join: opts.values_at(*i18n_keys).compact.all? { |v| !Array.wrap(v).all? }
|
101
|
+
}
|
91
102
|
i18n_keys.each { |attr| opts["#{translation_class.table_name}.#{attr}"] = opts.delete(attr) }
|
92
103
|
super(opts, *rest).send("join_#{association_name}", options)
|
93
104
|
else
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Backends
|
3
|
+
|
4
|
+
=begin
|
5
|
+
|
6
|
+
Stores translations for multiple attributes on a single shared Postgres jsonb
|
7
|
+
column (called a "container").
|
8
|
+
|
9
|
+
==Backend Options
|
10
|
+
|
11
|
+
===+column_name+
|
12
|
+
|
13
|
+
Name of the column for the translations container (where translations are
|
14
|
+
stored).
|
15
|
+
|
16
|
+
@see Mobility::Backends::ActiveRecord::Container
|
17
|
+
@see Mobility::Backends::Sequel::Container
|
18
|
+
@see https://www.postgresql.org/docs/current/static/datatype-json.html PostgreSQL Documentation for JSON Types
|
19
|
+
|
20
|
+
=end
|
21
|
+
module Container
|
22
|
+
extend Backend::OrmDelegator
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -10,12 +10,12 @@ Defines read and write methods that access the value at a key with value
|
|
10
10
|
# @!group Backend Accessors
|
11
11
|
#
|
12
12
|
# @!macro backend_reader
|
13
|
-
def read(locale,
|
13
|
+
def read(locale, _options = nil)
|
14
14
|
translations[locale]
|
15
15
|
end
|
16
16
|
|
17
17
|
# @!macro backend_writer
|
18
|
-
def write(locale, value,
|
18
|
+
def write(locale, value, _options = nil)
|
19
19
|
translations[locale] = value
|
20
20
|
end
|
21
21
|
# @!endgroup
|
@@ -10,10 +10,10 @@ Backend which does absolutely nothing. Mostly for testing purposes.
|
|
10
10
|
|
11
11
|
# @!group Backend Accessors
|
12
12
|
# @return [NilClass]
|
13
|
-
def read(
|
13
|
+
def read(_locale, _options = nil); end
|
14
14
|
|
15
15
|
# @return [NilClass]
|
16
|
-
def write(
|
16
|
+
def write(_locale, _value, _options = nil); end
|
17
17
|
# @!endgroup
|
18
18
|
|
19
19
|
# @!group Backend Configuration
|
@@ -16,14 +16,14 @@ Implements the {Mobility::Backends::Column} backend for Sequel models.
|
|
16
16
|
|
17
17
|
# @!group Backend Accessors
|
18
18
|
# @!macro backend_reader
|
19
|
-
def read(locale,
|
19
|
+
def read(locale, _options = nil)
|
20
20
|
column = column(locale)
|
21
21
|
model[column] if model.columns.include?(column)
|
22
22
|
end
|
23
23
|
|
24
24
|
# @!group Backend Accessors
|
25
25
|
# @!macro backend_writer
|
26
|
-
def write(locale, value,
|
26
|
+
def write(locale, value, _options = nil)
|
27
27
|
column = column(locale)
|
28
28
|
model[column] = value if model.columns.include?(column)
|
29
29
|
end
|
@@ -5,11 +5,11 @@ module Mobility
|
|
5
5
|
class Sequel::Column::QueryMethods < Sequel::QueryMethods
|
6
6
|
def initialize(attributes, _)
|
7
7
|
super
|
8
|
-
|
8
|
+
q = self
|
9
9
|
|
10
10
|
%w[exclude or where].each do |method_name|
|
11
11
|
define_method method_name do |*conds, &block|
|
12
|
-
if keys =
|
12
|
+
if keys = q.extract_attributes(conds.first)
|
13
13
|
cond = conds.first.dup
|
14
14
|
keys.each { |attr| cond[Column.column_name_for(attr)] = cond.delete(attr) }
|
15
15
|
super(cond, &block)
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Mobility
|
2
|
+
module Backends
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Implements the {Mobility::Backends::Container} backend for Sequel models.
|
6
|
+
|
7
|
+
=end
|
8
|
+
class Sequel::Container
|
9
|
+
include Sequel
|
10
|
+
|
11
|
+
require 'mobility/backends/sequel/container/query_methods'
|
12
|
+
|
13
|
+
# @return [Symbol] name of container column
|
14
|
+
attr_reader :column_name
|
15
|
+
|
16
|
+
# @!macro backend_constructor
|
17
|
+
# @option options [Symbol] column_name Name of container column
|
18
|
+
def initialize(model, attribute, options = {})
|
19
|
+
super
|
20
|
+
@column_name = options[:column_name]
|
21
|
+
end
|
22
|
+
|
23
|
+
# @!group Backend Accessors
|
24
|
+
#
|
25
|
+
# @note Translation may be a string, integer, boolean, hash or array
|
26
|
+
# since value is stored on a JSON hash.
|
27
|
+
# @param [Symbol] locale Locale to read
|
28
|
+
# @param [Hash] options
|
29
|
+
# @return [String,Integer,Boolean] Value of translation
|
30
|
+
def read(locale, _ = nil)
|
31
|
+
model_translations(locale)[attribute]
|
32
|
+
end
|
33
|
+
|
34
|
+
# @note Translation may be a string, integer, boolean, hash or array
|
35
|
+
# since value is stored on a JSON hash.
|
36
|
+
# @param [Symbol] locale Locale to write
|
37
|
+
# @param [String,Integer,Boolean] value Value to write
|
38
|
+
# @param [Hash] options
|
39
|
+
# @return [String,Integer,Boolean] Updated value
|
40
|
+
def write(locale, value, _ = nil)
|
41
|
+
set_attribute_translation(locale, value)
|
42
|
+
model_translations(locale)[attribute]
|
43
|
+
end
|
44
|
+
# @!endgroup
|
45
|
+
#
|
46
|
+
# @!group Backend Configuration
|
47
|
+
# @option options [Symbol] column_name (:translations) Name of column on which to store translations
|
48
|
+
def self.configure(options)
|
49
|
+
options[:column_name] ||= :translations
|
50
|
+
end
|
51
|
+
# @!endgroup
|
52
|
+
#
|
53
|
+
# @!macro backend_iterator
|
54
|
+
def each_locale
|
55
|
+
model[column_name].each do |l, _|
|
56
|
+
yield l.to_sym unless read(l).nil?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
setup do |attributes, options|
|
61
|
+
column_name = options[:column_name]
|
62
|
+
before_validation = Module.new do
|
63
|
+
define_method :before_validation do
|
64
|
+
self[column_name].each do |k, v|
|
65
|
+
v.delete_if { |_locale, translation| Util.blank?(translation) }
|
66
|
+
self[column_name].delete(k) if v.empty?
|
67
|
+
end
|
68
|
+
super()
|
69
|
+
end
|
70
|
+
end
|
71
|
+
include before_validation
|
72
|
+
include Mobility::Sequel::HashInitializer.new(column_name)
|
73
|
+
|
74
|
+
plugin :defaults_setter
|
75
|
+
attributes.each { |attribute| default_values[attribute.to_sym] = {} }
|
76
|
+
end
|
77
|
+
|
78
|
+
setup_query_methods(QueryMethods)
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def model_translations(locale)
|
83
|
+
model[column_name][locale.to_s] ||= {}
|
84
|
+
end
|
85
|
+
|
86
|
+
def set_attribute_translation(locale, value)
|
87
|
+
translations = model[column_name] || {}
|
88
|
+
translations[locale.to_s] ||= {}
|
89
|
+
# Explicitly mark translations column as changed if value changed,
|
90
|
+
# otherwise Sequel will not detect it.
|
91
|
+
# TODO: Find a cleaner/easier way to do this.
|
92
|
+
if translations[locale.to_s][attribute] != value
|
93
|
+
model.instance_variable_set(:@changed_columns, model.changed_columns | [column_name])
|
94
|
+
end
|
95
|
+
translations[locale.to_s][attribute] = value
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "mobility/backends/sequel/pg_query_methods"
|
2
|
+
require "mobility/backends/sequel/query_methods"
|
3
|
+
|
4
|
+
Sequel.extension :pg_json, :pg_json_ops
|
5
|
+
|
6
|
+
module Mobility
|
7
|
+
module Backends
|
8
|
+
class Sequel::Container::QueryMethods < Sequel::QueryMethods
|
9
|
+
include Sequel::PgQueryMethods
|
10
|
+
attr_reader :column_name
|
11
|
+
|
12
|
+
def initialize(attributes, options)
|
13
|
+
super
|
14
|
+
column_name = @column_name = options[:column_name]
|
15
|
+
|
16
|
+
define_query_methods
|
17
|
+
|
18
|
+
attributes.each do |attribute|
|
19
|
+
define_method :"first_by_#{attribute}" do |value|
|
20
|
+
where(::Sequel.pg_jsonb_op(column_name)[Mobility.locale.to_s].contains({ attribute => value })).
|
21
|
+
select_all(model.table_name).first
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def contains_value(key, value, locale)
|
29
|
+
build_op(column_name)[locale].contains({ key.to_s => value }.to_json)
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_locale(key, locale)
|
33
|
+
build_op(column_name).has_key?(locale) & build_op(column_name)[locale].has_key?(key.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_op(key)
|
37
|
+
::Sequel.pg_jsonb_op(key)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'mobility/backends/sequel/
|
1
|
+
require 'mobility/backends/sequel/pg_query_methods'
|
2
2
|
require "mobility/backends/sequel/query_methods"
|
3
3
|
|
4
4
|
Sequel.extension :pg_hstore, :pg_hstore_ops
|
@@ -6,12 +6,12 @@ Sequel.extension :pg_hstore, :pg_hstore_ops
|
|
6
6
|
module Mobility
|
7
7
|
module Backends
|
8
8
|
class Sequel::Hstore::QueryMethods < Sequel::QueryMethods
|
9
|
-
include
|
9
|
+
include Sequel::PgQueryMethods
|
10
10
|
|
11
11
|
def initialize(attributes, _)
|
12
12
|
super
|
13
13
|
|
14
|
-
define_query_methods
|
14
|
+
define_query_methods
|
15
15
|
|
16
16
|
attributes.each do |attribute|
|
17
17
|
define_method :"first_by_#{attribute}" do |value|
|
@@ -20,6 +20,20 @@ module Mobility
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def contains_value(key, value, locale)
|
27
|
+
build_op(key).contains(locale => value.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_locale(key, locale)
|
31
|
+
build_op(key).has_key?(locale)
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_op(key)
|
35
|
+
::Sequel.hstore_op(key)
|
36
|
+
end
|
23
37
|
end
|
24
38
|
end
|
25
39
|
end
|