admino 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e26dee36252bba0db083897fc29ced0358ab2046
4
- data.tar.gz: 0efed84c06512abd26698ea33479d7ed1a7b4746
3
+ metadata.gz: c15fcada38f4aa3e23bbc6b1a460565a632fb7aa
4
+ data.tar.gz: 2420006c1f8910d432cd5a3da44776ff8a9fc94b
5
5
  SHA512:
6
- metadata.gz: f036deab369232c67daf274146a06d1173a82ec26f77d444faf2e4bf66d550adce7224a6f0fe92d486b4eba44b5b9880a8fd216dd39309d3182178d083c69d19
7
- data.tar.gz: 1342bbb40cea481a7e4459f74381746a64f6d12c54809a87aad7315fd247d602c5f60ce74bb2ab1fef9aca5ec1e68c51733f62a26aeeea659255fb769478a317
6
+ metadata.gz: f3a34ac25cf486afeb7beb851dd6eecec93de4d7d6f47371570b0451cc533ed423c53a6a47ad5b6f9a80595533e5a0b6940a8b3da754f44e3793dc16fca330f9
7
+ data.tar.gz: 08c7f2fc10cb1762680895ce7cb18b257c20528a42de033d4e0c148ce8bf3360e07bc0c6d7ab08ae945af30cf3842eabf1a995f4707e3fcd3cfddfec9ac32940
@@ -1,3 +1,8 @@
1
+ # v0.0.5
2
+
3
+ * Rename Field into SearchField
4
+ * Admino::Table::Presenter no longer presents collection by default
5
+
1
6
  # v0.0.4
2
7
 
3
8
  * Rename Group into FilterGroup
data/README.md CHANGED
@@ -19,7 +19,7 @@ So yes, if you're starting a small, short-lived project, go ahead with them, it
19
19
 
20
20
  ### A modular approach to the problem
21
21
 
22
- The great thing is that you don't need to write a lot of code to get a more maintainable and modular administrative area.
22
+ The great thing is that you don't need to write a lot of code to get a more maintainable and modular administrative area.
23
23
  Gems like [Inherited Resources](https://github.com/josevalim/inherited_resources) and [Simple Form](https://github.com/plataformatec/simple_form), combined with [Rails 3.1+ template-inheritance](http://railscasts.com/episodes/269-template-inheritance) already give you ~90% of the time-saving features and the same super-DRY, declarative code that administrative interfaces offer, but with a far more relaxed contract.
24
24
 
25
25
  If a particular controller or view needs something different from the standard CRUD/REST treatment, you can just avoid using those gems in that specific context, and fall back to standard Rails code. No workarounds, no facepalms. It seems easy, right? It is.
@@ -38,68 +38,163 @@ And then execute:
38
38
 
39
39
  ## Admino::Query::Base
40
40
 
41
- A subclass of `Admino::Query::Base` represents a [Query object](http://martinfowler.com/eaaCatalog/queryObject.html), that is, an object responsible for returning a result set (ie. an `ActiveRecord::Relation`) based on business rules (ie. action params).
41
+ `Admino::Query::Base` implements the [Query object](http://martinfowler.com/eaaCatalog/queryObject.html) pattern, that is, an object responsible for returning a result set (ie. an `ActiveRecord::Relation`) based on business rules.
42
42
 
43
- Given a `Task` model with the following scopes:
43
+ Given a `Task` model, we can generate a `TasksQuery` query object subclassing `Admino::Query::Base`:
44
44
 
45
45
  ```ruby
46
- class Task < ActiveRecord::Base
47
- scope :text_matches, ->(text) { where(...) }
46
+ class TasksQuery < Admino::Query::Base
47
+ end
48
+ ```
48
49
 
49
- scope :completed, -> { where(completed: true) }
50
- scope :pending, -> { where(completed: false) }
50
+ Each query object gets initialized with a hash of params, and features a `#scope` method that returns the filtered/sorted result set. As you may have guessed, query objects can be great companions to index controller actions:
51
51
 
52
- scope :by_due_date, ->(direction) { order(due_date: direction) }
53
- scope :by_title, ->(direction) { order(title: direction) }
52
+ ```ruby
53
+ class TasksController < ApplicationController
54
+ def index
55
+ @query = TasksQuery.new(params)
56
+ @tasks = @query.scope
57
+ end
54
58
  end
55
59
  ```
56
60
 
57
- The following `TasksQuery` class can be created:
61
+ ### Building the query itself
62
+
63
+ You can specify how a `TaskQuery` must build a result set through a simple DSL.
64
+
65
+ #### `starting_scope`
66
+
67
+ The `starting_scope` method is in charge of defining the scope that will start the filtering/ordering chain:
58
68
 
59
69
  ```ruby
60
70
  class TasksQuery < Admino::Query::Base
61
- starting_scope { ProjectTask.all }
71
+ starting_scope { Task.all }
72
+ end
73
+
74
+ Task.create(title: 'Low priority task')
75
+
76
+ TaskQuery.new.scope.count # => 1
77
+ ```
78
+
79
+ #### `search_field`
80
+
81
+ Once you define the following field:
62
82
 
63
- field :text_matches
83
+ ```ruby
84
+ class TasksQuery < Admino::Query::Base
85
+ # ...
86
+ search_field :title_matches
87
+ end
88
+ ```
89
+ The `#scope` method will check the presence of the `params[:query][:title_matches]` key. If it finds it, it will augment the query with a
90
+ named scope called `:title_matches`, expected to be found within the `Task` model, that needs to accept an argument.
91
+
92
+ ```ruby
93
+ class Task < ActiveRecord::Base
94
+ scope :title_matches, ->(text) {
95
+ where('title ILIKE ?', "%#{text}%")
96
+ }
97
+ end
98
+
99
+ Task.create(title: 'Low priority task')
100
+ Task.create(title: 'Fix me ASAP!!1!')
101
+
102
+ TaskQuery.new.scope.count # => 2
103
+ TaskQuery.new(query: { title_matches: 'ASAP' }).scope.count # => 1
104
+ ```
105
+
106
+ #### `filter_by`
107
+
108
+ ```ruby
109
+ class TasksQuery < Admino::Query::Base
110
+ # ...
64
111
  filter_by :status, [:completed, :pending]
65
- sorting :by_due_date, :by_title
66
112
  end
67
113
  ```
68
114
 
69
- Every query object can declare:
115
+ Just like a search field, with a declared filter group the `#scope` method will check the presence of a `params[:query][:status]` key. If it finds it (and its value corresponds to one of the declared scopes) it will augment the query the scope itself:
116
+
117
+ ```ruby
118
+ class Task < ActiveRecord::Base
119
+ scope :completed, -> { where(completed: true) }
120
+ scope :pending, -> { where(completed: false) }
121
+ end
122
+
123
+ Task.create(title: 'First task', completed: true)
124
+ Task.create(title: 'Second task', completed: true)
125
+ Task.create(title: 'Third task', completed: false)
126
+
127
+ TaskQuery.new.scope.count # => 3
128
+ TaskQuery.new(query: { status: 'completed' }).scope.count # => 2
129
+ TaskQuery.new(query: { status: 'pending' }).scope.count # => 1
130
+ TaskQuery.new(query: { status: 'foobar' }).scope.count # => 3
131
+ ```
132
+
133
+ #### `sorting`
134
+
135
+ ```ruby
136
+ class TasksQuery < Admino::Query::Base
137
+ # ...
138
+ sorting :by_due_date, :by_title
139
+ end
140
+ ```
70
141
 
71
- * a **starting scope**, that is, the scope that will start the filtering/ordering chain;
72
- * a set of **search fields**, which represent model scopes that require an input to filter the result set;
73
- * a set of **filtering groups**, each of which is composed by a set of scopes that take no argument;
74
- * a set of **sorting scopes** that take a sigle argument (`:asc` or `:desc`) and thus are able to order the result set in both directions;
142
+ Once you declare some sorting scopes, the query object looks for a `params[:sorting]` key. If it exists (and corresponds to one of the declared scopes), it will augment the query with the scope itself. The model named scope will be called passing an argument that represents the direction of sorting (`:asc` or `:desc`).
75
143
 
76
- Each query object instance gets initialized with a hash of params. The `#scope` method will then perform the chaining of the scopes based on the given params, returning the final result set:
144
+ The direction passed to the scope will depend on the value of `params[:sort_order]`, and will default to `:asc`:
77
145
 
78
146
  ```ruby
79
- params = {
80
- query: {
81
- text_matches: 'ASAP'
82
- },
83
- status: 'pending',
84
- sorting: 'by_title',
85
- sort_order: 'desc'
86
- }
147
+ class Task < ActiveRecord::Base
148
+ scope :by_due_date, ->(direction) { order(due_date: direction) }
149
+ scope :by_title, ->(direction) { order(title: direction) }
150
+ end
151
+
152
+ expired_task = Task.create(due_date: 1.year.ago)
153
+ future_task = Task.create(due_date: 1.week.since)
87
154
 
88
- tasks = TasksQuery.new(params).scope
155
+ TaskQuery.new(sorting: 'by_due_date', sort_order: 'desc').scope # => [ future_task, expired_task ]
156
+ TaskQuery.new(sorting: 'by_due_date', sort_order: 'asc').scope # => [ expired_task, future_task ]
157
+ TaskQuery.new(sorting: 'by_due_date').scope # => [ expired_task, future_task ]
89
158
  ```
90
159
 
91
- As you may have guessed, query objects can be great companions to index controller actions:
160
+ #### `ending_scope`
161
+
162
+ It's very common ie. to paginate a result set. The block declared in the `ending_scope` block will be always appended to the end of the chain:
92
163
 
93
164
  ```ruby
94
- class ProjectTasksController < ApplicationController
95
- def index
96
- @query = TasksQuery.new(params)
97
- @project_tasks = @query.scope
98
- end
165
+ class TasksQuery < Admino::Query::Base
166
+ ending_scope { |q| page(q.params[:page]) }
99
167
  end
100
168
  ```
101
169
 
102
- But that's not all.
170
+ ### Inspecting the query state
171
+
172
+ A query object supports various methods to inspect the available search fields, filters and sortings, and their state:
173
+
174
+ ```ruby
175
+ query = TaskQuery.new
176
+ query.search_fields # => [ #<Admino::Query::SearchField>, ... ]
177
+ query.filter_groups # => [ #<Admino::Query::FilterGroup>, ... ]
178
+
179
+ search_field = query.search_field_by_name(:title_matches)
180
+
181
+ search_field.name # => :title_matches
182
+ search_field.present? # => true
183
+ search_field.value # => 'ASAP'
184
+
185
+ filter_group = query.filter_group_by_name(:status)
186
+
187
+ filter_group.name # => :status
188
+ filter_group.scopes # => [ :completed, :pending ]
189
+ filter_group.active_scope # => :completed
190
+ filter_group.is_scope_active?(:pending) # => false
191
+
192
+ sorting = query.sorting # => #<Admino::Query::Sorting>
193
+ sorting.scopes # => [ :by_title, :by_due_date ]
194
+ sorting.active_scope # => :by_due_date
195
+ sorting.is_scope_active?(:by_title) # => false
196
+ sorting.ascending? # => true
197
+ ```
103
198
 
104
199
  ### Presenting search form and filters to the user
105
200
 
@@ -112,8 +207,8 @@ Admino also offers a [Showcase presenter](https://github.com/stefanoverna/showca
112
207
  <%# generate the search form %>
113
208
  <%= query.form do |q| %>
114
209
  <p>
115
- <%= q.label :text_matches %>
116
- <%= q.text_field :text_matches %>
210
+ <%= q.label :title_matches %>
211
+ <%= q.text_field :title_matches %>
117
212
  </p>
118
213
  <p>
119
214
  <%= q.submit %>
@@ -131,11 +226,25 @@ Admino also offers a [Showcase presenter](https://github.com/stefanoverna/showca
131
226
  <% end %>
132
227
  </ul>
133
228
  <% end %>
229
+
230
+ <%# generate the sorting links %>
231
+ <h6>Sort by</h6>
232
+ <ul>
233
+ <% query.sorting.scopes.each do |scope| %>
234
+ <li>
235
+ <%= query.sorting.scope_link(scope) %>
236
+ </li>
237
+ <% end %>
238
+ </ul>
134
239
  ```
135
240
 
136
- The great thing is that the search form gets automatically filled in with the last input the user submitted, and a CSS class `is-active` gets added to the currently active filter scopes.
241
+ The great thing is that:
137
242
 
138
- If a particular filter has been clicked and is now active, it is possible to deactivate it by clicking it again.
243
+ * the search form gets automatically filled in with the last input the user submitted
244
+ * a `is-active` CSS class gets added to the currently active filter scopes
245
+ * if a particular filter link has been clicked and is now active, it is possible to deactivate it by clicking on the link again
246
+ * a `is-asc`/`is-desc` CSS class gets added to the currently active sorting scope
247
+ * if a particular sorting scope link has been clicked and is now in ascending order, it is possible to make it descending by clicking on the link again
139
248
 
140
249
  ### Simple Form support
141
250
 
@@ -150,7 +259,7 @@ en:
150
259
  query:
151
260
  attributes:
152
261
  tasks_query:
153
- text_matches: 'Contains text'
262
+ title_matches: 'Title contains'
154
263
  filter_groups:
155
264
  tasks_query:
156
265
  status:
@@ -158,13 +267,17 @@ en:
158
267
  scopes:
159
268
  completed: 'Completed'
160
269
  pending: 'Pending'
270
+ sorting_scopes:
271
+ task_query:
272
+ by_due_date: 'By due date'
273
+ by_title: 'By title'
161
274
  ```
162
275
 
163
- ### Output customisation
276
+ ### Output customization
164
277
 
165
- The query object and its presenter implement a number of additional methods and optional arguments that allow a great amount of flexibility: please refer to the tests to see all the possibile customisations available.
278
+ The presenter supports a number of optional arguments that allow a great amount of flexibility regarding customization of CSS classes, labels and HTML attributes. Please refer to the tests for the details.
166
279
 
167
- #### Overwriting the starting scope
280
+ ### Overwriting the starting scope
168
281
 
169
282
  Suppose you have to filter the tasks based on the `@current_user` work group. You can easily provide an alternative starting scope from the controller passing it as an argument to the `#scope` method:
170
283
 
@@ -175,9 +288,7 @@ def index
175
288
  end
176
289
  ```
177
290
 
178
- ### Default sortings
179
-
180
- #### Coertions
291
+ ### Coertions
181
292
 
182
293
  Admino can perform automatic coertions from a param string input to the type needed by the model named scope:
183
294
 
@@ -204,17 +315,249 @@ If a specific coercion cannot be performed with the provided input, the scope wo
204
315
 
205
316
  Please see the [`Coercible::Coercer::String`](https://github.com/solnic/coercible/blob/master/lib/coercible/coercer/string.rb) class for details.
206
317
 
207
- ### Ending the scope chain
318
+ ### Default sorting
208
319
 
209
- It's very common ie. to paginate the result set. `Admino::Query::Base` DSL makes it easy to append any scope to the end of the chain:
320
+ If you need to setup a default sorting, you can pass some optional arguments to a `scoping` declaration:
210
321
 
211
322
  ```ruby
212
323
  class TasksQuery < Admino::Query::Base
213
- ending_scope { |q| page(q.params[:page]) }
324
+ # ...
325
+ sorting :by_due_date, :by_title,
326
+ default_scope: :by_due_date,
327
+ default_direction: :desc
214
328
  end
215
329
  ```
216
330
 
217
331
  ## Admino::Table::Presenter
218
332
 
219
- WIP
333
+ Admino offers a [Showcase collection presenter](https://github.com/stefanoverna/showcase) that makes it really easy to generate HTML tables from a set of records:
334
+
335
+ ```erb
336
+ <%= Admino::Table::Presenter.new(@tasks, Task, self).to_html do |row, record| %>
337
+ <%= row.column :title %>
338
+ <%= row.column :completed do %>
339
+ <%= record.completed ? '✓' : '✗' %>
340
+ <% end %>
341
+ <%= row.column :due_date %>
342
+ <% end %>
343
+ ```
344
+
345
+ ```html
346
+ <table>
347
+ <thead>
348
+ <tr>
349
+ <th role='title'>Title</th>
350
+ <th role='completed'>Completed</th>
351
+ <th role='due_date'>Due date</th>
352
+ </tr>
353
+ <thead>
354
+ <tbody>
355
+ <tr id='task_1' class='is-even'>
356
+ <td role='title'>Call mum ASAP</td>
357
+ <td role='completed'>✓</td>
358
+ <td role='due_date'>2013-02-04</td>
359
+ </tr>
360
+ <tr id='task_2' class='is-odd'>
361
+ <!-- ... -->
362
+ </tr>
363
+ <tbody>
364
+ </table>
365
+ ```
366
+
367
+ ### Record actions
368
+
369
+ Often table rows needs to offer some kind of action associated with the record. The presenter implements the following DSL to support that:
370
+
371
+ ```erb
372
+ <%= Admino::Table::Presenter.new(@tasks, Task, self).to_html do |row, record| %>
373
+ <%# ... %>
374
+ <%= row.actions do %>
375
+ <%= row.action :show, admin_task_path(record) %>
376
+ <%= row.action :edit, edit_admin_task_path(record) %>
377
+ <%= row.action :destroy, admin_task_path(record), method: :delete %>
378
+ <% end %>
379
+ <% end %>
380
+ ```
381
+
382
+ ```html
383
+ <table>
384
+ <thead>
385
+ <tr>
386
+ <!-- ... -->
387
+ <th role='actions'>Actions</th>
388
+ </tr>
389
+ <thead>
390
+ <tbody>
391
+ <tr id='task_1' class='is-even'>
392
+ <!-- ... -->
393
+ <td role='actions'>
394
+ <a href='/admin/tasks/1' role='show'>Show</a>
395
+ <a href='/admin/tasks/1/edit' role='edit'>Edit</a>
396
+ <a href='/admin/tasks/1' role='destroy' data-method='delete'>Destroy</a>
397
+ </td>
398
+ </tr>
399
+ <tbody>
400
+ </table>
401
+ ```
402
+
403
+ ### Sortable columns
404
+
405
+ Once a query object is passed to the presenter, columns can be associated to specific sorting scopes of the query object using the `sorting` option:
406
+
407
+ ```erb
408
+ <% query = present(@query) %>
409
+
410
+ <%= Admino::Table::Presenter.new(@tasks, Task, query, self).to_html do |row, record| %>
411
+ <%= row.column :title, sorting: :by_title %>
412
+ <%= row.column :due_date, sorting: :by_due_date %>
413
+ <% end %>
414
+ ```
415
+
416
+ This generates links that allow the visitor to sort the result set in ascending and descending direction:
417
+
418
+ ```html
419
+ <table>
420
+ <thead>
421
+ <tr>
422
+ <th role='title'>
423
+ <a href="/admin/tasks?sorting=by_title&sort_order=desc" class='is-asc'>Title</a>
424
+ </th>
425
+ <th role='due_date'>
426
+ <a href="/admin/tasks?sorting=by_due_date&sort_order=asc" class='is-asc'>Due date</a>
427
+ </th>
428
+ </tr>
429
+ <thead>
430
+ <!-- ... -->
431
+ </table>
432
+ ```
433
+
434
+ ### Customizing the output
435
+
436
+ The `#column` and `#action` methods are very flexible, allowing youto change almost every aspect of the generated table cells:
437
+
438
+ ```erb
439
+ <%= Admino::Table::Presenter.new(@tasks, Task, self).to_html(class: 'table-class') do |row, record| %>
440
+ <%= row.column :title, 'Custom title',
441
+ class: 'custom-class', role: 'custom-role', data: { custom: 'true' },
442
+ sorting: :by_title, sorting_html_options: { desc_class: 'down' }
443
+ %>
444
+ <%= row.action :show, admin_task_path(record), 'Custom label',
445
+ class: 'custom-class', role: 'custom-role', data: { custom: 'true' }
446
+ %>
447
+ <% end %>
448
+ ```
449
+
450
+ If you need more power, you can also decide to subclass `Admino::Table::Presenter`. For each HTML element, there's a set of methods you can override to customize it's appeareance.
451
+ Table cells are generated through two collaborator classes: `Admino::Table::HeadRow` and `Admino::Table::ResourceRow`. You can easily replace them with a subclass if you want. To grasp the idea here's an example:
452
+
453
+ ```ruby
454
+ class CustomTablePresenter < Admino::Table::Presenter
455
+ private
456
+
457
+ def table_html_options
458
+ { class: 'table-class' }
459
+ end
460
+
461
+ def tbody_tr_html_options(resource_index)
462
+ { class: 'tr-class' }
463
+ end
464
+
465
+ def zebra_css_classes
466
+ %w(one two three)
467
+ end
468
+
469
+ def resource_row(resource, view_context)
470
+ ResourceRow.new(resource, view_context)
471
+ end
472
+
473
+ def head_row(collection_klass, query, view_context)
474
+ HeadRow.new(collection_klass, query, view_context)
475
+ end
476
+
477
+ class ResourceRow < Admino::Table::ResourceRow
478
+ private
479
+
480
+ def action_html_options(action_name)
481
+ { class: 'action-class' }
482
+ end
483
+
484
+ def show_action_html_options
485
+ { class: 'show-action-class' }
486
+ end
487
+
488
+ def column_html_options(attribute_name)
489
+ { class: 'column-class' }
490
+ end
491
+ end
492
+
493
+ class HeadRow < Admino::Table::ResourceRow
494
+ def column_html_options(attribute_name)
495
+ { class: 'column-class' }
496
+ end
497
+ end
498
+ end
499
+ ```
500
+
501
+ Please refer to the tests for all the details.
502
+
503
+ ### Inherited resources
504
+
505
+ If the action URLs can be programmatically generated, it becomes even easier to specify the table actions:
506
+
507
+ ```erb
508
+ <%= CustomTablePresenter.new(@tasks, Task, self).to_html do |row, record| %>
509
+ <%# ... %>
510
+ <%= row.actions :show, :edit, :destroy %>
511
+ <% end %>
512
+ ```
513
+ For instance, using [Inherited Resources](https://github.com/josevalim/inherited_resources) to generate controller actions, you can use its [helper methods](https://github.com/josevalim/inherited_resources#url-helpers) to build a custom subclass of `Admino::Table::Presenter`:
514
+
515
+ ```ruby
516
+ class CustomTablePresenter < Admino::Table::Presenter
517
+ private
518
+
519
+ def resource_row(resource, view_context)
520
+ ResourceRow.new(resource, view_context)
521
+ end
522
+
523
+ class ResourceRow < Admino::Table::ResourceRow
524
+ def show_action_url
525
+ h.resource_url(resource)
526
+ end
527
+
528
+ def edit_action_url
529
+ h.edit_resource_url(resource)
530
+ end
531
+
532
+ def destroy_action_url
533
+ h.resource_url(resource)
534
+ end
535
+
536
+ def destroy_action_html_options
537
+ { method: :delete }
538
+ end
539
+ end
540
+ end
541
+ ```
542
+
543
+ ### I18n
544
+
545
+ Column titles are generated using the model [`#human_attribute_name`](http://apidock.com/rails/ActiveRecord/Base/human_attribute_name/class) method, so if you already translated the model attribute names, you're good to go. To translate actions, please refer to the following YAML file:
546
+
547
+ ```yaml
548
+ en:
549
+ activerecord:
550
+ attributes:
551
+ task:
552
+ title: 'Title'
553
+ due_date: 'Due date'
554
+ completed: 'Completed?'
555
+ table:
556
+ actions:
557
+ task:
558
+ title: 'Actions'
559
+ show: 'Details'
560
+ edit: 'Edit task'
561
+ destroy: 'Delete'
562
+ ```
220
563
 
@@ -1,7 +1,7 @@
1
1
  require 'admino/query/base'
2
2
  require 'admino/query/base_presenter'
3
3
  require 'admino/query/configuration'
4
- require 'admino/query/field'
4
+ require 'admino/query/search_field'
5
5
  require 'admino/query/filter_group'
6
6
  require 'admino/query/filter_group_presenter'
7
7
  require 'admino/query/sorting'
@@ -14,7 +14,7 @@ module Admino
14
14
 
15
15
  attr_reader :params
16
16
  attr_reader :filter_groups
17
- attr_reader :fields
17
+ attr_reader :search_fields
18
18
  attr_reader :sorting
19
19
 
20
20
  def self.i18n_scope
@@ -26,7 +26,7 @@ module Admino
26
26
  @config = config
27
27
 
28
28
  init_filter_groups
29
- init_fields
29
+ init_search_fields
30
30
  init_sorting
31
31
  end
32
32
 
@@ -39,11 +39,11 @@ module Admino
39
39
 
40
40
  scope_builder = starting_scope
41
41
 
42
- scope_augmenters = fields + filter_groups
42
+ scope_augmenters = search_fields + filter_groups
43
43
  scope_augmenters << sorting if sorting
44
44
 
45
- scope_augmenters.each do |field|
46
- scope_builder = field.augment_scope(scope_builder)
45
+ scope_augmenters.each do |search_field|
46
+ scope_builder = search_field.augment_scope(scope_builder)
47
47
  end
48
48
 
49
49
  if config.ending_scope_callable
@@ -69,12 +69,12 @@ module Admino
69
69
  @filter_groups[name]
70
70
  end
71
71
 
72
- def fields
73
- @fields.values
72
+ def search_fields
73
+ @search_fields.values
74
74
  end
75
75
 
76
- def field_by_name(name)
77
- @fields[name]
76
+ def search_field_by_name(name)
77
+ @search_fields[name]
78
78
  end
79
79
 
80
80
  private
@@ -87,16 +87,17 @@ module Admino
87
87
  end
88
88
  end
89
89
 
90
- def init_fields
91
- @fields = {}
92
- config.fields.each do |config|
93
- @fields[config.name] = Field.new(config, params)
90
+ def init_search_fields
91
+ @search_fields = {}
92
+ config.search_fields.each do |config|
93
+ @search_fields[config.name] = SearchField.new(config, params)
94
94
  end
95
95
  end
96
96
 
97
97
  def init_sorting
98
98
  if config.sorting
99
- @sorting = Sorting.new(config.sorting, params)
99
+ i18n_key = self.class.model_name.i18n_key
100
+ @sorting = Sorting.new(config.sorting, params, i18n_key)
100
101
  end
101
102
  end
102
103
  end
@@ -1,7 +1,7 @@
1
1
  module Admino
2
2
  module Query
3
3
  class Configuration
4
- class Field
4
+ class SearchField
5
5
  attr_reader :name
6
6
  attr_reader :coerce_to
7
7
 
@@ -47,20 +47,20 @@ module Admino
47
47
  end
48
48
  end
49
49
 
50
- attr_reader :fields
50
+ attr_reader :search_fields
51
51
  attr_reader :filter_groups
52
52
  attr_reader :sorting
53
53
  attr_accessor :starting_scope_callable
54
54
  attr_accessor :ending_scope_callable
55
55
 
56
56
  def initialize
57
- @fields = []
57
+ @search_fields = []
58
58
  @filter_groups = []
59
59
  end
60
60
 
61
- def add_field(name, options = {})
62
- Field.new(name, options).tap do |field|
63
- self.fields << field
61
+ def add_search_field(name, options = {})
62
+ SearchField.new(name, options).tap do |search_field|
63
+ self.search_fields << search_field
64
64
  end
65
65
  end
66
66
 
@@ -5,11 +5,11 @@ module Admino
5
5
  @config ||= Admino::Query::Configuration.new
6
6
  end
7
7
 
8
- def field(name, options = {})
9
- config.add_field(name, options)
8
+ def search_field(name, options = {})
9
+ config.add_search_field(name, options)
10
10
 
11
11
  define_method name do
12
- field_by_name(name).value
12
+ search_field_by_name(name).value
13
13
  end
14
14
  end
15
15
 
@@ -56,7 +56,7 @@ module Admino
56
56
  scope: 'query.filter_groups',
57
57
  default: [
58
58
  :"#{i18n_key}.name",
59
- i18n_key.to_s.titleize
59
+ i18n_key.to_s.titleize.capitalize
60
60
  ]
61
61
  )
62
62
  end
@@ -4,7 +4,7 @@ require 'active_support/core_ext/hash'
4
4
 
5
5
  module Admino
6
6
  module Query
7
- class Field
7
+ class SearchField
8
8
  attr_reader :params
9
9
  attr_reader :config
10
10
 
@@ -7,8 +7,9 @@ module Admino
7
7
  class Sorting
8
8
  attr_reader :params
9
9
  attr_reader :config
10
+ attr_reader :query_i18n_key
10
11
 
11
- def initialize(config, params)
12
+ def initialize(config, params, query_i18n_key = nil)
12
13
  @config = config
13
14
  @params = ActiveSupport::HashWithIndifferentAccess.new(params)
14
15
  end
@@ -3,9 +3,11 @@ require 'showcase'
3
3
  module Admino
4
4
  module Query
5
5
  class SortingPresenter < Showcase::Presenter
6
- def scope_link(scope, label, *args)
6
+ def scope_link(scope, *args)
7
7
  options = args.extract_options!
8
8
 
9
+ label = args.first || scope_name(scope)
10
+
9
11
  desc_class = options.delete(:desc_class) { 'is-desc' }
10
12
  asc_class = options.delete(:asc_class) { 'is-asc' }
11
13
 
@@ -35,6 +37,14 @@ module Admino
35
37
 
36
38
  params
37
39
  end
40
+
41
+ def scope_name(scope)
42
+ I18n.t(
43
+ :"#{query_i18n_key}.#{scope}",
44
+ scope: 'query.sorting_scopes',
45
+ default: scope.to_s.titleize.capitalize
46
+ )
47
+ end
38
48
  end
39
49
  end
40
50
  end
@@ -20,7 +20,10 @@ module Admino
20
20
  label = I18n.t(
21
21
  :"#{resource_klass.model_name.i18n_key}.title",
22
22
  scope: 'table.actions',
23
- default: [ :title, 'Actions' ]
23
+ default: [
24
+ :title,
25
+ 'Actions'
26
+ ]
24
27
  )
25
28
 
26
29
  @columns << h.content_tag(:th, label.to_s, default_options)
@@ -59,9 +62,7 @@ module Admino
59
62
  private
60
63
 
61
64
  def column_html_options(attribute_name)
62
- if attribute_name
63
- { role: attribute_name.to_s.gsub(/_/, '-') }
64
- end
65
+ { role: attribute_name.to_s.gsub(/_/, '-') }
65
66
  end
66
67
  end
67
68
  end
@@ -9,12 +9,12 @@ module Admino
9
9
  attr_reader :query
10
10
 
11
11
  def self.tag_helper(name, tag, options = {})
12
- default_options_method = :"#{name}_html_options"
12
+ options_method = :"#{name}_html_options"
13
13
 
14
14
  define_method :"#{name}_tag" do |*args, &block|
15
15
  options = args.extract_options!
16
- if respond_to?(default_options_method, true)
17
- default_options = send(default_options_method, *args)
16
+ if respond_to?(options_method, true)
17
+ default_options = send(options_method, *args)
18
18
  html_options = Showcase::Helpers::HtmlOptions.new(default_options)
19
19
  html_options.merge_attrs!(options)
20
20
  options = html_options.to_h
@@ -50,11 +50,8 @@ module Admino
50
50
  end <<
51
51
  tbody_tag do
52
52
  collection.each_with_index.map do |resource, index|
53
- tr_html_options = {
54
- class: zebra_css_classes[index % zebra_css_classes.size],
55
- id: resource.dom_id
56
- }
57
- tbody_tr_tag(resource, index, tr_html_options) do
53
+ html_options = base_tbody_tr_html_options(resource, index)
54
+ tbody_tr_tag(resource, index, html_options) do
58
55
  row = resource_row(resource, view_context)
59
56
  h.capture(row, resource, &block) if block_given?
60
57
  row.to_html
@@ -67,7 +64,7 @@ module Admino
67
64
  private
68
65
 
69
66
  def collection
70
- @collection ||= present_collection(object)
67
+ object
71
68
  end
72
69
 
73
70
  def head_row(collection_klass, query, view_context)
@@ -78,6 +75,18 @@ module Admino
78
75
  ResourceRow.new(resource, view_context)
79
76
  end
80
77
 
78
+ def base_tbody_tr_html_options(resource, index)
79
+ options = {
80
+ class: zebra_css_classes[index % zebra_css_classes.size]
81
+ }
82
+
83
+ if resource.respond_to?(:dom_id)
84
+ options[:id] = resource.dom_id
85
+ end
86
+
87
+ options
88
+ end
89
+
81
90
  def zebra_css_classes
82
91
  %w(is-even is-odd)
83
92
  end
@@ -119,15 +119,11 @@ module Admino
119
119
  private
120
120
 
121
121
  def action_html_options(action_name)
122
- if action_name
123
- { role: action_name.to_s.gsub(/_/, '-') }
124
- end
122
+ { role: action_name.to_s.gsub(/_/, '-') }
125
123
  end
126
124
 
127
125
  def column_html_options(attribute_name)
128
- if attribute_name
129
- { role: attribute_name.to_s.gsub(/_/, '-') }
130
- end
126
+ { role: attribute_name.to_s.gsub(/_/, '-') }
131
127
  end
132
128
  end
133
129
  end
@@ -1,4 +1,4 @@
1
1
  module Admino
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.5"
3
3
  end
4
4
 
@@ -19,18 +19,18 @@ module Admino
19
19
  end
20
20
  end
21
21
 
22
- context 'with a declared field' do
22
+ context 'with a declared search_field' do
23
23
  let(:config) { Configuration.new }
24
- let(:field_config) { config.add_field(:field) }
24
+ let(:search_field_config) { config.add_search_field(:search_field) }
25
25
 
26
26
  before do
27
- field_config
27
+ search_field_config
28
28
  end
29
29
 
30
- it 'returns a configured Field' do
31
- field = query.field_by_name(:field)
32
- expect(field.config).to eq field_config
33
- expect(field.params).to eq params
30
+ it 'returns a configured SearchField' do
31
+ search_field = query.search_field_by_name(:search_field)
32
+ expect(search_field.config).to eq search_field_config
33
+ expect(search_field.params).to eq params
34
34
  end
35
35
  end
36
36
 
@@ -107,25 +107,25 @@ module Admino
107
107
  end
108
108
  end
109
109
 
110
- context 'with a set of fields and filter_groups' do
111
- let(:field_config) { config.add_field(:field) }
110
+ context 'with a set of search_fields and filter_groups' do
111
+ let(:search_field_config) { config.add_search_field(:search_field) }
112
112
  let(:filter_group_config) { config.add_filter_group(:filter_group, [:one, :two]) }
113
- let(:scope_chained_with_field) { double('scope 1') }
113
+ let(:scope_chained_with_search_field) { double('scope 1') }
114
114
  let(:final_chain) { double('scope 2') }
115
115
 
116
116
  before do
117
- field_config
117
+ search_field_config
118
118
  filter_group_config
119
119
  query
120
120
 
121
- query.field_by_name(:field).
121
+ query.search_field_by_name(:search_field).
122
122
  stub(:augment_scope).
123
123
  with(starting_scope).
124
- and_return(scope_chained_with_field)
124
+ and_return(scope_chained_with_search_field)
125
125
 
126
126
  query.filter_group_by_name(:filter_group).
127
127
  stub(:augment_scope).
128
- with(scope_chained_with_field).
128
+ with(scope_chained_with_search_field).
129
129
  and_return(final_chain)
130
130
  end
131
131
 
@@ -6,10 +6,10 @@ module Admino
6
6
  let(:config) { TestQuery.config }
7
7
  let(:instance) { TestQuery.new }
8
8
 
9
- it 'allows #field declaration' do
10
- field = config.fields.last
11
- expect(field.name).to eq :starting_from
12
- expect(field.coerce_to).to eq :to_date
9
+ it 'allows #search_field declaration' do
10
+ search_field = config.search_fields.last
11
+ expect(search_field.name).to eq :starting_from
12
+ expect(search_field.coerce_to).to eq :to_date
13
13
  end
14
14
 
15
15
  it 'allows #filter_by declaration' do
@@ -33,13 +33,13 @@ module Admino
33
33
  expect(config.ending_scope_callable.call).to eq 'end'
34
34
  end
35
35
 
36
- context 'with a field' do
37
- let(:field) { double('Field', value: 'value') }
36
+ context 'with a search_field' do
37
+ let(:search_field) { double('SearchField', value: 'value') }
38
38
 
39
39
  before do
40
- instance.stub(:field_by_name).
40
+ instance.stub(:search_field_by_name).
41
41
  with(:foo).
42
- and_return(field)
42
+ and_return(search_field)
43
43
  end
44
44
 
45
45
  it 'it generates a getter' do
@@ -123,7 +123,7 @@ module Admino
123
123
 
124
124
  context 'if no translation is available' do
125
125
  it 'falls back to a titleized version of the filter_group name' do
126
- expect(presenter.name).to eq 'Filter Group'
126
+ expect(presenter.name).to eq 'Filter group'
127
127
  end
128
128
  end
129
129
  end
@@ -37,7 +37,7 @@ module Admino
37
37
  let(:result) { filter_group.augment_scope(scope) }
38
38
  let(:scope) { ScopeMock.new('original') }
39
39
 
40
- context 'if the field has a value' do
40
+ context 'if the search_field has a value' do
41
41
  let(:params) { { 'foo' => 'bar' } }
42
42
 
43
43
  it 'returns the original scope chained with the filter_group scope' do
@@ -2,36 +2,36 @@ require 'spec_helper'
2
2
 
3
3
  module Admino
4
4
  module Query
5
- describe Field do
6
- subject(:field) { Field.new(config, params) }
7
- let(:config) { Configuration::Field.new(:foo) }
5
+ describe SearchField do
6
+ subject(:search_field) { SearchField.new(config, params) }
7
+ let(:config) { Configuration::SearchField.new(:foo) }
8
8
  let(:params) { {} }
9
9
 
10
10
  describe '#value' do
11
11
  context 'with a value' do
12
12
  let(:params) { { 'query' => { 'foo' => 'bar' } } }
13
13
 
14
- it 'returns the param value for the field' do
15
- expect(field.value).to eq 'bar'
14
+ it 'returns the param value for the search_field' do
15
+ expect(search_field.value).to eq 'bar'
16
16
  end
17
17
  end
18
18
 
19
19
  context 'else' do
20
20
  it 'returns nil' do
21
- expect(field.value).to be_nil
21
+ expect(search_field.value).to be_nil
22
22
  end
23
23
  end
24
24
 
25
25
  context 'with coertion' do
26
26
  let(:config) {
27
- Configuration::Field.new(:foo, coerce: :to_date)
27
+ Configuration::SearchField.new(:foo, coerce: :to_date)
28
28
  }
29
29
 
30
30
  context 'with a possible coertion' do
31
31
  let(:params) { { 'query' => { 'foo' => '2014-10-05' } } }
32
32
 
33
- it 'returns the coerced param value for the field' do
34
- expect(field.value).to be_a Date
33
+ it 'returns the coerced param value for the search_field' do
34
+ expect(search_field.value).to be_a Date
35
35
  end
36
36
  end
37
37
 
@@ -39,20 +39,20 @@ module Admino
39
39
  let(:params) { { 'query' => { 'foo' => '' } } }
40
40
 
41
41
  it 'returns nil' do
42
- expect(field.value).to be_nil
42
+ expect(search_field.value).to be_nil
43
43
  end
44
44
  end
45
45
  end
46
46
  end
47
47
 
48
48
  describe '#augment_scope' do
49
- let(:result) { field.augment_scope(scope) }
49
+ let(:result) { search_field.augment_scope(scope) }
50
50
  let(:scope) { ScopeMock.new('original') }
51
51
 
52
- context 'if the field has a value' do
52
+ context 'if the search_field has a value' do
53
53
  let(:params) { { 'query' => { 'foo' => 'bar' } } }
54
54
 
55
- it 'returns the original scope chained with the field scope' do
55
+ it 'returns the original scope chained with the search_field scope' do
56
56
  expect(result.chain).to eq [:foo, ['bar']]
57
57
  end
58
58
  end
@@ -5,7 +5,13 @@ module Admino
5
5
  describe SortingPresenter do
6
6
  subject(:presenter) { SortingPresenter.new(sorting, view) }
7
7
  let(:view) { RailsViewContext.new }
8
- let(:sorting) { double('Sorting', default_scope: 'by_name') }
8
+ let(:sorting) do
9
+ double(
10
+ 'Sorting',
11
+ default_scope: 'by_name',
12
+ query_i18n_key: 'query_name'
13
+ )
14
+ end
9
15
  let(:request_object) do
10
16
  double(
11
17
  'ActionDispatch::Request',
@@ -161,6 +167,27 @@ module Admino
161
167
  end
162
168
  end
163
169
  end
170
+
171
+ describe '#scope_name' do
172
+ context do
173
+ before do
174
+ I18n.backend.store_translations(
175
+ :en,
176
+ query: { sorting_scopes: { query_name: { by_name: 'Sort by name' } } }
177
+ )
178
+ end
179
+
180
+ it 'returns a I18n translatable name for the scope' do
181
+ expect(presenter.scope_name(:by_name)).to eq 'Sort by name'
182
+ end
183
+ end
184
+
185
+ context 'if no translation is available' do
186
+ it 'falls back to a titleized version of the scope name' do
187
+ expect(presenter.scope_name(:by_name)).to eq 'By name'
188
+ end
189
+ end
190
+ end
164
191
  end
165
192
  end
166
193
  end
@@ -73,7 +73,7 @@ module Admino
73
73
  context 'with "desc" value' do
74
74
  let(:params) { { 'sort_order' => 'desc' } }
75
75
 
76
- it 'returns the param value for the field' do
76
+ it 'returns the param value for the search_field' do
77
77
  expect(sorting).not_to be_ascending
78
78
  end
79
79
  end
@@ -92,7 +92,7 @@ module Admino
92
92
  let(:result) { sorting.augment_scope(scope) }
93
93
  let(:scope) { ScopeMock.new('original') }
94
94
 
95
- context 'if the field has a value' do
95
+ context 'if the search_field has a value' do
96
96
  let(:params) { { 'sorting' => 'by_title', 'sort_order' => 'desc' } }
97
97
 
98
98
  it 'returns the original scope chained with the current scope' do
@@ -11,20 +11,15 @@ module Admino
11
11
 
12
12
  let(:collection) { [ first_post, second_post ] }
13
13
  let(:first_post) { Post.new('1') }
14
- let(:first_post_presenter) { double('PresentedPost', dom_id: 'post_1') }
15
14
  let(:second_post) { Post.new('2') }
16
- let(:second_post_presenter) { double('PresentedPost', dom_id: 'post_2') }
17
15
 
18
16
  let(:head_row) { double('HeadRow', to_html: '<td id="thead_td"></td>'.html_safe) }
19
17
  let(:resource_row) { double('ResourceRow', to_html: '<td id="tbody_td"></td>'.html_safe) }
20
18
 
21
19
  before do
22
- PostPresenter.stub(:new).with(first_post, view).and_return(first_post_presenter)
23
- PostPresenter.stub(:new).with(second_post, view).and_return(second_post_presenter)
24
-
25
20
  HeadRow.stub(:new).with(Post, query, view).and_return(head_row)
26
- ResourceRow.stub(:new).with(first_post_presenter, view).and_return(resource_row)
27
- ResourceRow.stub(:new).with(second_post_presenter, view).and_return(resource_row)
21
+ ResourceRow.stub(:new).with(first_post, view).and_return(resource_row)
22
+ ResourceRow.stub(:new).with(second_post, view).and_return(resource_row)
28
23
  end
29
24
 
30
25
  describe '#.to_html' do
@@ -78,8 +73,8 @@ module Admino
78
73
  end
79
74
 
80
75
  it 'calls it once for each collection member passing the ResourceRow instance and the member itself' do
81
- expect(block_call_args[1]).to eq [resource_row, first_post_presenter]
82
- expect(block_call_args[2]).to eq [resource_row, second_post_presenter]
76
+ expect(block_call_args[1]).to eq [resource_row, first_post]
77
+ expect(block_call_args[2]).to eq [resource_row, second_post]
83
78
  end
84
79
  end
85
80
 
@@ -32,8 +32,8 @@ class ScopeMock
32
32
  end
33
33
 
34
34
  class TestQuery < Admino::Query::Base
35
- field :foo
36
- field :starting_from, coerce: :to_date
35
+ search_field :foo
36
+ search_field :starting_from, coerce: :to_date
37
37
 
38
38
  filter_by :bar, [:one, :two]
39
39
 
@@ -45,7 +45,7 @@ class TestQuery < Admino::Query::Base
45
45
  ending_scope { 'end' }
46
46
  end
47
47
 
48
- class Post < Struct.new(:key)
48
+ class Post < Struct.new(:key, :dom_id)
49
49
  extend ActiveModel::Naming
50
50
  extend ActiveModel::Translation
51
51
 
@@ -64,12 +64,10 @@ class Post < Struct.new(:key)
64
64
  def to_key
65
65
  [key]
66
66
  end
67
- end
68
-
69
- require 'showcase/traits'
70
67
 
71
- class PostPresenter < Showcase::Presenter
72
- include Showcase::Traits::Record
68
+ def dom_id
69
+ "post_#{key}"
70
+ end
73
71
  end
74
72
 
75
73
  require 'action_view'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: admino
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefano Verna
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-19 00:00:00.000000000 Z
11
+ date: 2014-03-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: showcase
@@ -187,9 +187,9 @@ files:
187
187
  - lib/admino/query/base_presenter.rb
188
188
  - lib/admino/query/configuration.rb
189
189
  - lib/admino/query/dsl.rb
190
- - lib/admino/query/field.rb
191
190
  - lib/admino/query/filter_group.rb
192
191
  - lib/admino/query/filter_group_presenter.rb
192
+ - lib/admino/query/search_field.rb
193
193
  - lib/admino/query/sorting.rb
194
194
  - lib/admino/query/sorting_presenter.rb
195
195
  - lib/admino/table.rb
@@ -201,9 +201,9 @@ files:
201
201
  - spec/admino/query/base_presenter_spec.rb
202
202
  - spec/admino/query/base_spec.rb
203
203
  - spec/admino/query/dsl_spec.rb
204
- - spec/admino/query/field_spec.rb
205
204
  - spec/admino/query/filter_group_presenter_spec.rb
206
205
  - spec/admino/query/filter_group_spec.rb
206
+ - spec/admino/query/search_field_spec.rb
207
207
  - spec/admino/query/sorting_presenter_spec.rb
208
208
  - spec/admino/query/sorting_spec.rb
209
209
  - spec/admino/table/head_row_spec.rb
@@ -239,9 +239,9 @@ test_files:
239
239
  - spec/admino/query/base_presenter_spec.rb
240
240
  - spec/admino/query/base_spec.rb
241
241
  - spec/admino/query/dsl_spec.rb
242
- - spec/admino/query/field_spec.rb
243
242
  - spec/admino/query/filter_group_presenter_spec.rb
244
243
  - spec/admino/query/filter_group_spec.rb
244
+ - spec/admino/query/search_field_spec.rb
245
245
  - spec/admino/query/sorting_presenter_spec.rb
246
246
  - spec/admino/query/sorting_spec.rb
247
247
  - spec/admino/table/head_row_spec.rb