Django 3 Object Evaluation error due to from_db_value()

Viewed 1006

I am attempting to update a project to Django 3 and I am running into a strange errors evaluating objects stating that:

TypeError: from_db_value() missing 1 required positional argument: 'context'

I read in the Django docs here: https://docs.djangoproject.com/en/3.0/releases/3.0/#features-removed-in-3-0

Support for the context argument of Field.from_db_value() and Expression.convert_value() is removed.

But I am not understanding what I need to do to fix this, as my call is simply this to get the error...

apps = Application.objects.filter(completed=False, canceled=False)
for app in apps:
    print(app)

Is there something I am not getting?

Here is the full traceback, it seems to give me nothing to work with

Traceback (most recent call last):
web_1       |   File "/usr/local/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
web_1       |     response = get_response(request)
web_1       |   File "/usr/local/lib/python3.7/site-packages/django/core/handlers/base.py", line 115, in _get_response
web_1       |     response = self.process_exception_by_middleware(e, request)
web_1       |   File "/usr/local/lib/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
web_1       |     response = wrapped_callback(request, *callback_args, **callback_kwargs)
web_1       |   File "/code/apps/users/decorators.py", line 23, in _wrapped_view
web_1       |     return view_func(request, *args, **kwargs)
web_1       |   File "/code/apps/users/decorators.py", line 23, in _wrapped_view
web_1       |     return view_func(request, *args, **kwargs)
web_1       |   File "/code/apps/reports/views/property_specific/availability_views.py", line 26, in availability_units_report
web_1       |     for app in apps:
web_1       |   File "/usr/local/lib/python3.7/site-packages/django/db/models/query.py", line 276, in __iter__
web_1       |     self._fetch_all()
web_1       |   File "/usr/local/lib/python3.7/site-packages/django/db/models/query.py", line 1261, in _fetch_all
web_1       |     self._result_cache = list(self._iterable_class(self))
web_1       |   File "/usr/local/lib/python3.7/site-packages/django/db/models/query.py", line 74, in __iter__
web_1       |     for row in compiler.results_iter(results):
web_1       |   File "/usr/local/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 1095, in apply_converters
web_1       |     value = converter(value, expression, connection)
web_1       | TypeError: from_db_value() missing 1 required positional argument: 'context'

Update Here is my associated Application Model if need be, although I see nothing in the 3.0 docs that I am declaring wrongly:

class Application(SafeDeleteModel, TimestampModel, UUID):
    class Meta:
        permissions = (
            ('view_ssn', 'Can view ssn'),
            ('change_ssn', 'Can change ssn'),
            ('waive_app_fee', 'Can waive app fee'),
            ('change_processing_management', 'Can change processing for management'),
            ('change_processing_compliance', 'Can change processing for compliance'),
            ('change_preleasing', 'Can change preleasing'),
            ('cancel_application', 'Can cancel application'),
            ('occupy_unit', 'Can occupy a unit'),
        )

    id = models.AutoField(primary_key=True)

    ...
    # I have no __str__ method for this model

Here is the inherited SafeDelete Model, built using django-safedelete https://github.com/makinacorpus/django-safedelete

from safedelete.models import SafeDeleteModel as BaseModel

class SafeDeleteModel(BaseModel):
    _safedelete_policy = SOFT_DELETE_CASCADE

    class Meta:
        abstract = True
        default_permissions = ('add', 'change', 'delete', 'view', 'undelete')

    def exclude(self, value):
        Logger.info(self)

    def update(self, new_dict):
        """
        Update object with form field dictionary on submission.
        :param new_dict:
        """
        fields = [x.name for x in self._meta.get_fields()]
        for key, value in new_dict.items():
            if key in fields:
                setattr(self, key, value)

My timestamp and UUID inherited models simply add UUID and Timestamp fields to my models in a more universal manner, there is no need to show them.

Here is BaseModel, but it just comes from the SafeDeleteModel so I could add a tiny bit more functionality.

class SafeDeleteModel(models.Model):
    """Abstract safedelete-ready model.

    .. note::
        To create your safedelete-ready models, you have to make them inherit from this model.

    :attribute deleted:
        DateTimeField set to the moment the object was deleted. Is set to
        ``None`` if the object has not been deleted.

    :attribute _safedelete_policy: define what happens when you delete an object.
        It can be one of ``HARD_DELETE``, ``SOFT_DELETE``, ``SOFT_DELETE_CASCADE``, ``NO_DELETE`` and ``HARD_DELETE_NOCASCADE``.
        Defaults to ``SOFT_DELETE``.

        >>> class MyModel(SafeDeleteModel):
        ...     _safedelete_policy = SOFT_DELETE
        ...     my_field = models.TextField()
        ...
        >>> # Now you have your model (with its ``deleted`` field, and custom manager and delete method)

    :attribute objects:
        The :class:`safedelete.managers.SafeDeleteManager` that returns the non-deleted models.

    :attribute all_objects:
        The :class:`safedelete.managers.SafeDeleteAllManager` that returns the all models (non-deleted and soft-deleted).

    :attribute deleted_objects:
        The :class:`safedelete.managers.SafeDeleteDeletedManager` that returns the soft-deleted models.
    """

    _safedelete_policy = SOFT_DELETE

    deleted = models.DateTimeField(editable=False, null=True)

    objects = SafeDeleteManager()
    all_objects = SafeDeleteAllManager()
    deleted_objects = SafeDeleteDeletedManager()

    class Meta:
        abstract = True

    def save(self, keep_deleted=False, **kwargs):
        """Save an object, un-deleting it if it was deleted.

        Args:
            keep_deleted: Do not undelete the model if soft-deleted. (default: {False})
            kwargs: Passed onto :func:`save`.

        .. note::
            Undeletes soft-deleted models by default.
        """

        # undelete signal has to happen here (and not in undelete)
        # in order to catch the case where a deleted model becomes
        # implicitly undeleted on-save.  If someone manually nulls out
        # deleted, it'll bypass this logic, which I think is fine, because
        # otherwise we'd have to shadow field changes to handle that case.

        was_undeleted = False
        if not keep_deleted:
            if self.deleted and self.pk:
                was_undeleted = True
            self.deleted = None

        super(SafeDeleteModel, self).save(**kwargs)

        if was_undeleted:
            # send undelete signal
            using = kwargs.get('using') or router.db_for_write(self.__class__, instance=self)
            post_undelete.send(sender=self.__class__, instance=self, using=using)

    def undelete(self, force_policy=None, **kwargs):
        """Undelete a soft-deleted model.

        Args:
            force_policy: Force a specific undelete policy. (default: {None})
            kwargs: Passed onto :func:`save`.

        .. note::
            Will raise a :class:`AssertionError` if the model was not soft-deleted.
        """
        current_policy = force_policy or self._safedelete_policy

        assert self.deleted
        self.save(keep_deleted=False, **kwargs)

        if current_policy == SOFT_DELETE_CASCADE:
            for related in related_objects(self):
                if is_safedelete_cls(related.__class__) and related.deleted:
                    related.undelete()

    def delete(self, force_policy=None, **kwargs):
        """Overrides Django's delete behaviour based on the model's delete policy.

        Args:
            force_policy: Force a specific delete policy. (default: {None})
            kwargs: Passed onto :func:`save` if soft deleted.
        """
        current_policy = self._safedelete_policy if (force_policy is None) else force_policy

        if current_policy == NO_DELETE:

            # Don't do anything.
            return

        elif current_policy == SOFT_DELETE:

            # Only soft-delete the object, marking it as deleted.
            self.deleted = timezone.now()
            using = kwargs.get('using') or router.db_for_write(self.__class__, instance=self)
            # send pre_softdelete signal
            pre_softdelete.send(sender=self.__class__, instance=self, using=using)
            super(SafeDeleteModel, self).save(**kwargs)
            # send softdelete signal
            post_softdelete.send(sender=self.__class__, instance=self, using=using)

        elif current_policy == HARD_DELETE:

            # Normally hard-delete the object.
            super(SafeDeleteModel, self).delete()

        elif current_policy == HARD_DELETE_NOCASCADE:

            # Hard-delete the object only if nothing would be deleted with it

            if not can_hard_delete(self):
                self.delete(force_policy=SOFT_DELETE, **kwargs)
            else:
                self.delete(force_policy=HARD_DELETE, **kwargs)

        elif current_policy == SOFT_DELETE_CASCADE:
            # Soft-delete on related objects before
            for related in related_objects(self):
                if is_safedelete_cls(related.__class__) and not related.deleted:
                    related.delete(force_policy=SOFT_DELETE, **kwargs)
            # soft-delete the object
            self.delete(force_policy=SOFT_DELETE, **kwargs)

    @classmethod
    def has_unique_fields(cls):
        """Checks if one of the fields of this model has a unique constraint set (unique=True)

        Args:
            model: Model instance to check
        """
        for field in cls._meta.fields:
            if field._unique:
                return True
        return False

    # We need to overwrite this check to ensure uniqueness is also checked
    # against "deleted" (but still in db) objects.
    # FIXME: Better/cleaner way ?
    def _perform_unique_checks(self, unique_checks):
        errors = {}

        for model_class, unique_check in unique_checks:
            lookup_kwargs = {}
            for field_name in unique_check:
                f = self._meta.get_field(field_name)
                lookup_value = getattr(self, f.attname)
                if lookup_value is None:
                    continue
                if f.primary_key and not self._state.adding:
                    continue
                lookup_kwargs[str(field_name)] = lookup_value
            if len(unique_check) != len(lookup_kwargs):
                continue

            # This is the changed line
            if hasattr(model_class, 'all_objects'):
                qs = model_class.all_objects.filter(**lookup_kwargs)
            else:
                qs = model_class._default_manager.filter(**lookup_kwargs)

            model_class_pk = self._get_pk_val(model_class._meta)
            if not self._state.adding and model_class_pk is not None:
                qs = qs.exclude(pk=model_class_pk)
            if qs.exists():
                if len(unique_check) == 1:
                    key = unique_check[0]
                else:
                    key = models.base.NON_FIELD_ERRORS
                errors.setdefault(key, []).append(
                    self.unique_error_message(model_class, unique_check)
                )
        return errors

And here is SafeDeleteManager for the model manager:

class SafeDeleteManager(models.Manager):
    """Default manager for the SafeDeleteModel.

    If _safedelete_visibility == DELETED_VISIBLE_BY_PK, the manager can returns deleted
    objects if they are accessed by primary key.

    :attribute _safedelete_visibility: define what happens when you query masked objects.
        It can be one of ``DELETED_INVISIBLE`` and ``DELETED_VISIBLE_BY_PK``.
        Defaults to ``DELETED_INVISIBLE``.

        >>> from safedelete.models import SafeDeleteModel
        >>> from safedelete.managers import SafeDeleteManager
        >>> class MyModelManager(SafeDeleteManager):
        ...     _safedelete_visibility = DELETED_VISIBLE_BY_PK
        ...
        >>> class MyModel(SafeDeleteModel):
        ...     _safedelete_policy = SOFT_DELETE
        ...     my_field = models.TextField()
        ...     objects = MyModelManager()
        ...
        >>>

    :attribute _queryset_class: define which class for queryset should be used
        This attribute allows to add custom filters for both deleted and not
        deleted objects. It is ``SafeDeleteQueryset`` by default.
        Custom queryset classes should be inherited from ``SafeDeleteQueryset``.
    """

    _safedelete_visibility = DELETED_INVISIBLE
    _safedelete_visibility_field = 'pk'
    _queryset_class = SafeDeleteQueryset

    def __init__(self, queryset_class=None, *args, **kwargs):
        """Hook for setting custom ``_queryset_class``.

        Example:

            class CustomQueryset(models.QuerySet):
                pass

            class MyModel(models.Model):
                my_field = models.TextField()

                objects = SafeDeleteManager(CustomQuerySet)
        """
        super(SafeDeleteManager, self).__init__(*args, **kwargs)
        if queryset_class:
            self._queryset_class = queryset_class

    def get_queryset(self):
        # Backwards compatibility, no need to move options to QuerySet.
        queryset = self._queryset_class(self.model, using=self._db)
        queryset._safedelete_visibility = self._safedelete_visibility
        queryset._safedelete_visibility_field = self._safedelete_visibility_field
        return queryset

    def all_with_deleted(self):
        """Show all models including the soft deleted models.

        .. note::
            This is useful for related managers as those don't have access to
            ``all_objects``.
        """
        return self.all(
            force_visibility=DELETED_VISIBLE
        )

    def deleted_only(self):
        """Only show the soft deleted models.

        .. note::
            This is useful for related managers as those don't have access to
            ``deleted_objects``.
        """
        return self.all(
            force_visibility=DELETED_ONLY_VISIBLE
        )

    def all(self, **kwargs):
        """Pass kwargs to ``SafeDeleteQuerySet.all()``.

        Args:
            force_visibility: Show deleted models. (default: {None})

        .. note::
            The ``force_visibility`` argument is meant for related managers when no
            other managers like ``all_objects`` or ``deleted_objects`` are available.
        """
        force_visibility = kwargs.pop('force_visibility', None)

        # We don't call all() on the queryset, see https://github.com/makinacorpus/django-safedelete/issues/81
        qs = self.get_queryset()
        if force_visibility is not None:
            qs._safedelete_force_visibility = force_visibility
        return qs

    def update_or_create(self, defaults=None, **kwargs):
        """See :func:`~django.db.models.Query.update_or_create.`.

        Change to regular djangoesk function:
        Regular update_or_create() fails on soft-deleted, existing record with unique constraint on non-id field
        If object is soft-deleted we don't update-or-create it but reset the deleted field to None.
        So the object is visible again like a create in any other case.

        Attention: If the object is "revived" from a soft-deleted state the created return value will
        still be false because the object is technically not created unless you set
        SAFE_DELETE_INTERPRET_UNDELETED_OBJECTS_AS_CREATED = True in the django settings.

        Args:
            defaults: Dict with defaults to update/create model instance with
            kwargs: Attributes to lookup model instance with
        """

        # Check if one of the model fields contains a unique constraint
        revived_soft_deleted_object = False
        if self.model.has_unique_fields():
            # Check if object is already soft-deleted
            deleted_object = self.all_with_deleted().filter(**kwargs).exclude(deleted=None).first()

            # If object is soft-deleted, reset delete-state...
            if deleted_object and deleted_object._safedelete_policy in self.get_soft_delete_policies():
                deleted_object.deleted = None
                deleted_object.save()
                revived_soft_deleted_object = True

        # Do the standard logic
        obj, created = super(SafeDeleteManager, self).update_or_create(defaults, **kwargs)

        # If object was soft-deleted and is "revived" and settings flag is True, show object as created
        if revived_soft_deleted_object and \
                getattr(settings, 'SAFE_DELETE_INTERPRET_UNDELETED_OBJECTS_AS_CREATED', False):
            created = True

        return obj, created

    @staticmethod
    def get_soft_delete_policies():
        """Returns all stati which stand for some kind of soft-delete"""
        return [SOFT_DELETE, SOFT_DELETE_CASCADE]

4 Answers

This ended up being an issue with incompatibilities using django-encrypted-model-fields from: https://pypi.org/project/django-encrypted-model-fields/

Overall the context arg from in the from_db_value() method in the code was the issue. I simply added a default value of None and I was able to update the system to Django 3.

from __future__ import unicode_literals
import django.db
import django.db.models
from django.utils.functional import cached_property
from django.core import validators
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
import cryptography.fernet
# from django.utils.six import PY2, string_types # no longer supported, use six
from six import string_types, PY2, text_type

class EncryptedMixin(object):
    def to_python(self, value):
        if value is None:
            return value

        if isinstance(value, (bytes, string_types[0])):
            if isinstance(value, bytes):
                value = value.decode('utf-8')
            try:
                value = decrypt_str(value)
            except cryptography.fernet.InvalidToken:
                pass

        return super(EncryptedMixin, self).to_python(value)

    # ---- ISSUE WAS IN THIS SIGNATURE
    def from_db_value(self, value, expression, connection, context=None):
        return self.to_python(value)

    def get_db_prep_save(self, value, connection):
        value = super(EncryptedMixin, self).get_db_prep_save(value, connection)

        if value is None:
            return value
        if PY2:
            return encrypt_str(text_type(value))
        # decode the encrypted value to a unicode string, else this breaks in pgsql
        return (encrypt_str(str(value))).decode('utf-8')

    def get_internal_type(self):
        return "TextField"

    def deconstruct(self):
        name, path, args, kwargs = super(EncryptedMixin, self).deconstruct()

        if 'max_length' in kwargs:
            del kwargs['max_length']

        return name, path, args, kwargs

I used this code to pinpoint the issue and loop over all models to see which ones threw the error, for me it was only the fields which were encrypted using that package.

    from django.apps import apps
    for model in apps.get_models():
        app_label = ContentType.objects.get_for_model(model).app_label
        model_name = model.__name__
        model = apps.get_model(app_label, model_name)
        print(model)
        # if model_name not in ['Resident', 'Application', 'Children', 'CoApplicant']:
        print(model.objects.first())

If anyone else comes here and doesn't find a working solution above - please check your models for a JSONField. I was able to resolve my issue by migrating model fields from django_extensions JSONField to the built-in models.JSONField. This resolved the error for me.

I faced the same issue and this post was helpful to solve it, but I would like to share some extra information. Exploring into django-encrypted-model-fields I found a dependency with django-fernet-fields. Adding these versions to my requirements solved the issue for me:

django-encrypted-model-fields==0.6.0
django-fernet-fields==0.6  # Version 0.5 was installed and caused the issue. 
Related