diff --git a/.gitignore b/.gitignore index 0374977..c7de6dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store ._* *.pyc +*.egg *.egg-info dist/ docs/_build/ diff --git a/djrill/signals.py b/djrill/signals.py new file mode 100644 index 0000000..f8f7ba1 --- /dev/null +++ b/djrill/signals.py @@ -0,0 +1,3 @@ +from django.dispatch import Signal + +webhook_event = Signal(providing_args=['event_type', 'data']) diff --git a/djrill/tests/__init__.py b/djrill/tests/__init__.py index 1dd0c92..b663f5b 100644 --- a/djrill/tests/__init__.py +++ b/djrill/tests/__init__.py @@ -2,3 +2,4 @@ from djrill.tests.test_admin import * from djrill.tests.test_legacy import * from djrill.tests.test_mandrill_send import * from djrill.tests.test_mandrill_send_template import * +from djrill.tests.test_mandrill_webhook import * diff --git a/djrill/tests/test_mandrill_webhook.py b/djrill/tests/test_mandrill_webhook.py new file mode 100644 index 0000000..5c3d3e9 --- /dev/null +++ b/djrill/tests/test_mandrill_webhook.py @@ -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) diff --git a/djrill/urls.py b/djrill/urls.py index e69de29..14bc25c 100644 --- a/djrill/urls.py +++ b/djrill/urls.py @@ -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'), +) diff --git a/djrill/views.py b/djrill/views.py index 5f9051e..8a420cf 100644 --- a/djrill/views.py +++ b/djrill/views.py @@ -4,19 +4,21 @@ from django import forms from django.conf import settings from django.contrib import messages from django.core.exceptions import ImproperlyConfigured -from django.views.generic import TemplateView - -from djrill import MANDRILL_API_URL +from django.views.generic import TemplateView, View +from django.http import HttpResponse +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt import requests +from djrill import MANDRILL_API_URL, signals + class DjrillAdminMedia(object): def _media(self): js = ["js/core.js", "js/jquery.min.js", "js/jquery.init.js"] - return forms.Media(js=["%s%s" % (settings.STATIC_URL, url) - for url in js]) + return forms.Media(js=["%s%s" % (settings.STATIC_URL, url) for url in js]) media = property(_media) @@ -29,15 +31,15 @@ class DjrillApiMixin(object): self.api_url = MANDRILL_API_URL if not self.api_key: - raise ImproperlyConfigured("You have not set your mandrill api key " - "in the settings file.") + raise ImproperlyConfigured( + "You have not set your mandrill api key in the settings file.") def get_context_data(self, **kwargs): kwargs = super(DjrillApiMixin, self).get_context_data(**kwargs) status = False req = requests.post("%s/%s" % (self.api_url, "users/ping.json"), - data={"key": self.api_key}) + data={"key": self.api_key}) if req.status_code == 200: status = True @@ -53,8 +55,9 @@ class DjrillApiJsonObjectsMixin(object): def get_api_uri(self): if self.api_uri is None: - raise NotImplementedError("%(cls)s is missing an api_uri. Define " - "%(cls)s.api_uri or override %(cls)s.get_api_uri()." % { + raise NotImplementedError( + "%(cls)s is missing an api_uri. " + "Define %(cls)s.api_uri or override %(cls)s.get_api_uri()." % { "cls": self.__class__.__name__ }) @@ -65,7 +68,7 @@ class DjrillApiJsonObjectsMixin(object): payload = json.dumps(request_dict) api_uri = extra_api_uri or self.api_uri req = requests.post("%s/%s" % (self.api_url, api_uri), - data=payload) + data=payload) if req.status_code == 200: return req.content messages.error(self.request, self._api_error_handler(req)) @@ -77,7 +80,25 @@ class DjrillApiJsonObjectsMixin(object): """ content = json.loads(req.content) return "Mandrill returned a %d response: %s" % (req.status_code, - 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): @@ -92,7 +113,7 @@ class DjrillIndexView(DjrillApiMixin, TemplateView): class DjrillSendersListView(DjrillAdminMedia, DjrillApiMixin, - DjrillApiJsonObjectsMixin, TemplateView): + DjrillApiJsonObjectsMixin, TemplateView): api_uri = "users/senders.json" template_name = "djrill/senders_list.html" @@ -109,7 +130,7 @@ class DjrillSendersListView(DjrillAdminMedia, DjrillApiMixin, class DjrillTagListView(DjrillAdminMedia, DjrillApiMixin, - DjrillApiJsonObjectsMixin, TemplateView): + DjrillApiJsonObjectsMixin, TemplateView): api_uri = "tags/list.json" template_name = "djrill/tags_list.html" @@ -125,7 +146,7 @@ class DjrillTagListView(DjrillAdminMedia, DjrillApiMixin, class DjrillUrlListView(DjrillAdminMedia, DjrillApiMixin, - DjrillApiJsonObjectsMixin, TemplateView): + DjrillApiJsonObjectsMixin, TemplateView): api_uri = "urls/list.json" template_name = "djrill/urls_list.html" @@ -138,3 +159,20 @@ class DjrillUrlListView(DjrillAdminMedia, DjrillApiMixin, "media": self.media }) 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()