search_object 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +8 -0
  4. data/CHANGELOG.md +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +283 -0
  8. data/Rakefile +6 -0
  9. data/example/.gitignore +16 -0
  10. data/example/.rspec +1 -0
  11. data/example/Gemfile +11 -0
  12. data/example/README.md +34 -0
  13. data/example/Rakefile +6 -0
  14. data/example/app/assets/javascripts/application.js +5 -0
  15. data/example/app/assets/stylesheets/application.css.scss +40 -0
  16. data/example/app/assets/stylesheets/reset.css +43 -0
  17. data/example/app/controllers/application_controller.rb +3 -0
  18. data/example/app/controllers/posts_controller.rb +5 -0
  19. data/example/app/models/.keep +0 -0
  20. data/example/app/models/post.rb +13 -0
  21. data/example/app/models/post_search.rb +44 -0
  22. data/example/app/models/user.rb +5 -0
  23. data/example/app/views/layouts/application.html.slim +12 -0
  24. data/example/app/views/posts/index.html.slim +48 -0
  25. data/example/bin/bundle +3 -0
  26. data/example/bin/rails +4 -0
  27. data/example/bin/rake +4 -0
  28. data/example/config.ru +4 -0
  29. data/example/config/application.rb +27 -0
  30. data/example/config/boot.rb +4 -0
  31. data/example/config/database.yml +12 -0
  32. data/example/config/environment.rb +5 -0
  33. data/example/config/environments/development.rb +29 -0
  34. data/example/config/environments/test.rb +37 -0
  35. data/example/config/initializers/filter_parameter_logging.rb +4 -0
  36. data/example/config/initializers/secret_token.rb +12 -0
  37. data/example/config/initializers/session_store.rb +3 -0
  38. data/example/config/initializers/wrap_parameters.rb +14 -0
  39. data/example/config/routes.rb +3 -0
  40. data/example/db/migrate/20131102130117_create_users.rb +10 -0
  41. data/example/db/migrate/20131102130413_create_posts.rb +18 -0
  42. data/example/db/schema.rb +40 -0
  43. data/example/db/seeds.rb +37 -0
  44. data/example/log/.keep +0 -0
  45. data/example/screenshot.png +0 -0
  46. data/example/spec/models/post_search_spec.rb +81 -0
  47. data/example/spec/spec_helper.rb +19 -0
  48. data/lib/search_object.rb +20 -0
  49. data/lib/search_object/base.rb +64 -0
  50. data/lib/search_object/helper.rb +36 -0
  51. data/lib/search_object/plugin/kaminari.rb +18 -0
  52. data/lib/search_object/plugin/model.rb +16 -0
  53. data/lib/search_object/plugin/paging.rb +42 -0
  54. data/lib/search_object/plugin/sorting.rb +54 -0
  55. data/lib/search_object/plugin/will_paginate.rb +17 -0
  56. data/lib/search_object/search.rb +26 -0
  57. data/lib/search_object/version.rb +3 -0
  58. data/search_object.gemspec +31 -0
  59. data/spec/search_object/base_spec.rb +237 -0
  60. data/spec/search_object/helper_spec.rb +30 -0
  61. data/spec/search_object/plugin/kaminari_spec.rb +50 -0
  62. data/spec/search_object/plugin/model_spec.rb +22 -0
  63. data/spec/search_object/plugin/paging_spec.rb +43 -0
  64. data/spec/search_object/plugin/sorting_spec.rb +139 -0
  65. data/spec/search_object/plugin/will_paginate_spec.rb +51 -0
  66. data/spec/search_object/search_spec.rb +72 -0
  67. data/spec/spec_helper.rb +13 -0
  68. data/spec/spec_helper_active_record.rb +19 -0
  69. data/spec/support/kaminari_setup.rb +7 -0
  70. metadata +292 -0
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format=documentation
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0
5
+ - rbx-19mode
6
+ script: bundle exec rspec spec
7
+ notifications:
8
+ email: false
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## Version 0.1
4
+
5
+ * initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in search_object.gemspec
4
+ gemspec
@@ -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.
@@ -0,0 +1,283 @@
1
+ [![Code Climate](https://codeclimate.com/github/RStankov/SearchObject.png)](https://codeclimate.com/github/RStankov/SearchObject)
2
+ [![Build Status](https://secure.travis-ci.org/RStankov/SearchObject.png)](http://travis-ci.org/RStankov/SearchObject)
3
+ [![Code coverage](https://coveralls.io/repos/RStankov/SearchObject/badge.png?branch=master)](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)**
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -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
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rails', '4.0.0'
4
+ gem 'sqlite3'
5
+ gem 'sass-rails', '~> 4.0.0'
6
+ gem 'slim'
7
+ gem 'jquery-rails'
8
+ gem 'bootstrap-sass'
9
+ gem 'will_paginate'
10
+ gem 'will_paginate-bootstrap'
11
+ gem 'rspec-rails'
@@ -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
+ ![Screenshot](https://raw.github.com/RStankov/SearchObject/master/example/screenshot.png)
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Example::Application.load_tasks
@@ -0,0 +1,5 @@
1
+ //= require jquery
2
+
3
+ $('form table :input').on('change', function(e) {
4
+ $(e.target).closest('form').submit();
5
+ });
@@ -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
+ }