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)