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 +4 -4
- data/app/assets/javascripts/para/inputs/nested_many.coffee +3 -3
- data/app/views/para/admin/resources/_navigation.html.haml +6 -3
- data/lib/para/cloneable/attachments_cloner.rb +74 -20
- data/lib/para/cloneable/include_tree_builder.rb +120 -23
- data/lib/para/cloneable.rb +20 -4
- data/lib/para/components_configuration.rb +12 -4
- data/lib/para/engine.rb +1 -0
- data/lib/para/ext/deep_cloneable.rb +26 -0
- data/lib/para/ext.rb +1 -0
- data/lib/para/version.rb +3 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6deb45940599a543b4257463561e645256df1db47b9cef62f1d287b97e841abd
|
4
|
+
data.tar.gz: a37ab4aca7ab0c23baf41a65083c84bdbdb6d6fe12437621b08f9a4f49baf22d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
4
|
-
|
5
|
-
= parent_component.
|
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
|
-
|
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
|
15
|
-
|
16
|
-
|
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
|
-
|
21
|
-
next unless
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
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
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
20
|
-
|
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
|
-
|
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
|
-
|
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] =
|
156
|
+
deep_relations[key] = clean_options_tree(value)
|
56
157
|
end
|
57
158
|
end
|
58
159
|
|
59
|
-
|
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
|
data/lib/para/cloneable.rb
CHANGED
@@ -17,12 +17,14 @@ module Para
|
|
17
17
|
# macro.
|
18
18
|
#
|
19
19
|
def deep_clone!(options = {})
|
20
|
-
|
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
|
-
|
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
|
-
|
38
|
-
sections_cache[identifier] =
|
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
|
-
|
46
|
-
components_cache[identifier] =
|
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
@@ -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
data/lib/para/version.rb
CHANGED
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.
|
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-
|
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
|