mobility 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -2
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +41 -1
  5. data/Gemfile.lock +3 -58
  6. data/README.md +22 -21
  7. data/lib/mobility.rb +3 -2
  8. data/lib/mobility/accumulator.rb +1 -2
  9. data/lib/mobility/active_model/backend_resetter.rb +1 -1
  10. data/lib/mobility/active_record.rb +12 -9
  11. data/lib/mobility/active_record/backend_resetter.rb +6 -7
  12. data/lib/mobility/active_record/uniqueness_validator.rb +12 -2
  13. data/lib/mobility/adapter.rb +1 -0
  14. data/lib/mobility/attributes.rb +3 -13
  15. data/lib/mobility/backends/active_record/column.rb +1 -0
  16. data/lib/mobility/backends/active_record/column/query_methods.rb +25 -20
  17. data/lib/mobility/backends/active_record/container/json_query_methods.rb +22 -16
  18. data/lib/mobility/backends/active_record/container/jsonb_query_methods.rb +19 -19
  19. data/lib/mobility/backends/active_record/hstore.rb +14 -12
  20. data/lib/mobility/backends/active_record/hstore/query_methods.rb +14 -5
  21. data/lib/mobility/backends/active_record/json.rb +21 -19
  22. data/lib/mobility/backends/active_record/json/query_methods.rb +16 -11
  23. data/lib/mobility/backends/active_record/jsonb.rb +21 -19
  24. data/lib/mobility/backends/active_record/jsonb/query_methods.rb +14 -5
  25. data/lib/mobility/backends/active_record/key_value.rb +9 -9
  26. data/lib/mobility/backends/active_record/key_value/query_methods.rb +53 -46
  27. data/lib/mobility/backends/active_record/pg_hash.rb +29 -25
  28. data/lib/mobility/backends/active_record/pg_query_methods.rb +76 -40
  29. data/lib/mobility/backends/active_record/query_methods.rb +17 -10
  30. data/lib/mobility/backends/active_record/serialized.rb +4 -2
  31. data/lib/mobility/backends/active_record/serialized/query_methods.rb +18 -15
  32. data/lib/mobility/backends/active_record/table.rb +21 -12
  33. data/lib/mobility/backends/active_record/table/query_methods.rb +82 -83
  34. data/lib/mobility/backends/hash_valued.rb +19 -0
  35. data/lib/mobility/backends/hstore.rb +3 -1
  36. data/lib/mobility/backends/json.rb +3 -1
  37. data/lib/mobility/backends/jsonb.rb +3 -1
  38. data/lib/mobility/backends/key_value.rb +32 -15
  39. data/lib/mobility/backends/sequel/column/query_methods.rb +16 -12
  40. data/lib/mobility/backends/sequel/container/json_query_methods.rb +25 -18
  41. data/lib/mobility/backends/sequel/container/jsonb_query_methods.rb +25 -18
  42. data/lib/mobility/backends/sequel/hstore.rb +14 -12
  43. data/lib/mobility/backends/sequel/hstore/query_methods.rb +18 -11
  44. data/lib/mobility/backends/sequel/json.rb +21 -19
  45. data/lib/mobility/backends/sequel/json/query_methods.rb +18 -11
  46. data/lib/mobility/backends/sequel/jsonb.rb +21 -19
  47. data/lib/mobility/backends/sequel/jsonb/query_methods.rb +18 -11
  48. data/lib/mobility/backends/sequel/key_value.rb +10 -11
  49. data/lib/mobility/backends/sequel/key_value/query_methods.rb +39 -34
  50. data/lib/mobility/backends/sequel/pg_hash.rb +37 -25
  51. data/lib/mobility/backends/sequel/pg_query_methods.rb +45 -20
  52. data/lib/mobility/backends/sequel/query_methods.rb +5 -0
  53. data/lib/mobility/backends/sequel/serialized.rb +18 -13
  54. data/lib/mobility/backends/sequel/serialized/query_methods.rb +10 -7
  55. data/lib/mobility/backends/sequel/table.rb +1 -1
  56. data/lib/mobility/backends/sequel/table/query_methods.rb +40 -35
  57. data/lib/mobility/plugins/cache/translation_cacher.rb +15 -15
  58. data/lib/mobility/plugins/default.rb +0 -7
  59. data/lib/mobility/plugins/fallbacks.rb +4 -0
  60. data/lib/mobility/sequel.rb +11 -5
  61. data/lib/mobility/sequel/backend_resetter.rb +6 -7
  62. data/lib/mobility/sequel/column_changes.rb +4 -4
  63. data/lib/mobility/version.rb +1 -1
  64. data/lib/rails/generators/mobility/backend_generators/base.rb +4 -0
  65. data/lib/rails/generators/mobility/backend_generators/table_backend.rb +0 -12
  66. data/lib/rails/generators/mobility/templates/column_translations.rb +2 -2
  67. data/lib/rails/generators/mobility/templates/create_string_translations.rb +5 -5
  68. data/lib/rails/generators/mobility/templates/create_text_translations.rb +5 -5
  69. data/lib/rails/generators/mobility/templates/initializer.rb +8 -0
  70. data/lib/rails/generators/mobility/templates/table_migration.rb +2 -3
  71. data/lib/rails/generators/mobility/templates/table_translations.rb +3 -4
  72. data/lib/rails/generators/mobility/translations_generator.rb +6 -5
  73. metadata +2 -3
  74. metadata.gz.sig +0 -0
  75. 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
- class ActiveRecord::Serialized::QueryMethods < ActiveRecord::QueryMethods
7
- include Serialized
6
+ module ActiveRecord
7
+ class Serialized::QueryMethods < QueryMethods
8
+ include Backends::Serialized
8
9
 
9
- def initialize(attributes, _)
10
- super
11
- q = self
10
+ def initialize(attributes, _)
11
+ super
12
+ q = self
12
13
 
13
- define_method :where! do |opts, *rest|
14
- q.check_opts(opts) || super(opts, *rest)
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
- def extended(relation)
19
- super
20
- q = self
19
+ def extended(relation)
20
+ super
21
+ q = self
21
22
 
22
- mod = Module.new do
23
- define_method :not do |opts, *rest|
24
- q.check_opts(opts) || super(opts, *rest)
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 { mobility_destroy_empty_table_translations(association_name) }
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.find { |t| t.locale == locale.to_s }
167
+ translation = translations.in_locale(locale)
162
168
  translation ||= translations.build(locale: locale)
163
169
  translation
164
170
  end
165
171
 
166
- module DestroyEmptyTranslations
167
- private
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
- 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
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
- class ActiveRecord::Table::QueryMethods < ActiveRecord::QueryMethods
7
- def initialize(attributes, association_name: nil, model_class: nil, subclass_name: nil, **options)
8
- super
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
- @association_name = association_name
11
- @translation_class = translation_class = model_class.const_get(subclass_name)
11
+ @association_name = association_name
12
+ @translation_class = translation_class = model_class.const_get(subclass_name)
12
13
 
13
- define_join_method(association_name, translation_class, **options)
14
- define_query_methods(association_name, translation_class, **options)
15
- end
14
+ define_join_method(association_name, translation_class, **options)
15
+ define_query_methods(association_name, translation_class, **options)
16
+ end
16
17
 
17
- def extended(relation)
18
- super
19
- association_name = @association_name
20
- translation_class = @translation_class
21
- q = self
18
+ def extended(relation)
19
+ super
20
+ association_name = @association_name
21
+ translation_class = @translation_class
22
+ q = self
22
23
 
23
- mod = Module.new do
24
- define_method :not do |opts, *rest|
25
- if i18n_keys = q.extract_attributes(opts)
26
- opts = opts.with_indifferent_access
27
- i18n_keys.each { |attr| opts["#{translation_class.table_name}.#{attr}"] = opts.delete(attr) }
28
- super(opts, *rest).send("join_#{association_name}")
29
- else
30
- super(opts, *rest)
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
- private
40
+ private
38
41
 
39
- def define_join_method(association_name, translation_class, foreign_key: nil, table_name: nil, **)
40
- define_method :"join_#{association_name}" do |**options|
41
- return self if joins_values.any? { |v| v.left.name == table_name.to_s }
42
- t = translation_class.arel_table
43
- m = arel_table
44
- join_type = options[:outer_join] ? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
45
- joins(m.join(t, join_type).
46
- on(t[foreign_key].eq(m[:id]).
47
- and(t[:locale].eq(Mobility.locale))).join_sources)
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
- def define_query_methods(association_name, translation_class, **)
52
- q = self
57
+ def define_query_methods(association_name, translation_class, **)
58
+ q = self
53
59
 
54
- # Note that Mobility will try to use inner/outer joins appropriate to the query,
55
- # so for example:
56
- #
57
- # Article.where(title: nil, content: nil) #=> OUTER JOIN (all nils)
58
- # Article.where(title: "foo", content: nil) #=> INNER JOIN (one non-nil)
59
- #
60
- # In the first case, if we are in (say) the "en" locale, then we should match articles
61
- # that have *no* article_translations with English locales (since no translation is
62
- # equivalent to a nil value). If we used an inner join in the first case, an article
63
- # with no English translations would be filtered out, so we use an outer join.
64
- #
65
- # When deciding whether to use an outer or inner join, array-valued
66
- # conditions are treated as nil if they have any values.
67
- #
68
- # Article.where(title: nil, content: ["foo", nil]) #=> OUTER JOIN (all nil or array with nil)
69
- # Article.where(title: "foo", content: ["foo", nil]) #=> INNER JOIN (one non-nil)
70
- # Article.where(title: ["foo", "bar"], content: ["foo", nil]) #=> INNER JOIN (one non-nil array)
71
- #
72
- # Note that if you call `where` multiple times, you may end up with an
73
- # outer join when a (faster) inner join would have worked fine:
74
- #
75
- # Article.where(title: nil).where(content: "foo") #=> OUTER JOIN
76
- # Article.where(title: [nil, "foo"]).where(content: "foo") #=> OUTER JOIN
77
- #
78
- # In this case, we are searching for a match on the article_translations table
79
- # which has a NULL title and a content equal to "foo". Since we need a positive
80
- # match for content, there must be an English translation on the article, thus
81
- # we can use an inner join. However, Mobility will use an outer join since we don't
82
- # want to modify the existing relation which has already been joined.
83
- #
84
- # To avoid this problem, simply make sure to either order your queries to place nil
85
- # values last, or include all queried attributes in a single `where`:
86
- #
87
- # Article.where(title: nil, content: "foo") #=> INNER JOIN
88
- #
89
- define_method :where! do |opts, *rest|
90
- if i18n_keys = q.extract_attributes(opts)
91
- opts = opts.with_indifferent_access
92
- options = {
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
- This backend has no options.
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
- This backend has no options.
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
- This backend has no options.
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. Determines which class to
29
- use for translations, which in turn determines which table to use to store
30
- translations (by default +text_translations+ for text type,
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 (:text) Column type to use
88
- # @raise [ArgumentError] if type is not either :text or :string
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] = (options[:type] || :text).to_sym
91
- raise ArgumentError, "type must be one of: [text, string]" unless [:text, :string].include?(options[:type])
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