Writing the Sidebar
More and more of my traffic (thanks to the wonderful stats tracking of both Mint and Google Analytics is coming from search engines.
So, in an attempt to try and keep visitors hanging around for longer (and to make it easier to see related stuff), I’d like to add a related articles Sidebar component.
For example, if users are on a page that mentions Domain Driven Design, I’d like them to see other articles that also cover Domain Driven Design.
And since Typo already uses tags for describing content, we can re-use the same mechanism. So, for any tags an article has, we can show other articles that share those same tags.
So let’s try writing it more as an XP story to focus our effort. How about…
The Story
As a blog owner, I’d like to make it easier for visitors to find content and related posts. I’d like to see a list of related articles appear in the sidebar when I view an article. I’d like relationships to be indicated through sharing tags.
Writing the Code
This post covers notes from when I was writing the Sidebar, and should cover my thought processes at the time pretty well. I’m definitely learning Ruby a whole lot still, so it was a great exercise for me to learn through. I’ve found also found Test Driven Development a great technique to pick up a new language and API etc. on my first ThoughtWorks project – a Java based banking project.
For anybody who isn’t familiar with Test Driven Development, or working with developer testing, I’d recommend this particularly great podcast from a talk Kent Beck gave around developer testing.
A lot of the tests make use of articles and tags that have been added to the fixtures. I won’t cover these here, but the changes are all inside the Subversion patch file.
The First Test
We want to know that we can find related articles so let’s get stuck in and write some of the unit testing code first. Once we have a list of related articles, then we can figure out how to display them. So inside our `article_test.rb` file let’s add the following
|
def test_can_find_related_article first_article = contents(:related_article_1) related_article = contents(:related_article_2) assert related_article.related_articles.include?(first_article)end
|
We’re using the existing `contents.yml` fixture to add some test articles (notably, `:related_article`). I won’t detail the changes here, since they’re mostly quite obvious. Full changes are available in the patch file should you want to see them.
The easiest way to make this test pass (and check our test is working correctly) is to return all articles.
|
def related_articles Article.find_allend
|
Great, it passed. But, we only want to display related articles if we do actually have related articles. So, let’s add a test that makes sure that if we have an article with no tags (and therefore no relationship to other articles) we have an empty array.
|
def test_returns_no_related_articles assert_equal 0, contents(:article3).related_articles.lengthend
|
If we run the test we’ll see it fail since we’re currently returning all articles. So, let’s test that we can restrict the articles returned to be those just matched by the tag. We can do that by changing our `related_articles` method
|
def related_articles articles = [] articles = Article.find_all unless self.tags.length > 0 articlesend
|
Run again and we’re good to go. Let’s now write a test to ensure that our related articles only includes those which share the same tag and that the first item is our related article (so we don’t find ourselves in the results etc.):
|
def test_can_find_only_related_article_with_same_tag first_article = contents(:related_article_1) related_article = contents(:related_article_2) assert_equal 1, first_article.related_articles.size assert_equal related_article, first_article.related_articles.firstend
|
If we run the test, we should see it fail since we have too many articles being returned.
1) Failure:
test_can_find_only_related_article_with_same_tag(TagTest) [test/unit/tag_test.rb:80]:
expected but was
.
Excellent. So now we need to write a proper finder method, and as with many things, there a couple of ways we could go forward.
One way, would be to write some custom SQL to do the querying in one hit, but I feel that that would probably take a little longer (and be a little more complex – and unclear) than just navigating the objects and their relationships. So, instead, we’ll write it as part of our `Article`.
|
def related_articles related = [] self.tags.each do |tag| related.concat tag.articles end related - [self]end
|
If we re-run our test we should see it pass. It’s not the prettiest code, but it works.
Finally, we also want to ensure that we only link to articles that are published, so we’ll need to remove any which haven’t been. Again, let’s write a test
|
def test_related_articles_only_includes_published_articles first_article = contents(:related_article_1) related_article = contents(:related_article_2) related_but_unpublished_article = contents(:article4) assert_equal 1, first_article.related_articles.size assert !first_article.related_articles.include?(related_but_unpublished_article)end
|
Let’s run the test and see that it fails because we need the additional `:related_article_6` entry in our fixture. After we add that we should see it fail with too many entries.
So, again, focusing on adding the simplest thing that’ll work, let’s add the code to filter out unpublished articles.
|
def related_articles related = [] result = [] self.tags.each do |tag| related.concat tag.articles end related.collect {|a| result << a if a.published && a != self} related - [self]end
|
If we run the test again now we should see it pass successfully. Of course, the code is pretty unattractive and un-Ruby like! So, let’s spend a little time tidying it up before we move on. How about…
|
def related_articles related = [] self.tags.each do |tag| tag.articles.collect {|a| related << a if a.published && a != self } end relatedend
|
That looks much tidier, and the tests still pass so we know it works. I’m happy with that for the time being, so let’s move on to the next part.
The Sidebar Component
We can now retrieve all articles that share the same tag. Rather than spend more time improving the algorithm for finding related articles, instead we’ll focus on delivering this simpler functionality into the hands of our customer sooner. This is what is often referred to as delivering ‘Vertical’ value: delivering a more complete but thinner story. A lot of what 37signals refer to in their Getting Real book has tie-ins to this kind of approach, focusing on simpler now, rather than waiting for everything.
So, the next step then is to write the Typo sidebar component that’s going to use our list of related articles in a list.
Now there doesn’t seem to be a great deal of content about writing tests for components in rails (read: nothing). But, since they’re supposed to be analogous to partials, but instead for controllers, it seems sensible to place them with our other functional tests. At least for the time being.
So, create a `related_articles_controller_test.rb` in the functional tests directory and let’s set about writing our first test.
The first thing we need to do is make sure we include the test helper (like all other functional tests). We also need to include our related articles controller (that doesn’t exist yet) so we can use it in our test. So, at the top of the file add the following
|
require File.dirname(__FILE__) + '/../test_helper'require File.dirname(__FILE__) + '/../../components/plugins/sidebars/related_articles_controller'
|
We also want to add all the setup stuff so we can use our controller inside our various tests, so let’s also add that
|
def setup @controller = Plugins::Sidebars::RelatedArticlesController.new @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new end
|
Now, on to our first Sidebar functional test.
The First Sidebar Test
We want to know that we can see the related links when we view one of our previously created articles (we’ll re-use the related articles stuff in the fixtures we created earlier for our unit tests).
|
def test_when_we_have_an_article_we_can_see_related_items get :content, :contents => [contents(:related_article_1)] assert_response :success assert_tag :tag => 'ul', :children => {:count => 2, :only => {:tag => 'li'}}end
|
Typo uses a `contents` array inside Rails’ `params` object to pass the items we’re viewing around. So, we’ll pass in one of our articles as that contents. We then go on to test that our controller renders successfully, and that we can a list with two items.
If we run the test, we’ll see that we haven’t created the sidebar component source file yet so let’s create the structure for our component. Add a `related_articles` directory under the `components/plugins/sidebars` directory. Then, inside that directory add `content.rhtml`, and finally, add `related_articles_controller.rb` inside the `components/plugins/sidebars` directory.
Once that’s been done, let’s add the various sidebar stub stuff (as well as our `content` action that we’re `GET`ting in our test:
|
class Plugins::Sidebars::RelatedArticlesController < Sidebars::ComponentPlugin description "Displays articles that share the same tag." def content #do stuff endend
|
If we run the test, we’ll see that we don’t find any lists, so we need to edit our view and wire it up.
So, we’re going to need our `RelatedArticlesController` to give us an array (`articles`) that’ll contain the links we want to show. If we run the test now, our `assert_response :success` will fail since our view can’t find what it needs. The simplest way to fix that is to have the following in our controller
|
def content @articles = []end
|
We’ll just use an empty array for the time being. That’ll then mean our second assertion will fail since we also need to see two items (for our article).
So, we’ll add the code to do that.
|
def content @articles = params[:contents].first.related_articlesend
|
If we run our test, we’ll see it passes. Onto the next test!
Now, seeing a list is all very well, but we need to be able to click links within that list that’ll take us to the various articles (since we’re trying to drive up the number of views people make when visiting).
|
def test_we_can_see_article_links article = contents(:related_article_2) get :content, :contents => [article] assert_tag :tag => 'li', :children => {:only => {:tag => 'a'}} assert_tag :tag => 'a', :attributes => {:href => article.location}, :content => article.titleend
|
Firstly, we’ll check our list items have anchor tags to display the links, and then check those links have the correct address in them.
If we run the test, you should see the message ”`no tag found matching :attributes=>{:href=>”/articles/2004/06/01/article-4”}, :tag=>”a”, :content=>”Article 4!”`” appear.
Typo has some helpers for writing links to various bits of content, so we can re-use those to actually generate the tag. So, inside our `content.rhtml` view, add
If we re-run we should see our test pass.
Since Typo uses the `contents` array in the `params` object to pass articles around for all pages we’ll need to check that we only see our sidebar when we’re viewing an article, and not viewing the home page (for instance).
|
def test_when_viewing_home_page_no_related_articles_shown get :content, :contents => [ contents(:related_articles_1), contents(:related_articles_2) ] assert_response :success assert_no_tag :tag => 'ul'end
|
If we re-run we’ll see our test fail because we can still see our list. So, we’ll need to add some logic to our controller to only provide an articles list if we’re viewing an individual article, otherwise we’ll just leave it with an empty array.
|
def content @articles = [] @articles = params[:contents].first.related_articles unless params[:contents].to_a.size > 1end
|
If we run our tests again we’ll see it pass. Now, that code’s pretty ugly so let’s try to refactor it a bit now that we have some tests to make sure we don’t break anything.
Firstly, let’s try and extract out how we get the current article we’re viewing from the `params` object as follows
|
def content @articles = [] @articles = article.related_articles unless params[:contents].to_a.size > 1endprivatedef article params[:contents].firstend
|
Then, let’s run our tests and make sure everything is still passing. Excellent, now it’s not immediately clear why we’re checking the length of `params[:contents]` (other than we don’t fill articles). So, let’s make it a little clearer
|
def content @articles = [] @articles = article.related_articles if viewing_article?end
|
and add another private method
|
def viewing_article? params[:contents].to_a.size == 1end
|
Much better. As a little safety net, we’ll also make sure that we don’t consider ourselves to be viewing an article unless the first item is also an article. So, let’s add
|
def test_unless_first_item_is_an_article_display_nothing get :content, :contents => [tags(:foo_tag)] assert_response :success assert_no_tag :tag => 'ul'end
|
If we run the test we’ll get a 500 error because we’re assuming that if we have one item in our array it’s an article and that we can get related articles. So, let’s add the code to only consider ourselves as viewing an article when the first item is indeed an article.
|
def viewing_article? params[:contents].to_a.size == 1 && params[:contents].first.kind_of?(Article)end
|
If we re-run the tests we’ll see it all run successfully.
Finally, if we don’t have any related articles, we’d prefer it if we didn’t see anything in the sidebar, rather than us having an empty list. Our test:
|
def test_if_no_related_articles_found_nothing_is_displayed get :index, :contents => [contents(:article3)], :sidebar => @sidebar assert_response :success assert_no_tag :tag => 'div'end
|
If we run the test, we’ll fail because we’ll still have our `div` containing an empty list. All we need to do is wrap our `div` within the following
Re-run the test, watch it pass, and we can proceed onto the Sidebar configuration aspect of our code.
Sidebar Configuration
The advantage of Typo’s Sidebars are that they allow you to configure them through the administration interface.
We want to be able to configure the maximum number of articles that are shown in the related articles list, and the title that will appear above the list.
Our first test is that our configuration includes a Title, that will appear above the list.
|
def test_can_configure_title_in_admin assert_equal "title", Plugins::Sidebars::RelatedArticlesController.fields.first.key assert_equal "Related Articles", Plugins::Sidebars::RelatedArticlesController.default_config["title"]end
|
We’re testing that we have a configurable title, and that the default value is “Related Articles”. As usual, run the tests and watch them fail. Onto the implementation (inside our `RelatedArticlesController`):
|
class Plugins::Sidebars::RelatedArticlesController < Sidebars::ComponentPlugin setting :title, 'Related Articles'
|
Re-run the tests and we can watch them pass.
Now we want to make sure we can see that title above our list, so let’s write a test that we find an `h3` element with our title in it
|
def test_can_see_default_title get :content, :contents => [contents(:related_article_3)] assert_response :success assert_tag :tag => "h3", :content => "Related Articles"end
|
At the moment, the test will fail because our template hasn’t yet had the code added. So, in the interest of getting it passing quickly, let’s do the following:
Related Articles
...
Our test will now pass, so we know that we’re finding the element correctly. Now let’s proceed to write another test that will break our shortsighted implementation. Here we’ll be using the `index` action within our controller, and providing it with Sidebar configuration. Note that this is different for our previous tests, since this is the first time we’ve needed to test this kind of code. At this point (when I was working) I changed the previous test code to match this.
123456789
def test_can_see_configured_title sidebar = Sidebar.new() sidebar.config = {:title => 'My Articles'} get :index, :contents => [contents(:related_article_3)], :sidebar => sidebar assert_response :success assert_tag :tag => "h3", :content => "My Articles"end
Here we’re also passing through our `:sidebar`, this is the representation of the item’s configuration. So, when we render our component our controller can pull the `title` `setting`. So, let’s add the code inside our view template
...
If we run the test, we’ll get a failure since our title configuration setting doesn’t exist on our Sidebar component yet. All Sidebars are configured the same way, and nicely (thanks to some refactoring from Piers Cawley a while back)
All we need to do is add a setting as follows
12
class Plugins::Sidebars::RelatedArticlesController < Sidebars::ComponentPlugin setting :title, 'Related Articles'
If we re-run the test all should pass, and we can write our second configuration test – that we are able to set the number of items we’d like to see in our list. We also want to have a label for our field also – this will appear next to the textbox to indicate what the setting controls.
12345
def test_can_configure_number_of_items_in_admin assert_equal 'count', Plugins::Sidebars::RelatedArticlesController.fields.last.key assert_equal 5, Plugins::Sidebars::RelatedArticlesController.default_config['count'] assert_equal 'Number of Articles', Plugins::Sidebars::RelatedArticlesController.fields.last.options[:label]end
If we run our test, we’ll see a failure because we only have one field
<"count"> expected but was<"title">
So, let’s add a setting to fix the first assertion
123
class Plugins::Sidebars::RelatedArticlesController < Sidebars::ComponentPlugin setting :title, 'Related Articles' setting :count, 5
If we re-run, we’ll have a failure because our configuration field’s options doesn’t contain a label. So, let’s add that to our setting definition in our controller
setting :count, 5, :label => 'Number of Articles'
If we re-run our test, we’ll see it pass!
Finally, we need to write the tests to ensure that we limit the number of items we actually see to the number that’s specified in the configuration. Let’s take our example from an earlier test. This time, we’ll say we only want to see one item though (rather than the two that are actually associated):
123456
def test_can_see_limited_number_of_items get :index, :contents => [contents(:related_article_3)], :sidebar => @sidebar assert_response :success assert_tag :tag => 'ul', :children => {:count => 1, :only => {:tag => 'li'}}end
To do this, we’ll need to add a limiter to our `related_articles` method that can limit the number of results. Let’s use a similar approach to elsewhere, so we want to be able to call `article.related_articles(:limit => 1)` (for example).
So, let’s add the code to our article’s test
1234567
def test_can_find_a_limited_result_set article = contents(:related_article_3) first_related_article = contents(:related_article_4) second_related_article = contents(:article2) assert_equal 1, article.related_articles(:limit => 1).sizeend
If we run the test now we’ll get an exception for not having enough parameters. So let’s add the parameter and run the test again
12
def related_articles(*params) related = []
We’ll get a test failure (which is good – we now need to implement the restriction).
1234567891011121314
def related_articles(*options) related = [] self.tags.each do |tag| tag.articles.collect {|a| related << a if a.published && a != self } end if options.length == 0 related else params = options[0] count = params[:limit] - 1 related[0..count] endend
If we run the test again now, we’ll see it pass. It works by converting the parameters to the method into an array. We then pull down the first of the parameters (assumed to be a hash of arguments), and we then retrieve a limit from the hash.
Next, we want to make sure that if we specify a limit which is larger than the items we have, we return all items anyway
1234567
def test_retrieves_all_items_when_specifying_an_out_of_range_index article = contents(:related_article_3) first_related_article = contents(:related_article_4) second_related_article = contents(:article2) assert_equal 2, article.related_articles(:limit => 5).sizeend
Brilliant. All tests passing, we can fire up the application, add our sidebar, and enjoy the result :)
Iteration Retrospective Well, it’s been an interesting experience. Not only because this is my first real dig around Typo’s code (which seems to be in a state of flux right now – but it’s great to see it’s getting attention!), and my first play around with Ruby code I’ve not written (which I’m sure is almost always not as great as it could be).
It took me a little while to figure out how to write and test the code (largely because of my unfamiliarity) with the Typo Sidebar code (and Typo in general). And the Sidebar seemed a little too complicated for my liking (I may try and refactor some of the code when I feel a little more confident in my understanding), but once again Rails’ functional testing made me so much more confident writing code.
What really didn’t help was not being able to find any tests for the various Sidebar components, so I had really no idea how to go about doing it! But writing code test first really made it easier for me to focus, and made me more confident the code I was writing was working. And, that should any future Typo changes be made, I know that my Sidebar is less likely to break unbeknownst to me! Piers has also commented that he is considering changing the way the Sidebars work. I’m definitely intrigued by his suggestions from what I’ve seen, and may even give some refactoring a go myself and see where I end up. You gotta love evolutionary design :)
I’m also not sure whether I like Rails’ definition of unit and functional testing, but I appreciate what it’s trying to achieve. I’m also still undecided as to whether I prefer having test object setup done inside fixtures versus inside the tests themselves. I had to do lots of to-ing and fro-ing between my unit test, `contents.yml`, `tags.yml` and `articles_tags.yml`. That context switching became expensive, especially during late night hotel hacking when I started to get tired and lose focus.
Not only that, but a lot of Typo’s tests rely on it having a predefined number of published articles (in an assumed order), by adding new articles for the new tests, it broke some of the existing tests which meant investigating that the number was indeed meant to change. I’m not too sure of the best way forward in this situation.
Where to go from here I’m posting this article, hopefully to give people ideas about how they can go about extending existing open source Rails code, and hopefully that people will take a look at my code and make suggestions/comments about it. I’m especially excited by the prospect of people getting my code from the public Subversion repository, and submitting patches in Trac.
It’s this process of constantly learning from other people (whilst pairing both with other ThoughtWorkers and client developers) that makes it such a brilliant place to learn (and have fun).
I’m definitely going to improve the way articles are considered to be related, hopefully ordering the list based on the number of shared tags. I’m also going to set to work on writing a plugin right away to make it easy for other Typo users to get this functionality into their install without needing to update their whole application.
Once I’ve made a few improvements (I’d also like to make sure the persistence store access code is a little better at not having to retrieve too much data in one go) I’ll be sure to make a ticket on Typosphere and see if I can get it into the trunk for the future, if that’s where the project wants these kinds of things to go.
I’ll work on getting a better packaged component together as soon as possible, look out for future posts!