jpwatts / django-positions

A Django field for custom model ordering.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Django 1.10 error on add new instance

MikeVL opened this issue · comments

I have model with PositionField. On create instance i got error:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/django/db/models/base.py", line 796, in save
    force_update=force_update, update_fields=update_fields)
  File "/usr/local/lib/python2.7/dist-packages/django/db/models/base.py", line 824, in save_base
    updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
  File "/usr/local/lib/python2.7/dist-packages/django/db/models/base.py", line 880, in _save_table
    raise ValueError("Cannot force an update in save() with no primary key.")
ValueError: Cannot force an update in save() with no primary key.

Confirm, same here

Have the same. I have spend some time to debug the problem, and I know what causes the issue, but cannot figure out how to fix it. So I will share what I discovered.

Everything breaks because of the change in implementation of get_deferred_fields method, in Django Model class (django.db.models.Model). This method is called from save() method of Model class see here.

It used to be like that (Django 1.9):
Source

    def get_deferred_fields(self):
        """
        Returns a set containing names of deferred fields for this instance.
        """
        return {
            f.attname for f in self._meta.concrete_fields
            if isinstance(self.__class__.__dict__.get(f.attname), DeferredAttribute)
        }

But now it is (Django 1.10):
Source

    def get_deferred_fields(self):
        """
        Returns a set containing names of deferred fields for this instance.
        """
        return {
            f.attname for f in self._meta.concrete_fields
            if f.attname not in self.__dict__
        }

The problem is that self.__dict__ does not contain our PositionField attribute, what causes the position attribute to be treated as deferred. Instead the self.__dict__ contains position attribute cache (deeper understanding of PositionField is needed).

Going a little bit deeper brought me to contribute_to_class method of PositionField class and one particular line setattr(cls, self.name, self):
Source

    def contribute_to_class(self, cls, name):
        super(PositionField, self).contribute_to_class(cls, name)
        for constraint in cls._meta.unique_together:
            if self.name in constraint:
                raise TypeError("%s can't be part of a unique constraint." % self.__class__.__name__)
        self.auto_now_fields = []
        for field in cls._meta.fields:
            if getattr(field, 'auto_now', False):
                self.auto_now_fields.append(field)
        setattr(cls, self.name, self)
        pre_delete.connect(self.prepare_delete, sender=cls)
        post_delete.connect(self.update_on_delete, sender=cls)
        post_save.connect(self.update_on_save, sender=cls)

That line causes the position attribute to be missing from self.__dict__ while checking for deferred fields. But this line also somehow sets the position attribute cache field, which is required for PositionField to work. The self.name is carries the attribute name of PositionFiled as we define it in the class like so:

    position = PositionField()

The self.name will contain the 'position' string.
(Not sure how it set's the cache attribute, if all it does it sets the position attribute #confused)

Few last things I came along:
As the documentation states there is a way that class attributes differ from the __dict__

See section Implementing Descriptors for another way in which attributes of a class retrieved via its instances may differ from the objects actually stored in the class’s __dict__.

Source, look for "Class instances" section.
But I don't really know how it work and if it is relevant in this case.

A workaround for that can be very simple, just override the get_deferred_fields in your class and make sure your position attribute is never deferred. But it is not a good way to solve this problem:

    def get_deferred_fields(self):
        deferred_set = super().get_deferred_fields()
        return {f for f in deferred_set if f != 'position'}

Assuming the position attribute in model class is called position.

New deferred logic indeed looks strange. Added workaround for that. Tested locally.

Hi, I had a similar error just by overriding save method and overriding get_deferred_fields worked as a workaround. So thanks! Have you considered reporting that to Django?

I faced the same issue and also for me overriding get_deferred_fields worked! Thanks a lot @akszydelko :)

Thank you @akszydelko this helped us immensely