plutonium 0.18.5 → 0.18.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) 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/resource/controller.rb +3 -3
  18. data/lib/plutonium/resource/controllers/authorizable.rb +1 -1
  19. data/lib/plutonium/resource/controllers/crud_actions.rb +17 -17
  20. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  21. data/lib/plutonium/resource/controllers/presentable.rb +2 -2
  22. data/lib/plutonium/resource/record/associated_with.rb +83 -0
  23. data/lib/plutonium/resource/record/associations.rb +92 -0
  24. data/lib/plutonium/resource/record/field_names.rb +83 -0
  25. data/lib/plutonium/resource/record/labeling.rb +19 -0
  26. data/lib/plutonium/resource/record/routes.rb +66 -0
  27. data/lib/plutonium/resource/record.rb +6 -258
  28. data/lib/plutonium/ui/breadcrumbs.rb +4 -4
  29. data/lib/plutonium/ui/component/methods.rb +4 -12
  30. data/lib/plutonium/ui/form/base.rb +22 -10
  31. data/lib/plutonium/ui/form/components/secure_association.rb +112 -0
  32. data/lib/plutonium/ui/form/components/secure_polymorphic_association.rb +54 -0
  33. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +0 -1
  34. data/lib/plutonium/ui/form/theme.rb +12 -1
  35. data/lib/plutonium/ui/page/show.rb +1 -1
  36. data/lib/plutonium/ui/page_header.rb +1 -1
  37. data/lib/plutonium/version.rb +1 -1
  38. data/package-lock.json +2 -2
  39. data/package.json +1 -1
  40. data/src/js/controllers/slim_select_controller.js +3 -1
  41. metadata +11 -4
  42. data/lib/plutonium/ui/form/components/belongs_to.rb +0 -66
  43. 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
@@ -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