para 0.8.15 → 0.9.3.2

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: eaabbc24f0c6d28e69cf3ebc0d837ffb52dc6abd1286a2ab6c23b0c8767e7a9d
4
- data.tar.gz: 5f7e0609618d87d13d27b8132c2a6a7aa6a46e365c4dc9c7ac1a687f843ceca7
3
+ metadata.gz: 6deb45940599a543b4257463561e645256df1db47b9cef62f1d287b97e841abd
4
+ data.tar.gz: a37ab4aca7ab0c23baf41a65083c84bdbdb6d6fe12437621b08f9a4f49baf22d
5
5
  SHA512:
6
- metadata.gz: 2d16ba804ed07daac026531e17c0f669a2cada924db5a22d87bff4b44018c3720dcf2177600bd6133dcba9c6da97a2a0a0253e3fbe6111ae24036f12596010be
7
- data.tar.gz: 31cf72e15c979a0b24f1aa18cb736b9d1c56b4af88057fe730b5375dd3b8e7fe4bbd034f5f38232ffb900cc6b591480dacfdff4742fdd36f23d3c56a14a116c6
6
+ metadata.gz: 229be291a706963be79ef902fb99fdedad27092a62f8c855d912b57c21d10f94ed8e9ffd254a9bb1f29ae465ab6ec1cbe3753af53096778ce6131bdca83ef3b3
7
+ data.tar.gz: 10e5675bb29448fd949112632bda77b2dc897d7ca3b2d2dd8dfbac4ef47a19d8ddf629aa6ef40cea98dbfed05bfc716afe8fe06b1c57b950e2af8b541f859672
@@ -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,29 +1,91 @@
1
1
  module Para
2
2
  module Cloneable
3
+ # This object acts as a service to compile a nested cloneable options hash to be
4
+ # provided to the `deep_clone` method from the `deep_cloneable` gem. It iterates over
5
+ # every reflections that must be included for a given model when it's cloned, and
6
+ # creates a nested hash of :include and :except directives based on the tree that
7
+ # is created by nested `acts_as_cloneable` calls on the different models of the
8
+ # application
9
+ #
10
+ # Example :
11
+ #
12
+ # Given the following model structure :
13
+ #
14
+ # class Article < ApplicationRecord
15
+ # acts_as_cloneable :category, :comments, except: [:publication_date]
16
+ #
17
+ # belongs_to :category
18
+ # has_many :comments
19
+ # end
20
+ #
21
+ # class Category < ApplicationRecord
22
+ # acts_as_cloneable :category, except: [:articles_count]
23
+ #
24
+ # has_many :articles
25
+ # end
26
+ #
27
+ # class Comment < ApplicationRecord
28
+ # acts_as_cloneable :author
29
+ #
30
+ # belongs_to :article
31
+ # belongs_to :author
32
+ # end
33
+ #
34
+ # class Author < ApplicationRecord
35
+ # acts_as_cloneable except: [:email]
36
+ #
37
+ # has_many :articles
38
+ # end
39
+ #
40
+ # The behavior would be :
41
+ #
42
+ # Para::Cloneable::IncludeTreeBuilder.new(article).build
43
+ # # => {
44
+ # include: [:category, { comments: :author }],
45
+ # except: [:publication_date, {
46
+ # category: [:articles_count],
47
+ # comments: { author: [:email] }
48
+ # }]
49
+ # }
50
+ #
3
51
  class IncludeTreeBuilder
4
52
  attr_reader :resource, :cloneable_options
5
53
 
6
- def initialize(resource, cloneable_options)
54
+ def initialize(resource)
7
55
  @resource = resource
8
- @cloneable_options = cloneable_options.deep_dup
56
+ @cloneable_options = resource.cloneable_options.deep_dup
9
57
  end
10
-
58
+
11
59
  def build
12
- include_tree = build_cloneable_tree(resource, cloneable_options[:include])
13
- cloneable_options[:include] = clean_include_tree(include_tree)
14
- cloneable_options
60
+ options_tree = build_cloneable_options_tree(resource)
61
+ exceptions = extract_exceptions_from(options_tree)
62
+ inclusions = clean_options_tree(options_tree)
63
+ cloneable_options.merge(include: inclusions, except: exceptions)
15
64
  end
16
-
65
+
17
66
  private
18
67
 
19
- def build_cloneable_tree(resource, include)
20
- include.each_with_object({}) do |reflection_name, hash|
68
+ # The cloneable options tree iterates over the resources' relations that are
69
+ # declared as included in the cloneable_options of the provided resource, and
70
+ # recursively checks included relations for its associated resources.
71
+ #
72
+ # It returns a nested hash with the included relations and their :except array
73
+ # if it exist, which include the attributes that shouldn't be duplicated when
74
+ # the resource is cloned.
75
+ #
76
+ def build_cloneable_options_tree(resource)
77
+ cloneable_options = resource.cloneable_options
78
+ options = {}
79
+
80
+ # Iterate over the resource's cloneable options' :include array and recursively
81
+ # add nested included resources to its own included resources.
82
+ options = cloneable_options[:include].each_with_object({}) do |reflection_name, hash|
21
83
  hash[reflection_name] = {}
22
84
 
23
85
  if (reflection = resource.class.reflections[reflection_name.to_s])
24
86
  reflection_options = hash[reflection_name]
25
87
  association_target = resource.send(reflection_name)
26
-
88
+
27
89
  if reflection.collection?
28
90
  association_target.each do |nested_resource|
29
91
  add_reflection_options(reflection_options, nested_resource)
@@ -31,37 +93,72 @@ module Para
31
93
  else
32
94
  add_reflection_options(reflection_options, association_target)
33
95
  end
34
- end
96
+ end
35
97
  end
98
+
99
+ # Add the :except array from the resource to the current options hash and merge
100
+ # it if one already exist from another resource of the same class.
101
+ options[:except] ||= []
102
+ options[:except] |= Array.wrap(cloneable_options[:except])
103
+
104
+ options
36
105
  end
37
106
 
38
107
  def add_reflection_options(reflection_options, nested_resource)
39
108
  options = nested_resource.class.try(:cloneable_options)
40
109
  return reflection_options unless options
41
-
42
- include_options = options[:include]
43
- target_options = build_cloneable_tree(nested_resource, include_options)
110
+
111
+ target_options = build_cloneable_options_tree(nested_resource)
44
112
  reflection_options.deep_merge!(target_options)
45
113
  end
46
114
 
47
- def clean_include_tree(tree)
115
+ # Iterates over the generated options tree to extract all the nested :except options
116
+ # into their own separate hash, removing :except keys from the original options
117
+ # tree hash.
118
+ #
119
+ def extract_exceptions_from(tree)
120
+ exceptions = tree.delete(:except) || []
121
+ nested_exceptions = {}
122
+
123
+ tree.each do |key, value|
124
+ next unless value.is_a?(Hash) && !value.empty?
125
+
126
+ sub_exceptions = extract_exceptions_from(value)
127
+ nested_exceptions[key] = sub_exceptions unless sub_exceptions.empty?
128
+ end
129
+
130
+ exceptions += [nested_exceptions] unless nested_exceptions.empty?
131
+ exceptions
132
+ end
133
+
134
+ # Iterates over the remaining options tree hash and converts empty hash values' keys
135
+ # to be stored in an array, and returns an array of symbols and hashes that is
136
+ # compatible with what is expected as argument for the :include option of the
137
+ # `deep_clone` method.
138
+ #
139
+ # Example :
140
+ #
141
+ # clean_options_tree({ category: {}, comments: { author: {} } })
142
+ # # => [:category, { comments: [:author] }]
143
+ #
144
+ def clean_options_tree(tree)
48
145
  shallow_relations = []
49
146
  deep_relations = {}
50
-
147
+
51
148
  tree.each do |key, value|
149
+ # If the value is an empty hash, consider it as a shallow relation and add
150
+ # it to the shallow relations array
52
151
  if !value || value.empty?
53
152
  shallow_relations << key
153
+ # If the value is a hash with nested keys, process its nested values and add
154
+ # the result to the deep relations hash
54
155
  else
55
- deep_relations[key] = clean_include_tree(value)
156
+ deep_relations[key] = clean_options_tree(value)
56
157
  end
57
158
  end
58
159
 
59
- if deep_relations.empty?
60
- shallow_relations
61
- else
62
- shallow_relations + [deep_relations]
63
- end
160
+ deep_relations.empty? ? shallow_relations : shallow_relations + [deep_relations]
64
161
  end
65
162
  end
66
163
  end
67
- end
164
+ 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.8.15'
4
+ VERSION = '0.9.3.2'
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.8.15
4
+ version: 0.9.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Valentin Ballestrino
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-22 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
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: '4.0'
20
20
  - - "<="
21
21
  - !ruby/object:Gem::Version
22
- version: '6.0'
22
+ version: '7.0'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: '4.0'
30
30
  - - "<="
31
31
  - !ruby/object:Gem::Version
32
- version: '6.0'
32
+ version: '7.0'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: rails-i18n
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -152,16 +152,22 @@ dependencies:
152
152
  name: paperclip
153
153
  requirement: !ruby/object:Gem::Requirement
154
154
  requirements:
155
- - - "~>"
155
+ - - ">="
156
156
  - !ruby/object:Gem::Version
157
157
  version: '4.2'
158
+ - - "<"
159
+ - !ruby/object:Gem::Version
160
+ version: '7.0'
158
161
  type: :runtime
159
162
  prerelease: false
160
163
  version_requirements: !ruby/object:Gem::Requirement
161
164
  requirements:
162
- - - "~>"
165
+ - - ">="
163
166
  - !ruby/object:Gem::Version
164
167
  version: '4.2'
168
+ - - "<"
169
+ - !ruby/object:Gem::Version
170
+ version: '7.0'
165
171
  - !ruby/object:Gem::Dependency
166
172
  name: cancancan
167
173
  requirement: !ruby/object:Gem::Requirement
@@ -334,16 +340,22 @@ dependencies:
334
340
  name: bootstrap-sass
335
341
  requirement: !ruby/object:Gem::Requirement
336
342
  requirements:
337
- - - "~>"
343
+ - - ">="
344
+ - !ruby/object:Gem::Version
345
+ version: '3.3'
346
+ - - "<"
338
347
  - !ruby/object:Gem::Version
339
- version: 3.3.0
348
+ version: '3.5'
340
349
  type: :runtime
341
350
  prerelease: false
342
351
  version_requirements: !ruby/object:Gem::Requirement
343
352
  requirements:
344
- - - "~>"
353
+ - - ">="
354
+ - !ruby/object:Gem::Version
355
+ version: '3.3'
356
+ - - "<"
345
357
  - !ruby/object:Gem::Version
346
- version: 3.3.0
358
+ version: '3.5'
347
359
  - !ruby/object:Gem::Dependency
348
360
  name: font-awesome-rails
349
361
  requirement: !ruby/object:Gem::Requirement
@@ -754,6 +766,7 @@ files:
754
766
  - lib/para/ext.rb
755
767
  - lib/para/ext/active_job_status.rb
756
768
  - lib/para/ext/active_record_nested_attributes.rb
769
+ - lib/para/ext/deep_cloneable.rb
757
770
  - lib/para/ext/paperclip.rb
758
771
  - lib/para/ext/request_iframe_xhr.rb
759
772
  - lib/para/form_builder.rb