plutonium 0.18.5 → 0.18.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/plutonium.js +1 -1
  3. data/app/assets/plutonium.js.map +2 -2
  4. data/app/assets/plutonium.min.js +1 -1
  5. data/app/assets/plutonium.min.js.map +2 -2
  6. data/app/views/resource/interactive_bulk_action.html.erb +1 -1
  7. data/app/views/resource/interactive_resource_action.html.erb +1 -1
  8. data/lib/generators/pu/eject/layout/layout_generator.rb +1 -2
  9. data/lib/generators/pu/eject/shell/shell_generator.rb +1 -3
  10. data/lib/generators/pu/lib/plutonium_generators/concerns/logger.rb +4 -0
  11. data/lib/generators/pu/lib/plutonium_generators/concerns/package_selector.rb +80 -0
  12. data/lib/generators/pu/lib/plutonium_generators/concerns/resource_selector.rb +48 -0
  13. data/lib/generators/pu/lib/plutonium_generators/generator.rb +2 -42
  14. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +1 -4
  15. data/lib/generators/pu/res/conn/conn_generator.rb +9 -21
  16. data/lib/generators/pu/res/scaffold/scaffold_generator.rb +10 -5
  17. data/lib/plutonium/models/has_cents.rb +4 -2
  18. data/lib/plutonium/resource/controller.rb +3 -3
  19. data/lib/plutonium/resource/controllers/authorizable.rb +1 -1
  20. data/lib/plutonium/resource/controllers/crud_actions.rb +17 -17
  21. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  22. data/lib/plutonium/resource/controllers/presentable.rb +2 -2
  23. data/lib/plutonium/resource/record/associated_with.rb +83 -0
  24. data/lib/plutonium/resource/record/associations.rb +92 -0
  25. data/lib/plutonium/resource/record/field_names.rb +83 -0
  26. data/lib/plutonium/resource/record/labeling.rb +19 -0
  27. data/lib/plutonium/resource/record/routes.rb +66 -0
  28. data/lib/plutonium/resource/record.rb +6 -258
  29. data/lib/plutonium/ui/breadcrumbs.rb +4 -4
  30. data/lib/plutonium/ui/component/methods.rb +4 -12
  31. data/lib/plutonium/ui/form/base.rb +22 -10
  32. data/lib/plutonium/ui/form/components/secure_association.rb +112 -0
  33. data/lib/plutonium/ui/form/components/secure_polymorphic_association.rb +54 -0
  34. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +0 -1
  35. data/lib/plutonium/ui/form/theme.rb +12 -1
  36. data/lib/plutonium/ui/page/show.rb +1 -1
  37. data/lib/plutonium/ui/page_header.rb +1 -1
  38. data/lib/plutonium/version.rb +1 -1
  39. data/package-lock.json +2 -2
  40. data/package.json +1 -1
  41. data/src/js/controllers/slim_select_controller.js +3 -1
  42. metadata +11 -4
  43. data/lib/plutonium/ui/form/components/belongs_to.rb +0 -66
  44. data/lib/plutonium/ui/form/components/has_many.rb +0 -66
@@ -1,4 +1,4 @@
1
- <%= render_component :breadcrumbs, resource_class:, parent: current_parent, resource: resource_record %>
1
+ <%= render_component :breadcrumbs, resource_class:, parent: current_parent, resource: resource_record! %>
2
2
 
3
3
  <%= render_component :dyna_frame_content do %>
4
4
  <%= render "interactive_action_form", interactive_action: current_interactive_action %>
@@ -1,4 +1,4 @@
1
- <%= render_component :breadcrumbs, resource_class:, parent: current_parent, resource: resource_record %>
1
+ <%= render_component :breadcrumbs, resource_class:, parent: current_parent, resource: resource_record! %>
2
2
 
3
3
  <%= render_component :dyna_frame_content do %>
4
4
  <%= render "interactive_action_form", interactive_action: current_interactive_action %>
@@ -11,7 +11,6 @@ module Pu
11
11
 
12
12
  desc "Eject layout views into your own project"
13
13
 
14
- class_option :dest, type: :string
15
14
  class_option :rodauth, type: :boolean
16
15
 
17
16
  def start
@@ -28,7 +27,7 @@ module Pu
28
27
  private
29
28
 
30
29
  def destination_portal
31
- @destination_portal || select_portal(options[:dest], msg: "Select destination portal")
30
+ portal_option(:dest, prompt: "Select destination portal")
32
31
  end
33
32
 
34
33
  def copy_file(source_path, destination_path)
@@ -11,8 +11,6 @@ module Pu
11
11
 
12
12
  desc "Eject layout shell (i.e header, sidebar) into your own project"
13
13
 
14
- class_option :dest, type: :string
15
-
16
14
  def start
17
15
  destination_dir = (destination_portal == "main_app") ? "app/views/" : "packages/#{destination_portal}/app/views"
18
16
  [
@@ -28,7 +26,7 @@ module Pu
28
26
  private
29
27
 
30
28
  def destination_portal
31
- @destination_portal || select_portal(options[:dest], msg: "Select destination portal")
29
+ portal_option(:dest, prompt: "Select destination portal")
32
30
  end
33
31
 
34
32
  def copy_file(source_path, destination_path)
@@ -11,6 +11,10 @@ module PlutoniumGenerators
11
11
  say format_log(msg, :info), :blue
12
12
  end
13
13
 
14
+ def warn(msg)
15
+ say format_log(msg, :warn), :yellow
16
+ end
17
+
14
18
  def success(msg)
15
19
  say format_log(msg, :success), :green
16
20
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlutoniumGenerators
4
+ module Concerns
5
+ module PackageSelector
6
+ def self.included(base)
7
+ base.send :class_option, :src, type: :string, desc: "The source package if applicable"
8
+ base.send :class_option, :dest, type: :string, desc: "The destination package if applicable"
9
+ end
10
+
11
+ private
12
+
13
+ def reserved_packages
14
+ %w[core reactor app main plutonium pluton8 plutonate]
15
+ end
16
+
17
+ def validate_package_name(package_name)
18
+ package_name = package_name.underscore
19
+ error("Package name is reserved\n\n#{reserved_packages.join "\n"}") if reserved_packages.include?(package_name)
20
+ error("Package name cannot end in `_app` or `_portal`") if /(_app|_portal)$/i.match?(package_name)
21
+ end
22
+
23
+ def available_packages
24
+ @available_packages ||= begin
25
+ packages = Dir["packages/*"].map { |dir| dir.gsub "packages/", "" }
26
+ packages - reserved_packages
27
+ end
28
+ end
29
+
30
+ def available_portals
31
+ @available_portals ||= ["main_app"] + available_packages.select { |pkg| pkg.ends_with?("_app") || pkg.ends_with?("_portal") }.sort
32
+ end
33
+
34
+ def available_features
35
+ @available_features ||= ["main_app"] + available_packages.select { |pkg| !(pkg.ends_with?("_app") || pkg.ends_with?("_portal")) }.sort
36
+ end
37
+
38
+ def select_package(selected_package = nil, msg: "Select package", pkgs: nil)
39
+ pkgs ||= available_packages
40
+ if pkgs.include?(selected_package)
41
+ selected_package
42
+ else
43
+ prompt.select(msg, pkgs)
44
+ end
45
+ end
46
+
47
+ def select_feature(selected_package = nil, msg: "Select feature")
48
+ select_package(selected_package, msg: msg, pkgs: available_features)
49
+ end
50
+
51
+ def feature_option(name, prompt: nil, option_key: nil)
52
+ # Get stored value or command line option
53
+ ivar = :"@#{name}_feature_option"
54
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
55
+
56
+ # Validate option or prompt user
57
+ option_key ||= name
58
+ value = select_feature(options[option_key], msg: prompt || "Select #{name} feature")
59
+ instance_variable_set(ivar, value)
60
+ value
61
+ end
62
+
63
+ def select_portal(selected_package = nil, msg: "Select portal")
64
+ select_package(selected_package, msg: msg, pkgs: available_portals)
65
+ end
66
+
67
+ def portal_option(name, prompt: nil, option_key: nil)
68
+ # Get stored value or command line option
69
+ ivar = :"@#{name}_portal_option"
70
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
71
+
72
+ # Validate option or prompt user
73
+ option_key ||= name
74
+ value = select_portal(options[option_key], msg: prompt || "Select #{name} portal")
75
+ instance_variable_set(ivar, value)
76
+ value
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlutoniumGenerators
4
+ module Concerns
5
+ module ResourceSelector
6
+ def self.included(base)
7
+ # base.send :class_option, :resources, type: :array, desc: "List of resource model names if applicable"
8
+ base.send :argument, :resources, type: :array, optional: true, default: [],
9
+ desc: "List of model names if applicable"
10
+ end
11
+
12
+ private
13
+
14
+ def available_resources(source_module)
15
+ Plutonium.eager_load_rails!
16
+
17
+ source_module.constantize.descendants.reject do |resource_klass|
18
+ next true if resource_klass.abstract_class?
19
+ next true if source_module == "ApplicationRecord" &&
20
+ resource_klass.ancestors.any? { |ancestor| ancestor.to_s.end_with?("::ResourceRecord") }
21
+ end.map(&:to_s).sort
22
+ end
23
+
24
+ def select_resources(source_module, prompt: "Select resources")
25
+ resources = available_resources(source_module)
26
+ error "No resources found" if resources.blank?
27
+
28
+ self.prompt.multi_select(prompt, resources)
29
+ end
30
+
31
+ def resources_selection(prompt: nil)
32
+ ivar = :@resources_selection
33
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
34
+
35
+ # Convert comma-separated string to array if from command line
36
+ value = resources.map(&:classify)
37
+ if value.empty?
38
+ source_feature = feature_option :src, prompt: "Select source feature"
39
+ source_module = (source_feature == "main_app") ? "ApplicationRecord" : "#{source_feature.camelize}::ResourceRecord"
40
+ value = select_resources(source_module, prompt: prompt || "Select #{source_module} resources")
41
+ end
42
+
43
+ instance_variable_set(ivar, value)
44
+ value
45
+ end
46
+ end
47
+ end
48
+ end
@@ -14,51 +14,11 @@ module PlutoniumGenerators
14
14
  base.send :class_option, :interactive, type: :boolean, desc: "Show prompts. Default: true"
15
15
  base.send :class_option, :bundle, type: :boolean, desc: "Run bundle after setup. Default: true"
16
16
  base.send :class_option, :lint, type: :boolean, desc: "Run linter after generation. Default: false"
17
- end
18
-
19
- protected
20
-
21
- def reserved_packages
22
- %w[core reactor app main plutonium pluton8 plutonate]
23
- end
24
-
25
- def validate_package_name(package_name)
26
- package_name = package_name.underscore
27
- error("Package name is reserved\n\n#{reserved_packages.join "\n"}") if reserved_packages.include?(package_name)
28
- error("Package name cannot end in `_app` or `_portal`") if /(_app|_portal)$/i.match?(package_name)
29
- end
30
-
31
- def available_packages
32
- @available_packages ||= begin
33
- packages = Dir["packages/*"].map { |dir| dir.gsub "packages/", "" }
34
- packages - reserved_packages
35
- end
36
- end
37
17
 
38
- def available_apps
39
- @available_apps ||= ["main_app"] + available_packages.select { |pkg| pkg.ends_with?("_app") || pkg.ends_with?("_portal") }.sort
18
+ base.include Concerns::PackageSelector
40
19
  end
41
20
 
42
- def available_features
43
- @available_features ||= ["main_app"] + available_packages.select { |pkg| !(pkg.ends_with?("_app") || pkg.ends_with?("_portal")) }.sort
44
- end
45
-
46
- def select_package(selected_package = nil, msg: "Select package", pkgs: nil)
47
- pkgs ||= available_packages
48
- if pkgs.include?(selected_package)
49
- selected_package
50
- else
51
- prompt.select(msg, pkgs)
52
- end
53
- end
54
-
55
- def select_portal(selected_package = nil, msg: "Select portal")
56
- select_package(selected_package, msg: msg, pkgs: available_apps)
57
- end
58
-
59
- def select_feature(selected_package = nil, msg: "Select feature")
60
- select_package(selected_package, msg: msg, pkgs: available_features)
61
- end
21
+ protected
62
22
 
63
23
  # ####################
64
24
 
@@ -13,8 +13,6 @@ module PlutoniumGenerators
13
13
  remove_task :create_module_file
14
14
  # remove_task :check_class_collision
15
15
 
16
- class_option :dest, type: :string
17
-
18
16
  # def check_class_collision # :doc:
19
17
  # class_collisions "#{options[:prefix]}#{name}#{options[:suffix]}"
20
18
  # end
@@ -41,7 +39,6 @@ module PlutoniumGenerators
41
39
  def name
42
40
  @pu_name ||= begin
43
41
  @original_name = @name
44
- @selected_destination_feature = select_feature selected_destination_feature, msg: "Select destination feature"
45
42
  @name = [main_app? ? nil : selected_destination_feature.underscore, super.singularize.underscore].compact.join "/"
46
43
  set_destination_root!
47
44
  @name
@@ -57,7 +54,7 @@ module PlutoniumGenerators
57
54
  end
58
55
 
59
56
  def selected_destination_feature
60
- @selected_destination_feature || options[:dest]
57
+ feature_option :dest, prompt: "Select destination feature"
61
58
  end
62
59
 
63
60
  def set_destination_root!
@@ -6,26 +6,20 @@ module Pu
6
6
  module Res
7
7
  class ConnGenerator < Rails::Generators::Base
8
8
  include PlutoniumGenerators::Generator
9
+ include PlutoniumGenerators::Concerns::ResourceSelector
9
10
 
10
11
  source_root File.expand_path("templates", __dir__)
11
12
 
12
- desc "Create a connection between a resource and an app"
13
+ desc(
14
+ "Create a connection between a resource and a portal\n\n" \
15
+ "e.g. rails g pu:res:conn todo --dest=dashboard_portal"
16
+ )
13
17
 
14
18
  # argument :name
15
19
 
16
20
  def start
17
- source_feature = select_feature msg: "Select source feature"
18
- source_module = (source_feature == "main_app") ? "ApplicationRecord" : "#{source_feature.camelize}::ResourceRecord"
19
-
20
- Plutonium.eager_load_rails!
21
- available_resources = source_module.constantize.descendants.reject do |model|
22
- next true if model.abstract_class?
23
- next true if source_module == "ApplicationRecord" && model.ancestors.any? { |ancestor| ancestor.to_s.end_with?("::ResourceRecord") }
24
- end.map(&:to_s).sort
25
- error "No resources found" if available_resources.blank?
26
- selected_resources = prompt.multi_select("Select resources", available_resources)
27
-
28
- @app_namespace = select_portal.camelize
21
+ selected_resources = resources_selection
22
+ @app_namespace = portal_option(:dest, prompt: "Select destination portal").camelize
29
23
 
30
24
  selected_resources.each do |resource|
31
25
  @resource_class = resource
@@ -97,13 +91,7 @@ module Pu
97
91
 
98
92
  def attributes
99
93
  resource_klass = resource_class.constantize
100
- unwanted_attrs = [
101
- resource_klass.primary_key.to_sym, # primary_key
102
- :created_at, :updated_at # timestamps
103
- ]
104
94
  resource_klass.content_columns.filter_map { |col|
105
- next if unwanted_attrs.include? col.name.to_sym
106
-
107
95
  PlutoniumGenerators::ModelGeneratorBase::GeneratedAttribute.parse resource_class, "#{col.name}:#{col.type}"
108
96
  }
109
97
  rescue ActiveRecord::StatementInvalid
@@ -116,11 +104,11 @@ module Pu
116
104
  end
117
105
 
118
106
  def policy_attributes_for_create
119
- default_policy_attributes
107
+ default_policy_attributes - [:created_at, :updated_at]
120
108
  end
121
109
 
122
110
  def policy_attributes_for_read
123
- default_policy_attributes + [:created_at, :updated_at]
111
+ default_policy_attributes
124
112
  end
125
113
  end
126
114
  end
@@ -4,6 +4,7 @@ require_relative "../../lib/plutonium_generators"
4
4
 
5
5
  module Pu
6
6
  module Res
7
+ # run `rails g pu:res:scaffold existing_resource` without any arguments to import an existing resource
7
8
  class ScaffoldGenerator < PlutoniumGenerators::ModelGeneratorBase
8
9
  include PlutoniumGenerators::Generator
9
10
 
@@ -17,9 +18,13 @@ module Pu
17
18
  return unless options[:model]
18
19
 
19
20
  model_class = class_name.safe_constantize
20
- if model_class.present? && attributes.empty? && prompt.yes?("Existing model class found. Do you want to import its attributes?")
21
- attributes_str = model_class.content_columns.map { |col| "#{col.name}:#{col.type}" }
22
- self.attributes = parse_attributes_internal!(attributes_str)
21
+ if model_class.present?
22
+ if attributes.empty?
23
+ attributes_str = model_class.content_columns.map { |col| "#{col.name}:#{col.type}" }
24
+ self.attributes = parse_attributes_internal!(attributes_str)
25
+ else
26
+ warn("Overwriting existing resource. You can leave out the attributes to import an existing resource.")
27
+ end
23
28
  end
24
29
  end
25
30
 
@@ -56,11 +61,11 @@ module Pu
56
61
  end
57
62
 
58
63
  def policy_attributes_for_create
59
- default_policy_attributes
64
+ default_policy_attributes - [:created_at, :updated_at]
60
65
  end
61
66
 
62
67
  def policy_attributes_for_read
63
- default_policy_attributes + [:created_at, :updated_at]
68
+ default_policy_attributes
64
69
  end
65
70
  end
66
71
  end
@@ -143,8 +143,10 @@ module Plutonium
143
143
  #
144
144
  # @param value [Numeric, nil] The decimal value to be set.
145
145
  def #{name}=(value)
146
- self.#{cents_name} = if value.present?
147
- (BigDecimal(value.to_s) * #{rate}).to_i
146
+ self.#{cents_name} = begin
147
+ (BigDecimal(value.to_s) * #{rate}).to_i if value.present?
148
+ rescue ArgumentError
149
+ nil
148
150
  end
149
151
  end
150
152
 
@@ -19,7 +19,7 @@ module Plutonium
19
19
  # https://github.com/ddnexus/pagy/blob/master/docs/extras/headers.md#headers
20
20
  after_action { pagy_headers_merge(@pagy) if @pagy }
21
21
 
22
- helper_method :current_parent, :resource_record, :resource_param_key, :resource_class
22
+ helper_method :current_parent, :resource_record!, :resource_record?, :resource_param_key, :resource_class
23
23
  end
24
24
 
25
25
  class_methods do
@@ -62,7 +62,7 @@ module Plutonium
62
62
  end
63
63
  end
64
64
 
65
- def resource_record
65
+ def resource_record!
66
66
  @resource_record ||= resource_record_relation.first!
67
67
  end
68
68
 
@@ -117,7 +117,7 @@ module Plutonium
117
117
  # Applies submitted resource params if they have been passed
118
118
  def maybe_apply_submitted_resource_params!
119
119
  ensure_get_request
120
- resource_record.attributes = submitted_resource_params if params[resource_param_key]
120
+ resource_record!.attributes = submitted_resource_params if params[resource_param_key]
121
121
  end
122
122
 
123
123
  # Returns the current parent based on path parameters
@@ -12,7 +12,7 @@ module Plutonium
12
12
  # include Plutonium::Resource::Controllers::Authorizable
13
13
  # end
14
14
  #
15
- # @note This module assumes the existence of methods like `resource_record`,
15
+ # @note This module assumes the existence of methods like `resource_record!`,
16
16
  # `resource_class`, `current_parent`, and `entity_scope_for_authorize`.
17
17
  #
18
18
  # @see ActionPolicy
@@ -21,8 +21,8 @@ module Plutonium
21
21
 
22
22
  # GET /resources/1(.{format})
23
23
  def show
24
- authorize_current! resource_record
25
- set_page_title resource_record.to_label.titleize
24
+ authorize_current! resource_record!
25
+ set_page_title resource_record!.to_label.titleize
26
26
 
27
27
  render :show
28
28
  end
@@ -46,7 +46,7 @@ module Plutonium
46
46
  @resource_record = resource_class.new resource_params
47
47
 
48
48
  respond_to do |format|
49
- if resource_record.save
49
+ if resource_record!.save
50
50
  format.html do
51
51
  redirect_to redirect_url_after_submit,
52
52
  notice: "#{resource_class.model_name.human} was successfully created."
@@ -57,7 +57,7 @@ module Plutonium
57
57
  render :new, status: :unprocessable_entity
58
58
  end
59
59
  format.any do
60
- @errors = resource_record.errors
60
+ @errors = resource_record!.errors
61
61
  render "errors", status: :unprocessable_entity
62
62
  end
63
63
  end
@@ -66,8 +66,8 @@ module Plutonium
66
66
 
67
67
  # GET /resources/1/edit
68
68
  def edit
69
- authorize_current! resource_record
70
- set_page_title "Update #{resource_record.to_label.titleize}"
69
+ authorize_current! resource_record!
70
+ set_page_title "Update #{resource_record!.to_label.titleize}"
71
71
 
72
72
  maybe_apply_submitted_resource_params!
73
73
 
@@ -76,11 +76,11 @@ module Plutonium
76
76
 
77
77
  # PATCH/PUT /resources/1(.{format})
78
78
  def update
79
- authorize_current! resource_record
80
- set_page_title "Update #{resource_record.to_label.titleize}"
79
+ authorize_current! resource_record!
80
+ set_page_title "Update #{resource_record!.to_label.titleize}"
81
81
 
82
82
  respond_to do |format|
83
- if resource_record.update(resource_params)
83
+ if resource_record!.update(resource_params)
84
84
  format.html do
85
85
  redirect_to redirect_url_after_submit, notice: "#{resource_class.model_name.human} was successfully updated.",
86
86
  status: :see_other
@@ -91,7 +91,7 @@ module Plutonium
91
91
  render :edit, status: :unprocessable_entity
92
92
  end
93
93
  format.any do
94
- @errors = resource_record.errors
94
+ @errors = resource_record!.errors
95
95
  render "errors", status: :unprocessable_entity
96
96
  end
97
97
  end
@@ -100,10 +100,10 @@ module Plutonium
100
100
 
101
101
  # DELETE /resources/1(.{format})
102
102
  def destroy
103
- authorize_current! resource_record
103
+ authorize_current! resource_record!
104
104
 
105
105
  respond_to do |format|
106
- resource_record.destroy
106
+ resource_record!.destroy
107
107
 
108
108
  format.html do
109
109
  redirect_to redirect_url_after_destroy,
@@ -112,11 +112,11 @@ module Plutonium
112
112
  format.json { head :no_content }
113
113
  rescue ActiveRecord::InvalidForeignKey
114
114
  format.html do
115
- redirect_to resource_url_for(resource_record),
115
+ redirect_to resource_url_for(resource_record!),
116
116
  alert: "#{resource_class.model_name.human} is referenced by other records."
117
117
  end
118
118
  format.any do
119
- @errors = ActiveModel::Errors.new resource_record
119
+ @errors = ActiveModel::Errors.new resource_record!
120
120
  @errors.add :base, :existing_references, message: "is referenced by other records"
121
121
 
122
122
  render "errors", status: :unprocessable_entity
@@ -133,9 +133,9 @@ module Plutonium
133
133
 
134
134
  url = case preferred_action_after_submit
135
135
  when "show"
136
- resource_url_for(resource_record) if current_policy.allowed_to? :show?
136
+ resource_url_for(resource_record!) if current_policy.allowed_to? :show?
137
137
  when "edit"
138
- resource_url_for(resource_record, action: :edit) if current_policy.allowed_to? :edit?
138
+ resource_url_for(resource_record!, action: :edit) if current_policy.allowed_to? :edit?
139
139
  when "new"
140
140
  resource_url_for(resource_class, action: :new) if current_policy.allowed_to? :new?
141
141
  when "index"
@@ -144,7 +144,7 @@ module Plutonium
144
144
  # ensure we have a valid value
145
145
  session[:action_after_submit_preference] = "show"
146
146
  end
147
- url || resource_url_for(resource_record)
147
+ url || resource_url_for(resource_record!)
148
148
  end
149
149
 
150
150
  def redirect_url_after_destroy
@@ -42,7 +42,7 @@ module Plutonium
42
42
  if outcome.success?
43
43
  outcome.to_response.process(self) do |value|
44
44
  respond_to do |format|
45
- return_url = redirect_url_after_action_on(resource_record)
45
+ return_url = redirect_url_after_action_on(resource_record!)
46
46
  format.any { redirect_to return_url, status: :see_other }
47
47
  if helpers.current_turbo_frame == "modal"
48
48
  format.turbo_stream do
@@ -202,7 +202,7 @@ module Plutonium
202
202
 
203
203
  def authorize_interactive_record_action!
204
204
  interactive_resource_action = params[:interactive_action]&.to_sym
205
- authorize_current! resource_record, to: :"#{interactive_resource_action}?"
205
+ authorize_current! resource_record!, to: :"#{interactive_resource_action}?"
206
206
  end
207
207
 
208
208
  def authorize_interactive_resource_action!
@@ -216,7 +216,7 @@ module Plutonium
216
216
 
217
217
  def build_interactive_record_action_interaction
218
218
  @interaction = current_interactive_action.interaction.new(view_context:)
219
- @interaction.attributes = interaction_params.merge(resource: resource_record)
219
+ @interaction.attributes = interaction_params.merge(resource: resource_record!)
220
220
  @interaction
221
221
  end
222
222
 
@@ -36,10 +36,10 @@ module Plutonium
36
36
  end
37
37
 
38
38
  def build_detail
39
- current_definition.detail_class.new(resource_record, resource_fields: presentable_attributes, resource_associations: permitted_associations, resource_definition: current_definition)
39
+ current_definition.detail_class.new(resource_record!, resource_fields: presentable_attributes, resource_associations: permitted_associations, resource_definition: current_definition)
40
40
  end
41
41
 
42
- def build_form(record = resource_record)
42
+ def build_form(record = resource_record!)
43
43
  current_definition.form_class.new(record, resource_fields: submittable_attributes, resource_definition: current_definition)
44
44
  end
45
45
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/plutonium/resource/associations.rb
4
+ module Plutonium
5
+ module Resource
6
+ module Record
7
+ module AssociatedWith
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ scope :associated_with, ->(record) do
12
+ named_scope = :"associated_with_#{record.model_name.singular}"
13
+ return send(named_scope, record) if respond_to?(named_scope)
14
+
15
+ own_association = klass.find_association_from_self_to_record(record)
16
+ if own_association
17
+ return klass.query_based_on_association(own_association, record)
18
+ end
19
+
20
+ record_association = klass.find_association_to_self_from_record(record)
21
+ if record_association
22
+ Plutonium.logger.warn do
23
+ [
24
+ "Using indirect association from #{record.class} to #{klass.name}",
25
+ "via '#{record_association.name}'.",
26
+ "This may result in poor query performance for large datasets",
27
+ "as it requires loading records to perform the association.",
28
+ "",
29
+ "Consider defining a direct association or implementing",
30
+ "a custom scope '#{named_scope}' for better performance."
31
+ ].join("\n")
32
+ end
33
+ return where(id: record.public_send(record_association.name))
34
+ end
35
+
36
+ klass.raise_unresolvable_association_error(record, named_scope)
37
+ end
38
+ end
39
+
40
+ class_methods do
41
+ def find_association_from_self_to_record(record)
42
+ reflect_on_all_associations.find do |assoc|
43
+ assoc.klass.name == record.class.name unless assoc.polymorphic?
44
+ rescue
45
+ assoc.check_validity!
46
+ raise
47
+ end
48
+ end
49
+
50
+ def find_association_to_self_from_record(record)
51
+ record.class.reflect_on_all_associations.find do |assoc|
52
+ assoc.klass.name == name
53
+ rescue
54
+ assoc.check_validity!
55
+ raise
56
+ end
57
+ end
58
+
59
+ def query_based_on_association(assoc, record)
60
+ case assoc.macro
61
+ when :has_one
62
+ joins(assoc.name).where(assoc.name => {record.class.primary_key => record.id})
63
+ when :belongs_to
64
+ where(assoc.name => record)
65
+ when :has_many
66
+ joins(assoc.name).where(assoc.klass.table_name => record)
67
+ else
68
+ raise NotImplementedError, "associated_with->##{assoc.macro}"
69
+ end
70
+ end
71
+
72
+ def raise_unresolvable_association_error(record, named_scope)
73
+ raise "Could not resolve the association between '#{name}' and '#{record.class.name}'\n\n" \
74
+ "Define\n" \
75
+ " 1. the associations between the models\n" \
76
+ " 2. a named scope on #{name} e.g.\n\n" \
77
+ "scope :#{named_scope}, ->(#{record.model_name.singular}) { do_something_here }"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end