plutonium 0.14.1 → 0.15.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (182) hide show
  1. checksums.yaml +4 -4
  2. data/README copy.md +1 -1
  3. data/README.md +1 -1
  4. data/app/assets/plutonium.css +1 -1
  5. data/app/views/{application → plutonium}/_resource_header.html copy.erb +1 -1
  6. data/app/views/{application → plutonium}/_resource_header.html.erb +1 -1
  7. data/app/views/{application → plutonium}/_resource_sidebar.html.erb +2 -0
  8. data/app/views/resource/_resource_details.html.erb +1 -36
  9. data/app/views/resource/_resource_form.html.erb +1 -5
  10. data/app/views/resource/_resource_table.html.erb +315 -85
  11. data/app/views/resource/edit.html.erb +1 -5
  12. data/app/views/resource/index.html.erb +1 -5
  13. data/app/views/resource/new.html.erb +1 -5
  14. data/app/views/resource/show.html.erb +1 -5
  15. data/config/initializers/pagy.rb +1 -0
  16. data/config/initializers/rabl.rb +27 -20
  17. data/gemfiles/rails_7.gemfile.lock +5 -1
  18. data/lib/generators/pu/core/install/templates/app/controllers/plutonium_controller.rb.tt +2 -0
  19. data/lib/generators/pu/core/install/templates/app/controllers/resource_controller.rb.tt +21 -1
  20. data/lib/generators/pu/core/install/templates/app/definitions/resource_definition.rb.tt +2 -0
  21. data/lib/generators/pu/core/install/templates/app/models/resource_record.rb.tt +0 -2
  22. data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +1 -8
  23. data/lib/generators/pu/eject/shell/shell_generator.rb +2 -2
  24. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +1 -1
  25. data/lib/generators/pu/lib/plutonium_generators/concerns/logger.rb +1 -1
  26. data/lib/generators/pu/lib/plutonium_generators/generator.rb +5 -3
  27. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +26 -2
  28. data/lib/generators/pu/pkg/{feature/feature_generator.rb → package/package_generator.rb} +4 -4
  29. data/lib/generators/pu/pkg/{feature → package}/templates/app/controllers/resource_controller.rb.tt +0 -2
  30. data/lib/generators/pu/pkg/package/templates/app/definitions/resource_definition.rb.tt +4 -0
  31. data/lib/generators/pu/pkg/package/templates/app/query_objects/resource_query_object.rb.tt +4 -0
  32. data/lib/generators/pu/pkg/{app/app_generator.rb → portal/portal_generator.rb} +10 -8
  33. data/lib/generators/pu/pkg/{app → portal}/templates/app/controllers/concerns/controller.rb.tt +3 -7
  34. data/lib/generators/pu/pkg/{app → portal}/templates/app/controllers/dashboard_controller.rb.tt +1 -1
  35. data/lib/generators/pu/pkg/portal/templates/app/controllers/plutonium_controller.rb.tt +5 -0
  36. data/lib/generators/pu/pkg/{app/templates/app/controllers/controller.rb.tt → portal/templates/app/controllers/resource_controller.rb.tt} +1 -1
  37. data/lib/generators/pu/pkg/portal/templates/app/definitions/resource_definition.rb.tt +4 -0
  38. data/lib/generators/pu/pkg/{app → portal}/templates/app/views/package/dashboard/index.html.erb +2 -1
  39. data/lib/generators/pu/res/conn/conn_generator.rb +78 -3
  40. data/lib/generators/pu/res/conn/templates/app/controllers/resource_controller.rb.tt +1 -1
  41. data/lib/generators/pu/res/conn/templates/app/definitions/resource_definition.rb.tt +3 -0
  42. data/lib/generators/pu/res/conn/templates/app/policies/resource_policy.rb.tt +29 -1
  43. data/lib/generators/pu/res/conn/templates/app/presenters/resource_presenter.rb.tt +1 -1
  44. data/lib/generators/pu/res/conn/templates/app/query_objects/resource_query_object.rb.tt +1 -1
  45. data/lib/generators/pu/res/model/model_generator.rb +0 -7
  46. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -1
  47. data/lib/generators/pu/res/scaffold/scaffold_generator.rb +22 -4
  48. data/lib/generators/pu/res/scaffold/templates/controller.rb.tt +0 -1
  49. data/lib/generators/pu/res/scaffold/templates/definition.rb.tt +4 -0
  50. data/lib/generators/pu/res/scaffold/templates/policy.rb.tt +2 -2
  51. data/lib/generators/pu/rodauth/templates/app/controllers/rodauth_controller.rb.tt +1 -1
  52. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +270 -0
  53. data/lib/plutonium/action/README.md +0 -0
  54. data/lib/plutonium/action/base.rb +103 -0
  55. data/lib/plutonium/action/interactive.rb +117 -0
  56. data/lib/plutonium/action/route_options.rb +65 -0
  57. data/lib/plutonium/action/simple.rb +8 -0
  58. data/lib/plutonium/auth.rb +1 -1
  59. data/lib/plutonium/configuration.rb +0 -8
  60. data/lib/plutonium/core/actions/collection.rb +1 -1
  61. data/lib/plutonium/core/associations/renderers/factory.rb +3 -1
  62. data/lib/plutonium/core/controller.rb +110 -0
  63. data/lib/plutonium/core/controllers/authorizable.rb +12 -35
  64. data/lib/plutonium/core/controllers/bootable.rb +38 -7
  65. data/lib/plutonium/core/controllers/entity_scoping.rb +6 -2
  66. data/lib/plutonium/core/fields/renderers/association_renderer.rb +1 -1
  67. data/lib/plutonium/core/ui/collection.rb +1 -1
  68. data/lib/plutonium/core/ui/detail.rb +1 -1
  69. data/lib/plutonium/core/ui/form.rb +1 -1
  70. data/lib/plutonium/definition/actions.rb +50 -0
  71. data/lib/plutonium/definition/base.rb +92 -0
  72. data/lib/plutonium/definition/config_attr.rb +30 -0
  73. data/lib/plutonium/definition/defineable_props.rb +96 -0
  74. data/lib/plutonium/definition/search.rb +21 -0
  75. data/lib/plutonium/engine/validator.rb +30 -0
  76. data/lib/plutonium/engine.rb +25 -0
  77. data/lib/plutonium/helpers/form_helper.rb +1 -3
  78. data/lib/plutonium/interaction/README.md +369 -0
  79. data/lib/plutonium/interaction/base.rb +75 -0
  80. data/lib/plutonium/interaction/concerns/presentable.rb +61 -0
  81. data/lib/plutonium/interaction/concerns/workflow_dsl.rb +82 -0
  82. data/lib/plutonium/interaction/outcome.rb +129 -0
  83. data/lib/plutonium/interaction/response/base.rb +63 -0
  84. data/lib/plutonium/interaction/response/null.rb +33 -0
  85. data/lib/plutonium/interaction/response/redirect.rb +30 -0
  86. data/lib/plutonium/interaction/response/render.rb +28 -0
  87. data/lib/plutonium/lib/bit_flags.rb +70 -9
  88. data/lib/plutonium/{config → lib}/overlayed_hash.rb +1 -1
  89. data/lib/plutonium/lib/smart_cache.rb +171 -0
  90. data/lib/plutonium/models/has_cents.rb +170 -0
  91. data/lib/plutonium/{pkg/base.rb → package/engine.rb} +10 -2
  92. data/lib/plutonium/{application → portal}/controller.rb +3 -11
  93. data/lib/plutonium/{application → portal}/dynamic_controllers.rb +4 -4
  94. data/lib/plutonium/portal/engine.rb +15 -0
  95. data/lib/plutonium/railtie.rb +33 -1
  96. data/lib/plutonium/reloader.rb +5 -5
  97. data/lib/plutonium/resource/controller.rb +51 -34
  98. data/lib/plutonium/resource/controllers/authorizable.rb +128 -0
  99. data/lib/plutonium/{core → resource}/controllers/crud_actions.rb +23 -22
  100. data/lib/plutonium/resource/controllers/defineable.rb +26 -0
  101. data/lib/plutonium/{core → resource}/controllers/interactive_actions.rb +12 -12
  102. data/lib/plutonium/resource/controllers/presentable.rb +41 -0
  103. data/lib/plutonium/resource/controllers/queryable.rb +44 -0
  104. data/lib/plutonium/resource/definition.rb +6 -0
  105. data/lib/plutonium/resource/policy.rb +25 -13
  106. data/lib/plutonium/resource/query_object.rb +50 -51
  107. data/lib/plutonium/resource/record.rb +6 -89
  108. data/lib/plutonium/resource/register.rb +82 -0
  109. data/lib/plutonium/routing/mapper_extensions.rb +1 -1
  110. data/lib/plutonium/routing/resource_registration.rb +1 -1
  111. data/lib/plutonium/routing/route_set_extensions.rb +6 -18
  112. data/lib/plutonium/ui/action_button.rb +125 -0
  113. data/lib/plutonium/ui/breadcrumbs.rb +163 -0
  114. data/lib/plutonium/ui/component/base.rb +13 -0
  115. data/lib/plutonium/ui/component/behaviour.rb +38 -0
  116. data/lib/plutonium/ui/component/kit.rb +31 -0
  117. data/lib/plutonium/ui/component/methods.rb +54 -0
  118. data/lib/plutonium/ui/display/base.rb +25 -0
  119. data/lib/plutonium/ui/display/component/association.rb +26 -0
  120. data/lib/plutonium/ui/display/resource.rb +77 -0
  121. data/lib/plutonium/ui/display/theme.rb +27 -0
  122. data/lib/plutonium/ui/dyna_frame/content.rb +20 -0
  123. data/lib/plutonium/ui/empty_card.rb +20 -0
  124. data/lib/plutonium/ui/form/base.rb +37 -0
  125. data/lib/plutonium/ui/form/resource.rb +75 -0
  126. data/lib/plutonium/ui/form/theme.rb +42 -0
  127. data/lib/plutonium/ui/page/base.rb +112 -0
  128. data/lib/plutonium/ui/page/edit.rb +23 -0
  129. data/lib/plutonium/ui/page/index.rb +27 -0
  130. data/lib/plutonium/ui/page/new.rb +23 -0
  131. data/lib/plutonium/ui/page/show.rb +27 -0
  132. data/lib/plutonium/ui/page_header.rb +49 -0
  133. data/lib/plutonium/ui/table/base.rb +13 -0
  134. data/lib/plutonium/ui/table/components/pagy_info.rb +70 -0
  135. data/lib/plutonium/ui/table/components/pagy_page_info.rb +70 -0
  136. data/lib/plutonium/ui/table/components/pagy_pagination.rb +105 -0
  137. data/lib/plutonium/ui/table/components/scopes_bar.rb +136 -0
  138. data/lib/plutonium/ui/table/components/search_bar.rb +158 -0
  139. data/lib/plutonium/ui/table/display_theme.rb +21 -0
  140. data/lib/plutonium/ui/table/resource.rb +98 -0
  141. data/lib/plutonium/ui/table/theme.rb +35 -0
  142. data/lib/plutonium/ui.rb +9 -0
  143. data/lib/plutonium/version.rb +5 -1
  144. data/lib/plutonium.rb +14 -1
  145. data/package-lock.json +19 -22
  146. data/package.json +4 -4
  147. data/src/css/plutonium.css +15 -0
  148. data/tailwind.options.js +11 -3
  149. metadata +218 -81
  150. data/lib/generators/pu/core/install/templates/app/presenters/resource_presenter.rb.tt +0 -2
  151. data/lib/generators/pu/core/install/templates/app/query_objects/resource_query_object.rb.tt +0 -2
  152. data/lib/generators/pu/pkg/feature/templates/app/query_objects/resource_query_object.rb.tt +0 -4
  153. data/lib/plutonium/concerns/resource_validatable.rb +0 -34
  154. data/lib/plutonium/config.rb +0 -9
  155. data/lib/plutonium/core/controllers/base.rb +0 -101
  156. data/lib/plutonium/core/controllers/presentable.rb +0 -65
  157. data/lib/plutonium/core/controllers/queryable.rb +0 -28
  158. data/lib/plutonium/pkg/app.rb +0 -35
  159. data/lib/plutonium/pkg/concerns/resource_validatable.rb +0 -36
  160. data/lib/plutonium/pkg/feature.rb +0 -18
  161. data/lib/plutonium/policy/initializer.rb +0 -22
  162. data/lib/plutonium/policy/scope.rb +0 -19
  163. data/lib/plutonium/pundit/context.rb +0 -18
  164. data/lib/plutonium/pundit/policy_finder.rb +0 -25
  165. data/lib/plutonium/resource/policy_context.rb +0 -5
  166. data/lib/plutonium/resource_register.rb +0 -83
  167. data/lib/plutonium/smart_cache.rb +0 -151
  168. /data/app/views/{application → plutonium}/_flash.html.erb +0 -0
  169. /data/app/views/{application → plutonium}/_flash_alerts.html.erb +0 -0
  170. /data/app/views/{application → plutonium}/_flash_toasts.html.erb +0 -0
  171. /data/lib/generators/pu/pkg/{app/templates/app/views/package → package/templates}/.keep +0 -0
  172. /data/lib/generators/pu/pkg/{feature → package}/templates/app/interactions/resource_interaction.rb.tt +0 -0
  173. /data/lib/generators/pu/pkg/{feature → package}/templates/app/models/resource_record.rb.tt +0 -0
  174. /data/lib/generators/pu/pkg/{feature → package}/templates/app/policies/resource_policy.rb.tt +0 -0
  175. /data/lib/generators/pu/pkg/{feature → package}/templates/app/presenters/resource_presenter.rb.tt +0 -0
  176. /data/lib/generators/pu/pkg/{feature → package}/templates/lib/engine.rb.tt +0 -0
  177. /data/lib/generators/pu/pkg/{app → portal}/templates/app/policies/resource_policy.rb.tt +0 -0
  178. /data/lib/generators/pu/pkg/{app → portal}/templates/app/presenters/resource_presenter.rb.tt +0 -0
  179. /data/lib/generators/pu/pkg/{app → portal}/templates/app/query_objects/resource_query_object.rb.tt +0 -0
  180. /data/lib/generators/pu/pkg/{feature/templates → portal/templates/app/views/package}/.keep +0 -0
  181. /data/lib/generators/pu/pkg/{app → portal}/templates/config/routes.rb.tt +0 -0
  182. /data/lib/generators/pu/pkg/{app → portal}/templates/lib/engine.rb.tt +0 -0
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Models
5
+ # HasCents module provides functionality to handle monetary values stored as cents
6
+ # and expose them as decimal values. It also ensures that validations applied to
7
+ # the cents attribute are inherited by the decimal attribute.
8
+ #
9
+ # @example Usage
10
+ # class Product < ApplicationRecord
11
+ # include Plutonium::Models::HasCents
12
+ #
13
+ # has_cents :price_cents
14
+ # has_cents :cost_cents, name: :wholesale_price, rate: 1000
15
+ # has_cents :quantity_cents, name: :quantity, rate: 1
16
+ # has_cents :total_cents, suffix: "value"
17
+ #
18
+ # validates :price_cents, numericality: { greater_than_or_equal_to: 0 }
19
+ # end
20
+ #
21
+ # @example Basic Usage
22
+ # product = Product.new(price: 10.99)
23
+ #
24
+ # product.price_cents #=> 1099
25
+ # product.price #=> 10.99
26
+ #
27
+ # product.wholesale_price = 5.5
28
+ # product.cost_cents #=> 5500
29
+ #
30
+ # product.quantity = 3
31
+ # product.quantity_cents #=> 3
32
+ #
33
+ # @example Truncation
34
+ # product.price = 10.991
35
+ # product.price_cents #=> 1099
36
+ #
37
+ # product.price = 10.995
38
+ # product.price_cents #=> 1099
39
+ #
40
+ # product.price = 10.999
41
+ # product.price_cents #=> 1099
42
+ #
43
+ # product.total_value = 100.50
44
+ # product.total_cents #=> 10050
45
+ #
46
+ # @example Validation Inheritance
47
+ # product = Product.new(price: -10.99)
48
+ # product.valid? #=> false
49
+ # product.errors[:price_cents] #=> ["must be greater than or equal to 0"]
50
+ # product.errors[:price] #=> ["is invalid"]
51
+ #
52
+ # @example Reflection
53
+ # Product.has_cents_attributes
54
+ # #=> {
55
+ # # price_cents: { name: :price, rate: 100 },
56
+ # # cost_cents: { name: :wholesale_price, rate: 1000 },
57
+ # # quantity_cents: { name: :quantity, rate: 1 },
58
+ # # total_cents: { name: :total_value, rate: 100 }
59
+ # # }
60
+ #
61
+ # Product.has_cents_attribute?(:price_cents) #=> true
62
+ # Product.has_cents_attribute?(:name) #=> false
63
+ # Product.has_cents_attributes[:cost_cents] #=> {name: :wholesale_price, rate: 1000}
64
+ #
65
+ # @note This module automatically handles validation propagation. If a validation error
66
+ # is applied to the cents attribute, the decimal attribute will be marked as invalid.
67
+ #
68
+ # @note The module uses BigDecimal for internal calculations to ensure precision
69
+ # in monetary operations.
70
+ #
71
+ # @see ClassMethods#has_cents for details on setting up attributes
72
+ module HasCents
73
+ extend ActiveSupport::Concern
74
+
75
+ included do
76
+ class_attribute :has_cents_attributes, instance_writer: false, default: {}
77
+ end
78
+
79
+ module ClassMethods
80
+ # # Inherit validations from cents attribute to decimal attribute
81
+ # def validate(*args, &block)
82
+ # options = args.extract_options!
83
+ # Array(options[:attributes]).each do |attribute|
84
+ # attribute = attribute.to_sym
85
+ # if has_cents_attribute?(attribute)
86
+ # decimal_attribute = has_cents_attributes[attribute][:name]
87
+ # options[:attributes] += [decimal_attribute]
88
+ # args = args.map do |validator|
89
+ # if validator.respond_to?(:attributes)
90
+ # validator.instance_variable_set(:@attributes, validator.attributes + [decimal_attribute])
91
+ # _validators[decimal_attribute] << validator
92
+ # validator
93
+ # end
94
+ # end
95
+ # end
96
+ # end
97
+
98
+ # super(*args, options, &block)
99
+ # end
100
+ # Defines getter and setter methods for a monetary value stored as cents,
101
+ # and ensures validations are applied to both cents and decimal attributes.
102
+ #
103
+ # @param cents_name [Symbol] The name of the attribute storing the cents value.
104
+ # @param name [Symbol, nil] The name for the generated methods. If nil, it's derived from cents_name.
105
+ # @param rate [Integer] The conversion rate from the decimal value to cents (default: 100).
106
+ # This represents how many cents are in one unit of the decimal value.
107
+ # For example:
108
+ # - rate: 100 for dollars/cents (1 dollar = 100 cents)
109
+ # - rate: 1000 for dollars/mils (1 dollar = 1000 mils)
110
+ # - rate: 1 for a whole number representation
111
+ # @param suffix [String] The suffix to append to the cents_name if name is not provided (default: "amount").
112
+ #
113
+ # @example Standard currency (dollars and cents)
114
+ # has_cents :price_cents
115
+ #
116
+ # @example Custom rate for a different currency division
117
+ # has_cents :amount_cents, name: :cost, rate: 1000
118
+ #
119
+ # @example Whole number storage without decimal places
120
+ # has_cents :quantity_cents, name: :quantity, rate: 1
121
+ #
122
+ # @example Using custom suffix
123
+ # has_cents :total_cents, suffix: "value"
124
+ def has_cents(cents_name, name: nil, rate: 100, suffix: "amount")
125
+ cents_name = cents_name.to_sym
126
+ name ||= cents_name.to_s.gsub(/_cents$/, "")
127
+ name = name.to_sym
128
+ name = (name == cents_name) ? :"#{cents_name}_#{suffix}" : name
129
+
130
+ self.has_cents_attributes = has_cents_attributes.merge(
131
+ cents_name => {name: name, rate: rate}
132
+ )
133
+
134
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
135
+ # Getter method for the decimal representation of the cents value.
136
+ #
137
+ # @return [BigDecimal, nil] The decimal value or nil if cents_name is not present.
138
+ def #{name}
139
+ #{cents_name}.to_d / #{rate} if #{cents_name}.present?
140
+ end
141
+
142
+ # Setter method for the decimal representation of the cents value.
143
+ #
144
+ # @param value [Numeric, nil] The decimal value to be set.
145
+ def #{name}=(value)
146
+ self.#{cents_name} = if value.present?
147
+ (BigDecimal(value.to_s) * #{rate}).to_i
148
+ end
149
+ end
150
+
151
+ # Mark decimal field as invalid if cents field is not valid
152
+ after_validation do
153
+ next unless errors[#{cents_name.inspect}].present?
154
+
155
+ errors.add(#{name.inspect}, :invalid)
156
+ end
157
+ RUBY
158
+ end
159
+
160
+ # Checks if a given attribute is defined with has_cents
161
+ #
162
+ # @param attribute [Symbol] The attribute to check
163
+ # @return [Boolean] true if the attribute is defined with has_cents, false otherwise
164
+ def has_cents_attribute?(attribute)
165
+ has_cents_attributes.key?(attribute.to_sym)
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -1,6 +1,6 @@
1
1
  module Plutonium
2
- module Pkg
3
- module Base
2
+ module Package
3
+ module Engine
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
@@ -16,6 +16,14 @@ module Plutonium
16
16
  end
17
17
  add_view_paths_initializer.instance_variable_set(:@block, ->(app) {})
18
18
  end
19
+
20
+ initializer :append_migrations do |app|
21
+ unless app.root.to_s.match root.to_s
22
+ config.paths["db/migrate"].expanded.each do |expanded_path|
23
+ app.config.paths["db/migrate"] << expanded_path
24
+ end
25
+ end
26
+ end
19
27
  end
20
28
  end
21
29
  end
@@ -1,14 +1,10 @@
1
1
  module Plutonium
2
- module Application
2
+ module Portal
3
3
  module Controller
4
4
  extend ActiveSupport::Concern
5
- include Plutonium::Core::Controllers::Base
5
+ include Plutonium::Core::Controller
6
6
 
7
- included do
8
- helper_method :registered_resources
9
- end
10
-
11
- private
7
+ # private
12
8
 
13
9
  # # Menu Builder
14
10
  # def build_namespace_node(namespaces, resource, parent)
@@ -34,10 +30,6 @@ module Plutonium
34
30
  # def build_sidebar_menu
35
31
  # build_namespace_tree(current_engine.resource_register)
36
32
  # end
37
-
38
- def registered_resources
39
- current_engine.resource_register.resources
40
- end
41
33
  end
42
34
  end
43
35
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plutonium
4
- module Application
4
+ module Portal
5
5
  # DynamicControllers module provides functionality for dynamically creating controller classes
6
6
  # when they are missing in the current module's namespace.
7
7
  #
8
8
  # @example Usage
9
9
  # module MyApp
10
- # include Plutonium::Application::DynamicControllers
10
+ # include Plutonium::Portal::DynamicControllers
11
11
  # end
12
12
  #
13
13
  # # Now, MyApp::SomeController will be dynamically created if it doesn't exist,
@@ -74,7 +74,7 @@ module Plutonium
74
74
  log_controller_creation(const_full_name, parent_controller)
75
75
  const_full_name.constantize
76
76
  rescue => e
77
- Plutonium.logger.error "[plutonium] Failed to create dynamic controller: #{e.message}"
77
+ Plutonium.logger.error { "[plutonium] Failed to create dynamic controller: #{e.message}" }
78
78
  raise
79
79
  end
80
80
 
@@ -100,7 +100,7 @@ module Plutonium
100
100
  # @param const_full_name [String] The full name of the created controller
101
101
  # @param parent_controller [Class] The parent controller class
102
102
  def log_controller_creation(const_full_name, parent_controller)
103
- Plutonium.logger.info "[plutonium] Dynamically created #{const_full_name} < #{parent_controller}"
103
+ Plutonium.logger.info { "[plutonium] Dynamically created #{const_full_name} < #{parent_controller}" }
104
104
  end
105
105
  end
106
106
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Portal
5
+ module Engine
6
+ extend ActiveSupport::Concern
7
+ include Plutonium::Engine
8
+ include Plutonium::Package::Engine
9
+
10
+ included do
11
+ isolate_namespace to_s.deconstantize.constantize
12
+ end
13
+ end
14
+ end
15
+ end
@@ -21,6 +21,14 @@ module Plutonium
21
21
  plutonium.css plutonium.png plutonium.ico
22
22
  ].freeze
23
23
 
24
+ initializer "plutonium.base" do
25
+ Rails.application.class.include Plutonium::Engine
26
+ end
27
+
28
+ initializer "plutonium.deprecator" do |app|
29
+ app.deprecators[:plutonium] = Plutonium.deprecator
30
+ end
31
+
24
32
  initializer "plutonium.assets" do
25
33
  setup_asset_pipeline if Rails.application.config.respond_to?(:assets)
26
34
  end
@@ -45,13 +53,22 @@ module Plutonium
45
53
  extend_action_dispatch
46
54
  end
47
55
 
56
+ initializer "plutonium.active_record_extensions" do
57
+ extend_active_record
58
+ end
59
+
60
+ initializer "plutonium.phlexi_themes" do
61
+ setup_phlexi_themes
62
+ end
63
+
48
64
  rake_tasks do
49
65
  load "tasks/create_rodauth_admin.rake"
50
66
  end
51
67
 
52
68
  config.after_initialize do
53
69
  Plutonium::Reloader.start! if Plutonium.configuration.enable_hotreload
54
- Plutonium::ZEITWERK_LOADER.eager_load if Rails.env.production?
70
+ Plutonium::Loader.eager_load if Rails.env.production?
71
+ ActionPolicy::PerThreadCache.enabled = !Rails.env.local?
55
72
  end
56
73
 
57
74
  private
@@ -83,10 +100,25 @@ module Plutonium
83
100
  )
84
101
  end
85
102
 
103
+ def setup_phlexi_themes
104
+ Rails.application.config.to_prepare do
105
+ Phlexi::Form::Theme.instance = Plutonium::UI::Form::Theme.instance
106
+ Phlexi::Display::Theme.instance = Plutonium::UI::Display::Theme.instance
107
+ Phlexi::Table::Theme.instance = Plutonium::UI::Table::Theme.instance
108
+ Phlexi::Table::DisplayTheme.instance = Plutonium::UI::Table::DisplayTheme.instance
109
+ end
110
+ end
111
+
86
112
  def extend_action_dispatch
87
113
  ActionDispatch::Routing::Mapper.prepend Plutonium::Routing::MapperExtensions
88
114
  ActionDispatch::Routing::RouteSet.prepend Plutonium::Routing::RouteSetExtensions
89
115
  Rails::Engine.include Plutonium::Routing::ResourceRegistration
90
116
  end
117
+
118
+ def extend_active_record
119
+ ActiveSupport.on_load(:active_record) do
120
+ include Plutonium::Resource::Record
121
+ end
122
+ end
91
123
  end
92
124
  end
@@ -65,7 +65,7 @@ module Plutonium
65
65
  # @return [void]
66
66
  def handle_file_changes(modified, added, removed)
67
67
  (modified + added).each do |file|
68
- Plutonium.logger.debug "[plutonium] change detected: #{file}"
68
+ Plutonium.logger.debug { "[plutonium] change detected: #{file}" }
69
69
 
70
70
  if file == __FILE__
71
71
  reload_file(file)
@@ -107,7 +107,7 @@ module Plutonium
107
107
  # @param file [String] path to the engine file
108
108
  # @return [void]
109
109
  def reload_engine_and_routes(file)
110
- Plutonium.logger.debug "[plutonium] reloading: engine+routes"
110
+ Plutonium.logger.debug { "[plutonium] reloading: engine+routes" }
111
111
  load file
112
112
  Rails.application.reload_routes!
113
113
  end
@@ -117,9 +117,9 @@ module Plutonium
117
117
  # @param file [String] path to the file
118
118
  # @return [void]
119
119
  def reload_framework_and_file(file)
120
- Plutonium.logger.debug "[plutonium] reloading: app+framework"
120
+ Plutonium.logger.debug { "[plutonium] reloading: app+framework" }
121
121
  Rails.application.reloader.reload!
122
- Plutonium::ZEITWERK_LOADER.reload
122
+ Plutonium::Loader.reload
123
123
  reload_components
124
124
  end
125
125
 
@@ -145,7 +145,7 @@ module Plutonium
145
145
  # @param error [StandardError] the error that occurred during reloading
146
146
  # @return [void]
147
147
  def log_reload_failure(file, error)
148
- Plutonium.logger.error "\n[plutonium] reloading failed\n\n#{error.message}\n"
148
+ Plutonium.logger.error { "\n[plutonium] reloading failed\n\n#{error.message}\n" }
149
149
  end
150
150
  end
151
151
  end
@@ -10,16 +10,15 @@ module Plutonium
10
10
  module Controller
11
11
  extend ActiveSupport::Concern
12
12
  include Pagy::Backend
13
- include Plutonium::Core::Controllers::Base
14
- include Plutonium::Core::Controllers::Authorizable
15
- include Plutonium::Core::Controllers::Presentable
16
- include Plutonium::Core::Controllers::Queryable
17
- include Plutonium::Core::Controllers::CrudActions
18
- include Plutonium::Core::Controllers::InteractiveActions
13
+ include Plutonium::Core::Controller
14
+ include Plutonium::Resource::Controllers::Defineable
15
+ include Plutonium::Resource::Controllers::Authorizable
16
+ include Plutonium::Resource::Controllers::Presentable
17
+ include Plutonium::Resource::Controllers::Queryable
18
+ include Plutonium::Resource::Controllers::CrudActions
19
+ include Plutonium::Resource::Controllers::InteractiveActions
19
20
 
20
21
  included do
21
- class_attribute :resource_class, instance_writer: false, instance_predicate: false
22
-
23
22
  # https://github.com/ddnexus/pagy/blob/master/docs/extras/headers.md#headers
24
23
  after_action { pagy_headers_merge(@pagy) if @pagy }
25
24
 
@@ -27,49 +26,55 @@ module Plutonium
27
26
  end
28
27
 
29
28
  class_methods do
29
+ include Plutonium::Lib::SmartCache
30
+
30
31
  # Sets the resource class for the controller
31
- # @param [Class] resource_class The resource class
32
+ # @param [ActiveRecord::Base] resource_class The resource class
32
33
  def controller_for(resource_class)
33
- self.resource_class = resource_class
34
+ @resource_class = resource_class
34
35
  end
36
+
37
+ # Gets the resource class for the controller
38
+ # @return [ActiveRecord::Base] The resource class
39
+ def resource_class
40
+ return @resource_class if @resource_class.present?
41
+
42
+ name.to_s.gsub(/^#{current_package}::/, "").gsub(/Controller$/, "").classify.constantize
43
+ rescue NameError
44
+ raise NameError, "Failed to determine the resource class. Please call `controller_for(MyResource)` in #{name}."
45
+ end
46
+ memoize_unless_reloading :resource_class
35
47
  end
36
48
 
37
49
  private
38
50
 
39
- # Creates a policy context
40
- # @return [Plutonium::Resource::PolicyContext] The policy context
41
- def policy_context
42
- Plutonium::Resource::PolicyContext.new(
43
- user: current_user,
44
- resource_context: resource_context
45
- )
51
+ def resource_class
52
+ self.class.resource_class
46
53
  end
47
54
 
48
55
  # Returns the resource record based on path parameters
49
56
  # @return [ActiveRecord::Base, nil] The resource record
50
57
  def resource_record
51
- @resource_record ||= policy_scope(resource_class).from_path_param(params[:id]).first! if params[:id].present?
58
+ @resource_record ||= current_authorized_scope.from_path_param(params[:id]).first! if params[:id].present?
52
59
  @resource_record
53
60
  end
54
61
 
55
62
  # Returns the submitted resource parameters
56
63
  # @return [Hash] The submitted resource parameters
57
64
  def submitted_resource_params
58
- @submitted_resource_params ||= begin
59
- strong_parameters = resource_class.strong_parameters_for(*permitted_attributes)
60
- params.require(resource_param_key).permit(*strong_parameters).nilify.to_h
61
- end
65
+ @submitted_resource_params ||= build_form(resource_class.new).extract_input(params)[resource_param_key.to_sym]
62
66
  end
63
67
 
64
68
  # Returns the resource parameters, including scoped and parent parameters
65
69
  # @return [Hash] The resource parameters
66
70
  def resource_params
67
- input_params = submitted_resource_params.dup
71
+ @resource_params ||= begin
72
+ input_params = submitted_resource_params.dup
73
+ override_entity_scoping_params(input_params)
74
+ override_parent_params(input_params)
68
75
 
69
- override_entity_scoping_params(input_params)
70
- override_parent_params(input_params)
71
-
72
- current_presenter.defined_field_inputs_for(*permitted_attributes).collect_all(input_params)
76
+ input_params
77
+ end
73
78
  end
74
79
 
75
80
  # Returns the resource parameter key
@@ -93,10 +98,20 @@ module Plutonium
93
98
  # @param [ActiveRecord::Base] resource_record The resource record
94
99
  # @return [Object] The resource presenter
95
100
  def resource_presenter(resource_class, resource_record)
96
- presenter_class = "#{current_package}::#{resource_class}Presenter".constantize
101
+ presenter_class = [current_package, "#{resource_class}Presenter"].compact.join("::").constantize
97
102
  presenter_class.new resource_context, resource_record
98
103
  rescue NameError
99
- super(resource_class, resource_record)
104
+ super
105
+ end
106
+
107
+ # Creates a resource definition
108
+ # @param [Class] resource_class The resource class
109
+ # @return [Object] The resource definition
110
+ def resource_definition(resource_class)
111
+ definition_class = [current_package, "#{resource_class}Definition"].compact.join("::").constantize
112
+ definition_class.new
113
+ rescue NameError
114
+ super
100
115
  end
101
116
 
102
117
  # Creates a resource query object
@@ -104,10 +119,10 @@ module Plutonium
104
119
  # @param [ActionController::Parameters] params The request parameters
105
120
  # @return [Object] The resource query object
106
121
  def resource_query_object(resource_class, params)
107
- query_object_class = "#{current_package}::#{resource_class}QueryObject".constantize
122
+ query_object_class = [current_package, "#{resource_class}QueryObject"].compact.join("::").constantize
108
123
  query_object_class.new resource_context, params
109
124
  rescue NameError
110
- super(resource_class, params)
125
+ super
111
126
  end
112
127
 
113
128
  # Applies submitted resource params if they have been passed
@@ -126,7 +141,9 @@ module Plutonium
126
141
  parent_class = current_engine.resource_register.route_key_lookup[parent_route_key]
127
142
  parent_scope = parent_class.from_path_param(params[parent_route_param])
128
143
  parent_scope = parent_scope.associated_with(current_scoped_entity) if scoped_to_entity?
129
- parent_scope.first!
144
+ current_parent = parent_scope.first!
145
+ authorize! current_parent, to: :read?
146
+ current_parent
130
147
  end
131
148
  end
132
149
 
@@ -173,9 +190,9 @@ module Plutonium
173
190
  # @param [Array] args The URL arguments
174
191
  # @param [Hash] kwargs The keyword arguments
175
192
  # @return [Array] The URL arguments
176
- def resource_url_args_for(*args, **kwargs)
193
+ def resource_url_args_for(*, **kwargs)
177
194
  kwargs[:parent] = current_parent unless kwargs.key?(:parent)
178
- super(*args, **kwargs)
195
+ super
179
196
  end
180
197
  end
181
198
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Resource
5
+ module Controllers
6
+ # The Authorizable module provides authorization functionality for controllers,
7
+ # specifically for the current resource being handled by the controller.
8
+ # It integrates with ActionPolicy to enforce authorization checks and scoping.
9
+ #
10
+ # @example Including the module in a controller
11
+ # class MyController < ApplicationController
12
+ # include Plutonium::Resource::Controllers::Authorizable
13
+ # end
14
+ #
15
+ # @note This module assumes the existence of methods like `resource_record`,
16
+ # `resource_class`, `current_parent`, and `entity_scope_for_authorize`.
17
+ #
18
+ # @see ActionPolicy
19
+ module Authorizable
20
+ extend ActiveSupport::Concern
21
+
22
+ # Custom exception for missing authorize_current call
23
+ class ActionMissingAuthorizeCurrent < ActionPolicy::UnauthorizedAction; end
24
+
25
+ # Custom exception for missing current_authorized_scope call
26
+ class ActionMissingCurrentAuthorizedScope < ActionPolicy::UnauthorizedAction; end
27
+
28
+ included do
29
+ verify_authorized
30
+ after_action :verify_authorize_current
31
+ after_action :verify_current_authorized_scope, except: %i[new create]
32
+
33
+ helper_method :current_policy, :permitted_attributes
34
+
35
+ attr_writer :authorize_current_count
36
+ attr_writer :current_authorized_scope_count
37
+
38
+ protected :authorize_current_count=, :authorize_current_count
39
+ protected :current_authorized_scope_count=, :current_authorized_scope_count
40
+ end
41
+
42
+ private
43
+
44
+ # Verifies that authorize_current has been called
45
+ #
46
+ # @raise [ActionMissingAuthorizeCurrent] if authorize_current hasn't been called
47
+ def verify_authorize_current
48
+ return if verify_authorized_skipped
49
+
50
+ raise ActionMissingAuthorizeCurrent.new(controller_path, action_name) if authorize_current_count.zero?
51
+ end
52
+
53
+ # Verifies that current_authorized_scope has been called
54
+ #
55
+ # @raise [ActionMissingCurrentAuthorizedScope] if current_authorized_scope hasn't been called
56
+ def verify_current_authorized_scope
57
+ return if verify_authorized_skipped
58
+
59
+ raise ActionMissingCurrentAuthorizedScope.new(controller_path, action_name) if current_authorized_scope_count.zero?
60
+ end
61
+
62
+ # @return [Integer] the number of times authorize_current has been called
63
+ def authorize_current_count
64
+ @authorize_current_count ||= 0
65
+ end
66
+
67
+ # @return [Integer] the number of times current_authorized_scope has been called
68
+ def current_authorized_scope_count
69
+ @current_authorized_scope_count ||= 0
70
+ end
71
+
72
+ # Returns the policy for the current resource
73
+ #
74
+ # @return [ActionPolicy::Base] the policy for the current resource
75
+ def current_policy
76
+ @current_policy ||= policy_for(record: current_policy_subject, context: current_policy_context)
77
+ end
78
+
79
+ # Returns the authorized scope for the current resource
80
+ #
81
+ # @return [ActiveRecord::Relation] the authorized scope for the current resource
82
+ def current_authorized_scope
83
+ self.current_authorized_scope_count += 1
84
+ authorized_scope(resource_class.all, context: current_policy_context)
85
+ end
86
+
87
+ # Sets the policy context scope value to the current parent if available
88
+ #
89
+ # @return [Hash] default context for the current resource's policy
90
+ def current_policy_context
91
+ {scope: current_parent || entity_scope_for_authorize}
92
+ end
93
+
94
+ # Authorizes the current action for the given record of the current resource
95
+ #
96
+ # @param record [Object] the record to authorize
97
+ # @param options [Hash] additional options for authorization
98
+ # @raise [ActionPolicy::Unauthorized] if the action is not authorized
99
+ def authorize_current!(record, **options)
100
+ options[:context] = (options[:context] || {}).deep_merge(current_policy_context)
101
+ authorize!(record, **options)
102
+ self.authorize_current_count += 1
103
+ end
104
+
105
+ # Returns the list of permitted attributes for the current action on the current resource
106
+ #
107
+ # @return [Array<Symbol>] the list of permitted attributes for the current action
108
+ def permitted_attributes
109
+ @permitted_attributes ||= current_policy.send_with_report(:"permitted_attributes_for_#{action_name}")
110
+ end
111
+
112
+ # Returns the list of permitted associations for the current resource
113
+ #
114
+ # @return [Array<Symbol>] the list of permitted associations
115
+ def permitted_associations
116
+ @permitted_associations ||= current_policy.send_with_report(:permitted_associations)
117
+ end
118
+
119
+ # Returns the subject for the current resource's policy
120
+ #
121
+ # @return [Object] the subject for the policy (either resource_record or resource_class)
122
+ def current_policy_subject
123
+ resource_record || resource_class
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end