#30564: Cannot create custom field that returns a queryset AND uses pre_save()
-------------------------------------+-------------------------------------
Reporter: Dan J | Owner: nobody
Strohl |
Type: Bug | Status: new
Component: Database | Version: 2.2
layer (models, ORM) |
Severity: Normal | Keywords:
Triage Stage: | Has patch: 0
Unreviewed |
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 0
UI/UX: 0 |
-------------------------------------+-------------------------------------
I tried creating a custom field that would store a string that was a list
of pks, then return that as a queryset. (yes, I could probably have done
it using a many2many field, but I was (am) trying to reduce the number of
db queries for this.
In any case, all seems to work OK until I implemented a def pre_save(self,
model_instance, add) method on my custom field.
What seems to be happening is that when the queryset comes back from the
pre-save, it is run through the SQLInsertCompiler.prepare_value, which
checks the value to see if it has a "resolve_expression" attrubute, which
it assumes is then a SQL expression and then tries checking for
"contains_column_references"... The QuerySet object DOES have the
"resolve_expression" attribute, but does NOT have the others that are in
the SQL expression objects.
I suspect this doesn't come up much.
My trace back
Error
Traceback (most recent call last):
{{{
File
"C:\Users\strohl\Documents\Project\Who\who_db\who_db_tests\test_model_methods.py",
line 101, in setUp
self.M1 = MixinTest.objects.create(name='M1')
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\manager.py", line 82, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\query.py", line 422, in create
obj.save(force_insert=True, using=self.db)
File "C:\Users\strohl\Documents\Project\Who\who_db\models.py", line 132,
in save
super(MixinTest, self).save(*args, **kwargs)
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\base.py", line 741, in save
force_update=force_update, update_fields=update_fields)
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\base.py", line 779, in save_base
force_update, using, update_fields,
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\base.py", line 870, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk,
raw)
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\base.py", line 908, in _do_insert
using=using, raw=raw)
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\manager.py", line 82, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\query.py", line 1186, in _insert
return query.get_compiler(using=using).execute_sql(return_id)
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\sql\compiler.py", line 1331, in execute_sql
for sql, params in self.as_sql():
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\sql\compiler.py", line 1275, in as_sql
for obj in self.query.objs
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\sql\compiler.py", line 1275, in <listcomp>
for obj in self.query.objs
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\sql\compiler.py", line 1274, in <listcomp>
[self.prepare_value(field, self.pre_save_val(field, obj)) for field in
fields]
File "C:\Users\strohl\Documents\VirtualEnv\Who\lib\site-
packages\django\db\models\sql\compiler.py", line 1205, in prepare_value
if value.contains_column_references:
AttributeError: 'Query' object has no attribute
'contains_column_references'
}}}
My custom field:
{{{
#!python
class PkListField(Field):
empty_strings_allowed = False
description = "PK List"
default_error_messages = {}
def __init__(self, *args, max_recs=100, max_pk_size=5, sep=',',
ordered=None, as_pks=True, model=None, manager='objects',
pre_save_func=None, **kwargs):
self.pk_list_model_manager = manager
self.max_recs = max_recs
self.max_pk_size = max_pk_size
self.sep = sep
self.as_pks = as_pks
self.ordered = ordered
self.pre_save_func = pre_save_func
self._pk_list_model = model
if not as_pks and model is None:
raise AttributeError('Model must be specified if not returning
as_pks')
if as_pks and ordered is not None and ordered != 'pk':
raise AttributeError('Data can only be ordered by PKs when
returning as pks')
kwargs['max_length'] = ((max_pk_size + 2) * max_recs) + 1
kwargs['blank'] = True
kwargs['default'] = sep + sep
super().__init__(*args, **kwargs)
@property
def pk_list_model(self):
if isinstance(self._pk_list_model, str):
self._pk_list_model = apps.get_model(self._pk_list_model)
return self._pk_list_model
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.pre_save_func is not None:
kwargs['pre_save_func'] = self.pre_save_func
if self.pk_list_model is not None:
kwargs['model'] = self.pk_list_model
if self.pk_list_model_manager is not 'objects':
kwargs['manager'] = self.pk_list_model_manager
if not self.as_pks:
kwargs['as_pks'] = self.as_pks
if self.max_recs != 100:
kwargs['max_recs'] = self.max_recs
if self.max_pk_size != 5:
kwargs['max_pk_size'] = self.max_pk_size
if self.sep != ',':
kwargs['sep'] = self.sep
if self.ordered is not None:
kwargs['ordered'] = self.ordered
del kwargs['max_length']
del kwargs['blank']
del kwargs['default']
return name, path, args, kwargs
def get_internal_type(self):
return "CharField"
def conv_str_to_python(self, value):
def conv_obj(obj_in):
if obj_in is None:
return []
elif isinstance(obj_in, int):
return [obj_in]
elif isinstance(obj_in, str):
if self.sep in obj_in:
return
conv_obj(obj_in.strip().strip(self.sep).split(self.sep))
if obj_in:
return [int(obj_in.strip())]
else:
return []
elif issubclass(obj_in.__class__, models.Model):
return [obj_in.pk]
else:
try:
tmp_ret = []
for item in obj_in:
tmp_ret.extend(conv_obj(item))
return tmp_ret
except TypeError:
raise ValidationError('Invalid object type: %r' %
obj_in)
if issubclass(value.__class__, models.QuerySet):
if self.as_pks:
return list(value.values_list('pk', flat=True))
else:
return value
if not value:
value = []
else:
value = conv_obj(value)
if self.as_pks:
if self.ordered == 'pk':
value.sort()
return value
tmp_mgr = getattr(self.pk_list_model, self.pk_list_model_manager)
print('in conv_to_python: value= %r' % value)
# if value:
tmp_ret = tmp_mgr.filter(pk__in=value)
if self.ordered:
tmp_ret = tmp_ret.order_by(*make_list(self.ordered))
print('in conv_to_python: returning filtered = %r' % tmp_ret)
return tmp_ret
# else:
# tmp_ret = tmp_mgr.none()
# print('in conv_to_python: returning none = %r' % tmp_ret)
# return tmp_ret
def to_python(self, value):
print(f'IN ({self.attname}) to_python({repr(value)})')
tmp_ret = self.conv_str_to_python(value)
print(f'OUT ({self.attname}) to_python({repr(tmp_ret)})')
return tmp_ret
def from_db_value(self, value, expression, connection):
print(f'IN ({self.attname}) from_db_value({repr(value)})')
tmp_ret = self.conv_str_to_python(value)
print(f'OUT ({self.attname}) from_db_value({repr(tmp_ret)})')
return tmp_ret
def conv_to_str(self, value, wrapped=True):
def conv_obj(obj_in):
if obj_in is None:
return []
elif isinstance(obj_in, int):
return [str(obj_in)]
elif isinstance(obj_in, str):
if self.sep in obj_in:
return
conv_obj(obj_in.strip().strip(self.sep).split(self.sep))
if obj_in:
return [obj_in.strip()]
else:
return []
elif issubclass(obj_in.__class__, models.Model):
return [str(obj_in.pk)]
else:
try:
tmp_ret = []
for item in obj_in:
tmp_ret.extend(conv_obj(item))
return tmp_ret
except TypeError:
raise ValidationError('Invalid object type: %r' %
obj_in)
if issubclass(value.__class__, models.QuerySet):
value = list(value.values_list('pk', flat=True))
value = self.sep.join(conv_obj(value))
if wrapped:
return self.sep + value + self.sep
else:
return value
def get_db_prep_value(self, value, connection, prepared=False):
print(f'IN ({self.attname}) get_db_prep_value({repr(value)})')
if not prepared:
value = self.get_prep_value(value)
value = super(PkListField, self).get_db_prep_value(value,
connection, prepared=True)
print(f'OUT ({self.attname}) get_db_prep_value({repr(value)})')
return value
def get_prep_value(self, value):
print(f'IN ({self.attname}) get_prep_value({repr(value)})')
value = super().get_prep_value(value)
value = self.conv_to_str(value)
print(f'OUT ({self.attname}) get_prep_value({repr(value)})')
return value
def pre_save(self, model_instance, add):
if self.pre_save_func is not None:
value = super(PkListField, self).pre_save(model_instance, add)
value = self.pre_save_func(value=value, field=self.attname,
model=model_instance, add=add)
setattr(model_instance, self.attname, value)
print(f'({self.attname}) OUT pre_Save({repr(value)})')
return value
else:
value = super().pre_save(model_instance, add)
print(f'OUT ({self.attname}) pre_save({repr(value)})')
return value
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/30564>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
--
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-updates/050.d7884453decb31be01cda23e0d2ad6af%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.