cccux 0.1.0 → 0.3.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -0
  3. data/README.md +124 -112
  4. data/Rakefile +57 -4
  5. data/app/assets/stylesheets/cccux/application.css +96 -72
  6. data/app/controllers/cccux/ability_permissions_controller.rb +138 -33
  7. data/app/controllers/cccux/application_controller.rb +7 -0
  8. data/app/controllers/cccux/cccux_controller.rb +20 -10
  9. data/app/controllers/cccux/dashboard_controller.rb +203 -32
  10. data/app/controllers/cccux/role_abilities_controller.rb +70 -0
  11. data/app/controllers/cccux/roles_controller.rb +70 -81
  12. data/app/controllers/cccux/users_controller.rb +22 -7
  13. data/app/controllers/concerns/cccux/application_controller_concern.rb +6 -2
  14. data/app/helpers/cccux/authorization_helper.rb +29 -21
  15. data/app/models/cccux/ability.rb +83 -32
  16. data/app/models/cccux/ability_permission.rb +9 -0
  17. data/app/models/cccux/post.rb +5 -0
  18. data/app/models/cccux/role.rb +19 -1
  19. data/app/models/cccux/role_ability.rb +3 -0
  20. data/app/models/concerns/cccux/user_concern.rb +5 -2
  21. data/app/views/cccux/ability_permissions/new.html.erb +2 -2
  22. data/app/views/cccux/dashboard/model_discovery.html.erb +7 -2
  23. data/app/views/cccux/roles/_form.html.erb +24 -71
  24. data/app/views/cccux/roles/edit.html.erb +5 -5
  25. data/app/views/cccux/roles/index.html.erb +1 -8
  26. data/app/views/cccux/roles/new.html.erb +1 -3
  27. data/app/views/cccux/users/edit.html.erb +4 -4
  28. data/app/views/cccux/users/new.html.erb +30 -15
  29. data/app/views/layouts/cccux/admin.html.erb +1 -2
  30. data/app/views/shared/_footer.html.erb +1 -1
  31. data/config/routes.rb +7 -6
  32. data/lib/cccux/engine.rb +7 -6
  33. data/lib/cccux/version.rb +1 -1
  34. data/lib/cccux.rb +12 -0
  35. data/lib/tasks/cccux.rake +271 -159
  36. metadata +10 -22
@@ -1,7 +1,5 @@
1
1
  module Cccux
2
2
  class AbilityPermissionsController < CccuxController
3
- # Ensure only Role Managers can access permission management
4
- before_action :ensure_role_manager
5
3
 
6
4
  before_action :set_ability_permission, only: [:show, :edit, :update, :destroy]
7
5
 
@@ -112,14 +110,10 @@ module Cccux
112
110
  def create_single_permission
113
111
  @ability_permission = Cccux::AbilityPermission.new(ability_permission_params)
114
112
 
115
- if @ability_permission.save
116
- redirect_to cccux.ability_permissions_path, notice: 'Permission was successfully created.'
117
- else
118
- @available_subjects = get_available_subjects
119
- @available_actions = get_available_actions
120
- @subject_actions_map = get_subject_actions_map
121
- render :new
113
+ unless @ability_permission.save
114
+ raise "AbilityPermission creation failed: \n#{@ability_permission.errors.full_messages.join(', ')}"
122
115
  end
116
+ redirect_to cccux.ability_permissions_path, notice: 'Permission was successfully created.'
123
117
  end
124
118
 
125
119
  def get_available_subjects
@@ -155,13 +149,26 @@ module Cccux
155
149
 
156
150
  # Add route-discovered actions
157
151
  route_actions = discover_actions_for_model(subject)
152
+ Rails.logger.debug "Route actions for #{subject}: #{route_actions}"
158
153
  actions += route_actions
154
+ # Add module-specific custom actions
155
+ if subject.include?('::')
156
+ module_name = subject.split('::').first
157
+ model_name = subject.split('::').last
158
+
159
+ # Discover custom actions for this module's controllers
160
+ custom_actions = discover_custom_actions_for_module(module_name, model_name)
161
+ actions += custom_actions
162
+ Rails.logger.debug "Added #{module_name} custom actions: #{custom_actions}"
163
+ end
159
164
 
160
165
  # Add existing actions for this subject
161
166
  existing_actions = Cccux::AbilityPermission.where(subject: subject).distinct.pluck(:action).compact
162
167
  actions += existing_actions
163
168
 
164
- actions.uniq.sort
169
+ final_actions = actions.uniq.sort
170
+ Rails.logger.debug "Final actions for #{subject}: #{final_actions}"
171
+ final_actions
165
172
  end
166
173
 
167
174
  def discover_application_models
@@ -194,11 +201,73 @@ module Cccux
194
201
  actions = []
195
202
 
196
203
  begin
197
- # Convert subject to potential route patterns
198
- resource_name = subject.underscore.pluralize
204
+ Rails.logger.debug "Discovering actions for subject: #{subject}"
199
205
 
200
- # For CCCUX models, also check the engine routes
201
- if subject.start_with?('Cccux::')
206
+ # Handle namespaced models (e.g., MegaBar::Page)
207
+ if subject.include?('::')
208
+ module_name = subject.split('::').first
209
+ model_name = subject.split('::').last.underscore.pluralize
210
+ controller_pattern = "#{module_name.underscore}/#{model_name}"
211
+
212
+ Rails.logger.debug "Namespaced model detected. Looking for controller: #{controller_pattern}"
213
+
214
+ # Try to find the engine for this module
215
+ engine_class = "#{module_name}::Engine".constantize rescue nil
216
+
217
+ if engine_class
218
+ Rails.logger.debug "Found engine: #{engine_class}"
219
+
220
+ # Look through engine routes for this namespaced controller
221
+ engine_class.routes.routes.each do |route|
222
+ controller_name = route.defaults[:controller]
223
+ next unless controller_name&.start_with?("#{module_name.underscore}/")
224
+ next unless route.defaults[:action]
225
+
226
+ action = route.defaults[:action]
227
+ Rails.logger.debug "Found #{module_name} engine route: #{controller_name}##{action}"
228
+
229
+ # Map HTTP verbs to standard actions and include custom actions
230
+ case action
231
+ when 'index' then actions << 'read'
232
+ when 'show' then actions << 'read'
233
+ when 'create' then actions << 'create'
234
+ when 'update' then actions << 'update'
235
+ when 'destroy' then actions << 'destroy'
236
+ when 'edit', 'new' then next # Skip these as they're UI actions
237
+ else
238
+ # Custom actions like 'move', 'administer_page', etc.
239
+ actions << action
240
+ end
241
+ end
242
+ else
243
+ Rails.logger.debug "No engine found for #{module_name}, checking main routes"
244
+
245
+ # Fallback to main Rails routes
246
+ Rails.application.routes.routes.each do |route|
247
+ controller_name = route.defaults[:controller]
248
+ next unless controller_name&.start_with?("#{module_name.underscore}/")
249
+ next unless route.defaults[:action]
250
+
251
+ action = route.defaults[:action]
252
+ Rails.logger.debug "Found #{module_name} route: #{controller_name}##{action}"
253
+
254
+ # Map HTTP verbs to standard actions and include custom actions
255
+ case action
256
+ when 'index' then actions << 'read'
257
+ when 'show' then actions << 'read'
258
+ when 'create' then actions << 'create'
259
+ when 'update' then actions << 'update'
260
+ when 'destroy' then actions << 'destroy'
261
+ when 'edit', 'new' then next # Skip these as they're UI actions
262
+ else
263
+ # Custom actions like 'move', 'administer_page', etc.
264
+ actions << action
265
+ end
266
+ end
267
+ end
268
+
269
+ # Handle CCCUX models
270
+ elsif subject.start_with?('Cccux::')
202
271
  cccux_resource_name = subject.gsub('Cccux::', '').underscore.pluralize
203
272
 
204
273
  # Look through CCCUX engine routes for this resource
@@ -222,26 +291,31 @@ module Cccux
222
291
  end
223
292
  end
224
293
  end
225
- end
226
-
227
- # Also look through main Rails routes for this resource
228
- Rails.application.routes.routes.each do |route|
229
- next unless route.path.spec.to_s.include?(resource_name)
230
294
 
231
- # Extract action from route
232
- if route.defaults[:action]
233
- action = route.defaults[:action]
234
- # Map HTTP verbs to standard actions and include custom actions
235
- case action
236
- when 'index' then actions << 'read'
237
- when 'show' then actions << 'read'
238
- when 'create' then actions << 'create'
239
- when 'update' then actions << 'update'
240
- when 'destroy' then actions << 'destroy'
241
- when 'edit', 'new' then next # Skip these as they're UI actions
242
- else
243
- # Custom actions
244
- actions << action
295
+ # Handle regular models
296
+ else
297
+ resource_name = subject.underscore.pluralize
298
+ Rails.logger.debug "Regular model detected. Resource name: #{resource_name}"
299
+
300
+ # Look through main Rails routes for this resource
301
+ Rails.application.routes.routes.each do |route|
302
+ next unless route.path.spec.to_s.include?(resource_name)
303
+
304
+ # Extract action from route
305
+ if route.defaults[:action]
306
+ action = route.defaults[:action]
307
+ # Map HTTP verbs to standard actions and include custom actions
308
+ case action
309
+ when 'index' then actions << 'read'
310
+ when 'show' then actions << 'read'
311
+ when 'create' then actions << 'create'
312
+ when 'update' then actions << 'update'
313
+ when 'destroy' then actions << 'destroy'
314
+ when 'edit', 'new' then next # Skip these as they're UI actions
315
+ else
316
+ # Custom actions
317
+ actions << action
318
+ end
245
319
  end
246
320
  end
247
321
  end
@@ -253,6 +327,37 @@ module Cccux
253
327
  actions.uniq
254
328
  end
255
329
 
330
+ def discover_custom_actions_for_module(module_name, model_name)
331
+ custom_actions = []
332
+
333
+ begin
334
+ # Convert model name to controller name pattern
335
+ controller_name = "#{module_name.downcase}/#{model_name.underscore.pluralize}"
336
+
337
+ Rails.logger.debug "Looking for custom actions in controller: #{controller_name}"
338
+
339
+ # Look through Rails routes for this module's controllers
340
+ Rails.application.routes.routes.each do |route|
341
+ next unless route.defaults[:controller]&.start_with?("#{module_name.downcase}/")
342
+ next unless route.defaults[:action]
343
+
344
+ action = route.defaults[:action]
345
+
346
+ # Skip standard CRUD actions and UI actions
347
+ next if %w[index show create update destroy new edit].include?(action)
348
+
349
+ # Include custom actions
350
+ custom_actions << action
351
+ Rails.logger.debug "Found custom action: #{action} in #{route.defaults[:controller]}"
352
+ end
353
+
354
+ rescue => e
355
+ Rails.logger.warn "Error discovering custom actions for #{module_name}::#{model_name}: #{e.message}"
356
+ end
357
+
358
+ custom_actions.uniq
359
+ end
360
+
256
361
  def skip_model?(model_class)
257
362
  excluded_patterns = [
258
363
  /^ActiveRecord::/,
@@ -29,7 +29,14 @@ module Cccux
29
29
 
30
30
  private
31
31
 
32
+ def devise_controller?
33
+ # Only return true if Devise is available and this is a Devise controller
34
+ defined?(Devise) && respond_to?(:devise_controller?) ? super : false
35
+ end
36
+
32
37
  def configure_permitted_parameters
38
+ return unless defined?(Devise) && respond_to?(:devise_parameter_sanitizer)
39
+
33
40
  devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name])
34
41
  devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name])
35
42
  end
@@ -4,7 +4,11 @@ module Cccux
4
4
 
5
5
  # Override the default error message for admin interface
6
6
  rescue_from CanCan::AccessDenied do |exception|
7
- redirect_to main_app.root_path, alert: 'Access denied. Only Role Managers can access the admin interface.'
7
+ respond_to do |format|
8
+ format.html { render file: Rails.root.join('public', '403.html'), status: :forbidden, layout: false }
9
+ format.json { render json: { error: 'Access denied' }, status: :forbidden }
10
+ format.any { head :forbidden }
11
+ end
8
12
  end
9
13
 
10
14
  rescue_from ActionController::RoutingError do |exception|
@@ -19,7 +23,7 @@ module Cccux
19
23
  # Automatically load and authorize resources for all actions
20
24
  # This works because CCCUX provides default roles (Guest, Basic User)
21
25
  # so every user has permissions to check against
22
- load_and_authorize_resource
26
+ # load_and_authorize_resource # Commented out to avoid conflicts with child controllers
23
27
 
24
28
  protected
25
29
 
@@ -42,20 +46,26 @@ module Cccux
42
46
  private
43
47
 
44
48
  def ensure_role_manager
45
- # Check if user is authenticated
46
49
  unless defined?(current_user) && current_user&.persisted?
47
50
  respond_to do |format|
48
- format.html { redirect_to main_app.root_path, alert: 'You must be logged in to access the admin interface.' }
49
- format.json { render json: { success: false, error: 'You must be logged in to access the admin interface.' }, status: :unauthorized }
51
+ if Rails.env.test?
52
+ format.html { render plain: "Access denied", status: :forbidden }
53
+ else
54
+ format.html { redirect_to main_app.root_path, alert: 'You must be logged in.' }
55
+ end
56
+ format.json { render json: { success: false, error: 'You must be logged in.' }, status: :unauthorized }
50
57
  end
51
58
  return
52
59
  end
53
-
54
- # Check if user has Role Manager role
55
- unless current_user.has_role?('Role Manager')
60
+
61
+ unless current_user.has_role?("Role Manager")
56
62
  respond_to do |format|
57
- format.html { redirect_to main_app.root_path, alert: 'Access denied. Only Role Managers can access the admin interface.' }
58
- format.json { render json: { success: false, error: 'Access denied. Only Role Managers can access the admin interface.' }, status: :forbidden }
63
+ if Rails.env.test?
64
+ format.html { render plain: "Access denied", status: :forbidden }
65
+ else
66
+ format.html { redirect_to main_app.root_path, alert: 'Access denied. You need the Role Manager role.' }
67
+ end
68
+ format.json { render json: { success: false, error: 'Access denied. You need the Role Manager role.' }, status: :forbidden }
59
69
  end
60
70
  return
61
71
  end
@@ -19,16 +19,6 @@ module Cccux
19
19
  @missing_models = @detected_models - @existing_models
20
20
  @actions = %w[read create update destroy]
21
21
 
22
- # Debug logging
23
- Rails.logger.info "DEBUG: Detected models: #{@detected_models.inspect}"
24
- Rails.logger.info "DEBUG: Existing models: #{@existing_models.inspect}"
25
- Rails.logger.info "DEBUG: Missing models: #{@missing_models.inspect}"
26
-
27
- # Additional debug for web context
28
- Rails.logger.info "DEBUG: Controller context - detected count: #{@detected_models.count}"
29
- Rails.logger.info "DEBUG: Controller context - missing count: #{@missing_models.count}"
30
- Rails.logger.info "DEBUG: Controller context - Order in detected? #{@detected_models.include?('Order')}"
31
- Rails.logger.info "DEBUG: Controller context - Order in existing? #{@existing_models.include?('Order')}"
32
22
  end
33
23
 
34
24
  def sync_permissions
@@ -57,12 +47,18 @@ module Cccux
57
47
 
58
48
  if added_permissions.any?
59
49
  redirect_to cccux.model_discovery_path,
60
- notice: "Successfully added #{added_permissions.count} permissions for #{models_to_add.count} models!"
50
+ notice: "Successfully added #{added_permissions.count} permissions for #{models_to_add.count} models! For each of these models, you'll probably want to add 'load_and_authorize_resource' to the controller."
61
51
  else
62
52
  redirect_to cccux.model_discovery_path,
63
53
  alert: "No new permissions were added. Models may already have permissions."
64
54
  end
65
55
  end
56
+
57
+ def clear_model_cache
58
+ clear_model_discovery_cache
59
+ redirect_to cccux.model_discovery_path,
60
+ notice: "Model discovery cache cleared! New models should now be visible."
61
+ end
66
62
 
67
63
  # Handle any unmatched routes in CCCUX - redirect to home
68
64
  def not_found
@@ -72,11 +68,63 @@ module Cccux
72
68
  private
73
69
 
74
70
  def detect_application_models
71
+ # Use cached results if available
72
+ return @detected_models_cache if @detected_models_cache
73
+
74
+ # Force Rails to reload constants for better model discovery
75
+ begin
76
+ Rails.application.eager_load! if Rails.application.config.eager_load
77
+ rescue => e
78
+ Rails.logger.warn "Could not eager load: #{e.message}"
79
+ end
80
+
81
+ # Also try to load models from app/models directory
82
+ begin
83
+ app_models_path = Rails.root.join('app', 'models')
84
+ if Dir.exist?(app_models_path)
85
+ Dir.glob(File.join(app_models_path, '**', '*.rb')).each do |model_file|
86
+ begin
87
+ load model_file unless $LOADED_FEATURES.include?(model_file)
88
+ rescue => e
89
+ Rails.logger.debug "Could not load model file #{model_file}: #{e.message}"
90
+ end
91
+ end
92
+ end
93
+ rescue => e
94
+ Rails.logger.warn "Could not load models from app/models: #{e.message}"
95
+ end
96
+
75
97
  models = []
76
98
 
77
99
  begin
78
- # Direct approach: Get models from database tables (bypasses all autoloading issues)
79
- Rails.logger.info "Detecting models from database tables..."
100
+ # Primary approach: Use Rails' built-in model discovery
101
+ Rails.logger.info "🔍 Using Rails model discovery approach..."
102
+
103
+ # Get all ApplicationRecord descendants (this includes all models)
104
+ if defined?(ApplicationRecord)
105
+ ApplicationRecord.descendants.each do |model_class|
106
+ next if model_class.abstract_class?
107
+ next if model_class.name.nil?
108
+ next if skip_model_by_name?(model_class.name)
109
+
110
+ models << model_class.name
111
+ Rails.logger.debug "Found model via ApplicationRecord: #{model_class.name}"
112
+ end
113
+ end
114
+
115
+ # Fallback: Get all ActiveRecord::Base descendants
116
+ ActiveRecord::Base.descendants.each do |model_class|
117
+ next if model_class.abstract_class?
118
+ next if model_class.name.nil?
119
+ next if skip_model_by_name?(model_class.name)
120
+ next if models.include?(model_class.name) # Avoid duplicates
121
+
122
+ models << model_class.name
123
+ Rails.logger.debug "Found model via ActiveRecord::Base: #{model_class.name}"
124
+ end
125
+
126
+ # Secondary approach: Database table discovery for any missing models
127
+ Rails.logger.info "🔍 Using database table discovery as backup..."
80
128
 
81
129
  application_tables = ActiveRecord::Base.connection.tables.reject do |table|
82
130
  # Skip Rails internal tables and CCCUX tables
@@ -84,36 +132,46 @@ module Cccux
84
132
  skip_table?(table)
85
133
  end
86
134
 
87
- Rails.logger.info "Found application tables: #{application_tables}"
135
+ # Discover modules and their table patterns
136
+ module_table_patterns = discover_module_table_patterns
88
137
 
89
138
  application_tables.each do |table|
90
139
  # Convert table name to model name
91
140
  model_name = table.singularize.camelize
92
141
 
93
- # Verify the model exists and is valid
94
- begin
95
- if Object.const_defined?(model_name)
96
- model_class = Object.const_get(model_name)
97
- if model_class.respond_to?(:table_name) &&
98
- model_class.table_name == table &&
99
- !skip_model_by_name?(model_name)
100
- models << model_name
101
- Rails.logger.info "✅ Found model: #{model_name} (table: #{table})"
102
- end
103
- else
104
- # Model constant doesn't exist yet, but table does - likely a valid model
105
- unless skip_model_by_name?(model_name)
142
+ # Check if this table belongs to a discovered module
143
+ module_name = find_module_for_table(table, module_table_patterns)
144
+ if module_name
145
+ # Extract the model name from the table (e.g., 'pages' from 'mega_bar_pages')
146
+ # Use the module's table prefix to extract the model name
147
+ prefix = module_table_patterns[module_name]
148
+ model_part = table.gsub(prefix, '').singularize.camelize
149
+ model_name = "#{module_name}::#{model_part}"
150
+ end
151
+
152
+ # Only add if not already found and not skipped
153
+ unless models.include?(model_name) || skip_model_by_name?(model_name)
154
+ # Try to verify the model exists
155
+ begin
156
+ if Object.const_defined?(model_name)
157
+ model_class = Object.const_get(model_name)
158
+ if model_class.respond_to?(:table_name) && model_class.table_name == table
159
+ models << model_name
160
+ Rails.logger.debug "Found model via table discovery: #{model_name}"
161
+ end
162
+ else
163
+ # Model constant doesn't exist but table does - likely a valid model
106
164
  models << model_name
107
- Rails.logger.info "Found model from table: #{model_name} (table: #{table})"
165
+ Rails.logger.debug "Found potential model via table: #{model_name}"
108
166
  end
167
+ rescue => e
168
+ Rails.logger.debug "Error verifying model #{model_name}: #{e.message}"
109
169
  end
110
- rescue => e
111
- Rails.logger.debug "Skipped table #{table}: #{e.message}"
112
170
  end
113
171
  end
114
172
 
115
173
  rescue => e
116
- Rails.logger.error "Error detecting models from database: #{e.message}"
174
+ Rails.logger.error "Error detecting models: #{e.message}"
117
175
  Rails.logger.error e.backtrace.join("\n")
118
176
  end
119
177
 
@@ -127,7 +185,107 @@ module Cccux
127
185
  Rails.logger.info " Application models: #{models.reject { |m| m.start_with?('Cccux::') }.join(', ')}"
128
186
  Rails.logger.info " CCCUX models: #{models.select { |m| m.start_with?('Cccux::') }.join(', ')}"
129
187
 
130
- models.uniq.sort
188
+ # Cache the results
189
+ @detected_models_cache = models.uniq.sort
190
+ @detected_models_cache
191
+ end
192
+
193
+ def discover_module_table_patterns
194
+ # Use cached results if available
195
+ return @module_table_patterns_cache if @module_table_patterns_cache
196
+
197
+ patterns = {}
198
+
199
+ # Skip Rails internal engines and third-party gems
200
+ skip_engines = %w[
201
+ Rails ActionView SolidCache Stimulus Turbo Importmap ActiveStorage
202
+ ActionCable ActionMailbox BestInPlace SolidCable SolidQueue ActionText
203
+ Devise Kaminari Mega132
204
+ ]
205
+
206
+ # Find Rails engines by looking for Engine classes
207
+ ObjectSpace.each_object(Class) do |klass|
208
+ next unless klass < Rails::Engine
209
+ next if klass == Rails::Engine # Skip the base class
210
+
211
+ # Extract module name from engine class (e.g., MegaBar::Engine -> MegaBar)
212
+ module_name = klass.name.split('::').first
213
+ next if skip_engines.include?(module_name) # Skip unwanted engines
214
+
215
+ # Convert module name to expected table prefix
216
+ table_prefix = module_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '') + '_'
217
+ patterns[module_name] = table_prefix
218
+ Rails.logger.info "Discovered engine: #{module_name} with table prefix: #{table_prefix}"
219
+ end
220
+
221
+ Rails.logger.info "Module table patterns: #{patterns}"
222
+
223
+ # Cache the results
224
+ @module_table_patterns_cache = patterns
225
+ @module_table_patterns_cache
226
+ end
227
+
228
+ def find_table_prefix_for_module(module_name)
229
+ begin
230
+ # Look for models in this module
231
+ module_const = Object.const_get(module_name)
232
+
233
+ # Find the first model in this module to determine the table prefix
234
+ module_const.constants.each do |const|
235
+ const_obj = module_const.const_get(const)
236
+ if const_obj.is_a?(Class) && const_obj < ActiveRecord::Base && const_obj != ActiveRecord::Base
237
+ table_name = const_obj.table_name
238
+ # Convert camelCase to snake_case for the prefix
239
+ expected_prefix = module_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '') + '_'
240
+ if table_name.start_with?(expected_prefix)
241
+ Rails.logger.debug "Found table prefix for #{module_name}: #{expected_prefix}"
242
+ return expected_prefix
243
+ end
244
+ end
245
+ end
246
+ rescue => e
247
+ Rails.logger.debug "Could not find table prefix for module #{module_name}: #{e.message}"
248
+ end
249
+
250
+ nil
251
+ end
252
+
253
+ def find_table_prefix_for_engine(engine_class)
254
+ begin
255
+ # Get the module name from the engine class
256
+ module_name = engine_class.name.split('::').first
257
+
258
+ # Look for models in this module
259
+ module_const = Object.const_get(module_name)
260
+
261
+ # Find the first model in this module to determine the table prefix
262
+ module_const.constants.each do |const|
263
+ const_obj = module_const.const_get(const)
264
+ if const_obj.is_a?(Class) && const_obj < ActiveRecord::Base && const_obj != ActiveRecord::Base
265
+ table_name = const_obj.table_name
266
+ # Convert camelCase to snake_case for the prefix
267
+ expected_prefix = module_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '') + '_'
268
+ if table_name.start_with?(expected_prefix)
269
+ Rails.logger.debug "Found table prefix for engine #{module_name}: #{expected_prefix}"
270
+ return expected_prefix
271
+ end
272
+ end
273
+ end
274
+ rescue => e
275
+ Rails.logger.debug "Could not find table prefix for engine #{engine_class.name}: #{e.message}"
276
+ end
277
+
278
+ nil
279
+ end
280
+
281
+ def find_module_for_table(table_name, module_patterns)
282
+ module_patterns.each do |module_name, prefix|
283
+ if table_name.start_with?(prefix)
284
+ return module_name
285
+ end
286
+ end
287
+
288
+ nil
131
289
  end
132
290
 
133
291
  def get_models_with_permissions
@@ -151,6 +309,9 @@ module Cccux
151
309
  /Migration/
152
310
  ]
153
311
 
312
+ # Don't skip any namespaced models (they should be discoverable)
313
+ return false if model_name.include?('::')
314
+
154
315
  excluded_patterns.any? { |pattern| model_name.match?(pattern) }
155
316
  end
156
317
 
@@ -165,8 +326,18 @@ module Cccux
165
326
  'versions' # PaperTrail
166
327
  ]
167
328
 
329
+ # Don't skip any module-prefixed tables (they should be discoverable)
330
+ # This will be handled by the module discovery logic
331
+ return false if table_name.include?('_') && !excluded_tables.include?(table_name)
332
+
168
333
  excluded_tables.include?(table_name) ||
169
334
  table_name.end_with?('_versions') # PaperTrail version tables
170
335
  end
336
+
337
+ def clear_model_discovery_cache
338
+ @detected_models_cache = nil
339
+ @module_table_patterns_cache = nil
340
+ Rails.logger.info "🔄 Model discovery cache cleared"
341
+ end
171
342
  end
172
343
  end
@@ -0,0 +1,70 @@
1
+ module Cccux
2
+ class RoleAbilitiesController < CccuxController
3
+ before_action :set_role, only: [:index, :create, :destroy]
4
+ before_action :set_role_ability, only: [:destroy]
5
+
6
+ def index
7
+ @role_abilities = @role.role_abilities.includes(:ability_permission)
8
+ @available_permissions = Cccux::AbilityPermission.all.group_by(&:subject)
9
+ end
10
+
11
+ def create
12
+ @role_ability = @role.role_abilities.build(role_ability_params)
13
+
14
+ if @role_ability.save
15
+ respond_to do |format|
16
+ format.html { redirect_to cccux.role_path(@role), notice: 'Permission was successfully assigned to role.' }
17
+ format.json { render json: @role_ability, status: :created }
18
+ end
19
+ else
20
+ respond_to do |format|
21
+ format.html { redirect_to cccux.role_path(@role), alert: "Failed to assign permission: #{@role_ability.errors.full_messages.join(', ')}" }
22
+ format.json { render json: { errors: @role_ability.errors }, status: :unprocessable_entity }
23
+ end
24
+ end
25
+ end
26
+
27
+ def destroy
28
+ if @role_ability.destroy
29
+ respond_to do |format|
30
+ format.html { redirect_to cccux.role_path(@role), notice: 'Permission was successfully removed from role.' }
31
+ format.json { head :no_content }
32
+ end
33
+ else
34
+ respond_to do |format|
35
+ format.html { redirect_to cccux.role_path(@role), alert: 'Failed to remove permission from role.' }
36
+ format.json { render json: { errors: @role_ability.errors }, status: :unprocessable_entity }
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def set_role
44
+ @role = Cccux::Role.find(params[:role_id])
45
+ end
46
+
47
+ def set_role_ability
48
+ @role_ability = @role.role_abilities.find(params[:id])
49
+ end
50
+
51
+ def role_ability_params
52
+ # Convert access_type to owned and context
53
+ params_copy = params.require(:cccux_role_ability).permit(:ability_permission_id, :access_type, :owned, :context, :ownership_source, :ownership_conditions)
54
+
55
+ if params_copy[:access_type].present?
56
+ case params_copy[:access_type]
57
+ when 'owned'
58
+ params_copy[:owned] = true
59
+ params_copy[:context] = 'owned'
60
+ when 'global'
61
+ params_copy[:owned] = false
62
+ params_copy[:context] = 'global'
63
+ end
64
+ params_copy.delete(:access_type)
65
+ end
66
+
67
+ params_copy
68
+ end
69
+ end
70
+ end