KristianOellegaard/django-hvad — pull request #139 by spectras (2014-05-07 11:46:05+00:00)
Title: Hvad sends post_save signal too early, breaks Haystack's RealtimeSignalProcessor

Hvad doesn't work well with Haystack's `RealtimeSignalProcessor`: when you try to create a new translated object in the admin, it fires the `post_save` signal as soon as the shared model is saved, before the translations are saved.

If you have SearchIndexes on any translated field, Haystack will try to access them to build the indexable representation of the object, and fail with a DoesNotExist exception:

.ve/local/lib/python2.7/site-packages/django/contrib/admin/options.py:990: in add_view
> new_object = self.save_form(request, form, change=False)
.ve/local/lib/python2.7/site-packages/django/contrib/admin/options.py:734: in save_form
> return form.save(commit=False)
.ve/local/lib/python2.7/site-packages/hvad/forms.py:160: in save
> super(TranslatableModelForm, self).save(True)
.ve/local/lib/python2.7/site-packages/django/forms/models.py:370: in save
> fail_message, commit, construct=False)
.ve/local/lib/python2.7/site-packages/django/forms/models.py:87: in save_instance
> instance.save()
.ve/local/lib/python2.7/site-packages/django/db/models/base.py:546: in save
> force_update=force_update, update_fields=update_fields)
.ve/local/lib/python2.7/site-packages/django/db/models/base.py:664: in save_base
> update_fields=update_fields, raw=raw, using=using)
.ve/local/lib/python2.7/site-packages/django/dispatch/dispatcher.py:170: in send
> response = receiver(signal=self, sender=sender, **named)
.ve/local/lib/python2.7/site-packages/haystack/signals.py:48: in handle_save
> index.update_object(instance, using=using)
.ve/local/lib/python2.7/site-packages/haystack/indexes.py:274: in update_object
> backend.update(self, [instance])
.ve/local/lib/python2.7/site-packages/haystack/backends/whoosh_backend.py:179: in update
> doc = index.full_prepare(obj)
.ve/local/lib/python2.7/site-packages/haystack/indexes.py:204: in full_prepare
> self.prepared_data = self.prepare(obj)
.ve/local/lib/python2.7/site-packages/haystack/indexes.py:195: in prepare
> self.prepared_data[field.index_fieldname] = field.prepare(obj)
.ve/local/lib/python2.7/site-packages/haystack/fields.py:154: in prepare
> return self.convert(super(CharField, self).prepare(obj))
.ve/local/lib/python2.7/site-packages/haystack/fields.py:83: in prepare
> raise SearchFieldError("The model '%s' does not have a model_attr '%s'." % (repr(obj), attr))
.ve/local/lib/python2.7/site-packages/django/db/models/base.py:421: in __repr__
> u = six.text_type(self)
news/models.py:64: in __unicode__
> return unicode(self.title)
.ve/local/lib/python2.7/site-packages/hvad/descriptors.py:35: in __get__
> return getattr(self.translation(instance), self.name)
.ve/local/lib/python2.7/site-packages/hvad/descriptors.py:15: in translation
> cached = get_translation(instance)
.ve/local/lib/python2.7/site-packages/hvad/utils.py:30: in get_translation
> return accessor.get(language_code=language_code)
.ve/local/lib/python2.7/site-packages/django/db/models/manager.py:143: in get
> return self.get_query_set().get(*args, **kwargs)
.ve/local/lib/python2.7/site-packages/django/utils/functional.py:15: in _curried
> return _curried_func(*(args+moreargs), **dict(kwargs, **morekwargs))
aptivate_monkeypatch/monkeypatch.py:280: in wrapper_with_patch
> return external_patch_function(original_function, *args, **kwargs)
aptivate_monkeypatches/monkeypatches.py:133: in queryset_get_with_exception_detail
> return original_function(self, *args, **kwargs)
.ve/local/lib/python2.7/site-packages/django/db/models/query.py:404: in get
> self.model._meta.object_name)
E DoesNotExist: NewsItemTranslation matching query does not exist. (query was: (), {'language_code': u'en'})

The cleanest solution appears to be modifying `TranslatableAdmin` to delay sending the `post_save` signal until after the translations are saved. I did that with this custom subclass of `TranslatableAdmin`:

from hvad.forms import TranslatableModelForm
class SmartTranslatableModelForm(TranslatableModelForm):
def save(self, commit=True):
from django.db.models.signals import post_save
old_receivers = post_save.receivers
received_events = []

def capture_events(signal, sender, **kwargs):
received_events.append([sender, kwargs])

try:
post_save.receivers = []
post_save.connect(capture_events)
result = super(SmartTranslatableModelForm, self).save(commit)
finally:
post_save.receivers = old_receivers

for sender, kwargs in received_events:
post_save.send(sender, **kwargs)

return result

from hvad.admin import TranslatableAdmin
class SmartTranslatableAdmin(TranslatableAdmin):
form = SmartTranslatableModelForm

You could easily write a test for this by listening for a `post_save` signal and trying to access a translated field of the instance from the signal handler.