spontaneous 0.2.0.beta5 → 0.2.0.beta6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (227) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +39 -0
  4. data/Gemfile +2 -0
  5. data/Readme.markdown +4 -4
  6. data/application/css/core.css.scss +144 -43
  7. data/application/css/definitions.css.scss +50 -16
  8. data/application/css/dialogue.css.scss +5 -2
  9. data/application/css/editing.css.scss +7 -7
  10. data/application/css/font.css.scss +1 -1
  11. data/application/css/meta.css.scss +6 -6
  12. data/application/css/popover.css.scss +6 -6
  13. data/application/css/top.css.scss +8 -1
  14. data/application/js/add_alias_dialogue.js +137 -36
  15. data/application/js/add_home_dialogue.js +10 -10
  16. data/application/js/ajax.js +26 -26
  17. data/application/js/authentication.js +2 -2
  18. data/application/js/box.js +21 -10
  19. data/application/js/box_container.js +13 -7
  20. data/application/js/compatibility.js +19 -17
  21. data/application/js/conflicted_field_dialogue.js +5 -5
  22. data/application/js/content.js +22 -16
  23. data/application/js/content_area.js +62 -33
  24. data/application/js/dialogue.js +16 -16
  25. data/application/js/dom.js +9 -10
  26. data/application/js/edit_panel.js +25 -20
  27. data/application/js/editing.js +21 -8
  28. data/application/js/entry.js +1 -1
  29. data/application/js/extensions.js +11 -11
  30. data/application/js/field/boolean.js +6 -6
  31. data/application/js/field/date.js +1 -1
  32. data/application/js/field/file.js +17 -17
  33. data/application/js/field/image.js +27 -27
  34. data/application/js/field/markdown.js +72 -71
  35. data/application/js/field/select.js +9 -9
  36. data/application/js/field/string.js +3 -3
  37. data/application/js/field/webvideo.js +2 -2
  38. data/application/js/field_preview.js +3 -0
  39. data/application/js/init.js +3 -2
  40. data/application/js/jquery-selection-position.js +13 -13
  41. data/application/js/location.js +17 -12
  42. data/application/js/login.js +2 -2
  43. data/application/js/meta_view/user_admin.js +101 -101
  44. data/application/js/metadata.js +1 -1
  45. data/application/js/page.js +2 -2
  46. data/application/js/page_browser.js +13 -13
  47. data/application/js/page_entry.js +1 -1
  48. data/application/js/panel/root_menu.js +10 -10
  49. data/application/js/popover.js +6 -5
  50. data/application/js/popover_view.js +5 -5
  51. data/application/js/preview.js +10 -4
  52. data/application/js/progress.js +6 -6
  53. data/application/js/properties.js +35 -6
  54. data/application/js/publish.js +43 -43
  55. data/application/js/require.js +14 -14
  56. data/application/js/services.js +3 -3
  57. data/application/js/sharded_upload.js +9 -8
  58. data/application/js/side_bar.js +5 -5
  59. data/application/js/state.js +2 -2
  60. data/application/js/status_bar.js +6 -6
  61. data/application/js/top_bar.js +97 -65
  62. data/application/js/types.js +9 -6
  63. data/application/js/upload.js +4 -4
  64. data/application/js/upload_manager.js +21 -21
  65. data/application/js/user.js +1 -1
  66. data/application/js/vendor/jquery.velocity.min.js +7 -0
  67. data/application/js/views.js +32 -8
  68. data/application/js/views/box_view.js +51 -31
  69. data/application/js/views/page_piece_view.js +17 -15
  70. data/application/js/views/page_view.js +54 -43
  71. data/application/js/views/piece_view.js +44 -37
  72. data/application/static/font/fontawesome-webfont-4f0022f25672c7f501c339cbf98d9117.ttf +0 -0
  73. data/application/views/index.erb +1 -0
  74. data/db/migrations/20130114120000_create_revision_tables.rb +2 -1
  75. data/db/migrations/20130813111009_increase_path_length.rb +11 -2
  76. data/db/migrations/20140506171823_add_index_to_target_id.rb +11 -0
  77. data/db/migrations/20140514090204_add_content_hash.rb +59 -0
  78. data/db/migrations/20140519150253_add_content_hash_timestamp.rb +20 -0
  79. data/lib/spontaneous.rb +0 -1
  80. data/lib/spontaneous/asset/environment.rb +77 -15
  81. data/lib/spontaneous/box.rb +21 -0
  82. data/lib/spontaneous/capistrano/deploy.rb +1 -1
  83. data/lib/spontaneous/capistrano/sync.rb +8 -7
  84. data/lib/spontaneous/change.rb +4 -2
  85. data/lib/spontaneous/cli/fields.rb +7 -3
  86. data/lib/spontaneous/cli/generate.rb +2 -0
  87. data/lib/spontaneous/cli/init.rb +24 -93
  88. data/lib/spontaneous/cli/init/db.rb +94 -0
  89. data/lib/spontaneous/cli/init/mysql.rb +17 -0
  90. data/lib/spontaneous/cli/init/postgresql.rb +33 -0
  91. data/lib/spontaneous/cli/init/sqlite.rb +14 -0
  92. data/lib/spontaneous/cli/site.rb +45 -20
  93. data/lib/spontaneous/collections/box_set.rb +3 -0
  94. data/lib/spontaneous/collections/entry_set.rb +43 -4
  95. data/lib/spontaneous/collections/field_set.rb +14 -2
  96. data/lib/spontaneous/data_mapper.rb +40 -7
  97. data/lib/spontaneous/data_mapper/content_model.rb +1 -1
  98. data/lib/spontaneous/data_mapper/content_model/associations.rb +63 -12
  99. data/lib/spontaneous/data_mapper/content_model/timestamps.rb +9 -14
  100. data/lib/spontaneous/data_mapper/content_table.rb +4 -2
  101. data/lib/spontaneous/data_mapper/dataset.rb +31 -2
  102. data/lib/spontaneous/data_mapper/scope.rb +37 -20
  103. data/lib/spontaneous/errors.rb +6 -0
  104. data/lib/spontaneous/facet.rb +20 -10
  105. data/lib/spontaneous/field/base.rb +8 -1
  106. data/lib/spontaneous/field/file.rb +28 -3
  107. data/lib/spontaneous/field/image.rb +2 -0
  108. data/lib/spontaneous/field/update.rb +6 -0
  109. data/lib/spontaneous/field/webvideo/vimeo.rb +6 -1
  110. data/lib/spontaneous/field/webvideo/vine.rb +1 -1
  111. data/lib/spontaneous/field/webvideo/youtube.rb +1 -1
  112. data/lib/spontaneous/generators/site.rb +6 -4
  113. data/lib/spontaneous/generators/site/.gitignore +1 -0
  114. data/lib/spontaneous/generators/site/Gemfile.tt +3 -3
  115. data/lib/spontaneous/generators/site/config/{indexes.rb.tt → initializers/indexes.rb.tt} +0 -0
  116. data/lib/spontaneous/generators/site/config/initializers/publishing.rb.tt +78 -0
  117. data/lib/spontaneous/generators/site/{config/database.yml.tt → db/mysql2.yml.tt} +7 -6
  118. data/lib/spontaneous/generators/site/db/postgres.yml.tt +25 -0
  119. data/lib/spontaneous/generators/site/db/sqlite3.yml.tt +18 -0
  120. data/lib/spontaneous/generators/site/public/humans.txt.tt +14 -0
  121. data/lib/spontaneous/generators/site/templates/layouts/standard.html.cut.tt +51 -0
  122. data/lib/spontaneous/loader.rb +1 -1
  123. data/lib/spontaneous/logger.rb +1 -1
  124. data/lib/spontaneous/media/image/optimizer.rb +1 -1
  125. data/lib/spontaneous/media/image/processor.rb +11 -2
  126. data/lib/spontaneous/media/image/renderable.rb +2 -0
  127. data/lib/spontaneous/model.rb +3 -0
  128. data/lib/spontaneous/model/box/allowed_types.rb +17 -4
  129. data/lib/spontaneous/model/core.rb +36 -3
  130. data/lib/spontaneous/model/core/aliases.rb +5 -2
  131. data/lib/spontaneous/model/core/boxes.rb +6 -0
  132. data/lib/spontaneous/model/core/cascading_change.rb +38 -0
  133. data/lib/spontaneous/model/core/content_hash.rb +171 -0
  134. data/lib/spontaneous/model/core/entries.rb +0 -19
  135. data/lib/spontaneous/model/core/fields.rb +11 -0
  136. data/lib/spontaneous/model/core/modifications.rb +22 -21
  137. data/lib/spontaneous/model/core/render.rb +3 -0
  138. data/lib/spontaneous/model/core/serialisation.rb +18 -17
  139. data/lib/spontaneous/model/page.rb +35 -8
  140. data/lib/spontaneous/model/page/page_tree.rb +20 -8
  141. data/lib/spontaneous/model/page/paths.rb +79 -50
  142. data/lib/spontaneous/model/page/singleton.rb +71 -0
  143. data/lib/spontaneous/model/page/site_map.rb +2 -1
  144. data/lib/spontaneous/model/page/site_timestamps.rb +2 -2
  145. data/lib/spontaneous/model/piece.rb +10 -0
  146. data/lib/spontaneous/output/context.rb +13 -6
  147. data/lib/spontaneous/output/format.rb +30 -5
  148. data/lib/spontaneous/output/helpers/script_helper.rb +8 -0
  149. data/lib/spontaneous/output/helpers/stylesheet_helper.rb +7 -0
  150. data/lib/spontaneous/output/renderable.rb +16 -0
  151. data/lib/spontaneous/output/store.rb +1 -1
  152. data/lib/spontaneous/output/template/renderer.rb +2 -2
  153. data/lib/spontaneous/page_piece.rb +25 -1
  154. data/lib/spontaneous/prototypes/box_prototype.rb +13 -0
  155. data/lib/spontaneous/prototypes/field_prototype.rb +7 -4
  156. data/lib/spontaneous/publishing.rb +10 -5
  157. data/lib/spontaneous/publishing/immediate.rb +32 -349
  158. data/lib/spontaneous/publishing/pipeline.rb +43 -0
  159. data/lib/spontaneous/publishing/progress.rb +186 -0
  160. data/lib/spontaneous/publishing/publish.rb +107 -0
  161. data/lib/spontaneous/publishing/rerender.rb +17 -0
  162. data/lib/spontaneous/publishing/revision.rb +53 -18
  163. data/lib/spontaneous/publishing/simultaneous.rb +1 -1
  164. data/lib/spontaneous/publishing/steps.rb +154 -0
  165. data/lib/spontaneous/publishing/steps/activate_revision.rb +45 -0
  166. data/lib/spontaneous/publishing/steps/archive_old_revisions.rb +22 -0
  167. data/lib/spontaneous/publishing/steps/base_step.rb +49 -0
  168. data/lib/spontaneous/publishing/steps/copy_static_files.rb +74 -0
  169. data/lib/spontaneous/publishing/steps/create_revision_directory.rb +24 -0
  170. data/lib/spontaneous/publishing/steps/generate_rackup_file.rb +51 -0
  171. data/lib/spontaneous/publishing/steps/generate_search_indexes.rb +24 -0
  172. data/lib/spontaneous/publishing/steps/render_revision.rb +69 -0
  173. data/lib/spontaneous/publishing/steps/write_revision_file.rb +43 -0
  174. data/lib/spontaneous/rack/back.rb +3 -1
  175. data/lib/spontaneous/rack/back/alias.rb +9 -8
  176. data/lib/spontaneous/rack/front.rb +1 -1
  177. data/lib/spontaneous/rack/middleware.rb +7 -4
  178. data/lib/spontaneous/rack/middleware/transaction.rb +14 -0
  179. data/lib/spontaneous/rack/page_controller.rb +23 -8
  180. data/lib/spontaneous/revision.rb +5 -10
  181. data/lib/spontaneous/schema.rb +5 -0
  182. data/lib/spontaneous/server.rb +3 -1
  183. data/lib/spontaneous/site.rb +17 -10
  184. data/lib/spontaneous/site/publishing.rb +25 -3
  185. data/lib/spontaneous/site/state.rb +7 -3
  186. data/lib/spontaneous/tasks/database.rake +5 -10
  187. data/lib/spontaneous/utils/database/mysql_dumper.rb +5 -1
  188. data/lib/spontaneous/version.rb +1 -1
  189. data/spontaneous.gemspec +4 -3
  190. data/test/fixtures/example_application/config/initializers/initializer1.rb +1 -0
  191. data/test/fixtures/example_application/config/initializers/initializer2.rb +1 -0
  192. data/test/fixtures/example_application/config/initializers/publishing.rb +13 -0
  193. data/test/fixtures/search/config/{indexes.rb → initializers/indexes.rb} +0 -0
  194. data/test/fixtures/serialisation/root_hash.yaml.erb +10 -4
  195. data/test/functional/test_application.rb +10 -0
  196. data/test/functional/test_back.rb +23 -5
  197. data/test/functional/test_cli.rb +98 -34
  198. data/test/functional/test_front.rb +7 -3
  199. data/test/test_helper.rb +35 -28
  200. data/test/unit/test_alias.rb +20 -3
  201. data/test/unit/test_assets.rb +58 -30
  202. data/test/unit/test_changesets.rb +20 -12
  203. data/test/unit/test_content_hash.rb +496 -0
  204. data/test/unit/test_context.rb +28 -1
  205. data/test/unit/test_controllers.rb +96 -61
  206. data/test/unit/test_crypt.rb +1 -8
  207. data/test/unit/test_datamapper.rb +95 -19
  208. data/test/unit/test_features.rb +1 -4
  209. data/test/unit/test_fields.rb +61 -12
  210. data/test/unit/test_generators.rb +39 -2
  211. data/test/unit/test_images.rb +3 -1
  212. data/test/unit/test_modifications.rb +224 -219
  213. data/test/unit/test_output_store.rb +10 -0
  214. data/test/unit/{test_formats.rb → test_outputs.rb} +75 -6
  215. data/test/unit/test_page.rb +61 -15
  216. data/test/unit/test_plugins.rb +2 -42
  217. data/test/unit/test_publishing_pipeline.rb +1050 -0
  218. data/test/unit/test_render.rb +30 -0
  219. data/test/unit/test_revisions.rb +110 -2
  220. data/test/unit/test_schema.rb +4 -0
  221. data/test/unit/test_search.rb +1 -1
  222. data/test/unit/test_serialisation.rb +6 -1
  223. data/test/unit/test_singletons.rb +159 -0
  224. data/test/unit/test_site.rb +71 -44
  225. metadata +140 -86
  226. data/application/static/font/fontawesome-webfont-1c66a4738b40ef0f6b1abca0ba9a796d.ttf +0 -0
  227. data/test/unit/test_publishing.rb +0 -330
@@ -69,6 +69,16 @@ describe "Output store" do
69
69
  ::File.read(::File.join(revision_path, 'dynamic', 'one.html')).must_equal "*template*"
70
70
  end
71
71
 
72
+ it "puts private roots in files starting with '#'" do
73
+ store.store_static(revision, "#one.html", "*template*")
74
+ ::File.read(::File.join(revision_path, 'static', '#one.html')).must_equal "*template*"
75
+ end
76
+
77
+ it "puts private files in directories starting with '#'" do
78
+ store.store_static(revision, "#private-tree/one.html", "*template*")
79
+ ::File.read(::File.join(revision_path, 'static', '#private-tree', 'one.html')).must_equal "*template*"
80
+ end
81
+
72
82
  it "enables the retrieval of available revisions" do
73
83
  store.store_static(1, "/one.html", "*template*")
74
84
  store.store_protected(2, "/one.html", "*template*")
@@ -2,18 +2,23 @@
2
2
 
3
3
  require File.expand_path('../../test_helper', __FILE__)
4
4
 
5
- describe "Formats" do
5
+ describe "Outputs" do
6
+ before do
7
+ @site = setup_site
8
+ @site.paths.add :templates, File.expand_path('../../fixtures/outputs/templates', __FILE__)
9
+ end
10
+
11
+ after do
12
+ teardown_site
13
+ end
14
+
6
15
  describe "Pages" do
7
16
  before do
8
- @site = setup_site
9
- @site.paths.add :templates, File.expand_path('../../fixtures/outputs/templates', __FILE__)
10
- # class Page < ::Page; end
11
17
  class FPage < ::Page; end
12
18
  end
13
19
 
14
20
  after do
15
21
  Object.send(:remove_const, :FPage) rescue nil
16
- teardown_site
17
22
  end
18
23
 
19
24
  describe "default output" do
@@ -260,6 +265,23 @@ describe "Formats" do
260
265
  output = @page.output(:html)
261
266
  @page.output(output).must_equal output
262
267
  end
268
+
269
+ it "marks identical outputs from identical pages as equal" do
270
+ assert @page.output(:html) == @page.output(:html)
271
+ end
272
+
273
+ it "marks identical outputs from different pages as different" do
274
+ page = FPage.create
275
+ refute @page.output(:html) == page.output(:html)
276
+ end
277
+
278
+ it "marks different outputs from same page as different" do
279
+ refute @page.output(:html) == @page.output(:pdf)
280
+ end
281
+
282
+ it "gives identical hashes for identical outputs" do
283
+ assert @page.output(:html).hash == @page.output(:html).hash
284
+ end
263
285
  end
264
286
 
265
287
  describe "with custom formats" do
@@ -295,7 +317,7 @@ describe "Formats" do
295
317
  it "allow custom post-processing of render" do
296
318
  FPage.add_output :atom, :postprocess => lambda { |page, output| output.gsub(/a/, 'x') }
297
319
  page = FPage.new
298
- page.render(:atom).must_match /<xtom>/
320
+ page.render(:atom).must_match %r{<xtom>}
299
321
  end
300
322
  end
301
323
 
@@ -370,4 +392,51 @@ describe "Formats" do
370
392
  end
371
393
  end
372
394
  end
395
+
396
+ describe "publishing" do
397
+ let(:revision) { 2 }
398
+ let(:renderer) { Spontaneous::Output::Template::PublishRenderer.new(@site, false) }
399
+ let(:transaction) { mock }
400
+ let(:page) { P.new(title: "Godot") }
401
+ let(:output) { page.output(:html) }
402
+
403
+ before do
404
+ class ::P < ::Page
405
+ field :title
406
+ end
407
+ end
408
+ after do
409
+ Object.send :remove_const, :P
410
+ end
411
+
412
+ it "renders static layouts for static pages" do
413
+ P.layout { "'${ title }'" }
414
+ transaction.expects(:store).with(output, false, "'Godot'")
415
+ output.publish_page(renderer, revision, transaction)
416
+ end
417
+
418
+ it "renders dynamic layouts for static pages" do
419
+ P.layout { "'${ title }' {{Time.now.to_i}}" }
420
+ transaction.expects(:store).with(output, true, "'Godot' {{Time.now.to_i}}")
421
+ output.publish_page(renderer, revision, transaction)
422
+ end
423
+
424
+ it "renders static layouts for dynamic pages" do
425
+ P.controller do
426
+ get { "Hello" }
427
+ end
428
+ P.layout { "'${ title }'" }
429
+ transaction.expects(:store).with(output, false, "'Godot'")
430
+ output.publish_page(renderer, revision, transaction)
431
+ end
432
+
433
+ it "renders dynamic layouts for dynamic pages" do
434
+ P.controller do
435
+ get { "Hello" }
436
+ end
437
+ P.layout { "'${ title }' {{Time.now.to_i}}" }
438
+ transaction.expects(:store).with(output, true, "'Godot' {{Time.now.to_i}}")
439
+ output.publish_page(renderer, revision, transaction)
440
+ end
441
+ end
373
442
  end
@@ -36,7 +36,7 @@ describe "Page" do
36
36
  end
37
37
  end
38
38
 
39
- describe "invisible roots" do
39
+ describe "private roots" do
40
40
  before do
41
41
  @root = Page.create
42
42
  assert @root.root?
@@ -58,7 +58,9 @@ describe "Page" do
58
58
  page = Page.new slug: "404"
59
59
  root.pages << page
60
60
  root.save
61
+ page.save
61
62
  root.path.must_equal "#error"
63
+ page.reload.path.must_equal "#error/404"
62
64
  root.slug = "changed"
63
65
  root.save
64
66
  root.path.must_equal "#changed"
@@ -87,7 +89,47 @@ describe "Page" do
87
89
  Page.root.must_equal nil
88
90
  root = ErrorPage.create_root "error"
89
91
  Page.root.must_equal nil
90
- @site["#error"].must_equal root
92
+ @site["#error"].must_equal root.reload
93
+ end
94
+
95
+ it "gives children of invisible roots the correct root page" do
96
+ root = ErrorPage.create_root "error"
97
+ child = ::Page.new
98
+ root.pages << child
99
+ root.save
100
+ child.save
101
+ child.tree_root.must_equal root
102
+ end
103
+
104
+ it "allows you to test if the page is an invisible root" do
105
+ @root.is_private_root?.must_equal false
106
+ child = ::Page.new
107
+ @root.sub << child
108
+ child.save
109
+ child.is_private_root?.must_equal false
110
+ invisible_root = ErrorPage.create_root "error"
111
+ invisible_root.is_private_root?.must_equal true
112
+ child = ::Page.new
113
+ invisible_root.pages << child
114
+ invisible_root.save
115
+ child.save
116
+ child.is_private_root?.must_equal false
117
+ end
118
+
119
+ it "allows you to test if a page belongs to an invisible sub-tree" do
120
+ @root.in_private_tree?.must_equal false
121
+ child = ::Page.new
122
+ @root.sub << child
123
+ child.save
124
+ child.in_private_tree?.must_equal false
125
+
126
+ invisible_root = ErrorPage.create_root "error"
127
+ invisible_root.in_private_tree?.must_equal true
128
+ child = ::Page.new
129
+ invisible_root.pages << child
130
+ invisible_root.save
131
+ child.save
132
+ child.in_private_tree?.must_equal true
91
133
  end
92
134
  end
93
135
 
@@ -154,13 +196,13 @@ describe "Page" do
154
196
  r.sub << o
155
197
  o.save
156
198
 
157
- p = Page.create(:title => "New Page")
158
- r.sub << p
159
- p.save
160
- p.slug = "my-slug"
161
- p.save
162
- o.slug.wont_equal p.slug
163
- p.path.must_equal "/my-slug-01"
199
+ page = Page.create(:title => "New Page")
200
+ r.sub << page
201
+ page.save
202
+ page.slug = "my-slug"
203
+ page.save
204
+ o.slug.wont_equal page.slug
205
+ page.path.must_equal "/my-slug-01"
164
206
  end
165
207
 
166
208
  it "fixes conflicting slugs created from titles automatically" do
@@ -229,6 +271,10 @@ describe "Page" do
229
271
  @t = Page[@t.id]
230
272
  end
231
273
 
274
+ it "knows its rootiness" do
275
+ assert @p.is_public_root?
276
+ end
277
+
232
278
  it "be able to find a reference to their inline entry" do
233
279
  @q.entry.class.must_equal Spontaneous::PagePiece
234
280
  end
@@ -442,9 +488,9 @@ describe "Page" do
442
488
  @child = Page.new
443
489
  @parent.things << @piece
444
490
  @piece.things << @child
445
- @parent.save
446
- @piece.save
447
- @child.save
491
+ @parent.save.reload
492
+ @piece.save.reload
493
+ @child.save.reload
448
494
  @page_piece = @parent.things.first.things.first
449
495
  end
450
496
 
@@ -455,7 +501,7 @@ describe "Page" do
455
501
  end
456
502
 
457
503
  it "know their page" do
458
- @page_piece.page.must_equal @parent
504
+ @page_piece.page.reload.must_equal @parent.reload
459
505
  end
460
506
 
461
507
  it "know their container" do
@@ -467,11 +513,11 @@ describe "Page" do
467
513
  end
468
514
 
469
515
  it "know their parent" do
470
- @page_piece.parent.must_equal @piece
516
+ @page_piece.parent.reload.must_equal @piece
471
517
  end
472
518
 
473
519
  it "know their owner" do
474
- @page_piece.owner.must_equal @piece
520
+ @page_piece.owner.reload.must_equal @piece
475
521
  end
476
522
 
477
523
  it "tests as equal to the page target" do
@@ -82,51 +82,11 @@ describe "Plugins" do
82
82
  Spontaneous.mode = :back
83
83
  get "/schema_plugin/subdir/sass.css"
84
84
  assert last_response.ok?, "Static file /schema_plugin/subdir/sass.css returned error code #{last_response.status}"
85
- last_response.body.must_match /^\s+color: #005a55;/
86
- last_response.body.must_match /^\s+padding: 42px;/
87
- end
88
-
89
- describe "during publishing" do
90
- before do
91
- Content.delete_revision(1) rescue nil
92
-
93
- Spontaneous.logger.silent! {
94
- @site.publish_all
95
- }
96
- end
97
-
98
- after do
99
- FileUtils.rm_rf(@revision_root) rescue nil
100
- Content.delete
101
- S::State.delete
102
- Content.delete_revision(1)
103
- end
104
-
105
- it "have their public files copied into the revision sandbox" do
106
- @static.each do |file|
107
- path = File.join(@site.revision_root, '00001', 'public', 'schema_plugin', file)
108
- assert File.exists?(path), "File '#{path}' should exist"
109
- end
110
- end
111
-
112
- it "have their SASS & Less templates rendered to static css" do
113
- sass_files =['subdir/sass.css']
114
- sass_files.each do |file|
115
- path = File.join(@site.revision_root, '00001', 'public', 'schema_plugin', file)
116
- assert File.exists?(path), "File '#{path}' should exist"
117
- css = File.read(path)
118
- css.must_match /color:#005a55;?/
119
- css.must_match /padding:42px;?/
120
- end
121
- end
85
+ last_response.body.must_match %r{^\s+color: #005a55;}
86
+ last_response.body.must_match %r{^\s+padding: 42px;}
122
87
  end
123
88
  end
124
89
 
125
-
126
- # describe "Functional plugins" do
127
- # # do I need anything here?
128
- # end
129
-
130
90
  describe "with schemas" do
131
91
  it "make content classes available to rest of app" do
132
92
  defined?(::SchemaPlugin).must_equal "constant"
@@ -0,0 +1,1050 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.expand_path('../../test_helper', __FILE__)
4
+
5
+ describe "Publishing Pipeline" do
6
+ let(:now) { Time.now }
7
+ let(:later) { now + 3600 }
8
+ let(:site) { @site }
9
+
10
+ start do
11
+ @site_root = Dir.mktmpdir
12
+ revision = 3
13
+ let(:site_root) { @site_root }
14
+ let(:revision) { revision }
15
+ template_source = File.expand_path(File.dirname(__FILE__) / "../fixtures/templates/publishing/templates")
16
+ FileUtils.cp_r(template_source, @site_root)
17
+
18
+ S::State.delete
19
+ S::State.create(revision: revision, published_revision: (revision - 1))
20
+ end
21
+
22
+ finish do
23
+ FileUtils.rm_r(@site_root)
24
+ Timecop.return
25
+ end
26
+
27
+ before do
28
+ Timecop.freeze(now)
29
+ @site = setup_site(site_root)
30
+ @site.background_mode = :immediate
31
+ @output_store = @site.output_store(:Memory)
32
+ S::State.revision.must_equal revision
33
+
34
+ class Page
35
+ field :title, :string, :default => "New Page"
36
+ box :things
37
+ add_output :xml
38
+ layout(:html) { "=${title}.html" }
39
+ layout(:xml) { "=${title}.xml" }
40
+ end
41
+
42
+ @pages = []
43
+ @pages << [@root = Page.create(title: "*Root*"), "*Root*"]
44
+ (0..3).inject(@root) do |parent, n|
45
+ page = Page.create(title: "Page #{n}", slug: "page-#{n}")
46
+ parent.things << page
47
+ @pages << [page, page.title.value]
48
+ page
49
+ end
50
+ @pages.each { |page, title| page.save }
51
+ @site.model.publish_all(revision)
52
+ end
53
+
54
+ after do
55
+ Content.delete
56
+ teardown_site(false)
57
+ end
58
+
59
+ def call(pages = nil)
60
+ step.call(@site, revision, pages)
61
+ end
62
+
63
+ describe "Progress" do
64
+ let(:total_steps) { 150 }
65
+ let(:progress) { progress_class.new }
66
+ let(:duration) { 205.635 }
67
+ let(:later) { now + duration } # 3m 25.635s
68
+
69
+ before do
70
+ progress.start(total_steps)
71
+ end
72
+
73
+ describe "Silent" do
74
+ let(:progress_class) { Spontaneous::Publishing::Progress::Silent }
75
+
76
+
77
+ it "starts at 0%" do
78
+ progress.percentage.must_equal 0.0
79
+ end
80
+
81
+ it "calculates progress % for a 1 step" do
82
+ progress.step
83
+ progress.position.must_equal 1
84
+ progress.percentage.must_equal 0.67
85
+ end
86
+
87
+ it "increases position by 1 for each step" do
88
+ 75.times { progress.step }
89
+ progress.position.must_equal 75
90
+ progress.percentage.must_equal 50.0
91
+ end
92
+
93
+ it "calculates progress % for an arbitrary step" do
94
+ progress.step(15)
95
+ progress.position.must_equal 15
96
+ progress.percentage.must_equal 10.0
97
+ end
98
+
99
+ it "calculates duration correctly" do
100
+ _ = progress # initialize the progress instance with time set to `now`
101
+ Timecop.travel(later) do |time|
102
+ progress.duration.round(1).must_equal duration.round(1)
103
+ end
104
+ end
105
+
106
+ it "gives a readable version of the duration" do
107
+ _ = progress # initialize the progress instance with time set to `now`
108
+ Timecop.travel(later) do |time|
109
+ progress.duration.to_s.must_equal "00h 03m 25.64s"
110
+ end
111
+ end
112
+
113
+ it "gives a readable version of the duration" do
114
+ _ = progress # initialize the progress instance with time set to `now`
115
+ h = 3; m = 36; s = 53.862
116
+ Timecop.travel(now + (h * 3600) + (m * 60) + s) do |time|
117
+ progress.duration.to_s.must_equal "03h 36m 53.86s"
118
+ end
119
+ end
120
+ end
121
+
122
+ describe "Multi" do
123
+ let(:progress_class) { Spontaneous::Publishing::Progress::Multi }
124
+ let(:progress1) { mock.tap { |m| m.stubs(:start) } }
125
+ let(:progress2) { mock.tap { |m| m.stubs(:start) } }
126
+ let(:progress) { progress_class.new(progress1, progress2) }
127
+
128
+ it "passes all calls to start onto the progress children" do
129
+ progress1.expects(:start).with(99)
130
+ progress2.expects(:start).with(99)
131
+ progress.start(99)
132
+ end
133
+
134
+ it "passes all calls to log onto the progress children" do
135
+ progress1.expects(:log).with("A message")
136
+ progress2.expects(:log).with("A message")
137
+ progress.log("A message")
138
+ end
139
+
140
+ it "passes all calls to step onto the progress children" do
141
+ progress1.expects(:step).with(1, "")
142
+ progress2.expects(:step).with(1, "")
143
+ progress.step
144
+ end
145
+
146
+ it "passes all arguments to step onto the progress children" do
147
+ progress1.expects(:step).with(23, "fish")
148
+ progress2.expects(:step).with(23, "fish")
149
+ progress.step(23, "fish")
150
+ end
151
+ it "passes calls to #stage onto the children" do
152
+ progress1.expects(:stage).with("doing")
153
+ progress2.expects(:stage).with("doing")
154
+ progress.stage("doing")
155
+ end
156
+ it "passes calls to #error onto the children" do
157
+ progress1.expects(:error).with("doing")
158
+ progress2.expects(:error).with("doing")
159
+ progress.error("doing")
160
+ end
161
+ it "passes calls to #done onto the children" do
162
+ progress1.expects(:done)
163
+ progress2.expects(:done)
164
+ progress.done
165
+ end
166
+ end
167
+ end
168
+
169
+ def run_step(progress = Spontaneous::Publishing::Progress::Silent.new)
170
+ # the overall publish coordinator will ensure that every step runs within the right scope
171
+ @site.model.scope(revision, true) do
172
+ step.call(@site, revision, nil, progress)
173
+ end
174
+ end
175
+
176
+ describe "CreateRevisionDirectory" do
177
+ let(:step) { Spontaneous::Publishing::Steps::CreateRevisionDirectory }
178
+ let(:path) { @site.revision_dir(revision) }
179
+
180
+ it "has the right shortcut name" do
181
+ step.to_sym.must_equal :create_revision_directory
182
+ end
183
+
184
+ it "creates the revision directory" do
185
+ refute File.exist?(path)
186
+ run_step
187
+ assert File.exist?(path)
188
+ end
189
+
190
+ it "creates a tmp dir for the revision" do
191
+ refute File.exist?(path / "tmp")
192
+ run_step
193
+ assert File.exist?(path / "tmp")
194
+ end
195
+
196
+ it "returns a step count of 1" do
197
+ step.count(@site, revision, nil).must_equal 1
198
+ end
199
+
200
+ it "updates the progress object" do
201
+ progress = mock()
202
+ progress.expects(:stage).with("creating revision directory")
203
+ progress.expects(:step).with(1, instance_of(String)).once
204
+ run_step(progress)
205
+ end
206
+
207
+ it "deletes the path on rollback" do
208
+ instance = run_step
209
+ instance.rollback
210
+ refute File.exist?(path)
211
+ end
212
+
213
+ it "runs rollback after throwing an exception" do
214
+ instance = mock
215
+ step.expects(:new).returns(instance)
216
+ instance.expects(:call).raises(Exception)
217
+ instance.expects(:rollback)
218
+ lambda{ run_step }.must_raise(Exception)
219
+ end
220
+ end
221
+
222
+ describe "RenderRevision" do
223
+ let(:step) { Spontaneous::Publishing::Steps::RenderRevision }
224
+
225
+ it "has the right shortcut name" do
226
+ step.to_sym.must_equal :render_revision
227
+ end
228
+
229
+ it "renders each page to the output store" do
230
+ store = @output_store.revision(revision).store
231
+ @pages.each do |page, title|
232
+ page.outputs.each do |output|
233
+ key = store.output_key(output, false)
234
+ store.expects(:store_static).with(revision, key, "=#{title}.#{output.name}", instance_of(Spontaneous::Output::Store::Transaction))
235
+ end
236
+ end
237
+ run_step
238
+ end
239
+
240
+ describe "private trees" do
241
+ let(:next_revision) { revision + 1 }
242
+ let(:progress) { Spontaneous::Publishing::Progress::Silent.new }
243
+ before do
244
+ class ::HiddenRootPage < Page
245
+ layout(:html) { "=${title}.html" }
246
+ layout(:xml) { "=${title}.xml" }
247
+ box :underneath
248
+ end
249
+ @private_root = HiddenRootPage.create_root('private-root', title: "Private Root")
250
+ @private_page = HiddenRootPage.create(title: "Private Page", slug: "private-page")
251
+ @private_root.underneath << @private_page
252
+ @private_root.save
253
+ @private_page.save
254
+
255
+ @site.model.publish_all(next_revision)
256
+ end
257
+
258
+ it "renders pages in private trees" do
259
+ store = @output_store.revision(revision).store
260
+ (@pages + [[@private_root, @private_root.title.value], [@private_page, @private_page.title.value]]).each do |page, title|
261
+ page.outputs.each do |output|
262
+ key = store.output_key(output, false)
263
+ store.expects(:store_static).with(next_revision, key, "=#{title}.#{output.name}", instance_of(Spontaneous::Output::Store::Transaction))
264
+ end
265
+ end
266
+ @site.model.scope(next_revision, true) do
267
+ step.call(@site, next_revision, nil, )
268
+ end
269
+ end
270
+ end
271
+
272
+ it "returns the correct number of steps" do
273
+ @site.model.scope(revision, true) do
274
+ step.count(@site, revision, nil).must_equal (@pages.length * 2)
275
+ end
276
+ end
277
+
278
+ it "updates the progress object" do
279
+ progress = mock()
280
+ progress.expects(:stage).with("rendering")
281
+ progress.expects(:step).with(1, instance_of(String)).times(@pages.length * 2)
282
+ run_step(progress)
283
+ end
284
+
285
+ it "returns an instance from #call" do
286
+ run_step.must_be_instance_of step
287
+ end
288
+
289
+ it "deletes the rendered files on rollback" do
290
+ instance = run_step
291
+ store = @output_store.revision(revision).store
292
+ store.expects(:delete_revision).with(revision)
293
+ instance.rollback
294
+ end
295
+
296
+ it "runs rollback after throwing an exception" do
297
+ instance = mock
298
+ step.expects(:new).returns(instance)
299
+ instance.expects(:call).raises(Exception)
300
+ instance.expects(:rollback)
301
+ lambda{ run_step }.must_raise(Exception)
302
+ end
303
+ end
304
+
305
+ describe "GenerateSearchIndexes" do
306
+ let(:step) { Spontaneous::Publishing::Steps::GenerateSearchIndexes }
307
+ let(:index_count) { 0 }
308
+
309
+ before do
310
+ index_count.times do |n|
311
+ @site.index "index#{n}".to_sym do; end
312
+ end
313
+ @site.indexes.length.must_equal index_count
314
+ end
315
+
316
+ it "has the right shortcut name" do
317
+ step.to_sym.must_equal :generate_search_indexes
318
+ end
319
+
320
+ it "returns the correct number of steps when there are no search indexes" do
321
+ @site.model.scope(revision, true) do
322
+ step.count(@site, revision, nil).must_equal 0
323
+ end
324
+ end
325
+
326
+ it "doesn't set the progress stage" do
327
+ progress = mock
328
+ progress.expects(:stage).with("indexing").never
329
+ run_step(progress)
330
+ end
331
+
332
+ it "doesn't attempt to add any pages to the index" do
333
+ @site.expects(:indexer).with(revision).never
334
+ run_step
335
+ end
336
+
337
+ describe "with indexes" do
338
+ let(:index_count) { 2 }
339
+
340
+ it "returns the correct number of steps" do
341
+ @site.model.scope(revision, true) do
342
+ step.count(@site, revision, nil).must_equal (@pages.length)
343
+ end
344
+ end
345
+
346
+ it "sets the progress stage to 'indexing'" do
347
+ progress = mock
348
+ progress.stubs(:step)
349
+ progress.expects(:stage).with("indexing").once
350
+ run_step(progress)
351
+ end
352
+
353
+ it "adds every page to the index" do
354
+ indexer = mock
355
+ S::Search::CompoundIndexer.expects(:new).returns(indexer)
356
+ indexer.stubs(:close)
357
+ @site.model.scope(revision, true) do
358
+ @site.pages.each do |page|
359
+ indexer.expects(:<<).with(page)
360
+ end
361
+ end
362
+ run_step
363
+ end
364
+
365
+ it "updates the progress object with each page" do
366
+ progress = Spontaneous::Publishing::Progress::Silent.new
367
+ progress.expects(:step).with(1, instance_of(String)).times(@pages.count)
368
+ run_step(progress)
369
+ end
370
+
371
+ # we can (currently) delegate this to the removal of the whole revision dir
372
+ # since the indexes are just on-disk. At the point where the search is able
373
+ # to integrate with other search engines then we're gonna need/have an api
374
+ # call to delete a revision
375
+ it "deletes the indexes on rollback"
376
+
377
+ it "runs rollback after throwing an exception" do
378
+ instance = mock
379
+ step.expects(:new).returns(instance)
380
+ instance.expects(:call).raises(Exception)
381
+ instance.expects(:rollback)
382
+ lambda{ run_step }.must_raise(Exception)
383
+ end
384
+ end
385
+ end
386
+
387
+ describe "CopyStaticFiles" do
388
+ let(:step) { Spontaneous::Publishing::Steps::CopyStaticFiles }
389
+ let(:application_path) { Pathname.new(File.expand_path("../../fixtures/example_application", __FILE__)) }
390
+ let(:fixtures_path) { application_path + "public" }
391
+ let(:revision_root) { @site.revision_dir(revision) }
392
+
393
+ def assert_static_files(namespace = nil)
394
+ Dir["#{fixtures_path}/**/*"].each do |fixture|
395
+ path = Pathname.new(fixture)
396
+ relative = path.relative_path_from(fixtures_path).to_s
397
+ revision_file = File.join([revision_root, "public", namespace, relative].compact)
398
+ File.exist?(revision_file).must_equal true
399
+ end
400
+ end
401
+
402
+ before do
403
+ FileUtils.cp_r(fixtures_path, @site.root)
404
+ File.exist?(@site.root / 'public/favicon.ico').must_equal true
405
+ end
406
+
407
+ it "has the right shortcut name" do
408
+ step.to_sym.must_equal :copy_static_files
409
+ end
410
+
411
+ it "sets the progress stage to 'copying files'" do
412
+ progress = mock
413
+ progress.stubs(:step)
414
+ progress.expects(:stage).with("copying files").once
415
+ run_step(progress)
416
+ end
417
+
418
+ it "steps the progress once for each facet" do
419
+ progress = mock
420
+ progress.stubs(:stage)
421
+ progress.expects(:step).with(1, instance_of(String)).once
422
+ run_step(progress)
423
+ end
424
+
425
+ it "gives its step count as the number of facets" do
426
+ step.count(@site, revision, nil).must_equal 1
427
+ end
428
+
429
+ it "copies files in the site's public dir" do
430
+ run_step
431
+ assert_static_files
432
+ end
433
+
434
+ it "deletes the copied files on rollback" do
435
+ instance = run_step
436
+ instance.rollback
437
+ refute File.exist?(File.join(revision_root, "public"))
438
+ end
439
+
440
+ it "runs rollback after throwing an exception" do
441
+ instance = mock
442
+ step.expects(:new).returns(instance)
443
+ instance.expects(:call).raises(Exception)
444
+ instance.expects(:rollback)
445
+ lambda{ run_step }.must_raise(Exception)
446
+ end
447
+
448
+ describe "facets" do
449
+ before do
450
+ @site.load_plugin(application_path)
451
+ end
452
+
453
+ it "copies plugin files under their namespace" do
454
+ run_step
455
+ assert_static_files
456
+ assert_static_files('example_application')
457
+ end
458
+
459
+ it "gives its step count as the number of facets" do
460
+ step.count(@site, revision, nil).must_equal 2
461
+ end
462
+
463
+ it "steps the progress once for each facet" do
464
+ progress = mock
465
+ progress.stubs(:stage)
466
+ progress.expects(:step).with(1, instance_of(String)).times(2)
467
+ run_step(progress)
468
+ end
469
+ end
470
+ end
471
+
472
+ describe "GenerateRackupFile" do
473
+ let(:step) { Spontaneous::Publishing::Steps::GenerateRackupFile }
474
+ let(:rackup_path) { @site.revision_dir(revision) / "config.ru" }
475
+
476
+ it "has the right shortcut name" do
477
+ step.to_sym.must_equal :generate_rackup_file
478
+ end
479
+
480
+ it "reports a step count of 1" do
481
+ step.count(@site, revision, nil).must_equal 1
482
+ end
483
+
484
+ it "sets the stage to 'create server config'" do
485
+ progress = mock
486
+ progress.stubs(:step)
487
+ progress.expects(:stage).with("create server config").once
488
+ run_step(progress)
489
+ end
490
+
491
+ it "increments the progress step by 1" do
492
+ progress = mock
493
+ progress.stubs(:stage)
494
+ progress.expects(:step).with(1, instance_of(String)).once
495
+ run_step(progress)
496
+ end
497
+
498
+ it "deletes the file on rollback" do
499
+ instance = run_step
500
+ instance.rollback
501
+ refute File.exist?(rackup_path)
502
+ end
503
+
504
+ it "runs rollback after throwing an exception" do
505
+ instance = mock
506
+ step.expects(:new).returns(instance)
507
+ instance.expects(:call).raises(Exception)
508
+ instance.expects(:rollback)
509
+ lambda{ run_step }.must_raise(Exception)
510
+ end
511
+
512
+ describe "config.ru" do
513
+ let(:config) { File.read(rackup_path) }
514
+
515
+ before do
516
+ run_step
517
+ end
518
+
519
+ it "creates a config.ru file in the root of the revision dir" do
520
+ File.exist?(rackup_path).must_equal true
521
+ end
522
+
523
+ it "sets the revision number in the ENV" do
524
+ config.must_match %r(ENV\["SPOT_REVISION"\] *= *"#{revision}")
525
+ end
526
+ end
527
+ end
528
+
529
+ describe "ActivateRevision" do
530
+ let(:step) { Spontaneous::Publishing::Steps::ActivateRevision }
531
+
532
+ it "has the right shortcut name" do
533
+ step.to_sym.must_equal :activate_revision
534
+ end
535
+
536
+ it "reports a step count of 1" do
537
+ step.count(@site, revision, nil).must_equal 2
538
+ end
539
+
540
+ it "sets the stage to 'activating revision'" do
541
+ progress = mock
542
+ progress.stubs(:step)
543
+ progress.expects(:stage).with("activating revision").once
544
+ run_step(progress)
545
+ end
546
+
547
+ it "increments the progress step by 2" do
548
+ progress = mock
549
+ progress.stubs(:stage)
550
+ progress.expects(:step).with(1, instance_of(String)).times(2)
551
+ run_step(progress)
552
+ end
553
+
554
+ # set the site :published_revision value to revision
555
+ it "updates the site's published revision setting" do
556
+ state = @site.state
557
+ state.published_revision.must_equal 2
558
+ state.revision.must_equal revision
559
+ run_step
560
+ state.reload
561
+ state.published_revision.must_equal revision
562
+ state.revision.must_equal revision + 1
563
+ end
564
+
565
+ it "rollback sets the site state back to how it was" do
566
+ instance = run_step
567
+ instance.rollback
568
+ state = @site.state.reload
569
+ state.published_revision.must_equal 2
570
+ state.revision.must_equal revision
571
+ end
572
+
573
+ it "runs rollback after throwing an exception" do
574
+ instance = mock
575
+ step.expects(:new).returns(instance)
576
+ instance.expects(:call).raises(Exception)
577
+ instance.expects(:rollback)
578
+ lambda{ run_step }.must_raise(Exception)
579
+ end
580
+
581
+ describe "with previous revision" do
582
+ let(:previous_revision_dir) { @site.revision_dir(revision-1) }
583
+ let(:new_revision_dir) { @site.revision_dir(revision) }
584
+
585
+ before do
586
+ FileUtils.mkdir_p(previous_revision_dir)
587
+ FileUtils.mkdir_p(new_revision_dir)
588
+ File.open(File.join(previous_revision_dir, 'REVISION'), 'w') { |file| file.write(revision-1) }
589
+ File.open(File.join(new_revision_dir, 'REVISION'), 'w') { |file| file.write(revision) }
590
+ FileUtils.ln_s(previous_revision_dir, @site.revision_dir)
591
+ end
592
+
593
+ it "symlinks the new revision to 'current'" do
594
+ run_step
595
+ File.read(File.join(@site.revision_dir, 'REVISION')).must_equal revision.to_s
596
+ end
597
+
598
+ it "rollback re-points the 'current' symlink to the previous directory" do
599
+ instance = run_step
600
+ instance.rollback
601
+ File.read(File.join(@site.revision_dir, 'REVISION')).must_equal (revision - 1).to_s
602
+ end
603
+ end
604
+
605
+ describe "without previous revision" do
606
+ let(:new_revision_dir) { @site.revision_dir(revision) }
607
+
608
+ before do
609
+ FileUtils.mkdir_p(new_revision_dir)
610
+ File.open(File.join(new_revision_dir, 'REVISION'), 'w') { |file| file.write(revision) }
611
+ end
612
+
613
+ it "symlinks the new revision to 'current'" do
614
+ run_step
615
+ File.read(File.join(@site.revision_dir, 'REVISION')).must_equal revision.to_s
616
+ end
617
+
618
+ it "rollback deletes the 'current' symlink" do
619
+ instance = run_step
620
+ instance.rollback
621
+ File.exist?(@site.revision_dir).must_equal false
622
+ end
623
+ end
624
+
625
+ # generate revision file (which triggers the server reload)
626
+ # progress.done
627
+ # site is published at this point -- user scripts can run
628
+ # clean up the revisions table
629
+
630
+ # within the coordinator?:
631
+ # set site :pending_revision to nil
632
+ # create PublishedRevision instance
633
+ end
634
+
635
+ describe "WriteRevisionFile" do
636
+ let(:step) { Spontaneous::Publishing::Steps::WriteRevisionFile }
637
+ let(:path) { @site.revision_root / "REVISION" }
638
+
639
+ it "has the right shortcut name" do
640
+ step.to_sym.must_equal :write_revision_file
641
+ end
642
+
643
+ it "reports a step count of 1" do
644
+ step.count(@site, revision, nil).must_equal 1
645
+ end
646
+
647
+ it "sets the stage to 'writing revision file'" do
648
+ progress = mock
649
+ progress.stubs(:step)
650
+ progress.expects(:stage).with("writing revision file").once
651
+ run_step(progress)
652
+ end
653
+
654
+ it "increments the progress step by 1" do
655
+ progress = mock
656
+ progress.stubs(:stage)
657
+ progress.expects(:step).with(1, instance_of(String)).once
658
+ run_step(progress)
659
+ end
660
+
661
+ it "writes a REVISION file with the current revision dirname" do
662
+ run_step
663
+ assert File.exist?(path)
664
+ r = File.read(path)
665
+ r.must_equal "00003"
666
+ end
667
+
668
+ it "deletes the file on rollback if none existed" do
669
+ instance = run_step
670
+ instance.rollback
671
+ refute File.exist?(path)
672
+ end
673
+
674
+ it "reverts the file on rollback" do
675
+ File.open(path, 'w') { |file| file.write("PREVIOUS") }
676
+ instance = run_step
677
+ instance.rollback
678
+ assert File.exist?(path)
679
+ File.read(path).must_equal "PREVIOUS"
680
+ end
681
+
682
+ it "runs rollback after throwing an exception" do
683
+ instance = mock
684
+ step.expects(:new).returns(instance)
685
+ instance.expects(:call).raises(Exception)
686
+ instance.expects(:rollback)
687
+ lambda{ run_step }.must_raise(Exception)
688
+ end
689
+ end
690
+
691
+ describe "ArchiveOldRevisions" do
692
+ let(:step) { Spontaneous::Publishing::Steps::ArchiveOldRevisions }
693
+
694
+ it "has the right shortcut name" do
695
+ step.to_sym.must_equal :archive_old_revisions
696
+ end
697
+
698
+ it "reports a step count of 1" do
699
+ step.count(@site, revision, nil).must_equal 1
700
+ end
701
+
702
+ it "sets the stage to 'archiving old revisions'" do
703
+ progress = mock
704
+ progress.stubs(:step)
705
+ progress.expects(:stage).with("archiving old revisions").once
706
+ run_step(progress)
707
+ end
708
+
709
+ it "increments the progress step by 1" do
710
+ progress = mock
711
+ progress.stubs(:stage)
712
+ progress.expects(:step).with(1).once
713
+ run_step(progress)
714
+ end
715
+
716
+ it "calls the cleanup command" do
717
+ @site.config.stubs(:keep_revisions).returns(12)
718
+ @site.model.expects(:cleanup_revisions).with(revision, 12)
719
+ run_step
720
+ end
721
+ end
722
+
723
+ describe "Pipeline" do
724
+ let(:steps) { [] }
725
+ let(:progress) do
726
+ mock.tap do |progress|
727
+ [:start, :stage, :step, :done].each { |method| progress.stubs(method) }
728
+ end
729
+ end
730
+ let(:pages) { @modified_pages }
731
+ let(:failing_step) {
732
+ mock.tap do |step|
733
+ step.stubs(:count).returns(12)
734
+ step.expects(:call).raises(Exception)
735
+ end
736
+ }
737
+
738
+ def run_steps(steps = steps, progress = progress)
739
+ Spontaneous::Publishing::Pipeline.new(steps).run(@site, revision, pages, progress)
740
+ end
741
+
742
+ def modify_some_pages
743
+ @modified_pages = @pages[0..1].map { |page, _| page.reload }
744
+ @modified_pages.each { |page| page.update(title: "Changed!") }
745
+ end
746
+
747
+ before do
748
+ modify_some_pages
749
+ end
750
+
751
+ it "runs every step" do
752
+ steps = [mock, mock]
753
+ steps.each do |step|
754
+ step.stubs(:count).returns(10)
755
+ step.expects(:call).with(site, revision, pages, progress)
756
+ end
757
+ run_steps(steps)
758
+ end
759
+
760
+ it "calculates the total step count & sets up the progress" do
761
+ steps = [mock, mock]
762
+ steps.each do |step|
763
+ step.stubs(:call)
764
+ step.expects(:count).with(site, revision, pages, progress).returns(12)
765
+ end
766
+ progress.expects(:start).with(24)
767
+ run_steps(steps)
768
+ end
769
+
770
+ it "calls #rollback on all steps completed before exception" do
771
+ steps = [mock, mock]
772
+ steps.each do |step|
773
+ step.expects(:call).returns(step)
774
+ step.stubs(:count).returns(12)
775
+ step.expects(:rollback)
776
+ end
777
+ steps << failing_step
778
+ lambda {
779
+ run_steps(steps)
780
+ }.must_raise(Exception)
781
+ end
782
+
783
+ it "doesn't call rollback on a step that doesn't support it" do
784
+ steps = [mock, mock]
785
+ steps.each do |step|
786
+ step.expects(:call).returns(step)
787
+ step.stubs(:count).returns(12)
788
+ end
789
+ steps << failing_step
790
+ lambda {
791
+ run_steps(steps)
792
+ }.must_raise(Exception)
793
+ end
794
+
795
+ it "doesn't call #count on steps that don't support it" do
796
+ steps = [
797
+ proc { |s, r, pa, pr| nil },
798
+ proc { |s, r, pa, pr| nil }
799
+ ]
800
+ run_steps(steps)
801
+ end
802
+ end
803
+
804
+ describe "Publish" do
805
+ let(:steps) {
806
+ [mock, mock].each do |step|
807
+ step.stubs(:call).returns(step)
808
+ end
809
+ }
810
+ let(:mprogress) { Spontaneous::Publishing::Progress }
811
+ let(:publish) { Spontaneous::Publishing::Publish.new(site, revision, actions) }
812
+ let(:actions) { Spontaneous::Publishing::Steps.new(steps, []) }
813
+
814
+ def modify_some_pages
815
+ @modified_pages = @pages[0..1].map { |page, _| page.reload }
816
+ Timecop.travel(later) do
817
+ @modified_pages.each { |page| page.update(title: "Changed!") }
818
+ end
819
+ @modified_pages = @modified_pages.map { |page| Page[page.id] }
820
+ end
821
+
822
+ before do
823
+ modify_some_pages
824
+ end
825
+
826
+ it "publishes all if passed every page" do
827
+ publish.expects(:publish_all)
828
+ publish.publish_pages(@site.model::Page.all)
829
+ end
830
+
831
+ it "publishes all if passed a list of every modified page" do
832
+ publish.expects(:publish_all)
833
+ publish.publish_pages(@modified_pages)
834
+ end
835
+
836
+ it "publish_all creates the content revision" do
837
+ @site.model.expects(:publish).with(revision, nil)
838
+ publish.publish_all
839
+ end
840
+
841
+ it "publish_pages creates the content revision" do
842
+ @site.model.expects(:publish).with(revision, [@modified_pages.first])
843
+ publish.publish_pages([@modified_pages.first])
844
+ end
845
+
846
+ it "runs all steps" do
847
+ pages_matcher = all_of(*@modified_pages.map { |page| PageMatcher.new(page)})
848
+ steps.each do |step|
849
+ step.expects(:call).with(site, revision, pages_matcher, instance_of(mprogress::Multi))
850
+ end
851
+ publish.publish_pages(@modified_pages)
852
+ end
853
+
854
+ it "sets the publishing timestamps" do
855
+ page = Page.create(title: "New page", slug: "page-new")
856
+ @root.things << page
857
+ page.save
858
+ page.first_published_at.must_equal nil
859
+ publish.publish_pages([page])
860
+ @site.model.with_editable do
861
+ (page.reload.first_published_at - now).must_be :<=, 1
862
+ end
863
+ end
864
+
865
+ # Need a custom matcher because a simple #== doesn't work as the
866
+ # test is running within the scope of our new revision
867
+ class PageMatcher < Mocha::ParameterMatchers::Base
868
+ def initialize(page)
869
+ @page = page
870
+ end
871
+
872
+ def matches?(available_parameters)
873
+ parameter = available_parameters.shift
874
+ parameter.any? { |param| param.class == @page.class && param.id == @page.id }
875
+ end
876
+ end
877
+
878
+ # badly worded. a "publish all" usually works with 'nil' as the modified
879
+ # page list (bad decision I guess). The current core publish steps
880
+ # don't actually use the list of pages that are being published
881
+ # but it's not crazy to assume that custom steps might want to know exactly
882
+ # which pages have been published (say if you want to send a tweet for new pages)
883
+ # so a publish all should convert the nil used internally into a list of all
884
+ # the modified pages for use by the steps
885
+ it "passes the list of modified pages to the publish steps" do
886
+ pages_matcher = all_of(*@modified_pages.map { |page| PageMatcher.new(page)})
887
+ steps.each do |step|
888
+ step.expects(:call).with(site, revision, pages_matcher, instance_of(mprogress::Multi))
889
+ end
890
+ publish.publish_all
891
+ end
892
+
893
+ it "sets the site pending revision" do
894
+ @site.expects(:pending_revision=).with(revision)
895
+ @site.expects(:pending_revision=).with(nil)
896
+ publish.publish_all
897
+ end
898
+
899
+ describe "on completion" do
900
+ it "sets the pending revision to nil" do
901
+ @site.expects(:pending_revision=).with(revision)
902
+ @site.expects(:pending_revision=).with(nil)
903
+ publish.publish_all
904
+ end
905
+
906
+ it "creates a new PublishedRevision entry" do
907
+ S::PublishedRevision.expects(:create).with(all_of(has_entry(revision: revision), has_entry(published_at: instance_of(Time))))
908
+ publish.publish_all
909
+ end
910
+
911
+ it "calls #done on the progress object" do
912
+ publish.progress.expects(:done)
913
+ publish.publish_all
914
+ end
915
+
916
+ it "resets the site's 'must_publish_all' flag" do
917
+ @site.expects(:must_publish_all!).with(false)
918
+ publish.publish_all
919
+ end
920
+
921
+ it "creates the revision" do
922
+ publish.publish_all
923
+ @site.model.database.tables.include?(@site.model.revision_table(revision)).must_equal true
924
+ end
925
+ end
926
+
927
+ describe "on error" do
928
+ let(:failing_step) {
929
+ mock.tap do |step|
930
+ step.stubs(:count).returns(12)
931
+ step.expects(:call).raises(Exception)
932
+ end
933
+ }
934
+
935
+ before do
936
+ steps << failing_step
937
+ end
938
+
939
+ it "sets the pending revision to nil" do
940
+ @site.expects(:pending_revision=).with(revision)
941
+ @site.expects(:pending_revision=).with(nil)
942
+ lambda {publish.publish_all }.must_raise(Exception)
943
+ end
944
+
945
+ it "doesn't create a new PublishedRevision entry" do
946
+ lambda {publish.publish_all }.must_raise(Exception)
947
+ r = S::PublishedRevision.filter(:revision => revision).first
948
+ r.must_equal nil
949
+ end
950
+
951
+ it "deletes the revision" do
952
+ lambda {publish.publish_all }.must_raise(Exception)
953
+ @site.model.database.tables.include?(@site.model.revision_table(revision)).must_equal false
954
+ end
955
+
956
+ it "calls #error on the progress object" do
957
+ publish.progress.expects(:error).with(instance_of(Exception))
958
+ lambda {publish.publish_all }.must_raise(Exception)
959
+ end
960
+
961
+ it "doesn't set the publishing timestamps" do
962
+ page = Page.create(title: "New page", slug: "page-new")
963
+ @root.things << page
964
+ page.save
965
+ page.first_published_at.must_equal nil
966
+ lambda {publish.publish_pages([page]) }.must_raise(Exception)
967
+ @site.model.with_editable do
968
+ page.reload.first_published_at.must_equal nil
969
+ end
970
+ end
971
+
972
+ end
973
+ end
974
+
975
+ describe "Site" do
976
+ class FakeStep; end
977
+ let(:steps) {
978
+ [FakeStep].map do |step|
979
+ step.stubs(:call)
980
+ step.stubs(:count).returns(10)
981
+ step
982
+ end
983
+ }
984
+
985
+ describe "steps" do
986
+ it "maps symbols to step classes" do
987
+ @site.publish do
988
+ run :create_revision_directory
989
+ end
990
+ @site.publish_steps.steps.first.must_equal Spontaneous::Publishing::Steps::CreateRevisionDirectory
991
+ end
992
+
993
+ it "passes all configured steps onto the publish system" do
994
+ steps.each do |step|
995
+ step.expects(:call).with(site, revision, anything, anything)
996
+ end
997
+ @site.publish do
998
+ run FakeStep
999
+ end
1000
+ @site.publish_all
1001
+ end
1002
+ end
1003
+
1004
+ describe "progess" do
1005
+ def mock_progress
1006
+ progress = mock
1007
+ progress.stubs(:log)
1008
+ progress.stubs(:step)
1009
+ progress.expects(:start)
1010
+ progress.stubs(:stage)
1011
+ progress.expects(:done)
1012
+ progress
1013
+ end
1014
+
1015
+ it "maps symbols to progress classes" do
1016
+ [
1017
+ [:none, Spontaneous::Publishing::Progress::Silent],
1018
+ [:silent, Spontaneous::Publishing::Progress::Silent],
1019
+ [:stdout, Spontaneous::Publishing::Progress::Stdout],
1020
+ [:log, Spontaneous::Publishing::Progress::Log],
1021
+ [:browser, Spontaneous::Publishing::Progress::Simultaneous],
1022
+ [:simultaneous, Spontaneous::Publishing::Progress::Simultaneous]
1023
+ ].each do |symbol, klass|
1024
+ @site.publish do
1025
+ log symbol
1026
+ end
1027
+ @site.publish_steps.progress.first.must_be_instance_of klass
1028
+ end
1029
+ end
1030
+
1031
+ it "sends progress to configured object" do
1032
+ progress = mock_progress
1033
+ progress.expects(:stage).with("something")
1034
+ @site.publish do
1035
+ notify progress
1036
+ run proc { progress.stage("something") }
1037
+ end
1038
+ @site.publish_all
1039
+ end
1040
+
1041
+ it "passes arguments to progress obj" do
1042
+ Spontaneous::Publishing::Progress::Log.expects(:new).with("publish.log").returns(mock_progress)
1043
+ @site.publish do
1044
+ notify :log, "publish.log"
1045
+ end
1046
+ @site.publish_all
1047
+ end
1048
+ end
1049
+ end
1050
+ end