dynamic_scope 0.5.4

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.
@@ -0,0 +1,104 @@
1
+ .dynamic_scope
2
+ fieldset
3
+ position: relative
4
+ margin: 5px 0
5
+ a
6
+ position: relative
7
+ top: 5px
8
+ &:before
9
+ content: image-url('dynamic_scope/fieldset-remove.png')
10
+
11
+ .dynamic_scope_add
12
+ text-decoration: none
13
+ font-size: 90%
14
+ span
15
+ text-decoration: underline
16
+
17
+ .dynamic_scope_add_and_submit
18
+ text-align: right
19
+
20
+ input
21
+ margin-bottom: 5px
22
+
23
+ .dynamic_scope_add
24
+ float: left
25
+
26
+ .dynamic_scope_inputs
27
+ float: left
28
+
29
+ .dynamic_scope_clear
30
+ text-decoration: underline
31
+
32
+ .dynamic_scope_value
33
+ width: 24em
34
+
35
+ .dynamic_scope_column, .dynamic_scope_operator
36
+ width: 9em
37
+
38
+ .dynamic-scope
39
+ .inputs
40
+ fieldset
41
+ position: relative
42
+ margin: 5px 0
43
+
44
+ a.fieldset-remove
45
+ display: inline-block
46
+ position: relative
47
+ vertical-align: middle
48
+ top: 4px
49
+ &:before
50
+ position: relative
51
+ top: 4px
52
+ margin-right: 3px
53
+ content: image-url('dynamic_scope/fieldset-remove.png')
54
+
55
+ .key, .operator, .value
56
+ display: inline-block
57
+ position: relative
58
+ box-sizing: border-box
59
+ vertical-align: top
60
+ margin: 0 6px 0 0
61
+
62
+ .key
63
+ width: 25%
64
+
65
+ .operator
66
+ width: 15%
67
+
68
+ .value
69
+ width: auto
70
+ max-width: 60%
71
+ min-width: 40%
72
+
73
+ div.value
74
+ width: 40%
75
+
76
+ > *
77
+ width: 100%
78
+
79
+ &.type-month
80
+ select.month, input.year
81
+ display: inline-block
82
+ width: calc(50% - 3px)
83
+ vertical-align: top
84
+ select.month
85
+ margin-right: 0
86
+ input.year
87
+ margin-right: 6px
88
+
89
+ .fieldset-add
90
+ text-decoration: none
91
+ span
92
+ text-decoration: underline
93
+
94
+ .clearquery
95
+ text-decoration: underline
96
+
97
+ // ui select styling fixes
98
+ .ui-select-match-item
99
+ padding-bottom: 3px
100
+ padding-top: 3px
101
+ margin-bottom: 1px
102
+
103
+ .ui-select-scope
104
+ height: 1.8em !important
@@ -0,0 +1,24 @@
1
+ de:
2
+ dynamic_scope:
3
+ operator:
4
+ matches: enthält
5
+ does_not_match: stimmt nicht überein
6
+ eq: ist
7
+ not_eq: ist nicht
8
+ gteq: mindestens
9
+ lteq: höchstens
10
+ in: umfasst
11
+ not_in: enthält nicht
12
+ begins_with: beginnt mit
13
+ ends_with: endet mit
14
+ eq_since: seit
15
+ gteq_since: mindestens seit
16
+ lteq_since: höchstens seit
17
+ values:
18
+ yes: Ja
19
+ no: Nein
20
+ add_fieldset: Filter hinzufügen
21
+ add_criteria: Filter hinzufügen
22
+ remove_fieldset: entfernen
23
+ clear: Suche zurücksetzen
24
+ submit: Suche
@@ -0,0 +1,24 @@
1
+ en:
2
+ dynamic_scope:
3
+ operator:
4
+ matches: matches
5
+ does_not_match: does not match
6
+ eq: is
7
+ not_eq: is not
8
+ gteq: at least
9
+ lteq: at most
10
+ in: includes
11
+ not_in: does not include
12
+ begins_with: begins with
13
+ ends_with: ends with
14
+ eq_since: since
15
+ gteq_since: at least since
16
+ lteq_since: at most since
17
+ values:
18
+ yes: Yes
19
+ no: No
20
+ add_fieldset: Add search criteria
21
+ add_criteria: Add search criteria
22
+ remove_fieldset: Remove
23
+ clear: Clear search
24
+ submit: Search
@@ -0,0 +1,24 @@
1
+ fr:
2
+ dynamic_scope:
3
+ operator:
4
+ matches: correspond
5
+ does_not_match: ne correspond pas
6
+ eq: est
7
+ not_eq: n'est pas
8
+ gteq: au moins
9
+ lteq: au maximum
10
+ in: comprend
11
+ not_in: ne comprend pas
12
+ begins_with: commence par
13
+ ends_with: se termine par
14
+ eq_since: depuis
15
+ gteq_since: depuis au moins
16
+ lteq_since: au maximum depuis
17
+ values:
18
+ yes: Oui
19
+ no: Non
20
+ add_fieldset: Ajouter filtre
21
+ add_criteria: Ajouter filtre
22
+ remove_fieldset: Supprimer
23
+ clear: Réinitialiser recherche
24
+ submit: Rechercher
@@ -0,0 +1,24 @@
1
+ it:
2
+ dynamic_scope:
3
+ operator:
4
+ matches: fiammiferi
5
+ does_not_match: non corrisponde
6
+ eq: è
7
+ not_eq: non è
8
+ gteq: almeno
9
+ lteq: al massimo
10
+ in: include
11
+ not_in: non include
12
+ begins_with: inizia con
13
+ ends_with: finisce con
14
+ eq_since: da
15
+ gteq_since: almendo since
16
+ lteq_since: al massimo da
17
+ values:
18
+ yes: Sì
19
+ no: No
20
+ add_fieldset: Aggiungi criteri di ricerca
21
+ add_criteria: Aggiungi criteri di ricerca
22
+ remove_fieldset: Rimuovi
23
+ clear: Ricerca chiara
24
+ submit: Ricerca
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "dynamic_scope/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "dynamic_scope"
7
+ s.version = DynamicScope::Version::VERSION
8
+ s.authors = ["Aleksandr Balakiriev"]
9
+ s.email = ["balakirevs@i.ua"]
10
+ s.homepage = ""
11
+ s.summary = %q{Dynamic scope for rails}
12
+ s.description = %q{Helpers and angular module to make fancy dynamic scopes with arel.}
13
+
14
+ s.required_ruby_version = ">= 3.2.0"
15
+
16
+ s.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ s.bindir = "exe"
20
+ s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ s.add_dependency('sass-rails', '>= 6.0')
24
+ s.add_dependency('coffee-rails', '>= 5.0')
25
+ s.add_dependency('i18n-js', '~> 3.9.2')
26
+
27
+ s.add_development_dependency "rspec", "~> 3.13"
28
+ s.add_development_dependency "rails", ">= 8.0"
29
+ end
@@ -0,0 +1,17 @@
1
+ require 'dynamic_scope/class_methods'
2
+
3
+ module DynamicScope
4
+ module ActsAsDynamicallyScopable
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def acts_as_dynamically_scopable(config)
9
+ class_eval do
10
+ include DynamicScope::ActiveRecord::Concern
11
+ end
12
+
13
+ self.dynamic_scope_config = config
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,71 @@
1
+ module DynamicScope
2
+ module ClassMethods
3
+ def dynamic_scope_is_empty?(params)
4
+ params.blank? || params.select{|key, value| value["value"].present? }.blank?
5
+ end
6
+
7
+ private
8
+
9
+ def dynamic_scope_to_ransack(params)
10
+ return {} unless params
11
+ params = params.deep_dup
12
+ g = 1
13
+ q = {
14
+ 'g' => {
15
+ '0' => {
16
+ 'm' => 'and'
17
+ }
18
+ }
19
+ }
20
+ params.each_pair do |index, args|
21
+ if args['name'] == 'any_field'
22
+ q['g'][(g+=1).to_s] = {
23
+ 'm' => 'or',
24
+ 'c' => {}
25
+ }
26
+ self.dynamic_scope_columns.each_with_index do |col, i|
27
+ q['g'][g.to_s]['c'][i.to_s] = {}
28
+ c = q['g'][g.to_s]['c'][i.to_s]
29
+ if col.first == :string && col.last != :any_field
30
+ c['a'] = { '0' => { 'name' => col.last.to_s } }
31
+ c['v'] = { '0' => { 'value' => args['value'] } }
32
+ end
33
+ c['p'] = args['operator']
34
+ end
35
+ else
36
+ col = dyn_scope_column(args['name'])
37
+ if col and col[0] == :age and col[2].present?
38
+ args = age_to_date(args)
39
+ args['name'] = col[2].to_s
40
+ end
41
+ q['g']['0']['c'] ||= {}
42
+ size = q['g']['0']['c'].size
43
+ q['g']['0']['c'][size.to_s] = {}
44
+ c = q['g']['0']['c'][size.to_s]
45
+
46
+ c['a'] = { '0' => { 'name' => args['name'] } }
47
+ c['p'] = args['operator']
48
+ c['v'] = { '0' => { 'value' => args['value'] } }
49
+ end
50
+ end
51
+ q
52
+ end
53
+
54
+ def dyn_scope_column(name)
55
+ dynamic_scope_columns.select{|c| c[1] == name.to_sym}.first
56
+ end
57
+
58
+ def age_to_date(args)
59
+ args['operator'] =
60
+ case args['operator']
61
+ when 'gt' then 'lt'
62
+ when 'lt' then 'gt'
63
+ else args['operator']
64
+ end
65
+ if args['value'].match(/^[0-9]+$/)
66
+ args['value'] = (Date.today - args['value'].to_i.years).to_s
67
+ end
68
+ args
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,19 @@
1
+ require 'active_support/concern'
2
+
3
+ module DynamicScope::Concerns::ActiveRecord
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :dynamic_scope_config
8
+ end
9
+
10
+ module ClassMethods
11
+ def dynamic_scope(query, config = nil)
12
+ config ||= self.dynamic_scope_config
13
+ scope = DynamicScope::Processor.new(all, query, config).scope
14
+ all.joins(scope.joins_values).distinct
15
+ .includes(scope.includes_values)
16
+ .and(scope.distinct)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,2 @@
1
+ module DynamicScope::Concerns
2
+ end
@@ -0,0 +1,7 @@
1
+ module DynamicScope
2
+ if defined?(::Rails::Engine)
3
+ class Engine < ::Rails::Engine #@private
4
+ isolate_namespace DynamicScope
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,89 @@
1
+ require 'dynamic_scope/query'
2
+
3
+ # Processor for dynamic scope.
4
+ #
5
+ # usage:
6
+ #
7
+ # params = {
8
+ # '0' => {key: 'id', operator: 'eq', value: '1'},
9
+ # }
10
+ #
11
+ # config = {
12
+ # id: {type: :integer}
13
+ # }
14
+ # processor = DynamicScope::Processor.new(Model.all, params, config)
15
+ # Model.all.merge(processor.scope)
16
+ #
17
+ #
18
+ # Config format:
19
+ # {
20
+ # key: {
21
+ # # Required
22
+ # type: :integer || :string || :enum || :datetime,
23
+ #
24
+ # # Required for type :enum
25
+ # values: ['value_1', 'value_2'],
26
+ #
27
+ # # Optional: uses key OR given value to determine which scope to call
28
+ # # scope receives (operator, value) as arguments.
29
+ # scope: true || :some_scope_name,
30
+ #
31
+ # # Opional: adds "OR :attribute IS NULL" to SQL statement
32
+ # # when operator is negative.
33
+ # null: true,
34
+ #
35
+ # # Optional: uses given value if attribute_name differs from key
36
+ # attribute_name: :some_column,
37
+ #
38
+ # # Optional: use when attribute resides in some other model,
39
+ # # uses rails relations to generate joins.
40
+ # # If given, the scope option affects this class.
41
+ # relation: :parent_model,
42
+ # relation: {parent_model: :granparent_model},
43
+ #
44
+ # # Optional: use to modify value coming from params
45
+ # value: lambda{|value| "#{value}"}
46
+ # }
47
+ #
48
+ # Params format:
49
+ # {
50
+ # '0' => {
51
+ # # Required: hash key of config
52
+ # key: 'key',
53
+ #
54
+ # # Required:
55
+ # # Supported operators are defined in DynamicScope::Query::OPERATORS
56
+ # operator: 'operator',
57
+ #
58
+ # # Required:
59
+ # value: 'value'
60
+ #
61
+ # # Opional: adds "OR :attribute IS NULL" to SQL statement
62
+ # null: true,
63
+ # }
64
+ # }
65
+ class DynamicScope::Processor
66
+ attr_reader :params, :config
67
+
68
+ def initialize(scope, params, config)
69
+ @scope = scope
70
+ @params = params
71
+ @config = config
72
+ end
73
+
74
+ def scope
75
+ @params.values.inject(@scope) do |memo, query|
76
+ if (query_scope = DynamicScope::Query.new(@scope, query, @config).scope)
77
+ if query_scope
78
+ memo = memo.joins(query_scope.joins_values) if query_scope.try(:joins_values).present?
79
+ memo = memo.includes(query_scope.includes_values) if query_scope.try(:includes_values).present?
80
+ memo.distinct.and(query_scope.distinct).distinct
81
+ else
82
+ memo.distinct
83
+ end
84
+ else
85
+ memo.distinct
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,205 @@
1
+ require 'dynamic_scope/query_helpers'
2
+
3
+ class DynamicScope::Query
4
+ include DynamicScope::QueryHelpers
5
+
6
+ class UnallowedOperator < ::Exception
7
+ end
8
+
9
+ class InvalidValue < ::Exception
10
+ end
11
+
12
+ class MissingRequiredKey < ::Exception
13
+ end
14
+
15
+ class UnsupportedType < ::Exception
16
+ end
17
+
18
+ REQUIRED_KEYS = [:key, :operator, :value].freeze
19
+
20
+ SUPPORTED_TYPES = [:integer, :string, :enum, :year, :month, :date, :date_future, :datetime, :boolean].freeze
21
+
22
+ OPERATORS = {
23
+ integer: [:eq, :not_eq, :gteq, :lteq, :in, :not_in],
24
+ string: [:matches, :does_not_match, :begins_with, :ends_with, :eq, :not_eq],
25
+ enum: [:eq, :not_eq, :in, :not_in],
26
+ year: [:eq, :gteq, :lteq],
27
+ month: [:eq, :gteq, :lteq, :eq_since, :gteq_since, :lteq_since],
28
+ date: [:eq, :gteq, :lteq],
29
+ date_future: [:eq, :gteq, :lteq],
30
+ datetime: [:eq, :gteq, :lteq],
31
+ boolean: [:eq, :not_eq]
32
+ }.freeze
33
+
34
+ NEGATIVE_OPERATORS = [:not_eq, :does_not_match, :not_in].freeze
35
+
36
+ attr_accessor :query, :config
37
+
38
+ def initialize(_scope, _query, _config)
39
+ @query = _query
40
+ @config = _config
41
+
42
+ check_query_for_missing_keys!
43
+
44
+ @scope = query_config[:relation].present? ? _scope.joins(query_config[:relation]) : _scope
45
+ end
46
+
47
+ def query_config
48
+ @config[query_key]
49
+ end
50
+
51
+ def arel_query
52
+ arel = scope_klass.arel_table[attribute_name]
53
+ aq = arel.send(operator_for_arel, value_for_arel)
54
+ if query_param_truthy?(@query[:null] || query_config[:null]) and NEGATIVE_OPERATORS.include?(operator)
55
+ aq = aq.or(arel.eq(nil))
56
+ end
57
+ aq
58
+ end
59
+
60
+ def scope
61
+ return nil if value.blank?
62
+ if custom_scope?
63
+ scope_klass.send(custom_scope_name, operator, value)
64
+ else
65
+ @scope.where(arel_query)
66
+ end
67
+ end
68
+
69
+ def operator
70
+ oper = @query[:operator].to_sym
71
+ if OPERATORS[type].include?(oper)
72
+ oper
73
+ else
74
+ raise UnallowedOperator.new(@query), "Query (#{@query}) has unallowed operator: #{oper}"
75
+ end
76
+ end
77
+
78
+ def attribute_name
79
+ (query_config[:attribute_name] || query_key).to_sym
80
+ end
81
+
82
+ def value
83
+ val = query_config[:value].is_a?(Proc) ? query_config[:value].call(@query[:value]) : @query[:value]
84
+ return nil if val.nil? || val.to_s.strip.length == 0
85
+ case type
86
+ when :string
87
+ val.to_s
88
+ when :integer
89
+ val.to_s.include?(',') ? val.split(',').map(&:to_i) : val.to_i
90
+ when :enum
91
+ val.split(',').each do |v|
92
+ unless allowed_values.include?(v)
93
+ raise InvalidValue.new(@query), "Query (#{@query}) has invalid value: #{v}"
94
+ end
95
+ end
96
+ val
97
+ when :year
98
+ val.to_s
99
+ when :month
100
+ val.to_s
101
+ when :date, :date_future
102
+ Date.parse(val.to_s)
103
+ when :datetime
104
+ DateTime.parse(val.to_s)
105
+ when :boolean
106
+ if ['1', 'true', true].include?(val)
107
+ true
108
+ elsif ['0', 'false', false].include?(val)
109
+ false
110
+ end
111
+ end
112
+ end
113
+
114
+ def value_for_arel
115
+ case type
116
+ when :string
117
+ if [:matches, :does_not_match].include?(operator)
118
+ "%#{value}%"
119
+ elsif [:begins_with].include?(operator)
120
+ "#{value}%"
121
+ elsif [:ends_with].include?(operator)
122
+ "%#{value}"
123
+ else
124
+ value
125
+ end
126
+ when :enum
127
+ if [:in, :not_in].include?(operator)
128
+ value.split(',')
129
+ else
130
+ value
131
+ end
132
+ else
133
+ value
134
+ end
135
+ end
136
+
137
+ def type
138
+ t = query_config[:type].to_sym
139
+ unless SUPPORTED_TYPES.include?(t)
140
+ raise UnsupportedType.new(query_config), "Config (#{query_config}) has unsupported type: #{t}"
141
+ end
142
+ t
143
+ end
144
+
145
+ def scope_klass
146
+ @scope_klass ||=
147
+ if query_config[:relation]
148
+ resolve_relation(@scope.klass, query_config[:relation])
149
+ else
150
+ @scope.klass
151
+ end
152
+ end
153
+
154
+ def operator_for_arel
155
+ {
156
+ begins_with: :matches,
157
+ ends_with: :matches
158
+ }[operator] || operator
159
+ end
160
+
161
+ private
162
+ def resolve_relation(klass, relation)
163
+ if relation.is_a?(Hash)
164
+ resolve_relation(klass.reflect_on_association(relation.first.first).klass, relation.first.last)
165
+ elsif relation.is_a?(Symbol)
166
+ klass.reflect_on_association(relation).klass
167
+ else
168
+ raise
169
+ end
170
+ end
171
+
172
+ def allowed_values
173
+ query_config[:values] || []
174
+ end
175
+
176
+ def check_query_for_missing_keys!
177
+ if missing_required_keys.length > 0
178
+ raise MissingRequiredKey.new(@query), "Query (#{@query}) has missing keys: #{missing_required_keys}"
179
+ end
180
+ end
181
+
182
+ def missing_required_keys
183
+ (REQUIRED_KEYS - @query.keys.map(&:to_sym))
184
+ end
185
+
186
+ def query_key
187
+ @query[:key].to_sym
188
+ end
189
+
190
+ def custom_scope?
191
+ query_config[:scope].present?
192
+ end
193
+
194
+ def custom_scope_name
195
+ if query_config[:scope].is_a?(Symbol) || query_config[:scope].is_a?(String)
196
+ query_config[:scope]
197
+ else
198
+ query_key
199
+ end
200
+ end
201
+
202
+ def use_outer_join?
203
+ NEGATIVE_OPERATORS.include?(operator)
204
+ end
205
+ end
@@ -0,0 +1,11 @@
1
+ module DynamicScope::QueryHelpers
2
+ TRUTHY_VALUES = ['1', 'true', true]
3
+
4
+ def query_param_truthy?(value)
5
+ TRUTHY_VALUES.include?(value)
6
+ end
7
+
8
+ def query_param_falsy?(value)
9
+ !query_param_truthy?(value)
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module DynamicScope
2
+ module Version
3
+ VERSION = '0.5.4'
4
+ end
5
+ end