Giter Site home page Giter Site logo

drf-writable-nested's Introduction

DRF Writable Nested

build codecov pypi pyversions

This is a writable nested model serializer for Django REST Framework which allows you to create/update your models with related nested data.

The following relations are supported:

  • OneToOne (direct/reverse)
  • ForeignKey (direct/reverse)
  • ManyToMany (direct/reverse excluding m2m relations with through model)
  • GenericRelation (this is always only reverse)

Requirements

  • Python (3.7, 3.8, 3.9, 3.10, 3.11)
  • Django (2.2, 3.0, 3.1, 3.2, 4.0, 4.1, 4.2)
  • djangorestframework (3.8+)

Installation

pip install drf-writable-nested

Usage

For example, for the following model structure:

from django.db import models


class Site(models.Model):
    url = models.CharField(max_length=100)


class User(models.Model):
    username = models.CharField(max_length=100)


class AccessKey(models.Model):
    key = models.CharField(max_length=100)


class Profile(models.Model):
    sites = models.ManyToManyField(Site)
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    access_key = models.ForeignKey(AccessKey, null=True, on_delete=models.CASCADE)


class Avatar(models.Model):
    image = models.CharField(max_length=100)
    profile = models.ForeignKey(Profile, related_name='avatars', on_delete=models.CASCADE)

We should create the following list of serializers:

from rest_framework import serializers
from drf_writable_nested.serializers import WritableNestedModelSerializer


class AvatarSerializer(serializers.ModelSerializer):
    image = serializers.CharField()

    class Meta:
        model = Avatar
        fields = ('pk', 'image',)


class SiteSerializer(serializers.ModelSerializer):
    url = serializers.CharField()

    class Meta:
        model = Site
        fields = ('pk', 'url',)


class AccessKeySerializer(serializers.ModelSerializer):

    class Meta:
        model = AccessKey
        fields = ('pk', 'key',)


class ProfileSerializer(WritableNestedModelSerializer):
    # Direct ManyToMany relation
    sites = SiteSerializer(many=True)

    # Reverse FK relation
    avatars = AvatarSerializer(many=True)

    # Direct FK relation
    access_key = AccessKeySerializer(allow_null=True)

    class Meta:
        model = Profile
        fields = ('pk', 'sites', 'avatars', 'access_key',)


class UserSerializer(WritableNestedModelSerializer):
    # Reverse OneToOne relation
    profile = ProfileSerializer()

    class Meta:
        model = User
        fields = ('pk', 'profile', 'username',)

Also, you can use NestedCreateMixin or NestedUpdateMixin from this package if you want to support only create or update logic.

For example, we can pass the following data with related nested fields to our main serializer:

data = {
    'username': 'test',
    'profile': {
        'access_key': {
            'key': 'key',
        },
        'sites': [
            {
                'url': 'http://google.com',
            },
            {
                'url': 'http://yahoo.com',
            },
        ],
        'avatars': [
            {
                'image': 'image-1.png',
            },
            {
                'image': 'image-2.png',
            },
        ],
    },
}

user_serializer = UserSerializer(data=data)
user_serializer.is_valid(raise_exception=True)
user = user_serializer.save()

This serializer will automatically create all nested relations and we receive a complete instance with filled data.

user_serializer = UserSerializer(instance=user)
print(user_serializer.data)
{
    'pk': 1,
    'username': 'test',
    'profile': {
        'pk': 1,
        'access_key': {
            'pk': 1,
            'key': 'key'
        },
        'sites': [
            {
                'pk': 1,
                'url': 'http://google.com',
            },
            {
                'pk': 2,
                'url': 'http://yahoo.com',
            },
        ],
        'avatars': [
            {
                'pk': 1,
                'image': 'image-1.png',
            },
            {
                'pk': 2,
                'image': 'image-2.png',
            },
        ],
    },
}

It is also possible to pass through values to nested serializers from the call to the base serializer's save method. These kwargs must be of type dict. E g:

# user_serializer created with 'data' as above
user = user_serializer.save(
    profile={
        'access_key': {'key': 'key2'},
    },
)
print(user.profile.access_key.key)
'key2'

Note: The same value will be used for all nested instances like default value but with higher priority.

Testing

To run unit tests, run:

# Setup the virtual environment
python3 -m venv envname
source envname/bin/activate

pip install django
pip install django-rest-framework
pip install -r requirements.txt

# Run tests
py.test

Known problems with solutions

Validation problem for nested serializers with unique fields on update

We have a special mixin UniqueFieldsMixin which solves this problem. The mixin moves UniqueValidator's from the validation stage to the save stage.

If you want more details, you can read related issues and articles: #1 http://www.django-rest-framework.org/api-guide/validators/#updating-nested-serializers

Example of usage:
class Child(models.Model):
    field = models.CharField(unique=True)


class Parent(models.Model):
    child = models.ForeignKey('Child')


class ChildSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
    class Meta:
        model = Child


class ParentSerializer(NestedUpdateMixin, serializers.ModelSerializer):
    child = ChildSerializer()

    class Meta:
        model = Parent

Note: UniqueFieldsMixin must be applied only on serializer which has unique fields.

Mixin ordering

When you are using both mixins (UniqueFieldsMixin and NestedCreateMixin or NestedUpdateMixin) you should put UniqueFieldsMixin ahead.

For example:

class ChildSerializer(UniqueFieldsMixin, NestedUpdateMixin,
        serializers.ModelSerializer):
Update problem for nested fields with form-data in PATCH and PUT methods

There is a special problem while we try to update any model object with nested fields within it via PUT or PATCH using form-data we can not update it. And it complains about fields not provided. So far, we came to know that this is also a problem in DRF. But we can follow a tricky way to solve it at least for now. See the below solution about the problem

If you want more details, you can read related issues and articles: #106 encode/django-rest-framework#7262 (comment)

Example:
# Models
class Voucher(models.Model):
    voucher_number = models.CharField(verbose_name="voucher number", max_length=10, default='')
    image = models.ImageField(upload_to="vouchers/images/", null=True, blank=True)

class VoucherRow(models.Model):
    voucher = models.ForeignKey(to='voucher.Voucher', on_delete=models.PROTECT, verbose_name='voucher',
                                related_name='voucherrows', null=True)
    account = models.CharField(verbose_name="fortnox account number", max_length=255)
    debit = models.DecimalField(verbose_name="amount", decimal_places=2, default=0.00, max_digits=12)
    credit = models.DecimalField(verbose_name="amount", decimal_places=2, default=0.00, max_digits=12)
    description = models.CharField(verbose_name="description", max_length=100, null=True, blank=True)

# Serializers for these models
class VoucherRowSerializer(WritableNestedModelSerializer):
    class Meta:
        model = VoucherRow
        fields = ('id', 'account', 'debit', 'credit', 'description',)


class VoucherSerializer(serializers.ModelSerializer):
    voucherrows = VoucherRowSerializer(many=True, required=False, read_only=True)
    class Meta:
        model = Voucher
        fields = ('id', 'participants', 'voucher_number', 'voucherrows', 'image')

Now if you want to update Voucher with VoucherRow and voucher image then you need to do it using form-data via PUT or PATCH request where your voucherrows fields are nested field. With the current implementation of the drf-writable-nested doesn't update it. Because it does not support something like-

voucherrows[1].account=1120
voucherrows[1].debit=1000.00
voucherrows[1].credit=0.00
voucherrows[1].description='Debited from Bank Account' 
voucherrows[2].account=1130
voucherrows[2].debit=0.00
voucherrows[2].credit=1000.00
voucherrows[2].description='Credited to Cash Account'

This is not supported at least for now. So, we can achieve the result in a different way. Instead of sending the array fields separately in this way we can convert the whole fields along with values in a json string like below and set it as value to the field voucherrows.

"[{\"account\": 1120, \"debit\": 1000.00, \"credit\": 0.00, \"description\": \"Debited from Bank Account\"}, {\"account\": 1130, \"debit\": 0.00, \"credit\": 1000.00, \"description\": \"Credited to Cash Account\"}]"

Now it'll be actually sent as a single field value to the application for the field voucherrows. From your views you need to parse it like below before sending it to the serializer-

class VoucherViewSet(viewsets.ModelViewSet):
    serializer_class = VoucherSerializer
    queryset = serializer_class.Meta.model.objects.all().order_by('-created_at')
    
    def update(self, request, *args, **kwargs):
        request.data.update({'voucherrows': json.loads(request.data.pop('voucherrows', None))})
        return super().update(request, *args, **kwargs)

Now, you'll get the voucherrows field with data in the right format in your serializers. Similar approach will be also applicable for generic views for django rest framework

Authors

2014-2022, beda.software

drf-writable-nested's People

Contributors

bierik avatar browniebroke avatar claytondaley avatar csdenboer avatar ir4y avatar izimobil avatar johnthagen avatar jpnauta avatar karamanolev avatar kavdev avatar kseniyashaydurova avatar leehanyeong avatar mands avatar ottoandrey avatar palfrey avatar pcarn avatar projkov avatar robinchow avatar ron8mcr avatar ruscoder avatar starryrbs avatar tsaipoan avatar yuekui avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

drf-writable-nested's Issues

Contributing a Different/Complementary Approach

Background: Before open sourcing our DIY nested writable logic, I wanted to look for existing package(s) -- to avoid fragmenting the users/contributors. I've been digging through this codebase to evaluate it as an alternative to our existing package. My conclusion is that it's missing a key feature (that we use extensively) due to a difference in design.

The Feature: If I follow the logic in this package, it achieves a get-or-create behavior by having the parent serializer lookup related objects by pk (or another package-wide name). Because the parent serializer does the lookup, it's impossible to look up related objects by non-PK fields (e.g. #12) unless you reconfigure the entire project to treat that field as the "pk field".

The Opportunity: Our approach delegates this behavior to the nested serializer. The nested serializer takes the data and returns a model instance (or list of instances) based on how its configured. This make it possible to configure each nested writable serializer with different behaviors. For example:

  • different nested serializers can use different update vs. create vs. both rules
  • match on pk, name, a composite key like ['field1', 'field2'], or even exact matches using __all__

To better illustrate the difference, consider a usage example:

# a generic serializer for the child Model; can be reused for top-level serialization
class ChildSerializer(ModelSerializer):
    ...  

# this "trivial" class mixes in the desired nested behavior
class GetOrCreateNestedChildSerializer(GetOrCreateSerializerMixin, ChildSerializer):
    pass

# NestedSaveMixin *only* decides the order of saves to ensure FK integrity
class ParentSerializer(NestedSaveMixin, ModelSerializer):

    # the nested serializer returns a model instance based on its configuration
    child = GetOrCreateNestedChildSerializer(
        # default behavior, but makes the point that you *can* change it per-serializer
        queryset=Patient.objects.all(),
        # define the matching criteria
        match_on=['name'],  
    )

An obvious advantage of your approach is that there's less configuration. It's theoretically possible to have dozens of nested serializers and mix the GetOrCreate mixin into the one parent class. At the same time, we need the UniqueFieldsMixin on most (all?) our child serializers; at that point the difference is pretty trivial.

If this package is ever going to support per-nested-serializer configurations, it will need to adopt an approach like this. To avoid creating a competing package, I'd be willing to take the time to get it integrated as a backwards-compatible extension to this package. Either way, I need to add a bunch of the bells-and-whistles found in your package so the "incremental" effort is mostly around backwards compatibility.

Obviously, this isn't a trivial change so I wanted to get buy-in before investing any time into it.

TypeError: Arguments to nested serializer's `save` must be dict's problem with POST request

Hello,

I have problem with nested serializers.
When I try to save nested objects from json, I have this following error :

TypeError: Arguments to nested serializer's `save` must be dict's

The argument passed to save is an instance of ComponentsParent

snippet of mixin.py (line 183)

...
def get_save_kwargs(self, field_name):
        save_kwargs = self.save_kwargs[field_name]
        print(type(save_kwargs))
        if not isinstance(save_kwargs, dict):
            raise TypeError(
                _("Arguments to nested serializer's `save` must be dict's")
            )

        return save_kwargs
...

And the print returned is <class 'my_app.models.ComponentsParent'>

My json

{
    "components": {
        "name": "Some Name",
        "coloring": [{
            "color": "#FFFFFF",
            "components": [{
                "value": "Some Value"
            }]
        },
        {
            "color": "#000000",
            "components": [{
                "value": "Another Value"
            }]
        }]
    }
}

models.py

from django.db import models


class Master(models.Model):
    components = models.ForeignKey('ComponentsParent', on_delete=models.CASCADE)


class ComponentsParent(models.Model):
    name = models.CharField(max_length=255)


class Coloring(models.Model):
    color = models.CharField(max_length=255)
    components = models.ForeignKey('ComponentsParent', on_delete=models.CASCADE, related_name='coloring')

class ComponentChild(models.Model):
    value = models.CharField(max_length=255)
    coloring = models.ForeignKey('Coloring', on_delete=models.CASCADE, null=True, blank=True, related_name='coloring_components')

views.py

from rest_framework import viewsets
from my_app.serializers import MasterSerializer
from my_app.models import Master


class MasterViewSet(viewsets.ModelViewSet):
    serializer_class = MasterSerializer

    def get_queryset(self):
        return Master.objects.all()

serializers.py

from drf_writable_nested import WritableNestedModelSerializer
from my_app.models import ComponentsParent, Coloring, ComponentChild, Master


class ComponentChildSerializer(WritableNestedModelSerializer):
    class Meta:
        model = ComponentChild
        fields = ('value',)


class ColoringSerializer(WritableNestedModelSerializer):
    components = ComponentChildSerializer(many=True, source='coloring_components')

    class Meta:
        model = Coloring
        fields = ('color', 'components')


class ComponentsParentSerializer(WritableNestedModelSerializer):
    coloring = ColoringSerializer(many=True)

    class Meta:
        model = ComponentsParent
        fields = ('coloring', 'name')


class MasterSerializer(WritableNestedModelSerializer):
    components = ComponentsParentSerializer()

    class Meta:
        model = Master
        fields = ('components',)

urls.py

from my_app import views
from django.conf.urls import url
from rest_framework.routers import DefaultRouter

urlpatterns = [
    url(r'$', views.MasterViewSet.as_view({'get': 'list', 'post': 'create'}))
]

router = DefaultRouter()

If you want more details, i'm here.

Nice day :)

Create on Primary and Update on Nested: Attribute Error on update_or_create_reverse_relations

So I am successfully creating objects, but now I need to be able to update them from a response. The scenario is that I have a multi-level nested serialize where I need to create the first two levels and then update the bottom level. I've added a mixin to populate the pk and instance on the bottom level, but I am getting AttributeError: 'category' object has no attribute 'items' which is spawning from update_or_create_reverse_relations. The mixin uses additional attributes to identify the existing records.
See below example.

Models:

class category(models.Model):
    name = models.CharField(max_length=255, help_text="",blank=True)
    short_name = models.CharField(max_length=255, help_text="",blank=True)
    external_id = models.IntegerField(help_text="",blank=True,null=True)
    sales_channel = models.ForeignKey('sales_channel',null=True,blank=True)
    purchase_channel = models.ForeignKey('purchase_channel',null=True,blank=True)
    def __str__(self):
        return self.name

class response_log_detail(models.Model):
    category = models.ManyToManyField(category)
    response_log = models.OneToOneField('response_log', null = True, blank = True)

class response_log(models.Model):
    timestamp = models.DateTimeField()
    status = models.CharField(max_length = 25, null=True, blank=True)
    category_id = models.ManyToManyField(category)

Mixin:

class AddPkInstanceMixin(s.ModelSerializer):
    
    def __init__(self, **kwargs):
        object_identifier = kwargs.pop('object_identifier', None)
        object_identifier_source = kwargs.pop('object_identifier_source', None)
        super(AddPkInstanceMixin, self ).__init__(**kwargs)
       
        
    def to_internal_value(self,data):    
        model = self.Meta.model
        id_value = data[self.object_identifier]
        if model is not None and id_value is not None:
            try:
                pk = model.objects.values('pk').get(**{self.object_identifier_source : id_value})
                if pk is not None:
                    data['pk']=pk['pk']
                    instance = model.objects.get(pk = pk['pk'])
                    return model.objects.get(pk = pk['pk'])
            except:
                pass
        return super().to_internal_value(data)

Serializers:

class categoryArray(AddPkInstanceMixin, s.ModelSerializer):

    object_identifier = 'categoryId'
    object_identifier_source = 'external_id'
    
    # Fields #
    categoryId = s.IntegerField(source='external_id')
    categoryName = s.CharField(source='name')
    categoryBriefName = s.CharField(source='short_name')

    class Meta:
        model = category
        fields = (
                'pk',
                'categoryId',
                'categoryName',
                'categoryBriefName',
                )

class getCategoriesResponseSerializer(WritableNestedModelSerializer):
    # Fields #
    categoryArray = categoryArray(many=True, source='category', partial=True)
    # errorMessage = bonanzaErrorMessage()
    # warnings = bonanzaWarnings()
    
    class Meta:
        model = response_log_detail
        fields = (
                #'pk',
                'categoryArray',
                # 'errorMessage',
                # 'warnings',
                )

class responseSerializer(WritableNestedModelSerializer):
    # Fields #
    getCategoriesResponse = getCategoriesResponseSerializer(source='response_log_detail' )
    ack = s.CharField(source='status')
    timestamp = s.DateTimeField()
    
    # errorMessage = bonanzaErrorMessage()
    # warnings = bonanzaWarnings()

    class Meta:
        model = response_log
        fields = (
        #'pk',
        
        'getCategoriesResponse',
        'ack',
        'timestamp',
        
        )

Run these to create the categories:

response = {'ack': 'Success',
 'getCategoriesResponse': {'categoryArray': [{'categoryBriefName': 'Collectibles',
    'categoryId': 1,
    'categoryLevel': 1,
    'categoryName': 'Collectibles',
    'leafCategory': 'false',
    'traitCount': 1},
   {'categoryBriefName': 'Everything Else',
    'categoryId': 99,
    'categoryLevel': 1,
    'categoryName': 'Everything Else',
    'leafCategory': 'false',
    'traitCount': 5}]},
 'timestamp': '2017-10-12T12:00:40.000Z',
 'version': '1.0'}


serializer = responseSerializer(data = response)
serializer.is_valid(raise_exception=True)
response_log = serializer.save()

Run these to try and update the categories: Changed name on Collectibles.

response = {'ack': 'Success',
 'getCategoriesResponse': {'categoryArray': [{'categoryBriefName': 'Collectibles - Name Change',
    'categoryId': 1,
    'categoryLevel': 1,
    'categoryName': 'Collectibles - Name Change',
    'leafCategory': 'false',
    'traitCount': 1},
   {'categoryBriefName': 'Everything Else',
    'categoryId': 99,
    'categoryLevel': 1,
    'categoryName': 'Everything Else',
    'leafCategory': 'false',
    'traitCount': 5}]},
 'timestamp': '2017-10-12T12:00:40.000Z',
 'version': '1.0'}


serializer = responseSerializer(data = response)
serializer.is_valid(raise_exception=True)
response_log = serializer.save()

DRFs built-in API not working, sending POST from elsewhere does

Hi,

Just started experimenting with drf-writable-nested and I couldn't get it to work. Very simple model, almost identical to your example in the README.md.
(User and Profile, profile has OneToOne with User)

I couldn't just get it to work with DRFs built-in API interface. It kept setting the profile to NULL and it was extremely frustrating. Until I found another issue here that had was also fixed by using a different client to interact with the API. So I tried that, and it worked.

The first one is the entry from the DRF API interface, and the second is from another REST client.

[
    {
        "pk": 1,
        "email": "[email protected]",
        "date_joined": "2018-05-01",
        "profile": null
    },
    {
        "pk": 2,
        "email": "[email protected]",
        "date_joined": "2018-05-01",
        "profile": {
            "pk": 1,
            "first_name": "Jake",
            "last_name": "Doe"
        }
    }
]

I do like DRFs built-in client, however.
Is there anything that may cause this issue, and is there any way to solve it?

EDIT:
Versions
Python 3.6
Django 2.0.4
DRF 3.8.2
drf-writable-nested 0.4.2

EDIT2:
More specifically, it seems to make the profile field NULL when sent form the HTML form window in DRF, while it does work normally when sent from the Raw data tab.

Support Python 3.7

For Travis what is needed is: travis-ci/travis-ci#9815 (comment)

I tried to create a big PR to do this in #59, but some of the builds didn't work with the updated matrix, perhaps because of mismatches discussed below.

Supported Django/Python versions are:
https://docs.djangoproject.com/en/2.1/faq/install/#what-python-version-can-i-use-with-django

Note that currently we test with versions of Python that aren't supported for the corresponding versions of Python. That will probably need to be fixed at some point. For example, Django 2.0 is tested with Python 2.7, but Django 2.0 doesn't officially support Python 2.7.

Avoid update to other instances

I need help on an issue I'm having and I don't see a standalone solution for this inside this project without overriding the parent serializer.

I have serializers setup like this:

class ChildSerializer(serializers.ModelSerializer):
    class Meta:
        model = Child
        fields = ('pk', 'name')


class ParentSerializer(WritableNestedModelSerializer):
    child = ChildSerializer()

    class Meta:
        model = Parent
        fields = ('pk', 'name', 'child')

I have two instances of each model set up like this;

>>> Parent.objects.values()
<QuerySet [{'id': 1, 'name': 'parent 1', 'child_id': 1}, {'id': 2, 'name': 'parent 2', 'child_id': 2}]>
>>> Child.objects.values()
<QuerySet [{'id': 1, 'name': 'child 1'}, {'id': 2, 'name': 'child 2'}]>

If I setup my POST or PATCH request like this:

POST /api/parent/1
{
    "name": "Changing parent 1", 
    "child": {
        "pk": 2, 
        "name": "Changing name of child 2"
    }
}

I end-up altering the child with id 2 and setting this value on parent 1.

>>> Parent.objects.values()
<QuerySet [{'id': 1, 'name': 'Changing parent 1', 'child_id': 2}, {'id': 2, 'name': 'parent 2', 'child_id': 2}]>
>>> Child.objects.values()
<QuerySet [{'id': 1, 'name': 'child 1'}, {'id': 2, 'name': 'Changing name of child 2'}]>

Is there a way to avoid this? Now I have to override validate_child on the parent serializer and check initial_data to see if the pk is different.

Add example project

There are a lot of corner cases were resolved in issues. It will be nice to add example project which will illustrate both simple and complicated cases, even cases drf-writable-nested do not support.

Can't add extra fields to serializer that is not on the model

Sometimes you need more information on the serializer than you are going to be saved on the model. This can't be done on serializers that inherit from the drf-writable-nested one's as it expects all serializer fields to be model fields as well. This is what happens if I e g add a field called custom_field:

File "/path/to/drf_writable_nested/mixins.py", line 73, in create
    relations, reverse_relations = self._extract_relations(validated_data)
File "/path/to/drf_writable_nested/mixins.py", line 19, in _extract_relations
    related_field, direct = self._get_related_field(field)
File "/path/to/drf_writable_nested/mixins.py", line 55, in _get_related_field
    related_field = model_class._meta.get_field(field.source)
File "/path/to/django/db/models/options.py", line 619, in get_field
    raise FieldDoesNotExist("%s has no field named '%s'" % (self.object_name, field_name))
django.core.exceptions.FieldDoesNotExist: Project has no field named 'custom_field'

Multipart PATCH requests cause m2m relation to be deleted

Hi,

Today I ran into this issue where the Post<>Author m2m relation was deleted when (partially) updating Post.title via a multipart form patch request. These are my findings so far:

My serializers (simplified):

class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        fields = ('id',)

class PostSerializer(WritableNestedModelSerializer):
    authors = AuthorSerializer(many=True)
    ...

    class Meta:
        fields = ('title', 'authors',)

When updating via the shell, all seems fine, authors relation is not removed:

>>> post = Post.objects.get(pk=9)
>>> serializer = PostSerializer(post, data={'title':'my title'}, partial=True)
>>> serializer.update(post, {'title':'my title'})
<Post: my title>
>>> serializer.is_valid()
True
>>> serializer.data
{'id': 9, 'title': 'my title', ... 'authors': [OrderedDict([('id', 3), ...])],}

But, when I'm updating the same object via a multi part form request, the Post<>Author relationship is removed on update:

curl --request PATCH \
  --url http://... \
  --form 'title=my title'

{
  "id": 9,
  "title": "my title",
  "authors": [], <---
  ...

Tested with djangorestframework==3.8.2, drf-writable-nested==0.5.1.

I'm happy to create a PR that fixes this issue, but I need some direction on how/where to fix this issue.

Related: #30

Getting Error The `.create()` method does not support writable nested fields by default

Hi,
First of all thanks for wonderful effort in developing such a useful package. Unfortunately I am facing an issue while I try to use it. My code is below

Models

from django.db import models

class A(models.Model):
	uid = models.CharField(max_length=300)

class B(models.Model):
	street = models.CharField(max_length=100, null=True, blank=True)
	model_a = models.ForeignKey(A, on_delete=models.CASCADE, related_name='matched_model')

Serializers

from rest_framework import serializers
from drf_writable_nested import WritableNestedModelSerializer

class BSerializer(serializers.ModelSerializer):
	class Meta:
		model = B
		fields = ('pk', 'street')

class ASerializer(WritableNestedModelSerializer):
	class Meta:
		model = A
		fields = ('pk', 'uid', 'addrs',)

	addrs = BSerializer(many=True)

I tried with following JSON

{
	"uid": "e923a7d0-2269-43c3-836e-9aa08a70d746",
	"addrs": [
	      {
		"street" : "1513 S Providence Rd, Columbia, Missouri, United States, 65211-0001"
	       }
	]
}

I get an error saying:

The `.create()` method does not support writable nested fields by default.
Write an explicit `.create()` method for serializer `app.serializers.ASerializer`, or set `read_only=True` on nested serializer fields.

Can anyone point out what I did wrong?
I am using

  • Django===2.1.2
  • Python===3.7.0
  • Djangorestframework===3.9.0
  • drf-writable-nested===0.5.1

Thanks in Advance!

Is support PATCH ?

This library is support PATCH ? When i tried PATCH, i got error:
django.utils.datastructures.MultiValueDictKeyError: 'my_modal_many_to_many'

AssertionError: The `.create()` method does not support writable nested fields by default.

I am having trouble getting this to work. I am still getting the following but I can very clearly see the .create in your code.

AssertionError: The .create() method does not support writable nested fields by default.

I am hoping you can help me as this is exactly what I need. I have tinkered this with for 2 days and have not been able to get around this AssertionError. Below is the code to reproduce. The data all looks correct, it just won't let me save it.

class category(models.Model):
    name = models.CharField(max_length=255, help_text="",blank=True)
    short_name = models.CharField(max_length=255, help_text="",blank=True)
    external_id = models.CharField(max_length=255, help_text="",blank=True)
   def __str__(self):
        return self.name

class response_log(models.Model):
    timestamp = models.DateTimeField()
    status = models.CharField(max_length = 25)
  
class response_log_detail(models.Model):
    response_log = models.ForeignKey(response_log)
    category = models.ManyToManyField(category)
from rest_framework import serializers as s
from drf_writable_nested import WritableNestedModelSerializer

class categoryArray(s.ModelSerializer):
    # Fields #
    categoryId = s.IntegerField(source='external_id')
    categoryName = s.CharField(source='name')
    categoryBriefName = s.CharField(source='short_name')

    class Meta:
        model = category
        fields = (
                'categoryId',
                'categoryName',
                'categoryBriefName',
                )

class getCategoriesResponse(WritableNestedModelSerializer):
    # Fields #
    categoryArray = categoryArray(many=True)
    # errorMessage = bonanzaErrorMessage()
    # warnings = bonanzaWarnings()
    class Meta:
        model = response_log_detail
        fields = (
                'pk',
                'categoryArray',
                # 'errorMessage',
                # 'warnings',
                )
class responseSerializer(WritableNestedModelSerializer):
    # Fields #
    getCategoriesResponse = getCategoriesResponse()
    ack = s.CharField(source='status')
    class Meta:
        model = response_log
        fields = (
        'pk',
        'ack',
        'getCategoriesResponse',
        'timestamp',
        )
response = {'ack': 'Success',
 'getCategoriesResponse': {'categoryArray': [{'categoryBriefName': 'Collectibles',
    'categoryId': 1,
    'categoryLevel': 1,
    'categoryName': 'Collectibles',
    'leafCategory': 'false',
    'traitCount': 1},
   {'categoryBriefName': 'Everything Else',
    'categoryId': 99,
    'categoryLevel': 1,
    'categoryName': 'Everything Else',
    'leafCategory': 'false',
    'traitCount': 5}]},
 'timestamp': '2017-10-12T12:00:40.000Z',
 'version': '1.0'}

serializer = responseSerializer(data = response)
serializer.is_valid(raise_exception=True)
response_log = serializer.save()

How to do a PUT request with nested object?

Hi, for example I have object Addreess:
'{ "zip": 122200, "country": { "id": 1, "title": "Italy" } }'

And I want to change address country to existing object France:
"country": { "id": 2, "title": "France" }

w can I do PUT request to object without creating new column in DB? Thank you.

Can this package be made to not depend on contenttypes?

I saw this package and was excited for the ease of using writable nested serializers. But then I realized it doesn't work in my app because I don't run django.contrib.contenttypes (nor do I want to for many reasons).

This is an open ended ask, I am happy to help if it can be done and if it would merit the effort.

Remove UniqueValidator from nested serializers on create method

Hello, first of all thank you for creating this library. I think it should be included in the core!

I'm having a problem with the nested user serializer. Validation prevents set an existing user on POST method:

HTTP 400 Bad Request
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
  
{
    "user": {
        "username": [
            "A user with that username already exists."
        ]
    }
}

My code:

class UserSerializer(HispaSerializerMixin, NestedCreateMixin, NestedUpdateMixin,
                     serializers.HyperlinkedModelSerializer):
    class Meta:
        model = get_user_model()
        fields = ('url', 'id', 'username', 'first_name', 'last_name', 'is_staff', 'is_active')

class GuardianUserPermSerializer(HispaSerializerMixin,
                                 NestedCreateMixin, NestedUpdateMixin,
                                 serializers.HyperlinkedModelSerializer):
    user = UserSerializer()

    class Meta:
        model = UserObjectPermission
        exclude = ()

I have investigated the matter a bit and have found a solution:
https://medium.com/django-rest-framework/dealing-with-unique-constraints-in-nested-serializers-dade33b831d9

However, drop validation is a very dirty solution. The validation should be removed only if the user already exists.

Thank you.

Atomic transaction

Now, to avoid partial object creation you should wrap your root serializer's save() into transaction.atomic.

I'm not sure about wrapping save() in the library in each serializer with mixin. What do you think?

Improve error messages for incorrectly defined fields

The problem described in #22. It is easy to make mistake in the source attribute, and you will always get a message: AssertionError: The .create() method does not support writable nested fields by default.

Need to add some checks for serializers fields.

Nested WritableNestedModelSerializer's fail to associate model tables correctly if in reverse alphabetic order

Minimal Example

Consider this minimal example:

from django.db import models
from drf_writable_nested import WritableNestedModelSerializer
from rest_framework.serializers import ModelSerializer


class House(models.Model):
    pass


class Cat(models.Model):
    house = models.ForeignKey(House, related_name='cats', on_delete=models.CASCADE, null=True)


class CatPoint(models.Model):
    cat = models.ForeignKey(Cat, related_name='location', on_delete=models.CASCADE, null=True)
    x = models.FloatField()
    y = models.FloatField()


class CatPointSerializer(ModelSerializer):
    class Meta:
        model = CatPoint
        fields = '__all__'


class CatSerializer(WritableNestedModelSerializer):
    location = CatPointSerializer(many=True)

    class Meta:
        model = Cat
        fields = '__all__'


class HouseSerializer(WritableNestedModelSerializer):
    cats = CatSerializer(many=True)

    class Meta:
        model = House
        fields = '__all__'


data = {
    "cats": [
        {
            "location": [
                {
                    "x": 1.0,
                    "y": 2.0
                }
            ]
        }
    ]
}

serializer = HouseSerializer(data=data)
a = serializer.is_valid()
serializer.save()

Results in the following traceback:

Unhandled exception in thread started by <_pydev_bundle.pydev_monkey._NewThreadStartupWithTrace object at 0x10723b940>
Traceback (most recent call last):
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/backends/utils.py", line 85, in _execute
    return self.cursor.execute(sql, params)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/backends/sqlite3/base.py", line 296, in execute
    return Database.Cursor.execute(self, query, params)
sqlite3.OperationalError: no such table: main.polls_cat__old

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Applications/PyCharm.app/Contents/helpers/pydev/_pydev_bundle/pydev_monkey.py", line 632, in __call__
    return self.original_func(*self.args, **self.kwargs)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/utils/autoreload.py", line 225, in wrapper
    fn(*args, **kwargs)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/core/management/commands/runserver.py", line 117, in inner_run
    self.check(display_num_errors=True)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/core/management/base.py", line 379, in check
    include_deployment_checks=include_deployment_checks,
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/core/management/base.py", line 366, in _run_checks
    return checks.run_checks(**kwargs)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/core/checks/registry.py", line 71, in run_checks
    new_errors = check(app_configs=app_configs)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/core/checks/urls.py", line 13, in check_url_config
    return check_resolver(resolver)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/core/checks/urls.py", line 23, in check_resolver
    return check_method()
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/urls/resolvers.py", line 396, in check
    for pattern in self.url_patterns:
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/utils/functional.py", line 37, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/urls/resolvers.py", line 533, in url_patterns
    patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/utils/functional.py", line 37, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/urls/resolvers.py", line 526, in urlconf_module
    return import_module(self.urlconf_name)
  File "/usr/local/Cellar/python/3.7.1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/user/PycharmProjects/django-test/proj/proj/urls.py", line 20, in <module>
    path('', include('polls.urls')),
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/urls/conf.py", line 34, in include
    urlconf_module = import_module(urlconf_module)
  File "/usr/local/Cellar/python/3.7.1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/user/PycharmProjects/django-test/proj/polls/urls.py", line 57, in <module>
    serializer.save()
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/drf_writable_nested/mixins.py", line 220, in save
    return super(BaseNestedModelSerializer, self).save(**kwargs)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/rest_framework/serializers.py", line 214, in save
    self.instance = self.create(validated_data)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/drf_writable_nested/mixins.py", line 248, in create
    self.update_or_create_reverse_relations(instance, reverse_relations)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/drf_writable_nested/mixins.py", line 175, in update_or_create_reverse_relations
    related_instance = serializer.save(**save_kwargs)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/drf_writable_nested/mixins.py", line 220, in save
    return super(BaseNestedModelSerializer, self).save(**kwargs)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/rest_framework/serializers.py", line 214, in save
    self.instance = self.create(validated_data)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/drf_writable_nested/mixins.py", line 248, in create
    self.update_or_create_reverse_relations(instance, reverse_relations)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/drf_writable_nested/mixins.py", line 175, in update_or_create_reverse_relations
    related_instance = serializer.save(**save_kwargs)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/rest_framework/serializers.py", line 214, in save
    self.instance = self.create(validated_data)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/rest_framework/serializers.py", line 940, in create
    instance = ModelClass._default_manager.create(**validated_data)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/models/query.py", line 413, in create
    obj.save(force_insert=True, using=self.db)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/models/base.py", line 718, in save
    force_update=force_update, update_fields=update_fields)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/models/base.py", line 748, in save_base
    updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/models/base.py", line 831, in _save_table
    result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/models/base.py", line 869, in _do_insert
    using=using, raw=raw)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/models/query.py", line 1136, in _insert
    return query.get_compiler(using=using).execute_sql(return_id)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 1289, in execute_sql
    cursor.execute(sql, params)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/backends/utils.py", line 100, in execute
    return super().execute(sql, params)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/backends/utils.py", line 68, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/backends/utils.py", line 77, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/backends/utils.py", line 85, in _execute
    return self.cursor.execute(sql, params)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/utils.py", line 89, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/backends/utils.py", line 85, in _execute
    return self.cursor.execute(sql, params)
  File "/Users/user/.local/share/virtualenvs/proj/lib/python3.7/site-packages/django/db/backends/sqlite3/base.py", line 296, in execute
    return Database.Cursor.execute(self, query, params)
django.db.utils.OperationalError: no such table: main.polls_cat__old

Note the suspicious polls_cat__old. This seems to be triggered because how models are created alphabetically:

$ ./manage.py makemigrations
Migrations for 'polls':
  polls/migrations/0001_initial.py
    - Create model Cat
    - Create model CatPoint
    - Create model House
    - Add field house to cat

Note how because Cat has a ForeignKey to House, its final creation is deferred and updated until after House is created after Cat.

If the code is refactored so that House -> AnimalHouse, so that it is first alphabetically:

from django.db import models
from drf_writable_nested import WritableNestedModelSerializer
from rest_framework.serializers import ModelSerializer


class AnimalHouse(models.Model):
    pass


class Cat(models.Model):
    house = models.ForeignKey(AnimalHouse, related_name='cats', on_delete=models.CASCADE, null=True)


class CatPoint(models.Model):
    cat = models.ForeignKey(Cat, related_name='location', on_delete=models.CASCADE, null=True)
    x = models.FloatField()
    y = models.FloatField()


class CatPointSerializer(ModelSerializer):
    class Meta:
        model = CatPoint
        fields = '__all__'


class CatSerializer(WritableNestedModelSerializer):
    location = CatPointSerializer(many=True)

    class Meta:
        model = Cat
        fields = '__all__'


class HouseSerializer(WritableNestedModelSerializer):
    cats = CatSerializer(many=True)

    class Meta:
        model = AnimalHouse
        fields = '__all__'


data = {
    "cats": [
        {
            "location": [
                {
                    "x": 1.0,
                    "y": 2.0
                }
            ]
        }
    ]
}

serializer = HouseSerializer(data=data)
a = serializer.is_valid()
serializer.save()

Note the output difference in makemigrations:

$ ./manage.py makemigrations
Migrations for 'polls':
  polls/migrations/0001_initial.py
    - Create model AnimalHouse
    - Create model Cat
    - Create model CatPoint

Now the same input data passes.

Environment

The exact versions used:

$ pip freeze
Django==2.1.4
django-rest-framework==0.1.0
djangorestframework==3.9.0
drf-writable-nested==0.5.1
pytz==2018.7

Database settings:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

The Problem

Is this a problem in drf-writable-nested, DRF itself, something else? IsWritableNestedModelSerializer somehow getting a reference to the temporary / old table?

Problem with User (out of the django box).

Hello, I tried to create User with profile:

in model.py:

from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, related_name='profile')
    city = models.CharField(max_length=250, blank=True, default='')

in serializers.py:

class ProfileSerializer(WritableNestedModelSerializer):
    class Meta:
        model = Profile
        fields = (
            'id',
            'city',
        )

class UserSerializer(WritableNestedModelSerializer):
    profile = ProfileSerializer()

    class Meta:
        model = User
        fields = (
            'id',
            'username',
            'email',
            'first_name',
            'last_name',
            'profile',
            'password',
        )
        extra_kwargs = {
            'password': {'write_only': True},
        }

then when I send it via Postman or shell, profile doesn't create.

to http://localhost:8000/api/users/ I send:

{
    "username": "wqeweeeeee",
    "email": "[email protected]",
    "first_name": "w",
    "last_name": "w",
    "profile": {
        "city": "e"
    },
    "password": "askdmkl"
}

and result is:

{
    "id": 14,
    "username": "wqeweeeeee",
    "email": "[email protected]",
    "first_name": "w",
    "last_name": "w",
    "profile": null
}

Other things works fine, where can be problem? Thank you.

PATCH-ing object's id

I've just stumbled upon a weird case, that I was able to update object's relations with sending PATCH request containing only the updated id (pointing to another object). As it's quite handy, I would use it in my code, but is it the supported way? My friend, just found that the error for missing object is in a wrong form - it's just HTML response withmatching query does not exist instead of regular JSON response.

Here are my example request/response pairs:*

1. PATCH /users/1/

Body:

{
  "address": {
      "id": 1 // Address is a FK relation and the one with ID=1 exists
  }
}

Response:

{
  "address": {
    "street": "first",
    "city": "first city
  }
}

Comment: in this case user.address__id is updated - not the id of Address object

2. PATCH /users/1/

Body:

{
  "address": {
      "id": 999 // Address with ID=999 doesn't exist
  }
}

Response:

DoesNotExist at /users/1/
Address matching query does not exist.

Removing nested objects from validated_data during create

I know this is an odd thing to do, I just want to see if its considered a bug.

Consider the following:

class ChildSerializer(serializers.ModelSerializer):
    class Meta:
        model = Child
        fields = ('id', 'name')

class ParentSerializer(WritableNestedModelSerializer):
    children = ChildSerializer(many=True)

    class Meta:
        model = Parent
        fields = ('id', 'name', 'children')

    def create(self, validated_data):
        del validated_data['children'][0]
        return super().create(validated_data)

# TEST
class RemovingValidatedDataTests(TestCase):

    def test_should_not_save_deleted(self):
        data = {
            'name': 'Fred Smith',
            'children': [
                {'name': 'Bobby'},
                {'name': 'Steve'}
            ]
        }
        serializer = ParentSerializer(data=data)
        self.assertTrue(serializer.is_valid())
        serializer.save()

        self.assertEqual(Parent.objects.count(), 1)
        self.assertEqual(Child.objects.count(), 1) . # fails is still 2

Using the above the Child.objects.count() is still 2 due to the fact that the ChildSerializer has the ParentSerializer.initial_data['children'] passed to it, and does not ever go near what was validated by the ParentSerializer.

Your thoughts would be appreciated. Thanks, Stu

RecursionError maximum recursion depth exceeded while calling a python object

i get this error "RecursionError maximum recursion depth exceeded while calling a python object" when calling serializer.save() method on SealCertificateSerializer
below is my serializers.

class MeterSerializer(serializers.ModelSerializer):
class Meta:
model = Meter
fields = 'all'

class SealSerializer(serializers.ModelSerializer):
class Meta:
model = Seal
fields = 'all'

class SealProfileSerializer(serializers.ModelSerializer):

class Meta:
    model = Profile
    fields = ( 'name', 'phone', 'signature', 'email', )

class SealCertificateSerializer(WritableNestedModelSerializer):
old_seal = SealSerializer(allow_null=True, many=False)
new_seal = SealSerializer(allow_null=True, many=False)
ibedc_rep = SealProfileSerializer(allow_null=True, many=False)
customer_rep = SealProfileSerializer(allow_null=True, many=False)
meter = MeterSerializer(allow_null=True, many=False)

class Meta:
    model = SealCertificate
    fields = ('account_number', 'customer_name', 'address', 'region', 'b_hub', 'auth_code', 'lat', 'lng', 'meter', 'old_seal', 'new_seal', 'ibedc_rep', 'customer_rep',)

def create(self, validated_data):
    sealcertificate_serializer = SealCertificateSerializer(data=validated_data)
    sealcertificate_serializer.is_valid(raise_exception=True)
    cert =sealcertificate_serializer.save()
    return cert.data`

thanks in advance

Update doesn't really update

Hi, I installed drf-writable-nested serializers on my Django REST API and I've seen that it doesn't really update the Reverse FK relations. I have these models:

class Question(models.Model):
    title = models.CharField(max_length=500)
    last_modified = models.DateField(auto_now=True)
    chapter = models.ForeignKey(Chapter, on_delete=models.CASCADE, related_name='questions')

    def __str__(self):
        return self.title

    class Meta:
        unique_together = (("id", "chapter"),)
        ordering = ['id']

class Answer(models.Model):
    title = models.CharField(max_length=1000)
    correct = models.BooleanField(default=False)
    question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='answers')

    def __str__(self):
        return self.title

    class Meta:
        unique_together = (("id", "question"),)
        ordering = ['id']

And these are my serializers:

class AnswerDetailSerializer(serializers.ModelSerializer):
    class Meta:
        model = Answer
        fields = ('id', 'title', 'correct')

class QuestionDetailSerializer(WritableNestedModelSerializer):
    answers = AnswerDetailSerializer(many=True)
    class Meta:
        model = Question
        fields = ('__all__')

So when I update a question with nested answers it just deletes all answers and create new ones, which gives me problems since I am also using the model Answers in another ManyToMany relation saving answers in Tests.

Is it because I use it wrong or it is just not updating but just delete and create? In that case I will have to write my own writable nested serializer.

OneToOne field partial_update with unique tel get unique error.

models.py:

class User(AbstractBaseUser, PermissionsMixin):
    tel = CharNullField(max_length=20,
                        unique=True,
                        null=True,
                        error_messages={
                            'unique': "The number is registered.",
                        },
                        verbose_name='tel')

class Staff(models.Model):
    user = models.OneToOneField('user.User',
                                null=True,
                                on_delete=models.CASCADE,
                                verbose_name='user ')

serializers.py:

class StaffUserCreateSerializer(ModelSerializer):

    def create(self, validated_data):
        groups = validated_data.pop('groups')
        instance = User.objects.create_user(**validated_data)
        instance.groups.add(*groups)
        return instance

    def update(self, instance, validated_data):
        password = validated_data.pop('password')
        super(StaffUserCreateSerializer, self).update(instance, validated_data)
        instance.set_password(password)
        instance.save()
        return instance

    def validate_password(self, value):
        password_validation.validate_password(value)
        return value

    class Meta:
        model = User
        fields = ('id', 'name', 'tel', 'email', 'password', 'is_active', 'is_staff', 'groups')

class StaffModifySerializer(WritableNestedModelSerializer):
    user = StaffUserCreateSerializer(label='User', partial=True)

    class Meta:
        model = Staff
        fields = ('user', 'job_state', 'bank_no', 'social_security_no', 'on_job')

tests.py:

class StaffTests(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(tel='18094213193', password='123456')
        self.user.user_permissions.add(*get_model_permission(Staff))
        token, _ = Token.objects.get_or_create(user=self.user)
        self.access_token = token.access_token
        self.group = Group.objects.create(name='test')
        self.data = {
            'user': {'name': 'YKH', 'tel': '17749503263',  'password': 'YKH123456']},
        }

    def test_staff_update(self):
        """update"""
        user = User.objects.create_user(tel='17749503263', password='YKH123456')
        instance = Staff.objects.create(user=user, no='17749503263')
        print('original๏ผš', instance.user.id)
        url = reverse('staff-detail', kwargs={'pk': instance.id})
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.access_token)
        data = {
            'user': {'id': 2, 'name': 'YKH1', 'tel': '17749503263', 'password': 'YKH654321'}
        }
        response = self.client.patch(url, data, format='json')
        print('update', json.dumps(response.data, ensure_ascii=False, indent=2))
        self.assertEqual(response.status_code, status.HTTP_200_OK)

when I run test_staff_update which update nested user with same tel,I get the error data": { "user": { "tel": [ "The number is registered." ] }, I know when use partial_update method you shouldn't pass the data do not need change,but i don't want do it in front end which use VUE.And I know this problem is cause by when get child field user,DRF don't pass instance kwargs to it.So i change my StaffModifySerializer to:

class StaffModifySerializer(WritableNestedModelSerializer):
    user = StaffUserCreateSerializer(label='user', partial=True)

    class Meta:
        model = Staff
        fields = ('user', 'job_state', 'bank_no', 'social_security_no', 'on_job')

    def __init__(self, *args, **kwargs):
        super(StaffModifySerializer, self).__init__(*args, **kwargs)
        import copy
        initial_data = copy.deepcopy(self.initial_data)
        relations, reverse_relations = self._extract_relations(initial_data)

        for field_name, (field, field_source) in relations.items():
            data = self.initial_data[field_name]
            model_class = field.Meta.model
            pk = self._get_related_pk(data, model_class)
            try:
                obj = model_class.objects.get(pk=pk, )

                self.fields[field_name] = self._get_serializer_for_field(
                    self.fields[field_name], instance=obj, data=data)
            except (model_class.DoesNotExist, ValueError):
                pass

the king mind is replace field user(StaffUserCreateSerializer(partial=True)) to new field user(StaffUserCreateSerializer(instance=User_by_id, partial=True)), I want to witre it inside WritableNestedModelSerializer but get error when use nested many_to_many,when Iโ€˜m free๏ผŒI will try to write PR.

[Question] Nested partial update with non-nullable fields.

I have something like:

class A(models.Model):
    x = models.IntegerField()
    y = models.IntegerField(default=0)

class B(models.Model):
    z = models.IntegerField()
    a = models.ForeignKey(A)

class BUpdateAPIView(UpdateAPIView):
   serializer_class = BUpdateSerializer
   ...

class AUpdateSerializer(ModelSerializer):
    class Meta:
         model = B
         fields = ('y', )

class BUpdateSerializer(WritableNestedModelSerializer):
    a = AUpdateSerializer()

    class Meta:
         model = B
         fields = ('z', 'a', )

And I'm trying to do a partial update (using method PATCH) on B, but it fails because x = null

KeyError in `delete_reverse_relations_if_need`?

Models

class Competition(models.Model):
    ...

class Phase(models.Model):
    competition = models.ForeignKey(Competition, on_delete=models.CASCADE, related_name='phases')
    ...

Serializers

class PhaseSerializer(WritableNestedModelSerializer):
    class Meta:
        model = Phase
        fields = (
            'id',
            # Competition is no longer referenced from here, I removed it some time ago to fix something,
            # I believe?
            # 'competition',
        )


class CompetitionSerializer(WritableNestedModelSerializer):
    producer = ProducerSerializer(required=False, validators=[])
    phases = PhaseSerializer(required=False, many=True)
    
   ...

    def create(self, validated_data):
        """
        This creates *AND* updates based on the combination of (remote_id, producer)
        """
        try:
            temp_instance = Competition.objects.get(
                remote_id=validated_data.get('remote_id'),
                producer__id=self.context['producer'].id
            )
        except ObjectDoesNotExist:
            temp_instance = None
        # If we have an existing instance from this producer
        # with the same remote_id, update it instead of making a new one
        if temp_instance:
            return self.update(temp_instance, validated_data)
        else:
            new_instance = super().create(validated_data)
            new_instance.producer = self.context['producer']
            new_instance.save()
            return new_instance

The problem

When creating a competition, things work fine. However, when I try to update the competition I receive this traceback:

Traceback (most recent call last):
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/django/core/handlers/exception.py", line 35, in inner
    response = get_response(request)
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/django/core/handlers/base.py", line 128, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/django/core/handlers/base.py", line 126, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/rest_framework/viewsets.py", line 103, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/rest_framework/views.py", line 483, in dispatch
    response = self.handle_exception(exc)
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/rest_framework/views.py", line 443, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/rest_framework/views.py", line 480, in dispatch
    response = handler(request, *args, **kwargs)
  File "/Users/eric/src/chahub/src/apps/api/views/competitions.py", line 43, in create
    self.perform_create(serializer)
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/rest_framework/mixins.py", line 26, in perform_create
    serializer.save()
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/rest_framework/serializers.py", line 728, in save
    self.instance = self.create(validated_data)
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/rest_framework/serializers.py", line 700, in create
    self.child.create(attrs) for attrs in validated_data
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/rest_framework/serializers.py", line 700, in <listcomp>
    self.child.create(attrs) for attrs in validated_data
  File "/Users/eric/src/chahub/src/apps/api/serializers/competitions.py", line 131, in create
    return self.update(temp_instance, validated_data)
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/drf_writable_nested/mixins.py", line 272, in update
    self.delete_reverse_relations_if_need(instance, reverse_relations)
  File "/Users/eric/.virtualenvs/chahub/lib/python3.6/site-packages/drf_writable_nested/mixins.py", line 285, in delete_reverse_relations_if_need
    related_data = self.get_initial()[field_name]
KeyError: 'phases'

Maybe I am doing something stupid in my create method? The serializer is meant to handle creating OR updating if the model already exists.

Thanks for any help!

Nested update validation fails when mixing partial and non-partial updates

I'm having some issues with mixing partial and non-partial updates after upgrading to v0.5.1. I believe the problem stems from this change. In my case I have an intermediate serializer that is being initialized as non-partial. As a result, a child serializer further down the chain is using the non-partial logic when performing validation when it should actually be partial.

My serializers are defined like this:

class ParentSerializer(WritableNestedModelSerializer):
    intermediates = IntermediateSerializer(required=False, many=True)

    class Meta:
        model = Parent
        fields = ('pk', 'intermediates', 'value_x', )

class IntermediateSerializer(WritableNestedModelSerializer):
    child = ChildSerializer()

    class Meta:
        model = Intermediate
        fields = ('pk', 'child', 'value_y', )

class ChildSerializer(ModelSerializer):
    class Meta:
        model = Child
        fields = ('pk', 'value_z', )

I'm performing a patch operation on an existing Parent object, trying to create a new Intermediate object tied to an existing Child object with pk = 1234. The patch data would look like:

{
   'intermediates': [
      {
         'child': {
            'pk': 1234
         }
      }
   ]
}

Trying to perform an update with this data results in the following error (value_z being a required field on the Child model):

{
    'intermediates': [
        {
            'child': {
                'value_z': [ErrorDetail(string='This field is required.', value_z='required')]
            }
        }
    ]
}

The ChildSerializer is trying to validate as if it's a non-partial update.

Any ideas on a good solution for this?

Problem with update - duplicate childs

Hello, I have a problem updating nested objects.
At first when I tried to update a nested objects with a simple PUT and objects with the ID but it duplicate my child objects.
Debugging I found all id fields wasn't in validated_data.
In request.data the id existed, but when I printed the validated_data on my serializer's redefined update method (this method just print and continue with default drf-writable-nested update) the ids didn't exists.
Then I tried to set read_only=True and required=False fields to id on my serializer, now the ids exists but I have integrity error on my sql.
It seems django is trying to create new objects with its id.
Some code:

MODELS:


class Persona(models.Model):
    nombres = models.CharField(max_length=100)
    // more simple fields ...
    sexo = models.ForeignKey(Sexo, related_name="personas",
                             on_delete=models.PROTECT,
                             blank=True,
                             null=True)
   // more related fields ...

class Padre(Persona):
    profesion = models.CharField(blank=True, max_length=100)
    // more simple fields ...

SERIALIZERS:

Latest child:

class PadreSerializer(WritableNestedModelSerializer):
    sexo = SexoSerializer(allow_null=True)
   // more related serialzier fields ...

    class Meta:
        model = Padre
        fields = ('id', 'sexo') #and more fields
        // I add this later.
        extra_kwargs = {
            'id': {'read_only': False, 'required': False},
        }

A first nested child:


class EstudianteNivelSecundarioSerializer(WritableNestedModelSerializer):
    sexo = SexoSerializer()
    papa = PadreSerializer(allow_null=True)
    mama = PadreSerializer(allow_null=True)
   
    class Meta:
        model = EstudianteNivelSecundario
        fields = ('id', 'papa', 'mama') #and more fields
    // I add this later.
    extra_kwargs = {
        'id': {'read_only': False, 'required': False},
    }

I send the put to this serializer:

class SolicitudAdmisionSecundarioSerializer(WritableNestedModelSerializer):
    estudiante_nivel_secundario = EstudianteNivelSecundarioSerializer()
    recaptcha = ReCaptchaField(write_only=True)

    class Meta:
        model = SolicitudAdmisionSecundario
        fields = ('id', 'estudiante_nivel_secundario', 'recaptcha') //and more fields
       // I add this later.
        extra_kwargs = {
            'id': {'read_only': False, 'required': False},
        }
    def update(self, instance, validated_data):
        print('VALIDATED', validated_data, flush=True)  // Ids didn't exists at first.
        print('INSTANCE', instance, flush=True) //Instance is ok.
        return super(SolicitudAdmisionSecundarioSerializer, self).update(instance, validated_data)

MYSQL ERROR:
https://pastebin.com/wQu6Sfqm
I hope I was clear. Thanks.

Support for other primary keys than 'pk' via a configuration

Custom primary key support has been worked on previously.

However, I believe I've encountered a use-case that has not been yet addressed: a primary key field that is not named pk and cannot be derived from the model definition via the Django _meta API.

Concretely, I've encountered this use-case on an existing project that uses multi-table inheritance extensively. I'm trying to integrate drf-writable-nested to simplify existing serializers. It has proven to be a perfect fit except for models that utilize multi-table inheritance.

Using the Django documentation's models as an example:

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

class Restaurant(Place):
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

With the following additional example model to illustrate the problem:

class UserFavorite(models.Model):
    user = models.ForeignKey( settings.AUTH_USER_MODEL)
    restaurant = models.ForeignKey(Restaurant)

In my API, I'd like to create a UserFavorite resource as follows:

{
   "user": 1068,
   "restaurants": [
        {
            "id": 34,
            "name": "Joe's Pizza",
            "address": "150 E 14th St",
            "serves_hot_dogs": false,
            "serves_pizza": true
        }
    ]
}

Assuming the correct serializers are configured, this is currently not possible with drf-writable-nested because the primary key field is not called pk and the attribute name for the primary key derived from the _meta API for the Restaurant model is place_ptr (which is automatically created by Django and as far as I know cannot be overridden). The automatically derived key (place_ptr) is both ugly and is difficult to build a convention around because each serializer that corresponds to multi-table inheritance models would need different key names.

I propose one (or both) of the following solutions:

  • Adding a setting that allows the user to globally define their convention for primary keys (e.g. pk, id, etc). django-rest-framework-jwt provides a good example of how settings could be added.
  • Adding an instance variable to BaseNestedModelSerializer that allows users to override the primary key field name on a per-serializer basis.

I look forward to adding a PR to address this use-case but would appreciate feedback before proceeding. Beyond projects that use multi-table inheritance, I see such a feature providing value to existing projects that want to use drf-writable-nested and have existing conventions around API payloads (i.e. where they have a single consistent name for primary keys that is not pk).

Foreign keys with on_delete=SET_NULL should not be deleted on update IMHO

Hi,

I have the following models (simplified for clarity):

class User(models.Model):
    first_name = models.CharField(
        'First name',
        max_length=150,
    )
    last_name = models.CharField(
        'Last name',
        max_length=150,
    )


class Badge(models.Model):
    number = models.BigIntegerField(
        'Number',
        primary_key=True,
        unique=True
    )
    user = models.ForeignKey(
        User,
        models.SET_NULL,
        verbose_name="Owner",
        related_name='badges',
        blank=True,
        null=True
    )

And the following serializers:

class UserBadgeSerializer(serializers.ModelSerializer):
    class Meta:
        model = Badge
        fields = ('number',)


class UserSerializer(WritableNestedModelSerializer):
    badges = UserBadgeSerializer(many=True, required=False)

    class Meta:
        model = User
        fields = (
            'pk', 'first_name', 'last_name', 'badges'
        )

As you can see a "Badge" can live without a user (hence the SET_NULL on the foreign key).
When I do the following request:


POST /api/users {
    "first_name": "Foo",
    "last_name": "Bar",
    "badges": [{"number": 1234}, {"number": 1235}]
}

All is fine, the user and badges are created and associated.
However, when I update the user to remove one of its badges:


PUT /api/users/1/ {
    "first_name": "Foo",
    "last_name": "Bar",
    "badges": [{"number": 1234}]
}

The badge 1235 is removed from the user (which is fine) but also gets deleted from the db.

I think (but I might be wrong) the correct behavior for foreign keys with on_delete != CASCADE, would be to only update the foreignkey and set it to None.

I have a working patch for this, if you're interested I can submit a pull request.

In short, in NestedUpdateMixin.delete_reverse_relations_if_need():

                    qs = model_class.objects.filter(pk__in=pks_to_delete)
                    if related_field.remote_field.on_delete == CASCADE:
                        qs.delete()
                    else:
                        qs.update(**{related_field.name: None})

Regards.

Update one related child model without deletion of other child models

How do you change a single related item, without deleting all other related models? If partial data is submitted delete_reverse_relations_if_need deletes every other model that is not included.

For example (with models from example):

class AvatarSerializer(serializers.ModelSerializer):
    image = serializers.CharField()

    class Meta:
        model = Avatar
        fields = ('pk', 'image',)

class ProfileSerializer(WritableNestedModelSerializer):
    # Reverse FK relation
    avatars = AvatarSerializer(many=True)

   class Meta:
        model = Profile
        fields = ('pk', 'avatars ',)

If the following avatars already exist:

'avatars': [
            {
                'pk': 1,
                'image': 'image-1.png',
            },
            {
               'pk': 2,
                'image': 'image-2.png',
            },
]

If I was to do a patch request with the following data:

data = {
    'pk': 1,
    'avatars': [
            {
                'pk': 1,
                'image': 'different-image-1.png',
            }
    ],
    },
}

Then it would delete the avatar object with pk=2. Why is this expected behavior? I want to patch a single related object, not update one object and delete every other object.

If I subclass NestedUpdateMixin and remove call to delete_reverse_relations_if_need then I get the expected result instead (the current pk=1 object is updated and pk=2 object is not deleted.

Integration with django-rest-polymorphic

Hi,

I am facing an issue when i try to integrate drf-writable-nested with django-rest-polymorphic. More specifically, if my model's depth is 2 or greater, the following error is thrown:

'ComponentA' object has no attribute '_save_kwargs'

Sample code, where the event occurs, can be seen below:

Models

from django.db import models
from polymorphic.models import PolymorphicModel

class Component(PolymorphicModel):
    class Meta:
        verbose_name = "Component"


class ComponentA(Component):
    class Meta:
        verbose_name = "ComponentA"

    subComponent = models.OneToOneField(
        "ComponentAA"
        on_delete=models.CASCADE
    )

class ComponentAA(models.Model):
    class Meta:
        verbose_name = "Corpus media"

    someText = models.Charfield(
        default = "some string"
    )

Serializers

from rest_framework import serializers, status, response
from drf_writable_nested import WritableNestedModelSerializer
from rest_polymorphic.serializers import PolymorphicSerializer
from .models import Component, ComponentA, ComponentAA

class ComponentAASerializer(serializer.ModelSerializer):
    class Meta:
        model = ComponentAA
        fields = ('id', 'someText')


class ComponentASerializer(WritableNestedModelSerializer):
    subComponent = ComponentAASerializer(many=False)

    class Meta:
        model = ComponentA
        fields = ('id', 'subComponent')


class ComponentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Component
        fields = '__all__'


class ComponentPolymorphicSerializer(PolymorphicSerializer, WritableNestedModelSerializer):
    class Meta:
        model = Component
        fields = '__all__'

    model_serializer_mapping = {
        Component: ComponentSerializer,
        ComponentA: ComponentASerializer
    }

    def to_resource_type(self, model_or_instance):
        return model_or_instance._meta.object_name.lower()

Any ideas on why this is happening?

Thank you in advance.

in NestedUpdateMixin would be better to delete_reverse before update_or_create_reverse

I think, if we have unique validator (let's say for field x),
and before request we had some related items
{"id":1, "x": 10},
{"id":2, "x": 5}

and in request
{
"id":42,
"stuf":"bar",
"related": [ {"id":2, "x": 10} ]
}

here we delete id=1, but the other "takes over" his value. (ps.: not tested)

so maybe switch the lines?

        self.update_or_create_reverse_relations(instance, reverse_relations)
        self.delete_reverse_relations_if_need(instance, reverse_relations)

Nested create ignore to_internal_value method

I need to abstract a nested representation:

// The request body I need
{
   "name": "the name",
   "topic_type": "the type"
}

instead of

{
    "name": "the name",
    "topic_type": {
        "topic_type": "the type"
    }
}

To achieve that, I use these serializers and models :

class TopicType(models.Model):
    topic_type = models.CharField(max_length=64)


class Topic(models.Model):
    name = models.CharField(max_length=64)
    topic_type = models.ForeignKey('TopicType', on_delete=models.CASCADE)



class TopicTypeSerializer(serializers.ModelSerializer):
    class Meta:
        model = TopicType
        fields = ('topic_type',)

    def to_representation(self, instance):
        return instance.topic_type

    def to_internal_value(self, data):
        return {'topic_type': data}

    def create(self, validated_data):
        return TopicType.objects.get_or_create(**validated_data)[0]


class TopicSerializer(WritableNestedModelSerializer):
    topic_type = TopicTypeSerializer()

    class Meta:
        model = Topic
        fields = ('name', 'topic_type')

On the line https://github.com/beda-software/drf-writable-nested/blob/master/drf_writable_nested/mixins.py#L174, it uses the initial_data and not the validated_data (so it doesn't use the to_internal_value's return) and crash with

File ".../lib/python3.6/site-packages/drf_writable_nested/mixins.py", line 108, in _get_related_pk
    pk = data.get('pk') or data.get(model_class._meta.pk.attname)
AttributeError: 'str' object has no attribute 'get'

My issue seems similar to #39
It would be nice to be able to customize nested deserialization behavior!

update_or_create_reverse_relations does not set parent on sub-serializers

I have a use case where I need to customise the create method of a nested serializer, because the model it creates depends on a field in the parent serializer.
If I override create(self, validated_data), self.parent (and self.root) are None within the method body. Normally self.parent and some other fields are set in rest_framework.fields.Field.bind() when the serializer is initially defined, but because update_or_create_reverse_relations creates a new serializer for the nested field, rather than mutating values on the existing one, these fields don't get set.

The simplest fix would be to add serializer.parent = self in one of two places indicated below, but I don't know whether there would be unintended side effects.

Another option would be to change the instance and/or data values on the the existing serializer (field in the code below) rather than creating a new one, but I presume there were side-effects from this that led to the current approach in the first place.

Within BaseNestedModelSerializer.update_or_create_reverse_relations:

for data in related_data:
    obj = instances.get(
        self._get_related_pk(data, field.Meta.model)
    )
    serializer = self._get_serializer_for_field(
        field,
        instance=obj,
        data=data,
    )
    # serializer.parent = self  # <== possibly add this here?
    serializer.is_valid(raise_exception=True)
    related_instance = serializer.save(**save_kwargs)

# elsewhere in BaseNestedModelSerializer
def _get_serializer_for_field(self, field, **kwargs):
    kwargs.update({
        'context': self.context,
        'partial': self.partial,
    })
    serializer = field.__class__(**kwargs)
    # serializer.parent = self  # <== alternatively add this here?
    return serializer

Duplicate child objects on PUT request

Duplicate child objects on PUT request

Hey! Sorry, if it's already covered.

I am working on a non profit project and i've 3 levels of deeply nested models.
Objects are created successfully when I use a POST request with below JSON data:

{
	"id": "4c0e3383-aa2d-4ad9-8dc4-1a40bd910ca0",
	"condition_block": {
		"id": "343feaef-23c1-456b-b6e2-d955a86fde9b",
		"referrer": {
			"id": "4de8a35e-b00f-4e64-af0b-9a03f67dfbc1",
			"list": [
				"Google None Profit"
			],
			"kind": "CONTAINS"
		},
		"name": "Google Non Profit Works"
	}
}

But when I issue a PUT request to /filters/4c0e3383-aa2d-4ad9-8dc4-1a40bd910ca0 with this JSON data:

{
	"condition_block": {
		"referrer": {
			"list": [
				"Google Non Profit"
			],
			"kind": "CONTAINS"
		}
	},
	"name": "Google Non Profit Works"
}

It get this error:

IntegrityError at /api/v1/filters/4c0e3383-aa2d-4ad9-8dc4-1a40bd910ca0
duplicate key value violates unique constraint "api_conditionblock_filter_id_key"
DETAIL:  Key (filter_id)=(4c0e3383-aa2d-4ad9-8dc4-1a40bd910ca0) already exists.

It just seems to create new child objects with the SAME parent id and fails.

Is this how it is designed to work?

Desired behavior

Fetch every child objects by ID and update them, especially when it's already known that they are in One to One relationship.

Models

class ReferrerCondition(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=200)
    condition_block = models.OneToOneField(
        ConditionBlock, null=True, related_name='referrer')


class ConditionBlock(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=200)
    filter = models.OneToOneField(
        Filter, null=True, related_name='condition_block')


class Filter(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=200)

Serializers


class ReferrerConditionSerializer(EnumFieldSerializerMixin,
                                  serializers.ModelSerializer):
    class Meta:
        model = ReferrerCondition
        exclude = ('condition_block', )


class ConditionBlockSerializer(WritableNestedModelSerializer):

    referrer = ReferrerConditionSerializer()

    class Meta:
        model = ConditionBlock
        exclude = ('filter', )


class FilterSerializer(EnumFieldSerializerMixin,
                       WritableNestedModelSerializer):

    condition_block = ConditionBlockSerializer()

    class Meta:
        model = Filter
        exclude = ()

Views

class FilterViewSet(viewsets.ModelViewSet):
    queryset = Filter.objects.all()
    serializer_class = FilterSerializer

Validation problem: custom validation errors raised from the nested serializer have a wrong path

I've used a custom validation method inside nested serializer and raised ValidationError.
For example:

class NestedSerializer(...):
    def validate_email(self, email):
        raise ValidationError(...)


class MySerializer(...):
    nested = NestedSerializer()

Usually, when DRF validated data with nested serializers, generic errors have correct path (e.g. { nested: { email: '' } })
In my example, errors have wrong path (e.g. { email: '' })

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.