para 0.9.3.1 → 0.9.3.2

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: 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