cccux 0.2.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c1e55d4d139414081a5c7f00ecf389054170abcf49297ec0c7d745afce027a9
4
- data.tar.gz: d03b064b695aab0b20a433247eef6e076afcd301de2d1493861ce452c656f54b
3
+ metadata.gz: afb1191e0b22997a111737dfd7b40e7f3cfb8803cc5046b2091d711ff915dada
4
+ data.tar.gz: 372d9570edf0194fa3759922a6c1d18e60cd3d8ce2bf4c78fbc4ecdd0be15d61
5
5
  SHA512:
6
- metadata.gz: a25c77cb991efcd84a02c215c63b9aca1514febfc055074718fb864c0d393ae6ecff3cd4d6a9149016a06107f336e1b0a84b6260e46f47b11752cf548fd8bcb9
7
- data.tar.gz: dd0157441f4d61a0a6d21d6744f0c5f060d52bae907ea70751991ccab1441e463ef25ea4472c79109ffefb45eecedb83e2bcea3bb19b6bcab67c1516bac9c768
6
+ metadata.gz: 0d235d116f1ad65bf811c70e486b29da1d40dc354e96252db8e0d6fd8a5cfa7e99d2c30e3093e5d161c0267e115c8031a6af827ec31cba6f2b64c8b4fa1ca855
7
+ data.tar.gz: ba3f46415d6a00afc76907be4523a290abfa43441d6f706e4d35d30ab96a8ca664e6df1a2149a48bfe1190d6b4faf5a47bf6c7c310fbe683b34b60060bdb26e1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.1] - 2025-01-27
9
+
10
+ ### Added
11
+ - **Multi-level Ownership Support**: Enhanced ownership system to support ownership chains (e.g., ProjectManager → Project → Task)
12
+ - **Improved Model Discovery**: Enhanced model discovery to include all application models in role editing interface
13
+ - **Enhanced Authorization Logic**: Improved Ability class to handle complex ownership relationships correctly
14
+ - **Project Management Integration**: Added support for Project, Task, and ProjectManager models with proper authorization
15
+ - **Comprehensive Test Coverage**: Added extensive test coverage for all major components and edge cases
16
+
17
+ ### Fixed
18
+ - **Model Loading Issues**: Fixed Zeitwerk autoloading issues with empty model files
19
+ - **Ownership Chain Logic**: Fixed ownership checking logic to properly traverse ownership relationships
20
+ - **User Registration**: Fixed ActiveModel::UnknownAttributeError during user signup process
21
+ - **Permission Inheritance**: Corrected permission inheritance for project managers and task ownership
22
+ - **Debug Output Removal**: Cleaned up all debug output statements from production code
23
+ - **Test Assertion Warnings**: Fixed auto-generated test methods and missing HTTP requests in tests
24
+
25
+ ### Technical Improvements
26
+ - **Enhanced Test Coverage**: Improved test coverage for complex ownership scenarios
27
+ - **Better Error Handling**: Enhanced error handling for missing models and invalid ownership configurations
28
+ - **Performance Optimizations**: Improved model loading and authorization checking performance
29
+ - **Code Quality**: Removed all debug statements and improved code organization
30
+
8
31
  ## [0.1.2] - 2025-01-27
9
32
 
10
33
  ### Added
@@ -4,7 +4,7 @@
4
4
  * This file contains styles for the CCCUX engine components.
5
5
  */
6
6
 
7
- /* Basic styling for CCCUX admin interface */
7
+ /* CCCUX Admin Styles */
8
8
  .cccux-admin {
9
9
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
10
10
  line-height: 1.6;
@@ -54,4 +54,73 @@
54
54
  .cccux-admin th {
55
55
  background: #f8f9fa;
56
56
  font-weight: 600;
57
+ }
58
+
59
+ /* CCCUX Footer Styles */
60
+ .cccux-footer {
61
+ font-size: 0.9rem;
62
+ color: #6c757d;
63
+ margin-top: 1rem;
64
+ padding: .5rem 0;
65
+ border-top: 1px solid #e5e5e5;
66
+ background-color: #f8f9fa;
67
+ }
68
+
69
+ .cccux-footer .footer-link {
70
+ color: #red;
71
+ text-decoration: none;
72
+ margin: 0 0.5rem;
73
+
74
+ }
75
+
76
+ .cccux-footer .footer-link:hover {
77
+ color: #0056b3;
78
+ text-decoration: underline;
79
+ }
80
+
81
+ .cccux-footer .footer-separator {
82
+ color: #dee2e6;
83
+ margin: 0 0.25rem;
84
+ }
85
+
86
+ .cccux-footer .user-info,
87
+ .cccux-footer .auth-links {
88
+ font-size: 0.85rem;
89
+ }
90
+
91
+ .cccux-footer .container {
92
+ max-width: 1200px;
93
+ margin: 0 auto;
94
+ padding: 0 1rem;
95
+ }
96
+
97
+ .cccux-footer .row {
98
+ display: flex;
99
+ justify-content: space-between;
100
+ align-items: center;
101
+ flex-wrap: wrap;
102
+ }
103
+
104
+ .cccux-footer .col-md-6 {
105
+ flex: 1;
106
+ min-width: 300px;
107
+ }
108
+
109
+ .cccux-footer .text-end {
110
+ text-align: right;
111
+ }
112
+
113
+ @media (max-width: 768px) {
114
+ .cccux-footer .row {
115
+ flex-direction: column;
116
+ gap: 0.5rem;
117
+ }
118
+
119
+ .cccux-footer .col-md-6 {
120
+ text-align: center;
121
+ }
122
+
123
+ .cccux-footer .text-end {
124
+ text-align: center;
125
+ }
57
126
  }
@@ -149,13 +149,26 @@ module Cccux
149
149
 
150
150
  # Add route-discovered actions
151
151
  route_actions = discover_actions_for_model(subject)
152
+ Rails.logger.debug "Route actions for #{subject}: #{route_actions}"
152
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
153
164
 
154
165
  # Add existing actions for this subject
155
166
  existing_actions = Cccux::AbilityPermission.where(subject: subject).distinct.pluck(:action).compact
156
167
  actions += existing_actions
157
168
 
158
- actions.uniq.sort
169
+ final_actions = actions.uniq.sort
170
+ Rails.logger.debug "Final actions for #{subject}: #{final_actions}"
171
+ final_actions
159
172
  end
160
173
 
161
174
  def discover_application_models
@@ -188,11 +201,73 @@ module Cccux
188
201
  actions = []
189
202
 
190
203
  begin
191
- # Convert subject to potential route patterns
192
- resource_name = subject.underscore.pluralize
204
+ Rails.logger.debug "Discovering actions for subject: #{subject}"
193
205
 
194
- # For CCCUX models, also check the engine routes
195
- 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::')
196
271
  cccux_resource_name = subject.gsub('Cccux::', '').underscore.pluralize
197
272
 
198
273
  # Look through CCCUX engine routes for this resource
@@ -216,26 +291,31 @@ module Cccux
216
291
  end
217
292
  end
218
293
  end
219
- end
220
-
221
- # Also look through main Rails routes for this resource
222
- Rails.application.routes.routes.each do |route|
223
- next unless route.path.spec.to_s.include?(resource_name)
224
294
 
225
- # Extract action from route
226
- if route.defaults[:action]
227
- action = route.defaults[:action]
228
- # Map HTTP verbs to standard actions and include custom actions
229
- case action
230
- when 'index' then actions << 'read'
231
- when 'show' then actions << 'read'
232
- when 'create' then actions << 'create'
233
- when 'update' then actions << 'update'
234
- when 'destroy' then actions << 'destroy'
235
- when 'edit', 'new' then next # Skip these as they're UI actions
236
- else
237
- # Custom actions
238
- 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
239
319
  end
240
320
  end
241
321
  end
@@ -247,6 +327,37 @@ module Cccux
247
327
  actions.uniq
248
328
  end
249
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
+
250
361
  def skip_model?(model_class)
251
362
  excluded_patterns = [
252
363
  /^ActiveRecord::/,
@@ -51,9 +51,9 @@ module Cccux
51
51
  if Rails.env.test?
52
52
  format.html { render plain: "Access denied", status: :forbidden }
53
53
  else
54
- format.html { redirect_to main_app.root_path, alert: 'You must be logged in to access the admin interface.' }
54
+ format.html { redirect_to main_app.root_path, alert: 'You must be logged in.' }
55
55
  end
56
- format.json { render json: { success: false, error: 'You must be logged in to access the admin interface.' }, status: :unauthorized }
56
+ format.json { render json: { success: false, error: 'You must be logged in.' }, status: :unauthorized }
57
57
  end
58
58
  return
59
59
  end
@@ -53,6 +53,12 @@ module Cccux
53
53
  alert: "No new permissions were added. Models may already have permissions."
54
54
  end
55
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
56
62
 
57
63
  # Handle any unmatched routes in CCCUX - redirect to home
58
64
  def not_found
@@ -62,10 +68,63 @@ module Cccux
62
68
  private
63
69
 
64
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
+
65
97
  models = []
66
98
 
67
99
  begin
68
- # Direct approach: Get models from database tables (bypasses all autoloading issues)
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..."
69
128
 
70
129
  application_tables = ActiveRecord::Base.connection.tables.reject do |table|
71
130
  # Skip Rails internal tables and CCCUX tables
@@ -73,32 +132,46 @@ module Cccux
73
132
  skip_table?(table)
74
133
  end
75
134
 
135
+ # Discover modules and their table patterns
136
+ module_table_patterns = discover_module_table_patterns
76
137
 
77
138
  application_tables.each do |table|
78
139
  # Convert table name to model name
79
140
  model_name = table.singularize.camelize
80
141
 
81
- # Verify the model exists and is valid
82
- begin
83
- if Object.const_defined?(model_name)
84
- model_class = Object.const_get(model_name)
85
- if model_class.respond_to?(:table_name) &&
86
- model_class.table_name == table &&
87
- !skip_model_by_name?(model_name)
88
- models << model_name
89
- end
90
- else
91
- # Model constant doesn't exist yet, but table does - likely a valid model
92
- 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
93
164
  models << model_name
165
+ Rails.logger.debug "Found potential model via table: #{model_name}"
94
166
  end
167
+ rescue => e
168
+ Rails.logger.debug "Error verifying model #{model_name}: #{e.message}"
95
169
  end
96
- rescue => e
97
170
  end
98
171
  end
99
172
 
100
173
  rescue => e
101
- Rails.logger.error "Error detecting models from database: #{e.message}"
174
+ Rails.logger.error "Error detecting models: #{e.message}"
102
175
  Rails.logger.error e.backtrace.join("\n")
103
176
  end
104
177
 
@@ -112,7 +185,107 @@ module Cccux
112
185
  Rails.logger.info " Application models: #{models.reject { |m| m.start_with?('Cccux::') }.join(', ')}"
113
186
  Rails.logger.info " CCCUX models: #{models.select { |m| m.start_with?('Cccux::') }.join(', ')}"
114
187
 
115
- 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
116
289
  end
117
290
 
118
291
  def get_models_with_permissions
@@ -136,6 +309,9 @@ module Cccux
136
309
  /Migration/
137
310
  ]
138
311
 
312
+ # Don't skip any namespaced models (they should be discoverable)
313
+ return false if model_name.include?('::')
314
+
139
315
  excluded_patterns.any? { |pattern| model_name.match?(pattern) }
140
316
  end
141
317
 
@@ -150,8 +326,18 @@ module Cccux
150
326
  'versions' # PaperTrail
151
327
  ]
152
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
+
153
333
  excluded_tables.include?(table_name) ||
154
334
  table_name.end_with?('_versions') # PaperTrail version tables
155
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
156
342
  end
157
343
  end
@@ -1,13 +1,11 @@
1
1
  module Cccux
2
2
  module AuthorizationHelper
3
3
  # Link helpers for common actions
4
- def link_if_can_index(subject, text, path, **opts)
5
- can?(:index, subject) ? link_to(text, path, **opts) : ""
6
- end
7
-
8
- def link_if_can_show(subject, text, path, **opts)
9
- if can?(:show, subject)
10
- link_to(text, path, **opts)
4
+ def link_if_can_with_wrapper(action, subject, text, path, **opts)
5
+ prepend = opts.delete(:prepend)
6
+ append = opts.delete(:append)
7
+ if can?(action, subject)
8
+ "#{prepend}#{link_to(text, path, **opts)}#{append}".html_safe
11
9
  elsif opts.delete(:show_text)
12
10
  text
13
11
  else
@@ -15,20 +13,28 @@ module Cccux
15
13
  end
16
14
  end
17
15
 
16
+ def link_if_can_index(subject, text, path, **opts)
17
+ link_if_can_with_wrapper(:index, subject, text, path, **opts)
18
+ end
19
+
20
+ def link_if_can_show(subject, text, path, **opts)
21
+ link_if_can_with_wrapper(:show, subject, text, path, **opts)
22
+ end
23
+
18
24
  def link_if_can_create(subject, text, path, **opts)
19
- can?(:create, subject) ? link_to(text, path, **opts) : ""
25
+ link_if_can_with_wrapper(:create, subject, text, path, **opts)
20
26
  end
21
27
 
22
28
  def link_if_can_edit(subject, text, path, **opts)
23
- can?(:edit, subject) ? link_to(text, path, **opts) : ""
29
+ link_if_can_with_wrapper(:edit, subject, text, path, **opts)
24
30
  end
25
31
 
26
32
  def link_if_can_update(subject, text, path, **opts)
27
- can?(:update, subject) ? link_to(text, path, **opts) : ""
33
+ link_if_can_with_wrapper(:update, subject, text, path, **opts)
28
34
  end
29
35
 
30
36
  def link_if_can_destroy(subject, text, path, **opts)
31
- can?(:destroy, subject) ? link_to(text, path, **opts) : ""
37
+ link_if_can_with_wrapper(:destroy, subject, text, path, **opts)
32
38
  end
33
39
 
34
40
  # Button helpers for common actions
@@ -172,6 +172,7 @@ module Cccux
172
172
  else
173
173
  candidates << subject
174
174
  candidates << "Cccux::#{subject}"
175
+
175
176
  end
176
177
 
177
178
  # Add more candidates for common patterns