diff --git a/CHANGELOG.md b/CHANGELOG.md index ff06c85..87bdaf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # next release * The admin interface now supports bookmarks for quickly creating drafts. Selected text on the page will be turned into Markdown. (#50) +* Posts with a header of `update: now` will have their `Updated` timestamps updated automatically on the next generate. (#49) * The 'markdown' filter now properly turns single quotes in Markdown into curly quotes. (#40) * Add `archive_page` template flag set to true when processing archive pages. (#41) * Make the `month` archive variable available to layouts as well as the archive template. (#41) diff --git a/Gemfile.lock b/Gemfile.lock index 1449a67..efce25f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,7 @@ PATH specs: serif (0.3.3) liquid (~> 2.4) + nokogiri (~> 1.5) rack (~> 1.0) redcarpet (~> 2.2) redhead (~> 0.0.8) @@ -17,14 +18,14 @@ GEM diff-lcs (1.1.3) liquid (2.5.0) multi_json (1.6.1) - nokogiri (1.5.5) + nokogiri (1.5.6) rack (1.5.2) rack-protection (1.5.0) rack rake (0.9.6) redcarpet (2.2.2) redhead (0.0.8) - reverse_markdown (0.4.3) + reverse_markdown (0.4.4) nokogiri rouge (0.3.2) thor @@ -45,8 +46,8 @@ GEM rack-protection (~> 1.4) tilt (~> 1.3, >= 1.3.4) thor (0.17.0) - tilt (1.3.5) - timecop (0.5.9.2) + tilt (1.3.6) + timecop (0.6.1) timeout_cache (0.0.2) PLATFORMS @@ -57,4 +58,4 @@ DEPENDENCIES rspec (~> 2.5) serif! simplecov (~> 0.7) - timecop (~> 0.5.5) + timecop (~> 0.6.1) diff --git a/README.md b/README.md index 454e3ea..d20d40c 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,12 @@ Now visit to view the site. * [Basic usage](#basics) * [Content and site structure](#content-and-site-structure) * [Publishing drafts](#publishing-drafts) +* [Updating posts](#updating-posts) * [Archive pages](#archive-pages) * [Configuration](#configuration) * [Deploying](#deploying) * [Customising the admin interface](#customising-the-admin-interface) -* [Custom tags](#custom-tags) +* [Custom tags and filters](#custom-tags-and-filters) * [Template variables](#template-variables) * [Developing Serif](#developing-serif) * [Changes and what's new](#changes-and-whats-new) @@ -280,6 +281,26 @@ This is a draft that will be published now. On the next site generation (`serif generate`) this draft will be automatically published, using the current time as the creation timestamp. +# Updating posts + +When you update a draft, you need to remember to change the updated time. As luck would have it, Serif takes care of timestamps for you! Just use a header of `update: now` at the top of your post: + +``` +title: My blog post +Created: 2013-01-01T12:01:30+00:00 +update: now +``` + +Now the next time the site is generated, the timestamp will be updated: + +``` +title: My blog post +Created: 2013-01-01T12:01:30+00:00 +Updated: 2013-03-18T19:03:30+00:00 +``` + +Admin users: this is all done for you. + # Archive pages By default, archive pages are made available at `/archive/:year/month`, e.g., `/archive/2012/11`. Individual archive pages can be customised by editing the `_templates/archive_page.html` file, which is used for each month. diff --git a/lib/serif/content_file.rb b/lib/serif/content_file.rb index 0977e10..ee20a43 100644 --- a/lib/serif/content_file.rb +++ b/lib/serif/content_file.rb @@ -122,11 +122,18 @@ def inspect def set_publish_time(time) @source.headers[:created] = time.xmlschema - @cached_headers = nil + headers_changed! end def set_updated_time(time) @source.headers[:updated] = time.xmlschema + headers_changed! + end + + # Invalidates the cached headers entirely. + # + # Any methods which alter headers should call this. + def headers_changed! @cached_headers = nil end diff --git a/lib/serif/draft.rb b/lib/serif/draft.rb index f33d64e..56eadfa 100644 --- a/lib/serif/draft.rb +++ b/lib/serif/draft.rb @@ -36,18 +36,16 @@ def publish! @path = Post.from_slug(site, slug).path end - # sets the autopublish flag to the given value. - # # if the assigned value is truthy, the "publish" header # is set to "now", otherwise the header is removed. def autopublish=(value) - @autopublish = value - if value @source.headers[:publish] = "now" else @source.headers.delete(:publish) end + + headers_changed! end # Checks the value of the "publish" header, and returns diff --git a/lib/serif/post.rb b/lib/serif/post.rb index 74fb1f8..81121c4 100755 --- a/lib/serif/post.rb +++ b/lib/serif/post.rb @@ -27,6 +27,36 @@ def url output end + # if the assigned value is truthy, the "update" header + # is set to "now", otherwise the header is removed. + def autoupdate=(value) + if value + @source.headers[:update] = "now" + else + @source.headers.delete(:update) + end + + headers_changed! + end + + # returns true if the post has been marked as needing a + # new updated timestamp header. + # + # this is based on the presence of an "update: now" header. + def autoupdate? + update_header = headers[:update] + update_header && update_header.strip == "now" + end + + # Updates the updated timestamp and saves the contents. + # + # If there is an "update" header (see autoupdate?), it is deleted. + def update! + @source.headers.delete(:update) + set_updated_time(Time.now) + save + end + def self.all(site) files = Dir[File.join(site.directory, dirname, "*")].select { |f| File.file?(f) }.map { |f| File.expand_path(f) } files.map { |f| new(site, f) } diff --git a/lib/serif/site.rb b/lib/serif/site.rb index a418982..ffb05d2 100644 --- a/lib/serif/site.rb +++ b/lib/serif/site.rb @@ -253,6 +253,9 @@ def generate # to operate on. preprocess_autopublish_drafts + # preprocess any posts that might have had an update flag set in the header + preprocess_autoupdate_posts + posts = self.posts files.each do |path| @@ -346,6 +349,15 @@ def generate private + def preprocess_autoupdate_posts + posts.each do |p| + if p.autoupdate? + puts "Auto-updating timestamp for: #{p.title} / #{p.slug}" + p.update! + end + end + end + # generates draft preview files for any unpublished drafts. # # uses the same template as live posts. diff --git a/serif.gemspec b/serif.gemspec index 9bf84b8..8b181e3 100644 --- a/serif.gemspec +++ b/serif.gemspec @@ -29,5 +29,5 @@ Gem::Specification.new do |s| s.add_development_dependency("rake", "~> 0.9") s.add_development_dependency("rspec", "~> 2.5") s.add_development_dependency("simplecov", "~> 0.7") - s.add_development_dependency("timecop", "~> 0.5.5") + s.add_development_dependency("timecop", "~> 0.6.1") end diff --git a/test/draft_spec.rb b/test/draft_spec.rb index e992e30..2db46f1 100644 --- a/test/draft_spec.rb +++ b/test/draft_spec.rb @@ -152,6 +152,20 @@ draft.delete! end + + it "carries its value through to #autopublish?" do + draft = D.new(@site) + draft.slug = "test-draft" + draft.title = "Some draft title" + draft.autopublish = false + draft.autopublish?.should be_false + + draft.autopublish = true + draft.autopublish?.should be_true + + draft.autopublish = false + draft.autopublish?.should be_false + end end describe "#autopublish?" do diff --git a/test/post_spec.rb b/test/post_spec.rb index 43f8c85..f868198 100644 --- a/test/post_spec.rb +++ b/test/post_spec.rb @@ -9,6 +9,21 @@ @posts = subject.posts end + around :each do |example| + begin + d = Serif::Draft.new(subject) + d.slug = "foo-bar-bar-temp" + d.title = "Testing title" + d.save("# some content") + d.publish! + @temporary_post = Serif::Post.from_slug(subject, d.slug) + + example.run + ensure + FileUtils.rm(@temporary_post.path) + end + end + it "uses the config file's permalink value" do @posts.all? { |p| p.url == "/test-blog/#{p.slug}" }.should be_true end @@ -18,4 +33,78 @@ @posts.all? { |p| p.inspect.should include(p.headers.inspect) } end end + + describe "#autoupdate=" do + it "sets the 'update' header to 'now' if truthy assigned value" do + @temporary_post.autoupdate = true + @temporary_post.headers[:update].should == "now" + end + + it "removes the 'update' header entirely if falsey assigned value" do + @temporary_post.autoupdate = false + @temporary_post.headers.key?(:update).should be_false + end + + it "marks the post as autoupdate? == true" do + @temporary_post.autoupdate?.should be_false + @temporary_post.autoupdate = true + @temporary_post.autoupdate?.should be_true + end + end + + describe "#autoupdate?" do + it "returns true if there is an update: now header" do + @temporary_post.stub(:headers) { { :update => "foo" } } + @temporary_post.autoupdate?.should be_false + @temporary_post.stub(:headers) { { :update => "now" } } + @temporary_post.autoupdate?.should be_true + end + + it "is ignorant of whitespace in the update header value" do + @temporary_post.stub(:headers) { { :update => "now" } } + @temporary_post.autoupdate?.should be_true + + (1..3).each do |left| + (1..3).each do |right| + @temporary_post.stub(:headers) { { :update => "#{" " * left}now#{" " * right}"} } + @temporary_post.autoupdate?.should be_true + end + end + end + end + + describe "#update!" do + it "sets the updated header timestamp to the current time" do + old_update_time = @temporary_post.updated + t = Time.now + 50 + + Timecop.freeze(t) do + @temporary_post.update! + @temporary_post.updated.should_not == old_update_time + @temporary_post.updated.to_i.should == t.to_i + @temporary_post.headers[:updated].to_i.should == t.to_i + end + end + + it "calls save and writes out the new timestamp value, without a publish: now header" do + @temporary_post.should_receive(:save).once.and_call_original + + t = Time.now + 50 + Timecop.freeze(t) do + @temporary_post.update! + + file_content = Redhead::String[File.read(@temporary_post.path)] + Time.parse(file_content.headers[:updated].value).to_i.should == t.to_i + file_content.headers[:publish].should be_nil + end + end + + it "marks the post as no longer auto-updating" do + @temporary_post.autoupdate?.should be_false + @temporary_post.autoupdate = true + @temporary_post.autoupdate?.should be_true + @temporary_post.update! + @temporary_post.autoupdate?.should be_false + end + end end \ No newline at end of file diff --git a/test/site_generation_spec.rb b/test/site_generation_spec.rb index a242401..57e496a 100644 --- a/test/site_generation_spec.rb +++ b/test/site_generation_spec.rb @@ -98,6 +98,34 @@ Dir[File.join(testing_dir("_site/drafts/sample-draft"), "*.html")].size.should == 1 end + context "for posts with an update: now header" do + around :each do |example| + begin + d = Serif::Draft.new(subject) + d.slug = "post-to-be-auto-updated" + d.title = "Testing title" + d.save("# some content") + d.publish! + + @temporary_post = Serif::Post.from_slug(subject, d.slug) + @temporary_post.autoupdate = true + @temporary_post.save + + example.run + ensure + FileUtils.rm(@temporary_post.path) + end + end + + it "sets the updated header to the current time" do + t = Time.now + 30 + Timecop.freeze(t) do + capture_stdout { subject.generate } + Serif::Post.from_slug(subject, @temporary_post.slug).updated.to_i.should == t.to_i + end + end + end + context "for drafts with a publish: now header" do before :all do @time = Time.utc(2012, 12, 21, 15, 30, 00)