Merge pull request #2161 from NextHub/master

Move models to respective test modules
This commit is contained in:
Tom Christie 2014-12-02 08:29:22 +00:00
commit d847e336c5
8 changed files with 51 additions and 144 deletions

View File

@ -3,7 +3,7 @@
[![build-status-image]][travis] [![build-status-image]][travis]
[![pypi-version]][pypi] [![pypi-version]][pypi]
**Awesome web-browseable Web APIs.** **Awesome web-browsable Web APIs.**
Full documentation for the project is available at [http://www.django-rest-framework.org][docs]. Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
@ -19,7 +19,7 @@ Django REST framework is a powerful and flexible toolkit for building Web APIs.
Some reasons you might want to use REST framework: Some reasons you might want to use REST framework:
* The [Web browseable API][sandbox] is a huge useability win for your developers. * The [Web browsable API][sandbox] is a huge usability win for your developers.
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box. * [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources. * [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers]. * Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
@ -27,7 +27,7 @@ Some reasons you might want to use REST framework:
There is a live example API for testing purposes, [available here][sandbox]. There is a live example API for testing purposes, [available here][sandbox].
**Below**: *Screenshot from the browseable API* **Below**: *Screenshot from the browsable API*
![Screenshot][image] ![Screenshot][image]
@ -86,7 +86,7 @@ router.register(r'users', UserViewSet)
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browseable API. # Additionally, we include login URLs for the browsable API.
urlpatterns = [ urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))

View File

@ -33,7 +33,7 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b
Some reasons you might want to use REST framework: Some reasons you might want to use REST framework:
* The [Web browseable API][sandbox] is a huge usability win for your developers. * The [Web browsable API][sandbox] is a huge usability win for your developers.
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box. * [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources. * [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers]. * Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
@ -134,7 +134,7 @@ Here's our project's root `urls.py` module:
router.register(r'users', UserViewSet) router.register(r'users', UserViewSet)
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browseable API. # Additionally, we include login URLs for the browsable API.
urlpatterns = [ urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))

View File

@ -35,7 +35,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browseable API. # Additionally, we include login URLs for the browsable API.
urlpatterns = [ urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
@ -207,9 +207,9 @@ The old-style signature will continue to function but will raise a `PendingDepre
## View names and descriptions ## View names and descriptions
The mechanics of how the names and descriptions used in the browseable API are generated has been modified and cleaned up somewhat. The mechanics of how the names and descriptions used in the browsable API are generated has been modified and cleaned up somewhat.
If you've been customizing this behavior, for example perhaps to use `rst` markup for the browseable API, then you'll need to take a look at the implementation to see what updates you need to make. If you've been customizing this behavior, for example perhaps to use `rst` markup for the browsable API, then you'll need to take a look at the implementation to see what updates you need to make.
Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated. Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated.

View File

@ -121,7 +121,7 @@ You can determine your currently installed version using `pip freeze`:
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode. * Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode.
* Fix `parse_header` argument convertion. * Fix `parse_header` argument convertion.
* Fix mediatype detection under Python 3. * Fix mediatype detection under Python 3.
* Web browseable API now offers blank option on dropdown when the field is not required. * Web browsable API now offers blank option on dropdown when the field is not required.
* `APIException` representation improved for logging purposes. * `APIException` representation improved for logging purposes.
* Allow source="*" within nested serializers. * Allow source="*" within nested serializers.
* Better support for custom oauth2 provider backends. * Better support for custom oauth2 provider backends.
@ -200,7 +200,7 @@ You can determine your currently installed version using `pip freeze`:
* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. * Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute.
* Added `cache` attribute to throttles to allow overriding of default cache. * Added `cache` attribute to throttles to allow overriding of default cache.
* 'Raw data' tab in browsable API now contains pre-populated data. * 'Raw data' tab in browsable API now contains pre-populated data.
* 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views. * 'Raw data' and 'HTML form' tab preference in browsable API now saved between page views.
* Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `required=True` argument fixed for boolean serializer fields.
* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
* Bugfix: Client sending empty string instead of file now clears `FileField`. * Bugfix: Client sending empty string instead of file now clears `FileField`.

View File

@ -112,7 +112,7 @@ Here's our re-wired `urls.py` file.
router.register(r'users', views.UserViewSet) router.register(r'users', views.UserViewSet)
# The API URLs are now determined automatically by the router. # The API URLs are now determined automatically by the router.
# Additionally, we include the login URLs for the browseable API. # Additionally, we include the login URLs for the browsable API.
urlpatterns = [ urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
@ -130,7 +130,7 @@ That doesn't mean it's always the right approach to take. There's a similar set
## Reviewing our work ## Reviewing our work
With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats. With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views. We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.

View File

@ -100,7 +100,7 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
router.register(r'groups', views.GroupViewSet) router.register(r'groups', views.GroupViewSet)
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browseable API. # Additionally, we include login URLs for the browsable API.
urlpatterns = [ urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))

View File

@ -3,62 +3,20 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
def foobar():
return 'foobar'
class CustomField(models.CharField):
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 12
super(CustomField, self).__init__(*args, **kwargs)
class RESTFrameworkModel(models.Model): class RESTFrameworkModel(models.Model):
""" """
Base for test models that sets app_label, so they play nicely. Base for test models that sets app_label, so they play nicely.
""" """
class Meta: class Meta:
app_label = 'tests' app_label = 'tests'
abstract = True abstract = True
class HasPositiveIntegerAsChoice(RESTFrameworkModel):
some_choices = ((1, 'A'), (2, 'B'), (3, 'C'))
some_integer = models.PositiveIntegerField(choices=some_choices)
class Anchor(RESTFrameworkModel):
text = models.CharField(max_length=100, default='anchor')
class BasicModel(RESTFrameworkModel): class BasicModel(RESTFrameworkModel):
text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description.")) text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description."))
class SlugBasedModel(RESTFrameworkModel):
text = models.CharField(max_length=100)
slug = models.SlugField(max_length=32)
class DefaultValueModel(RESTFrameworkModel):
text = models.CharField(default='foobar', max_length=100)
extra = models.CharField(blank=True, null=True, max_length=100)
class CallableDefaultValueModel(RESTFrameworkModel):
text = models.CharField(default=foobar, max_length=100)
class ManyToManyModel(RESTFrameworkModel):
rel = models.ManyToManyField(Anchor, help_text='Some help text.')
class ReadOnlyManyToManyModel(RESTFrameworkModel):
text = models.CharField(max_length=100, default='anchor')
rel = models.ManyToManyField(Anchor)
class BaseFilterableItem(RESTFrameworkModel): class BaseFilterableItem(RESTFrameworkModel):
text = models.CharField(max_length=100) text = models.CharField(max_length=100)
@ -71,73 +29,6 @@ class FilterableItem(BaseFilterableItem):
date = models.DateField() date = models.DateField()
# Model for regression test for #285
class Comment(RESTFrameworkModel):
email = models.EmailField()
content = models.CharField(max_length=200)
created = models.DateTimeField(auto_now_add=True)
class ActionItem(RESTFrameworkModel):
title = models.CharField(max_length=200)
started = models.NullBooleanField(default=False)
done = models.BooleanField(default=False)
info = CustomField(default='---', max_length=12)
# Models for reverse relations
class Person(RESTFrameworkModel):
name = models.CharField(max_length=10)
age = models.IntegerField(null=True, blank=True)
@property
def info(self):
return {
'name': self.name,
'age': self.age,
}
class BlogPost(RESTFrameworkModel):
title = models.CharField(max_length=100)
writer = models.ForeignKey(Person, null=True, blank=True)
def get_first_comment(self):
return self.blogpostcomment_set.all()[0]
class BlogPostComment(RESTFrameworkModel):
text = models.TextField()
blog_post = models.ForeignKey(BlogPost)
class Album(RESTFrameworkModel):
title = models.CharField(max_length=100, unique=True)
ref = models.CharField(max_length=10, unique=True, null=True, blank=True)
class Photo(RESTFrameworkModel):
description = models.TextField()
album = models.ForeignKey(Album)
# Model for issue #324
class BlankFieldModel(RESTFrameworkModel):
title = models.CharField(max_length=100, blank=True, null=False,
default="title")
# Model for issue #380
class OptionalRelationModel(RESTFrameworkModel):
other = models.ForeignKey('OptionalRelationModel', blank=True, null=True)
# Model for RegexField
class Book(RESTFrameworkModel):
isbn = models.CharField(max_length=13)
# Models for relations tests # Models for relations tests
# ManyToMany # ManyToMany
class ManyToManyTarget(RESTFrameworkModel): class ManyToManyTarget(RESTFrameworkModel):

View File

@ -6,12 +6,26 @@ from django.test import TestCase
from django.utils import six from django.utils import six
from rest_framework import generics, renderers, serializers, status from rest_framework import generics, renderers, serializers, status
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from tests.models import BasicModel, Comment, SlugBasedModel from tests.models import BasicModel, RESTFrameworkModel
from tests.models import ForeignKeySource, ForeignKeyTarget from tests.models import ForeignKeySource, ForeignKeyTarget
factory = APIRequestFactory() factory = APIRequestFactory()
# Models
class SlugBasedModel(RESTFrameworkModel):
text = models.CharField(max_length=100)
slug = models.SlugField(max_length=32)
# Model for regression test for #285
class Comment(RESTFrameworkModel):
email = models.EmailField()
content = models.CharField(max_length=200)
created = models.DateTimeField(auto_now_add=True)
# Serializers
class BasicSerializer(serializers.ModelSerializer): class BasicSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = BasicModel model = BasicModel
@ -22,6 +36,15 @@ class ForeignKeySerializer(serializers.ModelSerializer):
model = ForeignKeySource model = ForeignKeySource
class SlugSerializer(serializers.ModelSerializer):
slug = serializers.ReadOnlyField()
class Meta:
model = SlugBasedModel
fields = ('text', 'slug')
# Views
class RootView(generics.ListCreateAPIView): class RootView(generics.ListCreateAPIView):
queryset = BasicModel.objects.all() queryset = BasicModel.objects.all()
serializer_class = BasicSerializer serializer_class = BasicSerializer
@ -37,14 +60,6 @@ class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ForeignKeySerializer serializer_class = ForeignKeySerializer
class SlugSerializer(serializers.ModelSerializer):
slug = serializers.ReadOnlyField()
class Meta:
model = SlugBasedModel
fields = ('text', 'slug')
class SlugBasedInstanceView(InstanceView): class SlugBasedInstanceView(InstanceView):
""" """
A model with a slug-field. A model with a slug-field.
@ -54,6 +69,7 @@ class SlugBasedInstanceView(InstanceView):
lookup_field = 'slug' lookup_field = 'slug'
# Tests
class TestRootView(TestCase): class TestRootView(TestCase):
def setUp(self): def setUp(self):
""" """
@ -127,13 +143,13 @@ class TestRootView(TestCase):
self.assertEqual(created.text, 'foobar') self.assertEqual(created.text, 'foobar')
EXPECTED_QUERYS_FOR_PUT = 3 if django.VERSION < (1, 6) else 2 EXPECTED_QUERIES_FOR_PUT = 3 if django.VERSION < (1, 6) else 2
class TestInstanceView(TestCase): class TestInstanceView(TestCase):
def setUp(self): def setUp(self):
""" """
Create 3 BasicModel intances. Create 3 BasicModel instances.
""" """
items = ['foo', 'bar', 'baz', 'filtered out'] items = ['foo', 'bar', 'baz', 'filtered out']
for item in items: for item in items:
@ -173,7 +189,7 @@ class TestInstanceView(TestCase):
""" """
data = {'text': 'foobar'} data = {'text': 'foobar'}
request = factory.put('/1', data, format='json') request = factory.put('/1', data, format='json')
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT): with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
response = self.view(request, pk='1').render() response = self.view(request, pk='1').render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'}) self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'})
@ -187,7 +203,7 @@ class TestInstanceView(TestCase):
data = {'text': 'foobar'} data = {'text': 'foobar'}
request = factory.patch('/1', data, format='json') request = factory.patch('/1', data, format='json')
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT): with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
response = self.view(request, pk=1).render() response = self.view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
@ -222,7 +238,7 @@ class TestInstanceView(TestCase):
""" """
data = {'id': 999, 'text': 'foobar'} data = {'id': 999, 'text': 'foobar'}
request = factory.put('/1', data, format='json') request = factory.put('/1', data, format='json')
with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT): with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
response = self.view(request, pk=1).render() response = self.view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
@ -288,9 +304,10 @@ class TestOverriddenGetObject(TestCase):
Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the
queryset/model mechanism but instead overrides get_object() queryset/model mechanism but instead overrides get_object()
""" """
def setUp(self): def setUp(self):
""" """
Create 3 BasicModel intances. Create 3 BasicModel instances.
""" """
items = ['foo', 'bar', 'baz'] items = ['foo', 'bar', 'baz']
for item in items: for item in items:
@ -363,11 +380,11 @@ class ClassB(models.Model):
class ClassA(models.Model): class ClassA(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
childs = models.ManyToManyField(ClassB, blank=True, null=True) children = models.ManyToManyField(ClassB, blank=True, null=True)
class ClassASerializer(serializers.ModelSerializer): class ClassASerializer(serializers.ModelSerializer):
childs = serializers.PrimaryKeyRelatedField( children = serializers.PrimaryKeyRelatedField(
many=True, queryset=ClassB.objects.all() many=True, queryset=ClassB.objects.all()
) )
@ -380,8 +397,8 @@ class ExampleView(generics.ListCreateAPIView):
queryset = ClassA.objects.all() queryset = ClassA.objects.all()
class TestM2MBrowseableAPI(TestCase): class TestM2MBrowsableAPI(TestCase):
def test_m2m_in_browseable_api(self): def test_m2m_in_browsable_api(self):
""" """
Test for particularly ugly regression with m2m in browsable API Test for particularly ugly regression with m2m in browsable API
""" """
@ -424,7 +441,6 @@ class DynamicSerializerView(generics.ListCreateAPIView):
class TestFilterBackendAppliedToViews(TestCase): class TestFilterBackendAppliedToViews(TestCase):
def setUp(self): def setUp(self):
""" """
Create 3 BasicModel instances to filter on. Create 3 BasicModel instances to filter on.