search_object 0.1
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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +283 -0
- data/Rakefile +6 -0
- data/example/.gitignore +16 -0
- data/example/.rspec +1 -0
- data/example/Gemfile +11 -0
- data/example/README.md +34 -0
- data/example/Rakefile +6 -0
- data/example/app/assets/javascripts/application.js +5 -0
- data/example/app/assets/stylesheets/application.css.scss +40 -0
- data/example/app/assets/stylesheets/reset.css +43 -0
- data/example/app/controllers/application_controller.rb +3 -0
- data/example/app/controllers/posts_controller.rb +5 -0
- data/example/app/models/.keep +0 -0
- data/example/app/models/post.rb +13 -0
- data/example/app/models/post_search.rb +44 -0
- data/example/app/models/user.rb +5 -0
- data/example/app/views/layouts/application.html.slim +12 -0
- data/example/app/views/posts/index.html.slim +48 -0
- data/example/bin/bundle +3 -0
- data/example/bin/rails +4 -0
- data/example/bin/rake +4 -0
- data/example/config.ru +4 -0
- data/example/config/application.rb +27 -0
- data/example/config/boot.rb +4 -0
- data/example/config/database.yml +12 -0
- data/example/config/environment.rb +5 -0
- data/example/config/environments/development.rb +29 -0
- data/example/config/environments/test.rb +37 -0
- data/example/config/initializers/filter_parameter_logging.rb +4 -0
- data/example/config/initializers/secret_token.rb +12 -0
- data/example/config/initializers/session_store.rb +3 -0
- data/example/config/initializers/wrap_parameters.rb +14 -0
- data/example/config/routes.rb +3 -0
- data/example/db/migrate/20131102130117_create_users.rb +10 -0
- data/example/db/migrate/20131102130413_create_posts.rb +18 -0
- data/example/db/schema.rb +40 -0
- data/example/db/seeds.rb +37 -0
- data/example/log/.keep +0 -0
- data/example/screenshot.png +0 -0
- data/example/spec/models/post_search_spec.rb +81 -0
- data/example/spec/spec_helper.rb +19 -0
- data/lib/search_object.rb +20 -0
- data/lib/search_object/base.rb +64 -0
- data/lib/search_object/helper.rb +36 -0
- data/lib/search_object/plugin/kaminari.rb +18 -0
- data/lib/search_object/plugin/model.rb +16 -0
- data/lib/search_object/plugin/paging.rb +42 -0
- data/lib/search_object/plugin/sorting.rb +54 -0
- data/lib/search_object/plugin/will_paginate.rb +17 -0
- data/lib/search_object/search.rb +26 -0
- data/lib/search_object/version.rb +3 -0
- data/search_object.gemspec +31 -0
- data/spec/search_object/base_spec.rb +237 -0
- data/spec/search_object/helper_spec.rb +30 -0
- data/spec/search_object/plugin/kaminari_spec.rb +50 -0
- data/spec/search_object/plugin/model_spec.rb +22 -0
- data/spec/search_object/plugin/paging_spec.rb +43 -0
- data/spec/search_object/plugin/sorting_spec.rb +139 -0
- data/spec/search_object/plugin/will_paginate_spec.rb +51 -0
- data/spec/search_object/search_spec.rb +72 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/spec_helper_active_record.rb +19 -0
- data/spec/support/kaminari_setup.rb +7 -0
- metadata +292 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Radoslav Stankov
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
[](https://codeclimate.com/github/RStankov/SearchObject)
|
2
|
+
[](http://travis-ci.org/RStankov/SearchObject)
|
3
|
+
[](https://coveralls.io/r/RStankov/SearchObject)
|
4
|
+
|
5
|
+
# SearchObject
|
6
|
+
|
7
|
+
In many of my projects I've needed an object that performs several fairly complicated queries. Or, just some multi-field search forms. Most times I hand-coded them, but they would get complicated over time when other concerns were added like sorting, pagination and so on. So I decided to abstract this away and created ```SearchObject```, a DSL for creating such objects.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'search_object'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install search_object
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Just include the ```SearchObject.module``` and define your search options:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class PostSearch
|
29
|
+
include SearchObject.module
|
30
|
+
|
31
|
+
# Use .all (Rails4) or .scoped (Rails3) for ActiveRecord objects
|
32
|
+
scope { Post.all }
|
33
|
+
|
34
|
+
option :name { |scope, value| scope.where name: value }
|
35
|
+
option :created_at { |scope, dates| scope.created_after dates }
|
36
|
+
option :published, false { |scope, value| value ? scope.unopened : scope.opened }
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
Then you can just search the given scope:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
search = PostSearch.new(params[:filters])
|
44
|
+
|
45
|
+
# accessing search options
|
46
|
+
search.name # => name option
|
47
|
+
search.created_at # => created at option
|
48
|
+
|
49
|
+
# accessing results
|
50
|
+
search.count # => number of found results
|
51
|
+
search.results? # => is there any results found
|
52
|
+
search.results # => found results
|
53
|
+
|
54
|
+
# params for url generations
|
55
|
+
search.params # => option values
|
56
|
+
search.params opened: false # => overwrites the 'opened' option
|
57
|
+
```
|
58
|
+
|
59
|
+
## Example
|
60
|
+
|
61
|
+
You can find example of most imporatant features and plugins - [here](https://github.com/RStankov/SearchObject/tree/master/example).
|
62
|
+
|
63
|
+
## Plugins
|
64
|
+
|
65
|
+
```SearchObject``` support plugins, which are passed to ```SearchObject.module``` method.
|
66
|
+
|
67
|
+
Plugins are just plain Ruby modules, which are included with ```SearchObject.module```. They are located under ```SearchObject::Plugin``` module.
|
68
|
+
|
69
|
+
### Paginate plugin
|
70
|
+
|
71
|
+
Really simple paginte plugin, which uses the plain ```.limit``` and ```.offset``` methods.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
class ProductSearch
|
75
|
+
include SearchObject.module(:paging)
|
76
|
+
|
77
|
+
scope { Product.all }
|
78
|
+
|
79
|
+
option :name
|
80
|
+
option :category_name
|
81
|
+
|
82
|
+
# per page defaults to 25
|
83
|
+
# you can also overwrite per_page method
|
84
|
+
per_page 10
|
85
|
+
end
|
86
|
+
|
87
|
+
search = ProductSearch.new(params[:filters], params[:page]) # page number is required
|
88
|
+
search.page # => page number
|
89
|
+
search.per_page # => per page (10)
|
90
|
+
search.results # => paginated page results
|
91
|
+
```
|
92
|
+
|
93
|
+
Of course if you want more sophisticated pagination plugins you can use:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
include SearchObject.module(:will_paginate)
|
97
|
+
include SearchObject.module(:kaminari)
|
98
|
+
```
|
99
|
+
|
100
|
+
### Model plugin
|
101
|
+
|
102
|
+
Extends your search object with ```ActiveModel```, so you can use it in rails forms.
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class ProductSearch
|
106
|
+
include SearchObject.module(:model)
|
107
|
+
|
108
|
+
scope { Product.all }
|
109
|
+
|
110
|
+
option :name
|
111
|
+
option :category_name
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
```erb
|
116
|
+
# in some view:
|
117
|
+
<%= form_for ProductSearch.new do |form| %>
|
118
|
+
<% form.label :name %>
|
119
|
+
<% form.text_field :name %>
|
120
|
+
<% form.label :category_name %>
|
121
|
+
<% form.text_field :category_name %>
|
122
|
+
<% end %>
|
123
|
+
```
|
124
|
+
|
125
|
+
### Sorting plugin
|
126
|
+
|
127
|
+
Fixing the pain of dealing with sorting attributes and directions.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class ProductSearch
|
131
|
+
include SearchObject.module(:sorting)
|
132
|
+
|
133
|
+
scope { Product.all }
|
134
|
+
|
135
|
+
sort_by :name, :price
|
136
|
+
end
|
137
|
+
|
138
|
+
search = ProductSearch.new(sort: 'price desc')
|
139
|
+
search.results # => Product sorted my price DESC
|
140
|
+
search.sort_attribute # => 'price'
|
141
|
+
search.sort_direction # => 'desc'
|
142
|
+
|
143
|
+
# Smart sort checking
|
144
|
+
search.sort?('price') # => true
|
145
|
+
search.sort?('price desc') # => true
|
146
|
+
search.sort?('price asc') # => false
|
147
|
+
|
148
|
+
# Helpers for dealing with reversing sort direction
|
149
|
+
search.reverted_sort_direction # => 'asc'
|
150
|
+
search.sort_direction_for('price') # => 'asc'
|
151
|
+
search.sort_direction_for('name') # => 'desc'
|
152
|
+
|
153
|
+
# Params for sorting links
|
154
|
+
search.sort_params_for('name')
|
155
|
+
|
156
|
+
```
|
157
|
+
|
158
|
+
## Tips & Tricks
|
159
|
+
|
160
|
+
### Passing scope as argument
|
161
|
+
|
162
|
+
``` ruby
|
163
|
+
class ProductSearch
|
164
|
+
include SearchObject.module
|
165
|
+
|
166
|
+
scope :name
|
167
|
+
end
|
168
|
+
|
169
|
+
# first arguments is treated as scope (if no scope option is provided)
|
170
|
+
search = ProductSearch.new(Product.visible, params[:f])
|
171
|
+
search.results #=> products
|
172
|
+
```
|
173
|
+
|
174
|
+
|
175
|
+
### Handling nil options
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
class ProductSearch
|
179
|
+
include SearchObject.module
|
180
|
+
|
181
|
+
scope { Product.all }
|
182
|
+
|
183
|
+
# nil values returned from option blocks are ignored
|
184
|
+
scope :sold { |scope, value| scope.sold if value }
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
### Default option block
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
class ProductSearch
|
192
|
+
include SearchObject.module
|
193
|
+
|
194
|
+
scope { Product.all }
|
195
|
+
|
196
|
+
option :name # automaticly applies => { |scope, value| scope.where name: value unless value.blank? }
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
### Using instance method in option blocks
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
class ProductSearch
|
204
|
+
include SearchObject.module
|
205
|
+
|
206
|
+
scope { Product.all }
|
207
|
+
|
208
|
+
option :date { |scope, value| scope.by_date parse_dates(value) }
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
def parse_dates(date_string)
|
213
|
+
# some "magic" method to parse dates
|
214
|
+
end
|
215
|
+
end
|
216
|
+
```
|
217
|
+
|
218
|
+
### Active Record is not required at all
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
class ProductSearch
|
222
|
+
include SearchObject.module
|
223
|
+
|
224
|
+
scope { RemoteEndpoint.fetch_product_as_hashes }
|
225
|
+
|
226
|
+
option :name { |scope, value| scope.select { |product| product[:name] == value } }
|
227
|
+
option :category { |scope, value| scope.select { |product| product[:category] == value } }
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
### Overwriting methods
|
232
|
+
|
233
|
+
You can have fine grained scope, by overwriting ```initialize``` method:
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
class ProductSearch
|
237
|
+
include SearchObject.module
|
238
|
+
|
239
|
+
option :name
|
240
|
+
option :category_name
|
241
|
+
|
242
|
+
def initialize(user, filters)
|
243
|
+
super Product.visible_to(user), filters
|
244
|
+
end
|
245
|
+
end
|
246
|
+
```
|
247
|
+
|
248
|
+
Or you can add simple pagination by overwriting both ```initialize``` and ```fetch_results``` (used for fetching results):
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
class ProductSearch
|
252
|
+
include SearchObject.module
|
253
|
+
|
254
|
+
scope { Product.all }
|
255
|
+
|
256
|
+
option :name
|
257
|
+
option :category_name
|
258
|
+
|
259
|
+
attr_reader :page
|
260
|
+
|
261
|
+
def initialize(filters = {}, page = 0)
|
262
|
+
super filters
|
263
|
+
@page = page.to_i.abc
|
264
|
+
end
|
265
|
+
|
266
|
+
def fetch_results
|
267
|
+
super.paginate page: @page
|
268
|
+
end
|
269
|
+
end
|
270
|
+
```
|
271
|
+
|
272
|
+
## Contributing
|
273
|
+
|
274
|
+
1. Fork it
|
275
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
276
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
277
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
278
|
+
5. Run the tests (`rake`)
|
279
|
+
6. Create new Pull Request
|
280
|
+
|
281
|
+
## License
|
282
|
+
|
283
|
+
**[MIT License](https://github.com/RStankov/SearchObject/blob/master/LICENSE.txt)**
|
data/Rakefile
ADDED
data/example/.gitignore
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
2
|
+
#
|
3
|
+
# If you find yourself ignoring temporary files generated by your text editor
|
4
|
+
# or operating system, you probably want to add a global ignore instead:
|
5
|
+
# git config --global core.excludesfile '~/.gitignore_global'
|
6
|
+
|
7
|
+
# Ignore bundler config.
|
8
|
+
/.bundle
|
9
|
+
|
10
|
+
# Ignore the default SQLite database.
|
11
|
+
/db/*.sqlite3
|
12
|
+
/db/*.sqlite3-journal
|
13
|
+
|
14
|
+
# Ignore all logfiles and tempfiles.
|
15
|
+
/log/*.log
|
16
|
+
/tmp
|
data/example/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/example/Gemfile
ADDED
data/example/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# SearchObject example Rails application
|
2
|
+
|
3
|
+
This is example application showing, one of the possible usages of ```SearchObject```. It showcases the following features:
|
4
|
+
|
5
|
+
* Basic search object functionality
|
6
|
+
* Default options
|
7
|
+
* Using private method helpers in options
|
8
|
+
* Plugins: model, sorting, will_paginate
|
9
|
+
|
10
|
+
## Interesting files:
|
11
|
+
|
12
|
+
* [PostsController](https://github.com/RStankov/SearchObject/blob/master/example/app/controllers/posts_controller.rb)
|
13
|
+
* [PostSearch](https://github.com/RStankov/SearchObject/blob/master/example/app/models/post_search.rb)
|
14
|
+
* [posts/index.html.slim](https://github.com/RStankov/SearchObject/blob/master/example/app/views/posts/index.html.slim)
|
15
|
+
* [PostSearch spec](https://github.com/RStankov/SearchObject/blob/master/example/spec/models/post_search_spec.rb)
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
```
|
20
|
+
gem install bundler
|
21
|
+
bundle install
|
22
|
+
rake db:create
|
23
|
+
rake db:migrate
|
24
|
+
rake db:seed
|
25
|
+
|
26
|
+
rails server
|
27
|
+
```
|
28
|
+
|
29
|
+
From there just visit: [localhost:3000/](http://localhost:3000/)
|
30
|
+
|
31
|
+
|
32
|
+
## Screenshot
|
33
|
+
|
34
|
+

|
data/example/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
9
|
+
* compiled file, but it's generally better to create a new file per style scope.
|
10
|
+
*
|
11
|
+
*= require_self
|
12
|
+
*= require_tree .
|
13
|
+
*/
|
14
|
+
|
15
|
+
@import "reset";
|
16
|
+
@import "bootstrap";
|
17
|
+
|
18
|
+
body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; font-size: 14px; }
|
19
|
+
|
20
|
+
#search-form {
|
21
|
+
padding: 40px;
|
22
|
+
width: 100%;
|
23
|
+
min-width: 900px;
|
24
|
+
|
25
|
+
h1 { font-weight: 500; font-size: 36px; margin-bottom: 20px; }
|
26
|
+
|
27
|
+
input[type="search"],
|
28
|
+
input[type="date"],
|
29
|
+
input[type="text"] { color: #555555; border: 1px solid #cccccc; border-radius: 4px; font-size: 14px; }
|
30
|
+
|
31
|
+
fieldset { margin-bottom: 20px; padding: 10px; border: 1px solid #e7e7e7; border-radius: 4px; background-color: #f8f8f8; }
|
32
|
+
fieldset input[type="submit"] { @extend .btn; @extend .btn-default; }
|
33
|
+
fieldset input[type="search"],
|
34
|
+
fieldset input[type="date"] { vertical-align: middle; height: 34px; padding: 6px 12px; margin-right: 10px; }
|
35
|
+
|
36
|
+
table thead label { color: #428bca; }
|
37
|
+
table thead .active { font-weight: bold; color: black; }
|
38
|
+
table thead input[type="text"] { padding: 0 6px; width: 99%; }
|
39
|
+
table tbody .empty { text-align: center; }
|
40
|
+
}
|