# HG changeset patch # User Peter Sanchez # Date 1417145285 28800 # Thu Nov 27 19:28:05 2014 -0800 # Node ID d9aff87d5568ffe470e8df3c6281786541704448 # Parent 5beadd6a070de42895b6ec5cfd35d13bd272a5e1 Added a ton.. haven't even tested. Needs work for sure. diff --git a/trello_broker/api.py b/trello_broker/api.py --- a/trello_broker/api.py +++ b/trello_broker/api.py @@ -0,0 +1,55 @@ +import trello + + +def get_client(trello_token): + return trello.TrelloApi(trello_token.api_key, token=trello_token.api_token) + + +def get_all_trello_boards(trello_token, client=None): + ''' Get JSON data for all Trello boards the given + token has access to. + ''' + if client is None: + client = get_client(trello_token) + user_data = client.members.get('me') + data = client.members.get_board('me') # Personal boards + for oid in user_data['idOrganizations']: + # Cycle through all organization boards + data += client.organizations.get_board(oid) + return data + + +def get_all_trello_board_lists(trello_token, board_id, client=None): + ''' Get JSON data for all lists on a given Trello board. + ''' + if client is None: + client = get_client(trello_token) + return client.boards.get_list(board_id) + + +def get_card_from_board(trello_token, board_id, card_id, client=None): + ''' Get Card JSON data from short id (ie, #123) from specific board. + ''' + if client is None: + client = get_client(trello_token) + return client.boards.get_card_idCard(card_id, board_id) + + +def add_comment_to_card(trello_token, card_id, comment, client=None): + if client is None: + client = get_client(trello_token) + return client.cards.new_action_comment(card_id, comment) + + +def move_card(trello_token, card_id, new_list_id, client=None): + ''' Move a card from one list to another. + ''' + if client is None: + client = get_client(trello_token) + return client.cards.update_idList(card_id, new_list_id) + + +def archive_card(trello_token, card_id, client=None): + if client is None: + client = get_client(trello_token) + return client.cards.update_closed(card_id, True) diff --git a/trello_broker/management/__init__.py b/trello_broker/management/__init__.py new file mode 100644 diff --git a/trello_broker/management/commands/__init__.py b/trello_broker/management/commands/__init__.py new file mode 100644 diff --git a/trello_broker/management/commands/add_trello_token.py b/trello_broker/management/commands/add_trello_token.py new file mode 100644 diff --git a/trello_broker/management/commands/populate_trello_boards.py b/trello_broker/management/commands/populate_trello_boards.py new file mode 100644 diff --git a/trello_broker/models.py b/trello_broker/models.py --- a/trello_broker/models.py +++ b/trello_broker/models.py @@ -0,0 +1,131 @@ +from django.db import models +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + + +class BaseModel(models.Model): + created = models.DateTimeField(default=timezone.now) + modified = models.DateTimeField(default=timezone.now) + + class Meta: + abstract = True + + def __unicode__(self): + return getattr(self, 'name', u'') + + def save(self, *args, **kwargs): + self.modified = timezone.now() + super(BaseModel, self).save(*args, **kwargs) + + +class TrelloToken(BaseModel): + name = models.CharField( + max_length=100, + help_text=_('Name of the account. Used for internal identification'), + ) + api_key = models.CharField( + max_length=100, + help_text=_('Get this from https://trello.com/1/appKey/generate'), + ) + api_token = models.CharField( + max_length=100, + help_text=_( + 'Get this from https://trello.com/1/authorize?' + 'key=&name=&expiration=never&' + 'response_type=token&scope=read,write' + ), + ) + + +class TrelloBoard(BaseModel): + trello_token = models.ForeignKey('trello_broker.TrelloToken') + name = models.CharField(max_length=100, blank=True) + trello_id = models.CharField(max_length=100) + + +class TrelloList(BaseModel): + STATUS_ACTIVE = 1 + STATUS_ARCHIVED = 2 + STATUS_CHOICES = ( + (STATUS_ACTIVE, _('Active')), + (STATUS_ARCHIVED, _('Archived')), + ) + trello_board = models.ForeignKey( + 'trello_broker.TrelloBoard', + related_name='trello_lists', + ) + name = models.CharField(max_length=100) + status = models.PositiveIntegerField( + default=STATUS_ACTIVE, + choices=STATUS_CHOICES, + ) + trello_id = models.CharField(max_length=100) + + def __unicode__(self): + return u'{0}: {1}'.format(self.trello_board.name, self.name) + + +class BitBucketRepo(BaseModel): + name = models.CharField( + max_length=100, + help_text=_('Name of the repo. Used for internal identification'), + ) + slug = models.SlugField( + max_length=100, + help_text=_('Slug ID given by BitBucket for this repository.'), + ) + trello_board = models.ForeignKey('trello_broker.TrelloBoard') + + @property + def fix_rule(self): + if not hasattr(self, '_fix_rule'): + self._fix_rule = self.rules.filter( + action=BitBucketRule.ACTION_FIXES, + ).first() + return self._fix_rule + + @property + def ref_rule(self): + if not hasattr(self, '_ref_rule'): + self._ref_rule = self.rules.filter( + action=BitBucketRule.ACTION_REFERENCED, + ).first() + return self._ref_rule + + +class BitBucketRule(BaseModel): + ACTION_REFERENCED = 1 + ACTION_FIXES = 2 + ACTION_CHOICES = ( + (ACTION_REFERENCED, _('Referenced')), + (ACTION_FIXES, _('Fixes / Closes')), + ) + repo = models.ForeignKey( + 'trello_broker.BitBucketRepo', + related_name='rules', + ) + action = models.PositiveIntegerField( + default=ACTION_REFERENCED, + choices=ACTION_CHOICES, + ) + update = models.BooleanField( + default=True, + help_text=_('If checked, card will be updated with commit comment.'), + ) + archive = models.BooleanField( + default=False, + help_text=_('If checked, card will be archived.'), + ) + move = models.BooleanField( + default=False, + help_text=_('If checked, card will be moved ' + 'to specified Trello List.'), + ) + trello_list = models.ForeignKey( + 'trello_broker.TrelloList', + null=True, + blank=True, + ) + + def __unicode__(self): + return u'{0}: {1}'.format(self.repo.name, self.get_action_display()) diff --git a/trello_broker/templates/trello_broker/update_message.txt b/trello_broker/templates/trello_broker/update_message.txt new file mode 100644 --- /dev/null +++ b/trello_broker/templates/trello_broker/update_message.txt @@ -0,0 +1,1 @@ +Commit ```{{ changeset }}``` committed by {{ author }} with the message ```{{ commit_message }}``` - View at: {{ commit_url }} diff --git a/trello_broker/urls.py b/trello_broker/urls.py new file mode 100644 --- /dev/null +++ b/trello_broker/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import patterns, url +from .views import BitBucketPostView + + +urlpatterns = patterns('trello_broker.views', + url(r'^$', + BitBucketPostView.as_view(), + name='trello_broker_post'), +) diff --git a/trello_broker/utils.py b/trello_broker/utils.py --- a/trello_broker/utils.py +++ b/trello_broker/utils.py @@ -0,0 +1,92 @@ +import re +from urlparse import urljoin +from django.conf import settings +from django.template.loader import render_to_string +from . import api + + +# Based on pattern from https://bitbucket.org/sntran/trello-broker +re_pattern = re.compile(r''' + ( # start capturing the verb + fix # contains 'fix' + | close # or 'close' + | # or just to reference + ) # end capturing the verb + e? # maybe followed by 'e' + (?:s|d)? # or 's' or 'd', not capturing + \s # then a white space + [#] # and '#' to indicate the card + ([0-9]+) # with the card's short id. + ''', + re.VERBOSE | re.IGNORECASE, +) + + +def process_commits(repo, json_data): + ''' Function to parse commit messages and act on + BitBucketRule sets. + ''' + base_repo_url = u'{0}{1}'.format( + json_data['canon_url'], + json_data['repository']['absolute_url'], + ) + base_repo_url += '/' if base_repo_url[-1] != '/' else '' + base_commit_url = urljoin(base_repo_url, 'commits/') + trello_token = repo.trello_board.trello_token + + for commit in json_data['commits']: + proc_cards = [] + msg = commit['message'] + context = { + 'author': commit['author'], + 'author_full': commit['raw_author'], + 'changeset': commit['node'], + 'changeset_full': commit['raw_node'], + 'commit_url': urljoin(base_commit_url, commit['raw_node']) + 'commit_message': msg, + } + + for action, card_id in re_pattern.findall(msg): + if card_id in proc_cards: + # Referenced more than once in the commit msg + continue + + proc_cards.append(card_id) + use_rule = 'fix_rule' \ + if action.lower() in ['fix', 'close'] else 'ref_rule' + rule = getattr(repo, use_rule) + if not rule: + continue + + # Get Card information + card = api.get_card_from_board( + trello_token, + card_id, + repo.trello_board.trello_id, + ) + full_card_id = card['id'] + + # Let's process the rule + if rule.update: + # Update the card. + card_msg = render_to_string( + 'trello_broker/update_message.txt', + context, + ) + api.add_comment_to_card( + trello_token, + full_card_id, + card_msg, + ) + + if rule.move: + # Move the card + api.move_card( + trello_token, + full_card_id, + rule.trello_list.trello_id, + ) + + if rule.archive: + # Archive the card + api.archive_card(trello_token, full_card_id) diff --git a/trello_broker/views.py b/trello_broker/views.py --- a/trello_broker/views.py +++ b/trello_broker/views.py @@ -0,0 +1,38 @@ +import json +from django.conf import settings +from django.views.generic import View +from django.shortcuts import get_object_or_404 +from django.http import HttpResponse, HttpResponseBadRequest, Http404 +from .models import BitBucketRepo +from .utils import process_commits + +if getattr(settings, 'TRELLO_BROKER_USE_CELERY', False): + from .tasks import celery_process_commits +else: + celery_process_commits = None + + +def trello_distribute(repo, json_data): + ''' Helper function that decides how to process + Trello interaction (celery or not) + ''' + if celery_process_commits is not None: + celery_process_commits.delay(repo.id, json_data) + else: + process_commits(repo, json_data) + + +class BitBucketPostView(View): + def get(self, request, *args, **kwargs): + return HttpResponseBadRequest() + + def post(self, request, *args, **kwargs): + try: + json_data = json.loads(request.body) + repo_slug = json_data['respository']['slug'] + except (ValueError, KeyError): + return HttpResponseBadRequest() + + repo = get_object_or_404(BitBucketRepo, slug=repo_slug) + trello_distribute(repo, json_data) + return HttpResponse('OK')