#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.

Reply via email to