administrate 0.15.0 → 0.18.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.
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/stylesheets/administrate/application.scss +0 -1
  5. data/app/assets/stylesheets/administrate/base/_forms.scss +1 -1
  6. data/app/assets/stylesheets/administrate/components/_buttons.scss +12 -0
  7. data/app/assets/stylesheets/administrate/components/_flashes.scss +2 -2
  8. data/app/assets/stylesheets/administrate/library/_variables.scss +1 -1
  9. data/app/controllers/administrate/application_controller.rb +95 -17
  10. data/app/controllers/concerns/administrate/punditize.rb +4 -2
  11. data/app/helpers/administrate/application_helper.rb +24 -6
  12. data/app/views/administrate/application/_collection.html.erb +20 -24
  13. data/app/views/administrate/application/_collection_header_actions.html.erb +4 -0
  14. data/app/views/administrate/application/_collection_item_actions.html.erb +17 -0
  15. data/app/views/administrate/application/_flashes.html.erb +1 -0
  16. data/app/views/administrate/application/_form.html.erb +1 -1
  17. data/app/views/administrate/application/_icons.html.erb +1 -1
  18. data/app/views/administrate/application/_index_header.html.erb +28 -0
  19. data/app/views/administrate/application/_navigation.html.erb +2 -2
  20. data/app/views/administrate/application/_pagination.html.erb +1 -0
  21. data/app/views/administrate/application/edit.html.erb +1 -1
  22. data/app/views/administrate/application/index.html.erb +9 -29
  23. data/app/views/administrate/application/show.html.erb +9 -1
  24. data/app/views/fields/belongs_to/_index.html.erb +1 -1
  25. data/app/views/fields/belongs_to/_show.html.erb +1 -1
  26. data/app/views/fields/date/_form.html.erb +1 -3
  27. data/app/views/fields/date_time/_form.html.erb +1 -3
  28. data/app/views/fields/has_many/_index.html.erb +1 -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 +4 -2
  35. data/app/views/fields/time/_form.html.erb +2 -3
  36. data/app/views/fields/time/_index.html.erb +1 -1
  37. data/app/views/fields/time/_show.html.erb +1 -1
  38. data/app/views/fields/url/_index.html.erb +2 -2
  39. data/app/views/fields/url/_show.html.erb +2 -2
  40. data/config/locales/administrate.de.yml +2 -2
  41. data/config/locales/administrate.sl.yml +30 -0
  42. data/config/locales/administrate.zh-TW.yml +1 -1
  43. data/docs/adding_controllers_without_related_model.md +2 -4
  44. data/docs/authorization.md +25 -12
  45. data/docs/customizing_controller_actions.md +30 -7
  46. data/docs/customizing_dashboards.md +62 -6
  47. data/docs/extending_administrate.md +5 -5
  48. data/docs/getting_started.md +1 -1
  49. data/docs/guides/customising_search.md +149 -0
  50. data/docs/guides/hiding_dashboards_from_sidebar.md +4 -2
  51. data/docs/guides/scoping_has_many_relations.md +27 -0
  52. data/docs/guides.md +3 -1
  53. data/lib/administrate/base_dashboard.rb +30 -2
  54. data/lib/administrate/engine.rb +2 -2
  55. data/lib/administrate/field/associative.rb +7 -7
  56. data/lib/administrate/field/base.rb +4 -0
  57. data/lib/administrate/field/belongs_to.rb +4 -0
  58. data/lib/administrate/field/deferred.rb +4 -0
  59. data/lib/administrate/field/has_one.rb +4 -0
  60. data/lib/administrate/field/select.rb +4 -0
  61. data/lib/administrate/field/time.rb +11 -0
  62. data/lib/administrate/field/url.rb +4 -0
  63. data/lib/administrate/namespace.rb +1 -1
  64. data/lib/administrate/not_authorized_error.rb +18 -0
  65. data/lib/administrate/order.rb +35 -5
  66. data/lib/administrate/page/base.rb +4 -0
  67. data/lib/administrate/page/form.rb +9 -2
  68. data/lib/administrate/resource_resolver.rb +1 -1
  69. data/lib/administrate/search.rb +21 -17
  70. data/lib/administrate/version.rb +1 -1
  71. data/lib/administrate/view_generator.rb +1 -1
  72. data/lib/administrate.rb +18 -0
  73. data/lib/generators/administrate/dashboard/dashboard_generator.rb +15 -2
  74. data/lib/generators/administrate/dashboard/templates/controller.rb.erb +2 -2
  75. data/lib/generators/administrate/install/templates/application_controller.rb.erb +1 -1
  76. metadata +11 -49
  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,4 +16,4 @@ as a count of how many objects are associated through the relationship.
16
16
  [1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/HasMany
17
17
  %>
18
18
 
19
- <%= pluralize(field.data.size, field.attribute.to_s.humanize.downcase.singularize) %>
19
+ <%= pluralize(field.data.size, t("activerecord.models.#{field.attribute.to_s.singularize}", default: field.attribute.to_s.humanize.downcase.singularize, count: field.data.size)) %>
@@ -18,7 +18,7 @@ The form will be rendered as nested_from to parent relationship.
18
18
 
19
19
  <%= f.fields_for field.attribute, field.data || field.nested_form.resource.class.new do |has_one_f| %>
20
20
  <fieldset class="field-unit--nested">
21
- <legend><%= t "helpers.label.#{f.object_name}.#{field.nested_form.resource_name}", default: field.nested_form.resource_name.titleize %></legend>
21
+ <legend><%= t "helpers.label.#{f.object_name}.#{field.name}", default: field.name.titleize %></legend>
22
22
  <% field.nested_form.attributes.each do |attribute| -%>
23
23
  <div class="field-unit field-unit--<%= attribute.html_class %>">
24
24
  <%= render_field attribute, f: has_one_f %>
@@ -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],
@@ -27,7 +27,8 @@ to be displayed on a resource's edit form page.
27
27
  :last,
28
28
  :first,
29
29
  field.data,
30
- )
30
+ ),
31
+ include_blank: field.include_blank_option
31
32
  ) %>
32
33
  <% else %>
33
34
  <%= f.select(
@@ -37,7 +38,8 @@ to be displayed on a resource's edit form page.
37
38
  :to_s,
38
39
  :to_s,
39
40
  field.data,
40
- )
41
+ ),
42
+ include_blank: field.include_blank_option
41
43
  ) %>
42
44
  <% end %>
43
45
  </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,5 +15,5 @@ By default, the attribute is rendered as a text tag.
15
15
  %>
16
16
 
17
17
  <% if field.data %>
18
- <%= field.data.strftime("%I:%M%p").to_s %>
18
+ <%= field.time %>
19
19
  <% end %>
@@ -15,5 +15,5 @@ By default, the attribute is rendered as a text tag.
15
15
  %>
16
16
 
17
17
  <% if field.data %>
18
- <%= field.data.strftime("%I:%M%p").to_s %>
18
+ <%= field.time %>
19
19
  <% end %>
@@ -1,7 +1,7 @@
1
1
  <%#
2
2
  # Url Index Partial
3
3
 
4
- This partial renders an email address,
4
+ This partial renders a URL address,
5
5
  to be displayed on a resource's index page.
6
6
 
7
7
  By default, the value is rendered as an `a` element.
@@ -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 %>
@@ -1,7 +1,7 @@
1
1
  <%#
2
2
  # Url Show Partial
3
3
 
4
- This partial renders an email address,
4
+ This partial renders a URL address,
5
5
  to be displayed on a resource's show page.
6
6
 
7
7
  By default, the value is rendered as an `a` element.
@@ -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 %>
@@ -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:
@@ -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}
@@ -24,7 +24,7 @@ zh-TW:
24
24
  error: 錯誤
25
25
  errors: "%{pluralized_errors} 導致此 %{resource_name} 不能被儲存:"
26
26
  navigation:
27
- back_to_app: 返回应用
27
+ back_to_app: 返回首頁
28
28
  search:
29
29
  clear: 清除搜尋
30
30
  label: 搜尋 %{resource}
@@ -2,8 +2,6 @@
2
2
  title: Adding Controllers without a related Model
3
3
  ---
4
4
 
5
- # Adding Controllers without a related Model
6
-
7
5
  Sometimes you may want to add a custom controller that has no resource
8
6
  related to it (for example for a statistics page).
9
7
 
@@ -16,9 +14,9 @@ routes are displayed in the sidebar and then add a custom dashboard:
16
14
  <div style="padding: 20px">
17
15
  <h1>Stats</h1>
18
16
  <br>
19
- <p><b>Total Customers:</b> <%= @stats[:customer_count] %></h1>
17
+ <p><b>Total Customers:</b> <%= @stats[:customer_count] %></p>
20
18
  <br>
21
- <p><b>Total Orders:</b> <%= @stats[:order_count] %></h1>
19
+ <p><b>Total Orders:</b> <%= @stats[:order_count] %></p>
22
20
  </div>
23
21
  ```
24
22
 
@@ -49,23 +49,36 @@ end
49
49
 
50
50
  ## Authorization without Pundit
51
51
 
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:
52
+ Pundit is not necessary to implement authorization within Administrate. It is
53
+ simply a common solution that many in the community use, and for this reason
54
+ Administrate provides a plugin to work with it. However you can use a different
55
+ solution or roll out your own.
56
+
57
+ To integrate a different authorization solution, you will need to
58
+ implement some methods in `Admin::ApplicationController`
59
+ or its subclasses.
60
+
61
+ These are the methods to override, with examples:
55
62
 
56
63
  ```ruby
57
- # Limit the scope of the given resource
64
+ # Used in listings, such as the `index` actions. It
65
+ # restricts the scope of records that a user can access.
66
+ # Returns an ActiveRecord scope.
58
67
  def scoped_resource
59
68
  super.where(user: current_user)
60
69
  end
61
70
 
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
71
+ # Return true if the current user can access the given
72
+ # resource, false otherwise.
73
+ def authorized_action?(resource, action)
74
+ current_user.can?(resource, action)
70
75
  end
71
76
  ```
77
+
78
+ Additionally, the method `authorize_resource(resource)`
79
+ should throw an exception if the current user is not
80
+ allowed to access the given resource. Normally
81
+ you wouldn't need to override it, as the default
82
+ implementation uses `authorized_action?` to produce the
83
+ correct behaviour. However you may still want to override it
84
+ 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:
@@ -69,4 +74,22 @@ end
69
74
  def default_sorting_direction
70
75
  :desc
71
76
  end
72
- ```
77
+ ```
78
+
79
+ ## Customizing Redirects after actions
80
+
81
+ To set custom redirects after the actions `create`, `update` and `destroy` you can override `after_resource_created_path`, `after_resource_updated_path` or `after_resource_destroyed_path` like this:
82
+
83
+ ```ruby
84
+ def after_resource_destroyed_path(_requested_resource)
85
+ { action: :index, controller: :some_other_resource }
86
+ end
87
+
88
+ def after_resource_created_path(requested_resource)
89
+ [namespace, requested_resource.some_other_resource]
90
+ end
91
+
92
+ def after_resource_updated_path(requested_resource)
93
+ [namespace, requested_resource.some_other_resource]
94
+ end
95
+ ```
@@ -8,9 +8,9 @@ edit the dashboard file generated by the installation generator.
8
8
  By default, the file will look something like this:
9
9
 
10
10
  ```ruby
11
- require "administrate/dashboard/base"
11
+ require "administrate/base_dashboard"
12
12
 
13
- class CustomerDashboard < Administrate::Dashboard::Base
13
+ class CustomerDashboard < Administrate::BaseDashboard
14
14
  ATTRIBUTE_TYPES = {
15
15
  id: Field::Number,
16
16
  name: Field::String,
@@ -65,6 +65,7 @@ specify, including:
65
65
  - `Field::Select`
66
66
  - `Field::String`
67
67
  - `Field::Text`
68
+ - `Field::Url`
68
69
  - `Field::Password`
69
70
 
70
71
  ## Customizing Fields
@@ -76,8 +77,9 @@ which are specified through the `.with_options` class method:
76
77
 
77
78
  **Field::BelongsTo**
78
79
 
79
- `:order` - Specifies the order of the dropdown menu, can be ordered by more
80
- 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"`.
81
83
 
82
84
  `:scope` - Specifies a custom scope inside a callable. Useful for preloading.
83
85
  Example: `.with_options(scope: -> { MyModel.includes(:rel).limit(5) })`
@@ -182,9 +184,9 @@ Or, to display a distance in kilometers, using a space as the delimiter:
182
184
  distance: Field::Number.with_options(
183
185
  suffix: " km",
184
186
  decimals: 2,
185
- format: {
187
+ format: {
186
188
  formatter: :number_to_delimited,
187
- formatter_options: {
189
+ formatter_options: {
188
190
  delimiter: ' ',
189
191
  },
190
192
  },
@@ -219,9 +221,23 @@ objects to display as.
219
221
  `:collection` - Specify the options shown on the select field. It accept either
220
222
  an array or an object responding to `:call`. Defaults to `[]`.
221
223
 
224
+ To customize option labels, pass an array of pairs where the first element is the value submitted with the form and the second element is the label shown to the user.
225
+
226
+ For example:
227
+
228
+ ```ruby
229
+ currency = Field::Select.with_options(
230
+ collection: [ ['usd', 'Dollar'], ['eur', 'Euro'], ['yen', 'Yen'] ]
231
+ )
232
+
233
+ ```
234
+
222
235
  `:searchable` - Specify if the attribute should be considered when searching.
223
236
  Default is `true`.
224
237
 
238
+ `:include_blank` - Specifies if the select element to be rendered should include
239
+ blank option. Default is `false`.
240
+
225
241
  **Field::String**
226
242
 
227
243
  `:searchable` - Specify if the attribute should be considered when searching.
@@ -238,6 +254,17 @@ Default is `false`.
238
254
  `:truncate` - Set the number of characters to display in the index view.
239
255
  Defaults to `50`.
240
256
 
257
+ **Field::Url**
258
+
259
+ `:searchable` - Specify if the attribute should be considered when searching.
260
+ Default is `true`.
261
+
262
+ `:truncate` - Set the number of characters to display in the index view.
263
+ Defaults to `50`.
264
+
265
+ `:html_options` - Specify anchor tag attributes (e.g., `target="_blank"`).
266
+ Defaults is `{}`.
267
+
241
268
  **Field::Password**
242
269
 
243
270
  `:searchable` - Specify if the attribute should be considered when searching.
@@ -317,3 +344,32 @@ COLLECTION_FILTERS = {
317
344
  inactive: ->(resources) { resources.inactive }
318
345
  }
319
346
  ```
347
+
348
+ You can also define a filter with parameters:
349
+
350
+ ```ruby
351
+ COLLECTION_FILTERS = {
352
+ state: ->(resources, attr) { resources.where(state: attr) }
353
+ }
354
+ ```
355
+
356
+ You can now search your resource with 'state:open' and your
357
+ collection filter Proc will be called with with attr = open.
358
+
359
+ ## Form Attributes
360
+
361
+ You can define different attributes for new/create or edit/update actions:
362
+
363
+ ```ruby
364
+ FORM_ATTRIBUTES_NEW = [
365
+ :name,
366
+ :email
367
+ ]
368
+ FORM_ATTRIBUTES_EDIT = [
369
+ :name,
370
+ :email,
371
+ :orders
372
+ ]
373
+ ```
374
+
375
+ Or for custom action with constant name `"FORM_ATTRIBUTES_#{action.upcase}"`
@@ -5,13 +5,13 @@ title: Extending Administrate
5
5
  Apart from the configuration described in these pages, it is possible to
6
6
  extend Administrate's capabilities with the use of plugins. There are a
7
7
  number of plugins available, many of which can be found at [RubyGems.org].
8
- At the time of writing, these appear to be the most popular ones:
8
+ These are some popular examples:
9
9
 
10
10
  1. [ActiveStorage support](https://github.com/Dreamersoul/administrate-field-active_storage)
11
- 2. [Password field](https://github.com/valiot/administrate-field-password)
12
- 3. [Enum field](https://github.com/Valiot/administrate-field-enum)
13
- 4. [Nested has-many forms](https://github.com/nickcharlton/administrate-field-nested_has_many)
14
- 5. [Belongs-to with Ajax search](https://github.com/fishbrain/administrate-field-belongs_to_search)
11
+ 2. [Enum field](https://github.com/Valiot/administrate-field-enum)
12
+ 3. [Nested has-many forms](https://github.com/nickcharlton/administrate-field-nested_has_many)
13
+ 4. [Belongs-to with Ajax search](https://github.com/fishbrain/administrate-field-belongs_to_search)
14
+ 5. [JSONb field plugin for Administrate](https://github.com/codica2/administrate-field-jsonb/)
15
15
 
16
16
  See many more at https://rubygems.org/gems/administrate/reverse_dependencies.
17
17
 
@@ -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.4 and up.
6
+ applications version 5.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](/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.