Wiki

Case Status Kiln
Register Log In

Wiki

 
Milestones to Trello
  • RSS Feed

Last modified on 9/5/2012 4:35 PM by User.

Tags:

Milestones to Trello

Intro

This script solves one way of pivoting your FogBugz data into Trello. Milestones become boards. Active statuses become lists. Cases become cards.

becomes

 

Files

There's a good deal more setup involved with this recipe since you're not only working with the FogBugz API, but also the Trello API. Once you get the appropriate Python modules installed, you should only need the following two files:

fbMilestonesToTrello.py

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
160:
161:
162:
163:
'''
fbMilestonesToTrello

This script will take FogBugz cases from various milestones and put them as 
cards in Trello. It's pivots the data for a fairly narrow use case, but you can
of course edit the script to pivot howerver you want. You could run the script 
every 15 minutes or so for a fresh look at your FogBugz cases.

Constraints:
 - This script creates a new board for each active milestone
 - This script creates a new list for each Active status
 - You need to have the same Active statuses for each category type. Eg. if you
   have an 'Active (1. Dev)' status for Bug, you need an 'Active (1. Dev)' 
   status for Feature, Inquiry, and Schedule Item as well.
 - This script only updates 1 way: FogBugz ==> Trello
   Trello changes will not be propogated back to FogBugz.
   
To run this script:
 1. Set up FogBugz statuses and milestones appropriately (see above) or change
    the script to do what you want.
 2. Grab the latest copy of TrelloSimple.py from https://developers.kilnhg.com/Code/Trello/Group/TrelloSimple/File/trelloSimple.py?rev=tip
    Put it in the same directory as this script.
 3. In your fbSettings file, add TRELLO_APP_KEY and TRELLO_AUTH_TOKEN. See https://developers.kilnhg.com/Code/Trello/Group/TrelloSimple/File/readme.md?rev=tip
    for how to get those values.
 4. Update IX_PROJECT below to the correct value for your project. To get the
    value, go to the settings page for your project and look at the ixProject
    value in the URL.
 5. Run the code like so:
 
    > python fbMilestonesToTrello.py

See https://developers.fogbugz.com/default.asp?W194 for more
information on using the FogBugz XML API with Python.
'''

from fogbugz import FogBugz
from trelloSimple import TrelloSimple
import fbSettings
import sys

IX_PROJECT = 1

def main():
  fb = FogBugz(fbSettings.URL, fbSettings.TOKEN)
  trello = TrelloSimple(fbSettings.TRELLO_APP_KEY, fbSettings.TRELLO_AUTH_TOKEN)
  statuses = getActiveBugStatuses(fb)
  milestones = getMilestonesFromProject(fb, IX_PROJECT)

  for milestone in milestones:
    board = getBoardOrCreate(trello, milestone['sFixFor'])
    lists = getListsOrCreate(trello, board['id'], statuses)
    cases = getCasesInMilestone(fb, milestone, IX_PROJECT)
    cardsActive = []
    for case in cases:
      card = getCardOrCreate(trello, case, board, lists)
      idList = [list for list in lists if list['name'] == case['sStatus']][0]['id']
      moveCardToCorrectList(trello, card, idList)
      cardsActive.append(card)
    allCards = trello.get(['boards',board['id'],'cards'])
    for cardCurrent in allCards:
      if cardCurrent['id'] not in [crd['id'] for crd in cardsActive]:
        trello.put(['cards',cardCurrent['id'],'closed'],{'value':'true'})
      
def getMilestonesFromProject(fogbugz, ixProject):
  #get Active milestones from FogBugz.
  resp = fogbugz.listFixFors(ixProject=ixProject)
  milestones = []
  for fixfor in resp.fixfors.findAll("fixfor"):
    #don't include global milestones
    if fixfor.ixproject.string is not None:
      milestones.append({'ixFixFor':int(fixfor.ixfixfor.string),
                        'sFixFor':fixfor.sfixfor.string.encode('UTF-8')})
  return milestones

def getBoardOrCreate(trello, milestoneName):
  board = findBoardByTitle(trello, milestoneName,substringMatch=False)
  if not board:
    board = trello.post('boards',{'name':milestoneName})
    #clear lists
    lists = trello.get(['boards',board['id'],'lists'])
    for list in lists:
      trello.put(['lists',list['id'],'closed'],{'value':'true'})
  return board
  
def getActiveBugStatuses(fogbugz):
  '''
  This assumes that you have the same active statuses for bug, feature, inquiry, etc.
  '''
  resp = fogbugz.listCategories()
  ixCatBug = -1
  for cat in resp.categories.findAll('category'):
    if cat.scategory.string.encode('UTF-8') == 'Bug':
      ixCatBug = int(cat.ixcategory.string)
      break
  statuses = []
  resp = fogbugz.listStatuses(ixCategory=ixCatBug)
  for status in resp.statuses.findAll('status'):
    if status.fdeleted.string == 'false' and status.fresolved.string == 'false':
      statuses.append({'ixStatus':int(status.ixstatus.string),
                       'sStatus':status.sstatus.string.encode('UTF-8'),
                       'iOrder':int(status.iorder.string)})
  statuses.sort(key=lambda status: status['iOrder'])
  return statuses
  
def getListsOrCreate(trello, boardId, statuses):
  for status in statuses:
    list = findListByTitle(trello, boardId, status['sStatus'], substringMatch=False)
    if not list:
      list = trello.post(['lists'],{'name':status['sStatus'],'idBoard':boardId})
    #put list at end
    trello.put(['lists',list['id'],'pos'],{'value':'bottom'})
  lists = trello.get(['boards',boardId,'lists'])
  return lists

  
def getCasesInMilestone(fogbugz, milestone, ixProject):
  resp = fogbugz.search(q='status:Active milestone:"%s" project:"=%s"' % (milestone['sFixFor'],ixProject),
                        cols='ixBug,sTitle,sStatus')
  cases = []
  for case in resp.cases.findAll('case'):
    cases.append({'ixBug':int(case.ixbug.string),
                  'sTitle':case.stitle.string.encode('UTF-8'),
                  'sStatus':case.sstatus.string.encode('UTF-8'),
                  'cardTitle': '%s (%s)' % (case.stitle.string.encode('UTF-8'),int(case.ixbug.string))})
  return cases
  
def getCardOrCreate(trello, case, board, lists):
  card = findCardByTitle(trello, board['id'],case['cardTitle'], substringMatch=False)
  if not card:
    caseURL = '%s/?%s' % (fbSettings.URL, case['ixBug'])
    card = createCard(trello, 
                        [list for list in lists if list['name'] == case['sStatus']][0]['id'], 
                        case['cardTitle'], 
                        desc=caseURL)
    trello.post(['cards',card['id'],'actions','comments'],{'text':caseURL})
  return card
  
def moveCardToCorrectList(trello, card, idList):
  trello.put(['cards',card['id'],'idList'],{'value':idList})

def findListByTitle(trello, boardId, substring, filter='open', substringMatch=True):
  resp = trello.get(['boards',boardId,'lists',filter])
  for list in resp:
    if (substringMatch and (substring.lower() in list['name'].lower())) or (substring.lower() == substring.lower() in list['name'].lower()):
      return list

def findBoardByTitle(trello, substring, filter='open', substringMatch=True):
  resp = trello.get(['members','me'],{'boards':filter})
  for board in resp['boards']:
    if (substringMatch and (substring.lower() in board['name'].lower())) or (substring.lower() == substring.lower() in board['name'].lower()):
      return board
      
def findCardByTitle(trello, boardId,substring, filter='visible', substringMatch=True):
  resp = trello.get(['boards',boardId,'cards'],{"filter":filter})
  for card in resp:
    if (substringMatch and (substring.lower() in card['name'].lower())) or (substring.lower() == substring.lower() in card['name'].lower()):
      return card
      
def createCard(trello, listId, name, desc='', pos='bottom', idCardSource='', keepFromSource=''):
  resp = trello.post('cards',{'idList':listId,'name':name,'desc':desc,'pos':pos,'idCardSource':idCardSource,'keepFromSource':keepFromSource})
  return resp

main()

trelloSimple.py

An updated copy of trelloSimple can be found at https://developers.kilnhg.com/Code/Trello/Group/TrelloSimple/File/trelloSimple.py?rev=tip. The version below was tested with the above script.

Note: This code requires the python requests module to be installed.

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
try:
  import simplejson as json
except ImportError:
  import json
import requests
from urllib import quote_plus

class TrelloSimple(object):  
  def __init__(self, apikey, token=None):
    self._apikey = apikey
    self._token = token
    self._apiversion = 1
    self._proxies = None
    
  def set_token(self, token):
    self._token = token
    
  def set_proxy(self, proxies):
    self._proxies = proxies
    
  def get_token_url(self, app_name, expires='30days', write_access=True):
    return 'https://trello.com/1/authorize?key=%s&name=%s&expiration=%s&response_type=token&scope=%s' % (self._apikey, quote_plus(app_name), expires, 'read,write' if write_access else 'read')

  def get(self, urlPieces, arguments = None):
    return self._http_action('get',urlPieces, arguments)
    
  def put(self, urlPieces, arguments = None, files=None):
    return self._http_action('put',urlPieces, arguments,files)
  
  def post(self, urlPieces, arguments = None, files=None):
    return self._http_action('post',urlPieces, arguments,files)
    
  def delete(self, urlPieces, arguments = None):
    return self._http_action('delete',urlPieces, arguments)
    
  def _http_action(self, method, urlPieces, arguments = None, files=None):
    #If the user wants to pass in a formatted string for urlPieces, just use
    #the string. Otherwise, assume we have a list of strings and join with /.
    if not isinstance(urlPieces, basestring):
      urlPieces = '/'.join(urlPieces)
    baseUrl = 'https://trello.com/%s/%s' % (self._apiversion, urlPieces) 
    
    params = {'key':self._apikey,'token':self._token}
    
    if method in ['get','delete'] and arguments:
      params = dict(params.items() + arguments.items())
    if method == 'get':
      resp = requests.get(baseUrl,params=params, proxies=self._proxies)
    elif method == 'delete':
      resp = requests.delete(baseUrl,params=params, proxies=self._proxies)
    elif method == 'put':
      resp = requests.put(baseUrl,params=params,data=arguments, proxies=self._proxies,files=files)
    elif method == 'post':
      resp = requests.post(baseUrl,params=params,data=arguments, proxies=self._proxies, files=files)
    
    resp.raise_for_status()
    return json.loads(resp.content)