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 +4 -4
- data/CHANGELOG.md +23 -0
- data/app/assets/stylesheets/cccux/application.css +70 -1
- data/app/controllers/cccux/ability_permissions_controller.rb +135 -24
- data/app/controllers/cccux/cccux_controller.rb +2 -2
- data/app/controllers/cccux/dashboard_controller.rb +202 -16
- data/app/helpers/cccux/authorization_helper.rb +17 -11
- data/app/models/cccux/ability.rb +1 -0
- data/app/models/cccux/ability_permission.rb +9 -0
- data/app/models/concerns/cccux/user_concern.rb +3 -0
- data/app/views/cccux/ability_permissions/new.html.erb +2 -2
- data/app/views/cccux/dashboard/model_discovery.html.erb +7 -2
- data/app/views/shared/_footer.html.erb +1 -1
- data/config/routes.rb +1 -0
- data/lib/cccux/engine.rb +7 -6
- data/lib/cccux/version.rb +1 -1
- data/lib/cccux.rb +12 -0
- data/lib/tasks/cccux.rake +248 -157
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: afb1191e0b22997a111737dfd7b40e7f3cfb8803cc5046b2091d711ff915dada
|
|
4
|
+
data.tar.gz: 372d9570edf0194fa3759922a6c1d18e60cd3d8ce2bf4c78fbc4ecdd0be15d61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
/*
|
|
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
|
-
|
|
192
|
-
resource_name = subject.underscore.pluralize
|
|
204
|
+
Rails.logger.debug "Discovering actions for subject: #{subject}"
|
|
193
205
|
|
|
194
|
-
#
|
|
195
|
-
if subject.
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
#
|
|
238
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
link_if_can_with_wrapper(:destroy, subject, text, path, **opts)
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
# Button helpers for common actions
|