talent_scout 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1d013c77ffd08bf7bb7d36d4f30c10bc763a4ea79c437d80604985de86fadd74
4
+ data.tar.gz: a4a996e5b44c64af45806849f3744f06bf7f192d346416943739f172abf45180
5
+ SHA512:
6
+ metadata.gz: 24e7c840762d376d5dd9353e3f9b4f167424914e1320573fbcc5cf6dcb691ab5a78f4465261fd2ca029784dd5fac25192c8def19b1f2280401dd3a1452baed7a
7
+ data.tar.gz: b6be4607619b5dcae29c012fb5194b640e5a5bf5ad9de94f1cd80532cf9cfdb12d8d61f615297da0531b8fe2fa9504a589d7f3816da2192f3e6b7f603231cb58
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,741 @@
1
+ # talent_scout
2
+
3
+ Model-backed searches in Rails. A whiz-bang example:
4
+
5
+ ```ruby
6
+ ## app/searches/post_search.rb
7
+
8
+ class PostSearch < TalentScout::ModelSearch
9
+ criteria :title_includes do |string|
10
+ where("title LIKE ?", "%#{string}%")
11
+ end
12
+
13
+ criteria :within, choices: {
14
+ "Last 24 hours" => 24.hours,
15
+ "Past Week" => 1.week,
16
+ "Past Month" => 1.month,
17
+ "Past Year" => 1.year,
18
+ } do |duration|
19
+ where("created_at >= ?", duration.ago)
20
+ end
21
+
22
+ criteria :only_published, :boolean, default: true do |only|
23
+ where("published") if only
24
+ end
25
+
26
+ order :created_at, default: :desc
27
+ order :title
28
+ end
29
+ ```
30
+
31
+ ```ruby
32
+ ## app/controllers/posts_controller.rb
33
+
34
+ class PostsController < ApplicationController
35
+ def index
36
+ @search = model_search
37
+ @posts = @search.results
38
+ end
39
+ end
40
+ ```
41
+
42
+ ```html+erb
43
+ <!-- app/views/posts/index.html.erb -->
44
+
45
+ <%= form_with model: @search, method: :get do |form| %>
46
+ <%= form.text_field :title_includes %>
47
+ <%= form.select :within, @search.each_choice(:within), include_blank: true %>
48
+ <%= form.check_box :only_published %>
49
+ <%= form.submit %>
50
+ <% end %>
51
+
52
+ <table>
53
+ <thead>
54
+ <tr>
55
+ <th>
56
+ <%= link_to_search "Title", @search.toggle_order(:title) %>
57
+ <%= img_tag "#{@search.order_directions[:title] || "unsorted"}_icon.png" %>
58
+ </th>
59
+ <th>
60
+ <%= link_to_search "Time", @search.toggle_order(:created_at) %>
61
+ <%= img_tag "#{@search.order_directions[:created_at] || "unsorted"}_icon.png" %>
62
+ </th>
63
+ </tr>
64
+ </thead>
65
+ <tbody>
66
+ <% @posts.each do |post| %>
67
+ <tr>
68
+ <td><%= link_to post.title, post %></td>
69
+ <td><%= post.created_at %></td>
70
+ </tr>
71
+ <% end %>
72
+ </tbody>
73
+ </table>
74
+ ```
75
+
76
+ In the above example:
77
+
78
+ * The `PostSearch` class handles the responsibility of searching for
79
+ `Post` models. It can apply any combination of its defined criteria,
80
+ automatically ignoring missing or blank input values. It can also
81
+ order the results by one of its defined orders, in either ascending or
82
+ descending direction.
83
+ * `PostsController#index` uses the `model_search` helper to construct a
84
+ `PostSearch`, and assigns it to the `@search` variable for later use
85
+ in the view. The search results are also assigned to a variable for
86
+ use in the view.
87
+ * The view uses Rails' stock form builder to build a search form with
88
+ the `@search` variable. The `link_to_search` helper is used to create
89
+ links in the table header which sort the results. Note that the
90
+ `toggle_order` method used here returns a new search object, leaving
91
+ `@search` unmodified.
92
+
93
+ For a detailed explanation of the methods used in this example, see
94
+ the [API documentation](http://www.rubydoc.info/gems/talent_scout/).
95
+
96
+
97
+ ## Search Objects
98
+
99
+ You can use the `talent_scout:search` generator to generate search
100
+ object class definitions. For example,
101
+
102
+ ```bash
103
+ $ rails generate talent_scout:search post
104
+ ```
105
+
106
+ Will generate a file "app/searches/post_search.rb" containing:
107
+
108
+ ```ruby
109
+ class PostSearch < TalentScout::ModelSearch
110
+ end
111
+ ```
112
+
113
+ Search objects inherit from `TalentScout::ModelSearch`. Their target
114
+ model class is inferred from the search object's class name. For
115
+ example, `PostSearch` will search for `Post` models by default. To
116
+ override this behavior, use `ModelSearch::model_class=`:
117
+
118
+ ```ruby
119
+ class EmployeeSearch < TalentScout::ModelSearch
120
+ self.model_class = Person # will search for Person models instead of `Employee`
121
+ end
122
+ ```
123
+
124
+
125
+ ### Criteria
126
+
127
+ Search criteria are defined with the `ModelSearch::criteria` method.
128
+ Criteria definitions can be specified in one of three ways: with an
129
+ implicit where clause, with an explicit query block, or with a model
130
+ scope reference. To illustrate, the following three `:title` criteria
131
+ are all equivalent:
132
+
133
+ ```ruby
134
+ class Post < ActiveRecord::Base
135
+ scope :title_equals, ->(string){ where(title: string) }
136
+ end
137
+
138
+ class PostSearch < TalentScout::ModelSearch
139
+ criteria :title
140
+
141
+ criteria :title do |string|
142
+ where(title: string)
143
+ end
144
+
145
+ criteria :title, &:title_equals
146
+ end
147
+ ```
148
+
149
+ Note that explicit query blocks are evaluated in the context of the
150
+ model's `ActiveRecord::Relation`, just as model scopes are.
151
+
152
+
153
+ #### Criteria Type
154
+
155
+ A criteria definition can specify a data type, which causes its input
156
+ value to be typecast before being passed to the query block or scope.
157
+ As an example:
158
+
159
+ ```ruby
160
+ class PostSearch < TalentScout::ModelSearch
161
+ criteria :created_on, :date do |date|
162
+ where(created_at: date.beginning_of_day..date.end_of_day)
163
+ end
164
+ end
165
+
166
+ PostSearch.new(created_on: "Dec 31, 1999")
167
+ ```
168
+
169
+ Here, the string `"Dec 31, 1999"` passed to the `PostSearch` constructor
170
+ is typecast to a `Date` before being passed to the query block.
171
+
172
+ The default criteria type is `:string`, which means, by default, all
173
+ input values will be cast to strings. This default (as opposed to a
174
+ default of no typecasting) ensures consistent behavior no matter how the
175
+ search object is constructed, whether from strongly-typed values or from
176
+ search form request params.
177
+
178
+ Available criteria types are the same as for Active Model attributes:
179
+ `:big_integer`, `:boolean`, `:date`, `:datetime`, `:decimal`, `:float`,
180
+ `:integer`, `:string`, `:time`, plus any custom types you define.
181
+
182
+ An additional convenience type is also available: `:void`. The `:void`
183
+ type is just like `:boolean`, except that the criteria will not be
184
+ applied when the typecasted value is falsey. For example:
185
+
186
+ ```ruby
187
+ class PostSearch < TalentScout::ModelSearch
188
+ criteria :only_edited, :void do
189
+ where("modified_at > created_at")
190
+ end
191
+ end
192
+
193
+ # The following will apply `only_edited`:
194
+ PostSearch.new(only_edited: true)
195
+ PostSearch.new(only_edited: "1")
196
+
197
+ # The following will skip `only_edited`:
198
+ PostSearch.new(only_edited: false)
199
+ PostSearch.new(only_edited: "0")
200
+ PostSearch.new(only_edited: "")
201
+ ```
202
+
203
+
204
+ #### Criteria Choices
205
+
206
+ Instead of specifying a type, a criteria definition may specify choices.
207
+ Choices define a set of values which can be passed to the query block.
208
+
209
+ Choices can either be specified as an Array of human-friendly values:
210
+
211
+ ```ruby
212
+ class PostSearch < TalentScout::ModelSearch
213
+ criteria :category, choices: %w[Science Tech Engineering Math] do |name|
214
+ where(category: name.downcase)
215
+ end
216
+ end
217
+ ```
218
+
219
+ ...Or as a Hash with human-friendly keys:
220
+
221
+ ```ruby
222
+ class PostSearch < TalentScout::ModelSearch
223
+ criteria :within, choices: {
224
+ "Last 24 hours" => 24.hours,
225
+ "Past Week" => 1.week,
226
+ "Past Month" => 1.month,
227
+ "Past Year" => 1.year,
228
+ } do |duration|
229
+ where("created_at >= ?", duration.ago)
230
+ end
231
+ end
232
+ ```
233
+
234
+ The value passed to the query block will be one of the values in the
235
+ Array or one of the values in the Hash. The search object may be
236
+ constructed with any of the Array values or Hash keys or Hash values:
237
+
238
+ ```ruby
239
+ PostSearch.new(category: "Math")
240
+ PostSearch.new(within: "Last 24 hours")
241
+ PostSearch.new(within: 24.hours)
242
+ ```
243
+
244
+ But if an invalid choice is specified, the corresponding criteria will
245
+ not be applied:
246
+
247
+ ```ruby
248
+ # The following will skip the criteria, but will not raise an error:
249
+ PostSearch.new(category: "Marketing")
250
+ PostSearch.new(within: 12.hours)
251
+ ```
252
+
253
+
254
+ #### Criteria Default Value
255
+
256
+ A criteria definition can specify a default value, which will be passed
257
+ to the query block when the input value is missing. Default values will
258
+ also appear in search forms.
259
+
260
+ ```ruby
261
+ class PostSearch < TalentScout::ModelSearch
262
+ criteria :within_days, :integer, default: 7 do |num|
263
+ where("created_at >= ?", num.days.ago)
264
+ end
265
+ end
266
+
267
+ # The following are equivalent:
268
+ PostSearch.new()
269
+ PostSearch.new(within_days: 7)
270
+ ```
271
+
272
+
273
+ #### Criteria Resolution
274
+
275
+ A criteria will not be applied if any of the following are true:
276
+
277
+ * The criteria input value is missing, and no default value has been
278
+ specified.
279
+
280
+ * The search object was constructed with an `ActionController::Parameters`
281
+ (instead of a Hash), and the criteria input value is `blank?`, and no
282
+ default value has been specified. (This behavior prevents empty
283
+ search form fields from affecting search results.)
284
+
285
+ * The typecast of the criteria input value fails. For example:
286
+
287
+ ```ruby
288
+ class PostSearch < TalentScout::ModelSearch
289
+ criteria :created_on, :date do |date|
290
+ where(created_at: date.beginning_of_day..date.end_of_day)
291
+ end
292
+ end
293
+
294
+ # The following will skip `created_on`, but will not raise an error:
295
+ PostSearch.new(created_on: "BAD")
296
+ ```
297
+
298
+ * The criteria definition specifies type `:void`, and the typecasted
299
+ input value is falsey.
300
+
301
+ * The criteria definition specifies choices, and the input value is not
302
+ a valid choice.
303
+
304
+ * The criteria query block returns `nil`. For example:
305
+
306
+ ```ruby
307
+ class PostSearch < TalentScout::ModelSearch
308
+ criteria :minimum_upvotes, :integer do |minimum|
309
+ where("upvotes >= ?", minimum) unless minimum <= 0
310
+ end
311
+ end
312
+
313
+ # The following will skip the `minimum_upvotes` where clause:
314
+ PostSearch.new(minimum_upvotes: 0)
315
+ ```
316
+
317
+
318
+ ### Orders
319
+
320
+ Search result orders are defined with the `ModelSearch::order` method:
321
+
322
+ ```ruby
323
+ class PostSearch < TalentScout::ModelSearch
324
+ order :created_at
325
+ order :title
326
+ order :category
327
+ end
328
+
329
+ PostSearch.new(order: :created_at)
330
+ PostSearch.new(order: :title)
331
+ PostSearch.new(order: :category)
332
+ ```
333
+
334
+ Only one order can be applied at a time, but an order can be defined
335
+ over multiple columns:
336
+
337
+ ```ruby
338
+ class PostSearch < TalentScout::ModelSearch
339
+ order :category, [:category, :title]
340
+ end
341
+
342
+ # The following will order by "category, title":
343
+ PostSearch.new(order: :category)
344
+ ```
345
+
346
+ This restricted design was chosen because it allows curated multi-column
347
+ sorts with simpler single-column sorting UIs, and because it prevents
348
+ ad-hoc multi-column sorts that may not be backed by a database index.
349
+
350
+
351
+ #### Order Direction
352
+
353
+ An order can be applied in ascending or descending direction. The
354
+ `ModelSearch#toggle_order` method will apply an order in ascending
355
+ direction, or will change an applied order direction from ascending to
356
+ descending:
357
+
358
+ ```ruby
359
+ class PostSearch < TalentScout::ModelSearch
360
+ order :created_at
361
+ order :title
362
+ end
363
+
364
+ # The following will order by "title":
365
+ PostSearch.new().toggle_order(:title)
366
+ PostSearch.new(order: :created_at).toggle_order(:title)
367
+
368
+ # The following will order by "title DESC":
369
+ PostSearch.new(order: :title).toggle_order(:title)
370
+ ```
371
+
372
+ Note that the `toggle_order` method does not modify the existing search
373
+ object. Instead, it builds a new search object with the new order and
374
+ the criteria values of the previous search object.
375
+
376
+ When a multi-column order is applied in descending direction, all
377
+ columns are affected:
378
+
379
+ ```ruby
380
+ class PostSearch < TalentScout::ModelSearch
381
+ order :category, [:category, :title]
382
+ end
383
+
384
+ # The following will order by "category DESC, title DESC":
385
+ PostSearch.new(order: :category).toggle_order(:category)
386
+ ```
387
+
388
+ To circumvent this behavior, and instead fix a column in a static
389
+ direction, append `" ASC"` or `" DESC"` to the column name:
390
+
391
+ ```ruby
392
+ class PostSearch < TalentScout::ModelSearch
393
+ order :category, [:category, "created_at ASC"]
394
+ end
395
+
396
+ # The following will order by "category, created_at ASC":
397
+ PostSearch.new(order: :category)
398
+
399
+ # The following will order by "category DESC, created_at ASC":
400
+ PostSearch.new(order: :category).toggle_order(:category)
401
+ ```
402
+
403
+
404
+ #### Order Direction Suffixes
405
+
406
+ An order can be applied in ascending or descending direction directly,
407
+ without using `toggle_order`, by appending an appropriate suffix:
408
+
409
+ ```ruby
410
+ class PostSearch < TalentScout::ModelSearch
411
+ order :title
412
+ end
413
+
414
+ # The following will order by "title":
415
+ PostSearch.new(order: :title)
416
+ PostSearch.new(order: "title.asc")
417
+
418
+ # The following will order by "title DESC":
419
+ PostSearch.new(order: "title.desc")
420
+ ```
421
+
422
+ The default suffixes, as seen in the above example, are `".asc"` and
423
+ `".desc"`. These were chosen for their I18n-friendliness. They can be
424
+ overridden as part of the order definition:
425
+
426
+ ```ruby
427
+ class PostSearch < TalentScout::ModelSearch
428
+ order "Title", [:title], asc_suffix: " (A-Z)", desc_suffix: " (Z-A)"
429
+ end
430
+
431
+ # The following will order by "title":
432
+ PostSearch.new(order: "Title")
433
+ PostSearch.new(order: "Title (A-Z)")
434
+
435
+ # The following will order by "title DESC":
436
+ PostSearch.new(order: "Title (Z-A)")
437
+ ```
438
+
439
+
440
+ #### Default Order
441
+
442
+ An order can be designated as the default order, which will cause that
443
+ order to be applied when no order is otherwise specified:
444
+
445
+ ```ruby
446
+ class PostSearch < TalentScout::ModelSearch
447
+ order :created_at, default: :desc
448
+ order :title
449
+ end
450
+
451
+ # The following will order by "created_at DESC":
452
+ PostSearch.new()
453
+
454
+ # The following will order by "created_at":
455
+ PostSearch.new(order: :created_at)
456
+
457
+ # The following will order by "title":
458
+ PostSearch.new(order: :title)
459
+ ```
460
+
461
+ Note that the default order direction can be either ascending or
462
+ descending, by specifing `default: :asc` or `default: :desc`,
463
+ respectively. Also, just as only one order can be applied at a time,
464
+ only one order can be designated default.
465
+
466
+
467
+ ### Default Scope
468
+
469
+ A default search scope can be defined with `ModelSearch::default_scope`:
470
+
471
+ ```ruby
472
+ class PostSearch < TalentScout::ModelSearch
473
+ default_scope { where(published: true) }
474
+ end
475
+ ```
476
+
477
+ The default scope will be applied regardless of the criteria or order
478
+ input values.
479
+
480
+
481
+ ## Controllers
482
+
483
+ Controllers can use the `model_search` helper method to construct a
484
+ search object with the current request's query params:
485
+
486
+ ```ruby
487
+ class PostsController < ApplicationController
488
+ def index
489
+ @search = model_search
490
+ @posts = @search.results
491
+ end
492
+ end
493
+ ```
494
+
495
+ In the above example, `model_search` constructs a `PostSearch` object.
496
+ The model search class is automatically derived from the controller
497
+ class name. To override the model search class, use `::model_search_class=`:
498
+
499
+ ```ruby
500
+ class EmployeesController < ApplicationController
501
+ self.model_search_class = PersonSearch
502
+
503
+ def index
504
+ @search = model_search # will construct PersonSearch instead of `EmployeeSearch`
505
+ @employees = @search.results
506
+ end
507
+ end
508
+ ```
509
+
510
+ In these examples, the search object is stored in a variable for use in
511
+ the view, as are the search results. The search results will be an
512
+ `ActiveRecord::Relation`, so any additional scoping, such as pagination,
513
+ can be applied to `@search.results`.
514
+
515
+
516
+ ## Search Forms
517
+
518
+ Search forms can be rendered using Rails' form builder and a search
519
+ object:
520
+
521
+ ```html+erb
522
+ <%= form_with model: @search, method: :get do |form| %>
523
+ <%= form.text_field :title_includes %>
524
+ <%= form.date_field :created_on %>
525
+ <%= form.check_box :only_published %>
526
+ <%= form.submit %>
527
+ <% end %>
528
+ ```
529
+
530
+ Notice the `method: :get` argument to `form_with`; this is **required**.
531
+
532
+ Form fields will be populated with the criteria input (or default)
533
+ values of the same name from `@search`. Type-appropriate form fields
534
+ can be used, e.g. `date_field` for type `:date`, `check_box` for types
535
+ `:boolean` and `:void`, etc.
536
+
537
+ By default, the form will submit to the index action of the controller
538
+ that corresponds to the `model_class` of the search object. For
539
+ example, `PostSearch.model_class` is `Post`, so a form with an instance
540
+ of `PostSearch` will submit to `PostsController#index`. To change where
541
+ the form submits to, use the `:url` option of `form_with`.
542
+
543
+
544
+ ## Search Links
545
+
546
+ Search links can be rendered using the `link_to_search` view helper
547
+ method:
548
+
549
+ ```html+erb
550
+ <%= link_to_search "Sort by title", @search.toggle_order(:title, :asc) %>
551
+ ```
552
+
553
+ The link will automatically point to current controller and current
554
+ action, with query parameters from the given search object. To link to
555
+ a different controller or action, pass an options Hash in place of the
556
+ search object:
557
+
558
+ ```html+erb
559
+ <%= link_to_search "Sort by title", { controller: "posts", action: "index",
560
+ search: @search.toggle_order(:title, :asc) } %>
561
+ ```
562
+
563
+ The `link_to_search` helper also accepts the same HTML options that
564
+ Rails' `link_to` helper does:
565
+
566
+ ```html+erb
567
+ <%= link_to_search "Sort by title", @search.toggle_order(:title, :asc),
568
+ id: "title-sort-link", class: "sort-link" %>
569
+ ```
570
+
571
+ ...As well as a content block:
572
+
573
+ ```html+erb
574
+ <%= link_to_search @search.toggle_order(:title, :asc) do %>
575
+ Sort by title <%= img_tag "sort_icon.png" %>
576
+ <% end %>
577
+ ```
578
+
579
+
580
+ ## `ModelSearch` Helper Methods
581
+
582
+ The `ModelSearch` class provides several methods that are helpful when
583
+ rendering the view.
584
+
585
+ One such method is `ModelSearch#toggle_order`, which was shown in
586
+ [previous examples](#order-direction). Remember that `toggle_order` is
587
+ a builder-style method that does not modify the search object. Instead,
588
+ it duplicates the search object, and sets the order on the new search
589
+ object. Such behavior is suitable to generating links to multiple
590
+ variants of a search, such as sort links in table column headers.
591
+
592
+
593
+ ### `ModelSearch#with` and `ModelSearch#without`
594
+
595
+ Two additional builder-style methods are `ModelSearch#with` and
596
+ `ModelSearch#without`. Like `toggle_order`, both of these methods
597
+ return a new search object, leaving the original search object
598
+ unmodified. The `with` method accepts a Hash of criteria input values
599
+ to merge on top of the original set of criteria input values:
600
+
601
+ ```ruby
602
+ class PostSearch < TalentScout::ModelSearch
603
+ criteria :title
604
+ criteria :published, :boolean
605
+ end
606
+
607
+ # The following are equivalent:
608
+ PostSearch.new(title: "Maaaaath!", published: true)
609
+ PostSearch.new(title: "Maaaaath!").with(published: true)
610
+ PostSearch.new(title: "Math?").with(title: "Maaaaath!", published: true)
611
+ ```
612
+
613
+ The `without` method accepts a list of criteria input values to exclude
614
+ (default criteria values still apply):
615
+
616
+ ```ruby
617
+ class PostSearch < TalentScout::ModelSearch
618
+ criteria :title
619
+ criteria :published, :boolean, default: true
620
+ end
621
+
622
+ # The following are equivalent:
623
+ PostSearch.new(title: "Maaaaath!")
624
+ PostSearch.new(title: "Maaaaath!", published: false).without(:published)
625
+ ```
626
+
627
+
628
+ ### `ModelSearch#each_choice`
629
+
630
+ Another helpful method is `ModelSearch#each_choice`, which will iterate
631
+ over the defined choices for a given criteria. This can be used to
632
+ generate links to variants of a search, or to generate options for a
633
+ select box:
634
+
635
+ ```ruby
636
+ class PostSearch < TalentScout::ModelSearch
637
+ criteria :category, choices: %w[Science Tech Engineering Math]
638
+ end
639
+ ```
640
+
641
+ ```html+erb
642
+ <% @search.each_choice(:category) do |choice, chosen| %>
643
+ <%= link_to_search "Category: #{choice}", @search.with(category: choice),
644
+ class: ("active" if chosen) %>
645
+ <% end %>
646
+ ```
647
+
648
+ ```html+erb
649
+ <%= form_with model: @search, method: :get do |form| %>
650
+ <%= form.select :category, @search.each_choice(:category) %>
651
+ <%= form.submit %>
652
+ <% end %>
653
+ ```
654
+
655
+ If the block passed to `each_choice` accepts two arguments, the 2nd
656
+ argument will indicate if the choice is currently chosen. If no block
657
+ is passed to `each_choice`, it will return an `Enumerator`.
658
+
659
+ The `each_choice` method can also be invoked with `:order`. Doing so
660
+ will iterate over each direction of each defined order, yielding the
661
+ appropriate labels including direction suffix:
662
+
663
+ ```ruby
664
+ class PostSearch < TalentScout::ModelSearch
665
+ order "Title", [:title], asc_suffix: " (A-Z)", desc_suffix: " (Z-A)"
666
+ order "Time", [:created_at], asc_suffix: " (oldest first)", desc_suffix: " (newest first)"
667
+ end
668
+ ```
669
+
670
+ ```html+erb
671
+ <%= form_with model: @search, method: :get do |form| %>
672
+ <%= form.select :order, @search.each_choice(:order) %>
673
+ <%= form.submit %>
674
+ <% end %>
675
+ ```
676
+
677
+ The select box in the above form will list four options: "Title (A-Z)",
678
+ "Title (Z-A)", "Time (oldest first)", "Time (newest first)".
679
+
680
+
681
+ ### `ModelSearch#order_directions`
682
+
683
+ Finally, the `ModelSearch#order_directions` helper method returns a
684
+ `HashWithIndifferentAccess` reflecting the currently applied direction
685
+ of each defined order. It contains a key for each defined order, and
686
+ associates each key with either `:asc`, `:desc`, or `nil`.
687
+
688
+ ```ruby
689
+ class PostSearch < TalentScout::ModelSearch
690
+ order "Title", [:title]
691
+ order "Time", [:created_at]
692
+ end
693
+ ```
694
+
695
+ ```html+erb
696
+ <thead>
697
+ <tr>
698
+ <% @search.order_directions.each do |order, direction| %>
699
+ <th>
700
+ <%= link_to_search order, @search.toggle_order(order) %>
701
+ <%= img_tag "#{direction || "unsorted"}_icon.png" %>
702
+ </th>
703
+ <% end %>
704
+ </tr>
705
+ </thead>
706
+ ```
707
+
708
+ Remember that only one order can be applied at a time, so only one value
709
+ in the Hash, at most, will be non-`nil`.
710
+ >>>>>>> b0d201c... Update README
711
+
712
+
713
+ ## Installation
714
+
715
+ Add this line to your application's Gemfile:
716
+
717
+ ```ruby
718
+ gem "talent_scout"
719
+ ```
720
+
721
+ Then execute:
722
+
723
+ ```bash
724
+ $ bundle install
725
+ ```
726
+
727
+ And finally, run the installation generator:
728
+
729
+ ```bash
730
+ $ rails generate talent_scout:install
731
+ ```
732
+
733
+
734
+ ## Contributing
735
+
736
+ Run `rake test` to run the tests.
737
+
738
+
739
+ ## License
740
+
741
+ [MIT License](https://opensource.org/licenses/MIT)