Skip to content

Commit b996266

Browse files
committed
Merge master
2 parents b1b4703 + 9d136ab commit b996266

File tree

233 files changed

+8204
-3480
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

233 files changed

+8204
-3480
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ MANIFEST
1414

1515
!.gitignore
1616
!.travis.yml
17+
!.isort.cfg

.isort.cfg

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[settings]
2+
skip=.tox
3+
atomic=true
4+
multi_line_output=5
5+
known_third_party=pytest,django
6+
known_first_party=rest_framework

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ language: python
33
sudo: false
44

55
env:
6-
- TOX_ENV=py27-flake8
6+
- TOX_ENV=py27-lint
77
- TOX_ENV=py27-docs
88
- TOX_ENV=py34-django18
99
- TOX_ENV=py33-django18

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Some tips on good issue reporting:
3838

3939
## Triaging issues
4040

41-
Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to
41+
Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to:
4242

4343
* Read through the ticket - does it make sense, is it missing any context that would help explain it better?
4444
* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group?

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
159159

160160
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.svg?branch=master
161161
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
162-
[pypi-version]: https://pypip.in/version/djangorestframework/badge.svg
162+
[pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg
163163
[pypi]: https://pypi.python.org/pypi/djangorestframework
164164
[twitter]: https://twitter.com/_tomchristie
165165
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework

docs/api-guide/authentication.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ Unauthenticated responses that are denied permission will result in an `HTTP 403
247247

248248
If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `PATCH`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details.
249249

250+
**Warning**: Always use Django's standard login view when creating login pages. This will ensure your login views are properly protected.
251+
252+
CSRF validation in REST framework works slightly differently to standard Django due to the need to support both session and non-session based authentication to the same views. This means that only authenticated requests require CSRF tokens, and anonymous requests may be sent without CSRF tokens. This behaviour is not suitable for login views, which should always have CSRF validation applied.
253+
250254
# Custom authentication
251255

252256
To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.

docs/api-guide/fields.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Each serializer field class constructor takes at least these arguments. Some Fi
2020

2121
### `read_only`
2222

23+
Read-only fields are included in the API output, but should not be included in the input during create or update operations. Any 'read_only' fields that are incorrectly included in the serializer input will be ignored.
24+
2325
Set this to `True` to ensure that the field is used when serializing a representation, but is not used when creating or updating an instance during deserialization.
2426

2527
Defaults to `False`
@@ -183,6 +185,26 @@ A field that ensures the input is a valid UUID string. The `to_internal_value` m
183185

184186
"de305d54-75b4-431b-adb2-eb6b9e546013"
185187

188+
**Signature:** `UUIDField(format='hex_verbose')`
189+
190+
- `format`: Determines the representation format of the uuid value
191+
- `'hex_verbose'` - The cannoncical hex representation, including hyphens: `"5ce0e9a5-5ffa-654b-cee0-1238041fb31a"`
192+
- `'hex'` - The compact hex representation of the UUID, not including hyphens: `"5ce0e9a55ffa654bcee01238041fb31a"`
193+
- `'int'` - A 128 bit integer representation of the UUID: `"123456789012312313134124512351145145114"`
194+
- `'urn'` - RFC 4122 URN representation of the UUID: `"urn:uuid:5ce0e9a5-5ffa-654b-cee0-1238041fb31a"`
195+
Changing the `format` parameters only affects representation values. All formats are accepted by `to_internal_value`
196+
197+
## IPAddressField
198+
199+
A field that ensures the input is a valid IPv4 or IPv6 string.
200+
201+
Corresponds to `django.forms.fields.IPAddressField` and `django.forms.fields.GenericIPAddressField`.
202+
203+
**Signature**: `IPAddressField(protocol='both', unpack_ipv4=False, **options)`
204+
205+
- `protocol` Limits valid inputs to the specified protocol. Accepted values are 'both' (default), 'IPv4' or 'IPv6'. Matching is case insensitive.
206+
- `unpack_ipv4` Unpacks IPv4 mapped addresses like ::ffff:192.0.2.1. If this option is enabled that address would be unpacked to 192.0.2.1. Default is disabled. Can only be used when protocol is set to 'both'.
207+
186208
---
187209

188210
# Numeric fields
@@ -302,6 +324,18 @@ Corresponds to `django.db.models.fields.TimeField`
302324

303325
Format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`)
304326

327+
## DurationField
328+
329+
A Duration representation.
330+
Corresponds to `django.db.models.fields.DurationField`
331+
332+
The `validated_data` for these fields will contain a `datetime.timedelta` instance.
333+
The representation is a string following this format `'[DD] [HH:[MM:]]ss[.uuuuuu]'`.
334+
335+
**Note:** This field is only available with Django versions >= 1.8.
336+
337+
**Signature:** `DurationField()`
338+
305339
---
306340

307341
# Choice selection fields

docs/api-guide/filtering.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ If all you need is simple equality-based filtering, you can set a `filter_fields
149149
class ProductList(generics.ListAPIView):
150150
queryset = Product.objects.all()
151151
serializer_class = ProductSerializer
152+
filter_backends = (filters.DjangoFilterBackend,)
152153
filter_fields = ('category', 'in_stock')
153154

154155
This will automatically create a `FilterSet` class for the given fields, and will allow you to make requests such as:
@@ -174,6 +175,7 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha
174175
class ProductList(generics.ListAPIView):
175176
queryset = Product.objects.all()
176177
serializer_class = ProductSerializer
178+
filter_backends = (filters.DjangoFilterBackend,)
177179
filter_class = ProductFilter
178180

179181

docs/api-guide/generic-views.md

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -124,21 +124,24 @@ For example:
124124

125125
Note that if your API doesn't include any object level permissions, you may optionally exclude the `self.check_object_permissions`, and simply return the object from the `get_object_or_404` lookup.
126126

127-
#### `get_filter_backends(self)`
128-
129-
Returns the classes that should be used to filter the queryset. Defaults to returning the `filter_backends` attribute.
130-
131-
May be overridden to provide more complex behavior with filters, such as using different (or even exclusive) lists of filter_backends depending on different criteria.
132-
133-
For example:
134-
135-
def get_filter_backends(self):
136-
if "geo_route" in self.request.query_params:
137-
return (GeoRouteFilter, CategoryFilter)
138-
elif "geo_point" in self.request.query_params:
139-
return (GeoPointFilter, CategoryFilter)
140-
141-
return (CategoryFilter,)
127+
#### `filter_queryset(self, queryset)`
128+
129+
Given a queryset, filter it with whichever filter backends are in use, returning a new queryset.
130+
131+
For example:
132+
133+
def filter_queryset(self, queryset):
134+
filter_backends = (CategoryFilter,)
135+
136+
if 'geo_route' in self.request.query_params:
137+
filter_backends = (GeoRouteFilter, CategoryFilter)
138+
elif 'geo_point' in self.request.query_params:
139+
filter_backends = (GeoPointFilter, CategoryFilter)
140+
141+
for backend in list(filter_backends):
142+
queryset = backend().filter_queryset(self.request, queryset, view=self)
143+
144+
return queryset
142145

143146
#### `get_serializer_class(self)`
144147

@@ -192,7 +195,7 @@ These override points are also particularly useful for adding behavior that occu
192195
You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`.
193196

194197
* `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys.
195-
* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False, allow_add_remove=False)` - Returns a serializer instance.
198+
* `get_serializer(self, instance=None, data=None, many=False, partial=False)` - Returns a serializer instance.
196199
* `get_paginated_response(self, data)` - Returns a paginated style `Response` object.
197200
* `paginate_queryset(self, queryset)` - Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view.
198201
* `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backends are in use, returning a new queryset.
@@ -391,6 +394,10 @@ The following third party packages provide additional generic view implementatio
391394

392395
The [django-rest-framework-bulk package][django-rest-framework-bulk] implements generic view mixins as well as some common concrete generic views to allow to apply bulk operations via API requests.
393396

397+
## Django Rest Multiple Models
398+
399+
[Django Rest Multiple Models][django-rest-multiple-models] provides a generic view (and mixin) for sending multiple serialized models and/or querysets via a single API request.
400+
394401

395402
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
396403
[GenericAPIView]: #genericapiview
@@ -400,3 +407,6 @@ The [django-rest-framework-bulk package][django-rest-framework-bulk] implements
400407
[UpdateModelMixin]: #updatemodelmixin
401408
[DestroyModelMixin]: #destroymodelmixin
402409
[django-rest-framework-bulk]: https://github.com/miki725/django-rest-framework-bulk
410+
[django-rest-multiple-models]: https://github.com/Axiologue/DjangoRestMultipleModels
411+
412+

docs/api-guide/metadata.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ The following class could be used to limit the information that is returned to `
9898
'description': view.get_view_description()
9999
}
100100

101+
Then configure your settings to use this custom class:
102+
103+
REST_FRAMEWORK = {
104+
'DEFAULT_METADATA_CLASS': 'myproject.apps.core.MinimalMetadata'
105+
}
106+
101107
[cite]: http://tools.ietf.org/html/rfc7231#section-4.3.7
102108
[no-options]: https://www.mnot.net/blog/2012/10/29/NO_OPTIONS
103109
[json-schema]: http://json-schema.org/

docs/api-guide/pagination.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,11 +246,11 @@ Let's modify the built-in `PageNumberPagination` style, so that instead of inclu
246246
previous_url = self.get_previous_link()
247247

248248
if next_url is not None and previous_url is not None:
249-
link = '<{next_url}; rel="next">, <{previous_url}; rel="prev">'
249+
link = '<{next_url}>; rel="next", <{previous_url}>; rel="prev"'
250250
elif next_url is not None:
251-
link = '<{next_url}; rel="next">'
251+
link = '<{next_url}>; rel="next"'
252252
elif previous_url is not None:
253-
link = '<{previous_url}; rel="prev">'
253+
link = '<{previous_url}>; rel="prev"'
254254
else:
255255
link = ''
256256

docs/api-guide/parsers.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,17 +144,16 @@ By default this will include the following keys: `view`, `request`, `args`, `kwa
144144
The following is an example plaintext parser that will populate the `request.data` property with a string representing the body of the request.
145145

146146
class PlainTextParser(BaseParser):
147-
"""
148-
Plain text parser.
149-
"""
150-
151-
media_type = 'text/plain'
152-
153-
def parse(self, stream, media_type=None, parser_context=None):
154147
"""
155-
Simply return a string representing the body of the request.
148+
Plain text parser.
156149
"""
157-
return stream.read()
150+
media_type = 'text/plain'
151+
152+
def parse(self, stream, media_type=None, parser_context=None):
153+
"""
154+
Simply return a string representing the body of the request.
155+
"""
156+
return stream.read()
158157

159158
---
160159

docs/api-guide/permissions.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ Similar to `DjangoModelPermissions`, but also allows unauthenticated users to ha
150150

151151
This permission class ties into Django's standard [object permissions framework][objectpermissions] that allows per-object permissions on models. In order to use this permission class, you'll also need to add a permission backend that supports object-level permissions, such as [django-guardian][guardian].
152152

153-
As with `DjangoModelPermissions`, this permission must only be applied to views that have a `.queryset` property. Authorization will only be granted if the user *is authenticated* and has the *relevant per-object permissions* and *relevant model permissions* assigned.
153+
As with `DjangoModelPermissions`, this permission must only be applied to views that have a `.queryset` property or `.get_queryset()` method. Authorization will only be granted if the user *is authenticated* and has the *relevant per-object permissions* and *relevant model permissions* assigned.
154154

155155
* `POST` requests require the user to have the `add` permission on the model instance.
156156
* `PUT` and `PATCH` requests require the user to have the `change` permission on the model instance.
@@ -190,6 +190,16 @@ If you need to test if a request is a read operation or a write operation, you s
190190

191191
---
192192

193+
Custom permissions will raise a `PermissionDenied` exception if the test fails. To change the error message associated with the exception, implement a `message` attribute directly on your custom permission. Otherwise the `default_detail` attribute from `PermissionDenied` will be used.
194+
195+
from rest_framework import permissions
196+
197+
class CustomerAccessPermission(permissions.BasePermission):
198+
message = 'Adding customers not allowed.'
199+
200+
def has_permission(self, request, view):
201+
...
202+
193203
## Examples
194204

195205
The following is an example of a permission class that checks the incoming request's IP address against a blacklist, and denies the request if the IP has been blacklisted.
@@ -233,10 +243,6 @@ Also note that the generic views will only check the object-level permissions fo
233243

234244
The following third party packages are also available.
235245

236-
## DRF Any Permissions
237-
238-
The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to REST framework. Instead of all specified permissions being required, only one of the given permissions has to be true in order to get access to the view.
239-
240246
## Composed Permissions
241247

242248
The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components.

docs/api-guide/relations.md

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ By default this field is read-write, although you can change this behavior using
116116
* `queryset` - The queryset used for model instance lookups when validating the field input. Relationships must either set a queryset explicitly, or set `read_only=True`.
117117
* `many` - If applied to a to-many relationship, you should set this argument to `True`.
118118
* `allow_null` - If set to `True`, the field will accept values of `None` or the empty string for nullable relationships. Defaults to `False`.
119+
* `pk_field` - Set to a field to control serialization/deserialization of the primary key's value. For example, `pk_field=UUIDField(format='hex')` would serialize a UUID primary key into its compact hex representation.
120+
119121

120122
## HyperlinkedRelatedField
121123

@@ -254,17 +256,64 @@ For example, the following serializer:
254256

255257
Would serialize to a nested representation like this:
256258

259+
>>> album = Album.objects.create(album_name="The Grey Album", artist='Danger Mouse')
260+
>>> Track.objects.create(album=album, order=1, title='Public Service Announcement', duration=245)
261+
<Track: Track object>
262+
>>> Track.objects.create(album=album, order=2, title='What More Can I Say', duration=264)
263+
<Track: Track object>
264+
>>> Track.objects.create(album=album, order=3, title='Encore', duration=159)
265+
<Track: Track object>
266+
>>> serializer = AlbumSerializer(instance=album)
267+
>>> serializer.data
257268
{
258269
'album_name': 'The Grey Album',
259270
'artist': 'Danger Mouse',
260271
'tracks': [
261-
{'order': 1, 'title': 'Public Service Announcement'},
262-
{'order': 2, 'title': 'What More Can I Say'},
263-
{'order': 3, 'title': 'Encore'},
272+
{'order': 1, 'title': 'Public Service Announcement', 'duration': 245},
273+
{'order': 2, 'title': 'What More Can I Say', 'duration': 264},
274+
{'order': 3, 'title': 'Encore', 'duration': 159},
264275
...
265276
],
266277
}
267278

279+
# Writable nested serializers
280+
281+
Be default nested serializers are read-only. If you want to to support write-operations to a nested serializer field you'll need to create either or both of the `create()` and/or `update()` methods, in order to explicitly specify how the child relationships should be saved.
282+
283+
class TrackSerializer(serializers.ModelSerializer):
284+
class Meta:
285+
model = Track
286+
fields = ('order', 'title')
287+
288+
class AlbumSerializer(serializers.ModelSerializer):
289+
tracks = TrackSerializer(many=True)
290+
291+
class Meta:
292+
model = Album
293+
fields = ('album_name', 'artist', 'tracks')
294+
295+
def create(self, validated_data):
296+
tracks_data = validated_data.pop('tracks')
297+
album = Album.objects.create(**validated_data)
298+
for track_data in tracks_data:
299+
Track.objects.create(album=album, **track_data)
300+
return album
301+
302+
>>> data = {
303+
'album_name': 'The Grey Album',
304+
'artist': 'Danger Mouse',
305+
'tracks': [
306+
{'order': 1, 'title': 'Public Service Announcement', 'duration': 245},
307+
{'order': 2, 'title': 'What More Can I Say', 'duration': 264},
308+
{'order': 3, 'title': 'Encore', 'duration': 159},
309+
],
310+
}
311+
>>> serializer = AlbumSerializer(data=data)
312+
>>> serializer.is_valid()
313+
True
314+
>>> serializer.save()
315+
<Album: Album object>
316+
268317
# Custom relational fields
269318

270319
To implement a custom relational field, you should override `RelatedField`, and implement the `.to_representation(self, value)` method. This method takes the target of the field as the `value` argument, and should return the representation that should be used to serialize the target. The `value` argument will typically be a model instance.

0 commit comments

Comments
 (0)