Customizing Trello with Ruby
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”.
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!