Lots of work. Management commands all work well.
A trello_broker/admin.py +77 -0
@@ 0,0 1,77 @@ 
+from django.db.models import Q
+from django.contrib import admin
+from .forms import BitBucketRuleAdminForm
+from .models import (
+    STATUS_ACTIVE, TrelloToken, TrelloBoard, TrelloList,
+    BitBucketRepo, BitBucketRule,
+)
+
+
+class BaseTrelloBrokerAdmin(admin.ModelAdmin):
+    exclude = ('created', 'modified')
+
+
+class TrelloBoardAdmin(BaseTrelloBrokerAdmin):
+    actions = ['update_board',]
+
+    def update_board(self, request, queryset):
+        for board in queryset:
+            board.update_from_json()
+            board.populate_all_lists()
+    update_board.short_description = (
+        'Re-populate board & list data from Trello API'
+    )
+
+
+class BitBucketRuleInline(admin.TabularInline):
+    model = BitBucketRule
+    form = BitBucketRuleAdminForm
+
+    def get_extra(self, request, obj=None, **kwargs):
+        self._max_extra = len(BitBucketRule.ACTION_CHOICES)
+        extra = 0
+        if obj is not None:
+            self._cnt = obj.rules.count()
+            if (self._max_extra - self._cnt):
+                extra = 1
+        self._extra = extra
+        return extra
+
+    def get_max_num(self, request, obj=None, **kwargs):
+        if self._cnt < self._max_extra:
+            return (self._cnt + 1)
+        return 0
+
+    def get_formset(self, request, obj=None, **kwargs):
+        self._object = obj
+        return super(BitBucketRuleInline, self).get_formset(request,  obj, **kwargs)
+
+    def formfield_for_foreignkey(self, db_field, request, **kwargs):
+        if db_field.name == 'trello_list':
+            obj = getattr(self, '_object', None)
+            if obj is not None:
+                query = Q(trello_board=obj.trello_board) & \
+                        Q(status=STATUS_ACTIVE)
+                kwargs['queryset'] = TrelloList.objects.filter(query)
+        return super(BitBucketRuleInline, self).formfield_for_foreignkey(
+            db_field,
+            request,
+            **kwargs
+        )
+
+
+class BitBucketRepoAdmin(BaseTrelloBrokerAdmin):
+    inlines = [BitBucketRuleInline]
+
+    def get_formsets_with_inlines(self, request, obj=None):
+        if obj is None:
+            return
+
+        for inline in self.get_inline_instances(request, obj):
+            yield inline.get_formset(request, obj), inline
+
+
+admin.site.register(TrelloToken, BaseTrelloBrokerAdmin)
+admin.site.register(TrelloBoard, TrelloBoardAdmin)
+admin.site.register(TrelloList, BaseTrelloBrokerAdmin)
+admin.site.register(BitBucketRepo, BitBucketRepoAdmin)

          
M trello_broker/api.py +16 -0
@@ 19,6 19,22 @@ def get_all_trello_boards(trello_token, 
     return data
 
 
+def get_trello_board(trello_token, board_id, client=None):
+    ''' Get the JSON data for a specific board.
+    '''
+    if client is None:
+        client = get_client(trello_token)
+    return client.boards.get(board_id=board_id)
+
+
+def get_trello_list(trello_token, list_id, client=None):
+    ''' Get the JSON data for a specific list.
+    '''
+    if client is None:
+        client = get_client(trello_token)
+    return client.lists.get(list_id=list_id)
+
+
 def get_all_trello_board_lists(trello_token, board_id, client=None):
     ''' Get JSON data for all lists on a given Trello board.
     '''

          
A => trello_broker/forms.py +44 -0
@@ 0,0 1,44 @@ 
+from django import forms
+from django.db.models import Q
+from django.utils.translation import ugettext_lazy as _
+from .models import BitBucketRule
+
+
+class BitBucketRuleAdminForm(forms.ModelForm):
+    class Meta:
+        model = BitBucketRule
+        fields = ('repo', 'action', 'update', 'archive', 'move', 'trello_list')
+
+    def __init__(self, *args, **kwargs):
+        self.is_existing = 'instance' in kwargs
+        super(BitBucketRuleAdminForm, self).__init__(*args, **kwargs)
+
+    def clean_action(self):
+        if 'action' not in self.cleaned_data:
+            raise forms.ValidationError(_('This field is required.'))
+
+        action = self.cleaned_data['action']
+        query = Q(repo=self.cleaned_data['repo']) & \
+                Q(action=action)
+        if self.is_existing:
+            query &= ~Q(pk=self.instance.pk)
+
+        if BitBucketRule.objects.filter(query).exists():
+            raise forms.ValidationError(_(
+                'A matching rule already exists for this BitBucketRepo'
+            ))
+        return action
+
+    def clean_trello_list(self):
+        trello_list = self.cleaned_data.get('trello_list')
+        if trello_list:
+            if not self.cleaned_data.get('move', False):
+                raise forms.ValidationError(_(
+                    'Rule must have "Move" checked to select a Trello list'
+                ))
+        else:
+            if self.cleaned_data.get('move', False):
+                raise forms.ValidationError(_(
+                    'This field is required when "Move" is checked.'
+                ))
+        return trello_list

          
A trello_broker/management/commands/add_trello_token.py +29 -0
@@ 0,0 1,29 @@ 
+import trello
+from django.core.management.base import BaseCommand
+from trello_broker.models import TrelloToken
+
+
+class Command(BaseCommand):
+    help = 'Add Trello App API Token to the trello_broker app'
+
+    def handle(self, *args, **options):
+        print('Enter your Trello applications name.\n')
+        app_name = raw_input('App Name: ')
+        print(
+            'Enter your Trello user API Key. You can get it from:\n\n'
+            'https://trello.com/1/appKey/generate\n'
+        )
+        api_key = raw_input('API Key: ')
+        client = trello.TrelloApi(api_key)
+        url = client.get_token_url(app_name, expires='never')
+        print(
+            'Go to the following URL to get your API Token:\n\n'
+            '{0}\n'.format(url)
+        )
+        api_token = raw_input('API Token: ')
+        token = TrelloToken.objects.create(
+            name=app_name,
+            api_key=api_key,
+            api_token=api_token,
+        )
+        print('Saved token (ID: {0}) to the database.'.format(token.pk))

          
A trello_broker/management/commands/populate_trello_boards.py +30 -0
@@ 0,0 1,30 @@ 
+from django.core.management.base import BaseCommand
+from trello_broker import api
+from trello_broker.models import TrelloToken, TrelloBoard
+
+
+class Command(BaseCommand):
+    help = (
+        'Cycle through all Trello tokens and populate '
+        'all Trello Boards and Lists'
+    )
+
+    def handle(self, *args, **options):
+        for token in TrelloToken.objects.all():
+            print('Processing token {0}'.format(token.name))
+            boards = api.get_all_trello_boards(token)
+            for board in boards:
+                print('Processing board {0}'.format(board['name']))
+                _board = \
+                    TrelloBoard.objects.filter(trello_id=board['id']).first()
+                if _board:
+                    _board.update_from_json(json_data=board)
+                else:
+                    _board = TrelloBoard.objects.create(
+                        trello_token=token,
+                        name=board['name'],
+                        status=int(board['closed']),
+                        trello_id=board['id'],
+                    )
+
+                _board.populate_all_lists()

          
M trello_broker/models.py +90 -8
@@ 1,6 1,16 @@ 
 from django.db import models
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
+from . import api
+
+Q = models.Q
+
+STATUS_ACTIVE = 0
+STATUS_ARCHIVED = 1
+STATUS_CHOICES = (
+    (STATUS_ACTIVE, _('Active')),
+    (STATUS_ARCHIVED, _('Archived')),
+)
 
 
 class BaseModel(models.Model):

          
@@ 36,20 46,65 @@ class TrelloToken(BaseModel):
         ),
     )
 
+    class Meta:
+        verbose_name = 'Trello Token'
+        verbose_name_plural = 'Trello Tokens'
+
 
 class TrelloBoard(BaseModel):
     trello_token = models.ForeignKey('trello_broker.TrelloToken')
     name = models.CharField(max_length=100, blank=True)
+    status = models.PositiveIntegerField(
+        default=STATUS_ACTIVE,
+        choices=STATUS_CHOICES,
+    )
     trello_id = models.CharField(max_length=100)
 
+    class Meta:
+        verbose_name = 'Trello Board'
+        verbose_name_plural = 'Trello Boards'
+
+    def get_trello_json(self):
+        return api.get_trello_board(self.trello_token, self.trello_id)
+
+    def update_from_json(self, json_data=None):
+        if not json_data:
+            json_data = self.get_trello_json()
+        if not self.name == json_data['name']:
+            self.name = json_data['name']
+        if bool(self.status) != json_data['closed']:
+            self.status = int(json_data['closed'])
+        self.save()
+
+    def populate_all_lists(self, json_data=None):
+        if not json_data:
+            json_data = api.get_all_trello_board_lists(
+                self.trello_token,
+                self.trello_id,
+            )
+
+        all_list_ids = set()
+        for list_data in json_data:
+            all_list_ids.add(list_data['id'])
+            _list = \
+                self.trello_lists.filter(trello_id=list_data['id']).first()
+            if _list:
+                _list.update_from_json(json_data=list_data)
+            else:
+                _list = TrelloList.objects.create(
+                    trello_board=self,
+                    trello_id=list_data['id'],
+                    name=list_data['name'],
+                    status=int(list_data['closed']),
+                )
+
+        # Delete any db lists that are no longer living on Trello
+        query = Q(trello_board=self) & \
+                ~Q(trello_id__in=all_list_ids)
+        TrelloList.objects.filter(query).delete()
+
 
 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',

          
@@ 61,6 116,22 @@ class TrelloList(BaseModel):
     )
     trello_id = models.CharField(max_length=100)
 
+    class Meta:
+        verbose_name = 'Trello List'
+        verbose_name_plural = 'Trello Lists'
+
+    def get_trello_json(self):
+        return api.get_trello_list(self.trello_token, self.trello_id)
+
+    def update_from_json(self, json_data=None):
+        if not json_data:
+            json_data = self.get_trello_json()
+        if self.name != json_data['name']:
+            self.name = json_data['name']
+        if bool(self.status) != json_data['closed']:
+            self.status = int(json_data['closed'])
+        self.save()
+
     def __unicode__(self):
         return u'{0}: {1}'.format(self.trello_board.name, self.name)
 

          
@@ 74,7 145,15 @@ class BitBucketRepo(BaseModel):
         max_length=100,
         help_text=_('Slug ID given by BitBucket for this repository.'),
     )
-    trello_board = models.ForeignKey('trello_broker.TrelloBoard')
+    trello_board = models.ForeignKey(
+        'trello_broker.TrelloBoard',
+        limit_choices_to={'status': STATUS_ACTIVE},
+        related_name='repos',
+    )
+
+    class Meta:
+        verbose_name = 'BitBucket Repository'
+        verbose_name_plural = 'BitBucket Repositories'
 
     @property
     def fix_rule(self):

          
@@ 105,7 184,6 @@ class BitBucketRule(BaseModel):
         related_name='rules',
     )
     action = models.PositiveIntegerField(
-        default=ACTION_REFERENCED,
         choices=ACTION_CHOICES,
     )
     update = models.BooleanField(

          
@@ 127,5 205,9 @@ class BitBucketRule(BaseModel):
         blank=True,
     )
 
+    class Meta:
+        verbose_name = 'BitBucket Rule'
+        verbose_name_plural = 'BitBucket Rules'
+
     def __unicode__(self):
         return u'{0}: {1}'.format(self.repo.name, self.get_action_display())

          
A => trello_broker/settings.py +4 -0
@@ 0,0 1,4 @@ 
+from django.conf import settings
+
+
+USE_CELERY = getattr(settings, 'TRELLO_BROKER_USE_CELERY', False)

          
A => trello_broker/tasks.py +15 -0
@@ 0,0 1,15 @@ 
+from __future__ import absolute_import
+from celery import shared_task
+from .models import BitBucketRepo
+from .utils import process_commits
+
+
+@shared_task
+def celery_process_commits(repo_pk, json_data):
+    ''' Celery wrapper for utils.process_commits
+    '''
+    try:
+        repo = BitBucketRepo.objects.get(pk=repo_pk)
+    except BitBucketRepo.DoesNotExist:
+        return
+    process_commits(repo, json_data)

          
M trello_broker/utils.py +0 -1
@@ 1,6 1,5 @@ 
 import re
 from urlparse import urljoin
-from django.conf import settings
 from django.template.loader import render_to_string
 from . import api
 

          
M trello_broker/views.py +10 -4
@@ 1,12 1,14 @@ 
 import json
-from django.conf import settings
 from django.views.generic import View
 from django.shortcuts import get_object_or_404
+from django.views.decorators.csrf import csrf_exempt
+from django.utils.decorators import method_decorator
 from django.http import HttpResponse, HttpResponseBadRequest, Http404
 from .models import BitBucketRepo
 from .utils import process_commits
+from . import settings
 
-if getattr(settings, 'TRELLO_BROKER_USE_CELERY', False):
+if settings.USE_CELERY:
     from .tasks import celery_process_commits
 else:
     celery_process_commits = None

          
@@ 17,7 19,7 @@ def trello_distribute(repo, json_data):
         Trello interaction (celery or not)
     '''
     if celery_process_commits is not None:
-        celery_process_commits.delay(repo.id, json_data)
+        celery_process_commits.delay(repo.pk, json_data)
     else:
         process_commits(repo, json_data)
 

          
@@ 28,7 30,7 @@ class BitBucketPostView(View):
 
     def post(self, request, *args, **kwargs):
         try:
-            json_data = json.loads(request.body)
+            json_data = json.loads(request.POST['payload'])
             repo_slug = json_data['respository']['slug']
         except (ValueError, KeyError):
             return HttpResponseBadRequest()

          
@@ 36,3 38,7 @@ class BitBucketPostView(View):
         repo = get_object_or_404(BitBucketRepo, slug=repo_slug)
         trello_distribute(repo, json_data)
         return HttpResponse('OK')
+
+    @method_decorator(csrf_exempt)
+    def dispatch(self, *args, **kwargs):
+        return super(BitBucketPostView, self).dispatch(*args, **kwargs)