cm-admin 2.1.4 → 2.2.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/Gemfile.lock +1 -1
- data/app/assets/javascripts/cm_admin/filters.js +30 -3
- data/app/assets/javascripts/cm_admin/scaffolds.js +0 -1
- data/app/assets/stylesheets/cm_admin/base/filters.scss +20 -0
- data/app/assets/stylesheets/cm_admin/cm_admin.css.scss +0 -1
- data/app/controllers/cm_admin/resource_controller.rb +65 -62
- data/app/models/concerns/cm_admin/cm_role.rb +10 -6
- data/app/views/cm_admin/main/_associated_table.html.slim +10 -12
- data/app/views/cm_admin/main/_nested_table_form.html.slim +1 -1
- data/app/views/cm_admin/main/_sort.html.slim +27 -0
- data/app/views/cm_admin/main/_table.html.slim +4 -5
- data/app/views/cm_admin/roles/permissions.html.slim +2 -1
- data/app/views/layouts/cm_admin.html.slim +1 -0
- data/config/importmap.rb +0 -1
- data/config/routes.rb +7 -1
- data/docs/RoleManagement.md +36 -0
- data/lib/cm_admin/model.rb +85 -77
- data/lib/cm_admin/models/dsl_method.rb +23 -0
- data/lib/cm_admin/version.rb +1 -1
- data/lib/cm_admin/view_helpers/page_info_helper.rb +2 -1
- metadata +3 -3
- data/app/assets/stylesheets/cm_admin/dependency/fontawesome.all.css +0 -7831
data/docs/RoleManagement.md
CHANGED
@@ -62,6 +62,21 @@ end
|
|
62
62
|
4. Include `CmRole` in the `config.included_models` section of `config/initializers/zcm_admin.rb`.
|
63
63
|
5. Assign `cm_role_id` to `1` for any user in the `User` Model, and use that user to log in.
|
64
64
|
|
65
|
+
## Setting up scopes
|
66
|
+
|
67
|
+
By default, `Full Access` scopes is added to each permission item. To add additional scopes, use the following syntax:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
...
|
71
|
+
cm_admin do
|
72
|
+
actions only: []
|
73
|
+
set_icon "fa fa-user"
|
74
|
+
set_policy_scopes [{scope_name: 'test_supplier_filter', display_name: 'By Test Supplier'}]
|
75
|
+
cm_index do
|
76
|
+
page_title 'User'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
65
80
|
## Overriding Policies
|
66
81
|
|
67
82
|
By default, roles and policies are enabled for all models in the application. To override a policy, use the following syntax:
|
@@ -90,3 +105,24 @@ end
|
|
90
105
|
```
|
91
106
|
|
92
107
|
This structure helps ensure that your application's role and permission management is both flexible and secure.
|
108
|
+
|
109
|
+
|
110
|
+
## Permission based fields
|
111
|
+
|
112
|
+
We can apply permission logic to display a field on the interface. You can do this with the following syntax.
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
...
|
116
|
+
tab :details, '' do
|
117
|
+
row do
|
118
|
+
cm_show_section 'Details' do
|
119
|
+
field :status, field_type: :tag, tag_class: Item::STATUS_TAG_COLOR, display_if: -> (record) {
|
120
|
+
scoped_model = CmAdmin::ItemPolicy::ArchiveScope.new(Current.user, ::Item).resolve
|
121
|
+
return scoped_model.find_by(id: record.id).present?
|
122
|
+
}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
```
|
128
|
+
|
data/lib/cm_admin/model.rb
CHANGED
@@ -31,11 +31,12 @@ module CmAdmin
|
|
31
31
|
|
32
32
|
attr_accessor :available_actions, :actions_set, :available_fields, :additional_permitted_fields,
|
33
33
|
:current_action, :params, :filters, :available_tabs, :icon_name, :bulk_actions, :display_name,
|
34
|
-
:policy_scopes, :override_policy, :alerts
|
34
|
+
:policy_scopes, :override_policy, :alerts, :sort_columns, :default_sort_column, :default_sort_direction
|
35
35
|
attr_reader :name, :ar_model, :is_visible_on_sidebar, :importer
|
36
36
|
|
37
37
|
def initialize(entity, &block)
|
38
38
|
@name = entity.name
|
39
|
+
@display_name = entity.name
|
39
40
|
@ar_model = entity
|
40
41
|
@is_visible_on_sidebar = true
|
41
42
|
@icon_name = 'fa fa-th-large'
|
@@ -48,7 +49,9 @@ module CmAdmin
|
|
48
49
|
@params = nil
|
49
50
|
@override_policy = false
|
50
51
|
@filters ||= []
|
51
|
-
@policy_scopes ||= [{display_name: 'Full Access', scope_name: 'all'}]
|
52
|
+
@policy_scopes ||= [{ display_name: 'Full Access', scope_name: 'all' }]
|
53
|
+
@sort_columns ||= []
|
54
|
+
@default_sort_direction ||= 'asc'
|
52
55
|
@alerts = []
|
53
56
|
instance_eval(&block) if block_given?
|
54
57
|
actions unless @actions_set
|
@@ -65,28 +68,28 @@ module CmAdmin
|
|
65
68
|
|
66
69
|
def custom_controller_action(action_name, params)
|
67
70
|
current_action = CmAdmin::Models::Action.find_by(self, name: action_name.to_s)
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
return @ar_object
|
71
|
+
return unless current_action
|
72
|
+
|
73
|
+
@current_action = current_action
|
74
|
+
@ar_object = @ar_model.name.classify.constantize.find(params[:id])
|
75
|
+
if @current_action.child_records
|
76
|
+
child_records = @ar_object.send(@current_action.child_records)
|
77
|
+
@associated_model = CmAdmin::Model.find_by(name: @ar_model.reflect_on_association(@current_action.child_records).klass.name)
|
78
|
+
@associated_ar_object = if child_records.is_a? ActiveRecord::Relation
|
79
|
+
filter_by(params, child_records)
|
80
|
+
else
|
81
|
+
child_records
|
82
|
+
end
|
83
|
+
return @ar_object, @associated_model, @associated_ar_object
|
82
84
|
end
|
85
|
+
@ar_object
|
83
86
|
end
|
84
87
|
|
85
88
|
# Insert into actions according to config block
|
86
89
|
def actions(only: [], except: [])
|
87
90
|
acts = CmAdmin::DEFAULT_ACTIONS.keys
|
88
|
-
acts
|
89
|
-
acts
|
91
|
+
acts &= ([] << only).flatten if only.present?
|
92
|
+
acts -= ([] << except).flatten if except.present?
|
90
93
|
acts.each do |act|
|
91
94
|
action_defaults = CmAdmin::DEFAULT_ACTIONS[act]
|
92
95
|
@available_actions << CmAdmin::Models::Action.new(name: act.to_s, verb: action_defaults[:verb], path: action_defaults[:path])
|
@@ -106,7 +109,7 @@ module CmAdmin
|
|
106
109
|
@icon_name = name
|
107
110
|
end
|
108
111
|
|
109
|
-
def override_pundit_policy(override_status=false)
|
112
|
+
def override_pundit_policy(override_status = false)
|
110
113
|
@override_policy = override_status
|
111
114
|
end
|
112
115
|
|
@@ -114,7 +117,7 @@ module CmAdmin
|
|
114
117
|
@display_name = name
|
115
118
|
end
|
116
119
|
|
117
|
-
def permit_additional_fields(fields=[])
|
120
|
+
def permit_additional_fields(fields = [])
|
118
121
|
@additional_permitted_fields = fields
|
119
122
|
end
|
120
123
|
|
@@ -130,17 +133,17 @@ module CmAdmin
|
|
130
133
|
@display_name.present? ? @display_name : @name
|
131
134
|
end
|
132
135
|
|
133
|
-
def set_policy_scopes(scopes=[])
|
134
|
-
@policy_scopes = ([{display_name: 'Full Access', scope_name: 'all'}] + scopes).uniq
|
136
|
+
def set_policy_scopes(scopes = [])
|
137
|
+
@policy_scopes = ([{ display_name: 'Full Access', scope_name: 'all' }] + scopes).uniq
|
135
138
|
end
|
136
139
|
|
137
140
|
# Shared between export controller and resource controller
|
138
141
|
def filter_params(params)
|
139
142
|
# OPTIMIZE: Need to check if we can permit the filter_params in a better way
|
140
|
-
date_columns =
|
141
|
-
range_columns =
|
142
|
-
single_select_columns =
|
143
|
-
multi_select_columns =
|
143
|
+
date_columns = filters.select { |x| x.filter_type.eql?(:date) }.map(&:db_column_name)
|
144
|
+
range_columns = filters.select { |x| x.filter_type.eql?(:range) }.map(&:db_column_name)
|
145
|
+
single_select_columns = filters.select { |x| x.filter_type.eql?(:single_select) }.map(&:db_column_name)
|
146
|
+
multi_select_columns = filters.select { |x| x.filter_type.eql?(:multi_select) }.map { |x| Hash["#{x.db_column_name}", []] }
|
144
147
|
|
145
148
|
params.require(:filters).permit(:search, date: date_columns, range: range_columns, single_select: single_select_columns, multi_select: multi_select_columns) if params[:filters]
|
146
149
|
end
|
@@ -150,74 +153,79 @@ module CmAdmin
|
|
150
153
|
# Controller defined for each model
|
151
154
|
# If model is User, controller will be UsersController
|
152
155
|
def define_controller
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
156
|
+
if $available_actions.present?
|
157
|
+
klass = Class.new(CmAdmin::ResourceController) do
|
158
|
+
include Pundit::Authorization
|
159
|
+
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
160
|
+
|
161
|
+
$available_actions.each do |action|
|
162
|
+
define_method action.name.to_sym do
|
163
|
+
# controller_name & action_name from ActionController
|
164
|
+
@model = CmAdmin::Model.find_by(name: controller_name.classify)
|
165
|
+
@model.params = params
|
166
|
+
@action = CmAdmin::Models::Action.find_by(@model, name: action_name)
|
167
|
+
@model.current_action = @action
|
168
|
+
send(@action.controller_action_name, params)
|
169
|
+
# @ar_object = @model.try(@action.parent || action_name, params)
|
170
|
+
end
|
166
171
|
end
|
167
|
-
end
|
168
172
|
|
169
|
-
|
170
|
-
|
171
|
-
|
173
|
+
def pundit_user
|
174
|
+
Current.user
|
175
|
+
end
|
172
176
|
|
173
|
-
|
177
|
+
private
|
174
178
|
|
175
|
-
|
176
|
-
|
177
|
-
|
179
|
+
def user_not_authorized
|
180
|
+
flash[:alert] = 'You are not authorized to perform this action.'
|
181
|
+
redirect_to CmAdmin::Engine.mount_path + '/access-denied'
|
182
|
+
end
|
178
183
|
end
|
179
|
-
end
|
184
|
+
end
|
180
185
|
CmAdmin.const_set "#{@name}Controller", klass
|
181
186
|
end
|
182
187
|
|
183
188
|
def define_pundit_policy(ar_model)
|
184
|
-
|
189
|
+
if $available_actions.present?
|
190
|
+
klass = Class.new(ApplicationPolicy) do
|
191
|
+
$available_actions.each do |action|
|
192
|
+
define_method "#{action.name}?".to_sym do
|
193
|
+
return false unless Current.user.respond_to?(:cm_role_id)
|
194
|
+
return false if Current.user.cm_role.nil?
|
195
|
+
|
196
|
+
Current.user.cm_role.cm_permissions.where(action_name: action.name, ar_model_name: ar_model.name).present?
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
policy = CmAdmin.const_set "#{ar_model.name}Policy", klass
|
185
202
|
|
186
|
-
|
187
|
-
|
203
|
+
$available_actions.each do |action|
|
204
|
+
next if %w[custom_action_modal custom_action create update].include?(action.name)
|
188
205
|
|
189
|
-
|
190
|
-
|
206
|
+
klass = Class.new(policy) do
|
207
|
+
def initialize(user, scope)
|
208
|
+
@user = user
|
209
|
+
@scope = scope
|
210
|
+
end
|
191
211
|
|
192
|
-
|
212
|
+
define_method :resolve do
|
213
|
+
# action_name = Current.request_params.dig("action")
|
214
|
+
permission = Current.user.cm_role.cm_permissions.find_by(action_name: action.name, ar_model_name: ar_model.name)
|
215
|
+
if permission.present? && permission.scope_name.present?
|
216
|
+
scope.send(permission.scope_name)
|
217
|
+
else
|
218
|
+
scope.all
|
219
|
+
end
|
193
220
|
end
|
194
|
-
end
|
195
221
|
|
196
|
-
|
197
|
-
policy = CmAdmin.const_set "#{ar_model.name}Policy", klass
|
222
|
+
private
|
198
223
|
|
199
|
-
|
200
|
-
def initialize(user, scope)
|
201
|
-
@user = user
|
202
|
-
@scope = scope
|
224
|
+
attr_reader :user, :scope
|
203
225
|
end
|
204
|
-
|
205
|
-
define_method :resolve do
|
206
|
-
action_name = Current.request_params.dig("action")
|
207
|
-
permission = Current.user.cm_role.cm_permissions.find_by(action_name: action_name, ar_model_name: ar_model.name)
|
208
|
-
if permission.present? && permission.scope_name.present?
|
209
|
-
scope.send(permission.scope_name)
|
210
|
-
else
|
211
|
-
scope.all
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
private
|
216
|
-
|
217
|
-
attr_reader :user, :scope
|
218
|
-
end
|
219
226
|
|
220
|
-
|
227
|
+
policy.const_set "#{action.name.classify}Scope", klass
|
228
|
+
end
|
221
229
|
end
|
222
230
|
end
|
223
231
|
end
|
@@ -319,6 +319,7 @@ module CmAdmin
|
|
319
319
|
@filters << CmAdmin::Models::Filter.new(db_column_name:, filter_type:, options:)
|
320
320
|
end
|
321
321
|
|
322
|
+
# @deprecated: use {#sortable_columns} instead of this method
|
322
323
|
# Set sort direction for filters
|
323
324
|
# @param direction [Symbol] the direction of sort, +:asc+, +:desc+
|
324
325
|
# @example Setting sort direction
|
@@ -329,6 +330,7 @@ module CmAdmin
|
|
329
330
|
@current_action.sort_direction = direction.to_sym if @current_action
|
330
331
|
end
|
331
332
|
|
333
|
+
# @deprecated: use {#sortable_columns} instead of this method
|
332
334
|
# Set sort column for filters
|
333
335
|
# @param column [Symbol] the column name
|
334
336
|
# @example Setting sort column
|
@@ -374,6 +376,27 @@ module CmAdmin
|
|
374
376
|
def alert_box(header: nil, body: nil, type: nil, partial: nil, display_if: nil, html_attrs: {})
|
375
377
|
@section_fields << CmAdmin::Models::Alert.new(header, body, type, partial:, display_if:, html_attrs:)
|
376
378
|
end
|
379
|
+
|
380
|
+
# Configure sortable columns for model
|
381
|
+
# @param columns [Array] the array of hash for column and display name
|
382
|
+
# @example Sortable Columns
|
383
|
+
# sortable_columns([{column: 'id', display_name: 'ID'}, {column: 'updated_at', display_name: 'Last Updated At'}])
|
384
|
+
#
|
385
|
+
# @example Sortable Columns with default column and direction
|
386
|
+
# sortable_columns([{column: 'id', display_name: 'ID', default: true, default_direction: 'desc'}, {column: 'updated_at', display_name: 'Last Updated At'}])
|
387
|
+
def sortable_columns(columns)
|
388
|
+
@sort_columns = columns
|
389
|
+
default_column = columns.filter do |column|
|
390
|
+
column.key?(:default) || column.key?(:default_direction)
|
391
|
+
end
|
392
|
+
raise ArgumentError, 'only one column can be default' if default_column.size > 1
|
393
|
+
return if default_column.blank?
|
394
|
+
|
395
|
+
default_column = default_column.first
|
396
|
+
|
397
|
+
@default_sort_column = default_column[:column]
|
398
|
+
@default_sort_direction = default_column[:default_direction] if default_column[:default_direction].present?
|
399
|
+
end
|
377
400
|
end
|
378
401
|
end
|
379
402
|
end
|
data/lib/cm_admin/version.rb
CHANGED
@@ -39,7 +39,8 @@ module CmAdmin
|
|
39
39
|
|
40
40
|
def custom_action_items(custom_action, current_action_name)
|
41
41
|
if custom_action.name.present? && policy([:cm_admin, @model.name.classify.constantize]).send(:"#{custom_action.name}?")
|
42
|
-
|
42
|
+
scoped_model = "CmAdmin::#{@model.name}Policy::#{custom_action.name.classify}Scope".constantize.new(Current.user, @model.name.constantize).resolve
|
43
|
+
if custom_action.display_if.call(@ar_object) && scoped_model.find_by(id: params[:id])
|
43
44
|
case custom_action.display_type
|
44
45
|
when :icon_only
|
45
46
|
custom_action_icon(custom_action, current_action_name)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cm-admin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael
|
@@ -14,7 +14,7 @@ authors:
|
|
14
14
|
autorequire:
|
15
15
|
bindir: exe
|
16
16
|
cert_chain: []
|
17
|
-
date: 2024-
|
17
|
+
date: 2024-10-01 00:00:00.000000000 Z
|
18
18
|
dependencies:
|
19
19
|
- !ruby/object:Gem::Dependency
|
20
20
|
name: caxlsx_rails
|
@@ -345,7 +345,6 @@ files:
|
|
345
345
|
- app/assets/stylesheets/cm_admin/dependency/bootstrap/scss/utilities/_api.scss
|
346
346
|
- app/assets/stylesheets/cm_admin/dependency/bootstrap/scss/vendor/_rfs.scss
|
347
347
|
- app/assets/stylesheets/cm_admin/dependency/flatpickr.min.css
|
348
|
-
- app/assets/stylesheets/cm_admin/dependency/fontawesome.all.css
|
349
348
|
- app/assets/stylesheets/cm_admin/dependency/jquery-jgrowl.min.css
|
350
349
|
- app/assets/stylesheets/cm_admin/helpers/_mixins.scss
|
351
350
|
- app/assets/stylesheets/cm_admin/helpers/_variable.scss
|
@@ -387,6 +386,7 @@ files:
|
|
387
386
|
- app/views/cm_admin/main/_show_as_drawer.html.slim
|
388
387
|
- app/views/cm_admin/main/_show_content.html.slim
|
389
388
|
- app/views/cm_admin/main/_show_section.html.slim
|
389
|
+
- app/views/cm_admin/main/_sort.html.slim
|
390
390
|
- app/views/cm_admin/main/_table.html.slim
|
391
391
|
- app/views/cm_admin/main/_tabs.html.slim
|
392
392
|
- app/views/cm_admin/main/_top_navbar.html.slim
|