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.
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
+ }