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