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) |