# HG changeset patch # User Peter Sanchez # Date 1417382116 28800 # Sun Nov 30 13:15:16 2014 -0800 # Node ID 3f7b3a5a0d7326db2982c9f39ffa636f22b07679 # Parent 5066b739c603430ceb9fc29b6172231bd2bb1dc3 Fixing up models, errors found in testing, etc. diff --git a/trello_broker/admin.py b/trello_broker/admin.py --- a/trello_broker/admin.py +++ b/trello_broker/admin.py @@ -1,6 +1,6 @@ from django.db.models import Q from django.contrib import admin -from .forms import BitBucketRuleAdminForm +from .forms import BitBucketRepoAdminForm, BitBucketRuleAdminForm from .models import ( STATUS_ACTIVE, TrelloToken, TrelloBoard, TrelloList, BitBucketRepo, BitBucketRule, @@ -13,6 +13,7 @@ class TrelloBoardAdmin(BaseTrelloBrokerAdmin): actions = ['update_board',] + list_filter = ('trello_token__name', 'status') def update_board(self, request, queryset): for board in queryset: @@ -23,6 +24,10 @@ ) +class TrelloListAdmin(BaseTrelloBrokerAdmin): + list_filter = ('trello_board', 'status') + + class BitBucketRuleInline(admin.TabularInline): model = BitBucketRule form = BitBucketRuleAdminForm @@ -62,9 +67,11 @@ class BitBucketRepoAdmin(BaseTrelloBrokerAdmin): inlines = [BitBucketRuleInline] + form = BitBucketRepoAdminForm def get_formsets_with_inlines(self, request, obj=None): if obj is None: + # No inlines when adding a new object return for inline in self.get_inline_instances(request, obj): @@ -73,5 +80,5 @@ admin.site.register(TrelloToken, BaseTrelloBrokerAdmin) admin.site.register(TrelloBoard, TrelloBoardAdmin) -admin.site.register(TrelloList, BaseTrelloBrokerAdmin) +admin.site.register(TrelloList, TrelloListAdmin) 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 @@ -5,12 +5,11 @@ return trello.TrelloApi(trello_token.api_key, token=trello_token.api_token) -def get_all_trello_boards(trello_token, client=None): +def get_all_trello_boards(trello_token): ''' Get JSON data for all Trello boards the given token has access to. ''' - if client is None: - client = get_client(trello_token) + client = trello_token.client user_data = client.members.get('me') data = client.members.get_board('me') # Personal boards for oid in user_data['idOrganizations']: @@ -19,53 +18,46 @@ return data -def get_trello_board(trello_token, board_id, client=None): +def get_trello_board(trello_token, board_id): ''' Get the JSON data for a specific board. ''' - if client is None: - client = get_client(trello_token) + client = trello_token.client return client.boards.get(board_id=board_id) -def get_trello_list(trello_token, list_id, client=None): +def get_trello_list(trello_token, list_id): ''' Get the JSON data for a specific list. ''' - if client is None: - client = get_client(trello_token) + client = trello_token.client return client.lists.get(list_id=list_id) -def get_all_trello_board_lists(trello_token, board_id, client=None): +def get_all_trello_board_lists(trello_token, board_id): ''' Get JSON data for all lists on a given Trello board. ''' - if client is None: - client = get_client(trello_token) + client = trello_token.client return client.boards.get_list(board_id) -def get_card_from_board(trello_token, board_id, card_id, client=None): +def get_card_from_board(trello_token, board_id, card_id): ''' Get Card JSON data from short id (ie, #123) from specific board. ''' - if client is None: - client = get_client(trello_token) + client = trello_token.client 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) +def add_comment_to_card(trello_token, card_id, comment): + client = trello_token.client return client.cards.new_action_comment(card_id, comment) -def move_card(trello_token, card_id, new_list_id, client=None): +def move_card(trello_token, card_id, new_list_id): ''' Move a card from one list to another. ''' - if client is None: - client = get_client(trello_token) + client = trello_token.client 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) +def archive_card(trello_token, card_id): + client = trello_token.client return client.cards.update_closed(card_id, True) diff --git a/trello_broker/forms.py b/trello_broker/forms.py --- a/trello_broker/forms.py +++ b/trello_broker/forms.py @@ -1,7 +1,27 @@ from django import forms from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from .models import BitBucketRule +from .models import BitBucketRepo, BitBucketRule + + +class BitBucketRepoAdminForm(forms.ModelForm): + class Meta: + model = BitBucketRepo + fields = ('name', 'slug', 'access_key', 'trello_board') + + def clean_access_key(self): + access_key = self.cleaned_data.get('access_key') + slug = self.cleaned_data.get('slug') + if slug: + query = Q(slug=slug) & Q(access_key=access_key) + if hasattr(self, 'instance'): + query &= ~Q(pk=self.instance.pk) + if BitBucketRepo.objects.filter(query).exists(): + raise forms.ValidationError(_( + 'There is already a BitBucket Repo stored with the ' + 'same slug & access_key.' + )) + return access_key class BitBucketRuleAdminForm(forms.ModelForm): @@ -10,7 +30,6 @@ 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): @@ -20,7 +39,7 @@ action = self.cleaned_data['action'] query = Q(repo=self.cleaned_data['repo']) & \ Q(action=action) - if self.is_existing: + if hasattr(self, 'instance'): query &= ~Q(pk=self.instance.pk) if BitBucketRule.objects.filter(query).exists(): diff --git a/trello_broker/migrations/0001_initial.py b/trello_broker/migrations/0001_initial.py new file mode 100644 --- /dev/null +++ b/trello_broker/migrations/0001_initial.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='BitBucketRepo', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('modified', models.DateTimeField(default=django.utils.timezone.now)), + ('name', models.CharField(help_text='Name of the repo. Used for internal identification', max_length=100)), + ('slug', models.SlugField(help_text='Slug ID given by BitBucket for this repository.', max_length=100)), + ('access_key', models.CharField(help_text='Secret key used to "authenticate" the request. If saved here the key must be appended to the BitBucket hook URL like so: http://yourserver.com/broker/?access_key=', max_length=100, blank=True)), + ], + options={ + 'verbose_name': 'BitBucket Repository', + 'verbose_name_plural': 'BitBucket Repositories', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='BitBucketRule', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('modified', models.DateTimeField(default=django.utils.timezone.now)), + ('action', models.PositiveIntegerField(choices=[(1, 'Referenced'), (2, 'Fixes / Closes')])), + ('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.')), + ('repo', models.ForeignKey(related_name='rules', to='trello_broker.BitBucketRepo')), + ], + options={ + 'verbose_name': 'BitBucket Rule', + 'verbose_name_plural': 'BitBucket Rules', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='TrelloBoard', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('modified', models.DateTimeField(default=django.utils.timezone.now)), + ('name', models.CharField(max_length=100, blank=True)), + ('status', models.PositiveIntegerField(default=0, choices=[(0, 'Active'), (1, 'Archived')])), + ('trello_id', models.CharField(max_length=100)), + ], + options={ + 'verbose_name': 'Trello Board', + 'verbose_name_plural': 'Trello Boards', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='TrelloList', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('modified', models.DateTimeField(default=django.utils.timezone.now)), + ('name', models.CharField(max_length=100)), + ('status', models.PositiveIntegerField(default=0, choices=[(0, 'Active'), (1, 'Archived')])), + ('trello_id', models.CharField(max_length=100)), + ('trello_board', models.ForeignKey(related_name='trello_lists', to='trello_broker.TrelloBoard')), + ], + options={ + 'verbose_name': 'Trello List', + 'verbose_name_plural': 'Trello Lists', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='TrelloToken', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('modified', models.DateTimeField(default=django.utils.timezone.now)), + ('name', models.CharField(help_text='Name of the account. Used for internal identification', max_length=100)), + ('api_key', models.CharField(help_text='Get this from https://trello.com/1/appKey/generate', max_length=100)), + ('api_token', models.CharField(help_text='Get this from https://trello.com/1/authorize?key=&name=&expiration=never&response_type=token&scope=read,write', max_length=100)), + ], + options={ + 'verbose_name': 'Trello Token', + 'verbose_name_plural': 'Trello Tokens', + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='trelloboard', + name='trello_token', + field=models.ForeignKey(to='trello_broker.TrelloToken'), + preserve_default=True, + ), + migrations.AddField( + model_name='bitbucketrule', + name='trello_list', + field=models.ForeignKey(blank=True, to='trello_broker.TrelloList', null=True), + preserve_default=True, + ), + migrations.AlterUniqueTogether( + name='bitbucketrule', + unique_together=set([('repo', 'action')]), + ), + migrations.AddField( + model_name='bitbucketrepo', + name='trello_board', + field=models.ForeignKey(related_name='repos', to='trello_broker.TrelloBoard'), + preserve_default=True, + ), + migrations.AlterUniqueTogether( + name='bitbucketrepo', + unique_together=set([('slug', 'access_key')]), + ), + ] diff --git a/trello_broker/migrations/__init__.py b/trello_broker/migrations/__init__.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 @@ -41,7 +41,7 @@ max_length=100, help_text=_( 'Get this from https://trello.com/1/authorize?' - 'key=&name=&expiration=never&' + 'key=YOUR_API_KEY&name=Your+App+Name&expiration=never&' 'response_type=token&scope=read,write' ), ) @@ -50,6 +50,12 @@ verbose_name = 'Trello Token' verbose_name_plural = 'Trello Tokens' + @property + def client(self): + if not hasattr(self, '_api_client'): + self._api_client = api.get_client(self) + return self._api_client + class TrelloBoard(BaseModel): trello_token = models.ForeignKey('trello_broker.TrelloToken') @@ -151,7 +157,7 @@ help_text=_( 'Secret key used to "authenticate" the request. If saved ' 'here the key must be appended to the BitBucket hook URL like ' - 'so: http://yourserver.com/broker/?access_key=' + 'so: http://yourserver.com/broker/?access_key=YOUR_ACCESS_KEY' ), ) trello_board = models.ForeignKey( @@ -218,6 +224,7 @@ class Meta: verbose_name = 'BitBucket Rule' verbose_name_plural = 'BitBucket Rules' + unique_together = ('repo', 'action') 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 --- a/trello_broker/settings.py +++ b/trello_broker/settings.py @@ -2,3 +2,13 @@ USE_CELERY = getattr(settings, 'TRELLO_BROKER_USE_CELERY', False) + +RESTRICT_IPS = getattr(settings, 'TRELLO_BROKER_RESTRICT_IPS', False) + +# https://confluence.atlassian.com/display/BITBUCKET/What+are+the+Bitbucket+IP+addresses+I+should+use+to+configure+my+corporate+firewall +# Only needed if TRELLO_BROKER_RESTRICT_IPS is True +BITBUCKET_IPS = getattr( + settings, + 'TRELLO_BROKER_BITBUCKET_IPS', + ['131.103.20.165', '131.103.20.166'], +) diff --git a/trello_broker/utils.py b/trello_broker/utils.py --- a/trello_broker/utils.py +++ b/trello_broker/utils.py @@ -60,8 +60,8 @@ # Get Card information card = api.get_card_from_board( trello_token, + repo.trello_board.trello_id, card_id, - repo.trello_board.trello_id, ) full_card_id = card['id'] diff --git a/trello_broker/views.py b/trello_broker/views.py --- a/trello_broker/views.py +++ b/trello_broker/views.py @@ -29,15 +29,18 @@ return HttpResponseBadRequest() def post(self, request, *args, **kwargs): + if settings.RESTRICT_IPS: + if request.META['REMOTE_ADDR'] not in settings.BITBUCKET_IPS: + return HttpResponseBadRequest() + try: json_data = json.loads(request.POST['payload']) - repo_slug = json_data['respository']['slug'] - except (ValueError, KeyError): + repo_slug = json_data['repository']['slug'] + except (ValueError, KeyError) as e: return HttpResponseBadRequest() - query = Q(slug=repo_slug) - if 'access_key' in request.REQUEST: - query += Q(access_key=request.REQUEST['access_key']) + query = Q(slug=repo_slug) & \ + Q(access_key=request.REQUEST.get('access_key', '')) repo = get_object_or_404(BitBucketRepo, query) trello_distribute(repo, json_data) return HttpResponse('OK')