#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 django-updates+unsubscr...@googlegroups.com.
To post to this group, send email to django-updates@googlegroups.com.
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.

Reply via email to