Skip to content

Commit

Permalink
Fix ModelViewSet Nested bug (#65)
Browse files Browse the repository at this point in the history
* Add todo repro app
* Add explicit test against model named "Nested"
* Force serializers named NestedSerializer to be output as inline models
* Allow ref_name to rescue a NestedSerializer
* Add tests and documentation
  • Loading branch information
axnsan12 authored Feb 22, 2018
1 parent 10c7e22 commit d507308
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 11 deletions.
9 changes: 9 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
Changelog
#########

*********
**1.4.2**
*********

- **FIXED:** fixed a bug that causes a ``ModelViewSet`` generated from models with nested ``ForeignKey`` to output
models named ``Nested`` into the ``definitions`` section (:issue:`59`, :pr:`65`)
- **FIXED:** ``Response`` objects without a ``schema`` are now properly handled when passed through
``swagger_auto_schema`` (:issue:`66`)

*********
**1.4.1**
*********
Expand Down
28 changes: 27 additions & 1 deletion docs/custom_spec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ You can define some per-serializer options by adding a ``Meta`` class to your se
Currently, the only option you can add here is

* ``ref_name`` - a string which will be used as the model definition name for this serializer class; setting it to
``None`` will force the serializer to be generated as an inline model everywhere it is used
``None`` will force the serializer to be generated as an inline model everywhere it is used. If two serializers
have the same ``ref_name``, both their usages will be replaced with a reference to the same definition.
If this option is not specified, all serializers have an implicit name derived from their class name, minus any
``Serializer`` suffix (e.g. ``UserSerializer`` -> ``User``, ``SerializerWithSuffix`` -> ``SerializerWithSuffix``)


*************************
Subclassing and extending
Expand Down Expand Up @@ -301,3 +305,25 @@ A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``t
This means that you should generally avoid view or method-specific ``FieldInspector``\ s if you are dealing with
references (a.k.a named models), because you can never know which view will be the first to generate the schema
for a given serializer.

**IMPORTANT:** nested fields on ``ModelSerializer``\ s that are generated from model ``ForeignKeys`` will always be
output by value. If you want the by-reference behaviour you have to explictly set the serializer class of nested
fields instead of letting ``ModelSerializer`` generate one automatically; for example:

.. code-block:: python
class OneSerializer(serializers.ModelSerializer):
class Meta:
model = SomeModel
fields = ('id',)
class AnotherSerializer(serializers.ModelSerializer):
chilf = OneSerializer()
class Meta:
model = SomeParentModel
fields = ('id', 'child')
Another caveat that stems from this is that any serializer named "``NestedSerializer``" will be forced inline
unless it has a ``ref_name`` set explicitly.
3 changes: 2 additions & 1 deletion src/drf_yasg/inspectors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ def SwaggerType(existing_object=None, **instance_kwargs):
if default is not None:
instance_kwargs['default'] = default

instance_kwargs.setdefault('title', title)
if instance_kwargs.get('type', None) != openapi.TYPE_ARRAY:
instance_kwargs.setdefault('title', title)
instance_kwargs.setdefault('description', description)
instance_kwargs.update(kwargs)

Expand Down
17 changes: 15 additions & 2 deletions src/drf_yasg/inspectors/field.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import operator
from collections import OrderedDict
from decimal import Decimal
Expand All @@ -12,6 +13,8 @@
from ..utils import decimal_as_float, filter_none
from .base import FieldInspector, NotHandled, SerializerInspector

logger = logging.getLogger(__name__)


class InlineSerializerInspector(SerializerInspector):
"""Provides serializer conversions using :meth:`.FieldInspector.field_to_swagger_object`."""
Expand Down Expand Up @@ -54,10 +57,14 @@ def field_to_swagger_object(self, field, swagger_object_type, use_references, **

serializer = field
serializer_meta = getattr(serializer, 'Meta', None)
serializer_name = type(serializer).__name__
if hasattr(serializer_meta, 'ref_name'):
ref_name = serializer_meta.ref_name
elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
logger.debug("Forcing inline output for ModelSerializer named 'NestedSerializer': " + str(serializer))
ref_name = None
else:
ref_name = type(serializer).__name__
ref_name = serializer_name
if ref_name.endswith('Serializer'):
ref_name = ref_name[:-len('Serializer')]

Expand All @@ -77,11 +84,17 @@ def make_schema_definition():
if child.required:
required.append(property_name)

return SwaggerType(
result = SwaggerType(
type=openapi.TYPE_OBJECT,
properties=properties,
required=required or None,
)
if not ref_name:
# on an inline model, the title is derived from the field name
# but is visually displayed like the model named, which is confusing
# it is better to just remove title from inline models
del result.title
return result

if not ref_name or not use_references:
return make_schema_definition()
Expand Down
1 change: 1 addition & 0 deletions testproj/testproj/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
'snippets',
'users',
'articles',
'todo',
]

MIDDLEWARE = [
Expand Down
1 change: 1 addition & 0 deletions testproj/testproj/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ def root_redirect(request):
url(r'^snippets/', include('snippets.urls')),
url(r'^articles/', include('articles.urls')),
url(r'^users/', include('users.urls')),
url(r'^todo/', include('todo.urls')),
url(r'^plain/', plain_view),
]
Empty file added testproj/todo/__init__.py
Empty file.
40 changes: 40 additions & 0 deletions testproj/todo/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-02-21 23:26
from __future__ import unicode_literals

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


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Todo',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='TodoAnother',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50)),
('todo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='todo.Todo')),
],
),
migrations.CreateModel(
name='TodoYetAnother',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50)),
('todo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='todo.TodoAnother')),
],
),
]
Empty file.
15 changes: 15 additions & 0 deletions testproj/todo/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.db import models


class Todo(models.Model):
title = models.CharField(max_length=50)


class TodoAnother(models.Model):
todo = models.ForeignKey(Todo, on_delete=models.CASCADE)
title = models.CharField(max_length=50)


class TodoYetAnother(models.Model):
todo = models.ForeignKey(TodoAnother, on_delete=models.CASCADE)
title = models.CharField(max_length=50)
24 changes: 24 additions & 0 deletions testproj/todo/serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from rest_framework import serializers

from .models import Todo, TodoAnother, TodoYetAnother


class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ('title',)


class TodoAnotherSerializer(serializers.ModelSerializer):
todo = TodoSerializer()

class Meta:
model = TodoAnother
fields = ('title', 'todo')


class TodoYetAnotherSerializer(serializers.ModelSerializer):
class Meta:
model = TodoYetAnother
fields = ('title', 'todo')
depth = 2
10 changes: 10 additions & 0 deletions testproj/todo/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from rest_framework import routers

from todo import views

router = routers.DefaultRouter()
router.register(r'', views.TodoViewSet)
router.register(r'another', views.TodoAnotherViewSet)
router.register(r'yetanother', views.TodoYetAnotherViewSet)

urlpatterns = router.urls
19 changes: 19 additions & 0 deletions testproj/todo/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from rest_framework import viewsets

from .models import Todo, TodoAnother, TodoYetAnother
from .serializer import TodoAnotherSerializer, TodoSerializer, TodoYetAnotherSerializer


class TodoViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Todo.objects.all()
serializer_class = TodoSerializer


class TodoAnotherViewSet(viewsets.ReadOnlyModelViewSet):
queryset = TodoAnother.objects.all()
serializer_class = TodoAnotherSerializer


class TodoYetAnotherViewSet(viewsets.ReadOnlyModelViewSet):
queryset = TodoYetAnother.objects.all()
serializer_class = TodoYetAnotherSerializer
Loading

0 comments on commit d507308

Please sign in to comment.