administrate 0.17.0 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +0 -2
  3. data/app/assets/javascripts/administrate/application.js +0 -2
  4. data/app/assets/javascripts/administrate/components/select.js +3 -0
  5. data/app/assets/stylesheets/administrate/application.scss +0 -1
  6. data/app/assets/stylesheets/administrate/base/_forms.scss +1 -1
  7. data/app/assets/stylesheets/administrate/components/_buttons.scss +9 -0
  8. data/app/assets/stylesheets/administrate/components/_flashes.scss +2 -2
  9. data/app/assets/stylesheets/administrate/library/_variables.scss +1 -1
  10. data/app/controllers/administrate/application_controller.rb +84 -17
  11. data/app/controllers/concerns/administrate/punditize.rb +41 -13
  12. data/app/helpers/administrate/application_helper.rb +24 -6
  13. data/app/views/administrate/application/_collection.html.erb +5 -6
  14. data/app/views/administrate/application/_collection_header_actions.html.erb +2 -2
  15. data/app/views/administrate/application/_collection_item_actions.html.erb +4 -4
  16. data/app/views/administrate/application/_index_header.html.erb +2 -2
  17. data/app/views/administrate/application/_navigation.html.erb +2 -2
  18. data/app/views/administrate/application/_pagination.html.erb +1 -0
  19. data/app/views/administrate/application/edit.html.erb +2 -2
  20. data/app/views/administrate/application/index.html.erb +1 -1
  21. data/app/views/administrate/application/new.html.erb +1 -1
  22. data/app/views/administrate/application/show.html.erb +10 -2
  23. data/app/views/fields/belongs_to/_index.html.erb +1 -1
  24. data/app/views/fields/belongs_to/_show.html.erb +1 -1
  25. data/app/views/fields/date/_form.html.erb +1 -3
  26. data/app/views/fields/date_time/_form.html.erb +1 -3
  27. data/app/views/fields/has_many/_index.html.erb +1 -1
  28. data/app/views/fields/has_many/_show.html.erb +2 -1
  29. data/app/views/fields/has_one/_form.html.erb +1 -1
  30. data/app/views/fields/has_one/_index.html.erb +2 -1
  31. data/app/views/fields/has_one/_show.html.erb +3 -2
  32. data/app/views/fields/polymorphic/_index.html.erb +2 -1
  33. data/app/views/fields/polymorphic/_show.html.erb +1 -1
  34. data/app/views/fields/select/_form.html.erb +5 -18
  35. data/app/views/fields/time/_form.html.erb +2 -3
  36. data/app/views/fields/url/_index.html.erb +1 -1
  37. data/app/views/fields/url/_show.html.erb +1 -1
  38. data/app/views/layouts/administrate/application.html.erb +1 -1
  39. data/config/locales/administrate.de.yml +2 -2
  40. data/config/locales/administrate.ja.yml +5 -5
  41. data/config/locales/administrate.sl.yml +30 -0
  42. data/docs/adding_controllers_without_related_model.md +2 -2
  43. data/docs/authorization.md +43 -20
  44. data/docs/customizing_controller_actions.md +11 -6
  45. data/docs/customizing_dashboards.md +33 -8
  46. data/docs/getting_started.md +1 -1
  47. data/docs/guides/customising_search.md +149 -0
  48. data/docs/guides/hiding_dashboards_from_sidebar.md +4 -2
  49. data/docs/guides/scoping_has_many_relations.md +27 -0
  50. data/docs/guides.md +3 -1
  51. data/lib/administrate/base_dashboard.rb +23 -2
  52. data/lib/administrate/engine.rb +2 -2
  53. data/lib/administrate/field/associative.rb +18 -8
  54. data/lib/administrate/field/base.rb +4 -0
  55. data/lib/administrate/field/belongs_to.rb +9 -2
  56. data/lib/administrate/field/deferred.rb +4 -0
  57. data/lib/administrate/field/has_many.rb +15 -5
  58. data/lib/administrate/field/has_one.rb +4 -0
  59. data/lib/administrate/field/polymorphic.rb +2 -1
  60. data/lib/administrate/field/select.rb +19 -9
  61. data/lib/administrate/field/url.rb +4 -0
  62. data/lib/administrate/not_authorized_error.rb +20 -0
  63. data/lib/administrate/order.rb +68 -13
  64. data/lib/administrate/page/base.rb +4 -0
  65. data/lib/administrate/page/form.rb +0 -7
  66. data/lib/administrate/resource_resolver.rb +2 -1
  67. data/lib/administrate/search.rb +1 -1
  68. data/lib/administrate/version.rb +1 -1
  69. data/lib/administrate/view_generator.rb +6 -1
  70. data/lib/administrate.rb +18 -0
  71. data/lib/generators/administrate/dashboard/dashboard_generator.rb +20 -2
  72. data/lib/generators/administrate/dashboard/templates/controller.rb.erb +2 -2
  73. data/lib/generators/administrate/install/install_generator.rb +6 -1
  74. data/lib/generators/administrate/routes/routes_generator.rb +11 -2
  75. data/lib/generators/administrate/test_record.rb +21 -0
  76. metadata +10 -35
  77. data/app/assets/javascripts/administrate/components/date_time_picker.js +0 -14
  78. data/config/i18n-tasks.yml +0 -18
  79. data/config/routes.rb +0 -2
  80. data/config/unicorn.rb +0 -25
@@ -16,7 +16,8 @@ By default, the relationship is rendered as a link to the associated object.
16
16
  %>
17
17
 
18
18
  <% if field.linkable? %>
19
- <%= link_to(
19
+ <%= link_to_if(
20
+ accessible_action?(field.data, :show),
20
21
  field.display_associated_resource,
21
22
  [namespace, field.data],
22
23
  ) %>
@@ -18,7 +18,8 @@ All show page attributes of has_one relationship would be rendered
18
18
  <% if field.linkable? %>
19
19
  <fieldset class="attribute--nested">
20
20
  <legend>
21
- <%= link_to(
21
+ <%= link_to_if(
22
+ accessible_action?(field.data, :show),
22
23
  field.display_associated_resource,
23
24
  [namespace, field.data],
24
25
  ) %>
@@ -27,7 +28,7 @@ All show page attributes of has_one relationship would be rendered
27
28
  <div>
28
29
  <dt class="attribute-label">
29
30
  <%= t(
30
- "helpers.label.#{resource_name}.#{attribute.name}",
31
+ "helpers.label.#{field.associated_class_name.underscore}.#{attribute.name}",
31
32
  default: attribute.name.titleize,
32
33
  ) %>
33
34
  </dt>
@@ -17,7 +17,8 @@ By default, the relationship is rendered as a link to the associated object.
17
17
  %>
18
18
 
19
19
  <% if field.data %>
20
- <%= link_to(
20
+ <%= link_to_if(
21
+ accessible_action?(field.data, :show),
21
22
  field.display_associated_resource,
22
23
  [namespace, field.data]
23
24
  ) %>
@@ -17,7 +17,7 @@ By default, the relationship is rendered as a link to the associated object.
17
17
  %>
18
18
 
19
19
  <% if field.data %>
20
- <% if valid_action?(:show, field.data.class) %>
20
+ <% if accessible_action?(field.data, :show) %>
21
21
  <%= link_to(
22
22
  field.display_associated_resource,
23
23
  [namespace, field.data],
@@ -19,27 +19,14 @@ to be displayed on a resource's edit form page.
19
19
  <%= f.label field.attribute %>
20
20
  </div>
21
21
  <div class="field-unit__field">
22
- <% if field.selectable_options.first&.is_a?(Array) %>
23
- <%= f.select(
22
+ <%=
23
+ f.select(
24
24
  field.attribute,
25
- options_from_collection_for_select(
25
+ options_for_select(
26
26
  field.selectable_options,
27
- :last,
28
- :first,
29
27
  field.data,
30
28
  ),
31
29
  include_blank: field.include_blank_option
32
- ) %>
33
- <% else %>
34
- <%= f.select(
35
- field.attribute,
36
- options_from_collection_for_select(
37
- field.selectable_options,
38
- :to_s,
39
- :to_s,
40
- field.data,
41
- ),
42
- include_blank: field.include_blank_option
43
- ) %>
44
- <% end %>
30
+ )
31
+ %>
45
32
  </div>
@@ -2,7 +2,6 @@
2
2
  # Time Form Partial
3
3
 
4
4
  This partial renders an input element for time attributes.
5
- By default, the input is a text field that is augmented with [DateTimePicker].
6
5
 
7
6
  ## Local variables:
8
7
 
@@ -12,12 +11,12 @@ By default, the input is a text field that is augmented with [DateTimePicker].
12
11
  An instance of [Administrate::Field::Time][1].
13
12
  A wrapper around the tmie attributes pulled from the model.
14
13
 
15
- [DateTimePicker]: https://github.com/Eonasdan/bootstrap-datetimepicker
14
+ [1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/Time
16
15
  %>
17
16
 
18
17
  <div class="field-unit__label">
19
18
  <%= f.label field.attribute %>
20
19
  </div>
21
20
  <div class="field-unit__field">
22
- <%= f.text_field field.attribute, data: { type: 'time' }, value: field.data&.strftime("%H:%M:%S") %>
21
+ <%= f.time_field field.attribute, step: 1 %>
23
22
  </div>
@@ -15,6 +15,6 @@ By default, the value is rendered as an `a` element.
15
15
  [1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/Url
16
16
  %>
17
17
 
18
- <%= content_tag :a, href: field.data do %>
18
+ <%= content_tag :a, href: field.data, **field.html_options do %>
19
19
  <%= field.data %>
20
20
  <% end %>
@@ -15,6 +15,6 @@ By default, the value is rendered as an `a` element.
15
15
  [1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/Url
16
16
  %>
17
17
 
18
- <%= content_tag :a, href: field.data do %>
18
+ <%= content_tag :a, href: field.data, **field.html_options do %>
19
19
  <%= field.data %>
20
20
  <% end %>
@@ -31,7 +31,7 @@ By default, it renders:
31
31
  <div class="app-container">
32
32
  <%= render "navigation" -%>
33
33
 
34
- <main class="main-content" role="main">
34
+ <main class="main-content">
35
35
  <%= render "flashes" -%>
36
36
  <%= yield %>
37
37
  </main>
@@ -21,8 +21,8 @@ de:
21
21
  more: "%{count} von %{total_count}"
22
22
  none: Keine
23
23
  form:
24
- error: error
25
- errors: "%{pluralized_errors} haben das Speichern dieses %{resource_name} verhindert:"
24
+ error: Fehler
25
+ errors: "%{resource_name} konnte nicht gespeichert werden, es gab %{pluralized_errors}."
26
26
  navigation:
27
27
  back_to_app: Zurück zur App
28
28
  search:
@@ -5,9 +5,9 @@ ja:
5
5
  confirm: 本当によろしいですか?
6
6
  destroy: 削除
7
7
  edit: 編集
8
- edit_resource: 編集 %{name}
9
- show_resource: 参照 %{name}
10
- new_resource: 新規 %{name}
8
+ edit_resource: "%{name}を編集"
9
+ show_resource: "%{name}を参照"
10
+ new_resource: "%{name}を作成"
11
11
  back: 戻る
12
12
  controller:
13
13
  create:
@@ -22,9 +22,9 @@ ja:
22
22
  none: データがありません
23
23
  form:
24
24
  error: エラー
25
- errors: "%{pluralized_errors}のため%{resource_name}を保存できません。"
25
+ errors: "%{pluralized_errors}のため%{resource_name}を保存できませんでした。"
26
26
  navigation:
27
27
  back_to_app: アプリに戻る
28
28
  search:
29
29
  clear: 検索をクリアする
30
- label: サーチ %{resource}
30
+ label: "%{resource}を検索"
@@ -0,0 +1,30 @@
1
+ ---
2
+ sl:
3
+ administrate:
4
+ actions:
5
+ confirm: Ali ste preričani?
6
+ destroy: Izbriši
7
+ edit: Uredi
8
+ edit_resource: Uredi %{name}
9
+ show_resource: Prikaži %{name}
10
+ new_resource: Dodaj %{name}
11
+ back: Nazaj
12
+ controller:
13
+ create:
14
+ success: "%{resource} je dodan."
15
+ destroy:
16
+ success: "%{resource} je izbrisan."
17
+ update:
18
+ success: "%{resource} je posodobljen."
19
+ fields:
20
+ has_many:
21
+ more: Prikazanih %{count} od %{total_count}
22
+ none: Nobene
23
+ form:
24
+ error: napaka
25
+ errors: "%{resource_name} ni mogoče shraniti zaradi:"
26
+ navigation:
27
+ back_to_app: Nazaj v aplikacijo
28
+ search:
29
+ clear: Počisti iskanje
30
+ label: Išči %{resource}
@@ -14,9 +14,9 @@ routes are displayed in the sidebar and then add a custom dashboard:
14
14
  <div style="padding: 20px">
15
15
  <h1>Stats</h1>
16
16
  <br>
17
- <p><b>Total Customers:</b> <%= @stats[:customer_count] %></h1>
17
+ <p><b>Total Customers:</b> <%= @stats[:customer_count] %></p>
18
18
  <br>
19
- <p><b>Total Orders:</b> <%= @stats[:order_count] %></h1>
19
+ <p><b>Total Orders:</b> <%= @stats[:order_count] %></p>
20
20
  </div>
21
21
  ```
22
22
 
@@ -28,20 +28,30 @@ technically have access to see in the main app. For example, a user may
28
28
  have all public records in their scope, but you want to only show *their*
29
29
  records in the admin interface to reduce confusion.
30
30
 
31
- In this case, you can add an additional `resolve_admin` to your policy's
32
- scope and Administrate will use this instead of the `resolve` method.
31
+ In this case, you can add additional pundit `policy_namespace` in your controller
32
+ and Administrate will use the namespaced pundit policy instead.
33
33
 
34
34
  For example:
35
35
 
36
36
  ```ruby
37
- class PostPolicy < ApplicationPolicy
38
- class Scope < Scope
39
- def resolve
40
- scope.all
37
+ # app/controllers/admin/posts_controller.rb
38
+ module Admin
39
+ class PostsController < ApplicationController
40
+ include Administrate::Punditize
41
+
42
+ def policy_namespace
43
+ [:admin]
41
44
  end
45
+ end
46
+ end
42
47
 
43
- def resolve_admin
44
- scope.where(owner: user)
48
+ # app/policies/admin/post_policy.rb
49
+ module Admin
50
+ class PostPolicy < ApplicationPolicy
51
+ class Scope < Scope
52
+ def resolve
53
+ scope.where(owner: user)
54
+ end
45
55
  end
46
56
  end
47
57
  end
@@ -49,23 +59,36 @@ end
49
59
 
50
60
  ## Authorization without Pundit
51
61
 
52
- If you use a different authorization library, or you want to roll your own,
53
- you just need to override a few methods in your controllers or
54
- `Admin::ApplicationController`. For example:
62
+ Pundit is not necessary to implement authorization within Administrate. It is
63
+ simply a common solution that many in the community use, and for this reason
64
+ Administrate provides a plugin to work with it. However you can use a different
65
+ solution or roll out your own.
66
+
67
+ To integrate a different authorization solution, you will need to
68
+ implement some methods in `Admin::ApplicationController`
69
+ or its subclasses.
70
+
71
+ These are the methods to override, with examples:
55
72
 
56
73
  ```ruby
57
- # Limit the scope of the given resource
74
+ # Used in listings, such as the `index` actions. It
75
+ # restricts the scope of records that a user can access.
76
+ # Returns an ActiveRecord scope.
58
77
  def scoped_resource
59
78
  super.where(user: current_user)
60
79
  end
61
80
 
62
- # Raise an exception if the user is not permitted to access this resource
63
- def authorize_resource(resource)
64
- raise "Erg!" unless show_action?(params[:action], resource)
65
- end
66
-
67
- # Hide links to actions if the user is not allowed to do them
68
- def show_action?(action, resource)
69
- current_user.can? action, resource
81
+ # Return true if the current user can access the given
82
+ # resource, false otherwise.
83
+ def authorized_action?(resource, action)
84
+ current_user.can?(resource, action)
70
85
  end
71
86
  ```
87
+
88
+ Additionally, the method `authorize_resource(resource)`
89
+ should throw an exception if the current user is not
90
+ allowed to access the given resource. Normally
91
+ you wouldn't need to override it, as the default
92
+ implementation uses `authorized_action?` to produce the
93
+ correct behaviour. However you may still want to override it
94
+ if you want to raise a custom error type.
@@ -46,17 +46,22 @@ end
46
46
 
47
47
  ## Customizing Actions
48
48
 
49
- To enable or disable certain actions you could override `valid_action?` method in your dashboard controller like this:
49
+ To disable certain actions globally, you can disable their
50
+ routes in `config/routes.rb`, using the usual Rails
51
+ facilities for this. For example:
50
52
 
51
53
  ```ruby
52
- # disable 'edit' and 'destroy' links
53
- def valid_action?(name, resource = resource_class)
54
- %w[edit destroy].exclude?(name.to_s) && super
54
+ Rails.application.routes.draw do
55
+ # ...
56
+ namespace :admin do
57
+ # ...
58
+
59
+ # Payments can only be listed or displayed
60
+ resources :payments, only: [:index, :show]
61
+ end
55
62
  end
56
63
  ```
57
64
 
58
- Action is one of `new`, `edit`, `show`, `destroy`.
59
-
60
65
  ## Customizing Default Sorting
61
66
 
62
67
  To set the default sorting on the index action you could override `default_sorting_attribute` or `default_sorting_direction` in your dashboard controller like this:
@@ -77,8 +77,9 @@ which are specified through the `.with_options` class method:
77
77
 
78
78
  **Field::BelongsTo**
79
79
 
80
- `:order` - Specifies the order of the dropdown menu, can be ordered by more
81
- than one column. e.g.: `"name, email DESC"`.
80
+ `:order` - Specifies the column used to order the records. It will apply both in
81
+ the table views and in the dropdown menu on the record forms.
82
+ You can set multiple columns as well with direction. E.g.: `"name, email DESC"`.
82
83
 
83
84
  `:scope` - Specifies a custom scope inside a callable. Useful for preloading.
84
85
  Example: `.with_options(scope: -> { MyModel.includes(:rel).limit(5) })`
@@ -112,8 +113,8 @@ association `belongs_to :country`, from your model.
112
113
 
113
114
  **Field::HasMany**
114
115
 
115
- `:limit` - Set the number of resources to display in the show view. Default is
116
- `5`.
116
+ `:limit` - The number of resources (paginated) to display in the show view. To disable pagination,
117
+ set this to `0` or `false`. Default is `5`.
117
118
 
118
119
  `:sort_by` - What to sort the association by in the show view.
119
120
 
@@ -127,6 +128,10 @@ association `belongs_to :country`, from your model.
127
128
 
128
129
  **Field::HasOne**
129
130
 
131
+ `:order` - Specifies the column used to order the records. It will apply both in
132
+ the table views and in the dropdown menu on the record forms.
133
+ You can set multiple columns as well with direction. E.g.: `"name, email DESC"`.
134
+
130
135
  `:searchable` - Specify if the attribute should be considered when searching.
131
136
  Default is `false`.
132
137
 
@@ -217,14 +222,31 @@ objects to display as.
217
222
 
218
223
  **Field::Select**
219
224
 
220
- `:collection` - Specify the options shown on the select field. It accept either
221
- an array or an object responding to `:call`. Defaults to `[]`.
225
+ `:collection` - The options available to select. The format is the same as for Rails's own [`options_for_select`](https://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-options_for_select).
226
+
227
+ If the given value responds to `call`, this will be called and the result used instead. The call will receive an instance of the field as argument. For example:
228
+
229
+ ```ruby
230
+ confirmation: Field::Select.with_options(
231
+ collection: ->(field) {
232
+ person = field.resource
233
+ {
234
+ "no, #{person.name}" => "opt0",
235
+ "yes, #{person.name}" => "opt1",
236
+ "absolutely, #{person.name}" => "opt2",
237
+ }
238
+ },
239
+ )
240
+ ```
241
+
242
+ Administrate will detect if the attribute is an `ActiveRecord::Enum` and extract the available options. Note that if a `collection` is provided it will take precedence.
243
+
244
+ If no collection is provided and no enum can be detected, the list of options will be empty.
222
245
 
223
246
  `:searchable` - Specify if the attribute should be considered when searching.
224
247
  Default is `true`.
225
248
 
226
- `:include_blank` - Specifies if the select element to be rendered should include
227
- blank option. Default is `false`.
249
+ `:include_blank` - Similar to [the option of the same name accepted by Rails helpers](https://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html). If provided, a "blank" option will be added first to the list of options, with the value of `include_blank` as label.
228
250
 
229
251
  **Field::String**
230
252
 
@@ -250,6 +272,9 @@ Default is `true`.
250
272
  `:truncate` - Set the number of characters to display in the index view.
251
273
  Defaults to `50`.
252
274
 
275
+ `:html_options` - Specify anchor tag attributes (e.g., `target="_blank"`).
276
+ Defaults is `{}`.
277
+
253
278
  **Field::Password**
254
279
 
255
280
  `:searchable` - Specify if the attribute should be considered when searching.
@@ -3,7 +3,7 @@ title: Getting Started
3
3
  ---
4
4
 
5
5
  Administrate is released as a Ruby gem, and can be installed on Rails
6
- applications version 5.0 or greater. We support Ruby 2.6 and up.
6
+ applications version 6.0 or greater. We support Ruby 2.7 and up.
7
7
 
8
8
  First, add the following to your Gemfile:
9
9
 
@@ -0,0 +1,149 @@
1
+ ---
2
+ title: Customising the search
3
+ ---
4
+
5
+ Administrate dashboards provide a search function, but it is quite basic.
6
+ Things like search across complex associations, inside JSON columns, or outside
7
+ the database (eg: an Elasticsearch index) are not possible out of the box.
8
+
9
+ Fortunately, Administrate is just Rails, so you can use your existing Rails
10
+ knowledge to customize the search feature. Let's look into that.
11
+
12
+ ## In short
13
+
14
+ Override the `filter_resources` method in your admin controllers in order
15
+ to customize the search.
16
+
17
+ It has two parameters:
18
+
19
+ * `resources`: an ActiveRecord relation for the model on whose dashboard the
20
+ search originated.
21
+ * `search_term:`: a string representing the search query entered by the user.
22
+
23
+ Return an ActiveRecord relation for the same model as `resources`, matching
24
+ the desired search results.
25
+
26
+ ## In more detail
27
+
28
+ When you install Administrate in your application, it generates an admin
29
+ controller for each of your ActiveRecord models, as well as a base controller
30
+ that all of these inherit from.
31
+
32
+ For example, if you have two ActiveRecord models: `Person` and `Address`,
33
+ running `rails generate administrate:install` will get you the following
34
+ files (plus others that are not relevant here):
35
+
36
+ * `app/controllers/admin/people_controller.rb`
37
+ * `app/controllers/admin/addresses_controller.rb`
38
+ * `app/controllers/admin/application_controller.rb`
39
+
40
+ By default, searches are handled by the `index` action of the controller that
41
+ the user was visiting when they performed the search. For example, if a user
42
+ is visiting the People dashboard and submits a search, the user is sent to
43
+ the path `/admin/people?search=<search query>`. This is routed to
44
+ `Admin::PeopleController#index`, where the search query can be read as
45
+ `params[:search]`.
46
+
47
+ By default, these controllers are empty. Administrate's code is implemented
48
+ at `Administrate::ApplicationController`, from which all inherit. This is
49
+ where search is implemented. You can read the code yourself at:
50
+ https://github.com/thoughtbot/administrate/blob/main/app/controllers/administrate/application_controller.rb.
51
+
52
+ It is in the linked code that you can see what Administrate actually does.
53
+ For example, this is the `index` action at the time of writing these lines:
54
+
55
+ ```ruby
56
+ def index
57
+ authorize_resource(resource_class)
58
+ search_term = params[:search].to_s.strip
59
+ resources = filter_resources(scoped_resource, search_term: search_term)
60
+ resources = apply_collection_includes(resources)
61
+ resources = order.apply(resources)
62
+ resources = resources.page(params[:_page]).per(records_per_page)
63
+ page = Administrate::Page::Collection.new(dashboard, order: order)
64
+
65
+ render locals: {
66
+ resources: resources,
67
+ search_term: search_term,
68
+ page: page,
69
+ show_search_bar: show_search_bar?,
70
+ }
71
+ end
72
+ ```
73
+
74
+ What the above does is applying a few transforms
75
+ to the variable `resources`, filtering it, applying includes for associations,
76
+ ordering the results, paginating them, and finally handing them over to the
77
+ template in order to be rendered. All this is pretty standard Rails, although
78
+ split into individual steps that can be overriden by developers in order
79
+ to add customizations, and ultimately wrapped in an instance of
80
+ `Administrate::Page::Collection` which will read your dashboard definitions
81
+ and figure out what fields you want displayed.
82
+
83
+ It is the filtering part where the search is implemented. You will notice the
84
+ `filter_resources` method, which takes a parameter `search_term`. This is what
85
+ this method looks like at the moment:
86
+
87
+ ```ruby
88
+ def filter_resources(resources, search_term:)
89
+ Administrate::Search.new(
90
+ resources,
91
+ dashboard,
92
+ search_term,
93
+ ).run
94
+ end
95
+ ```
96
+
97
+ The class `Administrate::Search` implements the default search facilities
98
+ within Administrate... but you do not have to worry about it! You can ignore
99
+ it and implement your own search in `filter_resources`. For example, you
100
+ could write your own version in your controller, to override Administrate's
101
+ own. Something like this:
102
+
103
+ ```ruby
104
+ def filter_resources(resources, search_term:)
105
+ resources.where(first_name: search_term)
106
+ .or(People.where(last_name: search_term))
107
+ end
108
+ ```
109
+
110
+ It can be as complex (or simple) as you want, as long as the return value
111
+ of the method is an ActiveRecord relation.
112
+
113
+ What if you do not want to search in the DB? For example, say that your records
114
+ are indexed by Elasticsearch or something like that. You can still search
115
+ in your external index and convert the results to an ActiveRecord relation.
116
+ Here's an example:
117
+
118
+ ```ruby
119
+ def filter_resources(resources, search_term:)
120
+ # Run the search term through your search facility
121
+ results = MySuperDuperSearchSystem.search_people(search_term)
122
+
123
+ # Collect the ids of the results. This assumes that they will
124
+ # be the same ones as in the DB.
125
+ record_ids = results.entries.map(&:id)
126
+
127
+ # Use the ids to create an ActiveRecord relation and return it
128
+ People.where(id: record_ids)
129
+ end
130
+ ```
131
+
132
+ Note though: the records must still exist in the DB. Administrate does
133
+ require ActiveRecord in order to show tables, and to display, create and edit
134
+ records.
135
+
136
+ ## A working example
137
+
138
+ The [Administrate demo app](https://administrate-demo.herokuapp.com/admin)
139
+ includes an example of custom search in the "Log Entries" dashboard.
140
+ In this app, each `LogEntry` instance has a polymorphic `belongs_to`
141
+ association to a `:logeable`. Logeables are other models for which logs can be
142
+ created. At the moment these are `Order` and `Customer`.
143
+
144
+ Administrate's default search is not able to search across polymorphic
145
+ associations, and therefore it is not possible to search logs by the contents
146
+ of their logeables. Fortunately this can be fixed with a custom search. This is
147
+ done by implementing `Admin::LogEntriesController#filter_resources` to override
148
+ the default search. You can see the code at
149
+ https://github.com/thoughtbot/administrate/blob/main/spec/example_app/app/controllers/admin/log_entries_controller.rb
@@ -2,7 +2,8 @@
2
2
  title: Hiding Dashboards from the Sidebar
3
3
  ---
4
4
 
5
- Resources can be removed form the sidebar by removing their index action from the routes. For example:
5
+ Resources can be removed from the sidebar by removing their `index` action
6
+ from the routes. For example:
6
7
 
7
8
  ```ruby
8
9
  # config/routes.rb
@@ -16,4 +17,5 @@ Rails.application.routes.draw do
16
17
  end
17
18
  ```
18
19
 
19
- In this case, only Orders and Products will appear in the sidebar, while Line Items can still appear as an association.
20
+ In this case, only Orders and Products will appear in the sidebar, while
21
+ Line Items can still appear as an association.
@@ -0,0 +1,27 @@
1
+ ---
2
+ title: Scoping HasMany Relations
3
+ ---
4
+
5
+ To show a subset of a has_many relationship, create a new [has_many](https://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_many) relationship in your model (using the `scope` argument) and add it to the model's dashboard.
6
+
7
+ ## Creating a scoped has_many relationship
8
+
9
+ Models can define subsets of a `has_many` relationship by passing a callable (i.e. proc or lambda) as its second argument.
10
+
11
+ ```ruby
12
+ class Customer < ApplicationRecord
13
+ has_many :orders
14
+ has_many :processed_orders, ->{ where(processed: true) }, class_name: "Order"
15
+ ```
16
+
17
+ Since ActiveRecord infers the class name from the first argument, the new `has_many` relation needs to specify the model using the `class_name` option.
18
+
19
+ ## Add new relationship to dashboard
20
+
21
+ Your new scoped relation can be used in the dashboard just like the original `HasMany`. Notice the new field needs to specifiy the class name as an option like you did in the model.
22
+
23
+ ```ruby
24
+ ATTRIBUTE_TYPES = {
25
+ orders: Field::HasMany,
26
+ processed_orders: Field::HasMany.with_options(class_name: 'Order')
27
+ ```
data/docs/guides.md CHANGED
@@ -2,4 +2,6 @@
2
2
  title: Guides
3
3
  ---
4
4
 
5
- * [Hiding Dashboards from the Sidebar](./guides/hiding_dashboards_from_sidebar)
5
+ - [Hiding Dashboards from the Sidebar](./guides/hiding_dashboards_from_sidebar)
6
+ - [Customising the search](./guides/customising_search)
7
+ - [Scoping HasMany Relations](./guides/scoping_has_many_relations.md)