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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +8 -0
- data/README.md +146 -81
- data/app/assets/javascripts/godmin/batch-actions.js +18 -13
- data/app/assets/stylesheets/godmin/index.css.scss +15 -8
- data/app/views/godmin/resource/_batch_actions.html.erb +2 -4
- data/app/views/godmin/resource/_breadcrumb.html.erb +0 -3
- data/app/views/godmin/resource/_button_actions.html.erb +2 -2
- data/app/views/godmin/resource/_filters.html.erb +17 -18
- data/app/views/godmin/resource/_form.html.erb +1 -1
- data/app/views/godmin/resource/_pagination.html.erb +11 -11
- data/app/views/godmin/resource/_scopes.html.erb +4 -4
- data/app/views/godmin/resource/_table.html.erb +30 -30
- data/app/views/godmin/resource/index.html.erb +3 -10
- data/godmin.gemspec +4 -1
- data/lib/generators/godmin/authentication/templates/sessions_controller.rb +1 -1
- data/lib/generators/godmin/install/install_generator.rb +1 -1
- data/lib/generators/godmin/resource/resource_generator.rb +4 -0
- data/lib/generators/godmin/resource/templates/resource_controller.rb +1 -9
- data/lib/generators/godmin/resource/templates/resource_service.rb +8 -0
- data/lib/godmin.rb +4 -2
- data/lib/godmin/{application.rb → application_controller.rb} +1 -1
- data/lib/godmin/authentication.rb +1 -1
- data/lib/godmin/authentication/{sessions.rb → sessions_controller.rb} +1 -1
- data/lib/godmin/authorization.rb +1 -1
- data/lib/godmin/authorization/policy_finder.rb +3 -1
- data/lib/godmin/helpers/application.rb +10 -0
- data/lib/godmin/helpers/batch_actions.rb +7 -4
- data/lib/godmin/helpers/filters.rb +72 -73
- data/lib/godmin/helpers/tables.rb +1 -3
- data/lib/godmin/paginator.rb +47 -0
- data/lib/godmin/rails.rb +1 -6
- data/lib/godmin/resolver.rb +7 -2
- data/lib/godmin/resources/resource_controller.rb +170 -0
- data/lib/godmin/resources/resource_service.rb +82 -0
- data/lib/godmin/resources/resource_service/batch_actions.rb +38 -0
- data/lib/godmin/resources/resource_service/filters.rb +37 -0
- data/lib/godmin/resources/resource_service/ordering.rb +27 -0
- data/lib/godmin/resources/resource_service/pagination.rb +22 -0
- data/lib/godmin/resources/resource_service/scopes.rb +61 -0
- data/lib/godmin/version.rb +1 -1
- data/test/dummy/config/environments/production.rb +1 -1
- data/test/dummy/config/environments/test.rb +1 -1
- data/test/dummy/db/schema.rb +16 -0
- data/test/lib/godmin/helpers/filters_test.rb +26 -0
- data/test/lib/godmin/paginator_test.rb +84 -0
- data/test/lib/godmin/policy_finder_test.rb +35 -6
- data/test/lib/godmin/resolver_test.rb +6 -0
- data/test/lib/godmin/resources/resource_service/batch_actions_test.rb +45 -0
- data/test/lib/godmin/resources/resource_service/filters_test.rb +32 -0
- data/test/lib/godmin/resources/resource_service/ordering_test.rb +37 -0
- data/test/lib/godmin/resources/resource_service/pagination_test.rb +31 -0
- data/test/lib/godmin/resources/resource_service/scopes_test.rb +57 -0
- data/test/lib/godmin/resources/resource_service_test.rb +21 -0
- data/test/test_helper.rb +62 -0
- metadata +75 -17
- data/.hound.yml +0 -3
- data/lib/godmin/resource.rb +0 -177
- data/lib/godmin/resource/batch_actions.rb +0 -45
- data/lib/godmin/resource/filters.rb +0 -41
- data/lib/godmin/resource/ordering.rb +0 -25
- data/lib/godmin/resource/pagination.rb +0 -64
- data/lib/godmin/resource/scopes.rb +0 -54
- data/test/dummy/db/test.sqlite3 +0 -0
data/lib/godmin/authorization.rb
CHANGED
@@ -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
|
-
|
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?(
|
22
|
-
(options[:except] && !options[:except].include?(
|
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
|
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
|
-
|
17
|
+
string_filter_field(name, options, html_options)
|
8
18
|
when :select
|
9
|
-
|
19
|
+
select_filter_field(name, options, html_options)
|
10
20
|
when :multiselect
|
11
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
30
|
-
|
31
|
-
name,
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
44
|
-
|
45
|
-
name,
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
58
|
-
|
59
|
-
|
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
|
-
|
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
|
-
|
73
|
+
private
|
74
|
+
|
75
|
+
def filter_select(name, options, html_options)
|
82
76
|
unless options[:collection].is_a? Proc
|
83
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
91
|
+
@template.options_for_select(
|
92
|
+
collection,
|
93
|
+
selected: default_filter_value(name)
|
94
|
+
)
|
93
95
|
end
|
94
96
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
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
data/lib/godmin/resolver.rb
CHANGED
@@ -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
|
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
|
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
|