Skip to content

Allow FieldTracker on Django user model (and other children of abstract base classes) #331

@jcushman

Description

@jcushman

Problem

Here is a branch with failing tests that I think should work (and that I think worked in earlier versions?): master...jcushman:abstract-test-failure

This seems to be related to the stuff that @lucaswiman added in #317. @lucaswiman, I'm hoping based on that work you might have some clever idea for how to fix this. :)

Here's the situation: you have an abstract base class that defines a static attribute like is_active = True, and a concrete model inheriting from that class that defines a field like is_active = models.BooleanField(default=True). The model then throws an error on save():

from django.contrib.auth.models import AbstractUser

class MyUser(AbstractUser):
    tracker = FieldTracker()

MyUser().save()

Result:

    def __get__(self, instance, owner):
        if instance is None:
            return self
        was_deferred = self.field_name in instance.get_deferred_fields()
>       value = self.descriptor.__get__(instance, owner)
E       AttributeError: 'bool' object has no attribute '__get__'

model_utils/tracker.py:43: AttributeError

It would be great for this to work, because tracking changes on the Django user model is handy.

Debugging that error, I found the problem boils down to this:

class AbstractUser(models.Model):
    is_active = True

    class Meta:
        abstract = True

class MyUser(AbstractUser):
    is_active = models.BooleanField(default=True)
    tracker = FieldTracker()

MyUser().save()

The reason that fails is https://github.com/jazzband/django-model-utils/blob/master/model_utils/tracker.py#L218 :

descriptor = getattr(sender, field_name)
...
setattr(sender, field_name, wrapped_descriptor)

... which boils down to setting MyUser.is_active = DescriptorWrapper(MyUser.is_active). And that doesn't work because you expect MyUser.is_active to start with a value of DeferredAttribute('is_active'), but it actually returns True. For reasons I don't understand, when you override a static attribute on an abstract class, you get the base class's value back instead of the subclass's.

I tried tweaking tracker.py with variations on descriptor = getattr(sender, field_name) if field_name in sender.__dict__ else DeferredAttribute(field_name), but that broke foreign key fields and feels pretty janky anyway.

Any ideas on how to get this working?

Thanks!

Environment

  • Django Model Utils version: master
  • Django version: 2.1
  • Python version: 3.6
  • Other libraries used, if any:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions