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
@@ -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)
@@ -50,8 +50,16 @@ module Administrate
50
50
  attribute_types.keys
51
51
  end
52
52
 
53
- def form_attributes
54
- self.class::FORM_ATTRIBUTES
53
+ def form_attributes(action = nil)
54
+ specific_form_attributes_for(action) || self.class::FORM_ATTRIBUTES
55
+ end
56
+
57
+ def specific_form_attributes_for(action)
58
+ return unless action
59
+
60
+ cname = "FORM_ATTRIBUTES_#{action.upcase}"
61
+
62
+ self.class.const_get(cname) if self.class.const_defined?(cname)
55
63
  end
56
64
 
57
65
  def permitted_attributes
@@ -71,6 +79,12 @@ module Administrate
71
79
  self.class::COLLECTION_ATTRIBUTES
72
80
  end
73
81
 
82
+ def search_attributes
83
+ attribute_types.keys.select do |attribute|
84
+ attribute_types[attribute].searchable?
85
+ end
86
+ end
87
+
74
88
  def display_resource(resource)
75
89
  "#{resource.class} ##{resource.id}"
76
90
  end
@@ -80,9 +94,15 @@ module Administrate
80
94
  end
81
95
 
82
96
  def item_includes
97
+ # Deprecated, internal usage has moved to #item_associations
98
+ Administrate.warn_of_deprecated_method(self.class, :item_includes)
83
99
  attribute_includes(show_page_attributes)
84
100
  end
85
101
 
102
+ def item_associations
103
+ attribute_associated(show_page_attributes)
104
+ end
105
+
86
106
  private
87
107
 
88
108
  def attribute_not_found_message(attr)
@@ -90,6 +110,14 @@ module Administrate
90
110
  end
91
111
 
92
112
  def attribute_includes(attributes)
113
+ attributes.map do |key|
114
+ field = attribute_type_for(key)
115
+
116
+ key if field.eager_load?
117
+ end.compact
118
+ end
119
+
120
+ def attribute_associated(attributes)
93
121
  attributes.map do |key|
94
122
  field = attribute_type_for(key)
95
123
 
@@ -1,11 +1,11 @@
1
- require "datetime_picker_rails"
2
1
  require "jquery-rails"
3
2
  require "kaminari"
4
- require "momentjs-rails"
5
3
  require "sassc-rails"
6
4
  require "selectize-rails"
7
5
  require "sprockets/railtie"
8
6
 
7
+ require "administrate/namespace/resource"
8
+ require "administrate/not_authorized_error"
9
9
  require "administrate/page/form"
10
10
  require "administrate/page/show"
11
11
  require "administrate/page/collection"
@@ -12,7 +12,7 @@ module Administrate
12
12
  end
13
13
 
14
14
  def self.associated_class_name(resource_class, attr)
15
- reflection(resource_class, attr).class_name
15
+ associated_class(resource_class, attr).name
16
16
  end
17
17
 
18
18
  def self.reflection(resource_class, attr)
@@ -31,12 +31,6 @@ module Administrate
31
31
  end
32
32
  end
33
33
 
34
- private
35
-
36
- def associated_dashboard
37
- "#{associated_class_name}Dashboard".constantize.new
38
- end
39
-
40
34
  def associated_class_name
41
35
  if option_given?(:class_name)
42
36
  deprecated_option(:class_name)
@@ -48,6 +42,12 @@ module Administrate
48
42
  end
49
43
  end
50
44
 
45
+ private
46
+
47
+ def associated_dashboard
48
+ "#{associated_class_name}Dashboard".constantize.new
49
+ end
50
+
51
51
  def primary_key
52
52
  if option_given?(:primary_key)
53
53
  deprecated_option(:primary_key)
@@ -16,6 +16,10 @@ module Administrate
16
16
  self < Associative
17
17
  end
18
18
 
19
+ def self.eager_load?
20
+ false
21
+ end
22
+
19
23
  def self.searchable?
20
24
  false
21
25
  end
@@ -13,6 +13,10 @@ module Administrate
13
13
  end
14
14
  end
15
15
 
16
+ def self.eager_load?
17
+ true
18
+ end
19
+
16
20
  def permitted_attribute
17
21
  foreign_key
18
22
  end
@@ -25,6 +25,10 @@ module Administrate
25
25
  deferred_class.associative?
26
26
  end
27
27
 
28
+ def eager_load?
29
+ deferred_class.eager_load?
30
+ end
31
+
28
32
  def searchable?
29
33
  options.fetch(:searchable, deferred_class.searchable?)
30
34
  end
@@ -26,6 +26,10 @@ module Administrate
26
26
  { "#{attr}_attributes": related_dashboard_attributes }
27
27
  end
28
28
 
29
+ def self.eager_load?
30
+ true
31
+ end
32
+
29
33
  def nested_form
30
34
  @nested_form ||= Administrate::Page::Form.new(
31
35
  resolver.dashboard_class.new,
@@ -11,6 +11,10 @@ module Administrate
11
11
  collection
12
12
  end
13
13
 
14
+ def include_blank_option
15
+ options.fetch(:include_blank, false)
16
+ end
17
+
14
18
  private
15
19
 
16
20
  def collection
@@ -3,6 +3,17 @@ require_relative "base"
3
3
  module Administrate
4
4
  module Field
5
5
  class Time < Base
6
+ def time
7
+ return I18n.localize(data, format: format) if options[:format]
8
+
9
+ data.strftime("%I:%M%p")
10
+ end
11
+
12
+ private
13
+
14
+ def format
15
+ options[:format]
16
+ end
6
17
  end
7
18
  end
8
19
  end
@@ -11,6 +11,10 @@ module Administrate
11
11
  data.to_s[0...truncation_length]
12
12
  end
13
13
 
14
+ def html_options
15
+ @options[:html_options] || {}
16
+ end
17
+
14
18
  private
15
19
 
16
20
  def truncation_length
@@ -1,7 +1,7 @@
1
1
  module Administrate
2
2
  class Namespace
3
3
  def initialize(namespace)
4
- @namespace = namespace
4
+ @namespace = namespace.to_sym
5
5
  end
6
6
 
7
7
  def resources
@@ -0,0 +1,18 @@
1
+ module Administrate
2
+ class NotAuthorizedError < StandardError
3
+ def initialize(action:, resource:)
4
+ @action = action
5
+ @resource = resource
6
+
7
+ case resource
8
+ when Module, String, Symbol
9
+ super("Not allowed to perform #{action.inspect} on #{resource.inspect}")
10
+ else
11
+ super(
12
+ "Not allowed to perform #{action.inspect} on the given " +
13
+ resource.class.name
14
+ )
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,8 +1,9 @@
1
1
  module Administrate
2
2
  class Order
3
- def initialize(attribute = nil, direction = nil)
3
+ def initialize(attribute = nil, direction = nil, association_attribute: nil)
4
4
  @attribute = attribute
5
5
  @direction = sanitize_direction(direction)
6
+ @association_attribute = association_attribute
6
7
  end
7
8
 
8
9
  def apply(relation)
@@ -12,7 +13,7 @@ module Administrate
12
13
  order = "#{relation.table_name}.#{attribute} #{direction}"
13
14
 
14
15
  return relation.reorder(Arel.sql(order)) if
15
- relation.columns_hash.keys.include?(attribute.to_s)
16
+ column_exist?(relation, attribute)
16
17
 
17
18
  relation
18
19
  end
@@ -32,7 +33,7 @@ module Administrate
32
33
 
33
34
  private
34
35
 
35
- attr_reader :attribute
36
+ attr_reader :attribute, :association_attribute
36
37
 
37
38
  def sanitize_direction(direction)
38
39
  %w[asc desc].include?(direction.to_s) ? direction.to_sym : :asc
@@ -53,7 +54,7 @@ module Administrate
53
54
  def order_by_association(relation)
54
55
  return order_by_count(relation) if has_many_attribute?(relation)
55
56
 
56
- return order_by_id(relation) if belongs_to_attribute?(relation)
57
+ return order_by_attribute(relation) if belongs_to_attribute?(relation)
57
58
 
58
59
  relation
59
60
  end
@@ -68,7 +69,36 @@ module Administrate
68
69
  end
69
70
 
70
71
  def order_by_id(relation)
71
- relation.reorder("#{foreign_key(relation)} #{direction}")
72
+ relation.reorder(Arel.sql(order_by_id_query(relation)))
73
+ end
74
+
75
+ def order_by_attribute(relation)
76
+ if ordering_by_association_column?(relation)
77
+ relation.joins(
78
+ attribute.to_sym,
79
+ ).reorder(Arel.sql(order_by_attribute_query))
80
+ else
81
+ order_by_id(relation)
82
+ end
83
+ end
84
+
85
+ def ordering_by_association_column?(relation)
86
+ association_attribute &&
87
+ column_exist?(
88
+ reflect_association(relation).klass, association_attribute.to_sym
89
+ )
90
+ end
91
+
92
+ def column_exist?(table, column_name)
93
+ table.columns_hash.key?(column_name.to_s)
94
+ end
95
+
96
+ def order_by_id_query(relation)
97
+ "#{relation.table_name}.#{foreign_key(relation)} #{direction}"
98
+ end
99
+
100
+ def order_by_attribute_query
101
+ "#{attribute.tableize}.#{association_attribute} #{direction}"
72
102
  end
73
103
 
74
104
  def has_many_attribute?(relation)
@@ -23,6 +23,10 @@ module Administrate
23
23
  dashboard.try(:item_includes) || []
24
24
  end
25
25
 
26
+ def item_associations
27
+ dashboard.try(:item_associations) || []
28
+ end
29
+
26
30
  private
27
31
 
28
32
  def attribute_field(dashboard, resource, attribute_name, page)
@@ -10,8 +10,15 @@ module Administrate
10
10
 
11
11
  attr_reader :resource
12
12
 
13
- def attributes
14
- dashboard.form_attributes.map do |attribute|
13
+ def attributes(action = nil)
14
+ action =
15
+ case action
16
+ when "update" then "edit"
17
+ when "create" then "new"
18
+ else action
19
+ end
20
+
21
+ dashboard.form_attributes(action).map do |attribute|
15
22
  attribute_field(dashboard, resource, attribute, :form)
16
23
  end
17
24
  end
@@ -9,7 +9,7 @@ module Administrate
9
9
  end
10
10
 
11
11
  def namespace
12
- controller_path.split("/").first
12
+ controller_path.split("/").first.to_sym
13
13
  end
14
14
 
15
15
  def resource_class
@@ -4,14 +4,15 @@ require "active_support/core_ext/object/blank"
4
4
  module Administrate
5
5
  class Search
6
6
  class Query
7
- attr_reader :filters
7
+ attr_reader :filters, :valid_filters
8
8
 
9
9
  def blank?
10
10
  terms.blank? && filters.empty?
11
11
  end
12
12
 
13
- def initialize(original_query)
13
+ def initialize(original_query, valid_filters = nil)
14
14
  @original_query = original_query
15
+ @valid_filters = valid_filters
15
16
  @filters, @terms = parse_query(original_query)
16
17
  end
17
18
 
@@ -30,7 +31,7 @@ module Administrate
30
31
  private
31
32
 
32
33
  def filter?(word)
33
- word.match?(/^\w+:$/)
34
+ valid_filters&.any? { |filter| word.match?(/^#{filter}:\w*$/) }
34
35
  end
35
36
 
36
37
  def parse_query(query)
@@ -38,7 +39,7 @@ module Administrate
38
39
  terms = []
39
40
  query.to_s.split.each do |word|
40
41
  if filter?(word)
41
- filters << word.split(":").first
42
+ filters << word
42
43
  else
43
44
  terms << word
44
45
  end
@@ -47,10 +48,10 @@ module Administrate
47
48
  end
48
49
  end
49
50
 
50
- def initialize(scoped_resource, dashboard_class, term)
51
- @dashboard_class = dashboard_class
51
+ def initialize(scoped_resource, dashboard, term)
52
+ @dashboard = dashboard
52
53
  @scoped_resource = scoped_resource
53
- @query = Query.new(term)
54
+ @query = Query.new(term, valid_filters.keys)
54
55
  end
55
56
 
56
57
  def run
@@ -65,15 +66,20 @@ module Administrate
65
66
 
66
67
  private
67
68
 
68
- def apply_filter(filter, resources)
69
+ def apply_filter(filter, filter_param, resources)
69
70
  return resources unless filter
70
- filter.call(resources)
71
+ if filter.parameters.size == 1
72
+ filter.call(resources)
73
+ else
74
+ filter.call(resources, filter_param)
75
+ end
71
76
  end
72
77
 
73
78
  def filter_results(resources)
74
- query.filters.each do |filter_name|
79
+ query.filters.each do |filter_query|
80
+ filter_name, filter_param = filter_query.split(":")
75
81
  filter = valid_filters[filter_name]
76
- resources = apply_filter(filter, resources)
82
+ resources = apply_filter(filter, filter_param, resources)
77
83
  end
78
84
  resources
79
85
  end
@@ -102,9 +108,7 @@ module Administrate
102
108
  end
103
109
 
104
110
  def search_attributes
105
- attribute_types.keys.select do |attribute|
106
- attribute_types[attribute].searchable?
107
- end
111
+ @dashboard.search_attributes
108
112
  end
109
113
 
110
114
  def search_results(resources)
@@ -114,15 +118,15 @@ module Administrate
114
118
  end
115
119
 
116
120
  def valid_filters
117
- if @dashboard_class.const_defined?(:COLLECTION_FILTERS)
118
- @dashboard_class.const_get(:COLLECTION_FILTERS).stringify_keys
121
+ if @dashboard.class.const_defined?(:COLLECTION_FILTERS)
122
+ @dashboard.class.const_get(:COLLECTION_FILTERS).stringify_keys
119
123
  else
120
124
  {}
121
125
  end
122
126
  end
123
127
 
124
128
  def attribute_types
125
- @dashboard_class::ATTRIBUTE_TYPES
129
+ @dashboard.class.const_get(:ATTRIBUTE_TYPES)
126
130
  end
127
131
 
128
132
  def query_table_name(attr)
@@ -1,3 +1,3 @@
1
1
  module Administrate
2
- VERSION = "0.15.0".freeze
2
+ VERSION = "0.18.0".freeze
3
3
  end
@@ -5,7 +5,7 @@ require "administrate/namespace"
5
5
  module Administrate
6
6
  class ViewGenerator < Rails::Generators::Base
7
7
  include Administrate::GeneratorHelpers
8
- class_option :namespace, type: :string, default: "admin"
8
+ class_option :namespace, type: :string, default: :admin
9
9
 
10
10
  def self.template_source_path
11
11
  File.expand_path(
data/lib/administrate.rb CHANGED
@@ -20,4 +20,22 @@ module Administrate
20
20
  "if you think otherwise.",
21
21
  )
22
22
  end
23
+
24
+ def self.warn_of_deprecated_method(klass, method)
25
+ ActiveSupport::Deprecation.warn(
26
+ "The method #{klass}##{method} is deprecated. " +
27
+ "If you are seeing this message you are probably " +
28
+ "using a dashboard that depends explicitly on it. " +
29
+ "Please make sure you update it to a version that " +
30
+ "does not use a deprecated API",
31
+ )
32
+ end
33
+
34
+ def self.warn_of_deprecated_authorization_method(method)
35
+ ActiveSupport::Deprecation.warn(
36
+ "The method `#{method}` is deprecated. " +
37
+ "Please use `accessible_action?` instead, " +
38
+ "or see the documentation for other options.",
39
+ )
40
+ end
23
41
  end
@@ -27,7 +27,7 @@ module Administrate
27
27
  COLLECTION_ATTRIBUTE_LIMIT = 4
28
28
  READ_ONLY_ATTRIBUTES = %w[id created_at updated_at]
29
29
 
30
- class_option :namespace, type: :string, default: "admin"
30
+ class_option :namespace, type: :string, default: :admin
31
31
 
32
32
  source_root File.expand_path("../templates", __FILE__)
33
33
 
@@ -53,9 +53,22 @@ module Administrate
53
53
  end
54
54
 
55
55
  def attributes
56
- klass.reflections.keys +
56
+ attrs = (
57
+ klass.reflections.keys +
57
58
  klass.columns.map(&:name) -
58
59
  redundant_attributes
60
+ )
61
+
62
+ primary_key = attrs.delete(klass.primary_key)
63
+ created_at = attrs.delete("created_at")
64
+ updated_at = attrs.delete("updated_at")
65
+
66
+ [
67
+ primary_key,
68
+ *attrs.sort,
69
+ created_at,
70
+ updated_at,
71
+ ].compact
59
72
  end
60
73
 
61
74
  def form_attributes
@@ -1,5 +1,5 @@
1
- module <%= namespace.classify %>
2
- class <%= class_name.pluralize %>Controller < <%= namespace.classify %>::ApplicationController
1
+ module <%= namespace.to_s.camelize %>
2
+ class <%= class_name.pluralize %>Controller < <%= namespace.to_s.camelize %>::ApplicationController
3
3
  # Overwrite any of the RESTful controller actions to implement custom behavior
4
4
  # For example, you may want to send an email after a foo is updated.
5
5
  #
@@ -4,7 +4,7 @@
4
4
  #
5
5
  # If you want to add pagination or other controller-level concerns,
6
6
  # you're free to overwrite the RESTful controller actions.
7
- module <%= namespace.classify %>
7
+ module <%= namespace.camelize %>
8
8
  class ApplicationController < Administrate::ApplicationController
9
9
  before_action :authenticate_admin
10
10