# HG changeset patch # User Peter Sanchez # Date 1417311315 28800 # Sat Nov 29 17:35:15 2014 -0800 # Node ID c2c3d57ca7c395fa1e70bed449ed2a75fbe61293 # Parent 9326c27dce3c2a896339c3186c7d7db028e1828a Lots of work. Management commands all work well. diff --git a/trello_broker/admin.py b/trello_broker/admin.py --- a/trello_broker/admin.py +++ b/trello_broker/admin.py @@ -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) diff --git a/trello_broker/api.py b/trello_broker/api.py --- a/trello_broker/api.py +++ b/trello_broker/api.py @@ -19,6 +19,22 @@ 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. ''' diff --git a/trello_broker/forms.py b/trello_broker/forms.py new file mode 100644 --- /dev/null +++ b/trello_broker/forms.py @@ -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 diff --git a/trello_broker/management/commands/add_trello_token.py b/trello_broker/management/commands/add_trello_token.py --- a/trello_broker/management/commands/add_trello_token.py +++ b/trello_broker/management/commands/add_trello_token.py @@ -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)) diff --git a/trello_broker/management/commands/populate_trello_boards.py b/trello_broker/management/commands/populate_trello_boards.py --- a/trello_broker/management/commands/populate_trello_boards.py +++ b/trello_broker/management/commands/populate_trello_boards.py @@ -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() diff --git a/trello_broker/models.py b/trello_broker/models.py --- a/trello_broker/models.py +++ b/trello_broker/models.py @@ -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 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 @@ ) 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 @@ 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 @@ related_name='rules', ) action = models.PositiveIntegerField( - default=ACTION_REFERENCED, choices=ACTION_CHOICES, ) update = models.BooleanField( @@ -127,5 +205,9 @@ 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()) diff --git a/trello_broker/settings.py b/trello_broker/settings.py new file mode 100644 --- /dev/null +++ b/trello_broker/settings.py @@ -0,0 +1,4 @@ +from django.conf import settings + + +USE_CELERY = getattr(settings, 'TRELLO_BROKER_USE_CELERY', False) diff --git a/trello_broker/tasks.py b/trello_broker/tasks.py new file mode 100644 --- /dev/null +++ b/trello_broker/tasks.py @@ -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) diff --git a/trello_broker/utils.py b/trello_broker/utils.py --- a/trello_broker/utils.py +++ b/trello_broker/utils.py @@ -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 diff --git a/trello_broker/views.py b/trello_broker/views.py --- a/trello_broker/views.py +++ b/trello_broker/views.py @@ -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 @@ 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 @@ 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 @@ 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)