The dev team at gap intelligence is committed to following Agile Scrum best practices. Once a month we have Dev Days where we get to spend an entire day working on passion projects. These can range from developing internal tools and organizing our Gulls and Padres season ticket to learning new techniques and increasing efficiencies in our current products. For our last Dev Day I decided to work on a simplified version of a planning poker tool.

Planning poker helps us to estimate future stories to work on. First, developers submit their estimates. Only after all the estimates are submitted, do we get to see everybody else's estimate. The benefit of doing this is that we can estimate stories without influencing one another.

I decided to use Action Cable, which is a new feature introduced as part of Rails 5. Action Cable integrates WebSocket communication with Rails applications by providing an easy-to-use, out-of-the-box experience. This is unique in that HTTP connections usually get closed as soon as data is sent. With WebSockets, the connection between the server and client is kept alive. This way, you can communicate in "real time".

A good example of WebSockets in use would be a chat client. In this app, messages must be updated in real time so that communication between users is able to flow.

With the help of Nithin Bekal's Action Cable tutorial, I was able to build a working version of our own planning poker tool, which we dubbed SWAG (Scientific Wild-Ass Guess).

SWAG Planning Poker

Not wanting to rewrite Nithin's post, I'm going to highlight what was implemented to get this to work.

Back-End Code

First thing to note is that we didn't use a database with this application, or at least not for this first version. We're using Redis to store session usernames and estimates.

This is what the Session class looks like:

class Session
  def add_username(username)
    usernames.add(username)

    self
  end

  def delete_username(username)
    Estimate.new(username).delete

    usernames.delete(username)

    self
  end

  def usernames
    @usernames ||= Redis::Set.new('session-usernames')
  end
end

 

Pretty straightforward. We store usernames in a Redis Set and we have the ability to add and delete usernames from said set.

We then have the Estimate class:

class Estimate
  attr_reader :username

  def initialize(username)
    @username = username
  end

  def delete
    redis_value.delete

    self
  end

  def size
    redis_value.value
  end

  def size=(value)
    redis_value.value = value
  end

  private

  def redis_value
    @redis_value ||= Redis::Value.new("estimate-#{username}")
  end
end

 

Estimates get stored as simple Redis Values and each username can only have a single value. This ensures that each person only has a single value when estimating a story. So if a user changes their mind, the previous value gets overwritten.

But the real "complex" part of the code lives in the SessionStatus class:

class SessionStatus
  attr_reader :session

  delegate :usernames, to: :session

  def initialize(session)
    @session = session
  end

  def complete?
    estimates.all? { |estimate| estimate.size.present? }
  end

  def consensus
    label = '-'

    with_sizes = estimates.reject { |estimate| estimate.size == '-' }
    totals = with_sizes.each_with_object(Hash.new(0)) { |e, hash| hash[e.size.to_i] += 1 }

    label = totals.keys.first if complete? && totals.keys.one?
    label
  end

  def reset!
    estimates.each do |estimate|
      estimate.size = nil
    end
  end

  def results
    usernames.collect do |username|
      h = {
        username: username,
        size: '-',
        selected: username_has_estimate?(username)
      }

      h[:size] = Estimate.new(username).size if complete?

      h
    end
  end

  private

  def username_has_estimate?(username)
    estimates.any? do |estimate|
      estimate.username == username &&
      estimate.size.present? &&
      estimate.size != '-'
    end
  end

  def estimates
    @estimates ||= usernames.collect { |username| Estimate.new(username) }
  end
end

 

We use this class for a few things:

  • Determine if everybody has submitted their vote
  • Figure out if we have a consensus across all votes
  • Reset all votes to estimate a new story
  • Get a hash to display all users with their votes on screen

Focusing on how we get results to display on screen:

def results
  usernames.collect do |username|
    h = {
      username: username,
      size: '-',
      selected: username_has_estimate?(username)
    }

    h[:size] = Estimate.new(username).size if complete?

    h
  end
end

 

Here we're building a hash with all usernames and defaulting their size to a dash ('-'). This is the default value everybody gets before submitting their vote.

We then check to see if all users have voted. If that's the case, then we assign the correct value to each username in the hash.

Setting up Our Channel

Having all the information we need to display to each user, we can now go ahead and create an Action Cable Channel. This class will include the basic features needed for our planning poker tool.

class SwagChannel < ApplicationCable::Channel
  def subscribed
    ...
  end

  def unsubscribed
    ...
  end

  def size(data)
    ...
  end

  def reset(data)
    ...
  end

  private

  def render_summary
    ...
  end
end

 

The default actions are subscribe and unsubscribe. These actions get called whenever a user signs on or off. Here we just add or remove the username from our session and broadcast an update to all connected users.

 

def subscribed
  if current_user.present?
    session = Session.new
    session.add_username(current_user)

    ActionCable.server.broadcast(
      'estimates',
      summary: render_summary
    )

    stream_from 'estimates'
  end
end

def unsubscribed
  if current_user.present?
    session = Session.new
    session.delete_username(current_user)

    ActionCable.server.broadcast(
      'estimates',
      summary: render_summary
    )
  end
end

 

Whenever a user casts a vote, the size method gets called. Two things happen here: first we store each user's vote, and we broadcast an update to all users. If not all users have voted, we just make a mark displaying which users have voted and are waiting. If all users have already voted, then we display everybody's vote for all to see.

 

def size(data)
  estimate = Estimate.new(current_user)
  estimate.size = data['size']

  ActionCable.server.broadcast(
    'estimates',
    summary: render_summary
  )
end

Client-Side Code

The only thing that's missing to tie all this together is our client side code. This is where we turn on our cable connection and handle all our different interactions.

App.swag = App.cable.subscriptions.create "SwagChannel",
  connected: ->
    # Called when the subscription is ready for use on the server

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    $('#estimates').html(data.summary)

    if data.reset
      $('.btn-group button').removeClass('active')

  reset: () ->
    @perform 'reset'

  size: (val) ->
    @perform 'size', size: val

$(document).on 'change', 'input:radio[class="size-btn"]', (event) ->
  App.swag.size($(this).val())

$(document).on 'click', 'button.btn__reset', (event) ->
  App.swag.reset()

 

Here you can see that every time a user casts a vote, the size method gets called. This will then call the size method in our WagChannel class.

What I think is the most important part of this code is the received method. This is what gets called every time an update gets broadcasted to all users.

received: (data) ->
  $('#estimates').html(data.summary)

  if data.reset
    $('.btn-group button').removeClass('active')

 

Here we can see that whenever we receive an update, the estimates div contents get replaced.

Conclusion

What I think is the greatest benefit of using Action Cable is that everything just works. All the connections between client-side and server-side code are built for you. You just have to tie the pieces together.

Like I mentioned earlier, Nithin Bekal's Action Cable tutorial was of great help to me. There are many more resources out there on the subject, so it should be pretty straightforward to get started.

What's Next?

As you can see from the code presented here, this first version is pretty basic. The most immediate next step is to test this out with our team during one of our backlog grooming sessions. This will help us work out any kinks that may (definitely) exist. Then there's always the possibility of just opening this up as a free service to anybody that wishes to use it. For this to happen we would need to tweak the code to handle many sessions at a time, give users the ability to set custom size values, write specs and a lot more testing.

In the meantime, we're always looking for ways to improve our own process. Sometimes that means building our own tools; sometimes that means using an existing service. But always making sure that we're happy with what we choose and are getting value from it.

See you next time.