diff --git a/.travis.yml b/.travis.yml index 7074b05..ca99f72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ python: - "3.6" env: - - DJANGO_VERSION="1.4" - DJANGO_VERSION="1.5" - DJANGO_VERSION="1.6" - DJANGO_VERSION="1.7" @@ -24,10 +23,6 @@ script: python runtests.py matrix: exclude: - - python: "3.3" - env: DJANGO_VERSION="1.4" - - python: "3.4" - env: DJANGO_VERSION="1.4" - python: "3.3" env: DJANGO_VERSION="1.9" - python: "3.3" @@ -36,8 +31,6 @@ matrix: env: DJANGO_VERSION="1.11" - python: "3.3" env: DJANGO_VERSION="2.0" - - python: "3.6" - env: DJANGO_VERSION="1.4" - python: "3.6" env: DJANGO_VERSION="1.5" - python: "3.6" diff --git a/AUTHORS.md b/AUTHORS.md index 8a55463..d8f337f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -9,4 +9,10 @@ various contributors: ## Patches and Suggestions -- You? \ No newline at end of file +- [Bryan Helmig](https://github.com/bryanhelmig) +- [Arnaud Limbourg](https://github.com/arnaudlimbourg) +- [tdruez](https://github.com/tdruez) +- [Maina Nick](https://github.com/mainanick) +- Jonathan Moss +- [Erik Wickstrom](https://github.com/erikcw) +- [Yaroslav Klyuyev](https://github.com/imposeren) diff --git a/README.md b/README.md index 99ce348..cb4bdce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![Travis CI Build](https://img.shields.io/travis/zapier/django-rest-hooks/master.svg)](https://travis-ci.org/zapier/django-rest-hooks) [![PyPI Download](https://img.shields.io/pypi/v/django-rest-hooks.svg)](https://pypi.python.org/pypi/django-rest-hooks) [![PyPI Status](https://img.shields.io/pypi/status/django-rest-hooks.svg)](https://pypi.python.org/pypi/django-rest-hooks) + ## What are Django REST Hooks? @@ -32,6 +33,53 @@ If you want to make a Django form or API resource, you'll need to do that yourse (though we've provided some example bits of code below). +### Changelog + +#### Version 1.6.0: + +Improvements: + +* Default handler of `raw_hook_event` uses the same logic as other handlers + (see "Backwards incompatible changes" for details). + +* Lookup of event_name by model+action_name now has a complexity of `O(1)` + instead of `O(len(settings.HOOK_EVENTS))` + +* `HOOK_CUSTOM_MODEL` is now similar to `AUTH_USER_MODEL`: must be of the form + `app_label.model_name` (for django 1.7+). If old value is of the form + `app_label.models.model_name` then it's automatically adapted. + +* `rest_hooks.models.Hook` is now really "swappable", so table creation is + skipped if you have different `settings.HOOK_CUSTOM_MODEL` + +* `rest_hooks.models.AbstractHook.deliver_hook` now accepts a callable as + `payload_override` argument (must accept 2 arguments: hook, instance). This + was added to support old behavior of `raw_custom_event`. + +Fixes: + +* HookAdmin.form now honors `settings.HOOK_CUSTOM_MODEL` + +* event_name determined from action+model is now consistent between runs (see + "Backwards incompatible changes") + +Backwards incompatible changes: + +* Dropped support for django 1.4 +* Custom `HOOK_FINDER`-s should accept and handle new argument `payload_override`. + Built-in finder `rest_hooks.utls.find_and_fire_hook` already does this. +* If several event names in `settings.HOOK_EVENTS` share the same + `'app_label.model.action'` (including `'app_label.model.action+'`) then + `django.core.exceptions.ImproperlyConfigured` is raised +* Receiver of `raw_hook_event` now uses the same logic as receivers of other + signals: checks event_name against settings.HOOK_EVENTS, verifies model (if + instance is passed), uses `HOOK_FINDER`. Old behaviour can be achieved by + using `trust_event_name=True`, or `instance=None` to fire a signal. +* If you have `settings.HOOK_CUSTOM_MODEL` of the form different than + `app_label.models.model_name` or `app_label.model_name`, then it must + be changed to `app_label.model_name`. + + ### Development Running the tests for Django REST Hooks is very easy, just: @@ -57,7 +105,7 @@ python runtests.py ### Requirements * Python 2 or 3 (tested on 2.7, 3.3, 3.4, 3.6) -* Django 1.4+ (tested on 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 1.11, 2.0) +* Django 1.5+ (tested on 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 1.11, 2.0) ### Installing & Configuring diff --git a/devrequirements_1.4.txt b/devrequirements_1.4.txt deleted file mode 100644 index 2eacbb7..0000000 --- a/devrequirements_1.4.txt +++ /dev/null @@ -1,2 +0,0 @@ --r devrequirements.txt -Django>=1.4.6,<1.5 diff --git a/devrequirements_1.8.txt b/devrequirements_1.8.txt index 9339166..b0d2223 100644 --- a/devrequirements_1.8.txt +++ b/devrequirements_1.8.txt @@ -1,3 +1,3 @@ -r devrequirements.txt -django-contrib-comments>=1.6.1 +django-contrib-comments>=1.6.1,<1.7.0 Django>=1.8,<1.9 diff --git a/devrequirements_1.9.txt b/devrequirements_1.9.txt index 7955cf9..f1c6347 100644 --- a/devrequirements_1.9.txt +++ b/devrequirements_1.9.txt @@ -1,3 +1,3 @@ -r devrequirements.txt -django-contrib-comments>=1.6.2 +django-contrib-comments>=1.6.2,<1.7.0 Django>=1.9,<1.10 diff --git a/rest_hooks/__init__.py b/rest_hooks/__init__.py index 64f67e9..ebb7498 100644 --- a/rest_hooks/__init__.py +++ b/rest_hooks/__init__.py @@ -1 +1 @@ -VERSION = (1, 5, 0) +VERSION = (1, 6, 0) diff --git a/rest_hooks/admin.py b/rest_hooks/admin.py index 075b412..8bcda98 100644 --- a/rest_hooks/admin.py +++ b/rest_hooks/admin.py @@ -1,34 +1,39 @@ from django.contrib import admin from django.conf import settings from django import forms -from rest_hooks.models import Hook +from rest_hooks.utils import get_hook_model - -HOOK_EVENTS = getattr(settings, 'HOOK_EVENTS', None) -if HOOK_EVENTS is None: +if getattr(settings, 'HOOK_EVENTS', None) is None: raise Exception("You need to define settings.HOOK_EVENTS!") +HookModel = get_hook_model() + + class HookForm(forms.ModelForm): """ Model form to handle registered events, asuring only events declared on HOOK_EVENTS settings can be registered. """ - ADMIN_EVENTS = [(x, x) for x in HOOK_EVENTS.keys()] class Meta: - model = Hook + model = HookModel fields = ['user', 'target', 'event'] def __init__(self, *args, **kwargs): super(HookForm, self).__init__(*args, **kwargs) - self.fields['event'] = forms.ChoiceField(choices=self.ADMIN_EVENTS) + self.fields['event'] = forms.ChoiceField(choices=self.get_admin_events()) + + @classmethod + def get_admin_events(cls): + return [(x, x) for x in getattr(settings, 'HOOK_EVENTS', None).keys()] class HookAdmin(admin.ModelAdmin): - list_display = [f.name for f in Hook._meta.fields] + list_display = [f.name for f in HookModel._meta.fields] raw_id_fields = ['user', ] form = HookForm -admin.site.register(Hook, HookAdmin) + +admin.site.register(HookModel, HookAdmin) diff --git a/rest_hooks/migrations/0001_initial.py b/rest_hooks/migrations/0001_initial.py index 761c4f9..86de2ca 100644 --- a/rest_hooks/migrations/0001_initial.py +++ b/rest_hooks/migrations/0001_initial.py @@ -24,6 +24,7 @@ class Migration(migrations.Migration): ('user', models.ForeignKey(related_name='hooks', to=settings.AUTH_USER_MODEL, on_delete=django.db.models.deletion.CASCADE)), ], options={ + 'swappable': 'HOOK_CUSTOM_MODEL', }, bases=(models.Model,), ), diff --git a/rest_hooks/migrations/0002_swappable_hook_model.py b/rest_hooks/migrations/0002_swappable_hook_model.py new file mode 100644 index 0000000..00d418f --- /dev/null +++ b/rest_hooks/migrations/0002_swappable_hook_model.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rest_hooks', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='Hook', + options={ + 'swappable': 'HOOK_CUSTOM_MODEL', + }, + ), + ] diff --git a/rest_hooks/models.py b/rest_hooks/models.py index b441182..7f441c7 100644 --- a/rest_hooks/models.py +++ b/rest_hooks/models.py @@ -2,11 +2,15 @@ import requests -from django.core.exceptions import ValidationError +import django from django.conf import settings from django.core import serializers +from django.core.exceptions import ValidationError, ImproperlyConfigured from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from django.db.models.signals import post_save, post_delete +from django.test.signals import setting_changed +from django.dispatch import receiver try: # Django <= 1.6 backwards compatibility @@ -15,15 +19,44 @@ # Django >= 1.7 import json -from rest_hooks.utils import get_module, find_and_fire_hook, distill_model_event, get_hook_model +from rest_hooks.signals import hook_event, raw_hook_event, hook_sent_event +from rest_hooks.utils import distill_model_event, get_hook_model, get_module, find_and_fire_hook -from rest_hooks import signals +if getattr(settings, 'HOOK_CUSTOM_MODEL', None) is None: + settings.HOOK_CUSTOM_MODEL = 'rest_hooks.Hook' HOOK_EVENTS = getattr(settings, 'HOOK_EVENTS', None) if HOOK_EVENTS is None: raise Exception('You need to define settings.HOOK_EVENTS!') +_HOOK_EVENT_ACTIONS_CONFIG = None + + +def get_event_actions_config(): + global _HOOK_EVENT_ACTIONS_CONFIG + if _HOOK_EVENT_ACTIONS_CONFIG is None: + _HOOK_EVENT_ACTIONS_CONFIG = {} + for event_name, auto in HOOK_EVENTS.items(): + if not auto: + continue + model_label, action = auto.rsplit('.', 1) + action_parts = action.rsplit('+', 1) + action = action_parts[0] + ignore_user_override = False + if len(action_parts) == 2: + ignore_user_override = True + + model_config = _HOOK_EVENT_ACTIONS_CONFIG.setdefault(model_label, {}) + if action in model_config: + raise ImproperlyConfigured( + "settings.HOOK_EVENTS have a dublicate {action} for model " + "{model_label}".format(action=action, model_label=model_label) + ) + model_config[action] = (event_name, ignore_user_override,) + return _HOOK_EVENT_ACTIONS_CONFIG + + if getattr(settings, 'HOOK_THREADING', True): from rest_hooks.client import Client client = Client() @@ -91,8 +124,21 @@ def deliver_hook(self, instance, payload_override=None): Deliver the payload to the target URL. By default it serializes to JSON and POSTs. + + Args: + instance: instance that triggered event. + payload_override: JSON-serializable object or callable that will + return such object. If callable is used it should accept 2 + arguments: `hook` and `instance`. """ - payload = payload_override or self.serialize_hook(instance) + if payload_override is None: + payload = self.serialize_hook(instance) + else: + payload = payload_override + + if callable(payload): + payload = payload(self, instance) + if getattr(settings, 'HOOK_DELIVERER', None): deliverer = get_module(settings.HOOK_DELIVERER) deliverer(self.target, payload, instance=instance, hook=self) @@ -103,7 +149,7 @@ def deliver_hook(self, instance, payload_override=None): headers={'Content-Type': 'application/json'} ) - signals.hook_sent_event.send_robust(sender=self.__class__, payload=payload, instance=instance, hook=self) + hook_sent_event.send_robust(sender=self.__class__, payload=payload, instance=instance, hook=self) return None def __unicode__(self): @@ -111,20 +157,25 @@ def __unicode__(self): class Hook(AbstractHook): - pass + if django.VERSION >= (1, 7): + class Meta(AbstractHook.Meta): + swappable = 'HOOK_CUSTOM_MODEL' + ############## ### EVENTS ### ############## -from django.db.models.signals import post_save, post_delete -from django.dispatch import receiver - -from rest_hooks.signals import hook_event, raw_hook_event - -get_opts = lambda m: m._meta.concrete_model._meta +def get_model_label(instance): + if instance is None: + return None + opts = instance._meta.concrete_model._meta + try: + return opts.label + except AttributeError: + return '.'.join([opts.app_label, opts.object_name]) @receiver(post_save, dispatch_uid='instance-saved-hook') @@ -136,10 +187,9 @@ def model_saved(sender, instance, """ Automatically triggers "created" and "updated" actions. """ - opts = get_opts(instance) - model = '.'.join([opts.app_label, opts.object_name]) + model_label = get_model_label(instance) action = 'created' if created else 'updated' - distill_model_event(instance, model, action) + distill_model_event(instance, model_label, action) @receiver(post_delete, dispatch_uid='instance-deleted-hook') @@ -149,9 +199,8 @@ def model_deleted(sender, instance, """ Automatically triggers "deleted" actions. """ - opts = get_opts(instance) - model = '.'.join([opts.app_label, opts.object_name]) - distill_model_event(instance, model, 'deleted') + model_label = get_model_label(instance) + distill_model_event(instance, model_label, 'deleted') @receiver(hook_event, dispatch_uid='instance-custom-hook') @@ -162,30 +211,49 @@ def custom_action(sender, action, """ Manually trigger a custom action (or even a standard action). """ - opts = get_opts(instance) - model = '.'.join([opts.app_label, opts.object_name]) - distill_model_event(instance, model, action, user_override=user) + model_label = get_model_label(instance) + distill_model_event(instance, model_label, action, user_override=user) @receiver(raw_hook_event, dispatch_uid='raw-custom-hook') -def raw_custom_event(sender, event_name, - payload, - user, - send_hook_meta=True, - instance=None, - **kwargs): +def raw_custom_event( + sender, + event_name, + payload, + user, + send_hook_meta=True, + instance=None, + trust_event_name=False, + **kwargs + ): """ Give a full payload """ - HookModel = get_hook_model() - hooks = HookModel.objects.filter(user=user, event=event_name) - - for hook in hooks: - new_payload = payload - if send_hook_meta: - new_payload = { - 'hook': hook.dict(), - 'data': payload - } - - hook.deliver_hook(instance, payload_override=new_payload) + model_label = get_model_label(instance) + + new_payload = payload + + if send_hook_meta: + new_payload = lambda hook, instance: { + 'hook': hook.dict(), + 'data': payload + } + + distill_model_event( + instance, + model_label, + None, + user_override=user, + event_name=event_name, + trust_event_name=trust_event_name, + payload_override=new_payload, + ) + + +@receiver(setting_changed) +def handle_hook_events_change(sender, setting, *args, **kwargs): + global _HOOK_EVENT_ACTIONS_CONFIG + global HOOK_EVENTS + if setting == 'HOOK_EVENTS': + _HOOK_EVENT_ACTIONS_CONFIG = None + HOOK_EVENTS = settings.HOOK_EVENTS diff --git a/rest_hooks/tests.py b/rest_hooks/tests.py index b940049..d55d5dc 100644 --- a/rest_hooks/tests.py +++ b/rest_hooks/tests.py @@ -11,27 +11,38 @@ # Django >= 1.7 import json -from django.conf import settings from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.test import TestCase +from django.test.utils import override_settings try: from django.contrib.comments.models import Comment comments_app_label = 'comments' except ImportError: from django_comments.models import Comment comments_app_label = 'django_comments' -from django.contrib.sites.models import Site -from django.test import TestCase from rest_hooks import models -Hook = models.Hook - from rest_hooks import signals from rest_hooks.admin import HookForm +Hook = models.Hook + urlpatterns = [] +HOOK_EVENTS_OVERRIDE = { + 'comment.added': comments_app_label + '.Comment.created', + 'comment.changed': comments_app_label + '.Comment.updated', + 'comment.removed': comments_app_label + '.Comment.deleted', + 'comment.moderated': comments_app_label + '.Comment.moderated', + 'special.thing': None, +} +ALT_HOOK_EVENTS = dict(HOOK_EVENTS_OVERRIDE) +ALT_HOOK_EVENTS['comment.moderated'] += '+' + +@override_settings(HOOK_EVENTS=HOOK_EVENTS_OVERRIDE, HOOK_DELIVERER=None) class RESTHooksTest(TestCase): """ This test Class uses real HTTP calls to a requestbin service, making it easy @@ -43,29 +54,11 @@ class RESTHooksTest(TestCase): ############# def setUp(self): - self.HOOK_EVENTS = getattr(settings, 'HOOK_EVENTS', None) - self.HOOK_DELIVERER = getattr(settings, 'HOOK_DELIVERER', None) self.client = requests # force non-async for test cases self.user = User.objects.create_user('bob', 'bob@example.com', 'password') self.site, created = Site.objects.get_or_create(domain='example.com', name='example.com') - models.HOOK_EVENTS = { - 'comment.added': comments_app_label + '.Comment.created', - 'comment.changed': comments_app_label + '.Comment.updated', - 'comment.removed': comments_app_label + '.Comment.deleted', - 'comment.moderated': comments_app_label + '.Comment.moderated', - 'special.thing': None - } - - HookForm.ADMIN_EVENTS = [(x, x) for x in models.HOOK_EVENTS.keys()] - settings.HOOK_DELIVERER = None - - def tearDown(self): - HookForm.ADMIN_EVENTS = [(x, x) for x in self.HOOK_EVENTS.keys()] - models.HOOK_EVENTS = self.HOOK_EVENTS - settings.HOOK_DELIVERER = self.HOOK_DELIVERER - def make_hook(self, event, target): return Hook.objects.create( user=self.user, @@ -77,6 +70,20 @@ def make_hook(self, event, target): ### TESTS ### ############# + @override_settings(HOOK_EVENTS=ALT_HOOK_EVENTS) + def test_get_event_actions_config(self): + self.assertEquals( + models.get_event_actions_config(), + { + comments_app_label + '.Comment': { + 'created': ('comment.added', False), + 'updated': ('comment.changed', False), + 'deleted': ('comment.removed', False), + 'moderated': ('comment.moderated', True), + }, + } + ) + def test_no_user_property_fail(self): with self.assertRaises(Exception): models.find_and_fire_hook('some.fake.event', self.user) @@ -283,7 +290,7 @@ def test_valid_form(self): form_data = { 'user': self.user.id, 'target': "http://example.com", - 'event': HookForm.ADMIN_EVENTS[0][0] + 'event': HookForm.get_admin_events()[0][0] } form = HookForm(data=form_data) self.assertTrue(form.is_valid()) @@ -292,7 +299,7 @@ def test_form_save(self): form_data = { 'user': self.user.id, 'target': "http://example.com", - 'event': HookForm.ADMIN_EVENTS[0][0] + 'event': HookForm.get_admin_events()[0][0] } form = HookForm(data=form_data) @@ -304,10 +311,10 @@ def test_invalid_form(self): form = HookForm(data={}) self.assertFalse(form.is_valid()) + @override_settings(HOOK_CUSTOM_MODEL='rest_hooks.models.Hook') def test_get_custom_hook_model(self): # Using the default Hook model just to exercise get_hook_model's # lookup machinery. - settings.HOOK_CUSTOM_MODEL = 'rest_hooks.models.Hook' from rest_hooks.utils import get_hook_model from rest_hooks.models import AbstractHook HookModel = get_hook_model() diff --git a/rest_hooks/utils.py b/rest_hooks/utils.py index f8b1419..d6dd371 100644 --- a/rest_hooks/utils.py +++ b/rest_hooks/utils.py @@ -1,5 +1,17 @@ +import django + +try: + from django.apps import apps as django_apps +except ImportError: + django_apps = None +from django.core.exceptions import ImproperlyConfigured from django.conf import settings +if django.VERSION >= (2, 0,): + get_model_kwargs = {'require_ready': False} +else: + get_model_kwargs = {} + def get_module(path): """ @@ -29,19 +41,39 @@ def get_module(path): return func + def get_hook_model(): """ Returns the Custom Hook model if defined in settings, otherwise the default Hook model. """ - from rest_hooks.models import Hook - HookModel = Hook - if getattr(settings, 'HOOK_CUSTOM_MODEL', None): - HookModel = get_module(settings.HOOK_CUSTOM_MODEL) - return HookModel + model_label = getattr(settings, 'HOOK_CUSTOM_MODEL', None) + if django_apps: + model_label = (model_label or 'rest_hooks.Hook').replace('.models.', '.') + try: + return django_apps.get_model(model_label, **get_model_kwargs) + except ValueError: + raise ImproperlyConfigured("HOOK_CUSTOM_MODEL must be of the form 'app_label.model_name'") + except LookupError: + raise ImproperlyConfigured( + "HOOK_CUSTOM_MODEL refers to model '%s' that has not been installed" % model_label + ) + else: + if model_label in (None, 'rest_hooks.Hook'): + from rest_hooks.models import Hook + HookModel = Hook + else: + try: + HookModel = get_module(settings.HOOK_CUSTOM_MODEL) + except ImportError: + raise ImproperlyConfigured( + "HOOK_CUSTOM_MODEL refers to model '%s' that cannot be imported" % model_label + ) + return HookModel -def find_and_fire_hook(event_name, instance, user_override=None): + +def find_and_fire_hook(event_name, instance, user_override=None, payload_override=None): """ Look up Hooks that apply """ @@ -52,7 +84,7 @@ def find_and_fire_hook(event_name, instance, user_override=None): from django.contrib.auth.models import User from rest_hooks.models import HOOK_EVENTS - if not event_name in HOOK_EVENTS.keys(): + if event_name not in HOOK_EVENTS.keys(): raise Exception( '"{}" does not exist in `settings.HOOK_EVENTS`.'.format(event_name) ) @@ -81,32 +113,67 @@ def find_and_fire_hook(event_name, instance, user_override=None): hooks = HookModel.objects.filter(**filters) for hook in hooks: - hook.deliver_hook(instance) + hook.deliver_hook(instance, payload_override=payload_override) + + +def distill_model_event( + instance, + model=False, + action=False, + user_override=None, + event_name=False, + trust_event_name=False, + payload_override=None, + ): + """ + Take `event_name` or determine it using action and model + from settings.HOOK_EVENTS, and let hooks fly. + if `event_name` is passed together with `model` or `action`, then + they should be the same as in settings or `trust_event_name` should be + `True` -def distill_model_event(instance, model, action, user_override=None): - """ - Take created, updated and deleted actions for built-in - app/model mappings, convert to the defined event.name - and let hooks fly. + If event_name is not found or is invalidated, then just quit silently. + + If payload_override is passed, then it will be passed into HookModel.deliver_hook - If that model isn't represented, we just quit silenty. """ - from rest_hooks.models import HOOK_EVENTS + from rest_hooks.models import get_event_actions_config, HOOK_EVENTS + + if event_name is False and (model is False or action is False): + raise TypeError( + 'distill_model_event() requires either `event_name` argument or ' + 'both `model` and `action` arguments.' + ) + if event_name: + if trust_event_name: + pass + elif event_name in HOOK_EVENTS: + auto = HOOK_EVENTS[event_name] + if auto: + allowed_model, allowed_action = auto.rsplit('.', 1) + + allowed_action_parts = allowed_action.rsplit('+', 1) + allowed_action = allowed_action_parts[0] - event_name = None - for maybe_event_name, auto in HOOK_EVENTS.items(): - if auto: - # break auto into App.Model, Action - maybe_model, maybe_action = auto.rsplit('.', 1) - maybe_action = maybe_action.rsplit('+', 1) - if model == maybe_model and action == maybe_action[0]: - event_name = maybe_event_name - if len(maybe_action) == 2: + model = model or allowed_model + action = action or allowed_action + + if not (model == allowed_model and action == allowed_action): + event_name = None + + if len(allowed_action_parts) == 2: user_override = False + else: + event_actions_config = get_event_actions_config() + + event_name, ignore_user_override = event_actions_config.get(model, {}).get(action, (None, False)) + if ignore_user_override: + user_override = False if event_name: - finder = find_and_fire_hook if getattr(settings, 'HOOK_FINDER', None): finder = get_module(settings.HOOK_FINDER) - finder(event_name, instance, user_override=user_override) + else: + finder = find_and_fire_hook + finder(event_name, instance, user_override=user_override, payload_override=payload_override) diff --git a/setup.py b/setup.py index 733ba11..b103f45 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ author = 'Bryan Helmig', author_email = 'bryan@zapier.com', url = 'http://github.com/zapier/django-rest-hooks', - install_requires=['Django>=1.4','requests'], + install_requires=['Django>=1.5', 'requests'], packages=['rest_hooks'], package_data={ 'rest_hooks': [