Last active
October 21, 2018 18:24
-
-
Save tatwell/b4ed8ccae38727ed637fc19e6965bbd7 to your computer and use it in GitHub Desktop.
Trello Work-In-Progress Queue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# Get Trello API key and test token: https://trello.com/app-key | |
# | |
trello_api_key: TBA | |
trello_member_token: TBA | |
board_id: TBA |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# 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