spontaneous 0.2.0.beta9 → 0.2.0.beta10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/LICENSE +18 -17
  4. data/Rakefile +1 -1
  5. data/application/css/core.css.scss +1 -1
  6. data/application/css/dialogue.css.scss +8 -20
  7. data/application/js/preview.js +28 -7
  8. data/application/js/publish.js +15 -4
  9. data/application/js/top_bar.js +0 -16
  10. data/application/js/views/piece_view.js +1 -1
  11. data/lib/spontaneous/asset/environment.rb +16 -1
  12. data/lib/spontaneous/box.rb +68 -0
  13. data/lib/spontaneous/capistrano/deploy.rb +7 -4
  14. data/lib/spontaneous/capistrano/sync.rb +2 -2
  15. data/lib/spontaneous/cli/init.rb +70 -19
  16. data/lib/spontaneous/cli/init/db.rb +34 -55
  17. data/lib/spontaneous/cli/init/mysql.rb +5 -5
  18. data/lib/spontaneous/cli/init/postgresql.rb +8 -9
  19. data/lib/spontaneous/cli/init/sqlite.rb +1 -2
  20. data/lib/spontaneous/cli/migrate.rb +0 -1
  21. data/lib/spontaneous/cli/site.rb +4 -0
  22. data/lib/spontaneous/collections/entry_set.rb +11 -0
  23. data/lib/spontaneous/data_mapper/content_model.rb +2 -0
  24. data/lib/spontaneous/data_mapper/content_model/serialization.rb +2 -2
  25. data/lib/spontaneous/extensions/array.rb +12 -2
  26. data/lib/spontaneous/field/base.rb +10 -0
  27. data/lib/spontaneous/field/file.rb +32 -2
  28. data/lib/spontaneous/field/image.rb +24 -2
  29. data/lib/spontaneous/field/select.rb +8 -0
  30. data/lib/spontaneous/field/webvideo.rb +8 -0
  31. data/lib/spontaneous/generators/site/config/initializers/fields.rb +55 -0
  32. data/lib/spontaneous/json.rb +3 -2
  33. data/lib/spontaneous/logger.rb +2 -2
  34. data/lib/spontaneous/media/file.rb +3 -3
  35. data/lib/spontaneous/media/image/attributes.rb +72 -6
  36. data/lib/spontaneous/media/image/renderable.rb +53 -20
  37. data/lib/spontaneous/media/store.rb +3 -3
  38. data/lib/spontaneous/media/store/backend.rb +16 -0
  39. data/lib/spontaneous/media/store/cloud.rb +52 -12
  40. data/lib/spontaneous/media/store/local.rb +6 -3
  41. data/lib/spontaneous/model.rb +3 -0
  42. data/lib/spontaneous/model/core/entries.rb +34 -13
  43. data/lib/spontaneous/model/core/entry.rb +3 -1
  44. data/lib/spontaneous/model/page/controllers.rb +1 -2
  45. data/lib/spontaneous/model/page/paths.rb +18 -7
  46. data/lib/spontaneous/output/context.rb +0 -8
  47. data/lib/spontaneous/output/template/renderer.rb +2 -0
  48. data/lib/spontaneous/plugins/application/state.rb +0 -4
  49. data/lib/spontaneous/prototypes/field_prototype.rb +4 -0
  50. data/lib/spontaneous/publishing/immediate.rb +0 -5
  51. data/lib/spontaneous/publishing/progress.rb +2 -2
  52. data/lib/spontaneous/publishing/rerender.rb +1 -4
  53. data/lib/spontaneous/publishing/simultaneous.rb +19 -17
  54. data/lib/spontaneous/publishing/steps.rb +12 -3
  55. data/lib/spontaneous/rack.rb +2 -0
  56. data/lib/spontaneous/rack/asset_server.rb +5 -2
  57. data/lib/spontaneous/rack/back.rb +9 -1
  58. data/lib/spontaneous/rack/back/base.rb +1 -0
  59. data/lib/spontaneous/rack/back/changes.rb +5 -0
  60. data/lib/spontaneous/rack/back/preview.rb +4 -4
  61. data/lib/spontaneous/rack/back/private.rb +11 -0
  62. data/lib/spontaneous/rack/middleware/scope.rb +16 -4
  63. data/lib/spontaneous/rack/page_controller.rb +2 -2
  64. data/lib/spontaneous/rack/public.rb +52 -4
  65. data/lib/spontaneous/sequel.rb +10 -13
  66. data/lib/spontaneous/site.rb +28 -8
  67. data/lib/spontaneous/site/publishing.rb +1 -1
  68. data/lib/spontaneous/site/storage.rb +7 -4
  69. data/lib/spontaneous/tasks/environment.rake +3 -0
  70. data/lib/spontaneous/utils/database/postgres_dumper.rb +23 -2
  71. data/lib/spontaneous/version.rb +1 -1
  72. data/spontaneous.gemspec +7 -12
  73. data/test/fixtures/assets/public1/css/data.css.scss +1 -1
  74. data/test/functional/test_application.rb +15 -0
  75. data/test/functional/test_cli.rb +109 -3
  76. data/test/functional/test_front.rb +108 -10
  77. data/test/test_helper.rb +3 -3
  78. data/test/unit/fields/test_boolean_fields.rb +80 -0
  79. data/test/unit/fields/test_date_fields.rb +47 -0
  80. data/test/unit/fields/test_file_field.rb +210 -0
  81. data/test/unit/{test_images.rb → fields/test_image_fields.rb} +133 -15
  82. data/test/unit/fields/test_location_fields.rb +41 -0
  83. data/test/unit/fields/test_option_fields.rb +61 -0
  84. data/test/unit/fields/test_tag_list_fields.rb +45 -0
  85. data/test/unit/fields/test_text_fields.rb +124 -0
  86. data/test/unit/fields/test_web_video_fields.rb +198 -0
  87. data/test/unit/test_assets.rb +22 -22
  88. data/test/unit/test_boxes.rb +34 -13
  89. data/test/unit/test_changesets.rb +1 -0
  90. data/test/unit/test_extensions.rb +17 -0
  91. data/test/unit/test_fields.rb +20 -643
  92. data/test/unit/test_media.rb +9 -9
  93. data/test/unit/test_page.rb +47 -0
  94. data/test/unit/test_publishing_pipeline.rb +2 -2
  95. data/test/unit/test_serialisation.rb +37 -0
  96. data/test/unit/test_storage.rb +42 -3
  97. metadata +37 -17
@@ -147,15 +147,15 @@ describe "Assets" do
147
147
  let(:x_js_digest) { asset_digest('public1/x.js') }
148
148
  # these are compiled so fairly complex to calculate their digests
149
149
  # not impossible, but annoying
150
- let(:n_js_digest) { '74f175e03a4bdc8c807aba4ae0314938' }
151
- let(:m_js_digest) { 'dd35b142dc75b6ec15b2138e9e91c0c3' }
152
- let(:all_js_digest) { 'd406fc3c21d90828a2f0a718c89e8d99' }
150
+ let(:n_js_digest) { 'a5c86b004242d16e0dbf818068bbb248' }
151
+ let(:m_js_digest) { 'c0f2e1c2a7d1cc9666ccb48cc9fd610e' }
152
+ let(:all_js_digest) { '9759f54463d069b39d9c04c3e1d63745' }
153
153
 
154
- let(:a_css_digest) { '7b04d295476986c24d8c77245943e5b9' }
155
- let(:b_css_digest) { '266643993e14da14f2473d45f003bd2c' }
156
- let(:c_css_digest) { 'fc8ba0d0aae64081dc00b8444a198fb8' }
157
- let(:x_css_digest) { '2560aec2891794825eba770bf84823fb' }
158
- let(:all_css_digest) { 'cf61c624b91b9ea126804291ac55bd5d' }
154
+ let(:a_css_digest) { 'b4ec507466566b839328b924893e80fb' }
155
+ let(:b_css_digest) { '97b252913f48c160b1eed120f7544203' }
156
+ let(:c_css_digest) { '3fcc1da2378a42e97cbd11cb85b8115a' }
157
+ let(:x_css_digest) { '88b3052bb5d7723b6a6a8b628fbee34e' }
158
+ let(:all_css_digest) { '9bac5a609f816594005b07a3a8aa2368' }
159
159
 
160
160
  it "includes all js dependencies" do
161
161
  result = context.scripts('js/all', 'js/m', 'js/c', 'x')
@@ -204,15 +204,15 @@ describe "Assets" do
204
204
  let(:app) { Spontaneous::Rack::Back.application(site) }
205
205
  let(:context) { preview_context }
206
206
 
207
- let(:c_js_digest) { 'f669550dd7e10e9646ad781f44756950' }
208
- let(:x_js_digest) { '6b4c9176b2838a4949a18284543fc19c' }
207
+ let(:c_js_digest) { '3ab39368d6e128169ac2401bc49fcdb2' }
208
+ let(:x_js_digest) { 'e755ffcead8090406c04b2813b9bdce9' }
209
209
  let(:n_js_digest) { '74f175e03a4bdc8c807aba4ae0314938' }
210
- let(:m_js_digest) { 'dd35b142dc75b6ec15b2138e9e91c0c3' }
211
- let(:all_js_digest) { 'cd1f681752f5038421be0bc5ea0e855d' }
210
+ let(:m_js_digest) { 'c0f2e1c2a7d1cc9666ccb48cc9fd610e' }
211
+ let(:all_js_digest) { 'd782ef6abb69b1eb3e4c503478a660db' }
212
212
 
213
- let(:c_css_digest) { 'fc8ba0d0aae64081dc00b8444a198fb8' }
214
- let(:x_css_digest) { '2560aec2891794825eba770bf84823fb' }
215
- let(:all_css_digest) { 'bb2c289a27b3d5d4467dde6d60722fd3' }
213
+ let(:c_css_digest) { '3fcc1da2378a42e97cbd11cb85b8115a' }
214
+ let(:x_css_digest) { '88b3052bb5d7723b6a6a8b628fbee34e' }
215
+ let(:all_css_digest) { '4b837b285d3a1998c48e1811e62292d8' }
216
216
 
217
217
  describe "javascript" do
218
218
  it "include scripts as separate files with finger prints" do
@@ -382,14 +382,14 @@ describe "Assets" do
382
382
  end
383
383
 
384
384
  describe "javascript" do
385
- let(:all_sha) { "ed62549e8edc1f61a1e27136602f01d9" }
386
- let(:x_sha) { "66e92be1e412458f6ff02f4c5dd9beb1" }
385
+ let(:all_sha) { "29ced6e75f651ea6963bd2f2ffdd745e" }
386
+ let(:x_sha) { "2b8678f6d71dc1fc0e44ad9f6a5811b3" }
387
387
  it "bundles & fingerprints local scripts" do
388
388
  result = context.scripts('js/all', 'js/m.js', 'js/c.js', 'x')
389
389
  result.must_equal [
390
390
  %(<script type="text/javascript" src="/assets/js/all-#{all_sha}.js"></script>),
391
- '<script type="text/javascript" src="/assets/js/m-a5be7324bc314d5cf470a59c3732ef10.js"></script>',
392
- '<script type="text/javascript" src="/assets/js/c-c24bcbb4f9647b078cc919746aa7fc3a.js"></script>',
391
+ '<script type="text/javascript" src="/assets/js/m-66885c19e856373c6b9dab3a41885dbf.js"></script>',
392
+ '<script type="text/javascript" src="/assets/js/c-0201f986795c0d9b4fb850236496359f.js"></script>',
393
393
  %(<script type="text/javascript" src="/assets/x-#{x_sha}.js"></script>)
394
394
  ].join("\n")
395
395
  end
@@ -456,14 +456,14 @@ describe "Assets" do
456
456
  end
457
457
 
458
458
  describe "css" do
459
- let(:all_sha) { "2e17f25ddeba996223a6cd1e28e7a319" }
460
- let(:x_sha) { "2560aec2891794825eba770bf84823fb" }
459
+ let(:all_sha) { "a72e3c2850e95f5c89930cae40a66c29" }
460
+ let(:x_sha) { "88b3052bb5d7723b6a6a8b628fbee34e" }
461
461
 
462
462
  it "bundles & fingerprints local stylesheets" do
463
463
  result = context.stylesheets('css/all', 'css/a.css', 'x')
464
464
  result.must_equal [
465
465
  %(<link rel="stylesheet" href="/assets/css/all-#{all_sha}.css" />),
466
- '<link rel="stylesheet" href="/assets/css/a-0164c6d5b696ec2f2c5e70cade040da8.css" />',
466
+ '<link rel="stylesheet" href="/assets/css/a-46541ee6e70fb12c030698e0addc2c79.css" />',
467
467
  %(<link rel="stylesheet" href="/assets/x-#{x_sha}.css" />)
468
468
  ].join("\n")
469
469
  end
@@ -370,25 +370,46 @@ describe "Boxes" do
370
370
  instance.box3.contents.map { |e| e.label }.must_equal ["0", "1", "c", "2", "3"]
371
371
  end
372
372
 
373
+ it 'should be available as a list of ids' do
374
+ BlankContent.box :box3
375
+ instance = BlankContent.new
376
+ contents = 3.times.map { instance.words << StyledContent.new }
377
+ 3.times.map { instance.images << StyledContent.new }
378
+ instance.save
379
+ contents.each(&:save)
380
+ ids = contents.map(&:id)
381
+ instance.words.ids.must_equal ids
382
+ end
383
+
384
+ it 'should be clearable' do
385
+ instance = BlankContent.create
386
+ box = instance.images
387
+ count = 4
388
+ count.times { |n| box << StyledContent.new(:label => n)}
389
+ instance.save
390
+ instance.images.clear!
391
+ instance.reload.images.length.must_equal 0
392
+ end
393
+
373
394
  end
374
395
 
375
396
  describe "Allowed types" do
376
397
  before do
377
- class ::Allowed1 < Content
398
+ class ::Allowed1 < Piece
378
399
  style :frank
379
400
  style :freddy
380
401
  end
381
- class ::Allowed2 < Content
402
+ class ::Allowed2 < Piece
382
403
  style :john
383
404
  style :paul
384
405
  style :ringo
385
406
  style :george
386
407
  end
387
- class ::Allowed3 < Content
408
+ class ::Allowed3 < Piece
388
409
  style :arthur
389
410
  style :lancelot
390
411
  end
391
- class ::Allowed4 < Content; end
412
+ class ::Allowed4 < Piece; end
392
413
 
393
414
  class ::Allowed11 < ::Allowed1; end
394
415
  class ::Allowed111 < ::Allowed1; end
@@ -402,7 +423,7 @@ describe "Boxes" do
402
423
  class ::ChildClass < ::Parent
403
424
  end
404
425
 
405
- class ::Allowable < Content
426
+ class ::Allowable < Piece
406
427
  box :parents, :type => :Parent
407
428
  end
408
429
 
@@ -566,14 +587,14 @@ describe "Boxes" do
566
587
  @b = ::B.new
567
588
  @c = ::C.new
568
589
  [@a, @b, @c].each do |instance|
569
- instance.boxes[:a].stubs(:render).with(anything).returns("[a]")
570
- instance.boxes[:b].stubs(:render).with(anything).returns("[b]")
571
- instance.boxes[:c].stubs(:render).with(anything).returns("[c]")
572
- instance.boxes[:d].stubs(:render).with(anything).returns("[d]")
573
- end
574
- @b.boxes[:e].stubs(:render).with(anything).returns("[e]")
575
- @c.boxes[:e].stubs(:render).with(anything).returns("[e]")
576
- @c.boxes[:f].stubs(:render).with(anything).returns("[f]")
590
+ instance.boxes[:a].stubs(:render_inline).with(anything).returns("[a]")
591
+ instance.boxes[:b].stubs(:render_inline).with(anything).returns("[b]")
592
+ instance.boxes[:c].stubs(:render_inline).with(anything).returns("[c]")
593
+ instance.boxes[:d].stubs(:render_inline).with(anything).returns("[d]")
594
+ end
595
+ @b.boxes[:e].stubs(:render_inline).with(anything).returns("[e]")
596
+ @c.boxes[:e].stubs(:render_inline).with(anything).returns("[e]")
597
+ @c.boxes[:f].stubs(:render_inline).with(anything).returns("[f]")
577
598
  end
578
599
 
579
600
  after do
@@ -78,6 +78,7 @@ describe "Change" do
78
78
  it "not list new pieces as available for publish" do
79
79
  root = Page.create(:title => "root")
80
80
  Content.publish(@revision)
81
+ root.reload
81
82
  root.things << Piece.new
82
83
  root.save.reload
83
84
  result = outstanding_changes[:changes]
@@ -42,5 +42,22 @@ describe "Extensions" do
42
42
  result.must_equal [["js"], ["coffee", "coffee"], ["js"], ["coffee"]]
43
43
  end
44
44
  end
45
+
46
+ describe "Array" do
47
+ it 'responds to #render by calling #render_inline on the contents' do
48
+ array = 3.times.map { mock }
49
+ array.each do |m|
50
+ m.expects(:render_inline).returns("x")
51
+ end
52
+ array.render.must_equal 'xxx'
53
+ end
54
+ it 'responds to #render_using by calling #render_inline_using on the contents' do
55
+ array = 3.times.map { mock }
56
+ array.each do |m|
57
+ m.expects(:render_inline_using).returns("x")
58
+ end
59
+ array.render_using(mock).must_equal 'xxx'
60
+ end
61
+ end
45
62
  end
46
63
 
@@ -510,87 +510,6 @@ describe "Fields" do
510
510
  end
511
511
  end
512
512
 
513
- describe "String Fields" do
514
- before do
515
- @content_class = Class.new(::Piece) do
516
- field :title, :string
517
- end
518
- @instance = @content_class.new
519
- @field = @instance.title
520
- end
521
-
522
- it "should escape ampersands for the html format" do
523
- @field.value = "This & That"
524
- @field.value(:html).must_equal "This &amp; That"
525
- end
526
- end
527
-
528
- describe "Markdown fields" do
529
- before do
530
- class ::MarkdownContent < Piece
531
- field :text1, :markdown
532
- field :text2, :richtext
533
- field :text3, :markup
534
- end
535
- @instance = MarkdownContent.new
536
- end
537
- after do
538
- Object.send(:remove_const, :MarkdownContent)
539
- end
540
-
541
- it "be available as the :markdown type" do
542
- assert MarkdownContent.field_prototypes[:text1].field_class < Spontaneous::Field::Markdown
543
- end
544
- it "be available as the :richtext type" do
545
- assert MarkdownContent.field_prototypes[:text2].field_class < Spontaneous::Field::Markdown
546
- end
547
- it "be available as the :markup type" do
548
- assert MarkdownContent.field_prototypes[:text3].field_class < Spontaneous::Field::Markdown
549
- end
550
-
551
- it "process input into HTML" do
552
- @instance.text1 = "*Hello* **World**"
553
- @instance.text1.value.must_equal "<p><em>Hello</em> <strong>World</strong></p>\n"
554
- end
555
-
556
- it "use more sensible linebreaks" do
557
- @instance.text1 = "With\nLinebreak"
558
- @instance.text1.value.must_equal "<p>With<br />\nLinebreak</p>\n"
559
- @instance.text2 = "With \nLinebreak"
560
- @instance.text2.value.must_equal "<p>With<br />\nLinebreak</p>\n"
561
- end
562
- end
563
-
564
- describe "LongString fields" do
565
- before do
566
- class ::LongStringContent < Piece
567
- field :long1, :longstring
568
- field :long2, :long_string
569
- field :long3, :text
570
- end
571
- @instance = LongStringContent.new
572
- end
573
- after do
574
- Object.send(:remove_const, :LongStringContent)
575
- end
576
-
577
- it "is available as the :longstring type" do
578
- assert LongStringContent.field_prototypes[:long1].field_class < Spontaneous::Field::LongString
579
- end
580
-
581
- it "is available as the :long_string type" do
582
- assert LongStringContent.field_prototypes[:long2].field_class < Spontaneous::Field::LongString
583
- end
584
-
585
- it "is available as the :text type" do
586
- assert LongStringContent.field_prototypes[:long3].field_class < Spontaneous::Field::LongString
587
- end
588
-
589
- it "translates newlines to <br/> tags" do
590
- @instance.long1 = "this\nlong\nstring"
591
- @instance.long1.value.must_equal "this<br />\nlong<br />\nstring"
592
- end
593
- end
594
513
 
595
514
  describe "Editor classes" do
596
515
  it "be defined in base types" do
@@ -741,568 +660,6 @@ describe "Fields" do
741
660
  end
742
661
  end
743
662
 
744
- describe "String fields" do
745
- it "be aliased to the :title type" do
746
- @content_class = Class.new(::Piece) do
747
- field :title, default: "Right"
748
- field :something, :title
749
- end
750
- instance = @content_class.new
751
- assert instance.fields.title.class.ancestors.include?(Spontaneous::Field::String), ":title type should inherit from StringField"
752
- instance.title.value.must_equal "Right"
753
- end
754
- end
755
-
756
- describe "WebVideo fields" do
757
- before do
758
- @content_class = Class.new(::Piece) do
759
- field :video, :webvideo
760
- end
761
- @content_class.stubs(:name).returns("ContentClass")
762
- @instance = @content_class.new
763
- @field = @instance.video
764
- end
765
-
766
- it "have their own editor type" do
767
- @content_class.fields.video.export(nil)[:type].must_equal "Spontaneous.Field.WebVideo"
768
- @instance.video = "http://www.youtube.com/watch?v=_0jroAM_pO4&feature=feedrec_grec_index"
769
- fields = @instance.export(nil)[:fields]
770
- fields[0][:processed_value].must_equal @instance.video.src
771
- end
772
-
773
- it "recognise youtube URLs" do
774
- @instance.video = "http://www.youtube.com/watch?v=_0jroAM_pO4&feature=feedrec_grec_index"
775
- @instance.video.value.must_equal "http://www.youtube.com/watch?v=_0jroAM_pO4&amp;feature=feedrec_grec_index"
776
- @instance.video.video_id.must_equal "_0jroAM_pO4"
777
- @instance.video.provider_id.must_equal "youtube"
778
- end
779
-
780
- it "recognise Vimeo URLs" do
781
- @instance.video = "http://vimeo.com/31836285"
782
- @instance.video.value.must_equal "http://vimeo.com/31836285"
783
- @instance.video.video_id.must_equal "31836285"
784
- @instance.video.provider_id.must_equal "vimeo"
785
- end
786
-
787
- it "recognise Vine URLs" do
788
- @instance.video = "https://vine.co/v/brI7pTPb3qU"
789
- @instance.video.value.must_equal "https://vine.co/v/brI7pTPb3qU"
790
- @instance.video.video_id.must_equal "brI7pTPb3qU"
791
- @instance.video.provider_id.must_equal "vine"
792
- end
793
-
794
- it "silently handles unknown providers" do
795
- @instance.video = "https://idontdovideo.com/video?id=brI7pTPb3qU"
796
- @instance.video.value.must_equal "https://idontdovideo.com/video?id=brI7pTPb3qU"
797
- @instance.video.video_id.must_equal "https://idontdovideo.com/video?id=brI7pTPb3qU"
798
- @instance.video.provider_id.must_equal nil
799
- end
800
-
801
-
802
- it "use the YouTube api to extract video metadata" do
803
- youtube_info = {"thumbnail_large" => "http://i.ytimg.com/vi/_0jroAM_pO4/hqdefault.jpg", "thumbnail_small"=>"http://i.ytimg.com/vi/_0jroAM_pO4/default.jpg", "title" => "Hilarious QI Moment - Cricket", "description" => "Rob Brydon makes a rather embarassing choice of words whilst discussing the relationship between a cricket's chirping and the temperature. Taken from QI XL Series H episode 11 - Highs and Lows", "user_name" => "morthasa", "upload_date" => "2011-01-14 19:49:44", "tags" => "Hilarious, QI, Moment, Cricket, fun, 11, stephen, fry, alan, davies, Rob, Brydon, SeriesH, Fred, MacAulay, Sandi, Toksvig", "duration" => 78, "stats_number_of_likes" => 297, "stats_number_of_plays" => 53295, "stats_number_of_comments" => 46}#.symbolize_keys
804
-
805
- response_xml_file = File.expand_path("../../fixtures/fields/youtube_api_response.xml", __FILE__)
806
- connection = mock()
807
- Spontaneous::Field::WebVideo::YouTube.any_instance.expects(:open).with("http://gdata.youtube.com/feeds/api/videos/_0jroAM_pO4?v=2").returns(connection)
808
- doc = Nokogiri::XML(File.open(response_xml_file))
809
- Nokogiri.expects(:XML).with(connection).returns(doc)
810
- @field.value = "http://www.youtube.com/watch?v=_0jroAM_pO4"
811
- @field.values.must_equal youtube_info.merge(:video_id => "_0jroAM_pO4", :provider => "youtube", :html => "http://www.youtube.com/watch?v=_0jroAM_pO4")
812
- end
813
-
814
- it "use the Vimeo api to extract video metadata" do
815
- vimeo_info = {"id"=>29987529, "title"=>"Neon Indian Plays The UO Music Shop", "description"=>"Neon Indian plays electronic instruments from the UO Music Shop, Fall 2011. Read more at blog.urbanoutfitters.com.", "url"=>"http://vimeo.com/29987529", "upload_date"=>"2011-10-03 18:32:47", "mobile_url"=>"http://vimeo.com/m/29987529", "thumbnail_small"=>"http://b.vimeocdn.com/ts/203/565/203565974_100.jpg", "thumbnail_medium"=>"http://b.vimeocdn.com/ts/203/565/203565974_200.jpg", "thumbnail_large"=>"http://b.vimeocdn.com/ts/203/565/203565974_640.jpg", "user_name"=>"Urban Outfitters", "user_url"=>"http://vimeo.com/urbanoutfitters", "user_portrait_small"=>"http://b.vimeocdn.com/ps/251/111/2511118_30.jpg", "user_portrait_medium"=>"http://b.vimeocdn.com/ps/251/111/2511118_75.jpg", "user_portrait_large"=>"http://b.vimeocdn.com/ps/251/111/2511118_100.jpg", "user_portrait_huge"=>"http://b.vimeocdn.com/ps/251/111/2511118_300.jpg", "stats_number_of_likes"=>85, "stats_number_of_plays"=>26633, "stats_number_of_comments"=>0, "duration"=>100, "width"=>1280, "height"=>360, "tags"=>"neon indian, analog, korg, moog, theremin, micropiano, microkorg, kaossilator, kaossilator pro", "embed_privacy"=>"anywhere"}.symbolize_keys
816
-
817
- connection = mock()
818
- connection.expects(:read).returns(Spontaneous.encode_json([vimeo_info]))
819
- Spontaneous::Field::WebVideo::Vimeo.any_instance.expects(:open).with("http://vimeo.com/api/v2/video/29987529.json").returns(connection)
820
- @field.value = "http://vimeo.com/29987529"
821
- @field.values.must_equal vimeo_info.merge(:video_id => "29987529", :provider => "vimeo", :html => "http://vimeo.com/29987529")
822
- end
823
-
824
- describe "with player settings" do
825
- before do
826
- @content_class.field :video2, :webvideo, :player => {
827
- :width => 680, :height => 384,
828
- :fullscreen => true, :autoplay => true, :loop => true,
829
- :showinfo => false,
830
- :youtube => { :theme => 'light', :hd => true, :controls => false },
831
- :vimeo => { :color => "ccc", :api => true }
832
- }
833
- @instance = @content_class.new
834
- @field = @instance.video2
835
- end
836
-
837
- it "use the configuration in the youtube player HTML" do
838
- @field.value = "http://www.youtube.com/watch?v=_0jroAM_pO4&feature=feedrec_grec_index"
839
- html = @field.render(:html)
840
- html.must_match /^<iframe/
841
- html.must_match %r{src="//www\.youtube\.com/embed/_0jroAM_pO4}
842
- html.must_match /width="680"/
843
- html.must_match /height="384"/
844
- html.must_match /theme=light/
845
- html.must_match /hd=1/
846
- html.must_match /fs=1/
847
- html.must_match /controls=0/
848
- html.must_match /autoplay=1/
849
- html.must_match /showinfo=0/
850
- html.must_match /showsearch=0/
851
- @field.render(:html, :youtube => {:showsearch => 1}).must_match /showsearch=1/
852
- @field.render(:html, :youtube => {:theme => 'dark'}).must_match /theme=dark/
853
- @field.render(:html, :width => 100).must_match /width="100"/
854
- @field.render(:html, :loop => true).must_match /loop=1/
855
- end
856
-
857
- it "use the configuration in the Vimeo player HTML" do
858
- @field.value = "http://vimeo.com/31836285"
859
- html = @field.render(:html)
860
- html.must_match /^<iframe/
861
- html.must_match %r{src="//player\.vimeo\.com/video/31836285}
862
- html.must_match /width="680"/
863
- html.must_match /height="384"/
864
- html.must_match /color=ccc/
865
- html.must_match /webkitAllowFullScreen="yes"/
866
- html.must_match /allowFullScreen="yes"/
867
- html.must_match /autoplay=1/
868
- html.must_match /title=0/
869
- html.must_match /byline=0/
870
- html.must_match /portrait=0/
871
- html.must_match /api=1/
872
- @field.render(:html, :vimeo => {:color => 'f0abcd'}).must_match /color=f0abcd/
873
- @field.render(:html, :loop => true).must_match /loop=1/
874
- @field.render(:html, :title => true).must_match /title=1/
875
- @field.render(:html, :title => true).must_match /byline=0/
876
- end
877
-
878
- it "provide a version of the YouTube player params in JSON/JS format" do
879
- @field.value = "http://www.youtube.com/watch?v=_0jroAM_pO4&feature=feedrec_grec_index"
880
- json = Spontaneous::JSON.parse(@field.render(:json))
881
- json[:"tagname"].must_equal "iframe"
882
- json[:"tag"].must_equal "<iframe/>"
883
- attr = json[:"attr"]
884
- attr.must_be_instance_of(Hash)
885
- attr[:"src"].must_match %r{^//www\.youtube\.com/embed/_0jroAM_pO4}
886
- attr[:"src"].must_match /theme=light/
887
- attr[:"src"].must_match /hd=1/
888
- attr[:"src"].must_match /fs=1/
889
- attr[:"src"].must_match /controls=0/
890
- attr[:"src"].must_match /autoplay=1/
891
- attr[:"src"].must_match /showinfo=0/
892
- attr[:"src"].must_match /showsearch=0/
893
- attr[:"width"].must_equal 680
894
- attr[:"height"].must_equal 384
895
- attr[:"frameborder"].must_equal "0"
896
- attr[:"type"].must_equal "text/html"
897
- end
898
-
899
- it "provide a version of the Vimeo player params in JSON/JS format" do
900
- @field.value = "http://vimeo.com/31836285"
901
- json = Spontaneous::JSON.parse(@field.render(:json))
902
- json[:"tagname"].must_equal "iframe"
903
- json[:"tag"].must_equal "<iframe/>"
904
- attr = json[:"attr"]
905
- attr.must_be_instance_of(Hash)
906
- attr[:"src"].must_match /color=ccc/
907
- attr[:"src"].must_match /autoplay=1/
908
- attr[:"src"].must_match /title=0/
909
- attr[:"src"].must_match /byline=0/
910
- attr[:"src"].must_match /portrait=0/
911
- attr[:"src"].must_match /api=1/
912
- attr[:"webkitAllowFullScreen"].must_equal "yes"
913
- attr[:"allowFullScreen"].must_equal "yes"
914
- attr[:"width"].must_equal 680
915
- attr[:"height"].must_equal 384
916
- attr[:"frameborder"].must_equal "0"
917
- attr[:"type"].must_equal "text/html"
918
- end
919
-
920
-
921
- it "can properly embed a Vine video" do
922
- @field.value = "https://vine.co/v/brI7pTPb3qU"
923
- embed = @field.render(:html)
924
- embed.must_match %r(iframe)
925
- embed.must_match %r(src=["']//vine\.co/v/brI7pTPb3qU/card["'])
926
- # Vine videos are square
927
- embed.must_match %r(width=["']680["'])
928
- embed.must_match %r(height=["']680["'])
929
- end
930
-
931
- it "falls back to a simple iframe for unknown providers xxx" do
932
- @field.value = "https://unknownprovider.net/xx/brI7pTPb3qU"
933
- embed = @field.render(:html)
934
- embed.must_match %r(iframe)
935
- embed.must_match %r(src=["']https://unknownprovider.net/xx/brI7pTPb3qU["'])
936
- embed.must_match %r(width=["']680["'])
937
- embed.must_match %r(height=["']384["'])
938
- end
939
- end
940
-
941
- end
942
-
943
- describe "HTML fields" do
944
- before do
945
- @content_class = Class.new(::Piece) do
946
- field :raw, :html
947
- end
948
- @content_class.stubs(:name).returns("ContentClass")
949
- @instance = @content_class.new
950
- @field = @instance.raw
951
- end
952
-
953
- it "does no escaping of input" do
954
- @field.value = "<script>\n</script>"
955
- @field.value(:html).must_equal "<script>\n</script>"
956
- end
957
- end
958
-
959
- describe "Location fields" do
960
- before do
961
- @content_class = Class.new(::Piece) do
962
- field :location
963
- end
964
- @content_class.stubs(:name).returns("ContentClass")
965
- @instance = @content_class.new
966
- @field = @instance.location
967
- end
968
-
969
- it "use a standard string editor" do
970
- @content_class.fields.location.export(nil)[:type].must_equal "Spontaneous.Field.String"
971
- end
972
-
973
- it "successfully geolocate an address" do
974
- # TODO: use mocking to avoid an external API request to googles geolocation service
975
- @field.value = "Cambridge, England"
976
- @field.value(:lat).must_equal 52.2053370
977
- @field.value(:lng).must_equal 0.1218170
978
- @field.value(:country).must_equal "United Kingdom"
979
- @field.value(:formatted_address).must_equal "Cambridge, Cambridge, UK"
980
-
981
- @field.latitude.must_equal 52.2053370
982
- @field.longitude.must_equal 0.1218170
983
- @field.lat.must_equal 52.2053370
984
- @field.lng.must_equal 0.1218170
985
-
986
- @field.country.must_equal "United Kingdom"
987
- @field.formatted_address.must_equal "Cambridge, Cambridge, UK"
988
- end
989
- end
990
-
991
- describe "Option fields" do
992
- before do
993
- @content_class = Class.new(::Piece) do
994
- field :options, :select, :options => [
995
- ["a", "Value A"],
996
- ["b", "Value B"],
997
- ["c", "Value C"]
998
- ]
999
- end
1000
- @content_class.stubs(:name).returns("ContentClass")
1001
- @instance = @content_class.new
1002
- @field = @instance.options
1003
- end
1004
-
1005
- it "use a specific editor class" do
1006
- @content_class.fields.options.export(nil)[:type].must_equal "Spontaneous.Field.Select"
1007
- end
1008
-
1009
- it "select the options class for fields named options" do
1010
- @content_class.field :type, :select, :options => [["a", "A"]]
1011
- assert @content_class.fields.options.instance_class.ancestors.include?(Spontaneous::Field::Select)
1012
- end
1013
-
1014
- it "accept a list of strings as options" do
1015
- @content_class.field :type, :select, :options => ["a", "b"]
1016
- @instance = @content_class.new
1017
- @instance.type.option_list.must_equal [["a", "a"], ["b", "b"]]
1018
- end
1019
-
1020
- it "accept a json string as a value and convert it properly" do
1021
- @field.value = %(["a", "Value A"])
1022
- @field.value.must_equal "a"
1023
- @field.value(:label).must_equal "Value A"
1024
- @field.label.must_equal "Value A"
1025
- @field.unprocessed_value.must_equal %(["a", "Value A"])
1026
- end
1027
- end
1028
-
1029
- describe "File fields" do
1030
- let(:path) { File.expand_path("../../fixtures/images/vimlogo.pdf", __FILE__) }
1031
-
1032
- before do
1033
- assert File.exists?(path), "Test file #{path} does not exist"
1034
- @content_class = Class.new(::Piece)
1035
- @prototype = @content_class.field :file
1036
- @content_class.stubs(:name).returns("ContentClass")
1037
- @instance = @content_class.create
1038
- @field = @instance.file
1039
- end
1040
-
1041
- it "have a distinct editor class" do
1042
- @prototype.instance_class.editor_class.must_equal "Spontaneous.Field.File"
1043
- end
1044
-
1045
- it "adopt any field called 'file'" do
1046
- assert @field.is_a?(Spontaneous::Field::File), "Field should be an instance of FileField but instead has the following ancestors #{ @prototype.instance_class.ancestors }"
1047
- end
1048
-
1049
- it "copy files to the media folder" do
1050
- File.open(path, 'rb') do |file|
1051
- @field.value = {
1052
- :tempfile => file,
1053
- :type => "application/pdf",
1054
- :filename => "vimlogo.pdf"
1055
- }
1056
- end
1057
- url = @field.value
1058
- path = File.join File.dirname(Spontaneous.media_dir), url
1059
- assert File.exist?(path), "Media file should have been copied into place"
1060
- end
1061
-
1062
- it "generate the requisite file metadata" do
1063
- File.open(path, 'rb') do |file|
1064
- @field.value = {
1065
- :tempfile => file,
1066
- :type => "application/pdf",
1067
- :filename => "vimlogo.pdf"
1068
- }
1069
- end
1070
- @field.value(:html).must_match %r{/media/.+/vimlogo.pdf$}
1071
- @field.value.must_match %r{/media/.+/vimlogo.pdf$}
1072
- @field.path.must_equal @field.value
1073
- @field.value(:filesize).must_equal 2254
1074
- @field.filesize.must_equal 2254
1075
- @field.value(:filename).must_equal "vimlogo.pdf"
1076
- @field.filename.must_equal "vimlogo.pdf"
1077
- end
1078
-
1079
- it "just accept the given value if passed a path to a non-existant file" do
1080
- @field.value = "/images/nosuchfile.rtf"
1081
- @field.value.must_equal "/images/nosuchfile.rtf"
1082
- @field.filename.must_equal "nosuchfile.rtf"
1083
- @field.filesize.must_equal 0
1084
- end
1085
-
1086
- it "copy the given file if passed a path to an existing file" do
1087
- @field.value = path
1088
- @field.value.must_match %r{/media/.+/vimlogo.pdf$}
1089
- @field.filename.must_equal "vimlogo.pdf"
1090
- @field.filesize.must_equal 2254
1091
- end
1092
-
1093
- it "sets the unprocessed value to a JSON encoded array of MD5 hash & filename" do
1094
- @field.value = path
1095
- @instance.save
1096
- @field.unprocessed_value.must_equal ["vimlogo.pdf", "1de7e866d69c2f56b4a3f59ed1c98b74"].to_json
1097
- end
1098
-
1099
- it "sets the field hash to the MD5 hash of the file" do
1100
- @field.value = path
1101
- @field.file_hash.must_equal "1de7e866d69c2f56b4a3f59ed1c98b74"
1102
- end
1103
-
1104
- it "sets the original filename of the file" do
1105
- @field.value = path
1106
- @field.original_filename.must_equal "vimlogo.pdf"
1107
- end
1108
-
1109
- it "doesn't set the hash of a file that can't be found" do
1110
- @field.value = "/images/nosuchfile.rtf"
1111
- @field.file_hash.must_equal ""
1112
- end
1113
-
1114
- it "sets the original filename of a file that can't be found" do
1115
- @field.value = "/images/nosuchfile.rtf"
1116
- @field.original_filename.must_equal "/images/nosuchfile.rtf"
1117
- end
1118
-
1119
- describe "clearing" do
1120
- def assert_file_field_empty
1121
- @field.value.must_equal ''
1122
- @field.filename.must_equal ''
1123
- @field.filesize.must_equal 0
1124
- end
1125
-
1126
- before do
1127
- path = File.expand_path("../../fixtures/images/vimlogo.pdf", __FILE__)
1128
- @field.value = path
1129
- end
1130
-
1131
- it "clears the value if set to the empty string" do
1132
- @field.value = ''
1133
- assert_file_field_empty
1134
- end
1135
- end
1136
-
1137
- describe "with cloud storage" do
1138
- before do
1139
- ::Fog.mock!
1140
- @aws_credentials = {
1141
- :provider=>"AWS",
1142
- :aws_secret_access_key=>"SECRET_ACCESS_KEY",
1143
- :aws_access_key_id=>"ACCESS_KEY_ID"
1144
- }
1145
- @storage = S::Media::Store::Cloud.new(@aws_credentials, "media.example.com")
1146
- @site.expects(:storage).returns(@storage)
1147
- end
1148
-
1149
- it "sets the content-disposition header if defined as an 'attachment'" do
1150
- prototype = @content_class.field :attachment, :file, attachment: true
1151
- field = @instance.attachment
1152
- path = File.expand_path("../../fixtures/images/vimlogo.pdf", __FILE__)
1153
- @storage.expects(:copy).with(path, is_a(Array), { content_type: "application/pdf", content_disposition: 'attachment; filename=vimlogo.pdf'})
1154
- field.value = path
1155
- end
1156
- end
1157
- end
1158
- describe "Date fields" do
1159
- before do
1160
- @content_class = Class.new(::Piece)
1161
- @prototype = @content_class.field :date
1162
- @content_class.stubs(:name).returns("ContentClass")
1163
- @instance = @content_class.create
1164
- @field = @instance.date
1165
- end
1166
-
1167
- it "have a distinct editor class" do
1168
- @prototype.instance_class.editor_class.must_equal "Spontaneous.Field.Date"
1169
- end
1170
-
1171
- it "adopt any field called 'date'" do
1172
- assert @field.is_a?(Spontaneous::Field::Date), "Field should be an instance of DateField but instead has the following ancestors #{ @prototype.instance_class.ancestors }"
1173
- end
1174
-
1175
- it "default to an empty string" do
1176
- @field.value(:html).must_equal ""
1177
- @field.value(:plain).must_equal ""
1178
- end
1179
-
1180
- it "correctly parse strings" do
1181
- @field.value = "Friday, 8 June, 2012"
1182
- @field.value(:html).must_equal %(<time datetime="2012-06-08">Friday, 8 June, 2012</time>)
1183
- @field.value(:plain).must_equal %(Friday, 8 June, 2012)
1184
- @field.date.must_equal Date.parse("Friday, 8 June, 2012")
1185
- end
1186
-
1187
- it "allow for setting a custom default format" do
1188
- prototype = @content_class.field :datef, :date, :format => "%d %b %Y, %a"
1189
- instance = @content_class.new
1190
- field = instance.datef
1191
- field.value = "Friday, 8 June, 2012"
1192
- field.value(:html).must_equal %(<time datetime="2012-06-08">08 Jun 2012, Fri</time>)
1193
- field.value(:plain).must_equal %(08 Jun 2012, Fri)
1194
- end
1195
- end
1196
-
1197
- describe "Tag list fields" do
1198
- before do
1199
- @content_class = Class.new(::Piece)
1200
- @prototype = @content_class.field :tags
1201
- @content_class.stubs(:name).returns("ContentClass")
1202
- @instance = @content_class.create
1203
- @field = @instance.tags
1204
- end
1205
-
1206
- it "has a distinct editor class" # eventually...
1207
-
1208
- it "adopts any field called 'tags'" do
1209
- assert @field.is_a?(Spontaneous::Field::Tags), "Field should be an instance of TagsField but instead has the following ancestors #{ @prototype.instance_class.ancestors }"
1210
- end
1211
-
1212
- it "defaults to an empty list" do
1213
- @field.value(:html).must_equal ""
1214
- @field.value(:tags).must_equal []
1215
- end
1216
-
1217
- it "correctly parses strings" do
1218
- @field.value = 'this that "the other" more'
1219
- @field.value(:html).must_equal 'this that "the other" more'
1220
- @field.value(:tags).must_equal ["this", "that", "the other", "more"]
1221
- end
1222
-
1223
- it "includes Enumerable" do
1224
- @field.value = 'this that "the other" more'
1225
- @field.map(&:upcase).must_equal ["THIS", "THAT", "THE OTHER", "MORE"]
1226
- end
1227
-
1228
- it "allows for tags with commas" do
1229
- @field.value = %(this that "the, other" more)
1230
- @field.map(&:upcase).must_equal ["THIS", "THAT", "THE, OTHER", "MORE"]
1231
- end
1232
- end
1233
-
1234
- describe "Boolean fields" do
1235
-
1236
- before do
1237
- @content_class = Class.new(::Piece)
1238
- @prototype = @content_class.field :switch
1239
- @content_class.stubs(:name).returns("ContentClass")
1240
- @instance = @content_class.create
1241
- @field = @instance.switch
1242
- end
1243
-
1244
- it "has a distinct editor class" do
1245
- @prototype.instance_class.editor_class.must_equal "Spontaneous.Field.Boolean"
1246
- end
1247
-
1248
- it "adopts any field called 'switch'" do
1249
- assert @field.is_a?(Spontaneous::Field::Boolean), "Field should be an instance of Boolean but instead has the following ancestors #{ @prototype.instance_class.ancestors }"
1250
- end
1251
-
1252
- it "defaults to true" do
1253
- @field.value.must_equal true
1254
- @field.value(:html).must_equal "Yes"
1255
- @field.value(:string).must_equal "Yes"
1256
- end
1257
-
1258
- it "changes string value to 'No'" do
1259
- @field.value = false
1260
- @field.value(:string).must_equal "No"
1261
- end
1262
-
1263
- it "flags itself as 'empty' if false" do # I think...
1264
- @field.empty?.must_equal false
1265
- @field.value = false
1266
- @field.empty?.must_equal true
1267
- end
1268
-
1269
- it "uses the given state labels" do
1270
- prototype = @content_class.field :boolean, true: "Enabled", false: "Disabled"
1271
- field = prototype.to_field(@instance)
1272
- field.value.must_equal true
1273
- field.value(:string).must_equal "Enabled"
1274
- field.value = false
1275
- field.value(:string).must_equal "Disabled"
1276
- field.value(:html).must_equal "Disabled"
1277
- end
1278
-
1279
- it "uses the given default" do
1280
- prototype = @content_class.field :boolean, default: false, true: "On", false: "Off"
1281
- field = prototype.to_field(@instance)
1282
- field.value.must_equal false
1283
- field.value(:string).must_equal "Off"
1284
- end
1285
-
1286
- it "returns the string value from #to_s" do
1287
- prototype = @content_class.field :boolean, default: false, true: "On", false: "Off"
1288
- field = prototype.to_field(@instance)
1289
- field.to_s.must_equal "Off"
1290
- end
1291
-
1292
- it "has shortcut accessors" do
1293
- state = @field.value(:boolean)
1294
- @field.on?.must_equal state
1295
- @field.checked?.must_equal state
1296
- @field.enabled?.must_equal state
1297
- end
1298
-
1299
- it "exports the labels to the interface" do
1300
- prototype = @content_class.field :boolean, default: false, true: "Yes Please", false: "No Thanks"
1301
- exported = prototype.instance_class.export(nil)
1302
- exported.must_equal({:labels=>{:true=>"Yes Please", :false=>"No Thanks"}})
1303
- end
1304
- end
1305
-
1306
663
  describe "Asynchronous processing" do
1307
664
  before do
1308
665
  @site.background_mode = :simultaneous
@@ -1427,6 +784,7 @@ describe "Fields" do
1427
784
  :version => 1,
1428
785
  :value => {
1429
786
  :width=>50, :height=>67, :dimensions => [50,67], :filesize=>3951,
787
+ :storage_name=>"default",
1430
788
  :type=>"image/gif", :format => "gif",
1431
789
  :tempfile=>"#{@site.root}/cache/media/tmp/#{field.media_id}/something.gif",
1432
790
  :filename=>"something.gif",
@@ -1453,6 +811,7 @@ describe "Fields" do
1453
811
  :version => 1,
1454
812
  :value => {
1455
813
  :width=>50, :height=>67, :dimensions => [50,67], :filesize=>3951,
814
+ :storage_name=>"default",
1456
815
  :type=>"image/gif", :format => "gif",
1457
816
  :tempfile=>"#{@site.root}/cache/media/tmp/#{field.media_id}/something.gif",
1458
817
  :filename=>"something.gif",
@@ -1498,6 +857,7 @@ describe "Fields" do
1498
857
  :version => 1,
1499
858
  :value => {
1500
859
  :width=>50, :height=>67, :dimensions => [50,67], :filesize=>3951,
860
+ :storage_name=>"default",
1501
861
  :type=>"image/gif", :format => "gif",
1502
862
  :tempfile=>"#{@site.root}/cache/media/tmp/#{field.media_id}/something.gif",
1503
863
  :filename=>"something.gif",
@@ -1522,6 +882,7 @@ describe "Fields" do
1522
882
  :version => 1,
1523
883
  :value => {
1524
884
  :width=>50, :height=>67, :dimensions => [50,67], :filesize=>3951,
885
+ :storage_name=>"default",
1525
886
  :type=>"image/gif", :format => "gif",
1526
887
  :tempfile=>"#{@site.root}/cache/media/tmp/#{field.media_id}/something.gif",
1527
888
  :filename=>"something.gif",
@@ -1607,6 +968,22 @@ describe "Fields" do
1607
968
  end
1608
969
  end
1609
970
 
971
+ it "includes a local temp url for fields with pending values" do
972
+ field = @instance.image
973
+ Spontaneous::Simultaneous.expects(:fire).with(:update_fields, {
974
+ "fields" => [field.id]
975
+ })
976
+ File.open(@image, "r") do |file|
977
+ field.pending_version.must_equal 0
978
+ Spontaneous::Field.set(@site, field, {:tempfile => file, :filename => "something.gif", :type => "image/gif"}, nil, true)
979
+ export = field.export
980
+ values = export[:processed_value]
981
+ assert values.key?(:__pending__)
982
+ pending = values[:__pending__][:value]
983
+ pending[:src].must_match %r{^/media/}
984
+ end
985
+ end
986
+
1610
987
  describe "page locks" do
1611
988
  before do
1612
989
  @now = Time.now