doableware / djongo

Django and MongoDB database connector
https://www.djongomapper.com
GNU Affero General Public License v3.0
1.89k stars 355 forks source link

Error when insert nested document, object is not subscriptable #407

Open oraix opened 4 years ago

oraix commented 4 years ago

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 ```python 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 ```python 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. ```python 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 commented 4 years ago

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
SomeoneInParticular commented 4 years ago

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 commented 4 years ago

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

oraix commented 4 years ago

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.

SomeoneInParticular commented 4 years ago

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)

shiv-u commented 4 years ago

@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()

bpfrare commented 4 years ago

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)
oraix commented 4 years ago

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.

keshabdey commented 3 years ago

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