Table of Contents
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) |


Title Index
Recently Changed
Page Hierarchy
Incomplete
Tags
