para 0.9.0 → 0.9.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d193279195926cf9c9d771bc66d9d0087bfe61ff9e7662f1c66833278088dc1
4
- data.tar.gz: deaba7951bc96c94bf6175bf55061b2d14f988d9f992d538e27774ad21587a78
3
+ metadata.gz: 8f742ef6484e0ccd595c1a79e219f4cb2a03c72a7fbf9f584990c638412f19e0
4
+ data.tar.gz: 8c231925d6c2ecc4d0eb7208c5c4b530410d1a30d8a1276d849f31d3e52f5f76
5
5
  SHA512:
6
- metadata.gz: '0584c068c6a24a3a8a9c9a608f4c2288644df56ca6b34007c98da511ab22ec55178ff1462d43e411a43ecf4b00cc0dcbc05b7529ba6c2643302b8b8930c96810'
7
- data.tar.gz: 54d4160767fce3095334d2d81c370cf3fc103d07ef0db5afe1b180c2325fde2cd727d3db5c541bfe49395f607d69667b4fa83b33df22c1a06b22a619ecfa2981
6
+ metadata.gz: e42df31fcb4d4d490f5b5127939494fbc9e07855ee1123fd720f089c2e36737b4e1f706b347d36699270ed00eeeb4a228ca81050bdc8ae35b84bbde8a7dfd7b6
7
+ data.tar.gz: 707696ad2b1f6a2c43f6944f6543ca5dd8dd5a17c3c91942d0d3c38b43e5d2c652d1b76a35409c2de0019c11658f81a6922931d3e4126a6490a77ed189bd8e8a
@@ -5,7 +5,7 @@ class Para.NestedManyField
5
5
  @initializeOrderable()
6
6
  @initializeCocoon()
7
7
 
8
- @$field.on 'shown.bs.collapse', $.proxy(@collapseShown, this)
8
+ @$field.on 'shown.bs.collapse', @stoppingPropagation(@collapseShown)
9
9
 
10
10
  initializeOrderable: ->
11
11
  @orderable = @$field.hasClass('orderable')
@@ -21,11 +21,17 @@ class Para.NestedManyField
21
21
  $(el).find('.resource-position-field').val(i)
22
22
 
23
23
  initializeCocoon: ->
24
- @$fieldsList.on 'cocoon:after-insert', $.proxy(@afterInsertField, this)
25
- @$fieldsList.on 'cocoon:before-remove', $.proxy(@beforeRemoveField, this)
26
- @$fieldsList.on 'cocoon:after-remove', $.proxy(@afterRemoveField, this)
24
+ @$fieldsList.on 'cocoon:after-insert', @stoppingPropagation(@afterInsertField)
25
+ @$fieldsList.on 'cocoon:before-remove', @stoppingPropagation(@beforeRemoveField)
26
+ @$fieldsList.on 'cocoon:after-remove', @stoppingPropagation(@afterRemoveField)
27
27
 
28
- afterInsertField: (e, $element) ->
28
+ stoppingPropagation: (callback) =>
29
+ (e, args...) =>
30
+ e.stopPropagation()
31
+ callback(e, args...)
32
+
33
+
34
+ afterInsertField: (e, $element) =>
29
35
  if ($collapsible = $element.find('[data-open-on-insert="true"]')).length
30
36
  @openInsertedField($collapsible)
31
37
 
@@ -36,21 +42,21 @@ class Para.NestedManyField
36
42
 
37
43
  $element.simpleForm()
38
44
 
39
- beforeRemoveField: (e, $element) ->
45
+ beforeRemoveField: (e, $element) =>
40
46
  $nextEl = $element.next()
41
47
  # Remove attributes mappings field for new records since it will try to
42
48
  # create an empty nested resource otherwise
43
49
  $nextEl.remove() if $nextEl.is('[data-attributes-mappings]') and not $element.is('[data-persisted]')
44
50
 
45
51
  # When a sub field is removed, update every sub field position
46
- afterRemoveField: ->
52
+ afterRemoveField: =>
47
53
  @handleOrderingUpdated();
48
54
 
49
55
  openInsertedField: ($field) ->
50
56
  $target = $($field.attr('href'))
51
57
  $target.collapse('show')
52
58
 
53
- collapseShown: (e) ->
59
+ collapseShown: (e) =>
54
60
  $target = $(e.target)
55
61
 
56
62
  if $target.is("[data-rendered]") or $target.data("rendered")
@@ -8,7 +8,7 @@ module Para
8
8
  def create
9
9
  job = @exporter.perform_later(
10
10
  model_name: @component.try(:model).try(:name),
11
- search: params[:q],
11
+ search: params[:q]&.permit!,
12
12
  params: params.permit(@exporter.params_whitelist).to_h
13
13
  )
14
14
 
@@ -5,6 +5,7 @@ module Para
5
5
  @model = params[:model_name].constantize
6
6
  @object = params[:id] ? @model.find(params[:id]) : @model.new
7
7
  @object_name = params[:object_name]
8
+ @builder_options = params[:builder_options]&.permit! || {}
8
9
 
9
10
  render layout: false
10
11
  end
@@ -1,6 +1,15 @@
1
1
  - nested_form = nil
2
2
 
3
- - para_form_for(@object, url: "") do |form|
3
+ -# Initialize an unredered form builder that will only be used to create the nested fields
4
+ -# by using the yielded block form builder argument.
5
+ -#
6
+ -# The form's object name is set through the provided :object_name param to ensure that
7
+ -# the form inputs are named correctly in the loading form.
8
+ -#
9
+ -# Builder options allow to customize the options passed to the form builder to customize
10
+ -# the rendered partials.
11
+ -#
12
+ - para_form_for(@object, @builder_options.merge(url: "")) do |form|
4
13
  - form.object_name = @object_name
5
14
  - nested_form = capture do
6
15
  = render(partial: find_partial_for(@model, :remote_nested_form), locals: { form: form, model: @model, object: @object, object_name: @object_name })
@@ -1,10 +1,13 @@
1
1
  .top-nav-tabs-affix-placeholder
2
2
  %ul.top-nav-tabs-affix.nav.nav-tabs{ role: "tablist", "data-top-level-affix": true }
3
- %li{ class: ("active" if active_component == parent_component) }
4
- = link_to parent_component.path do
5
- = parent_component.name
3
+ - if can?(:manage, parent_component) && show_component?(parent_component)
4
+ %li{ class: ("active" if active_component == parent_component) }
5
+ = link_to parent_component.path do
6
+ = parent_component.name
6
7
 
7
8
  - parent_component.child_components.each do |child_component|
9
+ - next unless can?(:manage, child_component) && show_component?(child_component)
10
+
8
11
  %li{ class: ("active" if active_component == child_component) }
9
12
  = link_to child_component.path do
10
13
  = child_component.name
@@ -1 +1 @@
1
- = render partial: find_partial_for(model, :fields), locals: { form: form }
1
+ = render partial: find_partial_for(model, form.nested_fields_partial_name), locals: { form: form }
@@ -2,6 +2,7 @@
2
2
  .fields-list{ id: dom_identifier }
3
3
  = form.simple_fields_for attribute_name, resources, nested_attribute_name: attribute_name, orderable: orderable, track_attribute_mappings: render_partial do |nested_form|
4
4
  = render partial: find_partial_for(model, 'nested_many/container', partial_dir: 'inputs'), locals: { form: nested_form, model: nested_form.object.class, subclass: subclass, nested_locals: nested_locals, inset: inset, uncollapsed: uncollapsed, render_partial: render_partial, remote_partial_params: remote_partial_params }
5
+
5
6
  -# Add button
6
7
  - if add_button
7
8
  - if subclass
@@ -6,7 +6,7 @@
6
6
  = render partial: find_partial_for(model, 'nested_one/container', partial_dir: 'inputs'), locals: { nested_form: nested_form, form: form, model: nested_form.object.class, subclass: subclass, nested_locals: nested_locals }
7
7
 
8
8
  - else
9
- = render partial: find_partial_for(model, :fields), locals: { form: nested_form, parent: form.object }.merge(nested_locals)
9
+ = render partial: find_partial_for(model, form.nested_fields_partial_name), locals: { form: nested_form, parent: form.object }.merge(nested_locals)
10
10
 
11
11
  - if defined?(subclass) && subclass
12
- = render partial: 'para/inputs/nested_one/add_with_subclasses', locals: { form: form, model: model, dom_identifier: dom_identifier, nested_locals: nested_locals, attribute_name: attribute_name, subclasses: subclasses, add_button_label: add_button_label, add_button_class: add_button_class, subclass: subclass }
12
+ = render partial: 'para/inputs/nested_one/add_with_subclasses', locals: { form: form, model: model, dom_identifier: dom_identifier, nested_locals: nested_locals, attribute_name: attribute_name, subclasses: subclasses, add_button_label: add_button_label, add_button_class: add_button_class, subclass: subclass }
@@ -14,7 +14,7 @@
14
14
  .panel-collapse.form-inputs.collapse{ id: form.nested_resource_dom_id, class: ('in' if uncollapsed && form.object.persisted?), data: { rendered: render_partial, render_path: @component.path(remote_partial_params), id: form.object.id, :"object-name" => form.object_name, :"model-name" => model.name } }
15
15
  .panel-body{ data: { :"nested-form-container" => true } }
16
16
  - if render_partial
17
- = render partial: find_partial_for(model, :fields), locals: { form: form }.merge(nested_locals)
17
+ = render partial: find_partial_for(model, form.nested_fields_partial_name), locals: { form: form }.merge(nested_locals)
18
18
  - else
19
19
  = fa_icon "spinner spin"
20
20
 
@@ -6,4 +6,4 @@
6
6
  %i.fa.fa-angle-up
7
7
 
8
8
  .panel-body.panel-collapse.form-inputs.collapse{ id: nested_form.nested_resource_dom_id }
9
- = render partial: find_partial_for(model, :fields), locals: { form: nested_form, parent: nested_form.object }.merge(nested_locals)
9
+ = render partial: find_partial_for(model, form.nested_fields_partial_name), locals: { form: nested_form, parent: nested_form.object }.merge(nested_locals)
@@ -1,36 +1,90 @@
1
1
  module Para
2
2
  module Cloneable
3
3
  class AttachmentsCloner
4
- attr_reader :original, :clone
4
+ attr_reader :original, :clone, :dictionary
5
5
 
6
- def initialize(original, clone)
6
+ # Handle both one and many attachment relations
7
+ ATTACHMENTS_RELATION_REGEX = /_attachments?\z/
8
+
9
+ def initialize(original, clone, dictionary)
7
10
  @original = original
8
11
  @clone = clone
12
+ @dictionary = dictionary
9
13
  end
10
-
14
+
11
15
  def clone!
12
16
  return unless defined?(ActiveStorage)
13
-
14
- attachment_reflections = original.class.reflections.select { |k, v|
15
- k.to_s.match(/_attachment\z/) &&
16
- v.options[:class_name] == "ActiveStorage::Attachment"
17
- }
17
+
18
+ attachment_reflections = original.class.reflections.select do |name, reflection|
19
+ name.to_s.match(ATTACHMENTS_RELATION_REGEX) &&
20
+ reflection.options[:class_name] == "ActiveStorage::Attachment"
21
+ end
18
22
 
19
23
  attachment_reflections.each do |name, reflection|
20
- original_attachment = original.send(name)
21
- next unless original_attachment
22
-
23
- association_name = name.gsub(/_attachment\z/, "")
24
-
25
- Para::ActiveStorageDownloader.new(original_attachment).download_blob_to_tempfile do |tempfile|
26
- clone.send(association_name).attach({
27
- io: tempfile,
28
- filename: original_attachment.blob.filename,
29
- content_type: original_attachment.blob.content_type
30
- })
24
+ association_target = original.send(name)
25
+ next unless association_target
26
+
27
+ if reflection.collection?
28
+ association_target.each do |attachment|
29
+ clone_attachment(name, attachment)
30
+ end
31
+ else
32
+ clone_attachment(name, association_target)
31
33
  end
32
34
  end
33
35
  end
36
+
37
+ def clone_attachment(name, original_attachment)
38
+ association_name = name.gsub(ATTACHMENTS_RELATION_REGEX, "")
39
+ original_blob = original_attachment.blob
40
+
41
+ # Handle missing file in storage service by bypassing the attachment cloning
42
+ return unless ActiveStorage::Blob.service.exist?(original_blob&.key)
43
+
44
+ Para::ActiveStorageDownloader.new(original_attachment).download_blob_to_tempfile do |tempfile|
45
+ attachment_target = clone.send(association_name)
46
+
47
+ attachment_target.attach({
48
+ io: tempfile,
49
+ filename: original_blob.filename,
50
+ content_type: original_blob.content_type
51
+ })
52
+
53
+ cloned_attachment = find_cloned_attachment(attachment_target, original_blob)
54
+
55
+ # Store the cloned attachment and blob into the deep_cloneable dictionary used
56
+ # by the `deep_clone` method to ensure that, if needed during the cloning
57
+ # operation, they won't be cloned once more and are accessible for processing
58
+ store_cloned(original_attachment, cloned_attachment)
59
+ store_cloned(original_blob, cloned_attachment.blob)
60
+ end
61
+ end
62
+
63
+ # Seemlessly handle one and many attachment relations return values and fetch
64
+ # the attachment that we just cloned by comparing blobs checksum, as depending
65
+ # which ActiveStorage version we're on (Rails 5.2 or 6), the `#attach` method
66
+ # doesn't always return the same, so for now we still handle the Rails 5.2 case.
67
+ def find_cloned_attachment(attachment_target, original_blob)
68
+ attachments = if attachment_target.attachments.any?
69
+ attachment_target.attachments
70
+ else
71
+ [attachment_target.attachment]
72
+ end
73
+
74
+ attachment = attachments.find do |att|
75
+ att.blob.checksum == original_blob.checksum
76
+ end
77
+ end
78
+
79
+ # This stores the source and clone resources into the deep_clone dictionary, which
80
+ # simulates what the deep_cloneable gem does when it clones a resource
81
+ #
82
+ def store_cloned(source, clone)
83
+ store_key = source.class.name.tableize.to_sym
84
+
85
+ dictionary[store_key] ||= {}
86
+ dictionary[store_key][source] = clone
87
+ end
34
88
  end
35
89
  end
36
- end
90
+ end
@@ -1,67 +1,196 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Para
2
4
  module Cloneable
5
+ # This object acts as a service to compile a nested cloneable options hash to be
6
+ # provided to the `deep_clone` method from the `deep_cloneable` gem. It iterates over
7
+ # every reflections that must be included for a given model when it's cloned, and
8
+ # creates a nested hash of :include and :except directives based on the tree that
9
+ # is created by nested `acts_as_cloneable` calls on the different models of the
10
+ # application
11
+ #
12
+ # Example :
13
+ #
14
+ # Given the following model structure :
15
+ #
16
+ # class Article < ApplicationRecord
17
+ # acts_as_cloneable :category, :comments, except: [:publication_date]
18
+ #
19
+ # belongs_to :category
20
+ # has_many :comments
21
+ # end
22
+ #
23
+ # class Category < ApplicationRecord
24
+ # acts_as_cloneable :category, except: [:articles_count]
25
+ #
26
+ # has_many :articles
27
+ # end
28
+ #
29
+ # class Comment < ApplicationRecord
30
+ # acts_as_cloneable :author
31
+ #
32
+ # belongs_to :article
33
+ # belongs_to :author
34
+ # end
35
+ #
36
+ # class Author < ApplicationRecord
37
+ # acts_as_cloneable except: [:email]
38
+ #
39
+ # has_many :articles
40
+ # end
41
+ #
42
+ # The behavior would be :
43
+ #
44
+ # Para::Cloneable::IncludeTreeBuilder.new(article).build
45
+ # # => {
46
+ # include: [:category, { comments: :author }],
47
+ # except: [:publication_date, {
48
+ # category: [:articles_count],
49
+ # comments: { author: [:email] }
50
+ # }]
51
+ # }
52
+ #
3
53
  class IncludeTreeBuilder
4
- attr_reader :resource, :cloneable_options
54
+ attr_reader :resource, :cloneable_options
5
55
 
6
- def initialize(resource, cloneable_options)
56
+ def initialize(resource)
7
57
  @resource = resource
8
- @cloneable_options = cloneable_options.deep_dup
58
+ @cloneable_options = resource.cloneable_options.deep_dup
9
59
  end
10
-
60
+
11
61
  def build
12
- include_tree = build_cloneable_tree(resource, cloneable_options[:include])
13
- cloneable_options[:include] = clean_include_tree(include_tree)
14
- cloneable_options
62
+ options_tree = build_cloneable_options_tree(resource)
63
+ exceptions = extract_exceptions_from(options_tree)
64
+ inclusions = clean_options_tree(options_tree)
65
+ cloneable_options.merge(include: inclusions, except: exceptions)
15
66
  end
16
-
67
+
17
68
  private
18
69
 
19
- def build_cloneable_tree(resource, include)
20
- include.each_with_object({}) do |reflection_name, hash|
70
+ # The cloneable options tree iterates over the resources' relations that are
71
+ # declared as included in the cloneable_options of the provided resource, and
72
+ # recursively checks included relations for its associated resources.
73
+ #
74
+ # It returns a nested hash with the included relations and their :except array
75
+ # if it exist, which include the attributes that shouldn't be duplicated when
76
+ # the resource is cloned.
77
+ #
78
+ def build_cloneable_options_tree(resource, path = [])
79
+ cloneable_options = resource.cloneable_options
80
+
81
+ # Iterate over the resource's cloneable options' :include array and recursively
82
+ # add nested included resources to its own included resources.
83
+ options = cloneable_options[:include].each_with_object({}) do |reflection_name, hash|
84
+ # This avoids cyclic dependencies issues by stopping nested association
85
+ # inclusions before the cycle starts.
86
+ #
87
+ # For example, if a post includes its author, and the author includes its posts,
88
+ # this would make the system fail with a stack level too deep error. Here this
89
+ # guard allows the inclusion to stop at :
90
+ #
91
+ # { posts: { author: { posts: { author: {}}}}}
92
+ #
93
+ # Which ensures that, using the dictionary strategy of deep_cloneable, all
94
+ # posts' authors' posts will have their author mapped to an already cloned
95
+ # author when it comes to cloning the "author" 4th level of the include tree.
96
+ #
97
+ # This is not the most optimized solution, but works well enough as if the
98
+ # author's posts match previously cloned posts, they won't be cloned as they'll
99
+ # exist in the cloned resources dictionary.
100
+ next if path.length >= 4 &&
101
+ path[-4] == path[-2] &&
102
+ path[-2] == reflection_name &&
103
+ path[-3] == path[-1]
104
+
21
105
  hash[reflection_name] = {}
22
106
 
23
- if (reflection = resource.class.reflections[reflection_name.to_s])
24
- reflection_options = hash[reflection_name]
25
- association_target = resource.send(reflection_name)
26
-
27
- if reflection.collection?
28
- association_target.each do |nested_resource|
29
- add_reflection_options(reflection_options, nested_resource)
30
- end
31
- else
32
- add_reflection_options(reflection_options, association_target)
107
+ unless (reflection = resource.class.reflections[reflection_name.to_s])
108
+ next
109
+ end
110
+
111
+ reflection_options = hash[reflection_name]
112
+ association_target = resource.send(reflection_name)
113
+
114
+ if reflection.collection?
115
+ association_target.each do |nested_resource|
116
+ add_reflection_options(
117
+ reflection_options,
118
+ nested_resource,
119
+ [*path, reflection_name]
120
+ )
33
121
  end
34
- end
122
+ else
123
+ add_reflection_options(
124
+ reflection_options,
125
+ association_target,
126
+ [*path, reflection_name]
127
+ )
128
+ end
35
129
  end
130
+
131
+ # Add the :except array from the resource to the current options hash and merge
132
+ # it if one already exist from another resource of the same class.
133
+ options[:except] ||= []
134
+ options[:except] |= Array.wrap(cloneable_options[:except])
135
+
136
+ options
36
137
  end
37
138
 
38
- def add_reflection_options(reflection_options, nested_resource)
139
+ def add_reflection_options(reflection_options, nested_resource, path)
39
140
  options = nested_resource.class.try(:cloneable_options)
40
141
  return reflection_options unless options
41
-
42
- include_options = options[:include]
43
- target_options = build_cloneable_tree(nested_resource, include_options)
142
+
143
+ target_options = build_cloneable_options_tree(nested_resource, path)
44
144
  reflection_options.deep_merge!(target_options)
45
145
  end
46
146
 
47
- def clean_include_tree(tree)
147
+ # Iterates over the generated options tree to extract all the nested :except options
148
+ # into their own separate hash, removing :except keys from the original options
149
+ # tree hash.
150
+ #
151
+ def extract_exceptions_from(tree)
152
+ exceptions = tree.delete(:except) || []
153
+ nested_exceptions = {}
154
+
155
+ tree.each do |key, value|
156
+ next unless value.is_a?(Hash) && !value.empty?
157
+
158
+ sub_exceptions = extract_exceptions_from(value)
159
+ nested_exceptions[key] = sub_exceptions unless sub_exceptions.empty?
160
+ end
161
+
162
+ exceptions += [nested_exceptions] unless nested_exceptions.empty?
163
+ exceptions
164
+ end
165
+
166
+ # Iterates over the remaining options tree hash and converts empty hash values' keys
167
+ # to be stored in an array, and returns an array of symbols and hashes that is
168
+ # compatible with what is expected as argument for the :include option of the
169
+ # `deep_clone` method.
170
+ #
171
+ # Example :
172
+ #
173
+ # clean_options_tree({ category: {}, comments: { author: {} } })
174
+ # # => [:category, { comments: [:author] }]
175
+ #
176
+ def clean_options_tree(tree)
48
177
  shallow_relations = []
49
178
  deep_relations = {}
50
-
179
+
51
180
  tree.each do |key, value|
181
+ # If the value is an empty hash, consider it as a shallow relation and add
182
+ # it to the shallow relations array
52
183
  if !value || value.empty?
53
184
  shallow_relations << key
185
+ # If the value is a hash with nested keys, process its nested values and add
186
+ # the result to the deep relations hash
54
187
  else
55
- deep_relations[key] = clean_include_tree(value)
188
+ deep_relations[key] = clean_options_tree(value)
56
189
  end
57
190
  end
58
191
 
59
- if deep_relations.empty?
60
- shallow_relations
61
- else
62
- shallow_relations + [deep_relations]
63
- end
192
+ deep_relations.empty? ? shallow_relations : shallow_relations + [deep_relations]
64
193
  end
65
194
  end
66
195
  end
67
- end
196
+ end
@@ -17,12 +17,14 @@ module Para
17
17
  # macro.
18
18
  #
19
19
  def deep_clone!(options = {})
20
- processed_options = Para::Cloneable::IncludeTreeBuilder.new(self, cloneable_options).build
20
+ dictionary = options[:dictionary] ||= {}
21
+
22
+ processed_options = Para::Cloneable::IncludeTreeBuilder.new(self).build
21
23
  options = options.reverse_merge(processed_options)
22
24
  callback = build_clone_callback(options.delete(:prepare))
23
-
25
+
24
26
  deep_clone(options) do |original, clone|
25
- Para::Cloneable::AttachmentsCloner.new(original, clone).clone!
27
+ Para::Cloneable::AttachmentsCloner.new(original, clone, dictionary).clone!
26
28
  callback&.call(original, clone)
27
29
  end
28
30
  end
@@ -63,9 +65,23 @@ module Para
63
65
  # if other sibling models don't define those relations
64
66
  options[:skip_missing_associations] = true
65
67
 
66
- self.cloneable_options = options.reverse_merge({
68
+ # If `acts_as_cloneable` is called multiple times, for example by a parent class
69
+ # and the by its subclass, ensure that we don't lose the previously defined
70
+ # cloneable options, to avoid having to repeat the same include options in each
71
+ # subclass, if it has to define subclass specific cloneable options.
72
+ previous_cloneable_options = cloneable_options || {}
73
+
74
+ # Prepare the new cloneable options hash with the provided arguments
75
+ new_cloneable_options = options.reverse_merge({
67
76
  include: args
68
77
  })
78
+
79
+ # Merges previous and new cloneable options into the cloneable_options class
80
+ # attribute, also merging the `:include` array
81
+ self.cloneable_options =
82
+ previous_cloneable_options.merge(new_cloneable_options) do |key, a, b|
83
+ a.is_a?(Array) && b.is_a?(Array) ? (a + b).uniq : b
84
+ end
69
85
  end
70
86
 
71
87
  def cloneable?
@@ -34,16 +34,24 @@ module Para
34
34
  def section_for(identifier)
35
35
  if (section = sections_cache[identifier])
36
36
  section
37
- elsif (section_id = sections_ids_hash[identifier])
38
- sections_cache[identifier] = Para::ComponentSection.find(section_id)
37
+ else
38
+ sections_cache[identifier] = if (section_id = sections_ids_hash[identifier])
39
+ Para::ComponentSection.find(section_id)
40
+ else
41
+ Para::ComponentSection.find_by(identifier: identifier)
42
+ end
39
43
  end
40
44
  end
41
45
 
42
46
  def component_for(identifier)
43
47
  if (component = components_cache[identifier])
44
48
  component
45
- elsif (component_id = components_ids_hash[identifier])
46
- components_cache[identifier] = Para::Component::Base.find(component_id)
49
+ else
50
+ components_cache[identifier] = if (component_id = components_ids_hash[identifier])
51
+ Para::Component::Base.find(component_id)
52
+ else
53
+ Para::Component::Base.find_by(identifier: identifier)
54
+ end
47
55
  end
48
56
  end
49
57
 
data/lib/para/engine.rb CHANGED
@@ -24,6 +24,7 @@ module Para
24
24
 
25
25
  initializer 'Para Cloneable' do
26
26
  ActiveSupport.on_load(:active_record) do
27
+ prepend Para::Ext::DeepCloneExtension
27
28
  include Para::Cloneable
28
29
  end
29
30
  end
@@ -0,0 +1,26 @@
1
+ require "deep_cloneable/deep_clone"
2
+
3
+ module Para
4
+ module Ext
5
+ module DeepCloneExtension
6
+ # Override the default deep_cloneable method to avoid nested :except rules that target
7
+ # polymorphic relations to try to assign default values to unexisting attributes on
8
+ # models that don't define the excluded attribute
9
+ #
10
+ # For example, we can have :
11
+ #
12
+ # { except: { comments: { author: [:confirmation_token] } } }
13
+ #
14
+ # Because some comments have a an author that's a user, and the user `acts_as_cloneable`
15
+ # macro defines `{ except: [:confirmation_token] }`, but if one of the comments has
16
+ # an anonymous user in its author relation, this method would faild with a
17
+ # ActiveModel::MissingAttributeError.
18
+ #
19
+ def dup_default_attribute_value_to(kopy, attribute, origin)
20
+ return unless kopy.attributes.keys.include?(attribute.to_s)
21
+
22
+ kopy[attribute] = origin.class.column_defaults.dup[attribute.to_s]
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/para/ext.rb CHANGED
@@ -10,6 +10,7 @@ module Para
10
10
  end
11
11
 
12
12
  require 'para/ext/paperclip'
13
+ require 'para/ext/deep_cloneable'
13
14
  require 'para/ext/active_job_status'
14
15
  require 'para/ext/active_record_nested_attributes'
15
16
  require 'para/ext/request_iframe_xhr'
@@ -76,6 +76,14 @@ module Para
76
76
  nested? && options[:parent_builder].object
77
77
  end
78
78
 
79
+ # Returns the partial name to be looked up for rendering used inside the nested
80
+ # fields partials, for the nested fields container and the remote nested fields
81
+ # partial.
82
+ #
83
+ def nested_fields_partial_name
84
+ :fields
85
+ end
86
+
79
87
  private
80
88
 
81
89
  def default_resource_name
@@ -3,7 +3,7 @@ module Para
3
3
  class NestedManyInput < NestedBaseInput
4
4
  attr_reader :resource
5
5
 
6
- def input(wrapper_options = nil)
6
+ def input(_wrapper_options = nil)
7
7
  input_html_options[:class] << "nested-many"
8
8
 
9
9
  orderable = options.fetch(:orderable, model.orderable?)
@@ -40,6 +40,8 @@ module Para
40
40
  end
41
41
  end
42
42
 
43
+ private
44
+
43
45
  def parent_model
44
46
  @parent_model ||= @builder.object.class
45
47
  end
data/lib/para/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Para
2
- VERSION = '0.9.0'
4
+ VERSION = '0.9.3.3'
3
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: para
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Valentin Ballestrino
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-14 00:00:00.000000000 Z
11
+ date: 2021-12-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -766,6 +766,7 @@ files:
766
766
  - lib/para/ext.rb
767
767
  - lib/para/ext/active_job_status.rb
768
768
  - lib/para/ext/active_record_nested_attributes.rb
769
+ - lib/para/ext/deep_cloneable.rb
769
770
  - lib/para/ext/paperclip.rb
770
771
  - lib/para/ext/request_iframe_xhr.rb
771
772
  - lib/para/form_builder.rb