class Command(BaseCommand): def __init__(self): super().__init__() self.trello_client = TrelloClient(settings.TRELLO_API_KEY, settings.TRELLO_API_TOKEN) def handle(self, *args, **options): """ Run command """ boards = TASK_CONFIG['boards'] cards = self.get_relevant_cards(boards) roles = TASK_CONFIG['roles'] users = User.objects.filter(role__in=roles) # send cards to its member based on role for user in users: member_id = self.trello_client.get_member(user.email).id member_cards = [ card for card in cards if member_id in card.member_ids ] if member_cards: self.send_data(member_cards, [user.email]) # send cards to the additional emails additional_emails = TASK_CONFIG.get('additional_emails') if additional_emails: self.send_data(cards, additional_emails) def send_data(self, cards, recipients): """ send relevant cards links to recipients """ email_template = Template(EMAIL_TEMPLATE) html_content = email_template.render(Context({'cards': cards})) send_mail(subject="Trello Manager - Feature Freeze", message="Cards left before feature freeze", from_email=settings.EMAIL_HOST_USER, recipient_list=recipients, html_message=html_content) def get_relevant_cards(self, boards): cards = [] for board_dict in boards: board_obj = self.trello_client.get_board(board_dict['id']) for list_dict in board_dict['lists']: list_obj = board_obj.get_list(list_dict['id']) cards += list_obj.list_cards() return cards
print "Finding Board" for b in client.list_boards(): if b.name == strboardname: print "Found Board - " + strboardname for l in b.all_lists(): if l.name == 'Information': continue f.write("<H2><U>" + l.name + "</U></H2>") #print l.name for c in l.list_cards(): print "Fetching Card Comments -", c.name c.fetch() members = '' for m in c.member_id: member = client.get_member(m) if members == '': members += member.full_name else: members += ", " members += member.full_name #print "\t", c.name, "(", members, ")" f.write("<H3>" + c.name + "(" + members + ")</H3>") f.write("<UL>") for comment in c.comments: #print "\t\t", c.comments commentdatetime = datetime.datetime.strptime(comment['date'], '%Y-%m-%dT%H:%M:%S.%fZ') commentdate = commentdatetime.strftime('%m/%d/%Y %H:%M') if lastdate <= commentdatetime.date() <= today: #print commentdate, comment['data']['text']
load_dotenv(dotenv_path) API_KEY = AP= os.environ.get("API_KEY") API_TOKEN = AP= os.environ.get("API_TOKEN") BOARD_ID = AP= os.environ.get("BOARD_ID") today = datetime.now() es = Elasticsearch("localhost:9200") client = TrelloClient(API_KEY, token=API_TOKEN) board = client.get_board(BOARD_ID) members = board.all_members() for m in members: member = client.get_member(m.id) d = datetime(2017, 4, 1, 0, 0) while True: since = d before = since + relativedelta(months=1) actions = client.fetch_json( '/members/' + member.id + '/actions', query_params={'limit': 1000, "since": since.strftime("%Y-%m-%d"), "before": before.strftime("%Y-%m-%d")}) for a in actions: utc_date = parse(a["date"]) jst_date = utc_date.astimezone(timezone('Asia/Tokyo')) a["date"] = jst_date.strftime("%Y-%m-%dT%H:%M:%S%z") a["hour"] = jst_date.hour a["weekday"] = jst_date.weekday() if "text" in a["data"]: a["text_length"] = len(a["data"]["text"])
csvfile = StringIO() writer = csv.DictWriter(csvfile, fieldnames=reader.fieldnames) writer.writeheader() hacknight_date = last_hacknight(datetime.datetime.now(pytz.utc)) for row in reader: # If entries for this date already exist, ignore them and rewrite. if row['date'] == hacknight_date.strftime('%Y-%m-%d'): continue writer.writerow(row) for card in cards: assigned_members = [client.get_member(member_id) for member_id in card.member_ids] data = { 'date': hacknight_date.strftime('%Y-%m-%d'), 'project': card.name, 'person': '', 'trello_card_id': card.id, } if assigned_members: data.update({'person': assigned_members[0].username}) writer.writerow(data) if DEBUG: with open('projects.csv', 'w') as f: f.write(csvfile.getvalue())
def main(): usage = '%prog (load|rebuild|commit|push|test|config|update)'\ ' [options] args... are you ready?' parser = OptionParser(usage) parser.add_option('-i', '--id', type='int', dest='id') parser.add_option('-u', '--user', dest='user') parser.add_option('-c', '--comment', dest='comment') parser.add_option('-g', '--gitpath', default=realpath(__file__), dest='gitpath') parser.add_option('-p', '--params', dest='params') parser.add_option('-t', '--title', dest='title') parser.add_option('-d', '--description', dest='description', default="") (options, args) = parser.parse_args() repo = Repo(options.gitpath) conf_path = join(expanduser("~"), '.gitr.json') if not len(args): handle_error(msg='Some action are required', handle=parser) if not isfile(conf_path): conf_src = join(dirname(realpath(__file__)), 'gitr.json') shutil.copy(conf_src, conf_path) with open(conf_path, 'r') as infile: json_data = json.load(infile) BOARD_ID = json_data['BOARD_ID'] CREATE_LIST_ID = json_data['CREATE_LIST_ID'] DONE_LIST_ID = json_data['DONE_LIST_ID'] MEMBERS = json_data['MEMBERS'] USER = MEMBERS[int(json_data['USER'])] client = TrelloClient(api_key=json_data['API_KEY'], token=json_data['TOKEN'], api_secret=json_data['API_SECRET']) if ('update' in args): create_list = client.get_list(CREATE_LIST_ID) card = get_current_card(repo, create_list) if options.description: card._set_remote_attribute('description', options.description) if options.title or options.params: def sub_match(match): match_args = [arg for arg in match.groups()] if options.title: match_args[4] = options.title if options.params: params = check_params(options.params) if (len(params) > 2): match_args[2] = params[2] elif (len(params) == 2): match_args[1] = params[1] match_args[3] = params[0] return '#%s. %sº (%s,%s) %s' % tuple(match_args) name = re.sub('#(\d+).(\d+)º \((\d+),(\d+)\) (.*)', sub_match, card.name) card._set_remote_attribute('name', name) elif ('members' in args): for idx, member_id in enumerate(MEMBERS): member = client.get_member(member_id) logging.info("*%s %d. %s (%s)" % ((USER == member_id) and '*' or '', idx, member.username, member.full_name)) elif ('config' in args): if options.params: params = options.params.split(',') for param in params: try: key, value = param.split(':') except ValueError: handle_error('(-p <key:value, key2:value2, ...>)'\ ' format is wrong.') json_data[key] = value if options.user: json_data['USER'] = options.user if not (options.params and options.user): members = client.get_board_members(BOARD_ID) json_data['MEMBERS'] = [member['id'] for member in members] json_data['USER'] = json_data['MEMBERS'].index(client.me()['id']) with open(conf_path, 'w') as outfile: json.dump(json_data, outfile) for key, value in json_data.items(): logging.info("* %s: %s" %(key, value)) elif ('load' in args): if not options.params: handle_error('(-p <t_estimated>)You must include '\ 'almost time estimated.') params = check_params(options.params) (t_estimated, priority, t_real) = (params + [1, 0])[:3] if options.id: short_id = options.id create_list = client.get_list(CREATE_LIST_ID) card = get_card_by_id(create_list, short_id) if not card: handle_error('Card not found') full_title = '%dº (%d,%d) %s' % (priority, t_estimated, t_real, card.name) card._set_remote_attribute('name', "#%d.%s" % (short_id, full_title)) checkout_by_id(repo, short_id) else: if not options.title: handle_error('(-t <title>) to load is required') full_title = '%dº (%d,%d) %s' % (priority, t_estimated, t_real, options.title) check_untracked(repo) create_list = client.get_list(CREATE_LIST_ID) card = create_list.add_card(full_title, options.description) if options.user: users = check_params(options.user) for member_pos in users: member_id = MEMBERS[member_pos] if (member_id <> USER): card.assign(member_id) card.assign(USER) card.fetch() short_id = int(card.short_id) card._set_remote_attribute('name', "#%d.%s" % (short_id, card.name)) checkout_by_id(repo, short_id) elif ('commit' in args): create_list = client.get_list(CREATE_LIST_ID) card = get_current_card(repo, create_list) if not options.comment: handle_error('(-c <comment>) to commit is required') card.comment(options.comment) git_add_commit(repo, options.comment) elif ('push' in args): create_list = client.get_list(CREATE_LIST_ID) card = get_current_card(repo, create_list) message = "CLOSED AT %s.\n%s" % (time.strftime('%Y-%m-%d %H:%M'), options.comment or "") git_add_commit(repo, message) card.comment(message) card.change_list(DONE_LIST_ID) try: repo.remotes.origin.pull() except CalledProcessError: pass active_branch = repo.active_branch.name repo.remotes.origin.push() repo.heads.devel.checkout() check_call(('git', 'merge', active_branch)) elif ('rebuild' in args): done_list = client.get_list(DONE_LIST_ID) card = get_current_card(repo, done_list) else: create_list = client.get_list(CREATE_LIST_ID) card = get_current_card(repo, create_list) message = "TESTED AT %s.\n%s" % (time.strftime('%Y-%m-%d %H:%M'), options.comment or "") git_add_commit(repo, message) repo.heads.devel.checkout() repo.remotes.origin.pull() checkout_by_id(repo, card.short_id, 'I') try: check_call(('git', 'merge', 'F-#%d' % card.short_id)) except CalledProcessError as error: handle_error('Conflict merge.') else: try: git_add_commit(repo, 'merge devel - I-#%d' % card.short_id) except CalledProcessError as error: handle_error(error)
class TrelloBoardTestCase(unittest.TestCase): """ Tests for TrelloClient API. Note these test are in order to preserve dependencies, as an API integration cannot be tested independently. """ def setUp(self): self._trello = TrelloClient(os.environ['TRELLO_API_KEY'], token=os.environ['TRELLO_TOKEN']) for b in self._trello.list_boards(): if b.name == os.environ['TRELLO_TEST_BOARD_NAME']: self._board = b break try: self._list = self._board.open_lists()[0] except IndexError: self._list = self._board.add_list('List') def _add_card(self, name, description=None): try: card = self._list.add_card(name, description) self.assertIsNotNone(card, msg="card is None") self.assertIsNotNone(card.id, msg="id not provided") self.assertEquals(card.name, name) return card except Exception as e: print(str(e)) self.fail("Caught Exception adding card") def test40_add_card(self): name = "Testing from Python - no desc" card = self._add_card(name) self.assertIsNotNone(card.closed, msg="closed not provided") self.assertIsNotNone(card.url, msg="url not provided") card2 = self._trello.get_card(card.id) self.assertEqual(card.name, card2.name) def test41_add_card(self): name = "Testing from Python" description = "Description goes here" card = self._add_card(name, description) self.assertEquals(card.description, description) self.assertIsNotNone(card.closed, msg="closed not provided") self.assertIsNotNone(card.url, msg="url not provided") card.fetch() self.assertIsNotNone(card.member_id) self.assertIsNotNone(card.short_id) self.assertIsNotNone(card.list_id) self.assertIsNotNone(card.comments) self.assertIsNotNone(card.checklists) self.assertIsInstance(card.create_date, datetime) def test42_add_card_with_comments(self): name = "Card with comments" comment = "Hello World!" card = self._add_card(name) card.comment(comment) card.fetch(True) self.assertEquals(card.description, '') self.assertIsNotNone(card.closed, msg="closed not provided") self.assertIsNotNone(card.url, msg="url not provided") self.assertEquals(len(card.comments), 1) self.assertEquals(card.comments[0]['data']['text'], comment) def test43_delete_checklist(self): name = "Card with comments" card = self._list.add_card(name) card.fetch(True) name = 'Checklists' checklist = card.add_checklist(name, ['item1', 'item2']) self.assertIsNotNone(checklist, msg="checklist is None") self.assertIsNotNone(checklist.id, msg="id not provided") self.assertEquals(checklist.name, name) checklist.delete() card.delete() def test44_attach_url_to_card(self): name = "Testing from Python - url" card = self._add_card(name) card.attach(name='lwn', url='http://lwn.net/') card.fetch() self.assertEquals(card.badges['attachments'], 1) card.delete() def test52_get_cards(self): cards = self._board.get_cards() self.assertEquals(len(cards), 4) for card in cards: if card.name == 'Testing from Python': self.assertEqual(card.description, 'Description goes here') elif card.name == 'Testing from Python - no desc': self.assertEqual(card.description, '') elif card.name == 'Card with comments': self.assertEqual(card.description, '') else: self.fail(msg='Unexpected card found') self.assertIsInstance(self._board.all_cards(), list) self.assertIsInstance(self._board.open_cards(), list) self.assertIsInstance(self._board.closed_cards(), list) def test52_add_card_set_due(self): name = "Testing from Python" description = "Description goes here" card = self._list.add_card(name, description) # Set the due date to be 3 days from now today = datetime.today() day_detla = timedelta(3) due_date = today + day_detla card.set_due(due_date) expected_due_date = card.due # Refresh the due date from cloud card.fetch() actual_due_date = card.due[:10] self.assertEquals(expected_due_date, actual_due_date) def test53_checklist(self): name = "Testing from Python" description = "Description goes here" card = self._list.add_card(name, description) name = 'Checklists' checklist = card.add_checklist(name, ['item1', 'item2']) self.assertIsNotNone(checklist, msg="checklist is None") self.assertIsNotNone(checklist.id, msg="id not provided") self.assertEquals(checklist.name, name) checklist.rename('Renamed') self.assertEquals(checklist.name, 'Renamed') def test54_set(self): name = "Testing from Python" description = "Description goes here" card = self._list.add_card('noname') card.set_name(name) card.set_description(description) self.assertEquals(card.name, name) self.assertEquals(card.description, description) def test60_delete_cards(self): cards = self._board.get_cards() for card in cards: card.delete() def test70_all_members(self): self.assertTrue(len(self._board.all_members()) > 0) def test71_normal_members(self): self.assertTrue(len(self._board.normal_members()) >= 0) def test72_admin_members(self): self.assertTrue(len(self._board.admin_members()) > 0) def test73_owner_members(self): members = self._board.owner_members() self.assertTrue(len(members) > 0) member = members[0].fetch() self.assertNotEqual(member.status, None) self.assertNotEqual(member.id, None) self.assertNotEqual(member.bio, None) self.assertNotEqual(member.url, None) self.assertNotEqual(member.username, None) self.assertNotEqual(member.full_name, None) self.assertNotEqual(member.initials, None) member2 = self._trello.get_member(member.id) self.assertEqual(member.username, member2.username) def test80_unauthorized(self): client = TrelloClient('a') self.assertRaises(Unauthorized, client.list_boards) def test81_resource_unavailable(self): self.assertRaises(ResourceUnavailable, self._trello.get_card, '0') def test90_get_board(self): board = self._trello.get_board(self._board.id) self.assertEqual(self._board.name, board.name)
class Trellist(object): """The main object - initialize and use run() :param apikey: Your api-key. :param apisecret: Your api secret token. Get it from a protected file that you don't commit. Keep it secret! :param boardname: Name of the board to work on. :param done_list_name: The name of the done list on your board. Only one. :param releases_list_name: The name of the list to put release cards on. :param create_comments: True by default. Create a comment on the release card for each done card :param create_release_if_zero_done: If nothing is done, should you make a sad empty release card? """ def __init__( self, apikey, apisecret, boardname, done_list_name="done", releases_list_name="releases", create_release_if_zero_done=False, create_comments=True, ): self.client = TrelloClient(api_key=apikey, api_secret=apisecret) self.board = self.get_board(boardname) self.done = self.get_list_by_name(done_list_name) self.releases = self.get_list_by_name(releases_list_name) self.release_template = "{date} release: {count} done" self.card_summary_template = "- {card.name} {card.members_initials}" self.create_release_if_zero_done = create_release_if_zero_done self.create_comment_per_item = create_comments def run(self): """Runs through all the methods to perform the work""" logger.info(f"get all cards in the done board: {self.done.name}") cards = self.get_done_cards() logger.info(f"got {len(cards)} cards") if cards or self.create_release_if_zero_done: release_card = self.create_release_card(cards, self.release_template, self.card_summary_template) for card in cards: if self.create_comment_per_item: self.add_comment_to_release(release_card, card) card.set_closed(True) logger.info("finished run") def get_board(self, board_name): """Gets the open board object by a name, otherwise raises an Error to let you know we don't have that board :param board_name: actual name of a board you have access to """ board = self.first(self.client.list_boards(), lambda b: b.name == board_name and not b.closed) if board: return board raise ValueError( ("Couldn't find an open board named '{}'. Check the name in your" " trello - are there extra quote marks in this message?" ).format(board_name)) def get_list_by_name(self, name): """Iterate lists and get the first one matching the name passed in :param name: Name of a list on the board you've passed in """ trello_list = self.first(self.board.list_lists(), lambda l: l.name == name) if trello_list: return trello_list raise ValueError( ("Couldn't find a list named '{}'. Check the name in your trello" " - are there extra quote marks in this message?").format(name)) def get_card_members(self, card): """Return an array of Trello.Member objects of this card :param card: """ return [self.get_member(member_id) for member_id in card.member_id] @lru_cache() def get_member(self, member_id): """Get a member. We only use our own function so that we can cache calls :param member_id: """ return self.client.get_member(member_id) def first(self, iterable, condition): """Iterates an iterable and returns the first item that meets a condition or None :param iterable: An iterable to fetch the first itemm that meets condition :param condition: The condition to evaluate per item """ return next((i for i in iterable if condition(i)), None) def get_done_cards(self): """Get every card from the done list""" return self.done.list_cards() def prep_card(self, card): """Take the card and add arrays of all member initials, full_names and usernames for use in templates :param card: a card to modify with extra information """ card.members = self.get_card_members(card) card.members_initials = "" card.members_full_names = "" card.members_usernames = "" if len(card.members): card.members_initials = [ member.initials for member in card.members ] card.members_full_names = [ member.full_name for member in card.members ] card.members_usernames = [ member.username for member in card.members ] return card def summarize_these(self, cards, template, prep_function): """Run a list of cards through a template and return those joined by newlines The template can reference any part of a Trello.card as well as 3 handy arrays - members_initials - members_full_names - members_usernames These will either have an empty string or an array of strings that show up nicely in string.format :param cards: Card objects to summarize :param template: A template for each card. We pass the full card to format :param prep_function: A function that will add make the card more friendly to format """ summary = "\n".join([ self.summarize_this(card, template, prep_function) for card in cards ]) return summary def summarize_this(self, card, template, prep_function): """Summarize a card by passing it to a template after prepping it with values from a prep_function :param card: a Card to summarize :param template: a string template. We'll call format and pass in a prepped Card :param prep_function: use this to add extra information to the card """ card = prep_function(self, card) summary = template.format(card=card) return summary def create_release_card(self, cards, release_template, card_summary_template): """Returns a new release card, with a title from template and description based on a summary of the cards :param cards: Cards in this release :param release_template: A format string for release card name that we pass in date and length of cards :param card_summary_template: A string we format with per each card """ release_card_name = release_template.format(date=date.today(), count=len(cards)) # turn list of names of cards into a summary summary = self.summarize_these(cards, template=card_summary_template, prep_function=self.prep_card) logger.info(f"{summary}") release_card = self.releases.add_card(release_card_name, summary) return release_card def add_comment_to_release( self, release_card, card, comment_format="{card.name}\n{card.url}\n{card.description}", ): """add_comment_to_release :param release_card: A card to comment on :param card: A card to summarize in a comment :param comment_format: The template, passed in the card. """ comment_text = comment_format.format(card=card) release_card.comment(comment_text)
class Trello: # cache shared across all instances of this class list_cache = {} def __init__(self, member, api_key, api_secret, token): self.trello = TrelloClient(api_key=api_key, api_secret=api_secret, token=token) self.member = member self.cards = [] print(f"Fetching cards for {self.member}") self.get_member_cards() def get_member_cards(self): member = self.trello.get_member(self.member) self.cards = member.fetch_cards() self.preprocess_cards() def get_list_name(self, list_id): """ cache trello List objects """ if list_id not in self.list_cache: list_data = self.trello.get_list(list_id) # ensure we stay under 100 hits per 10 sec period limit time.sleep(0.15) self.list_cache[list_id] = list_data return self.list_cache[list_id].name def date_str_to_datetime_obj(self, date_str): # trim off miscroseconds and Z char, and set to UTC trimmed = date_str[0:-5] + "+0000" return datetime.datetime.strptime(trimmed, "%Y-%m-%dT%H:%M:%S%z").astimezone() def preprocess_cards(self): for c in self.cards: # store actual list name c['listName'] = self.get_list_name(c['idList']) # localize dates c['dateLastActivity'] = self.date_str_to_datetime_obj( c['dateLastActivity']) if c['due']: c['due'] = self.date_str_to_datetime_obj(c['due']) def get_done(self): """ return the cards that are considered 'Done' """ done = [c for c in self.cards if c['dueComplete']] done = sorted(done, key=lambda c: c['dateLastActivity']) return done def get_to_complete(self): """ return the cards that are considered 'To Be Completed'. that means if it has a due date and it's not marked completed yet. """ to_complete = [ c for c in self.cards if not c['dueComplete'] and c['due'] is not None ] to_complete = sorted(to_complete, key=lambda c: c['due']) return to_complete def get_missing_due_date(self): # cards should never be missing a due date missing_due_date = [ c for c in self.cards if not c['dueComplete'] and c['due'] is None ] missing_due_date = sorted(missing_due_date, key=lambda c: c['dateLastActivity']) return missing_due_date def output_to_file(self): """ for debugging """ output = """ <html> <head> <style> h1 { } table th { text-align: left; } table td { text-align: left; } </style> </head> <body> """ output += "<h1>Done</h1>\n" output += "<table><thead><th>Est. Completion Date</th><th>Name</th></thead>\n" for card in self.get_done(): output += "<tr><td>%s</td><td><a href='%s'>%s</a></td></tr>\n" % ( card['dateLastActivity'].strftime("%c"), card['shortUrl'], card['name']) output += "</table>" output += "<h1>To Be Completed</h1>\n" output += "<table><thead><th>Due Date</th><th>Name</th></thead>\n" for card in self.get_to_complete(): output += "<tr><td>%s</td><td><a href='%s'>%s</a></td></tr>\n" % ( card['due'].strftime("%c"), card['shortUrl'], card['name']) output += "</table>" output += "<h1>Missing Due Date</h1>" output += "<table><thead><th>Last Updated</th><th>Name</th></thead>\n" for card in self.get_missing_due_date(): output += "<tr><td>%s</td><td><a href='%s'>%s</a></td></tr>\n" % ( card['dateLastActivity'].strftime("%c"), card['shortUrl'], card['name']) output += """ </body> </html> """ f = open("allcards.html", "w") f.write(output) f.close()
class Command(BaseCommand): def __init__(self): super().__init__() self.trello_client = TrelloClient(settings.TRELLO_API_KEY, settings.TRELLO_API_TOKEN) self.github_client = Github( settings.GITHUB_TOKEN).get_organization('bluevine-dev') repos = self.github_client.get_repos() unmerged_pull_requests = [] for repo in repos: unmerged_pull_requests += [ pr for pr in repo.get_pulls() if not pr.merged_at ] self.unmerged_pull_requests = { pr.html_url: pr for pr in unmerged_pull_requests } def add_arguments(self, parser): # pylint: disable=no-self-use """ Add extra arguments """ super().add_arguments(parser) parser.add_argument('--csv', action='store_true', default=False, help='attach a csv to the generated email') parser.add_argument('--user', action='store', help='run command for specified user') def handle(self, *args, **options): """ Run command """ username = options.get('user') if username: users = User.objects.filter(username=username) else: users = User.objects.filter(role='TL') # generate unmerged pull request email to all desired users for user in users: member_id = self.trello_client.get_member(user.email).id data = self._get_unmerged_pull_requests(member_id) attachment = None if data and options['csv']: attachment = self.create_pull_requests_csv(data, member_id) logger.info(f'attachment {attachment} was created') self.send_unmerged_pull_requests_data( data=data, recipients=['*****@*****.**'], attachment=attachment) def _get_unmerged_pull_requests(self, member_id): """ Returns all unmerged pull requests related to cards """ boards = self.trello_client.list_boards(board_filter='open') pull_requests_per_board = [] for board in boards: cards = board.get_cards(card_filter='open') # get only member cards cards = [card for card in cards if member_id in card.member_id] pull_requests_per_card = [] for card in cards: attachments = card.get_attachments() pr_attachments = [ attachment for attachment in attachments if all(s in attachment.url for s in ['github.com', 'pull']) ] # check for unmerged pull request in card unmerged_pull_requests = [] for pr_attachment in pr_attachments: if pr_attachment.url in self.unmerged_pull_requests: pull_request_data = { 'name': pr_attachment.name, "url": pr_attachment.url, } unmerged_pull_requests.append(pull_request_data) if unmerged_pull_requests: card_data = { 'name': card.name, 'url': card.url, 'pull_requests': unmerged_pull_requests } pull_requests_per_card.append(card_data) if pull_requests_per_card: board_data = { 'name': board.name, 'cards': pull_requests_per_card } pull_requests_per_board.append(board_data) return pull_requests_per_board @staticmethod def send_unmerged_pull_requests_data(data, recipients, attachment=None): """ Sends an email according to given data """ html_content = Command.create_email_template(data) email_message = EmailMessage( subject='Trello Manager - Unmerged Pull Requests', body=html_content, from_email=settings.EMAIL_HOST_USER, to=recipients) email_message.content_subtype = 'html' if attachment: email_message.attach_file(attachment) email_message.send() logger.info(f'Email was sent to {recipients}') @staticmethod def create_pull_requests_csv(data, member_id): """ create a temporary csv file """ import os, tempfile, csv from datetime import datetime now = datetime.now().strftime("%m_%d_%Y__%H%M%S") file_path = os.path.join( tempfile.gettempdir(), f'unmerged_pull_request_{member_id}_{now}.csv') with open(file_path, 'w') as csv_file: headers = [ 'board name', 'card name', 'card url', 'pull request name', 'pull request url' ] writer = csv.writer(csv_file) writer.writerow(headers) for board in data: cards = board['cards'] for card in cards: pull_requests = card['pull_requests'] for pull_request in pull_requests: writer.writerow([ board['name'], card['name'], card['url'], pull_request['name'], pull_request['url'] ]) return file_path @staticmethod def fetch_data_from_pull_request_url(pull_request_url): """ Parses and returns pull request data from a pull request url """ split_url = pull_request_url.split("/") owner = split_url[3] repo = split_url[4] number = int(split_url[-1]) return owner, repo, number def _fetch_cards_by_member(self, member_id): """ Fetches all the cards for this member """ cards = self.trello_client.fetch_json('/members/' + member_id + '/cards', query_params={ 'filter': 'visible', 'fields': 'name,idBoard,url', 'attachments': 'true' }) return sorted(cards, key=lambda card: card['idBoard']) def _get_board_name_by_id(self, board_id): """ Returns the name of the board """ if board_id not in self.boards_names: self.boards_names[board_id] = self.trello_client.get_board( board_id).name return self.boards_names[board_id] def _get_pull_request_by_url(self, pull_request_url): owner, repo, number = self.fetch_data_from_pull_request_url( pull_request_url) return self.github_client.get_repo(repo).get_pull(int(number)) @staticmethod def create_email_template(data): email_template = Template(EMAIL_TEMPLATE) return email_template.render(Context({'boards': data}))
class TrelloClient: def __init__(self, api_key, api_secret, token, token_secret): self.trello_client = Client(api_key=api_key, api_secret=api_secret, token=token, token_secret=token_secret) self._uid = None self._project = None self._board = None self._lists = None self._board_labels = None self._lists_filter = None self._only_my_cards = False @property def whoami(self): """ Get my Trello UID :return: my Trello UID :rtype: string """ if self._uid is None: self._uid = self.trello_client.get_member('me').id return self._uid def project(self, project): """ Set the class working project :param project: TelloWarrior project object """ if self._project == None or self._project.name != project.name: self._board = self.get_board(project.trello_board_name) self._lists = self.get_lists() self._board_labels = self.get_board_labels() self._lists_filter = project.trello_lists_filter self._only_my_cards = project.only_my_cards def get_board(self, board_name): """ Get a open Trello board from name, if it does not exist create it :param board_name: the board name :return: a Tello board :rtype: Trello board object """ for trello_board in self.trello_client.list_boards( board_filter='open'): if trello_board.name == board_name and not trello_board.closed: logger.debug('Trello board {} found'.format(board_name)) return trello_board logger.debug('Creating Trello board {}'.format(board_name)) return self.trello_client.add_board(board_name) def get_lists(self): """ Get the open lists of a Trello board :return: a list of Trello list objects :rtype: list """ return self._board.open_lists() def get_list(self, list_name): """ Get a Trello list from list name, if it does not exist create it :param list_name: the list name :return: a Tello list :rtype: Trello list object """ if self._lists == None: raise ClientError('get_list') for trello_list in self._lists: if trello_list.name == list_name: logger.debug('Trello list {} found'.format(list_name)) return trello_list logger.debug('Creating Trello list {}'.format(list_name)) trello_list = self._board.add_list(list_name) self._lists.append(trello_list) # Update _lists with new list return trello_list def get_board_labels(self): """ Get the labels of a Trello board :param board_name: the board name :return: a list of Trello label objects :rtype: list """ return self._board.get_labels() def get_board_label(self, label_name): """ Get a Trello board label from label name, if it does not exist create it :param label_name: the label name :return: a Tello label :rtype: Trello label object """ if self._board_labels == None: raise ClientError('get_board_label') for board_label in self._board_labels: if board_label.name == label_name: logger.debug('Trello board label {} found'.format(label_name)) return board_label logger.debug('Creating Trello board label {}'.format(label_name)) board_label = self._board.add_label(label_name, 'black') self._board_labels.append( board_label) # Update _board_labels with new label return board_label def get_cards_dict(self): """ Get all cards of a list of Trello lists in a dictionary :return: a dict with Cards :rtype: dict """ trello_cards_dict = {} if self._lists_filter is not None: trello_lists = filter( lambda trello_list: trello_list.name not in self._lists_filter, self._lists) for trello_list in trello_lists: logger.debug('Getting Trello cards of list {}'.format( trello_list.name)) trello_cards_dict[trello_list.name] = trello_list.list_cards() if self._only_my_cards: trello_cards_dict[trello_list.name] = filter( lambda trello_card: self.whoami in trello_card.member_ids, trello_cards_dict[trello_list.name]) return trello_cards_dict def delete_trello_card(self, trello_card_id): """ Delete (forever) a Trello card by ID :param trello_card_id: ID of Trello card """ try: self.trello_client.get_card(trello_card_id).delete() except ResourceUnavailable: logger.warning( 'Cannot find Trello card with ID {} deleted in Task Warrior. Maybe you also deleted it in Trello?' .format(trello_card_id))
class TrelloBoardTestCase(unittest.TestCase): """ Tests for TrelloClient API. Note these test are in order to preserve dependencies, as an API integration cannot be tested independently. """ def setUp(self): self._trello = TrelloClient(os.environ['TRELLO_API_KEY'], token=os.environ['TRELLO_TOKEN']) for b in self._trello.list_boards(): if b.name == os.environ['TRELLO_TEST_BOARD_NAME']: self._board = b break try: self._list = self._board.open_lists()[0] except IndexError: self._list = self._board.add_list('List') def _add_card(self, name, description=None): try: card = self._list.add_card(name, description) self.assertIsNotNone(card, msg="card is None") self.assertIsNotNone(card.id, msg="id not provided") self.assertEquals(card.name, name) return card except Exception as e: print(str(e)) self.fail("Caught Exception adding card") def test40_add_card(self): name = "Testing from Python - no desc" card = self._add_card(name) self.assertIsNotNone(card.closed, msg="closed not provided") self.assertIsNotNone(card.url, msg="url not provided") card2 = self._trello.get_card(card.id) self.assertEqual(card.name, card2.name) def test41_add_card(self): name = "Testing from Python" description = "Description goes here" card = self._add_card(name, description) self.assertEquals(card.description, description) self.assertIsNotNone(card.closed, msg="closed not provided") self.assertIsNotNone(card.url, msg="url not provided") card.fetch() self.assertIsNotNone(card.member_id) self.assertIsNotNone(card.short_id) self.assertIsNotNone(card.list_id) self.assertIsNotNone(card.comments) self.assertIsNotNone(card.checklists) self.assertIsInstance(card.create_date, datetime) def test42_add_card_with_comments(self): name = "Card with comments" comment = "Hello World!" card = self._add_card(name) card.comment(comment) card.fetch(True) self.assertEquals(card.description, '') self.assertIsNotNone(card.closed, msg="closed not provided") self.assertIsNotNone(card.url, msg="url not provided") self.assertEquals(len(card.comments), 1) self.assertEquals(card.comments[0]['data']['text'], comment) def test43_delete_checklist(self): name = "Card with comments" card = self._list.add_card(name) card.fetch(True) name = 'Checklists' checklist = card.add_checklist(name, ['item1', 'item2']) self.assertIsNotNone(checklist, msg="checklist is None") self.assertIsNotNone(checklist.id, msg="id not provided") self.assertEquals(checklist.name, name) checklist.delete() card.delete() def test44_attach_url_to_card(self): name = "Testing from Python - url" card = self._add_card(name) card.attach(name='lwn', url='http://lwn.net/') card.fetch() self.assertEquals(card.badges['attachments'], 1) card.delete() def test52_get_cards(self): cards = self._board.get_cards() self.assertEquals(len(cards), 4) for card in cards: if card.name == 'Testing from Python': self.assertEqual(card.description, 'Description goes here') elif card.name == 'Testing from Python - no desc': self.assertEqual(card.description, '') elif card.name == 'Card with comments': self.assertEqual(card.description, '') else: self.fail(msg='Unexpected card found') self.assertIsInstance(self._board.all_cards(), list) self.assertIsInstance(self._board.open_cards(), list) self.assertIsInstance(self._board.closed_cards(), list) def test52_add_card_set_due(self): name = "Testing from Python" description = "Description goes here" card = self._list.add_card(name, description) # Set the due date to be 3 days from now today = datetime.today() day_detla = timedelta(3) due_date = today + day_detla card.set_due(due_date) expected_due_date = card.due # Refresh the due date from cloud card.fetch() actual_due_date = card.due[:10] self.assertEquals(expected_due_date, actual_due_date) # Note that set_due passes only the date, stripping time self.assertEquals(card.due_date.date(), due_date.date()) def test53_checklist(self): name = "Testing from Python" description = "Description goes here" card = self._list.add_card(name, description) name = 'Checklists' checklist = card.add_checklist(name, ['item1', 'item2']) self.assertIsNotNone(checklist, msg="checklist is None") self.assertIsNotNone(checklist.id, msg="id not provided") self.assertEquals(checklist.name, name) checklist.rename('Renamed') self.assertEquals(checklist.name, 'Renamed') def test54_set(self): name = "Testing from Python" description = "Description goes here" card = self._list.add_card('noname') card.set_name(name) card.set_description(description) self.assertEquals(card.name, name) self.assertEquals(card.description, description) def test55_set_pos(self): card_names = lambda: [c.name for c in self._list.list_cards()] self._list.add_card('card1') card2 = self._list.add_card('card2') names = card_names() self.assertGreater(names.index('card2'), names.index('card1')) card2.set_pos('top') names = card_names() self.assertGreater(names.index('card1'), names.index('card2')) card2.set_pos('bottom') names = card_names() self.assertGreater(names.index('card2'), names.index('card1')) def test60_delete_cards(self): cards = self._board.get_cards() for card in cards: card.delete() def test70_all_members(self): self.assertTrue(len(self._board.all_members()) > 0) def test71_normal_members(self): self.assertTrue(len(self._board.normal_members()) >= 0) def test72_admin_members(self): self.assertTrue(len(self._board.admin_members()) > 0) def test73_owner_members(self): members = self._board.owner_members() self.assertTrue(len(members) > 0) member = members[0].fetch() self.assertNotEqual(member.status, None) self.assertNotEqual(member.id, None) self.assertNotEqual(member.bio, None) self.assertNotEqual(member.url, None) self.assertNotEqual(member.username, None) self.assertNotEqual(member.full_name, None) self.assertNotEqual(member.initials, None) member2 = self._trello.get_member(member.id) self.assertEqual(member.username, member2.username) def test80_unauthorized(self): client = TrelloClient('a') self.assertRaises(Unauthorized, client.list_boards) def test81_resource_unavailable(self): self.assertRaises(ResourceUnavailable, self._trello.get_card, '0') def test90_get_board(self): board = self._trello.get_board(self._board.id) self.assertEqual(self._board.name, board.name)
fields = [] for p in plugin_data: if p.get('idPlugin') == FREE_FIELD_PLUGIN_ID: value = json.loads(p['value']) fields = [STORIES[i] for i, v in value['fields'].items() if v] return fields progress_cpt = {} cards_by_story = {} dci_board = client.get_board('0eMozAiR') for l in dci_board.all_lists(): if l.name in ACTIVE_lISTS.keys(): trello_list = dci_board.get_list(l.id) for c in trello_list.list_cards(): c.list = l c.members = [client.get_member(i) for i in c.member_ids] c.status = ACTIVE_lISTS[l.name] c.fetch(eager=True) c.checklist_items = [] try: c.checklist_items = c._checklists[0].items except IndexError: pass stories = get_stories(client, c) if not stories: stories += ['other'] stories += ['all'] for story in stories: if story not in cards_by_story: cards_by_story[story] = [] progress_cpt[story] = Cpt()