Skip to content

Instantly share code, notes, and snippets.

@tatwell
Last active October 21, 2018 18:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tatwell/b4ed8ccae38727ed637fc19e6965bbd7 to your computer and use it in GitHub Desktop.
Save tatwell/b4ed8ccae38727ed637fc19e6965bbd7 to your computer and use it in GitHub Desktop.
Trello Work-In-Progress Queue
#
# Configuration:
# - $ gem install ruby-trello
# - updated settings in credentials.yml
#
require 'trello'
require 'yaml'
require 'pry'
#
# Constants
#
#
# Trello Action
#
class TrelloAction
EVENTS = [:create_story, :groom_story, :commit_story, :complete_story]
attr_accessor :action, :event, :object, :queues, :transactions
def initialize(action)
@action = action
@object = categorize_object
@event = categorize_event
end
#
# Class Methods
#
def self.dump_board(board_id)
actions = []
board = Trello::Board.find(board_id)
board_actions = board.actions(limit: 1000).reverse
board_actions.each do |board_action|
action = TrelloAction.new(board_action)
actions << action
puts action
end
actions
end
def self.select_by_object(actions, object)
actions.select{ |a| a.object == object }
end
def self.select_by_event(actions, event)
actions.select{ |a| a.event == event }
end
#
# Attrs
#
def trello_type
@action.type
end
def raw_data
@action.data
end
def trello_object_id
return raw_data['card']['id'] if card?
return raw_data['board']['id'] if board?
return raw_data['list']['id'] if list?
end
def card?
@object == :card
end
def list?
@object == :list
end
def board?
@object == :board
end
#
# Instance Methods
#
def categorize_event
card_creation_events = ['convertToCardFromCheckItem', 'copyCard', 'createCard']
return :created if trello_type == 'createList'
return :added_member if trello_type == 'addMemberToCard'
return :deleted if trello_type == 'deleteCard'
return :created if card_creation_events.include? trello_type
return :changed_due_date if card? && old_data?('due')
return :changed_description if card? && old_data?('desc')
return :changed_list if card? && old_data?('idList')
return :changed_name if card? && old_data?('name')
return :changed_name if list? && old_data?('name')
return :changed_name if board? && old_data?('name')
return :changed_position if card? && old_data?('pos')
return :changed_position if list? && old_data?('pos')
return :marked_complete if card? && old_data('dueComplete') == false
return :closed if card? && old_data('closed') == false
return :reopened if card? && old_data('closed') == true
return @object if [:board, :checklist, :comment, :plugin].include? @object
nil
end
def old_data(key)
return nil if @action.data['old'].nil?
@action.data['old'][key]
end
def old_data?(key)
return false if @action.data['old'].nil?
@action.data['old'].has_key? key
end
def categorize_object
type_object_map = {
board: ['addMemberToBoard', 'addToOrganizationBoard', 'createBoard', 'updateBoard'],
card: ['addMemberToCard', 'convertToCardFromCheckItem', 'copyCard', 'createCard',
'deleteCard', 'updateCard'],
checklist: ['addChecklistToCard', 'removeChecklistFromCard', 'updateCheckItemStateOnCard',
'updateChecklist'],
comment: ['commentCard'],
list: ['createList', 'updateList'],
plugin: ['enablePlugin']
}
type_object_map.each do |obj, types|
return obj if types.include?(trello_type)
end
nil
end
def to_s
f = '#<Action id=%s object=%s object_id=%s event=%s type=%s>'
format(f, @action.id, @object, trello_object_id, @event, trello_type)
end
end
#
# Main
#
def main
credentials = YAML.load_file('credentials.yml')
Trello.configure do |config|
config.developer_public_key = credentials['trello_api_key']
config.member_token = credentials['trello_member_token']
end
actions = TrelloAction.dump_board(credentials['board_id'])
binding.pry
end
#
# Runtime
#
if __FILE__ == $0
main()
end
#
# Get Trello API key and test token: https://trello.com/app-key
#
trello_api_key: TBA
trello_member_token: TBA
board_id: TBA
#
# Reference: https://trello.com/c/JhhTtf5E
#
# Configuration:
# - $ gem install ruby-trello
# - updated settings in credentials.yml
#
require 'trello'
require 'yaml'
require 'pry'
#
# Constants
#
AGILE_TOOLS_PLUGIN_ID = '59d4ef8cfea15a55b0086614'.freeze
#
# WIP (Work-in-Progress) Ledger
#
class WipLedger
WIP_QUEUES = [:wish_heap, :backlog, :current_sprint]
attr_accessor :board, :events, :queues, :transactions
def initialize(board_id)
@board = Trello::Board.find(board_id)
@queues = init_queues
@transactions = []
@events = rebalance
end
def init_queues
queues = {}
WIP_QUEUES.each{ |q| queues[q] = {} }
queues
end
def rebalance
wip_events = []
all_actions.each do |action|
event = WipEvent.new(action)
next unless event.wip?
wip_events << event
@transactions << transact(event)
puts event
end
wip_events
end
def all_actions
max_actions = 1000
all_actions = []
more = true
before = nil
while more
actions = @board.actions(limit: max_actions, before: before)
actions.each{ |action| all_actions << action }
before = actions.last.id
more = actions.length == max_actions
end
all_actions.reverse
end
def transact(event)
transaction = {columns: {}, time: event.created_at}
if event.card_created?
add_to_queue(event.story)
puts "Created story: #{event.story}"
elsif event.story.moved?
@queues[event.story.old_queue].delete(event.story_id)
add_to_queue(event.story)
puts "Moved story: #{event.story}"
end
transaction[:columns] = compute_columns
transaction
end
def add_to_queue(story)
@queues[story.queue] = {} if @queues[story.queue].nil?
@queues[story.queue][story.id] = story
end
def compute_columns
columns = {
wish_heap_stories: open_stories(:wish_heap).length,
wish_heap_points: sum_points(open_stories(:wish_heap)),
backlog_stories: open_sized_stories(:backlog).length,
backlog_points: sum_points(open_sized_stories(:backlog)),
sprint_stories: open_sized_stories(:current_sprint).length,
sprint_points: sum_points(open_sized_stories(:current_sprint)),
total_wip: wip_points
}
end
def open_stories(queue)
@queues[queue].values.keep_if(&:open?)
end
def sized_stories(queue)
@queues[queue].values.keep_if{ |story| story.points > 0 }
end
def open_sized_stories(queue)
# intersection (&) of open and sized stories
open_stories(queue) & sized_stories(queue)
end
def sum_points(stories)
stories.map{ |story| story.estimated_points(self) }.sum
end
def wip_points
sum_points(open_stories(:wish_heap)) +
sum_points(open_sized_stories(:backlog)) +
sum_points(open_sized_stories(:current_sprint))
end
def average_story_points
total_stories = 0
total_points = 0
WIP_QUEUES.each do |q|
stories = sized_stories(q)
total_stories += stories.length
total_points += sum_points(stories)
end
return 0 unless total_stories > 0
(total_points.to_d / total_stories.to_d).ceil
end
def latest
transactions.last
end
end
#
# WIP Event
# Enhanced interface for Trello board action.
#
class WipEvent
attr_accessor :action, :story
def initialize(action)
@action = action
@story = associate_story if wip?
end
def associate_story
story = Story.find_by_event(self)
if story
story.events << self
else
story = Story.new(self)
end
story
end
def card
@action.card
end
def story_id
card.id
end
def wip?
card_created? || card_moved?
end
def card_created?
['createCard', 'copyCard', 'convertToCardFromCheckItem'].include? @action.type
end
def card_moved?
@action.type == 'updateCard' && @action.data['listAfter'].present?
end
def created_at
@action.date
end
def to_s
format("<WipEvent created=%s type=%s>", created_at, @action.type)
end
end
#
# Story
# User story interface wrapping Trello card.
#
class Story
@@storage = {}
attr_accessor :events, :points
def initialize(event)
@events = [event]
@card = action.card
@points = fetch_points
Story.store(self)
end
def self.store(story)
@@storage[story.id] = story
end
def self.find_by_event(event)
@@storage[event.story_id]
end
def self.storage
@@storage
end
def id
card.id
end
def event
@events.last
end
def action
event.action
end
def card
action.card
end
def list
return action.data['list'] if action.data['list']
return action.data['listAfter'] if action.data['listAfter']
nil
end
def queue
list['name'].parameterize.underscore.to_sym unless list.nil?
end
def old_queue
list = action.data['listBefore']
list['name'].parameterize.underscore.to_sym unless list.nil?
end
def estimated_points(ledger)
return @points if @points > 0
ledger.average_story_points
end
def fetch_points
agile_plugin = @card.plugin_data.find { |pd| pd.idPlugin == AGILE_TOOLS_PLUGIN_ID }
agile_plugin.present? ? agile_plugin.value['points'].to_i : 0
end
def created?
['createCard', 'copyCard', 'convertToCardFromCheckItem'].include? action.type
end
def moved?
old_queue.present?
end
def closed?
card.closed
end
def open?
!closed?
end
def to_s
f = '<Story id=%s name="%s" queue=%s points=%s events=%s>'
format(f, id, card.name, queue, points, @events.length)
end
end
#
# Main
#
def main
credentials = YAML.load_file('credentials.yml')
Trello.configure do |config|
config.developer_public_key = credentials['trello_api_key']
config.member_token = credentials['trello_member_token']
end
ledger = WipLedger.new(credentials['board_id'])
binding.pry
end
#
# Runtime
#
if __FILE__ == $0
main()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment