para 0.9.0 → 0.9.3.3

Sign up to get free protection for your applications and to get access to all the features.
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