djongo icon indicating copy to clipboard operation
djongo copied to clipboard

Error when insert nested document, object is not subscriptable

Open oraix opened this issue 5 years ago • 9 comments

One line description of the issue

Error when insert data to Mongodb with Djongo (install last version from github) TypeError: 'Note' object is not subscriptable

Python script

Model snippet person.py

class Person(Subject):
    gender = models.CharField(max_length=4) 
    name = models.CharField(max_length=64)     
    principal = models.BooleanField(default=False)
    notes = ArrayField( model_container=Note, null=True, blank=True)

note.py


class Note(models.Model):
    subject = models.CharField(max_length=128, null=True, blank=True)
    text = models.TextField(max_length=1024, blank=True)

the issue probably located in file fields.py , line 139.


    def _value_thru_fields(self,
                           func_name: str,
                           value: dict,
                           *other_args):
        processed_value = {}
        errors = {}
        for field in self.model_container._meta.get_fields():
            try:
                try:
                    field_value = value[field.attname]  #### line 139, value is a Note object instead of dict.
                except KeyError:
                    raise ValidationError(f'Value for field "{field}" not supplied')
                processed_value[field.attname] = getattr(field, func_name)(field_value, *other_args)
            except ValidationError as e:
                errors[field.name] = e.error_list

        if errors:
            e = ValidationError(errors)
            raise ValidationError(str(e))

        return processed_value

Traceback

TypeError: Got a TypeError when calling Person.objects.create(). This may be because you have a writable field on the serializer class that is not a valid argument to Person.objects.create(). You may need to make the field read-only, or override the PersonSerializer.create() method to handle this correctly. Original exception was: Traceback (most recent call last): File "D:\dev\lan\landjango\landjango\rest_meets_djongo\serializers.py", line 192, in create instance = model_class._default_manager.create(**data) File "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\manager.py", line 82, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\query.py", line 422, in create obj.save(force_insert=True, using=self.db) File "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\base.py", line 741, in save force_update=force_update, update_fields=update_fields) File "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\base.py", line 779, in save_base force_update, using, update_fields, File "D:\dev\virtualenvs\apienv\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 "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\base.py", line 908, in _do_insert using=using, raw=raw) File "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\manager.py", line 82, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\query.py", line 1186, in _insert return query.get_compiler(using=using).execute_sql(return_id) File "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\sql\compiler.py", line 1374, in execute_sql for sql, params in self.as_sql(): File "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\sql\compiler.py", line 1318, in as_sql for obj in self.query.objs File "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\sql\compiler.py", line 1318, in for obj in self.query.objs File "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\sql\compiler.py", line 1317, in [self.prepare_value(field, self.pre_save_val(field, obj)) for field in fields] File "D:\dev\virtualenvs\apienv\lib\site-packages\django\db\models\sql\compiler.py", line 1258, in prepare_value value = field.get_db_prep_save(value, connection=self.connection) File "D:\dev\virtualenvs\apienv\lib\site-packages\djongo\models\fields.py", line 209, in get_db_prep_save return self.get_prep_value(value) File "D:\dev\virtualenvs\apienv\lib\site-packages\djongo\models\fields.py", line 217, in get_prep_value value) File "D:\dev\virtualenvs\apienv\lib\site-packages\djongo\models\fields.py", line 311, in _value_thru_fields *other_args) File "D:\dev\virtualenvs\apienv\lib\site-packages\djongo\models\fields.py", line 139, in _value_thru_fields field_value = value[field.attname] TypeError: 'Note' object is not subscriptable

oraix avatar May 09 '20 13:05 oraix

As a workaround, I fall back the version to djongo 1.2.38 with rest_meets_djongo 0.0.11 with a patch for lookup to resolve filter issue


from django.db.models import Model
from django.db.models.lookups import PatternLookup, IExact
from djongo.models import fields
from djongo.models.fields import useful_field

# replace original lookup.py  the method of PatternLookup.process_rhs
# def process_rhs(self, qn, connection):
#     rhs, params = super().process_rhs(qn, connection)
#     if self.rhs_is_direct_value() and params and not self.bilateral_transforms:
#         params[0] = self.param_pattern % connection.ops.prep_for_like_query(params[0])
#     return rhs, params

def process_rhs_pattern(self, qn, connection):
    rhs, params = super(PatternLookup, self).process_rhs(qn, connection)
    if self.rhs_is_direct_value() and params and not self.bilateral_transforms:
        if isinstance(params[0], dict):
            # Prevent Django's PatternLookup from casting our query dict {'field': 'to_match'}
            # to a str "%{'field': 'to_match'}%"
            field, to_match = next(iter(params[0].items()))
            params[0] = {field: self.param_pattern % connection.ops.prep_for_like_query(to_match)}
connection.ops.prep_for_like_query(to_match)
    else:
        params[0] = self.param_pattern % connection.ops.prep_for_like_query(params[0])
    return rhs, params

def process_rhs_iexact(self, qn, connection):
    rhs, params = super(IExact, self).process_rhs(qn, connection)
    if params:
        if isinstance(params[0], dict):
            # Prevent Django's PatternLookup from casting our query dict {'field': 'to_match'}
            # to a str "%{'field': 'to_match'}%"
            field, to_match = next(iter(params[0].items()))
            params[0] = {field: connection.ops.prep_for_iexact_query(to_match)}
connection.ops.prep_for_like_query(to_match)
        else:
            params[0] = connection.ops.prep_for_iexact_query(params[0])
    return rhs, params

class MyArrayModelField(fields.ArrayModelField):
    def get_db_prep_value(self, value, connection, prepared=False):
        if prepared:
            return value

        if value is None and self.blank:
            return None

        if not isinstance(value, list):
            raise ValueError(
                'Expected value to be type list,'
                f'Got type {type(value)} instead'
            )

        ret = []
        for a_mdl in value:
            mdl_ob = {}
            if not isinstance(a_mdl, Model):
                raise ValueError('Array items must be Model instances')
            for fld in a_mdl._meta.get_fields():
                if not useful_field(fld):
                    continue
                fld_value = getattr(a_mdl, fld.attname)
                mdl_ob[fld.attname] = fld.get_db_prep_value(fld_value, connection, prepared)
            ret.append(mdl_ob)
        return ret

    def get_lookup(self, lookup_name):
        """
            Make django use our `get_lookup` method
            when building the lookup class for an EmbeddedModelField instance.
            """
        lookup = super(self.__class__, self).get_lookup(lookup_name)
        if issubclass(lookup, PatternLookup):
            lookup = type('Djongo' + lookup.__name__, (lookup,), {'process_rhs': process_rhs_pattern})
        elif issubclass(lookup, IExact):
            lookup = type('Djongo' + lookup.__name__, (lookup,), {'process_rhs': process_rhs_iexact})
        return lookup


class MyEmbeddedModelField(fields.EmbeddedModelField):
    def get_lookup(self, lookup_name):
        """
            Make django use our `get_lookup` method
            when building the lookup class for an EmbeddedModelField instance.
            """
        lookup = super(self.__class__, self).get_lookup(lookup_name)
        if issubclass(lookup, PatternLookup):
            lookup = type('Djongo' + lookup.__name__, (lookup,), {'process_rhs': process_rhs_pattern})
        elif issubclass(lookup, IExact):
            lookup = type('Djongo' + lookup.__name__, (lookup,), {'process_rhs': process_rhs_iexact})
        return lookup

oraix avatar May 09 '20 13:05 oraix

Can you please give us the use-case that raises said error (is it an update, creation, or retrieval)? I suspect that whats happening is you're trying to update the value to null; I'm currently patching that into rest-meets-djongo, as its not yet supported (slight oversight, my apologies). If its something else, please let us know so we can make a more representative test cases.

SomeoneInParticular avatar May 10 '20 01:05 SomeoneInParticular

Update; changes allowing null updates have been made, and are available in v0.0.13 of rest-meets-djongo. Hopefully that fixes the issue

SomeoneInParticular avatar May 10 '20 01:05 SomeoneInParticular

the issue was triggered when insert data for nested model.

File "D:\dev\virtualenvs\apienv\lib\site-packages\djongo\models\fields.py", line 139, in _value_thru_fields field_value = value[field.attname]
TypeError: 'Note' object is not subscriptable

Value is assigned as a Note object, the expression is not valid for it. I guess it should be a OrderedDict.

oraix avatar May 10 '20 09:05 oraix

Are you passing in a Note without wrapping it in a list? Seems like the most likely culprit here. (I'll look into adding a more informative error message for that case)

EDIT: Doing some quick testing with 1.3.1, that doesn't seem to be the case either (and my error messages report it as such, regardless of initially submitted type)

SomeoneInParticular avatar May 11 '20 00:05 SomeoneInParticular

@oraix I was also facing the same issue and you can solve this by wrapping the dictionary data in a list. Follow these steps in your case :

person_obj, person_created = Person.objects.get_or_create(name = <"any name">) if person_created: person_obj.gender = "Male" person_obj.principal = false data = {"subject":"Computer Science","text":"Computer science is AWESOME"} person_obj.notes=[data] person_obj.save()

shiv-u avatar May 16 '20 10:05 shiv-u

I made a workaround, I had added the following code in my abstract model. I your case, the note model.

https://stackoverflow.com/questions/216972/what-does-it-mean-if-a-python-object-is-subscriptable-or-not


class Note(models.Model):
    subject = models.CharField(max_length=128, null=True, blank=True)
    text = models.TextField(max_length=1024, blank=True)
    
    def __getitem__(self, name):
       return getattr(self, name)

bpfrare avatar May 28 '20 18:05 bpfrare

I made a workaround, I had added the following code in my abstract model. I your case, the note model.

https://stackoverflow.com/questions/216972/what-does-it-mean-if-a-python-object-is-subscriptable-or-not

class Note(models.Model):
    subject = models.CharField(max_length=128, null=True, blank=True)
    text = models.TextField(max_length=1024, blank=True)
    
    def __getitem__(self, name):
       return getattr(self, name)

Thanks you, It's a very helpful workaround.

oraix avatar Jun 07 '20 09:06 oraix

@nesdis can you update this workaround in the code samples of djongomapper website

keshabdey avatar May 05 '21 13:05 keshabdey