Search code examples
pythondjangodjango-modelsdjango-signals

Using Django Signals post_save error "save() prohibited to prevent data loss to do unsave related object ''myModel."


Python 3.6.5 & Django 1.11

Pseudo explanation:

  • Two Models, with model2 using model1 as a ForeignKey
  • Admin for model1 uses an inline for model2
  • On model1, I want to get a shortened URL and store that in a property when there is a new entry. To do that, model1 has to be saved because the long URL (the one I want to shorten) is based on Autoslug.

Relevant portion of Models.py

class Model1(models.Model):
    name = models.CharField(max_length=100)
    slug = AutoSlugField(populate_from='name', unique=True,
                     null=True, default=None)


class Model2(models.Model):    
    owner = models.ForeignKey(
        'Model1',
        on_delete=models.CASCADE,
    )

This is my signal...

@receiver(post_save, sender=Model1)
def shorten_url(sender, instance, created, **kwargs):
    if created:
        if not instance.short_url:
            url_short = url_shorten(instance.get_absolute_url())
            instance.short_url = url_short
            post_save.disconnect(shorten_url, sender=sender)
            instance.save()
            post_save.connect(shorten_url, sender=sender)
            return instance
        else:
            post_save.disconnect(shorten_url, sender=sender)
            instance.save()
            post_save.connect(shorten_url, sender=sender)
            return instance

The disconnect, connect is to prevent loops, I'm not 100% sure that it is correct yet either, but without it, I see calls to shorten the url over and over...

The issue I'm trying to solve now is that I get the error save() prohibited to prevent data loss to unsaved related object 'model1' when I am calling save().

Here is the traceback:

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/core/handlers/exception.py" in inner
41.             response = get_response(request)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/core/handlers/base.py" in _legacy_get_response
249.             response = self._get_response(request)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
187.                 response = self.process_exception_by_middleware(e, request)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
185.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/contrib/admin/options.py" in wrapper
551.                 return self.admin_site.admin_view(view)(*args, **kwargs)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/utils/decorators.py" in _wrapped_view
149.                     response = view_func(request, *args, **kwargs)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/views/decorators/cache.py" in _wrapped_view_func
57.         response = view_func(request, *args, **kwargs)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/contrib/admin/sites.py" in inner
224.             return view(request, *args, **kwargs)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/contrib/admin/options.py" in add_view
1508.         return self.changeform_view(request, None, form_url, extra_context)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/utils/decorators.py" in _wrapper
67.             return bound_func(*args, **kwargs)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/utils/decorators.py" in _wrapped_view
149.                     response = view_func(request, *args, **kwargs)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/utils/decorators.py" in bound_func
63.                 return func.__get__(self, type(self))(*args2, **kwargs2)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/contrib/admin/options.py" in changeform_view
1408.             return self._changeform_view(request, object_id, form_url, extra_context)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/contrib/admin/options.py" in _changeform_view
1449.                 self.save_related(request, form, formsets, not add)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/contrib/admin/options.py" in save_related
1003.             self.save_formset(request, form, formset, change=change)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/contrib/admin/options.py" in save_formset
991.         formset.save()

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/forms/models.py" in save
649.         return self.save_existing_objects(commit) + self.save_new_objects(commit)

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/forms/models.py" in save_new_objects
783.             self.new_objects.append(self.save_new(form, commit=commit))

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/forms/models.py" in save_new
927.             obj.save()

File "/home/username/.virtualenvs/project/lib/python3.6/site-packages/django/db/models/base.py" in save
762.                         "unsaved related object '%s'." % field.name

Exception Type: ValueError at /admin/wa/model1/add/
Exception Value: save() prohibited to prevent data loss due to unsaved related object 'model1'.

I assume that this is because I'm post_save on model1 and I have model2 rows waiting for a model1 key the error isn't saying that. If the error said model2 that would make more sense to me, but it doesn't.

In looking at base.py line 762 which is raising the error, it also appears to be handling this case of an unsaved (no PK) object but again.. I'm post_save here aren't I?

Another Update

In attempting to break this problem down further, I submitted the admin form with no data in the inline form section (i.e. no data in Model2). When I do this, I get two entries in Model1 each with NO PK... PK==None. wow.

Thanks for the help!


Solution

  • For the future... The key point to note was when I discovered that the ID was not being generated and if you hit that you should think... what else has changed recently.

    In my case, a few days ago on my dev box I had changed from SQLITE3 to PostgreSQL in anticipation of doing the same in production. However, my procedure was flawed and what happed was I had a perfectly working READ database, but none of the IDs were autogenerating on write.

    In my case I had used PGLOADER to load the data. Do not do that. Use the Django tools to dumpdata and loaddata.