Merge branch 'master' into test/multipart_nested_serializer

This commit is contained in:
James Keys 2016-03-07 15:44:54 +08:00
commit a75c80d509
44 changed files with 264 additions and 204 deletions

View File

@ -1,5 +1,8 @@
language: python
python:
- "3.5"
sudo: false
env:
@ -12,19 +15,13 @@ env:
- TOX_ENV=py33-django18
- TOX_ENV=py32-django18
- TOX_ENV=py27-django18
- TOX_ENV=py34-django17
- TOX_ENV=py33-django17
- TOX_ENV=py32-django17
- TOX_ENV=py27-django17
matrix:
# Python 3.5 not yet available on travis, watch this to see when it is.
fast_finish: true
allow_failures:
- env: TOX_ENV=py35-django19
install:
- pip install tox
# Virtualenv < 14 is required to keep the Python 3.2 builds running.
- pip install tox "virtualenv<14"
script:
- tox -e $TOX_ENV

View File

@ -33,7 +33,7 @@ Some tips on good issue reporting:
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation.
* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability, bug fixes, and great documentation.
* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened.
## Triaging issues
@ -118,10 +118,6 @@ Always run the tests before submitting pull requests, and ideally run `tox` in o
Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect.
![Travis status][travis-status]
*Above: Travis build notifications*
## Managing compatibility issues
Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use.
@ -203,7 +199,6 @@ If you want to draw attention to a note or warning, use a pair of enclosing line
[so-filter]: http://stackexchange.com/filters/66475/rest-framework
[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
[pep-8]: http://www.python.org/dev/peps/pep-0008/
[travis-status]: ../img/travis-status.png
[pull-requests]: https://help.github.com/articles/using-pull-requests
[tox]: http://tox.readthedocs.org/en/latest/
[markdown]: http://daringfireball.net/projects/markdown/basics

14
ISSUE_TEMPLATE.md Normal file
View File

@ -0,0 +1,14 @@
## Checklist
- [ ] I have verified that that issue exists against the `master` branch of Django REST framework.
- [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
- [ ] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.)
- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](http://www.django-rest-framework.org/topics/third-party-resources/#about-third-party-packages) where possible.)
- [ ] I have reduced the issue to the simplest possible case.
- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)
## Steps to reproduce
## Expected behavior
## Actual behavior

View File

@ -1,6 +1,6 @@
# License
Copyright (c) 2011-2015, Tom Christie
Copyright (c) 2011-2016, Tom Christie
All rights reserved.
Redistribution and use in source and binary forms, with or without

5
PULL_REQUEST_TEMPLATE.md Normal file
View File

@ -0,0 +1,5 @@
*Note*: Before submitting this pull request, please review our [contributing guidelines](https://github.com/tomchristie/django-rest-framework/blob/master/CONTRIBUTING.md#pull-requests).
## Description
Please describe your pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. When linking to an issue, please use `refs #...` in the description of the pull request.

View File

@ -61,7 +61,9 @@ Startup up a new project like so...
pip install django
pip install djangorestframework
django-admin.py startproject example .
./manage.py syncdb
./manage.py migrate
./manage.py createsuperuser
Now edit the `example/urls.py` module in your project:

View File

@ -81,7 +81,13 @@ A text string that may be used as a description of the field in HTML form fields
### `initial`
A value that should be used for pre-populating the value of HTML form fields.
A value that should be used for pre-populating the value of HTML form fields. You may pass a callable to it, just as
you may do with any regular Django `Field`:
import datetime
from rest_framework import serializers
class ExampleSerializer(serializers.Serializer):
day = serializers.DateField(initial=datetime.date.today)
### `style`
@ -96,9 +102,9 @@ Two examples here are `'input_type'` and `'base_template'`:
# Use a radio input instead of a select input.
color_channel = serializers.ChoiceField(
choices=['red', 'green', 'blue']
style = {'base_template': 'radio.html'}
}
choices=['red', 'green', 'blue'],
style={'base_template': 'radio.html'}
)
For more details see the [HTML & Forms][html-and-forms] documentation.

View File

@ -317,4 +317,4 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin`
[link-header]: ../img/link-header-pagination.png
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
[disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/
[disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api

View File

@ -330,6 +330,8 @@ To implement a custom relational field, you should override `RelatedField`, and
If you want to implement a read-write relational field, you must also implement the `.to_internal_value(self, data)` method.
To provide a dynamic queryset based on the `context`, you can also override `.get_queryset(self)` instead of specifying `.queryset` on the class or when initializing the field.
## Example
For example, we could define a relational field to serialize a track to a custom string representation, using its ordering, title, and duration.
@ -505,7 +507,7 @@ For example, given the following model for a tag, which has a generic relationsh
def __unicode__(self):
return self.tag_name
And the following two models, which may be have associated tags:
And the following two models, which may have associated tags:
class Bookmark(models.Model):
"""
@ -578,9 +580,14 @@ The following third party packages are also available.
The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources.
## Rest Framework Generic Relations
The [rest-framework-generic-relations][drf-nested-relations] library provides read/write serialization for generic foreign keys.
[cite]: http://lwn.net/Articles/193245/
[reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward
[routers]: http://www.django-rest-framework.org/api-guide/routers#defaultrouter
[generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1
[2.2-announcement]: ../topics/2.2-announcement.md
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
[drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations

View File

@ -43,7 +43,7 @@ We can now use `CommentSerializer` to serialize a comment, or list of comments.
serializer = CommentSerializer(comment)
serializer.data
# {'email': u'leila@example.com', 'content': u'foo bar', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)}
# {'email': 'leila@example.com', 'content': 'foo bar', 'created': '2016-01-27T15:17:10.375877'}
At this point we've translated the model instance into Python native datatypes. To finalise the serialization process we render the data into `json`.
@ -51,7 +51,7 @@ At this point we've translated the model instance into Python native datatypes.
json = JSONRenderer().render(serializer.data)
json
# '{"email": "leila@example.com", "content": "foo bar", "created": "2012-08-22T16:20:09.822"}'
# b'{"email":"leila@example.com","content":"foo bar","created":"2016-01-27T15:17:10.375877"}'
## Deserializing objects
@ -384,7 +384,7 @@ This manager class now more nicely encapsulates that user instances and profile
has_support_contract=validated_data['profile']['has_support_contract']
)
For more details on this approach see the Django documentation on [model managers](model-managers), and [this blogpost on using model and manager classes](encapsulation-blogpost).
For more details on this approach see the Django documentation on [model managers][model-managers], and [this blogpost on using model and manager classes][encapsulation-blogpost].
## Dealing with multiple objects
@ -775,6 +775,8 @@ To support multiple updates you'll need to do so explicitly. When writing your m
* How should removals be handled? Do they imply object deletion, or removing a relationship? Should they be silently ignored, or are they invalid?
* How should ordering be handled? Does changing the position of two items imply any state change or is it ignored?
You will need to add an explicit `id` field to the instance serializer. The default implicitly-generated `id` field is marked as `read_only`. This causes it to be removed on updates. Once you declare it explicitly, it will be available in the list serializer's `update` method.
Here's an example of how you might choose to implement multiple updates:
class BookListSerializer(serializers.ListSerializer):
@ -786,7 +788,7 @@ Here's an example of how you might choose to implement multiple updates:
# Perform creations and updates.
ret = []
for book_id, data in data_mapping.items():
book = book_mapping.get(book_id, None):
book = book_mapping.get(book_id, None)
if book is None:
ret.append(self.child.create(data))
else:
@ -805,6 +807,8 @@ Here's an example of how you might choose to implement multiple updates:
id = serializers.IntegerField()
...
id = serializers.IntegerField(required=False)
class Meta:
list_serializer_class = BookListSerializer

View File

@ -143,7 +143,7 @@ Your URL conf must include a pattern that matches the version with a `'version'`
## NamespaceVersioning
To the client, this scheme is the same as `URLParameterVersioning`. The only difference is how it is configured in your Django application, as it uses URL namespacing, instead of URL keyword arguments.
To the client, this scheme is the same as `URLPathVersioning`. The only difference is how it is configured in your Django application, as it uses URL namespacing, instead of URL keyword arguments.
GET /v1/something/ HTTP/1.1
Host: example.com
@ -165,7 +165,7 @@ In the following example we're giving a set of views two different possible URL
url(r'^v2/bookings/', include('bookings.urls', namespace='v2'))
]
Both `URLParameterVersioning` and `NamespaceVersioning` are reasonable if you just need a simple versioning scheme. The `URLParameterVersioning` approach might be better suitable for small ad-hoc projects, and the `NamespaceVersioning` is probably easier to manage for larger projects.
Both `URLPathVersioning` and `NamespaceVersioning` are reasonable if you just need a simple versioning scheme. The `URLPathVersioning` approach might be better suitable for small ad-hoc projects, and the `NamespaceVersioning` is probably easier to manage for larger projects.
## HostNameVersioning
@ -214,7 +214,7 @@ If your versioning scheme is based on the request URL, you will also want to alt
[cite]: http://www.slideshare.net/evolve_conference/201308-fielding-evolve/31
[roy-fielding-on-versioning]: http://www.infoq.com/articles/roy-fielding-on-versioning
[klabnik-guidelines]: http://blog.steveklabnik.com/posts/2011-07-03-nobody-understands-rest-or-http#i_want_my_api_to_be_versioned
[heroku-guidelines]: https://github.com/interagent/http-api-design#version-with-accepts-header
[heroku-guidelines]: https://github.com/interagent/http-api-design/blob/master/en/foundations/require-versioning-in-the-accepts-header.md
[json-parameters]: http://tools.ietf.org/html/rfc4627#section-6
[vendor-media-type]: http://en.wikipedia.org/wiki/Internet_media_type#Vendor_tree
[lvh]: https://reinteractive.net/posts/199-developing-and-testing-rails-applications-with-subdomains

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 567 KiB

View File

@ -14,7 +14,7 @@ The most common way to document Web APIs today is to produce documentation that
#### DRF Docs
[DRF Docs][drfdocs-repo] allows you to document Web APIs made with Django REST Framework and it is authored by Emmanouil Konstantinidis. It's made to work out of the box and its setup should not take more than a couple of minutes. Complete documentation can be found on the [website][drfdocs-website] while there is also a [demo][drfdocs-demo] available for people to see what it looks like.
[DRF Docs][drfdocs-repo] allows you to document Web APIs made with Django REST Framework and it is authored by Emmanouil Konstantinidis. It's made to work out of the box and its setup should not take more than a couple of minutes. Complete documentation can be found on the [website][drfdocs-website] while there is also a [demo][drfdocs-demo] available for people to see what it looks like. **Live API Endpoints** allow you to utilize the endpoints from within the documentation in a neat way.
Features include customizing the template with your branding, settings for hiding the docs depending on the environment and more.

View File

@ -66,9 +66,10 @@ The following view demonstrates an example of using a serializer in a template f
def post(self, request, pk):
profile = get_object_or_404(Profile, pk=pk)
serializer = ProfileSerializer(profile)
serializer = ProfileSerializer(profile, data=request.data)
if not serializer.is_valid():
return Response({'serializer': serializer, 'profile': profile})
serializer.save()
return redirect('profile-list')
**profile_detail.html**:

View File

@ -40,6 +40,12 @@ You can determine your currently installed version using `pip freeze`:
## 3.3.x series
### 3.4
**Unreleased**
* Dropped support for EOL Django 1.7 ([#3933][gh3933])
### 3.3.2
**Date**: [14th December 2015][3.3.2-milestone].

View File

@ -45,7 +45,7 @@ We'll need to add our new `snippets` app and the `rest_framework` app to `INSTAL
INSTALLED_APPS = (
...
'rest_framework',
'snippets',
'snippets.apps.SnippetsConfig',
)
Okay, we're ready to roll.
@ -159,7 +159,7 @@ Deserialization is similar. First we parse a stream into Python native datatype
stream = BytesIO(content)
data = JSONParser().parse(stream)
...then we restore those native datatypes into to a fully populated object instance.
...then we restore those native datatypes into a fully populated object instance.
serializer = SnippetSerializer(data=data)
serializer.is_valid()
@ -293,11 +293,11 @@ Finally we need to wire these views up. Create the `snippets/urls.py` file:
url(r'^snippets/$', views.snippet_list),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
]
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs.
from django.conf.urls import url, include
urlpatterns = [
url(r'^', include('snippets.urls')),
]
@ -325,7 +325,7 @@ Quit out of the shell...
In another terminal window, we can test the server.
We can test our API using using [curl][curl] or [httpie][httpie]. Httpie is a user friendly http client that's written in Python. Let's install that.
We can test our API using [curl][curl] or [httpie][httpie]. Httpie is a user friendly http client that's written in Python. Let's install that.
You can install httpie using pip:

View File

@ -206,7 +206,7 @@ If we try to create a snippet without authenticating, we'll get an error:
We can make a successful request by including the username and password of one of the users we created earlier.
http -a tom:password POST http://127.0.0.1:8000/snippets/ code="print 789"
http -a tom:password123 POST http://127.0.0.1:8000/snippets/ code="print 789"
{
"id": 5,

View File

@ -11,7 +11,7 @@ Right now we have endpoints for 'snippets' and 'users', but we don't have a sing
from rest_framework.reverse import reverse
@api_view(('GET',))
@api_view(['GET'])
def api_root(request, format=None):
return Response({
'users': reverse('user-list', request=request, format=format),

View File

@ -28,7 +28,7 @@ Now sync your database for the first time:
python manage.py migrate
We'll also create an initial user named `admin` with a password of `password`. We'll authenticate as that user later in our example.
We'll also create an initial user named `admin` with a password of `password123`. We'll authenticate as that user later in our example.
python manage.py createsuperuser
@ -134,7 +134,7 @@ We're now ready to test the API we've built. Let's fire up the server from the
We can now access our API, both from the command-line, using tools like `curl`...
bash: curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
bash: curl -H 'Accept: application/json; indent=4' -u admin:password123 http://127.0.0.1:8000/users/
{
"count": 2,
"next": null,
@ -157,7 +157,7 @@ We can now access our API, both from the command-line, using tools like `curl`..
Or using the [httpie][httpie], command line tool...
bash: http -a username:password http://127.0.0.1:8000/users/
bash: http -a admin:password123 http://127.0.0.1:8000/users/
HTTP/1.1 200 OK
...

View File

@ -1,4 +1,4 @@
# Optional packages which may be used with REST framework.
markdown==2.5.2
markdown==2.6.4
django-guardian==1.3.2
django-filter==0.10.0

View File

@ -0,0 +1 @@
default_app_config = 'rest_framework.authtoken.apps.AuthTokenConfig'

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class AuthTokenConfig(AppConfig):
name = 'rest_framework.authtoken'
verbose_name = _("Auth Token")

View File

@ -4,6 +4,7 @@ import os
from django.conf import settings
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist.
# Note that we don't perform this code in the compat module due to
@ -17,10 +18,20 @@ class Token(models.Model):
"""
The default authorization token model.
"""
key = models.CharField(max_length=40, primary_key=True)
key = models.CharField(_("Key"), max_length=40, primary_key=True)
user = models.OneToOneField(AUTH_USER_MODEL, related_name='auth_token',
on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
on_delete=models.CASCADE, verbose_name=_("User"))
created = models.DateTimeField(_("Created"), auto_now_add=True)
class Meta:
# Work around for a bug in Django:
# https://code.djangoproject.com/ticket/19422
#
# Also see corresponding ticket:
# https://github.com/tomchristie/django-rest-framework/issues/705
abstract = 'rest_framework.authtoken' not in settings.INSTALLED_APPS
verbose_name = _("Token")
verbose_name_plural = _("Tokens")
def save(self, *args, **kwargs):
if not self.key:

View File

@ -5,8 +5,8 @@ from rest_framework import serializers
class AuthTokenSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField(style={'input_type': 'password'})
username = serializers.CharField(label=_("Username"))
password = serializers.CharField(label=_("Password"), style={'input_type': 'password'})
def validate(self, attrs):
username = attrs.get('username')

View File

@ -86,18 +86,6 @@ except ImportError:
crispy_forms = None
if django.VERSION >= (1, 6):
def clean_manytomany_helptext(text):
return text
else:
# Up to version 1.5 many to many fields automatically suffix
# the `help_text` attribute with hardcoded text.
def clean_manytomany_helptext(text):
if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'):
text = text[:-69]
return text
# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
# Fixes (#1712). We keep the try/except for the test suite.
guardian = None
@ -109,41 +97,6 @@ except ImportError:
pass
# MinValueValidator, MaxValueValidator et al. only accept `message` in 1.8+
if django.VERSION >= (1, 8):
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.validators import MinLengthValidator, MaxLengthValidator
else:
from django.core.validators import MinValueValidator as DjangoMinValueValidator
from django.core.validators import MaxValueValidator as DjangoMaxValueValidator
from django.core.validators import MinLengthValidator as DjangoMinLengthValidator
from django.core.validators import MaxLengthValidator as DjangoMaxLengthValidator
class MinValueValidator(DjangoMinValueValidator):
def __init__(self, *args, **kwargs):
self.message = kwargs.pop('message', self.message)
super(MinValueValidator, self).__init__(*args, **kwargs)
class MaxValueValidator(DjangoMaxValueValidator):
def __init__(self, *args, **kwargs):
self.message = kwargs.pop('message', self.message)
super(MaxValueValidator, self).__init__(*args, **kwargs)
class MinLengthValidator(DjangoMinLengthValidator):
def __init__(self, *args, **kwargs):
self.message = kwargs.pop('message', self.message)
super(MinLengthValidator, self).__init__(*args, **kwargs)
class MaxLengthValidator(DjangoMaxLengthValidator):
def __init__(self, *args, **kwargs):
self.message = kwargs.pop('message', self.message)
super(MaxLengthValidator, self).__init__(*args, **kwargs)
# PATCH method is not implemented by Django
if 'patch' not in View.http_method_names:
View.http_method_names = View.http_method_names + ['patch']
@ -153,16 +106,25 @@ if 'patch' not in View.http_method_names:
try:
import markdown
if markdown.version <= '2.2':
HEADERID_EXT_PATH = 'headerid'
else:
HEADERID_EXT_PATH = 'markdown.extensions.headerid'
def apply_markdown(text):
"""
Simple wrapper around :func:`markdown.markdown` to set the base level
of '#' style headers to <h2>.
"""
extensions = ['headerid(level=2)']
safe_mode = False
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
extensions = [HEADERID_EXT_PATH]
extension_configs = {
HEADERID_EXT_PATH: {
'level': '2'
}
}
md = markdown.Markdown(
extensions=extensions, extension_configs=extension_configs
)
return md.convert(text)
except ImportError:
apply_markdown = None
@ -179,13 +141,6 @@ else:
LONG_SEPARATORS = (b', ', b': ')
INDENT_SEPARATORS = (b',', b': ')
if django.VERSION >= (1, 8):
from django.db.models import DurationField
from django.utils.dateparse import parse_duration
from django.utils.duration import duration_string
else:
DurationField = duration_string = parse_duration = None
try:
# DecimalValidator is unavailable in Django < 1.9
from django.core.validators import DecimalValidator
@ -214,14 +169,14 @@ def template_render(template, context=None, request=None):
"""
Passing Context or RequestContext to Template.render is deprecated in 1.9+,
see https://github.com/django/django/pull/3883 and
https://github.com/django/django/blob/1.9rc1/django/template/backends/django.py#L82-L84
https://github.com/django/django/blob/1.9/django/template/backends/django.py#L82-L84
:param template: Template instance
:param context: dict
:param request: Request instance
:return: rendered template as SafeText instance
"""
if django.VERSION < (1, 8) or isinstance(template, Template):
if isinstance(template, Template):
if request:
context = RequestContext(request, context)
else:
@ -230,32 +185,3 @@ def template_render(template, context=None, request=None):
# backends template, e.g. django.template.backends.django.Template
else:
return template.render(context, request=request)
def get_all_related_objects(opts):
"""
Django 1.8 changed meta api, see
https://docs.djangoproject.com/en/1.8/ref/models/meta/#migrating-old-meta-api
https://code.djangoproject.com/ticket/12663
https://github.com/django/django/pull/3848
:param opts: Options instance
:return: list of relations except many-to-many ones
"""
if django.VERSION < (1, 9):
return opts.get_all_related_objects()
else:
return [r for r in opts.related_objects if not r.field.many_to_many]
def get_all_related_many_to_many_objects(opts):
"""
Django 1.8 changed meta api, see docstr in compat.get_all_related_objects()
:param opts: Options instance
:return: list of many-to-many relations
"""
if django.VERSION < (1, 9):
return opts.get_all_related_many_to_many_objects()
else:
return [r for r in opts.related_objects if r.field.many_to_many]

View File

@ -28,7 +28,7 @@ def _force_text_recursive(data):
]
if isinstance(data, ReturnList):
return ReturnList(ret, serializer=data.serializer)
return data
return ret
elif isinstance(data, dict):
ret = {
key: _force_text_recursive(value)
@ -36,7 +36,7 @@ def _force_text_recursive(data):
}
if isinstance(data, ReturnDict):
return ReturnDict(ret, serializer=data.serializer)
return data
return ret
return force_text(data)

View File

@ -14,23 +14,23 @@ from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import (
EmailValidator, RegexValidator, URLValidator, ip_address_validators
EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
MinValueValidator, RegexValidator, URLValidator, ip_address_validators
)
from django.forms import FilePathField as DjangoFilePathField
from django.forms import ImageField as DjangoImageField
from django.utils import six, timezone
from django.utils.dateparse import parse_date, parse_datetime, parse_time
from django.utils.dateparse import (
parse_date, parse_datetime, parse_duration, parse_time
)
from django.utils.duration import duration_string
from django.utils.encoding import is_protected_type, smart_text
from django.utils.functional import cached_property
from django.utils.ipv6 import clean_ipv6_address
from django.utils.translation import ugettext_lazy as _
from rest_framework import ISO_8601
from rest_framework.compat import (
MaxLengthValidator, MaxValueValidator, MinLengthValidator,
MinValueValidator, duration_string, parse_duration, unicode_repr,
unicode_to_repr
)
from rest_framework.compat import unicode_repr, unicode_to_repr
from rest_framework.exceptions import ValidationError
from rest_framework.settings import api_settings
from rest_framework.utils import html, humanize_datetime, representation
@ -117,9 +117,9 @@ def to_choices_dict(choices):
"""
Convert choices into key/value dicts.
pairwise_choices([1]) -> {1: 1}
pairwise_choices([(1, '1st'), (2, '2nd')]) -> {1: '1st', 2: '2nd'}
pairwise_choices([('Group', ((1, '1st'), 2))]) -> {'Group': {1: '1st', 2: '2nd'}}
to_choices_dict([1]) -> {1: 1}
to_choices_dict([(1, '1st'), (2, '2nd')]) -> {1: '1st', 2: '2nd'}
to_choices_dict([('Group', ((1, '1st'), 2))]) -> {'Group': {1: '1st', 2: '2nd'}}
"""
# Allow single, paired or grouped choices style:
# choices = [1, 2, 3]
@ -145,8 +145,8 @@ def flatten_choices_dict(choices):
"""
Convert a group choices dict into a flat dict of choices.
flatten_choices({1: '1st', 2: '2nd'}) -> {1: '1st', 2: '2nd'}
flatten_choices({'Group': {1: '1st', 2: '2nd'}}) -> {1: '1st', 2: '2nd'}
flatten_choices_dict({1: '1st', 2: '2nd'}) -> {1: '1st', 2: '2nd'}
flatten_choices_dict({'Group': {1: '1st', 2: '2nd'}}) -> {1: '1st', 2: '2nd'}
"""
ret = OrderedDict()
for key, value in choices.items():
@ -370,6 +370,8 @@ class Field(object):
Return a value to use when the field is being returned as a primitive
value, without any object instance.
"""
if callable(self.initial):
return self.initial()
return self.initial
def get_value(self, dictionary):
@ -1215,12 +1217,6 @@ class DurationField(Field):
'invalid': _('Duration has wrong format. Use one of these formats instead: {format}.'),
}
def __init__(self, *args, **kwargs):
if parse_duration is None:
raise NotImplementedError(
'DurationField not supported for django versions prior to 1.8')
return super(DurationField, self).__init__(*args, **kwargs)
def to_internal_value(self, value):
if isinstance(value, datetime.timedelta):
return value

View File

@ -21,6 +21,15 @@ from rest_framework.reverse import reverse
from rest_framework.utils import html
def method_overridden(method_name, klass, instance):
"""
Determine if a method has been overridden.
"""
method = getattr(klass, method_name)
default_method = getattr(method, '__func__', method) # Python 3 compat
return default_method is not getattr(instance, method_name).__func__
class Hyperlink(six.text_type):
"""
A string like object that additionally has an associated name.
@ -65,10 +74,12 @@ class RelatedField(Field):
self.queryset = kwargs.pop('queryset', self.queryset)
self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff)
self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text)
assert self.queryset is not None or kwargs.get('read_only', None), (
'Relational field must provide a `queryset` argument, '
'or set read_only=`True`.'
)
if not method_overridden('get_queryset', RelatedField, self):
assert self.queryset is not None or kwargs.get('read_only', None), (
'Relational field must provide a `queryset` argument, '
'override `get_queryset`, or set read_only=`True`.'
)
assert not (self.queryset is not None and kwargs.get('read_only', None)), (
'Relational fields should not provide a `queryset` argument, '
'when setting read_only=`True`.'
@ -269,7 +280,7 @@ class HyperlinkedRelatedField(RelatedField):
attributes are not configured to correctly match the URL conf.
"""
# Unsaved objects will not yet have a valid URL.
if hasattr(obj, 'pk') and obj.pk is None:
if hasattr(obj, 'pk') and obj.pk in (None, ''):
return None
lookup_value = getattr(obj, self.lookup_field)

View File

@ -11,7 +11,6 @@ from __future__ import unicode_literals
import json
from collections import OrderedDict
import django
from django import forms
from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import Page
@ -341,6 +340,7 @@ class HTMLFormRenderer(BaseRenderer):
"""
Render serializer data and return an HTML form, as a string.
"""
renderer_context = renderer_context or {}
form = data.serializer
style = renderer_context.get('style', {})
@ -772,7 +772,7 @@ class MultiPartRenderer(BaseRenderer):
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
format = 'multipart'
charset = 'utf-8'
BOUNDARY = 'BoUnDaRyStRiNg' if django.VERSION >= (1, 5) else b'BoUnDaRyStRiNg'
BOUNDARY = 'BoUnDaRyStRiNg'
def render(self, data, accepted_media_type=None, renderer_context=None):
if hasattr(data, 'items'):

View File

@ -15,12 +15,12 @@ from __future__ import unicode_literals
import warnings
from django.db import models
from django.db.models import DurationField as ModelDurationField
from django.db.models.fields import Field as DjangoModelField
from django.db.models.fields import FieldDoesNotExist
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from rest_framework.compat import DurationField as ModelDurationField
from rest_framework.compat import JSONField as ModelJSONField
from rest_framework.compat import postgres_fields, unicode_to_repr
from rest_framework.utils import model_meta
@ -715,7 +715,7 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data):
isinstance(validated_data[key], (list, dict))
for key, field in serializer.fields.items()
), (
'The `.{method_name}()` method does not support writable nested'
'The `.{method_name}()` method does not support writable nested '
'fields by default.\nWrite an explicit `.{method_name}()` method for '
'serializer `{module}.{class_name}`, or set `read_only=True` on '
'nested serializer fields.'.format(

View File

@ -36,7 +36,7 @@
<span>
{% block branding %}
<a class='navbar-brand' rel="nofollow" href='http://www.django-rest-framework.org'>
Django REST framework <span class="version">{{ version }}</span>
Django REST framework
</a>
{% endblock %}
</span>
@ -166,6 +166,7 @@
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
<div class="modal-body">
<fieldset>
{% csrf_token %}
{{ post_form }}
</fieldset>
</div>

View File

@ -37,7 +37,7 @@
<span>
{% block branding %}
<a class='navbar-brand' rel="nofollow" href='http://www.django-rest-framework.org'>
Django REST framework <span class="version">{{ version }}</span>
Django REST framework
</a>
{% endblock %}
</span>

View File

@ -25,7 +25,7 @@
<input type="text" name="username" maxlength="100"
autocapitalize="off"
autocorrect="off" class="form-control textinput textInput"
id="id_username" required
id="id_username" required autofocus
{% if form.username.value %}value="{{ form.username.value }}"{% endif %}>
{% if form.username.errors %}
<p class="text-error">

View File

@ -1,7 +1,7 @@
<div class="form-group {% if field.errors %}has-error{% endif %}">
<div class="checkbox">
<label>
<input type="checkbox" name="{{ field.name }}" value="true" {% if value %}checked{% endif %}>
<input type="checkbox" name="{{ field.name }}" value="true" {% if field.value %}checked{% endif %}>
{% if field.label %}{{ field.label }}{% endif %}
</label>
</div>

View File

@ -4,7 +4,6 @@
# to make it harder for the user to import the wrong thing without realizing.
from __future__ import unicode_literals
import django
from django.conf import settings
from django.test import testcases
from django.test.client import Client as DjangoClient
@ -223,9 +222,9 @@ class APITestCase(testcases.TestCase):
client_class = APIClient
if django.VERSION >= (1, 4):
class APISimpleTestCase(testcases.SimpleTestCase):
client_class = APIClient
class APISimpleTestCase(testcases.SimpleTestCase):
client_class = APIClient
class APILiveServerTestCase(testcases.LiveServerTestCase):
client_class = APIClient
class APILiveServerTestCase(testcases.LiveServerTestCase):
client_class = APIClient

View File

@ -13,10 +13,6 @@ from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils import six
from rest_framework.compat import (
get_all_related_many_to_many_objects, get_all_related_objects
)
FieldInfo = namedtuple('FieldResult', [
'pk', # Model field instance
'fields', # Dict of field name -> model field instance
@ -138,7 +134,8 @@ def _get_reverse_relationships(opts):
# See: https://code.djangoproject.com/ticket/24208
reverse_relations = OrderedDict()
for relation in get_all_related_objects(opts):
all_related_objects = [r for r in opts.related_objects if not r.field.many_to_many]
for relation in all_related_objects:
accessor_name = relation.get_accessor_name()
related = getattr(relation, 'related_model', relation.model)
reverse_relations[accessor_name] = RelationInfo(
@ -150,7 +147,8 @@ def _get_reverse_relationships(opts):
)
# Deal with reverse many-to-many relationships.
for relation in get_all_related_many_to_many_objects(opts):
all_related_many_to_many_objects = [r for r in opts.related_objects if r.field.many_to_many]
for relation in all_related_many_to_many_objects:
accessor_name = relation.get_accessor_name()
related = getattr(relation, 'related_model', relation.model)
reverse_relations[accessor_name] = RelationInfo(

21
tests/test_exceptions.py Normal file
View File

@ -0,0 +1,21 @@
from __future__ import unicode_literals
from django.test import TestCase
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import _force_text_recursive
class ExceptionTestCase(TestCase):
def test_force_text_recursive(self):
s = "sfdsfggiuytraetfdlklj"
self.assertEqual(_force_text_recursive(_(s)), s)
self.assertEqual(type(_force_text_recursive(_(s))), type(s))
self.assertEqual(_force_text_recursive({'a': _(s)})['a'], s)
self.assertEqual(type(_force_text_recursive({'a': _(s)})['a']), type(s))
self.assertEqual(_force_text_recursive([[_(s)]])[0][0], s)
self.assertEqual(type(_force_text_recursive([[_(s)]])[0][0]), type(s))

View File

@ -3,7 +3,6 @@ import os
import uuid
from decimal import Decimal
import django
import pytest
from django.http import QueryDict
from django.utils import six, timezone
@ -191,6 +190,24 @@ class TestInitial:
}
class TestInitialWithCallable:
def setup(self):
def initial_value():
return 123
class TestSerializer(serializers.Serializer):
initial_field = serializers.IntegerField(initial=initial_value)
self.serializer = TestSerializer()
def test_initial_should_accept_callable(self):
"""
Follows the default ``Field.initial`` behaviour where they accept a
callable to produce the initial value"""
assert self.serializer.data == {
'initial_field': 123,
}
class TestLabel:
def setup(self):
class TestSerializer(serializers.Serializer):
@ -951,7 +968,7 @@ class TestDateTimeField(FieldValues):
datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
# Django 1.4 does not support timezone string parsing.
'2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
}
invalid_inputs = {
'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
@ -1077,8 +1094,6 @@ class TestNoOutputFormatTimeField(FieldValues):
field = serializers.TimeField(format=None)
@pytest.mark.skipif(django.VERSION < (1, 8),
reason='DurationField is only available for django1.8+')
class TestDurationField(FieldValues):
"""
Valid and invalid values for `DurationField`.
@ -1097,8 +1112,7 @@ class TestDurationField(FieldValues):
outputs = {
datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123): '3 08:32:01.000123',
}
if django.VERSION >= (1, 8):
field = serializers.DurationField()
field = serializers.DurationField()
# Choice types...

View File

@ -1,6 +1,5 @@
from __future__ import unicode_literals
import django
import pytest
from django.db import models
from django.shortcuts import get_object_or_404
@ -158,7 +157,7 @@ class TestRootView(TestCase):
self.assertIn(expected_error, response.rendered_content.decode('utf-8'))
EXPECTED_QUERIES_FOR_PUT = 3 if django.VERSION < (1, 6) else 2
EXPECTED_QUERIES_FOR_PUT = 2
class TestInstanceView(TestCase):

View File

@ -10,18 +10,16 @@ from __future__ import unicode_literals
import decimal
from collections import OrderedDict
import django
import pytest
from django.core.exceptions import ImproperlyConfigured
from django.core.validators import (
MaxValueValidator, MinLengthValidator, MinValueValidator
)
from django.db import models
from django.db.models import DurationField as ModelDurationField
from django.test import TestCase
from django.utils import six
from rest_framework import serializers
from rest_framework.compat import DurationField as ModelDurationField
from rest_framework.compat import unicode_repr
@ -341,8 +339,6 @@ class TestRegularFieldMappings(TestCase):
assert implicit.data == explicit.data
@pytest.mark.skipif(django.VERSION < (1, 8),
reason='DurationField is only available for django1.8+')
class TestDurationFieldMapping(TestCase):
def test_duration_field(self):
class DurationFieldModel(models.Model):

View File

@ -87,6 +87,18 @@ class TestProxiedPrimaryKeyRelatedField(APISimpleTestCase):
assert representation == self.instance.pk.int
class TestHyperlinkedRelatedField(APISimpleTestCase):
def setUp(self):
self.field = serializers.HyperlinkedRelatedField(
view_name='example', read_only=True)
self.field.reverse = mock_reverse
self.field._context = {'request': True}
def test_representation_unsaved_object_with_non_nullable_pk(self):
representation = self.field.to_representation(MockObject(pk=''))
assert representation is None
class TestHyperlinkedIdentityField(APISimpleTestCase):
def setUp(self):
self.instance = MockObject(pk=1, name='foo')
@ -176,6 +188,16 @@ class TestSlugRelatedField(APISimpleTestCase):
representation = self.field.to_representation(self.instance)
assert representation == self.instance.name
def test_overriding_get_queryset(self):
qs = self.queryset
class NoQuerySetSlugRelatedField(serializers.SlugRelatedField):
def get_queryset(self):
return qs
field = NoQuerySetSlugRelatedField(slug_field='name')
field.to_internal_value(self.instance.name)
class TestManyRelatedField(APISimpleTestCase):
def setUp(self):

View File

@ -10,6 +10,7 @@ from django.core.cache import cache
from django.db import models
from django.test import TestCase
from django.utils import six
from django.utils.safestring import SafeText
from django.utils.translation import ugettext_lazy as _
from rest_framework import permissions, serializers, status
@ -459,3 +460,28 @@ class TestHiddenFieldHTMLFormRenderer(TestCase):
field = serializer['published']
rendered = renderer.render_field(field, {})
assert rendered == ''
class TestHTMLFormRenderer(TestCase):
def setUp(self):
class TestSerializer(serializers.Serializer):
test_field = serializers.CharField()
self.renderer = HTMLFormRenderer()
self.serializer = TestSerializer(data={})
def test_render_with_default_args(self):
self.serializer.is_valid()
renderer = HTMLFormRenderer()
result = renderer.render(self.serializer.data)
self.assertIsInstance(result, SafeText)
def test_render_with_provided_args(self):
self.serializer.is_valid()
renderer = HTMLFormRenderer()
result = renderer.render(self.serializer.data, None, {})
self.assertIsInstance(result, SafeText)

View File

@ -3,8 +3,6 @@ Tests for content parsing, and form-overloaded content parsing.
"""
from __future__ import unicode_literals
import django
import pytest
from django.conf.urls import url
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
@ -201,8 +199,6 @@ class TestAuthSetter(TestCase):
self.assertEqual(request.auth, 'DUMMY')
@pytest.mark.skipif(django.VERSION < (1, 7),
reason='secure argument is only available for django1.7+')
class TestSecure(TestCase):
def test_default_secure_false(self):

View File

@ -4,17 +4,17 @@ addopts=--tb=short
[tox]
envlist =
py27-{lint,docs},
{py27,py32,py33,py34}-django{17,18},
{py27,py32,py33,py34}-django18,
{py27,py34,py35}-django{19}
[testenv]
commands = ./runtests.py --fast {posargs} --coverage -rw
setenv =
PYTHONDONTWRITEBYTECODE=1
PYTHONWARNINGS=once
deps =
django17: Django==1.7.11
django18: Django==1.8.7
django19: Django==1.9
django18: Django==1.8.10
django19: Django==1.9.3
-rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt
@ -29,4 +29,3 @@ commands = mkdocs build
deps =
-rrequirements/requirements-testing.txt
-rrequirements/requirements-documentation.txt