Customizing Trello with Ruby

Arne Brasseur

trello-icon

Trello is a great collaboration tool. It’s based around boards, which contain lists of cards. How you use it is up to you, as it’s one of those simple tools with many uses. Want to plan or coordinate with multiple people, or just manage your own todos? Just set up a board and get going.

All of Trello’s features can be controlled through its API, and it’s completely free, including use of the web and mobile apps, as well as access to the API. With the ruby-trello gem you can be automating your boards in no time.

Getting Started

Sign up if you haven’t already, and grab your developer keys, as you will need these to make API requests. Install the ruby-trello gem

gem install ruby-trello

and request a “member token” that your brand new application can use to identify itself

TRELLO_DEVELOPER_PUBLIC_KEY="your key" # found at https://trello.com/1/appKey/generate
puts "please visit"
puts "https://trello.com/1/authorize?key=#{TRELLO_DEVELOPER_PUBLIC_KEY}&name=SitepointTutorial&expiration=never&response_type=token&scope=read,write"
puts "and take note of the token"

Now we can start setting up and using the Trello API

require 'trello'

TRELLO_DEVELOPER_PUBLIC_KEY="your key"
TRELLO_MEMBER_TOKEN="the token you just got"

Trello.configure do |trello|
  trello.developer_public_key = TRELLO_DEVELOPER_PUBLIC_KEY
  trello.member_token = TRELLO_MEMBER_TOKEN
end

Trello::Board.all.each do |board|
  puts "* #{board.name}"
end

Building Blocks: Boards, Lists and Cards

If all went well, you just saw a list of your existing Trello boards pop up in the terminal. Boards are the top-level organizing concept in Trello, represented by instances of Trello::Board in the Ruby API wrapper.

Note that Trello::Board has a class method named all, just like ActiveRecord models do. This is no coincidence. ruby-trello makes use of ActiveModel under the hood, making its core classes walk and quack a lot like Rails models. This makes the API seem familiar to Rails developers, and it means that you can use instances of Trello::Board, Trello::Card or Trello::List with Rails form helpers. For example, all these objects have a valid? method, just like ActiveRecord instances, and when invalid the errors method will tell you what the problem is.

Building on Top of Trello

To demonstrate using the API, we will implement a simple “app” using Trello as the user interface. Imagine as an organization you want to keep an eye on how people talk about you on Twitter. As a first step, you want to simply count the number of positive, negative, and neutral tweets. To do so, we will set up a board with four lists. For every tweet, a card will pop up in the “Incoming” list. The friendly customer support people can then drag each tweet to the “Positive”, “Neutral”, or “Negative” list. At the top of each list, we’ll add a card that keeps a tally of the number of cards added. Every now and then we archive all cards and update the count.

Here you see our app in action, tracking mentions of the term “books”.

Twitter Follow-up "app" built on top of Trello

Creating the Board and Lists

To get started, get a reference to our board. If it isn’t found, create it. Trello will already add a few lists for us (Todo, Done), but we prefer to set up our own. As such, close the Trello-provided lists first and then add our own. Finally, add the card that will keep track of the count to the last three lists.

BOARD_NAME = 'Twitter Followup'
BOARD_DESC = 'Handling mentions on Twitter'
LIST_NAMES = ['Incoming', 'Positive', 'Neutral', 'Negative']

board = Trello::Board.all.detect do |board|
  board.name =~ BOARD_NAME
end

unless board
  board = Trello::Board.create(
    name: BOARD_NAME,
    description: BOARD_DESC
  )
  board.lists.each(&:close!)

  LIST_NAMES.reverse.each do |name|
    Trello::List.create(name: name, board_id: board.id)
  end

  board.lists.drop(1).each do |list|
    Trello::Card.create(name: '[0]', list_id: list.id)
  end
end

Notice how Trello::Card and Trello::List each take the id of the enclosing entity, respectively the list and the board.

For a List, the ruby-trello gem doesn’t give you more options than that. For a Card you can get more creative though.

card = Trello::Card.create(
  name: 'card with memberd and labels',
  list_id: list.id,
  card_labels: [ :yellow, :green ],
  member_ids: [ Trello::Member.find('arnebrasseur').id ]
)

Updating a card’s attributes works by simply setting new values, and consequently calling card.save.

card = board.lists[0].cards[0]
card.name = 'First card in the first list, I am'
card.desc += "\n\nThe end of my description, this is."
card.save

Notice that ActiveRecord-like syntax board.lists.create(...) does not work. To add a list to a board, or a card to a list, you have to explicitly get the id of the board or list and pass it to the new object.

The Beating Heart

To make our app ‘tick’, we will need two different background jobs. One is responsible for pushing Tweets onto the board, the other takes care of counting and archiving the cards that have been sorted into categories. We will combine both in a single script, so we can invoke it like this:

$ twitter_followup.rb stream sitepoint

To get all tweets that mention Sitepoint Use Twitter’s streaming API, or:

$ twitter_followup.rb process

To process cards that have been sorted.

The following code block is a straightforward way of reading the command line arguments and delegating to either a listen_to_tweets method, or to process_sorted_cards. The board is already available to us. The definition of twitter_client is beyond the scope of this article, but you can have a look at the complete script, or find out more about using the Twitter gem on Sitepoint.

case ARGV[0]
when 'stream'
  topics = ARGV.drop(1)
  listen_to_tweets(board, twitter_client, topics)
when 'process'
  loop do
    process_sorted_cards(board)
    sleep 5
  end
end

Drinking From Twitter’s Firehose

Support for Twitter’s streaming API is a relatively new addition to twitter gem. Lucky for us, since it makes implementing listen_to_tweets short and sweet.

def listen_to_tweets(board, twitter_client, topics)
  incoming_list = board.lists.first

  twitter_client.filter(:track => topics.join(",")) do |object|
    if object.is_a?(Twitter::Tweet)
      Trello::Card.create(
        list_id: incoming_list.id,
        name: object.text,
        desc: object.url.to_s
      )
    end
  end
end

The code sets the tweet’s text as the card’s “name”, which is the part you see when looking at the board. When you open a card you see its description, which is used here to keep a link back to the original tweet. We will pull those pieces of data back out again before archiving the card.

Adding Things Up

To do the actual count-and-archive step, we implement a few helper methods. The first returns the Trello::List instances that need processing by looping over the lists on the board. Using Enumerable#find returns the first object for which the given block returns a truthy value.

def score_lists(board)
  LIST_NAMES.drop(1).map do |name|
    board.lists.find { |list| list.name == name }
  end
end

Similarly, we need a reference to the Trello::Card that holds the count for each column. A regular expression looks for a card with only digits surrounded by square brackets. The \A, which matches the beginning, and \z, which matches the end of the string, are important anchors to prevent false positives. They are usually preferred over ^, $, since these will match the beginning and end of any line in a multi-line string.

def find_score_card(list)
  list.cards.find do |card|
    card.name =~ /\A\[\d+\]\z/
  end
end

Finally, process each card that has been sorted, archive it, and update the score card. The score card’s description keeps a list of all the tweets that have been added to it. Card descriptions allow Markdown formatting. To get a bullet point list, including links to the original tweets, our markup will look like this

* tweet text [↗](http://link.to.the.tweet)`
* next tweet #trelloRuby [↗](http://link.to.the/second/tweet)`

The process_card method handles an individual card that is ready to be archived:

def process_card(card, score_card)
  old_score = score_card.name[/\d+/].to_i
  new_score = old_score + 1

  tweet_text = card.name
  tweet_link = card.desc

  score_card.name = "[#{new_score}]"
  score_card.desc = "* #{tweet_text} [↗](#{tweet_link}) \n" + score_card.desc

  card.closed = true

  [card, score_card].each(&:save)
end

Note that the action called “archiving” in Trello’s UI is consistently called “closing” in the API. This goes for boards, lists, and cards, as well. Trello does not allow deleting anything, but it’s possible to archive/close any object. This means it will longer show up, unless explicitly requested. Setting the closed property and saving the card (or list, board), can also be done in a single step, e.g. card.close!.

Almost done, we just need to go through the score_lists and process each card, except the score_card itself:

def process_sorted_cards(board)
  score_lists(board).each do |list|
    score_card = find_score_card(list)
    list.cards.each do |card|
      unless card.id == score_card.id
        process_card(card, score_card)
      end
    end
  end
end

Go Out and Play

Trello has a nice, clean interface, and through the API, a lot is possible.

  • Extend our Twitter app so writing comments automatically replies to the tweet.
  • Integrate with Git or Github through post-commit and web hooks, to schedule a code review task for every commit.
  • Perhaps randomly adding animated cat gifs to each card on Friday afternoon sounds more like your thing.

So what would you like to build? Let us know in the comments!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • KerryJTruong

    Google is paying 80$ per hour! Work for few hoursand have more time with friends & family! On tuesday I got a great new Land Rover Range Rover>>CLICK NEXT TAB FOR MORE INFO AND HELP</b