A trello_broker/api.py +55 -0
@@ 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)
A => trello_broker/management/__init__.py +0 -0
A => trello_broker/management/commands/__init__.py +0 -0
A => trello_broker/management/commands/add_trello_token.py +0 -0
A => trello_broker/management/commands/populate_trello_boards.py +0 -0
A trello_broker/models.py +131 -0
@@ 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=<your api_key>&name=<Your+App+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())
A => trello_broker/templates/trello_broker/update_message.txt +1 -0
@@ 0,0 1,1 @@
+Commit ```{{ changeset }}``` committed by {{ author }} with the message ```{{ commit_message }}``` - View at: {{ commit_url }}
A => trello_broker/urls.py +9 -0
@@ 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'),
+)
A trello_broker/utils.py +92 -0
@@ 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)
A trello_broker/views.py +38 -0
@@ 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')