diff --git a/.gitignore b/.gitignore index ff0958f1..ac41fdd8 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,7 @@ pip-delete-this-directory.txt # VirtualEnv .venv/ +# Developers *.sw* +manage.py +.DS_Store diff --git a/example/factories/__init__.py b/example/factories.py similarity index 100% rename from example/factories/__init__.py rename to example/factories.py diff --git a/example/models.py b/example/models.py index 6c1c6078..5cc70b47 100644 --- a/example/models.py +++ b/example/models.py @@ -28,6 +28,9 @@ class TaggedItem(BaseModel): def __str__(self): return self.tag + class Meta: + ordering = ('id',) + @python_2_unicode_compatible class Blog(BaseModel): @@ -38,6 +41,9 @@ class Blog(BaseModel): def __str__(self): return self.name + class Meta: + ordering = ('id',) + @python_2_unicode_compatible class Author(BaseModel): @@ -47,6 +53,9 @@ class Author(BaseModel): def __str__(self): return self.name + class Meta: + ordering = ('id',) + @python_2_unicode_compatible class AuthorBio(BaseModel): @@ -56,6 +65,9 @@ class AuthorBio(BaseModel): def __str__(self): return self.author.name + class Meta: + ordering = ('id',) + @python_2_unicode_compatible class Entry(BaseModel): @@ -73,6 +85,9 @@ class Entry(BaseModel): def __str__(self): return self.headline + class Meta: + ordering = ('id',) + @python_2_unicode_compatible class Comment(BaseModel): @@ -87,6 +102,9 @@ class Comment(BaseModel): def __str__(self): return self.body + class Meta: + ordering = ('id',) + class Project(PolymorphicModel): topic = models.CharField(max_length=30) diff --git a/example/settings/dev.py b/example/settings/dev.py index c5a1f742..fb969c45 100644 --- a/example/settings/dev.py +++ b/example/settings/dev.py @@ -25,6 +25,7 @@ 'rest_framework', 'polymorphic', 'example', + 'debug_toolbar', ] TEMPLATES = [ @@ -58,7 +59,11 @@ PASSWORD_HASHERS = ('django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', ) -MIDDLEWARE_CLASSES = () +MIDDLEWARE_CLASSES = ( + 'debug_toolbar.middleware.DebugToolbarMiddleware', +) + +INTERNAL_IPS = ('127.0.0.1', ) JSON_API_FORMAT_KEYS = 'camelize' JSON_API_FORMAT_TYPES = 'camelize' @@ -74,7 +79,13 @@ ), 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework_json_api.renderers.JSONRenderer', - 'rest_framework.renderers.BrowsableAPIRenderer', + + # If you're performance testing, you will want to use the browseable API + # without forms, as the forms can generate their own queries. + # If performance testing, enable: + 'example.utils.BrowsableAPIRendererWithoutForms', + # Otherwise, to play around with the browseable API, enable: + #'rest_framework.renderers.BrowsableAPIRenderer', ), 'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata', } diff --git a/example/tests/test_performance.py b/example/tests/test_performance.py new file mode 100644 index 00000000..c81c4d75 --- /dev/null +++ b/example/tests/test_performance.py @@ -0,0 +1,55 @@ +from django.utils import timezone +from rest_framework.test import APITestCase + +from example.factories import CommentFactory +from example.models import Author, Blog, Comment, Entry + + +class PerformanceTestCase(APITestCase): + def setUp(self): + self.author = Author.objects.create(name='Super powerful superhero', email='i.am@lost.com') + self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog") + self.other_blog = Blog.objects.create(name='Other blog', tagline="It's another blog") + self.first_entry = Entry.objects.create( + blog=self.blog, + headline='headline one', + body_text='body_text two', + pub_date=timezone.now(), + mod_date=timezone.now(), + n_comments=0, + n_pingbacks=0, + rating=3 + ) + self.second_entry = Entry.objects.create( + blog=self.blog, + headline='headline two', + body_text='body_text one', + pub_date=timezone.now(), + mod_date=timezone.now(), + n_comments=0, + n_pingbacks=0, + rating=1 + ) + self.comment = Comment.objects.create(entry=self.first_entry) + CommentFactory.create_batch(50) + + def test_query_count_no_includes(self): + """ We expect a simple list view to issue only two queries. + + 1. The number of results in the set (e.g. a COUNT query), only necessary because we're using PageNumberPagination + 2. The SELECT query for the set + """ + with self.assertNumQueries(2): + response = self.client.get('/comments?page_size=25') + self.assertEqual(len(response.data['results']), 25) + + def test_query_count_include_author(self): + """ We expect a list view with an include have three queries: + + 1. Primary resource COUNT query + 2. Primary resource SELECT + 3. Author's prefetched + """ + with self.assertNumQueries(3): + response = self.client.get('/comments?include=author&page_size=25') + self.assertEqual(len(response.data['results']), 25) diff --git a/example/tests/test_views.py b/example/tests/test_views.py index e8c11ff8..9d73a136 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -4,11 +4,10 @@ from django.utils import timezone from rest_framework.reverse import reverse from rest_framework.test import APITestCase, force_authenticate - from rest_framework_json_api.utils import format_resource_type from . import TestBase -from .. import views +from .. import factories, views from example.models import Author, Blog, Comment, Entry diff --git a/example/urls.py b/example/urls.py index 9c789274..d6b58f3d 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.conf.urls import include, url from rest_framework import routers @@ -22,3 +23,10 @@ urlpatterns = [ url(r'^', include(router.urls)), ] + + +if settings.DEBUG: + import debug_toolbar + urlpatterns = [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] + urlpatterns diff --git a/example/utils.py b/example/utils.py new file mode 100644 index 00000000..65403038 --- /dev/null +++ b/example/utils.py @@ -0,0 +1,20 @@ +from rest_framework.renderers import BrowsableAPIRenderer + + +class BrowsableAPIRendererWithoutForms(BrowsableAPIRenderer): + """Renders the browsable api, but excludes the forms.""" + + def get_context(self, *args, **kwargs): + ctx = super().get_context(*args, **kwargs) + ctx['display_edit_forms'] = False + return ctx + + def show_form_for_method(self, view, method, request, obj): + """We never want to do this! So just return False.""" + return False + + def get_rendered_html_form(self, data, view, method, request): + """Why render _any_ forms at all. This method should return + rendered HTML, so let's simply return an empty string. + """ + return "" diff --git a/example/views.py b/example/views.py index b65b96cf..ca87ca14 100644 --- a/example/views.py +++ b/example/views.py @@ -1,10 +1,9 @@ import rest_framework.parsers import rest_framework.renderers -from rest_framework import exceptions - import rest_framework_json_api.metadata import rest_framework_json_api.parsers import rest_framework_json_api.renderers +from rest_framework import exceptions from rest_framework_json_api.utils import format_drf_errors from rest_framework_json_api.views import ModelViewSet, RelationshipView diff --git a/requirements-development.txt b/requirements-development.txt index 01255bb2..8a06afd6 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -10,4 +10,6 @@ recommonmark Sphinx sphinx_rtd_theme tox +mock +django-debug-toolbar packaging==16.8 \ No newline at end of file diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 1b4e82ee..6c4d3c54 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -1,12 +1,17 @@ import collections import json +from collections import OrderedDict + +import six import inflection +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import NoReverseMatch from django.utils.translation import ugettext_lazy as _ from rest_framework.fields import MISSING_ERROR_MESSAGE -from rest_framework.relations import * # noqa: F403 +from rest_framework.relations import MANY_RELATION_KWARGS, PrimaryKeyRelatedField +from rest_framework.reverse import reverse from rest_framework.serializers import Serializer - from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.utils import ( Hyperlink, diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index 66d6add7..98c04189 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -4,7 +4,6 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import ParseError from rest_framework.serializers import * # noqa: F403 - from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.relations import ResourceRelatedField from rest_framework_json_api.utils import (