由于 from_db_value() 导致的 Django 3 对象评估错误
我正尝试将项目更新到 Django 3,但在评估对象时遇到了奇怪的错误,提示如下:
TypeError: from_db_value() 缺少 1 个必需的位置参数:“context”
我在此处阅读了 Django 文档: 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.
但我不明白我需要做什么来解决这个问题,因为我的调用只是这样来获取错误……
apps = Application.objects.filter(completed=False, canceled=False)
for app in apps:
print(app)
有什么我可以做的吗?我没有得到什么?
这是完整的回溯,似乎没有给我任何有用的信息
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'
更新 如果需要的话,这是我的关联应用程序模型,尽管我在 3.0 文档中没有看到我错误声明的内容:
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
这是继承的 SafeDelete 模型,使用 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)
我的时间戳和 UUID 继承模型只是以更通用的方式将 UUID 和时间戳字段添加到我的模型中,无需显示它们。
这是 BaseModel,但它只是来自 SafeDeleteModel,因此我可以添加更多功能。
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
这是模型管理器的 SafeDeleteManager:
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]
这最终导致使用 django-encrypted-model-fields 不兼容的问题,来自: https://pypi.org/project/django-encrypted-model-fields/
总的来说,代码中
from_db_value()
方法中的
context
参数是问题所在。我只需添加一个默认值
None
,我就能将系统更新到 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
我使用此代码来查明问题并循环遍历所有模型以查看哪些模型引发了错误,对我来说,只有使用该包加密的字段才会引发错误。
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())
如果其他人来到这里,却找不到上述可行的解决方案 - 请检查您的模型是否有 JSONField。我能够通过将模型字段从 django_extensions JSONField 迁移到内置 models.JSONField 来解决我的问题。这为我解决了错误。
Django encrypted_fields 模块似乎有同样的问题:
https://pypi.org/project/django-encrypted-fields/
修复方法与 ViaTech 的答案相同。