collate 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +267 -0
- data/Rakefile +10 -0
- data/circle.yml +25 -0
- data/collate.gemspec +40 -0
- data/deploy.sh +6 -0
- data/lib/collate.rb +6 -0
- data/lib/collate/active_record_extension.rb +175 -0
- data/lib/collate/engine.rb +14 -0
- data/lib/collate/filter.rb +118 -0
- data/lib/collate/version.rb +3 -0
- metadata +205 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 46caae4fc83f2556a1c2835b951dfb89dd57d557
|
4
|
+
data.tar.gz: 99278ff861ac8cfb50ba7cdcb1b6ca2940c311ea
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 070f0611c0adea44f057b62f37d49d93fff18c04ef5681c7638b0491c178908dac17200f5b5966e9f1c10f1971291b7c59b37e4958d35acaa52e42d127531554
|
7
|
+
data.tar.gz: 5db48b737d9c51c0be11283d74e5e7108f3e1546012c54d8dc969e00e6ff899e60510f3f21e305a8c6ef7d0178d2211b7798c002856fc2f04683f10f3ecf0f5c
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Nicholas Page
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
# Collate
|
2
|
+
|
3
|
+
[![CircleCI](https://img.shields.io/circleci/project/github/trackingboard/collate.svg)](https://circleci.com/gh/trackingboard/collate)
|
4
|
+
[![Coveralls](https://img.shields.io/coveralls/trackingboard/collate.svg)](https://coveralls.io/github/trackingboard/collate?branch=master)
|
5
|
+
[![Gem](https://img.shields.io/gem/v/collate.svg)]()
|
6
|
+
[![Gem](https://img.shields.io/gem/dt/collate.svg)]()
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
```gem install collate```
|
11
|
+
|
12
|
+
or with bundler in your Gemfile:
|
13
|
+
|
14
|
+
```gem 'collate'```
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
This gem currently only supports PostgreSQL.
|
19
|
+
|
20
|
+
To use collate in a model, include several collation definitions. The first argument is the name of the database column to use in the query. The simplest example looks like this:
|
21
|
+
|
22
|
+
```
|
23
|
+
collate_on :name
|
24
|
+
```
|
25
|
+
|
26
|
+
This will add a filter to the model that will grab all records where the ```name``` column equals the parameter that is passed in.
|
27
|
+
|
28
|
+
### Operators
|
29
|
+
|
30
|
+
You can currently collate using multiple types of operators. To specify an operator to collate on, you can pass in the keyword argument ```operator```, like this:
|
31
|
+
|
32
|
+
```
|
33
|
+
collate_on :name, operator: :ilike
|
34
|
+
```
|
35
|
+
|
36
|
+
Translates to:
|
37
|
+
|
38
|
+
```
|
39
|
+
WHERE name ILIKE ?
|
40
|
+
```
|
41
|
+
|
42
|
+
Here are the currently available operators:
|
43
|
+
|
44
|
+
| Operator | Behavior |
|
45
|
+
|:------------------|:--------------------|
|
46
|
+
| ```:eq``` | ```field = ?``` |
|
47
|
+
| ```:ilike``` | ```field ILIKE ?``` |
|
48
|
+
| ```:in``` | ```field IN (?)``` |
|
49
|
+
| ```:le``` | ```field <= ?``` |
|
50
|
+
| ```:ge``` | ```field >= ?``` |
|
51
|
+
| ```:null``` | ```field IS NULL``` |
|
52
|
+
| ```:contains``` | ```field @> ?``` |
|
53
|
+
| ```:present?``` | ```field = ?``` |
|
54
|
+
| ```:&``` | ```field & ?``` |
|
55
|
+
|
56
|
+
### Field Transformations
|
57
|
+
|
58
|
+
Field transformations are database functions applied to a field before the operator is used to compare it with the value. Field transformations are passed in as an array of tuples, where the first element in the tuple is the symbol for the transfomation, and the second element is the first argument to the database function.
|
59
|
+
|
60
|
+
For example:
|
61
|
+
|
62
|
+
```
|
63
|
+
collate_on :name, field_transformations: [[:split, ' ']]
|
64
|
+
```
|
65
|
+
|
66
|
+
This would translate to this PostgreSQL query:
|
67
|
+
|
68
|
+
```
|
69
|
+
WHERE string_to_array(name, ' ') = ?
|
70
|
+
```
|
71
|
+
|
72
|
+
Here are the available field transformations:
|
73
|
+
|
74
|
+
| Transformation | Behavior |
|
75
|
+
|:-------------------------|:--------------------|
|
76
|
+
| ```:date_difference``` | ```date_difference(arg1, field)``` |
|
77
|
+
| ```:date_part``` | ```date_part(arg1, field)``` |
|
78
|
+
| ```:array_agg``` | ```array_agg(field)``` |
|
79
|
+
| ```:downcase``` | ```lower(field)``` |
|
80
|
+
| ```:split``` | ```string_to_array(field, arg1)``` |
|
81
|
+
| ```:array_length``` | ```array_length(field, arg1)``` |
|
82
|
+
|
83
|
+
These transformations can also be chained together on the same filter. They are applied in the order they appear in the array that is passed in.
|
84
|
+
|
85
|
+
For example:
|
86
|
+
|
87
|
+
```
|
88
|
+
collate_on :name, field_transformations: [[:split, ' '], [:array_length, 1]]
|
89
|
+
```
|
90
|
+
|
91
|
+
Translates to this PostgreSQL query:
|
92
|
+
|
93
|
+
```
|
94
|
+
WHERE array_length(string_to_array(name, ' '), 1) = ?
|
95
|
+
```
|
96
|
+
|
97
|
+
### Value Transformations
|
98
|
+
|
99
|
+
Value transformations are functions applied to the user-supplied value before it is passed to the database query. They are passed in the same way as the field transmorations, as an array of tuples.
|
100
|
+
|
101
|
+
For example:
|
102
|
+
|
103
|
+
```
|
104
|
+
collate_on :name, value_transformations: [[:join, ', ']]
|
105
|
+
```
|
106
|
+
|
107
|
+
Translates to the following code:
|
108
|
+
|
109
|
+
```
|
110
|
+
value = value.join(', ')
|
111
|
+
ar_rel = ar_rel.where("name = ?", value)
|
112
|
+
```
|
113
|
+
|
114
|
+
Here are the available value transformations:
|
115
|
+
|
116
|
+
| Transformation | Behavior |
|
117
|
+
|:-------------------------|:--------------------|
|
118
|
+
| ```:join``` | ```value = value.join(arg1)``` |
|
119
|
+
| ```:downcase``` | ```value = value.downcase``` |
|
120
|
+
| ```:string_part``` | ```value = "%#{value}%"``` |
|
121
|
+
|
122
|
+
### Additional Arguments
|
123
|
+
|
124
|
+
There are many other additional arguments you can initialize a filter with. Here is a list of all of them:
|
125
|
+
|
126
|
+
#### Label
|
127
|
+
--------------
|
128
|
+
```
|
129
|
+
collate_on :name, label: 'Character Name'
|
130
|
+
```
|
131
|
+
|
132
|
+
This argument will overwrite the default label for the filter, which is ```field.to_s.titleize```
|
133
|
+
|
134
|
+
#### Not
|
135
|
+
--------------
|
136
|
+
```
|
137
|
+
collate_on :name, not: true
|
138
|
+
```
|
139
|
+
|
140
|
+
This argument causes the entire query to be surrounded by a NOT(). The above, for example, translates to this PostgreSQL query:
|
141
|
+
|
142
|
+
```
|
143
|
+
WHERE NOT(name = ?)
|
144
|
+
```
|
145
|
+
|
146
|
+
#### Having
|
147
|
+
--------------
|
148
|
+
```
|
149
|
+
collate_on :name, having: true
|
150
|
+
```
|
151
|
+
|
152
|
+
This argument tells the gem to use ```having``` instead of ```where``` in the ActiveRecord query. The above example then becomes:
|
153
|
+
|
154
|
+
```
|
155
|
+
HAVING name = ?
|
156
|
+
```
|
157
|
+
|
158
|
+
#### Joins
|
159
|
+
--------------
|
160
|
+
```
|
161
|
+
collate_on 'genres.id', operator: :in, joins: [:genres, :movies => [:people]]
|
162
|
+
```
|
163
|
+
|
164
|
+
This argument tells the gem to use the ActiveRecord ```joins``` method with the value passed in. You can pass in an array of values, and it will evaluate each one in succession. The above code would then run this before any query is evaluated:
|
165
|
+
|
166
|
+
```
|
167
|
+
ar_rel = ar_rel.joins(:genres)
|
168
|
+
ar_rel = ar_rel.joins(:movies => [:people])
|
169
|
+
```
|
170
|
+
|
171
|
+
#### Joins_prefix
|
172
|
+
--------------
|
173
|
+
```
|
174
|
+
collate_on 'select_genres.id', operator: :in, joins: [:genres], joins_prefix: 'select_'
|
175
|
+
```
|
176
|
+
|
177
|
+
This argument will tell the gem to join in the relations specified in the ```joins``` argument, but to prefix all table names with the prefix specified. The above code would then translate to the following PostgreSQL query:
|
178
|
+
|
179
|
+
```
|
180
|
+
INNER JOIN genres AS select_genres ON ...
|
181
|
+
```
|
182
|
+
|
183
|
+
#### Component
|
184
|
+
--------------
|
185
|
+
```
|
186
|
+
collate_on 'genres.id', operator: :in, component: {load_records: true}
|
187
|
+
```
|
188
|
+
|
189
|
+
This argument is used for rendering in the views. Currently the gem does not have helper methods for the views, but when those are added, that is how you would set the various options.
|
190
|
+
|
191
|
+
### The View
|
192
|
+
|
193
|
+
To know the ```params``` key to use for each filter, you need to look at the filter's ```param_key``` method value. The filters are organized in a hierarchal scheme in the class variable ```collate_filters``` for the model you included the DSL on.
|
194
|
+
|
195
|
+
Here is a small example of a few filters:
|
196
|
+
|
197
|
+
```
|
198
|
+
class Person < ActiveRecord::Base
|
199
|
+
collate_on :name, operator: :ilike
|
200
|
+
collate_on :birthday, operator: :le, label: 'Birthday Before'
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
And here is how ```Person.collate_filters``` would look like:
|
205
|
+
|
206
|
+
```
|
207
|
+
{
|
208
|
+
:main=> {
|
209
|
+
:label=>"Main",
|
210
|
+
:filters=> [
|
211
|
+
#<Collate::Filter
|
212
|
+
@base_model_table_name="people",
|
213
|
+
@component={:type=>"string"},
|
214
|
+
@field="people.name",
|
215
|
+
@field_transformations={},
|
216
|
+
@grouping=nil,
|
217
|
+
@html_id="1people_name",
|
218
|
+
@joins=nil,
|
219
|
+
@label="Name",
|
220
|
+
@operator=:ilike,
|
221
|
+
@value_transformations={}>
|
222
|
+
,
|
223
|
+
#<Collate::Filter
|
224
|
+
@base_model_table_name="people",
|
225
|
+
@component={:type=>"string"},
|
226
|
+
@field="people.birthday",
|
227
|
+
@field_transformations={},
|
228
|
+
@grouping=nil,
|
229
|
+
@html_id="1people_name",
|
230
|
+
@joins=nil,
|
231
|
+
@label="Birthday Before",
|
232
|
+
@operator=:le,
|
233
|
+
@value_transformations={}>
|
234
|
+
]
|
235
|
+
}
|
236
|
+
}
|
237
|
+
```
|
238
|
+
|
239
|
+
In order to use this in a view, you could have some HAML like this:
|
240
|
+
|
241
|
+
```
|
242
|
+
= form_tag '', :method => :get do
|
243
|
+
- Person.collate_filters.each do |group_key, group|
|
244
|
+
- filters = group[:filters]
|
245
|
+
- filters.each do |filter|
|
246
|
+
- case filter.component[:type]
|
247
|
+
- when "string"
|
248
|
+
= filter.label
|
249
|
+
%br
|
250
|
+
= text_field_tag filter.param_key, params[filter.param_key], id: "#{filter.html_id}", style:'width:100%'
|
251
|
+
|
252
|
+
```
|
253
|
+
|
254
|
+
This will ensure that the keys that the inputs are submitted with match the parameter key that the gem is expecting for that specific filter.
|
255
|
+
|
256
|
+
## Contributing
|
257
|
+
|
258
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/trackingboard/collate.
|
259
|
+
|
260
|
+
1. Fork.
|
261
|
+
2. Branch.
|
262
|
+
3. Pull Request your feature branch or fix.
|
263
|
+
4. 🍕
|
264
|
+
|
265
|
+
## License
|
266
|
+
|
267
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/circle.yml
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
dependencies:
|
2
|
+
override:
|
3
|
+
- rvm --default use ruby-2.3.3
|
4
|
+
- gem install bundler
|
5
|
+
- bundle
|
6
|
+
cache_directories:
|
7
|
+
- "/opt/circleci/.rvm/gems/ruby-2.3.3"
|
8
|
+
|
9
|
+
database:
|
10
|
+
override:
|
11
|
+
- psql -c 'create database collate_test;' -U postgres
|
12
|
+
|
13
|
+
test:
|
14
|
+
override:
|
15
|
+
- RAILS_ENV=test bundle exec rake test
|
16
|
+
|
17
|
+
deployment:
|
18
|
+
release:
|
19
|
+
tag: /^([0-9]+\.{0,1}){1,3}(\-([a-z0-9]+\.{0,1})+){0,1}(\+(build\.{0,1}){0,1}([a-z0-9]+\.{0,1}){0,}){0,1}$/
|
20
|
+
commands:
|
21
|
+
- gem build collate.gemspec
|
22
|
+
- chmod +x deploy.sh
|
23
|
+
- sh deploy.sh
|
24
|
+
- chmod 0600 ~/.gem/credentials
|
25
|
+
- gem push collate-${CIRCLE_TAG}.gem
|
data/collate.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'collate/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "collate"
|
8
|
+
spec.version = Collate::VERSION
|
9
|
+
spec.authors = ["Nicholas Page", "Colleen McGuckin"]
|
10
|
+
spec.email = ["npage85@gmail.com", "colleenmcguckin@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Facilitates the filtering of ActiveRecord models using simplified DSL.}
|
13
|
+
spec.description = %q{Add some DSL to your model, then run a single method in your controller, and your model now accepts filtering through the parameters.}
|
14
|
+
spec.homepage = "https://github.com/trackingboard/collate"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
if spec.respond_to?(:metadata)
|
18
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
19
|
+
else
|
20
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
21
|
+
"public gem pushes."
|
22
|
+
end
|
23
|
+
|
24
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
f.match(%r{^(test|spec|features|app|coverage|config|log)/})
|
26
|
+
end
|
27
|
+
spec.bindir = "exe"
|
28
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = ["lib"]
|
30
|
+
|
31
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
32
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
33
|
+
spec.add_development_dependency "rails", "~> 4.2", ">= 4.2.6"
|
34
|
+
spec.add_development_dependency "pg", "~> 0.18.4"
|
35
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
36
|
+
spec.add_development_dependency "pry-rescue", "~> 1.4", ">= 1.4.2"
|
37
|
+
spec.add_development_dependency "simplecov", "~> 0.12.0"
|
38
|
+
spec.add_development_dependency "coveralls", "~> 0.8.15"
|
39
|
+
spec.add_development_dependency "haml", "~> 4.0", ">= 4.0.7"
|
40
|
+
end
|
data/deploy.sh
ADDED
data/lib/collate.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
require_relative 'filter'
|
2
|
+
|
3
|
+
module Collate
|
4
|
+
module ActiveRecordExtension
|
5
|
+
|
6
|
+
def collate_on field, opts={}
|
7
|
+
initialize_collate
|
8
|
+
|
9
|
+
self.collate_filters[self.default_group] ||= {filters: []}.merge(self.group_options)
|
10
|
+
|
11
|
+
self.collate_filters[self.default_group][:filters] << Collate::Filter.new(field, opts.merge({base_model_table_name: self.table_name}))
|
12
|
+
end
|
13
|
+
|
14
|
+
def collate_group name, **opts, &blk
|
15
|
+
initialize_collate
|
16
|
+
|
17
|
+
opts[:label] ||= name.to_s.titleize
|
18
|
+
self.group_options = opts
|
19
|
+
self.default_group = name
|
20
|
+
blk.call
|
21
|
+
end
|
22
|
+
|
23
|
+
def collate params
|
24
|
+
ar_rel = self.all
|
25
|
+
|
26
|
+
self.collate_filters.each do |group_key, group|
|
27
|
+
group[:filters].each do |filter|
|
28
|
+
if params[filter.param_key].present? || params["#{filter.param_key}[]"].present?
|
29
|
+
ar_rel = apply_filter(ar_rel, filter, params[filter.param_key] || params["#{filter.param_key}[]"])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
ar_rel
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def initialize_collate
|
40
|
+
if !self.method_defined? :collate_filters
|
41
|
+
class << self
|
42
|
+
attr_accessor :collate_filters, :default_group, :group_options
|
43
|
+
end
|
44
|
+
end
|
45
|
+
self.collate_filters ||= {}
|
46
|
+
self.default_group ||= :main
|
47
|
+
self.group_options ||= {}
|
48
|
+
end
|
49
|
+
|
50
|
+
def apply_filter ar_rel, filter, filter_value
|
51
|
+
if filter.joins
|
52
|
+
filter.joins.each do |join|
|
53
|
+
ar_rel = if filter.joins_prefix
|
54
|
+
prefix_index = 0
|
55
|
+
original_query = ar_rel.model.unscoped.joins(join).to_sql
|
56
|
+
|
57
|
+
previous_replacements = {}
|
58
|
+
new_query = original_query.split('INNER JOIN').drop(1).map do |chunk|
|
59
|
+
table_name = /([\"'])(?:\\\1|.)*?\1/.match(chunk)[0].gsub('"','')
|
60
|
+
|
61
|
+
if previous_replacements.has_key?("\"#{table_name}\"")
|
62
|
+
previous_replacements.delete("\"#{table_name}\"")
|
63
|
+
prefix_index += 1
|
64
|
+
|
65
|
+
check_alias_match = /(?<="#{table_name}" ")[^"]*(?=")/.match(chunk)
|
66
|
+
if check_alias_match
|
67
|
+
chunk = chunk.partition(check_alias_match[0]).drop(1).join('').prepend('"').gsub(check_alias_match[0], "#{table_name}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
previous_replacements.each do |the_match, replacement|
|
72
|
+
chunk = chunk.gsub(the_match, replacement)
|
73
|
+
end
|
74
|
+
|
75
|
+
replaced = chunk.gsub("\"#{table_name}\"", "\"#{filter.joins_prefix[prefix_index]}#{table_name}\"")
|
76
|
+
|
77
|
+
previous_replacements["\"#{table_name}\""] = "\"#{filter.joins_prefix[prefix_index]}#{table_name}\""
|
78
|
+
|
79
|
+
"\"#{table_name}\" AS #{replaced}"
|
80
|
+
end.join(' INNER JOIN ').prepend('INNER JOIN ')
|
81
|
+
|
82
|
+
ar_rel.joins(new_query)
|
83
|
+
else
|
84
|
+
ar_rel.joins(join)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
ar_rel = ar_rel.group(filter.grouping) if filter.grouping
|
90
|
+
|
91
|
+
field_query = filter.field
|
92
|
+
|
93
|
+
filter.field_transformations.each do |ft|
|
94
|
+
transformation = ft
|
95
|
+
transformation = ft[0] if !transformation.is_a? Symbol
|
96
|
+
field_query = case transformation
|
97
|
+
when :date_difference
|
98
|
+
"age(#{ft[1]}, #{field_query})"
|
99
|
+
when :date_part
|
100
|
+
"date_part('#{ft[1]}', #{field_query})"
|
101
|
+
when :array_agg
|
102
|
+
"array_agg(#{field_query})"
|
103
|
+
when :downcase
|
104
|
+
"lower(#{field_query})"
|
105
|
+
when :split
|
106
|
+
"string_to_array(#{field_query}, '#{ft[1]}')"
|
107
|
+
when :array_length
|
108
|
+
"array_length(#{field_query}, '#{ft[1]}')"
|
109
|
+
else
|
110
|
+
field_query
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if filter.component[:load_records]
|
115
|
+
results = filter.component[:load_record_model].constantize.where("#{filter.component[:load_record_field]} IN (?)", filter_value)
|
116
|
+
|
117
|
+
filter.component[:values] = results.map{ |r| {id: r.id, text: r.name} }
|
118
|
+
end
|
119
|
+
|
120
|
+
if filter.component[:tags]
|
121
|
+
filter.component[:values] = filter_value.map { |v| {id: v, text: v} }
|
122
|
+
end
|
123
|
+
|
124
|
+
filter.value_transformations.each do |vt|
|
125
|
+
transformation = vt
|
126
|
+
transformation = vt[0] if !transformation.is_a? Symbol
|
127
|
+
|
128
|
+
filter_value = case transformation
|
129
|
+
when :join
|
130
|
+
"{#{filter_value.join(', ')}}"
|
131
|
+
when :downcase
|
132
|
+
filter_value.downcase
|
133
|
+
when :string_part
|
134
|
+
"%#{filter_value}%"
|
135
|
+
else
|
136
|
+
filter_value
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
ar_method = filter.having ? "having" : "where"
|
141
|
+
|
142
|
+
query_string = case filter.operator
|
143
|
+
when :eq
|
144
|
+
"#{field_query} = ?"
|
145
|
+
when :ilike
|
146
|
+
"#{field_query} ILIKE ?"
|
147
|
+
when :in
|
148
|
+
"#{field_query} IN (?)"
|
149
|
+
when :le
|
150
|
+
"#{field_query} <= ?"
|
151
|
+
when :ge
|
152
|
+
"#{field_query} >= ?"
|
153
|
+
when :null
|
154
|
+
"#{field_query} IS NULL"
|
155
|
+
when :contains
|
156
|
+
"#{field_query} @> ?"
|
157
|
+
when :present?
|
158
|
+
"#{field_query} = true"
|
159
|
+
when :&
|
160
|
+
"#{field_query} && ?"
|
161
|
+
else
|
162
|
+
""
|
163
|
+
end
|
164
|
+
|
165
|
+
query_string = "NOT(#{query_string})" if filter.not
|
166
|
+
|
167
|
+
ar_rel = if query_string.include?('?')
|
168
|
+
ar_rel.send(ar_method, query_string, filter_value)
|
169
|
+
else
|
170
|
+
ar_rel.send(ar_method, query_string)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Collate
|
2
|
+
class Filter
|
3
|
+
OPERATORS = [:eq, :ilike, :in, :le, :ge, :null, :contains, :present?, :&]
|
4
|
+
|
5
|
+
FIELD_TRANSFORMATIONS = [:date_difference, :date_part, :array_agg, :downcase, :split, :array_length]
|
6
|
+
AGGREGATE_TRANSFORMATIONS = [:array_agg]
|
7
|
+
VALUE_TRANSFORMATIONS = [:join, :downcase, :string_part]
|
8
|
+
|
9
|
+
attr_accessor :field, :operator, :base_model_table_name, :field_transformations, :label,
|
10
|
+
:component, :joins, :value_transformations, :grouping, :html_id, :having,
|
11
|
+
:joins_prefix, :not
|
12
|
+
|
13
|
+
def initialize(field, opt={})
|
14
|
+
opt.each do |field, value|
|
15
|
+
self.send("#{field}=", value)
|
16
|
+
end
|
17
|
+
|
18
|
+
self.component ||= {}
|
19
|
+
|
20
|
+
self.field = field
|
21
|
+
self.label ||= field.to_s.titleize
|
22
|
+
self.operator ||= if field.to_s.last(3) == '.id' || field.to_s.last(3) == '_id'
|
23
|
+
:in
|
24
|
+
elsif self.component[:type] == 'checkboxgroup'
|
25
|
+
:in
|
26
|
+
else
|
27
|
+
:eq
|
28
|
+
end
|
29
|
+
self.field = "#{base_model_table_name}.#{field}" if field.is_a? Symbol
|
30
|
+
self.field_transformations ||= {}
|
31
|
+
self.value_transformations ||= {}
|
32
|
+
|
33
|
+
self.html_id ||= param_key.gsub('{','').gsub('}','').gsub('.','_')
|
34
|
+
|
35
|
+
field_parts = self.field.to_s.partition('.')
|
36
|
+
table_name = field_parts[0]
|
37
|
+
field_selector = field_parts[2]
|
38
|
+
|
39
|
+
self.component = if self.operator == :in
|
40
|
+
self.component.reverse_merge({type: 'select', multiple: true, values: []})
|
41
|
+
elsif self.operator == :null || self.operator == :present?
|
42
|
+
self.component.reverse_merge({type: 'checkbox'})
|
43
|
+
elsif self.component[:tags]
|
44
|
+
self.component.reverse_merge({type: 'select', multiple: true})
|
45
|
+
else
|
46
|
+
self.component.reverse_merge({type: 'string'})
|
47
|
+
end
|
48
|
+
|
49
|
+
if self.component[:load_records]
|
50
|
+
model_name = if field_selector.last(3) == '_id'
|
51
|
+
field_selector.chomp(field_selector.last(3))
|
52
|
+
elsif self.field.to_s.last(3) == '.id'
|
53
|
+
table_name.singularize
|
54
|
+
else
|
55
|
+
table_name.singularize
|
56
|
+
end
|
57
|
+
|
58
|
+
self.component[:load_record_model] ||= model_name.titleize
|
59
|
+
self.component[:load_record_field] ||= "id"
|
60
|
+
self.component[:load_record_route] ||= "/#{model_name.pluralize}.json"
|
61
|
+
end
|
62
|
+
|
63
|
+
self.joins ||= if table_name != base_model_table_name
|
64
|
+
join_name = table_name.to_sym
|
65
|
+
base_model_table_name.singularize.titleize.constantize.reflect_on_all_associations.each do |assoc|
|
66
|
+
|
67
|
+
join_name = assoc.name if assoc.plural_name == table_name
|
68
|
+
end
|
69
|
+
|
70
|
+
[join_name.to_sym]
|
71
|
+
end
|
72
|
+
self.joins_prefix = [self.joins_prefix] if self.joins_prefix.is_a? String
|
73
|
+
|
74
|
+
if !opt.has_key?(:having) && (field_transformations.to_a.flatten & AGGREGATE_TRANSFORMATIONS).any?
|
75
|
+
self.having = true
|
76
|
+
end
|
77
|
+
|
78
|
+
if self.value_transformations.empty? && self.operator == :ilike
|
79
|
+
self.value_transformations = [:string_part]
|
80
|
+
end
|
81
|
+
|
82
|
+
self.grouping ||= if self.having
|
83
|
+
"#{base_model_table_name}.id"
|
84
|
+
end
|
85
|
+
|
86
|
+
if self.component[:values]
|
87
|
+
self.component[:values] = if self.component[:values].is_a?(Array) && self.component[:values].all? { |item| item.is_a? String }
|
88
|
+
self.component[:values].map { |s| {id: s, text: s.titleize} }
|
89
|
+
elsif self.component[:values].is_a?(Array) && self.component[:values].all? { |item| item.is_a? Symbol }
|
90
|
+
self.component[:values].map { |s| {id: s, text: s.to_s.titleize} }
|
91
|
+
elsif self.component[:values].respond_to?(:<) && self.component[:values] < ActiveRecord::Base
|
92
|
+
self.component[:values].table_exists? ? self.component[:values].all.map { |m| {id: m.id, text: m.name} } : []
|
93
|
+
else
|
94
|
+
self.component[:values]
|
95
|
+
end
|
96
|
+
elsif component[:tags]
|
97
|
+
self.component[:values] = []
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def param_key
|
102
|
+
key = ""
|
103
|
+
field_transformations.each do |ft|
|
104
|
+
transformation = ft
|
105
|
+
transformation = ft[0] if !transformation.is_a? Symbol
|
106
|
+
key += FIELD_TRANSFORMATIONS.index(transformation).to_s
|
107
|
+
end
|
108
|
+
key += OPERATORS.index(operator).to_s
|
109
|
+
value_transformations.each do |vt|
|
110
|
+
transformation = vt
|
111
|
+
transformation = vt[0] if !transformation.is_a? Symbol
|
112
|
+
key += VALUE_TRANSFORMATIONS.index(transformation).to_s
|
113
|
+
end
|
114
|
+
|
115
|
+
"{#{key}}#{field}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
metadata
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: collate
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nicholas Page
|
8
|
+
- Colleen McGuckin
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2017-02-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.14'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.14'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rake
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '10.0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '10.0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rails
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '4.2'
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: 4.2.6
|
52
|
+
type: :development
|
53
|
+
prerelease: false
|
54
|
+
version_requirements: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - "~>"
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '4.2'
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 4.2.6
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: pg
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.18.4
|
69
|
+
type: :development
|
70
|
+
prerelease: false
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.18.4
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: minitest
|
78
|
+
requirement: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.0'
|
83
|
+
type: :development
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '5.0'
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: pry-rescue
|
92
|
+
requirement: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.4'
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 1.4.2
|
100
|
+
type: :development
|
101
|
+
prerelease: false
|
102
|
+
version_requirements: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - "~>"
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '1.4'
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 1.4.2
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: simplecov
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 0.12.0
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 0.12.0
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: coveralls
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 0.8.15
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: 0.8.15
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: haml
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '4.0'
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: 4.0.7
|
148
|
+
type: :development
|
149
|
+
prerelease: false
|
150
|
+
version_requirements: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - "~>"
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '4.0'
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: 4.0.7
|
158
|
+
description: Add some DSL to your model, then run a single method in your controller,
|
159
|
+
and your model now accepts filtering through the parameters.
|
160
|
+
email:
|
161
|
+
- npage85@gmail.com
|
162
|
+
- colleenmcguckin@gmail.com
|
163
|
+
executables: []
|
164
|
+
extensions: []
|
165
|
+
extra_rdoc_files: []
|
166
|
+
files:
|
167
|
+
- ".gitignore"
|
168
|
+
- Gemfile
|
169
|
+
- LICENSE.txt
|
170
|
+
- README.md
|
171
|
+
- Rakefile
|
172
|
+
- circle.yml
|
173
|
+
- collate.gemspec
|
174
|
+
- deploy.sh
|
175
|
+
- lib/collate.rb
|
176
|
+
- lib/collate/active_record_extension.rb
|
177
|
+
- lib/collate/engine.rb
|
178
|
+
- lib/collate/filter.rb
|
179
|
+
- lib/collate/version.rb
|
180
|
+
homepage: https://github.com/trackingboard/collate
|
181
|
+
licenses:
|
182
|
+
- MIT
|
183
|
+
metadata:
|
184
|
+
allowed_push_host: https://rubygems.org
|
185
|
+
post_install_message:
|
186
|
+
rdoc_options: []
|
187
|
+
require_paths:
|
188
|
+
- lib
|
189
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - ">="
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '0'
|
194
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
195
|
+
requirements:
|
196
|
+
- - ">="
|
197
|
+
- !ruby/object:Gem::Version
|
198
|
+
version: '0'
|
199
|
+
requirements: []
|
200
|
+
rubyforge_project:
|
201
|
+
rubygems_version: 2.5.2
|
202
|
+
signing_key:
|
203
|
+
specification_version: 4
|
204
|
+
summary: Facilitates the filtering of ActiveRecord models using simplified DSL.
|
205
|
+
test_files: []
|