Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New-Style Serializers #101

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,58 @@ py.test
```


New-Style Serializers
================

In 2021, an enhanced set of mixins were added that permit fine-grained control of nested
Serializer behavior using a `match_on` argument. New-style serializers delegate control
of the Create/Update behavior to the nested Serializer. The parent Serializer need only
resolve nested serializers in the right order; this is handled by the `RelatedSaveMixin`.

New-style Serializers provide the following semantics:

- Get: retrieve a matching object (but DO NOT update)
- Update: retrieve and update a matching object
- Create: create an object using the entire payload
- Combinations of the above e.g. GetOrCreate and UpdateOrCreate

The matching of `data` to a specific `instance` is driven by a list of fields found in
`match_on`. This value is obtained from:

- the `match_on` kwarg provided when the field is initialized
- the DEFAULT_MATCH_ON class attribute

The new-style Serializers may be used as top-level Serializers to provide get-or-create
behaviors to DRF endpoints. Examples of use can be found in
`test_nested_serializer_mixins.py`.

Migration
---------

To convert an existing serializer to the new style serializers, the following procedure
is recommended:

1. Convert nested serializers by replacing `serializers.ModelSerializer` with
`UpdateOrCreateNestedSerializerMixin, serializers.ModelSerializer` which preserves
backwards-compatible behavior.
1. Convert parent serializer by replacing `WritableNestedModelSerializer` with
`RelatedSaveMixin, serializers.ModelSerializer`.
1. Verify that your test cases still pass.
1. Modify serializers (and test cases) to new-style behavior. For example, add an
explicit `match_on` or switch the mixin to an alternative behavior like
`GetOrCreateNestedSerializerMixin`.

All test cases were duplicated for new-style serializers so you can see examples of
converted serializers in `tests/serializers.py`. For example `TeamSerializer` and
`UserSerializer` become `NewTeamSerializer` and `NewUserSerializer`. Examples of
`DEFAULT_MATCH_ON` can be found in `tests/serializers.py`. One example of an explicit
specified `match_on` is present, but non-default `match_on` values are not found in
`tests` because they were not required to produce existing behaviors.

NOTE: While `RelatedSaveMixin` is the backwards-compatible mixin for the top-level
class, it is also possible to use other mixins to get complex matching behavior without
modifying the view.

Known problems with solutions
=============================

Expand Down
532 changes: 528 additions & 4 deletions drf_writable_nested/mixins.py

Large diffs are not rendered by default.

190 changes: 190 additions & 0 deletions tests/migrations/0002_auto_20210201_1452.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# Generated by Django 2.1.3 on 2021-02-01 14:52

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('tests', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='Child',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
],
),
migrations.CreateModel(
name='ContextChild',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='GrandParent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='LookupChild',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
],
),
migrations.CreateModel(
name='LookupGrandParent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='LookupOneToOneChild',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
],
),
migrations.CreateModel(
name='LookupParent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('child', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parent', to='tests.LookupChild')),
('child2', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parent2', to='tests.LookupChild')),
],
),
migrations.CreateModel(
name='LookupReverseChild',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='tests.LookupParent')),
],
),
migrations.CreateModel(
name='M2MSource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
],
),
migrations.CreateModel(
name='M2MTarget',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
],
),
migrations.CreateModel(
name='NewProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('age', models.IntegerField()),
],
),
migrations.CreateModel(
name='NewUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.TextField()),
],
),
migrations.CreateModel(
name='Parent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('child', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.Child')),
],
),
migrations.CreateModel(
name='ParentMany',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('children', models.ManyToManyField(to='tests.Child')),
],
),
migrations.CreateModel(
name='ReadOnlyChild',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
],
),
migrations.CreateModel(
name='ReadOnlyParent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('child', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.ReadOnlyChild')),
],
),
migrations.CreateModel(
name='ReverseChild',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
],
),
migrations.CreateModel(
name='ReverseManyChild',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
],
),
migrations.CreateModel(
name='ReverseManyParent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='ReverseParent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.AddField(
model_name='reversemanychild',
name='parent',
field=models.ManyToManyField(related_name='children', to='tests.ReverseManyParent'),
),
migrations.AddField(
model_name='reversechild',
name='parent',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='tests.ReverseParent'),
),
migrations.AddField(
model_name='newprofile',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to='tests.NewUser'),
),
migrations.AddField(
model_name='m2msource',
name='forward',
field=models.ManyToManyField(related_name='reverse', to='tests.M2MTarget'),
),
migrations.AddField(
model_name='lookuponetoonechild',
name='parent',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='one_to_one', to='tests.LookupParent'),
),
migrations.AddField(
model_name='lookupgrandparent',
name='child',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.LookupParent'),
),
migrations.AddField(
model_name='grandparent',
name='child',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.Parent'),
),
]
89 changes: 89 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import uuid

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models
Expand Down Expand Up @@ -147,3 +149,90 @@ class I86Name(models.Model):
class I86Genre(models.Model):
pass


class ReadOnlyChild(models.Model):
name = models.TextField()


class ReadOnlyParent(models.Model):
child = models.ForeignKey(ReadOnlyChild, on_delete=models.CASCADE)


class Child(models.Model):
name = models.TextField()


class Parent(models.Model):
child = models.ForeignKey(Child, on_delete=models.CASCADE)


class ParentMany(models.Model):
children = models.ManyToManyField(Child)


class ReverseParent(models.Model):
pass


class ReverseChild(models.Model):
name = models.TextField()
parent = models.ForeignKey(ReverseParent, on_delete=models.CASCADE, related_name='children')


class ReverseManyParent(models.Model):
pass


class ReverseManyChild(models.Model):
name = models.TextField()
parent = models.ManyToManyField(ReverseManyParent, related_name='children')


class LookupChild(models.Model):
name = models.TextField()


class LookupParent(models.Model):
child = models.ForeignKey(LookupChild, on_delete=models.CASCADE, related_name='parent')
child2 = models.ForeignKey(LookupChild, on_delete=models.CASCADE, related_name='parent2')


class LookupReverseChild(models.Model):
name = models.TextField()
parent = models.ForeignKey(LookupParent, on_delete=models.CASCADE, related_name='children')


class LookupOneToOneChild(models.Model):
name = models.TextField()
parent = models.OneToOneField(LookupParent, on_delete=models.CASCADE, related_name='one_to_one')


class LookupGrandParent(models.Model):
child = models.ForeignKey(LookupParent, on_delete=models.CASCADE)


class M2MTarget(models.Model):
name = models.TextField()


class M2MSource(models.Model):
forward = models.ManyToManyField(M2MTarget, related_name='reverse')
name = models.TextField()


class GrandParent(models.Model):
child = models.ForeignKey(Parent, on_delete=models.CASCADE)


class ContextChild(models.Model):
name = models.TextField()
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)


class NewUser (models.Model):
username = models.TextField()


class NewProfile(models.Model):
user = models.OneToOneField(NewUser, on_delete=models.CASCADE, related_name='profile')
age = models.IntegerField()
Loading