para 0.9.3.1 → 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: 39e971f0c36fc8256afd9883b9865af8be4628934c8aad4d998aa0efc47173a5
4
- data.tar.gz: d0cb9cc08791c12f92ad514714d05782d643fa167511215ac53ddb765c030b50
3
+ metadata.gz: 6deb45940599a543b4257463561e645256df1db47b9cef62f1d287b97e841abd
4
+ data.tar.gz: a37ab4aca7ab0c23baf41a65083c84bdbdb6d6fe12437621b08f9a4f49baf22d
5
5
  SHA512:
6
- metadata.gz: cdb7af1bcc3fa32b75acc7e0212f02d612037f90ef06c6b7c988002e376582cd620099ffc3267c49e70488b33c6da36d80b5bb75d0557c870a98d59490e5d40a
7
- data.tar.gz: bddb7765e1d09f4bce763884fffbd1d9815c346cb028dc227f26287060724f9ba5ad4ed9d0bdbc682fa79b8aa07952dec5c1ec45234a902631fbe50729d17531
6
+ metadata.gz: 229be291a706963be79ef902fb99fdedad27092a62f8c855d912b57c21d10f94ed8e9ffd254a9bb1f29ae465ab6ec1cbe3753af53096778ce6131bdca83ef3b3
7
+ data.tar.gz: 10e5675bb29448fd949112632bda77b2dc897d7ca3b2d2dd8dfbac4ef47a19d8ddf629aa6ef40cea98dbfed05bfc716afe8fe06b1c57b950e2af8b541f859672
@@ -21,9 +21,9 @@ class Para.NestedManyField
21
21
  $(el).find('.resource-position-field').val(i)
22
22
 
23
23
  initializeCocoon: ->
24
- @$fieldsList.on 'cocoon:after-insert', @afterInsertField
25
- @$fieldsList.on 'cocoon:before-remove', @beforeRemoveField
26
- @$fieldsList.on 'cocoon:after-remove', @afterRemoveField
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
28
  stoppingPropagation: (callback) =>
29
29
  (e, args...) =>
@@ -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,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'
data/lib/para/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Para
2
- VERSION = '0.9.3.1'
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.9.3.1
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: 2021-03-08 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