Merge pull request #33 from jpadilla/webhooks

Webhooks
This commit is contained in:
Mike Edmunds
2013-04-17 10:30:47 -07:00
6 changed files with 145 additions and 15 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.DS_Store .DS_Store
._* ._*
*.pyc *.pyc
*.egg
*.egg-info *.egg-info
dist/ dist/
docs/_build/ docs/_build/

3
djrill/signals.py Normal file
View File

@@ -0,0 +1,3 @@
from django.dispatch import Signal
webhook_event = Signal(providing_args=['event_type', 'data'])

View File

@@ -2,3 +2,4 @@ from djrill.tests.test_admin import *
from djrill.tests.test_legacy import * from djrill.tests.test_legacy import *
from djrill.tests.test_mandrill_send import * from djrill.tests.test_mandrill_send import *
from djrill.tests.test_mandrill_send_template import * from djrill.tests.test_mandrill_send_template import *
from djrill.tests.test_mandrill_webhook import *

View File

@@ -0,0 +1,74 @@
import json
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
from ..signals import webhook_event
class DjrillWebhookSecretMixinTests(TestCase):
"""
Test mixin used in optional Mandrill webhook support
"""
def test_missing_secret(self):
del settings.DJRILL_WEBHOOK_SECRET
with self.assertRaises(ImproperlyConfigured):
self.client.get('/webhook/')
def test_incorrect_secret(self):
settings.DJRILL_WEBHOOK_SECRET = 'abc123'
response = self.client.head('/webhook/?secret=wrong')
self.assertEqual(response.status_code, 403)
def test_default_secret_name(self):
del settings.DJRILL_WEBHOOK_SECRET_NAME
settings.DJRILL_WEBHOOK_SECRET = 'abc123'
response = self.client.head('/webhook/?secret=abc123')
self.assertEqual(response.status_code, 200)
def test_custom_secret_name(self):
settings.DJRILL_WEBHOOK_SECRET = 'abc123'
settings.DJRILL_WEBHOOK_SECRET_NAME = 'verysecret'
response = self.client.head('/webhook/?verysecret=abc123')
self.assertEqual(response.status_code, 200)
class DjrillWebhookViewTests(TestCase):
"""
Test optional Mandrill webhook view
"""
def setUp(self):
settings.DJRILL_WEBHOOK_SECRET = 'abc123'
def test_head_request(self):
response = self.client.head('/webhook/?secret=abc123')
self.assertEqual(response.status_code, 200)
def test_post_request_invalid_json(self):
response = self.client.post('/webhook/?secret=abc123')
self.assertEqual(response.status_code, 400)
def test_post_request_valid_json(self):
response = self.client.post('/webhook/?secret=abc123', {
'mandrill_events': json.dumps([{"event": "send", "msg": {}}])
})
self.assertEqual(response.status_code, 200)
def test_webhook_send_signal(self):
def my_callback(sender, event_type, data, **kwargs):
self.assertEqual(event_type, 'send')
webhook_event.connect(my_callback)
response = self.client.post('/webhook/?secret=abc123', {
'mandrill_events': json.dumps([{"event": "send", "msg": {}}])
})
self.assertEqual(response.status_code, 200)

View File

@@ -0,0 +1,13 @@
try:
from django.conf.urls import patterns, url
except ImportError:
from django.conf.urls.defaults import patterns, url
from .views import DjrillWebhookView
urlpatterns = patterns(
'',
url(r'^webhook/$', DjrillWebhookView.as_view(), name='djrill_webhook'),
)

View File

@@ -4,19 +4,21 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.views.generic import TemplateView from django.views.generic import TemplateView, View
from django.http import HttpResponse
from djrill import MANDRILL_API_URL from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
import requests import requests
from djrill import MANDRILL_API_URL, signals
class DjrillAdminMedia(object): class DjrillAdminMedia(object):
def _media(self): def _media(self):
js = ["js/core.js", "js/jquery.min.js", "js/jquery.init.js"] js = ["js/core.js", "js/jquery.min.js", "js/jquery.init.js"]
return forms.Media(js=["%s%s" % (settings.STATIC_URL, url) return forms.Media(js=["%s%s" % (settings.STATIC_URL, url) for url in js])
for url in js])
media = property(_media) media = property(_media)
@@ -29,8 +31,8 @@ class DjrillApiMixin(object):
self.api_url = MANDRILL_API_URL self.api_url = MANDRILL_API_URL
if not self.api_key: if not self.api_key:
raise ImproperlyConfigured("You have not set your mandrill api key " raise ImproperlyConfigured(
"in the settings file.") "You have not set your mandrill api key in the settings file.")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(DjrillApiMixin, self).get_context_data(**kwargs) kwargs = super(DjrillApiMixin, self).get_context_data(**kwargs)
@@ -53,8 +55,9 @@ class DjrillApiJsonObjectsMixin(object):
def get_api_uri(self): def get_api_uri(self):
if self.api_uri is None: if self.api_uri is None:
raise NotImplementedError("%(cls)s is missing an api_uri. Define " raise NotImplementedError(
"%(cls)s.api_uri or override %(cls)s.get_api_uri()." % { "%(cls)s is missing an api_uri. "
"Define %(cls)s.api_uri or override %(cls)s.get_api_uri()." % {
"cls": self.__class__.__name__ "cls": self.__class__.__name__
}) })
@@ -80,6 +83,24 @@ class DjrillApiJsonObjectsMixin(object):
content["message"]) content["message"])
class DjrillWebhookSecretMixin(object):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
secret = getattr(settings, 'DJRILL_WEBHOOK_SECRET', None)
secret_name = getattr(settings, 'DJRILL_WEBHOOK_SECRET_NAME', 'secret')
if secret is None:
raise ImproperlyConfigured(
"You have not set DJRILL_WEBHOOK_SECRET in the settings file.")
if request.GET.get(secret_name) != secret:
return HttpResponse(status=403)
return super(DjrillWebhookSecretMixin, self).dispatch(
request, *args, **kwargs)
class DjrillIndexView(DjrillApiMixin, TemplateView): class DjrillIndexView(DjrillApiMixin, TemplateView):
template_name = "djrill/status.html" template_name = "djrill/status.html"
@@ -138,3 +159,20 @@ class DjrillUrlListView(DjrillAdminMedia, DjrillApiMixin,
"media": self.media "media": self.media
}) })
return self.render_to_response(context) return self.render_to_response(context)
class DjrillWebhookView(DjrillWebhookSecretMixin, View):
def head(self, request, *args, **kwargs):
return HttpResponse()
def post(self, request, *args, **kwargs):
try:
data = json.loads(request.POST.get('mandrill_events'))
except TypeError:
return HttpResponse(status=400)
for event in data:
signals.webhook_event.send(
sender=None, event_type=event['event'], data=event)
return HttpResponse()