godmin 0.10.3 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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