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.
- checksums.yaml +4 -4
- data/README copy.md +1 -1
- data/README.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/app/views/{application → plutonium}/_resource_header.html copy.erb +1 -1
- data/app/views/{application → plutonium}/_resource_header.html.erb +1 -1
- data/app/views/{application → plutonium}/_resource_sidebar.html.erb +2 -0
- data/app/views/resource/_resource_details.html.erb +1 -36
- data/app/views/resource/_resource_form.html.erb +1 -5
- data/app/views/resource/_resource_table.html.erb +315 -85
- data/app/views/resource/edit.html.erb +1 -5
- data/app/views/resource/index.html.erb +1 -5
- data/app/views/resource/new.html.erb +1 -5
- data/app/views/resource/show.html.erb +1 -5
- data/config/initializers/pagy.rb +1 -0
- data/config/initializers/rabl.rb +27 -20
- data/gemfiles/rails_7.gemfile.lock +5 -1
- data/lib/generators/pu/core/install/templates/app/controllers/plutonium_controller.rb.tt +2 -0
- data/lib/generators/pu/core/install/templates/app/controllers/resource_controller.rb.tt +21 -1
- data/lib/generators/pu/core/install/templates/app/definitions/resource_definition.rb.tt +2 -0
- data/lib/generators/pu/core/install/templates/app/models/resource_record.rb.tt +0 -2
- data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +1 -8
- data/lib/generators/pu/eject/shell/shell_generator.rb +2 -2
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +1 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/logger.rb +1 -1
- data/lib/generators/pu/lib/plutonium_generators/generator.rb +5 -3
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +26 -2
- data/lib/generators/pu/pkg/{feature/feature_generator.rb → package/package_generator.rb} +4 -4
- data/lib/generators/pu/pkg/{feature → package}/templates/app/controllers/resource_controller.rb.tt +0 -2
- data/lib/generators/pu/pkg/package/templates/app/definitions/resource_definition.rb.tt +4 -0
- data/lib/generators/pu/pkg/package/templates/app/query_objects/resource_query_object.rb.tt +4 -0
- data/lib/generators/pu/pkg/{app/app_generator.rb → portal/portal_generator.rb} +10 -8
- data/lib/generators/pu/pkg/{app → portal}/templates/app/controllers/concerns/controller.rb.tt +3 -7
- data/lib/generators/pu/pkg/{app → portal}/templates/app/controllers/dashboard_controller.rb.tt +1 -1
- data/lib/generators/pu/pkg/portal/templates/app/controllers/plutonium_controller.rb.tt +5 -0
- data/lib/generators/pu/pkg/{app/templates/app/controllers/controller.rb.tt → portal/templates/app/controllers/resource_controller.rb.tt} +1 -1
- data/lib/generators/pu/pkg/portal/templates/app/definitions/resource_definition.rb.tt +4 -0
- data/lib/generators/pu/pkg/{app → portal}/templates/app/views/package/dashboard/index.html.erb +2 -1
- data/lib/generators/pu/res/conn/conn_generator.rb +78 -3
- data/lib/generators/pu/res/conn/templates/app/controllers/resource_controller.rb.tt +1 -1
- data/lib/generators/pu/res/conn/templates/app/definitions/resource_definition.rb.tt +3 -0
- data/lib/generators/pu/res/conn/templates/app/policies/resource_policy.rb.tt +29 -1
- data/lib/generators/pu/res/conn/templates/app/presenters/resource_presenter.rb.tt +1 -1
- data/lib/generators/pu/res/conn/templates/app/query_objects/resource_query_object.rb.tt +1 -1
- data/lib/generators/pu/res/model/model_generator.rb +0 -7
- data/lib/generators/pu/res/model/templates/model.rb.tt +4 -1
- data/lib/generators/pu/res/scaffold/scaffold_generator.rb +22 -4
- data/lib/generators/pu/res/scaffold/templates/controller.rb.tt +0 -1
- data/lib/generators/pu/res/scaffold/templates/definition.rb.tt +4 -0
- data/lib/generators/pu/res/scaffold/templates/policy.rb.tt +2 -2
- data/lib/generators/pu/rodauth/templates/app/controllers/rodauth_controller.rb.tt +1 -1
- data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +270 -0
- data/lib/plutonium/action/README.md +0 -0
- data/lib/plutonium/action/base.rb +103 -0
- data/lib/plutonium/action/interactive.rb +117 -0
- data/lib/plutonium/action/route_options.rb +65 -0
- data/lib/plutonium/action/simple.rb +8 -0
- data/lib/plutonium/auth.rb +1 -1
- data/lib/plutonium/configuration.rb +0 -8
- data/lib/plutonium/core/actions/collection.rb +1 -1
- data/lib/plutonium/core/associations/renderers/factory.rb +3 -1
- data/lib/plutonium/core/controller.rb +110 -0
- data/lib/plutonium/core/controllers/authorizable.rb +12 -35
- data/lib/plutonium/core/controllers/bootable.rb +38 -7
- data/lib/plutonium/core/controllers/entity_scoping.rb +6 -2
- data/lib/plutonium/core/fields/renderers/association_renderer.rb +1 -1
- data/lib/plutonium/core/ui/collection.rb +1 -1
- data/lib/plutonium/core/ui/detail.rb +1 -1
- data/lib/plutonium/core/ui/form.rb +1 -1
- data/lib/plutonium/definition/actions.rb +50 -0
- data/lib/plutonium/definition/base.rb +92 -0
- data/lib/plutonium/definition/config_attr.rb +30 -0
- data/lib/plutonium/definition/defineable_props.rb +96 -0
- data/lib/plutonium/definition/search.rb +21 -0
- data/lib/plutonium/engine/validator.rb +30 -0
- data/lib/plutonium/engine.rb +25 -0
- data/lib/plutonium/helpers/form_helper.rb +1 -3
- data/lib/plutonium/interaction/README.md +369 -0
- data/lib/plutonium/interaction/base.rb +75 -0
- data/lib/plutonium/interaction/concerns/presentable.rb +61 -0
- data/lib/plutonium/interaction/concerns/workflow_dsl.rb +82 -0
- data/lib/plutonium/interaction/outcome.rb +129 -0
- data/lib/plutonium/interaction/response/base.rb +63 -0
- data/lib/plutonium/interaction/response/null.rb +33 -0
- data/lib/plutonium/interaction/response/redirect.rb +30 -0
- data/lib/plutonium/interaction/response/render.rb +28 -0
- data/lib/plutonium/lib/bit_flags.rb +70 -9
- data/lib/plutonium/{config → lib}/overlayed_hash.rb +1 -1
- data/lib/plutonium/lib/smart_cache.rb +171 -0
- data/lib/plutonium/models/has_cents.rb +170 -0
- data/lib/plutonium/{pkg/base.rb → package/engine.rb} +10 -2
- data/lib/plutonium/{application → portal}/controller.rb +3 -11
- data/lib/plutonium/{application → portal}/dynamic_controllers.rb +4 -4
- data/lib/plutonium/portal/engine.rb +15 -0
- data/lib/plutonium/railtie.rb +33 -1
- data/lib/plutonium/reloader.rb +5 -5
- data/lib/plutonium/resource/controller.rb +51 -34
- data/lib/plutonium/resource/controllers/authorizable.rb +128 -0
- data/lib/plutonium/{core → resource}/controllers/crud_actions.rb +23 -22
- data/lib/plutonium/resource/controllers/defineable.rb +26 -0
- data/lib/plutonium/{core → resource}/controllers/interactive_actions.rb +12 -12
- data/lib/plutonium/resource/controllers/presentable.rb +41 -0
- data/lib/plutonium/resource/controllers/queryable.rb +44 -0
- data/lib/plutonium/resource/definition.rb +6 -0
- data/lib/plutonium/resource/policy.rb +25 -13
- data/lib/plutonium/resource/query_object.rb +50 -51
- data/lib/plutonium/resource/record.rb +6 -89
- data/lib/plutonium/resource/register.rb +82 -0
- data/lib/plutonium/routing/mapper_extensions.rb +1 -1
- data/lib/plutonium/routing/resource_registration.rb +1 -1
- data/lib/plutonium/routing/route_set_extensions.rb +6 -18
- data/lib/plutonium/ui/action_button.rb +125 -0
- data/lib/plutonium/ui/breadcrumbs.rb +163 -0
- data/lib/plutonium/ui/component/base.rb +13 -0
- data/lib/plutonium/ui/component/behaviour.rb +38 -0
- data/lib/plutonium/ui/component/kit.rb +31 -0
- data/lib/plutonium/ui/component/methods.rb +54 -0
- data/lib/plutonium/ui/display/base.rb +25 -0
- data/lib/plutonium/ui/display/component/association.rb +26 -0
- data/lib/plutonium/ui/display/resource.rb +77 -0
- data/lib/plutonium/ui/display/theme.rb +27 -0
- data/lib/plutonium/ui/dyna_frame/content.rb +20 -0
- data/lib/plutonium/ui/empty_card.rb +20 -0
- data/lib/plutonium/ui/form/base.rb +37 -0
- data/lib/plutonium/ui/form/resource.rb +75 -0
- data/lib/plutonium/ui/form/theme.rb +42 -0
- data/lib/plutonium/ui/page/base.rb +112 -0
- data/lib/plutonium/ui/page/edit.rb +23 -0
- data/lib/plutonium/ui/page/index.rb +27 -0
- data/lib/plutonium/ui/page/new.rb +23 -0
- data/lib/plutonium/ui/page/show.rb +27 -0
- data/lib/plutonium/ui/page_header.rb +49 -0
- data/lib/plutonium/ui/table/base.rb +13 -0
- data/lib/plutonium/ui/table/components/pagy_info.rb +70 -0
- data/lib/plutonium/ui/table/components/pagy_page_info.rb +70 -0
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +105 -0
- data/lib/plutonium/ui/table/components/scopes_bar.rb +136 -0
- data/lib/plutonium/ui/table/components/search_bar.rb +158 -0
- data/lib/plutonium/ui/table/display_theme.rb +21 -0
- data/lib/plutonium/ui/table/resource.rb +98 -0
- data/lib/plutonium/ui/table/theme.rb +35 -0
- data/lib/plutonium/ui.rb +9 -0
- data/lib/plutonium/version.rb +5 -1
- data/lib/plutonium.rb +14 -1
- data/package-lock.json +19 -22
- data/package.json +4 -4
- data/src/css/plutonium.css +15 -0
- data/tailwind.options.js +11 -3
- metadata +218 -81
- data/lib/generators/pu/core/install/templates/app/presenters/resource_presenter.rb.tt +0 -2
- data/lib/generators/pu/core/install/templates/app/query_objects/resource_query_object.rb.tt +0 -2
- data/lib/generators/pu/pkg/feature/templates/app/query_objects/resource_query_object.rb.tt +0 -4
- data/lib/plutonium/concerns/resource_validatable.rb +0 -34
- data/lib/plutonium/config.rb +0 -9
- data/lib/plutonium/core/controllers/base.rb +0 -101
- data/lib/plutonium/core/controllers/presentable.rb +0 -65
- data/lib/plutonium/core/controllers/queryable.rb +0 -28
- data/lib/plutonium/pkg/app.rb +0 -35
- data/lib/plutonium/pkg/concerns/resource_validatable.rb +0 -36
- data/lib/plutonium/pkg/feature.rb +0 -18
- data/lib/plutonium/policy/initializer.rb +0 -22
- data/lib/plutonium/policy/scope.rb +0 -19
- data/lib/plutonium/pundit/context.rb +0 -18
- data/lib/plutonium/pundit/policy_finder.rb +0 -25
- data/lib/plutonium/resource/policy_context.rb +0 -5
- data/lib/plutonium/resource_register.rb +0 -83
- data/lib/plutonium/smart_cache.rb +0 -151
- /data/app/views/{application → plutonium}/_flash.html.erb +0 -0
- /data/app/views/{application → plutonium}/_flash_alerts.html.erb +0 -0
- /data/app/views/{application → plutonium}/_flash_toasts.html.erb +0 -0
- /data/lib/generators/pu/pkg/{app/templates/app/views/package → package/templates}/.keep +0 -0
- /data/lib/generators/pu/pkg/{feature → package}/templates/app/interactions/resource_interaction.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{feature → package}/templates/app/models/resource_record.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{feature → package}/templates/app/policies/resource_policy.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{feature → package}/templates/app/presenters/resource_presenter.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{feature → package}/templates/lib/engine.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{app → portal}/templates/app/policies/resource_policy.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{app → portal}/templates/app/presenters/resource_presenter.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{app → portal}/templates/app/query_objects/resource_query_object.rb.tt +0 -0
- /data/lib/generators/pu/pkg/{feature/templates → portal/templates/app/views/package}/.keep +0 -0
- /data/lib/generators/pu/pkg/{app → portal}/templates/config/routes.rb.tt +0 -0
- /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
|
3
|
-
module
|
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
|
2
|
+
module Portal
|
3
3
|
module Controller
|
4
4
|
extend ActiveSupport::Concern
|
5
|
-
include Plutonium::Core::
|
5
|
+
include Plutonium::Core::Controller
|
6
6
|
|
7
|
-
|
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
|
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::
|
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
|
data/lib/plutonium/railtie.rb
CHANGED
@@ -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::
|
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
|
data/lib/plutonium/reloader.rb
CHANGED
@@ -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::
|
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::
|
14
|
-
include Plutonium::
|
15
|
-
include Plutonium::
|
16
|
-
include Plutonium::
|
17
|
-
include Plutonium::
|
18
|
-
include Plutonium::
|
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 [
|
32
|
+
# @param [ActiveRecord::Base] resource_class The resource class
|
32
33
|
def controller_for(resource_class)
|
33
|
-
|
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
|
-
|
40
|
-
|
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 ||=
|
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 ||=
|
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
|
-
|
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
|
-
|
70
|
-
|
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 = "#{
|
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
|
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 = "#{
|
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
|
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(
|
193
|
+
def resource_url_args_for(*, **kwargs)
|
177
194
|
kwargs[:parent] = current_parent unless kwargs.key?(:parent)
|
178
|
-
super
|
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
|