How To: Write Time-Sensitive or Expiring Content

Posted by Sean Cribbs on Thursday, November 23, 2006 | |

Suppose you have a page that you want to show on your Radiant website for a limited time. At my day job, the Human Resources department needs to have job postings open up and expire on certain days, and in the past, with our manual update of pages on the server, this was very tedious. Here’s how we’ll solve this problem with Radiant.

Tag Design

This is definitely a case when we need to write a new tag, so let's design it to fit our needs.

  <r:expires on="2006-12-01">
    Your time sensitive content goes here!
  </r:expires>

The tag above will let us make the content expire on the first of December. That's great and all, but what if we want to be over-achievers and post the job before HR opens the position for application? Let's design another tag.

  <r:valid-dates from="2006-11-24" to="2006-12-01">
    Your time sensitive content goes here!
  </r:valid-dates>

Beautiful. Now let's see how to make this happen with Radiant.

Tag implementation

These should be implemented as global tags, so refer back to my first How-To if you want to see how. However, since we're using the "mental" branch of the current codebase, I'll give you a taste of how to implement the tags in the new way. Let's start with the tag definition.

  tag "expires" do |tag|
    expires = Date.parse(tag.attr["on"])
    tag.expand if Date.today <= expires
  end

The first line pulls out the value of the attribute we put on the tag and parses it into a Date. If you wanted to specify a time as well as a date, you would need to use DateTime.strptime, and then modify the next line to use Time.now instead of Date.today. Alternatively, if you're familiar with it, you could use the wonderful Chronic library. The second line will parse and expand the contents of the tag if the current date is the same or before the current date. The second tag we designed above is implemented very similarly.

  tag "valid-dates" do |tag|
    from = Date.parse(tag.attr["from"])
    to = Date.parse(tag.attr["to"])
    tag.expand if (from..to).include? Date.today
  end

Just like the tag definition above, the first two lines parse out our tag attributes. The third line uses a little Ruby trick which is to create a range of dates using from..to. Then we see if the current date is within the range using include? and expand the tag if that's the case.

Implementing the tags with the "mental" branch

The "mental" branch of Radiant, which contains a preview of the API for extensions that will be solidified in 0.6-Lapidary release, has a little different way of handling global tags. Behaviors went the way of the dodo, so now you mix your tags directly into the Page model with a module. Sound confusing? I'll show you how.

First, we're going to create a module to hold our tags. Open up your favorite text editor and type this in.

  module ExpiryTags
    include Radiant::Taggable

    # Insert the tag definitions from above here
    # ...
  end

As the comment says, dump those tag definitions we created above into the module code, right before the end. The second line of the above snippet adds some methods to our module that allow you to write your tag definitions in it directly, and help us when we add them to the Page model. Now let's make those tags accessible from all pages. Add this line to the bottom of your file, outside the module (after the end).

  Page.send :include, ExpiryTags

That's what Rubyists mean when they say "mixing in a module". It's just like the "include" we did above to get the Radiant::Taggable methods inside our module, except we're doing it from the outside.

With the mental branch, you can choose several ways to get this new code into Radiant. You can save the file we just created into the lib/patches directory, or you can generate an extension and put the Page.send line above in the activate method of your extension. A lot of that is probably Greek to some, but I'll explain more in a future How To where we'll convert a behavior to the new extension system.

Test, Test, Test!

You may have noticed that when you generate an extension, you get some stuff in the test/ directory. It's on purpose! In the past I haven't been too strict about writing tests, but let's write some to see if our tags work. Let's assume you've generated an "ExpiryTags" extension and we've opened the generated functional test.

  class ExpiryTagsExtensionTest < Test::Unit::TestCase
    def setup
      @page = Page.new
      @page.send :lazy_initialize_parser_and_context
      @yesterday = Date.today - 1
      @tomorrow = Date.today + 1
    end

    # Custom assertion - make sure some Radius tags render a certain text
    def assert_renders_to(tags, expected)
      text = @page.send :parse, tags
      assert_equal expected, text
    end

    def test_includes_expiry_tags
      assert Page.included_modules.include? ExpiryTags
    end

    def test_expires_tag
      assert_renders_to %{<r:expires on="#{@yesterday.to_s}">This does not show!</r:expires>}, ""
      assert_renders_to %{<r:expires on="#{@tomorrow.to_s}">This does show!</r:expires>}, "This does show!"
    end

    def test_valid_dates_tag
      assert_renders_to %{<r:valid-dates from="#{@yesterday.to_s}" to="#{@tomorrow.to_s}">This does show!</r:valid-dates>}, "This does show!"
      assert_renders_to %{<r:valid-dates from="#{@tomorrow.to_s}" to="#{(@tomorrow+1).to_s}">This does not show!</r:valid-dates>}, ""
      assert_renders_to %{<r:valid-dates from="#{(@yesterday-1).to_s}" to="#{@yesterday.to_s}">This does not show!</r:valid-dates>}, ""
    end
  end

You can use that custom assertion in all of your extensions that create global tags. With how easy it is now, I know I'll be testing all of my tags from now on.