releaf-content 1.0.8 → 1.0.9
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/services/releaf/content/node/copy.rb +51 -33
- data/lib/releaf-content.rb +1 -0
- data/spec/features/nodes_services_spec.rb +184 -1
- data/spec/fixtures/dummy.png +0 -0
- data/spec/services/releaf/content/node/copy_spec.rb +283 -67
- metadata +21 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a3bb38f60b14152aac0b00614ff0d3ba1558b3c
|
4
|
+
data.tar.gz: cad1145449af7968692665a2804302f5b66384d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7053c97279c704c480cf8eb46f66dbf94611dd34d50934cf78a1cc92aba8b2b69918c422115ecdcbdc8d6a6ba161254b4567ebf7b477010b9bbdedeb3019c795
|
7
|
+
data.tar.gz: d448e9e2920b641d3dc12ad41354e5f5469c088b5c0f3eda854d3710bf4e03e2b0b60aa909cce6ac1bfdc6a764fbc85aa03552bd314f977f1ed234499bef3246
|
@@ -11,6 +11,7 @@ module Releaf
|
|
11
11
|
node.class.transaction do
|
12
12
|
new_node = make_copy
|
13
13
|
end
|
14
|
+
|
14
15
|
new_node
|
15
16
|
rescue ActiveRecord::RecordInvalid
|
16
17
|
add_error_and_raise 'descendant invalid'
|
@@ -24,6 +25,16 @@ module Releaf
|
|
24
25
|
add_error_and_raise("source or descendant node can't be parent of new node")
|
25
26
|
end
|
26
27
|
|
28
|
+
def make_copy
|
29
|
+
new_node = duplicate_under
|
30
|
+
|
31
|
+
node.children.each do |child|
|
32
|
+
self.class.new(node: child, parent_id: new_node.id).make_copy
|
33
|
+
end
|
34
|
+
|
35
|
+
new_node
|
36
|
+
end
|
37
|
+
|
27
38
|
def duplicate_under
|
28
39
|
new_node = nil
|
29
40
|
node.class.transaction do
|
@@ -34,57 +45,64 @@ module Releaf
|
|
34
45
|
Releaf::Content::Node::SaveUnderParent.call(node: new_node, parent_id: parent_id)
|
35
46
|
end
|
36
47
|
end
|
48
|
+
|
37
49
|
new_node
|
38
50
|
end
|
39
51
|
|
40
52
|
def duplicate_content
|
41
|
-
|
53
|
+
if node.content.present?
|
54
|
+
new_content = duplicate_object(node.content)
|
55
|
+
new_content.save!
|
56
|
+
new_content
|
57
|
+
else
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
42
61
|
|
43
|
-
|
44
|
-
|
62
|
+
def duplicate_object object
|
63
|
+
object.deep_clone include: duplicatable_associations(object.class) do |original, copy|
|
64
|
+
supplement_object_duplication(original, copy)
|
65
|
+
end
|
66
|
+
end
|
45
67
|
|
46
|
-
|
47
|
-
|
68
|
+
def supplement_object_duplication(original, copy)
|
69
|
+
duplicate_dragonfly_attachments(original, copy)
|
48
70
|
end
|
49
71
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
72
|
+
def duplicatable_associations(owner_class)
|
73
|
+
Releaf::ResourceBase.new(owner_class).associations.collect do |association|
|
74
|
+
{ association.name => duplicatable_associations(association.klass) }
|
75
|
+
end
|
53
76
|
end
|
54
77
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
78
|
+
def duplicate_dragonfly_attachments(original, copy)
|
79
|
+
attachment_keys = original.dragonfly_attachments.keys
|
80
|
+
return unless attachment_keys.present?
|
81
|
+
|
82
|
+
# during the dup() call the copy object has its dragonfly_attachments property duplicated from the original.
|
83
|
+
# here it gets set to nil to force its reinitialization on next access
|
84
|
+
copy.instance_variable_set(:@dragonfly_attachments, nil)
|
85
|
+
|
86
|
+
# all uids must be cleared in a separate loop before accessing any of the attachments
|
87
|
+
# (accessing any of the accessors regenerates the dragonfly_attachments collection and loads all uids)
|
88
|
+
attachment_keys.each { |accessor| copy.send("#{accessor}_uid=", nil) }
|
59
89
|
|
60
|
-
|
90
|
+
# once all uids have been reset, each attachment may be reassigned from the original.
|
91
|
+
# reassignment forces dragonfly to internally treat the new attachment as a copy
|
92
|
+
attachment_keys.each do |accessor|
|
93
|
+
attachment = original.send(accessor)
|
94
|
+
|
95
|
+
if attachment.present?
|
61
96
|
begin
|
62
|
-
|
97
|
+
attachment.path # verify that the file exists
|
63
98
|
rescue Dragonfly::Job::Fetch::NotFound
|
64
|
-
|
99
|
+
attachment = nil
|
65
100
|
end
|
66
101
|
end
|
67
102
|
|
68
|
-
|
69
|
-
new_content.send("#{accessor_name}=", dragonfly_attachment)
|
103
|
+
copy.send("#{accessor}=", attachment)
|
70
104
|
end
|
71
105
|
end
|
72
|
-
|
73
|
-
def content_dragonfly_attributes
|
74
|
-
node.content.class.attribute_names.select do |attribute_name|
|
75
|
-
Releaf::Builders::Utilities::ResolveAttributeFieldMethodName.new(object: node.content, attribute_name: attribute_name).file?
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def make_copy
|
80
|
-
new_node = duplicate_under
|
81
|
-
|
82
|
-
node.children.each do |child|
|
83
|
-
self.class.new(node: child, parent_id: new_node.id).make_copy
|
84
|
-
end
|
85
|
-
|
86
|
-
new_node
|
87
|
-
end
|
88
106
|
end
|
89
107
|
end
|
90
108
|
end
|
data/lib/releaf-content.rb
CHANGED
@@ -64,7 +64,7 @@ describe "Nodes services (copy, move)" do
|
|
64
64
|
|
65
65
|
end
|
66
66
|
|
67
|
-
describe "
|
67
|
+
describe "Basic node copying", create_nodes: true do
|
68
68
|
before create_nodes: true do
|
69
69
|
@home_page_node = create(:home_page_node, locale: "lv")
|
70
70
|
@home_page_node_2 = create(:home_page_node, locale: "en")
|
@@ -204,4 +204,187 @@ describe "Nodes services (copy, move)" do
|
|
204
204
|
end
|
205
205
|
end
|
206
206
|
end
|
207
|
+
|
208
|
+
|
209
|
+
feature "Deep copying of content nodes", js: true do
|
210
|
+
|
211
|
+
let(:node_class) { Node }
|
212
|
+
let(:locale) { "en" }
|
213
|
+
|
214
|
+
background do
|
215
|
+
Rails.cache.clear
|
216
|
+
# preload ActsAsNode classes
|
217
|
+
Rails.application.eager_load!
|
218
|
+
|
219
|
+
create(:home_page_node, name: "Root page", locale: "en", slug: "en")
|
220
|
+
auth_as_user
|
221
|
+
end
|
222
|
+
|
223
|
+
def expect_different_values original, copy
|
224
|
+
expect(original).to be_present
|
225
|
+
expect(copy).to be_present
|
226
|
+
expect(original).to_not eq copy
|
227
|
+
end
|
228
|
+
|
229
|
+
scenario "Deep copying of content nodes with nested associations and files" do
|
230
|
+
visit "/admin/nodes/"
|
231
|
+
open_toolbox("Add child", node_class.first, ".view-index .collection li")
|
232
|
+
|
233
|
+
dummy_file_path = File.expand_path('../fixtures/dummy.png', __dir__)
|
234
|
+
|
235
|
+
within('.dialog.content-type.initialized') do
|
236
|
+
click_link "Banner page"
|
237
|
+
end
|
238
|
+
|
239
|
+
expect(page).to have_css('input[type="text"][name="resource[content_type]"][value="Banner page"]')
|
240
|
+
|
241
|
+
fill_in 'Name', with: "Banner page"
|
242
|
+
fill_in 'Slug', with: "banner-page"
|
243
|
+
|
244
|
+
attach_file('Top banner', dummy_file_path)
|
245
|
+
attach_file('Bottom banner', dummy_file_path)
|
246
|
+
|
247
|
+
within('section.nested[data-name="banner_groups"]') do
|
248
|
+
add_nested_item "banner_groups", 0 do
|
249
|
+
fill_in "Title", with: "Banner group 1 title"
|
250
|
+
|
251
|
+
within('section.nested[data-name="banners"]') do
|
252
|
+
add_nested_item "banners", 0 do
|
253
|
+
attach_file('Image', dummy_file_path)
|
254
|
+
fill_in "Url", with: "Banner-1-url"
|
255
|
+
end
|
256
|
+
add_nested_item "banners", 1 do
|
257
|
+
attach_file('Image', dummy_file_path)
|
258
|
+
fill_in "Url", with: "Banner-2-url"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
add_nested_item "banner_groups", 1 do
|
264
|
+
fill_in "Title", with: "Banner group 2 title"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
wait_for_all_richtexts
|
269
|
+
save_and_check_response "Create succeeded"
|
270
|
+
|
271
|
+
open_toolbox("Copy")
|
272
|
+
|
273
|
+
within('.dialog.copy.initialized') do
|
274
|
+
find('label', text: "Root page").click
|
275
|
+
click_button "Copy"
|
276
|
+
end
|
277
|
+
|
278
|
+
expect(page).to have_css('body > .notifications .notification[data-id="resource_status"][data-type="success"]', text: "Copy succeeded")
|
279
|
+
|
280
|
+
# verify record count in db
|
281
|
+
expect(node_class.where(content_type: 'BannerPage').count).to eq 2
|
282
|
+
expect(BannerGroup.count).to eq 4
|
283
|
+
expect(Banner.count).to eq 4
|
284
|
+
|
285
|
+
|
286
|
+
# verify that direct and nested files are duplicated correctly
|
287
|
+
[
|
288
|
+
{ original: BannerPage.first, copy: BannerPage.last, accessor: :top_banner },
|
289
|
+
{ original: BannerPage.first, copy: BannerPage.last, accessor: :bottom_banner },
|
290
|
+
{
|
291
|
+
original: BannerPage.first.banner_groups.first.banners.first,
|
292
|
+
copy: BannerPage.last.banner_groups.first.banners.first,
|
293
|
+
accessor: :image
|
294
|
+
}
|
295
|
+
].each do |entry|
|
296
|
+
original = entry[:original]
|
297
|
+
copy = entry[:copy]
|
298
|
+
accessor = entry[:accessor]
|
299
|
+
expect(original.id).to_not eq copy.id
|
300
|
+
|
301
|
+
# verify that uids exist and are different
|
302
|
+
expect_different_values(original.send("#{accessor}_uid"), copy.send("#{accessor}_uid"))
|
303
|
+
|
304
|
+
# verify files exist and are different
|
305
|
+
original_file = original.send(accessor)
|
306
|
+
copied_file = copy.send(accessor)
|
307
|
+
|
308
|
+
expect_different_values(original_file.path, copied_file.path)
|
309
|
+
expect(original_file.path).to start_with Dragonfly.app.datastore.root_path
|
310
|
+
expect(copied_file.path).to start_with Dragonfly.app.datastore.root_path
|
311
|
+
expect(File.exist?(original_file.path)).to be true
|
312
|
+
expect(File.exist?(copied_file.path)).to be true
|
313
|
+
end
|
314
|
+
|
315
|
+
|
316
|
+
# change values in the copied node to make sure associations are not linked
|
317
|
+
original_id = node_class.where(content_type: "BannerPage").order(:item_position).first.id
|
318
|
+
copy_id = node_class.where(content_type: "BannerPage").order(:item_position).last.id
|
319
|
+
expect(copy_id).to_not eq original_id
|
320
|
+
|
321
|
+
visit "/admin/nodes/#{copy_id}/edit/"
|
322
|
+
|
323
|
+
within('section.nested[data-name="banner_groups"]') do
|
324
|
+
within('.item[data-name="banner_groups"][data-index="0"]') do
|
325
|
+
expect(page).to have_field('Title', with: 'Banner group 1 title')
|
326
|
+
fill_in "Title", with: "Copied banner group 1 title"
|
327
|
+
|
328
|
+
within('.item[data-name="banners"][data-index="0"]') do
|
329
|
+
expect(page).to have_field('Url', with: 'Banner-1-url')
|
330
|
+
fill_in "Url", with: "Copied-banner-1-url"
|
331
|
+
end
|
332
|
+
within('.item[data-name="banners"][data-index="1"]') do
|
333
|
+
expect(page).to have_field('Url', with: 'Banner-2-url')
|
334
|
+
fill_in "Url", with: "Copied-banner-2-url"
|
335
|
+
end
|
336
|
+
end
|
337
|
+
within('.item[data-name="banner_groups"][data-index="1"]') do
|
338
|
+
expect(page).to have_field('Title', with: 'Banner group 2 title')
|
339
|
+
fill_in "Title", with: "Copied banner group 2 title"
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
copied_file_urls = {
|
344
|
+
top_banner: find('.field[data-name="top_banner"]').find_link[:href],
|
345
|
+
bottom_banner: find('.field[data-name="bottom_banner"]').find_link[:href],
|
346
|
+
nested_banner: find('.item[data-name="banner_groups"][data-index="0"] .item[data-name="banners"][data-index="0"] .field[data-name="image"]').find_link[:href]
|
347
|
+
}
|
348
|
+
|
349
|
+
wait_for_all_richtexts
|
350
|
+
save_and_check_response "Update succeeded"
|
351
|
+
|
352
|
+
# verify that the original banner page still has the old values
|
353
|
+
visit "/admin/nodes/#{original_id}/edit/"
|
354
|
+
|
355
|
+
within('section.nested[data-name="banner_groups"]') do
|
356
|
+
within('.item[data-name="banner_groups"][data-index="0"]') do
|
357
|
+
expect(page).to have_field('Title', with: 'Banner group 1 title')
|
358
|
+
|
359
|
+
within('.item[data-name="banners"][data-index="0"]') do
|
360
|
+
expect(page).to have_field('Url', with: 'Banner-1-url')
|
361
|
+
end
|
362
|
+
within('.item[data-name="banners"][data-index="1"]') do
|
363
|
+
expect(page).to have_field('Url', with: 'Banner-2-url')
|
364
|
+
end
|
365
|
+
end
|
366
|
+
within('.item[data-name="banner_groups"][data-index="1"]') do
|
367
|
+
expect(page).to have_field('Title', with: 'Banner group 2 title')
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
original_file_urls = {
|
372
|
+
top_banner: find('.field[data-name="top_banner"]').find_link[:href],
|
373
|
+
bottom_banner: find('.field[data-name="bottom_banner"]').find_link[:href],
|
374
|
+
nested_banner: find('.item[data-name="banner_groups"][data-index="0"] .item[data-name="banners"][data-index="0"] .field[data-name="image"]').find_link[:href]
|
375
|
+
}
|
376
|
+
|
377
|
+
# make sure that original and copied file urls are different and working
|
378
|
+
original_file_urls.each do |key, original_url|
|
379
|
+
expect_different_values(original_url, copied_file_urls[key])
|
380
|
+
|
381
|
+
[original_url, copied_file_urls[key]].each do |file_url|
|
382
|
+
visit file_url
|
383
|
+
expect(page.status_code).to eq 200
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
end
|
388
|
+
|
389
|
+
end
|
207
390
|
end
|
Binary file
|
@@ -1,90 +1,59 @@
|
|
1
1
|
require "rails_helper"
|
2
2
|
|
3
3
|
describe Releaf::Content::Node::Copy do
|
4
|
-
class DummyNodeServiceIncluder
|
5
|
-
include Releaf::Content::Node::Service
|
6
|
-
end
|
7
4
|
|
8
|
-
let(:
|
9
|
-
subject{ described_class.new(node: node, parent_id: 12) }
|
5
|
+
let(:root_node) { create(:home_page_node) }
|
10
6
|
|
11
|
-
|
12
|
-
|
7
|
+
let(:node) { create(node_factory, parent: root_node) }
|
8
|
+
let(:node_factory) { :text_page_node }
|
13
9
|
|
14
|
-
|
15
|
-
|
16
|
-
expect( subject.duplicate_content ).to be nil
|
17
|
-
end
|
18
|
-
end
|
10
|
+
let(:content_factory) { :text_page }
|
11
|
+
let(:content_attributes) { { } }
|
19
12
|
|
20
|
-
|
21
|
-
|
13
|
+
let(:complex_node_factory) { :banner_page_node }
|
14
|
+
let(:complex_content_factory) { :banner_page }
|
15
|
+
let(:complex_content_duplicatable_associations) {
|
16
|
+
[
|
17
|
+
{
|
18
|
+
banner_groups: [
|
19
|
+
{ banners: [] }
|
20
|
+
]
|
21
|
+
}
|
22
|
+
] }
|
22
23
|
|
23
|
-
|
24
|
-
content = HomePage.new
|
25
|
-
expect( content ).to receive(:save!)
|
26
|
-
expect( node.content.class ).to receive(:new).and_return(content)
|
27
|
-
expect( subject.duplicate_content ).to eq content
|
28
|
-
end
|
24
|
+
let(:file) { File.new(File.expand_path('../../../../fixtures/dummy.png', __dir__)) }
|
29
25
|
|
30
|
-
|
31
|
-
content = HomePage.new
|
32
|
-
expect( content ).to receive(:save!)
|
33
|
-
allow( node.content.class ).to receive(:new).and_return(content)
|
34
|
-
expect( subject ).to receive(:duplicate_content_dragonfly_attributes).with(content)
|
35
|
-
expect( subject.duplicate_content ).to eq content
|
36
|
-
end
|
26
|
+
let(:original) { create(content_factory, content_attributes) }
|
37
27
|
|
38
|
-
it "doesn't return same as original content" do
|
39
|
-
result = subject.duplicate_content
|
40
|
-
expect( result ).to be_an_instance_of HomePage
|
41
|
-
expect( result.id ).to_not eq node.content_id
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
28
|
|
46
|
-
describe "#
|
47
|
-
|
48
|
-
|
29
|
+
describe "#make_copy" do
|
30
|
+
# this is a double check to verify file duplication by doing the full copying process.
|
31
|
+
# #duplicate_dragonfly_attachments is tested separately in more detail below
|
32
|
+
subject { described_class.new(node: node, parent_id: root_node.id ) }
|
49
33
|
|
50
|
-
|
51
|
-
|
52
|
-
end
|
53
|
-
end
|
34
|
+
context "when node content has dragonfly file fields" do
|
35
|
+
let(:node_factory) { complex_node_factory }
|
54
36
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
new_content = HomePage.new()
|
60
|
-
node.content = old_content
|
61
|
-
|
62
|
-
expect(new_content).to receive(:banner=).with(old_content.banner)
|
63
|
-
expect(new_content).to receive(:banner_uid=).with(nil)
|
64
|
-
subject.duplicate_content_dragonfly_attributes(new_content)
|
65
|
-
end
|
66
|
-
end
|
37
|
+
it "duplicates dragonfly attachments, storing them as separate files that are no longer linked" do
|
38
|
+
node.content.top_banner = file
|
39
|
+
node.save!
|
40
|
+
copy = subject.make_copy
|
67
41
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
node.content = old_content
|
42
|
+
# make sure the uids are non-empty and different
|
43
|
+
expect(node.content.top_banner_uid).to be_present
|
44
|
+
expect(copy.content.top_banner_uid).to be_present
|
45
|
+
expect(copy.content.top_banner_uid).to_not eq node.content.top_banner_uid
|
73
46
|
|
74
|
-
|
75
|
-
expect(
|
76
|
-
|
47
|
+
# make sure the files are stored and exist
|
48
|
+
expect(copy.content.top_banner.path).to start_with Dragonfly.app.datastore.root_path
|
49
|
+
expect(node.content.top_banner.path).to start_with Dragonfly.app.datastore.root_path
|
50
|
+
expect(copy.content.top_banner.path).to_not eq node.content.top_banner.path
|
77
51
|
end
|
78
52
|
end
|
79
|
-
end
|
80
53
|
|
81
|
-
describe "#content_dragonfly_attributes" do
|
82
|
-
it "returns array of node content object dragonfly attributes" do
|
83
|
-
node.content = HomePage.new
|
84
|
-
expect(subject.content_dragonfly_attributes).to eq(["banner_uid"])
|
85
|
-
end
|
86
54
|
end
|
87
55
|
|
56
|
+
|
88
57
|
describe "#duplicate_under" do
|
89
58
|
let!(:source_node) { create(:node, locale: "lv") }
|
90
59
|
let!(:target_node) { create(:node, locale: "en") }
|
@@ -112,4 +81,251 @@ describe Releaf::Content::Node::Copy do
|
|
112
81
|
subject.duplicate_under
|
113
82
|
end
|
114
83
|
end
|
84
|
+
|
85
|
+
|
86
|
+
describe "#duplicate_content" do
|
87
|
+
|
88
|
+
let(:content) { node.content }
|
89
|
+
|
90
|
+
before do
|
91
|
+
allow(subject).to receive(:node).and_return(node)
|
92
|
+
end
|
93
|
+
|
94
|
+
context "when node has a content object" do
|
95
|
+
|
96
|
+
it "calls #duplicate_object with current content" do
|
97
|
+
expect(subject).to receive(:duplicate_object).with(content).and_return(content)
|
98
|
+
subject.duplicate_content
|
99
|
+
end
|
100
|
+
|
101
|
+
it "saves the duplicated result" do
|
102
|
+
new_content = subject.duplicate_content
|
103
|
+
expect(new_content.new_record?).to be false
|
104
|
+
end
|
105
|
+
|
106
|
+
it "returns the newly saved content" do
|
107
|
+
new_content = subject.duplicate_content
|
108
|
+
expect(new_content.id).to_not eq content.id
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when node does not have a content object" do
|
113
|
+
before do
|
114
|
+
node.content = nil
|
115
|
+
end
|
116
|
+
|
117
|
+
it "does nothing" do
|
118
|
+
expect(subject).to_not receive(:duplicate_object)
|
119
|
+
expect(node).to_not receive(:save!)
|
120
|
+
subject.duplicate_content
|
121
|
+
end
|
122
|
+
it "returns nil" do
|
123
|
+
expect(subject.duplicate_content).to be_nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
describe "#duplicate_object" do
|
131
|
+
|
132
|
+
context "when object has no duplicatable associations" do
|
133
|
+
let(:content_factory) { :text_page }
|
134
|
+
let(:content_attributes) { { text_html: "html" } }
|
135
|
+
|
136
|
+
it "builds an unsaved copy of the object" do
|
137
|
+
copy = subject.duplicate_object(original)
|
138
|
+
expect(copy.class).to be original.class
|
139
|
+
expect(copy.new_record?).to be true
|
140
|
+
|
141
|
+
expect(copy.text_html).to eq original.text_html
|
142
|
+
end
|
143
|
+
|
144
|
+
it "uses #deep_clone for copying" do
|
145
|
+
expect(subject).to receive(:duplicatable_associations).with(original.class).and_call_original
|
146
|
+
expect(original).to receive(:deep_clone).with( include: [] ).and_call_original
|
147
|
+
subject.duplicate_object(original)
|
148
|
+
end
|
149
|
+
|
150
|
+
it "calls #supplement_object_duplication with the original object and its new copy" do
|
151
|
+
copy = original.dup
|
152
|
+
|
153
|
+
# stub #dup (which is called by deep_clone internally) so that it returns a known instance
|
154
|
+
allow(original).to receive(:dup).and_return(copy)
|
155
|
+
|
156
|
+
expect(subject).to receive(:supplement_object_duplication).with(original, copy)
|
157
|
+
subject.duplicate_object(original)
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
context "when object has duplicatable associations" do
|
163
|
+
let(:content_factory) { complex_content_factory }
|
164
|
+
|
165
|
+
let(:content_attributes) do
|
166
|
+
{
|
167
|
+
intro_text_html: "Intro html",
|
168
|
+
banner_groups_attributes: [
|
169
|
+
{
|
170
|
+
title: "Group title",
|
171
|
+
banners_attributes: [
|
172
|
+
url: "Banner url"
|
173
|
+
]
|
174
|
+
},
|
175
|
+
]
|
176
|
+
}
|
177
|
+
end
|
178
|
+
|
179
|
+
it "passes duplicatable associations to #deep_clone" do
|
180
|
+
expect(original).to receive(:deep_clone).with( include: complex_content_duplicatable_associations ).and_call_original
|
181
|
+
subject.duplicate_object(original)
|
182
|
+
end
|
183
|
+
|
184
|
+
it "builds an unsaved copy with nested copies of associated objects" do
|
185
|
+
copy = subject.duplicate_object(original)
|
186
|
+
expect(copy.class).to be original.class
|
187
|
+
expect(copy.new_record?).to be true
|
188
|
+
expect(copy.intro_text_html).to eq original.intro_text_html
|
189
|
+
|
190
|
+
original_item = original.banner_groups.first
|
191
|
+
copied_item = copy.banner_groups.first
|
192
|
+
expect(copied_item.class).to be original_item.class
|
193
|
+
expect(copied_item.new_record?).to be true
|
194
|
+
expect(copied_item.title).to eq original_item.title
|
195
|
+
|
196
|
+
original_nested_item = original.banner_groups.first.banners.first
|
197
|
+
copied_nested_item = copy.banner_groups.first.banners.first
|
198
|
+
expect(copied_nested_item.class).to be original_nested_item.class
|
199
|
+
expect(copied_nested_item.new_record?).to be true
|
200
|
+
expect(copied_nested_item.url).to eq original_nested_item.url
|
201
|
+
end
|
202
|
+
|
203
|
+
it "calls #supplement_object_duplication on all cloned objects" do
|
204
|
+
original_item = original.banner_groups.first
|
205
|
+
original_nested_item = original_item.banners.first
|
206
|
+
|
207
|
+
expect(subject).to receive(:supplement_object_duplication).with(original, kind_of(original.class))
|
208
|
+
expect(subject).to receive(:supplement_object_duplication).with(original_item, kind_of(original_item.class))
|
209
|
+
expect(subject).to receive(:supplement_object_duplication).with(original_nested_item, kind_of(original_nested_item.class))
|
210
|
+
subject.duplicate_object(original)
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
describe "#supplement_object_duplication" do
|
218
|
+
it "calls #duplicate_dragonfly_attachments with given original and copy" do
|
219
|
+
expect(subject).to receive(:duplicate_dragonfly_attachments).with(:foo, :bar)
|
220
|
+
subject.supplement_object_duplication(:foo, :bar)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
describe "#duplicatable_associations" do
|
225
|
+
let(:content_class) { build(content_factory).class }
|
226
|
+
|
227
|
+
it "uses Releaf::ResourceBase to detect duplicatable associations" do
|
228
|
+
expect(Releaf::ResourceBase).to receive(:new).with(content_class).and_call_original
|
229
|
+
subject.duplicatable_associations(content_class)
|
230
|
+
end
|
231
|
+
|
232
|
+
context "when given class has duplicatable associations" do
|
233
|
+
it "returns duplicatable association names as nested arrays of hashes" do
|
234
|
+
expect(subject.duplicatable_associations(content_class)).to eq []
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
context "when given class has no duplicatable associations" do
|
239
|
+
let(:content_factory) { complex_content_factory }
|
240
|
+
it "returns an empty array" do
|
241
|
+
expect(subject.duplicatable_associations(content_class)).to eq complex_content_duplicatable_associations
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
describe "#duplicate_dragonfly_attachments" do
|
248
|
+
let(:content_factory) { complex_content_factory }
|
249
|
+
let(:copy) { original.dup }
|
250
|
+
|
251
|
+
context "when the original object's dragonfly attributes" do
|
252
|
+
|
253
|
+
context "are blank" do
|
254
|
+
it "sets the file attributes to nil on the given copy" do
|
255
|
+
expect(copy).to receive(:top_banner_uid=).with(nil).ordered
|
256
|
+
expect(copy).to receive(:top_banner=).with(nil).ordered
|
257
|
+
subject.duplicate_dragonfly_attachments(original, copy)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
context "contain files" do
|
262
|
+
|
263
|
+
it "separates the attachments so that dragonfly treats them as separate files" do
|
264
|
+
# it is important that the owner class used in this test has at least two dragonfly attributes
|
265
|
+
# and the test is not using the first one.
|
266
|
+
# (an earlier implementation worked only with the first file
|
267
|
+
# and would have passed the test if the test used the first file)
|
268
|
+
expect(original.dragonfly_attachments.keys.find_index(:bottom_banner)).to be > 0
|
269
|
+
|
270
|
+
original.bottom_banner = file
|
271
|
+
original.save!
|
272
|
+
|
273
|
+
subject.duplicate_dragonfly_attachments(original, copy)
|
274
|
+
copy.save!
|
275
|
+
|
276
|
+
# refetch the objects. calling #reload on them is not enough
|
277
|
+
original_id = original.id
|
278
|
+
copy_id = copy.id
|
279
|
+
klass = original.class
|
280
|
+
|
281
|
+
original = klass.find(original_id)
|
282
|
+
copy = klass.find(copy_id)
|
283
|
+
|
284
|
+
# make sure the uids are non-empty and different
|
285
|
+
expect(original.bottom_banner_uid).to be_present
|
286
|
+
expect(copy.bottom_banner_uid).to be_present
|
287
|
+
expect(copy.bottom_banner_uid).to_not eq original.bottom_banner_uid
|
288
|
+
|
289
|
+
# make sure the files are stored and exist
|
290
|
+
expect(copy.bottom_banner.path).to start_with Dragonfly.app.datastore.root_path
|
291
|
+
expect(original.bottom_banner.path).to start_with Dragonfly.app.datastore.root_path
|
292
|
+
expect(copy.bottom_banner.path).to_not eq original.bottom_banner.path
|
293
|
+
|
294
|
+
# make sure the metadata is readable as well
|
295
|
+
expect(copy.bottom_banner.format).to eq original.bottom_banner.format
|
296
|
+
end
|
297
|
+
|
298
|
+
end
|
299
|
+
|
300
|
+
context "contain files that are missing" do
|
301
|
+
|
302
|
+
it "sets the file attributes to nil on the given copy" do
|
303
|
+
original.top_banner = file
|
304
|
+
original.save!
|
305
|
+
|
306
|
+
original_id = original.id
|
307
|
+
|
308
|
+
# simulate deletion from filesystem by setting the uid to an invalid path
|
309
|
+
original.top_banner_uid = "xxx" + original.top_banner_uid
|
310
|
+
original.save!
|
311
|
+
|
312
|
+
klass = original.class
|
313
|
+
original = klass.find(original_id)
|
314
|
+
|
315
|
+
subject.duplicate_dragonfly_attachments(original, copy)
|
316
|
+
copy.save!
|
317
|
+
copy_id = copy.id
|
318
|
+
copy = klass.find(copy_id)
|
319
|
+
|
320
|
+
expect(copy.top_banner_uid).to be_nil
|
321
|
+
expect(copy.top_banner).to be_nil
|
322
|
+
end
|
323
|
+
|
324
|
+
end
|
325
|
+
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
|
330
|
+
|
115
331
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: releaf-content
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- CubeSystems
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-02-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: releaf-core
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 1.0.
|
19
|
+
version: 1.0.9
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 1.0.
|
26
|
+
version: 1.0.9
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: stringex
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: deep_cloneable
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.2.2
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.2.2
|
55
69
|
description: Content subsystem for releaf
|
56
70
|
email: info@cubesystems.lv
|
57
71
|
executables: []
|
@@ -100,6 +114,7 @@ files:
|
|
100
114
|
- spec/controllers/releaf/content/nodes_controller_spec.rb
|
101
115
|
- spec/features/nodes_services_spec.rb
|
102
116
|
- spec/features/nodes_spec.rb
|
117
|
+
- spec/fixtures/dummy.png
|
103
118
|
- spec/lib/releaf/content/acts_as_node_spec.rb
|
104
119
|
- spec/lib/releaf/content/configuration_spec.rb
|
105
120
|
- spec/lib/releaf/content/engine_spec.rb
|
@@ -134,7 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
134
149
|
version: '0'
|
135
150
|
requirements: []
|
136
151
|
rubyforge_project:
|
137
|
-
rubygems_version: 2.
|
152
|
+
rubygems_version: 2.6.10
|
138
153
|
signing_key:
|
139
154
|
specification_version: 4
|
140
155
|
summary: Node and content routes support for releaf
|
@@ -146,6 +161,7 @@ test_files:
|
|
146
161
|
- spec/controllers/releaf/content/nodes_controller_spec.rb
|
147
162
|
- spec/features/nodes_services_spec.rb
|
148
163
|
- spec/features/nodes_spec.rb
|
164
|
+
- spec/fixtures/dummy.png
|
149
165
|
- spec/lib/releaf/content/acts_as_node_spec.rb
|
150
166
|
- spec/lib/releaf/content/configuration_spec.rb
|
151
167
|
- spec/lib/releaf/content/engine_spec.rb
|