godmin 0.10.3 → 0.11.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +8 -0
  4. data/README.md +146 -81
  5. data/app/assets/javascripts/godmin/batch-actions.js +18 -13
  6. data/app/assets/stylesheets/godmin/index.css.scss +15 -8
  7. data/app/views/godmin/resource/_batch_actions.html.erb +2 -4
  8. data/app/views/godmin/resource/_breadcrumb.html.erb +0 -3
  9. data/app/views/godmin/resource/_button_actions.html.erb +2 -2
  10. data/app/views/godmin/resource/_filters.html.erb +17 -18
  11. data/app/views/godmin/resource/_form.html.erb +1 -1
  12. data/app/views/godmin/resource/_pagination.html.erb +11 -11
  13. data/app/views/godmin/resource/_scopes.html.erb +4 -4
  14. data/app/views/godmin/resource/_table.html.erb +30 -30
  15. data/app/views/godmin/resource/index.html.erb +3 -10
  16. data/godmin.gemspec +4 -1
  17. data/lib/generators/godmin/authentication/templates/sessions_controller.rb +1 -1
  18. data/lib/generators/godmin/install/install_generator.rb +1 -1
  19. data/lib/generators/godmin/resource/resource_generator.rb +4 -0
  20. data/lib/generators/godmin/resource/templates/resource_controller.rb +1 -9
  21. data/lib/generators/godmin/resource/templates/resource_service.rb +8 -0
  22. data/lib/godmin.rb +4 -2
  23. data/lib/godmin/{application.rb → application_controller.rb} +1 -1
  24. data/lib/godmin/authentication.rb +1 -1
  25. data/lib/godmin/authentication/{sessions.rb → sessions_controller.rb} +1 -1
  26. data/lib/godmin/authorization.rb +1 -1
  27. data/lib/godmin/authorization/policy_finder.rb +3 -1
  28. data/lib/godmin/helpers/application.rb +10 -0
  29. data/lib/godmin/helpers/batch_actions.rb +7 -4
  30. data/lib/godmin/helpers/filters.rb +72 -73
  31. data/lib/godmin/helpers/tables.rb +1 -3
  32. data/lib/godmin/paginator.rb +47 -0
  33. data/lib/godmin/rails.rb +1 -6
  34. data/lib/godmin/resolver.rb +7 -2
  35. data/lib/godmin/resources/resource_controller.rb +170 -0
  36. data/lib/godmin/resources/resource_service.rb +82 -0
  37. data/lib/godmin/resources/resource_service/batch_actions.rb +38 -0
  38. data/lib/godmin/resources/resource_service/filters.rb +37 -0
  39. data/lib/godmin/resources/resource_service/ordering.rb +27 -0
  40. data/lib/godmin/resources/resource_service/pagination.rb +22 -0
  41. data/lib/godmin/resources/resource_service/scopes.rb +61 -0
  42. data/lib/godmin/version.rb +1 -1
  43. data/test/dummy/config/environments/production.rb +1 -1
  44. data/test/dummy/config/environments/test.rb +1 -1
  45. data/test/dummy/db/schema.rb +16 -0
  46. data/test/lib/godmin/helpers/filters_test.rb +26 -0
  47. data/test/lib/godmin/paginator_test.rb +84 -0
  48. data/test/lib/godmin/policy_finder_test.rb +35 -6
  49. data/test/lib/godmin/resolver_test.rb +6 -0
  50. data/test/lib/godmin/resources/resource_service/batch_actions_test.rb +45 -0
  51. data/test/lib/godmin/resources/resource_service/filters_test.rb +32 -0
  52. data/test/lib/godmin/resources/resource_service/ordering_test.rb +37 -0
  53. data/test/lib/godmin/resources/resource_service/pagination_test.rb +31 -0
  54. data/test/lib/godmin/resources/resource_service/scopes_test.rb +57 -0
  55. data/test/lib/godmin/resources/resource_service_test.rb +21 -0
  56. data/test/test_helper.rb +62 -0
  57. metadata +75 -17
  58. data/.hound.yml +0 -3
  59. data/lib/godmin/resource.rb +0 -177
  60. data/lib/godmin/resource/batch_actions.rb +0 -45
  61. data/lib/godmin/resource/filters.rb +0 -41
  62. data/lib/godmin/resource/ordering.rb +0 -25
  63. data/lib/godmin/resource/pagination.rb +0 -64
  64. data/lib/godmin/resource/scopes.rb +0 -54
  65. data/test/dummy/db/test.sqlite3 +0 -0
@@ -1,4 +1,4 @@
1
- require "godmin/authentication/sessions"
1
+ require "godmin/authentication/sessions_controller"
2
2
  require "godmin/authentication/user"
3
3
 
4
4
  module Godmin
@@ -1,6 +1,6 @@
1
1
  module Godmin
2
2
  module Authentication
3
- module Sessions
3
+ module SessionsController
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
@@ -22,7 +22,7 @@ module Godmin
22
22
  end
23
23
 
24
24
  def policy(record)
25
- policies[record] ||= PolicyFinder.find(record).constantize.new(admin_user, record)
25
+ policies[record] ||= PolicyFinder.find(record).new(admin_user, record)
26
26
  end
27
27
 
28
28
  def policies
@@ -3,6 +3,8 @@ module Godmin
3
3
  class PolicyFinder
4
4
  class << self
5
5
  def find(object)
6
+ return object.policy_class if object.respond_to?(:policy_class)
7
+ return object.class.policy_class if object.class.respond_to?(:policy_class)
6
8
  klass =
7
9
  if object.respond_to?(:model_name)
8
10
  object.model_name
@@ -20,7 +22,7 @@ module Godmin
20
22
  "#{Godmin.namespace.classify}::#{klass}Policy"
21
23
  else
22
24
  "#{klass}Policy"
23
- end
25
+ end.constantize
24
26
  end
25
27
  end
26
28
  end
@@ -1,6 +1,16 @@
1
1
  module Godmin
2
2
  module Helpers
3
3
  module Application
4
+ # Renders the provided partial with locals if it exists, otherwise
5
+ # yields the given block.
6
+ def partial_override(partial, locals = {})
7
+ if lookup_context.exists?(partial, nil, true)
8
+ render partial: partial, locals: locals
9
+ else
10
+ yield
11
+ end
12
+ end
13
+
4
14
  # Wraps the policy helper so that it is always accessible, even when
5
15
  # authorization is not enabled. When that is the case, it returns a
6
16
  # policy that always returns true.
@@ -5,8 +5,11 @@ module Godmin
5
5
  return unless include_batch_action_link?(options)
6
6
 
7
7
  link_to(
8
- translate_scoped("batch_actions.labels.#{name}", default: name.to_s.titleize), "#",
9
- class: "btn btn-default hidden", data: {
8
+ translate_scoped("batch_actions.labels.#{name}", default: name.to_s.titleize),
9
+ @resource_class,
10
+ method: :patch,
11
+ class: "btn btn-default hidden",
12
+ data: {
10
13
  behavior: "batch-actions-action-link",
11
14
  confirm: options[:confirm] ? translate_scoped("batch_actions.confirm_message") : false,
12
15
  value: name
@@ -18,8 +21,8 @@ module Godmin
18
21
 
19
22
  def include_batch_action_link?(options)
20
23
  (options[:only].nil? && options[:except].nil?) ||
21
- (options[:only] && options[:only].include?(params[:scope].to_sym)) ||
22
- (options[:except] && !options[:except].include?(params[:scope].to_sym))
24
+ (options[:only] && options[:only].include?(@resource_service.scope.to_sym)) ||
25
+ (options[:except] && !options[:except].include?(@resource_service.scope.to_sym))
23
26
  end
24
27
  end
25
28
  end
@@ -1,109 +1,108 @@
1
1
  module Godmin
2
2
  module Helpers
3
3
  module Filters
4
- def filter_input_tag(name, options)
4
+ def filter_form(url: params)
5
+ bootstrap_form_tag url: url, method: :get, layout: :inline, builder: FormBuilders::FilterFormBuilder do |f|
6
+ yield(f)
7
+ end
8
+ end
9
+ end
10
+ end
11
+
12
+ module FormBuilders
13
+ class FilterFormBuilder < BootstrapForm::FormBuilder
14
+ def filter_field(name, options, html_options = {})
5
15
  case options[:as]
6
16
  when :string
7
- filter_string_tag(name, options)
17
+ string_filter_field(name, options, html_options)
8
18
  when :select
9
- filter_select_tag(name, options)
19
+ select_filter_field(name, options, html_options)
10
20
  when :multiselect
11
- filter_multiselect_tag(name, options)
12
- when :checkboxes
13
- filter_checkbox_tags(name, options)
21
+ multiselect_filter_field(name, options, html_options)
14
22
  end
15
23
  end
16
24
 
17
- private
18
-
19
- def filter_string_tag(name, _options)
20
- text_field_tag(
21
- name,
22
- default_filter_value(name),
23
- name: "filter[#{name}]",
24
- class: "form-control",
25
- placeholder: translate_scoped("filters.labels.#{name}", default: name.to_s.titleize)
25
+ def string_filter_field(name, _options, html_options = {})
26
+ text_field(
27
+ name, {
28
+ name: "filter[#{name}]",
29
+ value: default_filter_value(name),
30
+ placeholder: @template.translate_scoped("filters.labels.#{name}", default: name.to_s.titleize),
31
+ wrapper_class: "filter"
32
+ }.deep_merge(html_options)
26
33
  )
27
34
  end
28
35
 
29
- def filter_select_tag(name, options)
30
- filter_select_tag_helper(
31
- name,
32
- options,
33
- name: "filter[#{name}]",
34
- include_blank: true,
35
- class: "form-control",
36
- data: {
37
- behavior: "select-box",
38
- placeholder: translate_scoped("filters.select.placeholder.one")
39
- }
36
+ def select_filter_field(name, options, html_options = {})
37
+ filter_select(
38
+ name, options, {
39
+ name: "filter[#{name}]",
40
+ data: {
41
+ placeholder: @template.translate_scoped("filters.select.placeholder.one")
42
+ }
43
+ }.deep_merge(html_options)
40
44
  )
41
45
  end
42
46
 
43
- def filter_multiselect_tag(name, options)
44
- filter_select_tag_helper(
45
- name,
46
- options,
47
- name: "filter[#{name}][]",
48
- multiple: true,
49
- class: "form-control",
50
- data: {
51
- behavior: "select-box",
52
- placeholder: translate_scoped("filters.select.placeholder.many")
53
- }
47
+ def multiselect_filter_field(name, options, html_options = {})
48
+ filter_select(
49
+ name, options, {
50
+ name: "filter[#{name}][]",
51
+ multiple: true,
52
+ data: {
53
+ placeholder: @template.translate_scoped("filters.select.placeholder.many")
54
+ }
55
+ }.deep_merge(html_options)
54
56
  )
55
57
  end
56
58
 
57
- def filter_select_tag_helper(name, options, html_options)
58
- unless options[:collection].is_a? Proc
59
- raise "A collection proc must be specified for select filters"
60
- end
61
-
62
- collection = options[:collection].call
63
-
64
- if collection.is_a? ActiveRecord::Relation
65
- choices = options_from_collection_for_select(
66
- collection,
67
- options[:option_value],
68
- options[:option_text],
69
- selected: default_filter_value(name)
70
- )
71
- else
72
- choices = options_for_select(
73
- collection,
74
- selected: default_filter_value(name)
75
- )
76
- end
59
+ def apply_filters_button
60
+ submit @template.translate_scoped("filters.buttons.apply")
61
+ end
77
62
 
78
- select_tag(name, choices, html_options)
63
+ def clear_filters_button
64
+ @template.link_to(
65
+ @template.translate_scoped("filters.buttons.clear"),
66
+ @template.url_for(
67
+ @template.params.slice(:scope, :order)
68
+ ),
69
+ class: "btn btn-default"
70
+ )
79
71
  end
80
72
 
81
- def filter_checkbox_tags(name, options)
73
+ private
74
+
75
+ def filter_select(name, options, html_options)
82
76
  unless options[:collection].is_a? Proc
83
- raise "A collection proc must be specified for checkbox filters"
77
+ fail "A collection proc must be specified for select filters"
84
78
  end
85
79
 
86
80
  collection = options[:collection].call
87
81
 
88
- collection.map do |item|
89
- text, value = if !item.is_a?(String) && item.respond_to?(:first) && item.respond_to?(:last)
90
- [item.first.to_s, item.last.to_s]
82
+ choices =
83
+ if collection.is_a? ActiveRecord::Relation
84
+ @template.options_from_collection_for_select(
85
+ collection,
86
+ options[:option_value],
87
+ options[:option_text],
88
+ selected: default_filter_value(name)
89
+ )
91
90
  else
92
- [item.to_s, item.to_s]
91
+ @template.options_for_select(
92
+ collection,
93
+ selected: default_filter_value(name)
94
+ )
93
95
  end
94
96
 
95
- is_checked = default_filter_value(name) ? default_filter_value(name).include?(value) : false
96
-
97
- content_tag :div, class: "checkbox" do
98
- label_tag("#{name}_#{value}") do
99
- check_box_tag("filter[#{name}][]", value, is_checked, id: "#{name}_#{value}") << text
100
- end
101
- end
102
- end.join("\n").html_safe
97
+ select(
98
+ name, choices, { include_blank: true, wrapper_class: "filter" }, {
99
+ data: { behavior: "select-box" }
100
+ }.deep_merge(html_options)
101
+ )
103
102
  end
104
103
 
105
104
  def default_filter_value(name)
106
- params[:filter] ? params[:filter][name] : nil
105
+ @template.params[:filter] ? @template.params[:filter][name] : nil
107
106
  end
108
107
  end
109
108
  end
@@ -20,9 +20,7 @@ module Godmin
20
20
  end
21
21
 
22
22
  def column_value(resource, attribute)
23
- if lookup_context.exists?("columns/#{attribute}", nil, true)
24
- render partial: "columns/#{attribute}", locals: { resource: resource }
25
- else
23
+ partial_override "columns/#{attribute}", resource: resource do
26
24
  column_value = resource.send(attribute)
27
25
 
28
26
  if column_value.is_a?(Date) || column_value.is_a?(Time)
@@ -0,0 +1,47 @@
1
+ module Godmin
2
+ class Paginator
3
+ WINDOW_SIZE = 7.freeze
4
+
5
+ attr_reader :per_page, :current_page
6
+
7
+ def initialize(resources, per_page: 25, current_page: nil)
8
+ @resources = resources
9
+ @per_page = per_page
10
+ @current_page = current_page ? current_page.to_i : 1
11
+ end
12
+
13
+ def paginate
14
+ @resources.limit(per_page).offset(offset)
15
+ end
16
+
17
+ def pages
18
+ @pages ||= begin
19
+ pages = (1..total_pages).to_a
20
+
21
+ return pages unless total_pages > WINDOW_SIZE
22
+
23
+ if current_page < WINDOW_SIZE
24
+ pages.slice(0, WINDOW_SIZE)
25
+ elsif current_page > (total_pages - WINDOW_SIZE)
26
+ pages.slice(-WINDOW_SIZE, WINDOW_SIZE)
27
+ else
28
+ pages.slice(pages.index(current_page) - (WINDOW_SIZE / 2), WINDOW_SIZE)
29
+ end
30
+ end
31
+ end
32
+
33
+ def total_pages
34
+ @total_pages ||= (total_resources.to_f / per_page).ceil
35
+ end
36
+
37
+ def total_resources
38
+ @total_resources ||= @resources.count
39
+ end
40
+
41
+ private
42
+
43
+ def offset
44
+ (current_page * per_page) - per_page
45
+ end
46
+ end
47
+ end
data/lib/godmin/rails.rb CHANGED
@@ -18,12 +18,7 @@ module ActionDispatch::Routing
18
18
  Godmin.resources << resources.first
19
19
  end
20
20
 
21
- super(*resources) do
22
- if block_given?
23
- yield
24
- end
25
- post "batch_action", on: :collection
26
- end
21
+ super
27
22
  end
28
23
 
29
24
  yield
@@ -36,15 +36,20 @@ module Godmin
36
36
  class EngineResolver < Resolver
37
37
  def initialize(controller_name)
38
38
  super [Godmin.namespace, "app/views"].compact.join("/")
39
- self.namespace = Godmin.namespace
39
+ self.namespace = Godmin.namespace
40
40
  self.controller_name = controller_name
41
41
  end
42
+
43
+ def template_paths(prefix, _partial)
44
+ return [] if prefix =~ /^godmin\//
45
+ super
46
+ end
42
47
  end
43
48
 
44
49
  class GodminResolver < Resolver
45
50
  def initialize(controller_name)
46
51
  super [Godmin::Engine.root, "app/views"].compact.join("/")
47
- self.namespace = "godmin"
52
+ self.namespace = "godmin"
48
53
  self.controller_name = controller_name
49
54
  end
50
55
  end
@@ -0,0 +1,170 @@
1
+ require "godmin/helpers/batch_actions"
2
+ require "godmin/helpers/filters"
3
+ require "godmin/helpers/tables"
4
+
5
+ module Godmin
6
+ module Resources
7
+ module ResourceController
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ helper Godmin::Helpers::BatchActions
12
+ helper Godmin::Helpers::Filters
13
+ helper Godmin::Helpers::Tables
14
+
15
+ before_action :set_resource_service
16
+ before_action :set_resource_class
17
+ before_action :set_resources, only: :index
18
+ before_action :set_resource, only: [:show, :new, :edit, :create, :destroy]
19
+ end
20
+
21
+ def index
22
+ respond_to do |format|
23
+ format.html
24
+ format.json { render json: @resources.to_json }
25
+ end
26
+ end
27
+
28
+ def show
29
+ respond_to do |format|
30
+ format.html
31
+ format.json { render json: @resource.to_json }
32
+ end
33
+ end
34
+
35
+ def new; end
36
+
37
+ def edit; end
38
+
39
+ def create
40
+ respond_to do |format|
41
+ if @resource_service.create_resource(@resource)
42
+ format.html { redirect_to redirect_after_create, notice: redirect_flash_message }
43
+ format.json { render :show, status: :created, location: @resource }
44
+ else
45
+ format.html { render :edit }
46
+ format.json { render json: @resource.errors, status: :unprocessable_entity }
47
+ end
48
+ end
49
+ end
50
+
51
+ def update
52
+ return if perform_batch_action
53
+
54
+ set_resource
55
+
56
+ respond_to do |format|
57
+ if @resource_service.update_resource(@resource, resource_params)
58
+ format.html { redirect_to redirect_after_update, notice: redirect_flash_message }
59
+ format.json { render :show, status: :ok, location: @resource }
60
+ else
61
+ format.html { render :edit }
62
+ format.json { render json: @resource.errors, status: :unprocessable_entity }
63
+ end
64
+ end
65
+ end
66
+
67
+ def destroy
68
+ @resource.destroy
69
+
70
+ respond_to do |format|
71
+ format.html { redirect_to redirect_after_destroy, notice: redirect_flash_message }
72
+ format.json { head :no_content }
73
+ end
74
+ end
75
+
76
+ protected
77
+
78
+ def set_resource_service
79
+ @resource_service = resource_service
80
+ end
81
+
82
+ def set_resource_class
83
+ @resource_class = resource_class
84
+ end
85
+
86
+ def set_resources
87
+ @resources = resources
88
+ authorize(@resources) if authorization_enabled?
89
+ end
90
+
91
+ def set_resource
92
+ @resource = resource
93
+ authorize(@resource) if authorization_enabled?
94
+ end
95
+
96
+ def resource_service_class
97
+ "#{controller_path.singularize}_service".classify.constantize
98
+ end
99
+
100
+ def resource_service
101
+ if authentication_enabled?
102
+ resource_service_class.new(admin_user: admin_user)
103
+ else
104
+ resource_service_class.new
105
+ end
106
+ end
107
+
108
+ def resource_class
109
+ @resource_service.resource_class
110
+ end
111
+
112
+ def resources
113
+ @resource_service.resources(params)
114
+ end
115
+
116
+ def resource
117
+ if params[:id]
118
+ @resource_service.find_resource(params[:id])
119
+ else
120
+ case action_name
121
+ when "create"
122
+ @resource_service.build_resource(resource_params)
123
+ when "new"
124
+ @resource_service.build_resource(nil)
125
+ end
126
+ end
127
+ end
128
+
129
+ def resource_params
130
+ params.require(resource_class.model_name.param_key.to_sym).permit(@resource_service.attrs_for_form)
131
+ end
132
+
133
+ def redirect_after_create
134
+ redirect_after_save
135
+ end
136
+
137
+ def redirect_after_update
138
+ redirect_after_save
139
+ end
140
+
141
+ def redirect_after_save
142
+ @resource
143
+ end
144
+
145
+ def redirect_after_destroy
146
+ resource_class.model_name.route_key.to_sym
147
+ end
148
+
149
+ def redirect_flash_message
150
+ translate_scoped("flash.#{action_name}", resource: @resource.class.model_name.human)
151
+ end
152
+
153
+ def perform_batch_action
154
+ return false unless params[:batch_action].present?
155
+
156
+ item_ids = params[:id].split(",").map(&:to_i)
157
+
158
+ if @resource_service.batch_action(params[:batch_action], item_ids)
159
+ flash[:updated_ids] = item_ids
160
+
161
+ if respond_to?("redirect_after_batch_action_#{params[:batch_action]}", true)
162
+ redirect_to send("redirect_after_batch_action_#{params[:batch_action]}") and return true
163
+ end
164
+ end
165
+
166
+ redirect_to :back and return true
167
+ end
168
+ end
169
+ end
170
+ end