mobility 0.3.6 → 0.4.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
- 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
|