Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
D
django-rest-framework
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
django-rest-framework
Commits
f4b1dcb1
Commit
f4b1dcb1
authored
Sep 24, 2014
by
Tom Christie
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
OPTIONS support
parent
aa84432f
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
315 additions
and
80 deletions
+315
-80
rest_framework/generics.py
+1
-50
rest_framework/metadata.py
+126
-0
rest_framework/serializers.py
+4
-4
rest_framework/settings.py
+2
-0
rest_framework/utils/field_mapping.py
+11
-7
rest_framework/views.py
+5
-19
tests/test_metadata.py
+166
-0
No files found.
rest_framework/generics.py
View file @
f4b1dcb1
...
...
@@ -4,13 +4,11 @@ Generic views that provide commonly needed behaviour.
from
__future__
import
unicode_literals
from
django.db.models.query
import
QuerySet
from
django.core.exceptions
import
PermissionDenied
from
django.core.paginator
import
Paginator
,
InvalidPage
from
django.http
import
Http404
from
django.shortcuts
import
get_object_or_404
as
_get_object_or_404
from
django.utils.translation
import
ugettext
as
_
from
rest_framework
import
views
,
mixins
,
exceptions
from
rest_framework.request
import
clone_request
from
rest_framework
import
views
,
mixins
from
rest_framework.settings
import
api_settings
...
...
@@ -249,53 +247,6 @@ class GenericAPIView(views.APIView):
return
obj
# The following are placeholder methods,
# and are intended to be overridden.
#
# The are not called by GenericAPIView directly,
# but are used by the mixin methods.
def
metadata
(
self
,
request
):
"""
Return a dictionary of metadata about the view.
Used to return responses for OPTIONS requests.
We override the default behavior, and add some extra information
about the required request body for POST and PUT operations.
"""
ret
=
super
(
GenericAPIView
,
self
)
.
metadata
(
request
)
actions
=
{}
for
method
in
(
'PUT'
,
'POST'
):
if
method
not
in
self
.
allowed_methods
:
continue
cloned_request
=
clone_request
(
request
,
method
)
try
:
# Test global permissions
self
.
check_permissions
(
cloned_request
)
# Test object permissions
if
method
==
'PUT'
:
try
:
self
.
get_object
()
except
Http404
:
# Http404 should be acceptable and the serializer
# metadata should be populated. Except this so the
# outer "else" clause of the try-except-else block
# will be executed.
pass
except
(
exceptions
.
APIException
,
PermissionDenied
):
pass
else
:
# If user has appropriate permissions for the view, include
# appropriate metadata about the fields that should be supplied.
serializer
=
self
.
get_serializer
()
actions
[
method
]
=
serializer
.
metadata
()
if
actions
:
ret
[
'actions'
]
=
actions
return
ret
# Concrete view classes that provide method handlers
# by composing the mixin classes with the base view.
...
...
rest_framework/metadata.py
0 → 100644
View file @
f4b1dcb1
"""
The metadata API is used to allow cusomization of how `OPTIONS` requests
are handled. We currently provide a single default implementation that returns
some fairly ad-hoc information about the view.
Future implementations might use JSON schema or other definations in order
to return this information in a more standardized way.
"""
from
__future__
import
unicode_literals
from
django.core.exceptions
import
PermissionDenied
from
django.http
import
Http404
from
django.utils
import
six
from
django.utils.datastructures
import
SortedDict
from
rest_framework
import
exceptions
,
serializers
from
rest_framework.compat
import
force_text
from
rest_framework.request
import
clone_request
from
rest_framework.utils.field_mapping
import
ClassLookupDict
class
BaseMetadata
(
object
):
def
determine_metadata
(
self
,
request
,
view
):
"""
Return a dictionary of metadata about the view.
Used to return responses for OPTIONS requests.
"""
raise
NotImplementedError
(
".determine_metadata() must be overridden."
)
class
SimpleMetadata
(
BaseMetadata
):
"""
This is the default metadata implementation.
It returns an ad-hoc set of information about the view.
There are not any formalized standards for `OPTIONS` responses
for us to base this on.
"""
label_lookup
=
ClassLookupDict
({
serializers
.
Field
:
'field'
,
serializers
.
BooleanField
:
'boolean'
,
serializers
.
CharField
:
'string'
,
serializers
.
URLField
:
'url'
,
serializers
.
EmailField
:
'email'
,
serializers
.
RegexField
:
'regex'
,
serializers
.
SlugField
:
'slug'
,
serializers
.
IntegerField
:
'integer'
,
serializers
.
FloatField
:
'float'
,
serializers
.
DecimalField
:
'decimal'
,
serializers
.
DateField
:
'date'
,
serializers
.
DateTimeField
:
'datetime'
,
serializers
.
TimeField
:
'time'
,
serializers
.
ChoiceField
:
'choice'
,
serializers
.
MultipleChoiceField
:
'multiple choice'
,
serializers
.
FileField
:
'file upload'
,
serializers
.
ImageField
:
'image upload'
,
})
def
determine_metadata
(
self
,
request
,
view
):
metadata
=
SortedDict
()
metadata
[
'name'
]
=
view
.
get_view_name
()
metadata
[
'description'
]
=
view
.
get_view_description
()
metadata
[
'renders'
]
=
[
renderer
.
media_type
for
renderer
in
view
.
renderer_classes
]
metadata
[
'parses'
]
=
[
parser
.
media_type
for
parser
in
view
.
parser_classes
]
if
hasattr
(
view
,
'get_serializer'
):
actions
=
self
.
determine_actions
(
request
,
view
)
if
actions
:
metadata
[
'actions'
]
=
actions
return
metadata
def
determine_actions
(
self
,
request
,
view
):
"""
For generic class based views we return information about
the fields that are accepted for 'PUT' and 'POST' methods.
"""
actions
=
{}
for
method
in
set
([
'PUT'
,
'POST'
])
&
set
(
view
.
allowed_methods
):
view
.
request
=
clone_request
(
request
,
method
)
try
:
# Test global permissions
if
hasattr
(
view
,
'check_permissions'
):
view
.
check_permissions
(
view
.
request
)
# Test object permissions
if
method
==
'PUT'
and
hasattr
(
view
,
'get_object'
):
view
.
get_object
()
except
(
exceptions
.
APIException
,
PermissionDenied
,
Http404
):
pass
else
:
# If user has appropriate permissions for the view, include
# appropriate metadata about the fields that should be supplied.
serializer
=
view
.
get_serializer
()
actions
[
method
]
=
self
.
get_serializer_info
(
serializer
)
finally
:
view
.
request
=
request
return
actions
def
get_serializer_info
(
self
,
serializer
):
"""
Given an instance of a serializer, return a dictionary of metadata
about its fields.
"""
return
SortedDict
([
(
field_name
,
self
.
get_field_info
(
field
))
for
field_name
,
field
in
six
.
iteritems
(
serializer
.
fields
)
])
def
get_field_info
(
self
,
field
):
"""
Given an instance of a serializer field, return a dictionary
of metadata about it.
"""
field_info
=
SortedDict
()
field_info
[
'type'
]
=
self
.
label_lookup
[
field
]
field_info
[
'required'
]
=
getattr
(
field
,
'required'
,
False
)
for
attr
in
[
'read_only'
,
'label'
,
'help_text'
,
'min_length'
,
'max_length'
]:
value
=
getattr
(
field
,
attr
,
None
)
if
value
is
not
None
and
value
!=
''
:
field_info
[
attr
]
=
force_text
(
value
,
strings_only
=
True
)
if
hasattr
(
field
,
'choices'
):
field_info
[
'choices'
]
=
[
{
'value'
:
choice_value
,
'display_name'
:
choice_name
}
for
choice_value
,
choice_name
in
field
.
choices
.
items
()
]
return
field_info
rest_framework/serializers.py
View file @
f4b1dcb1
...
...
@@ -21,7 +21,7 @@ from rest_framework.utils import html, model_meta, representation
from
rest_framework.utils.field_mapping
import
(
get_url_kwargs
,
get_field_kwargs
,
get_relation_kwargs
,
get_nested_relation_kwargs
,
lookup_class
ClassLookupDict
)
import
copy
import
inspect
...
...
@@ -318,7 +318,7 @@ class ListSerializer(BaseSerializer):
class
ModelSerializer
(
Serializer
):
_field_mapping
=
{
_field_mapping
=
ClassLookupDict
(
{
models
.
AutoField
:
IntegerField
,
models
.
BigIntegerField
:
IntegerField
,
models
.
BooleanField
:
BooleanField
,
...
...
@@ -341,7 +341,7 @@ class ModelSerializer(Serializer):
models
.
TextField
:
CharField
,
models
.
TimeField
:
TimeField
,
models
.
URLField
:
URLField
,
}
}
)
_related_class
=
PrimaryKeyRelatedField
def
create
(
self
,
attrs
):
...
...
@@ -400,7 +400,7 @@ class ModelSerializer(Serializer):
elif
field_name
in
info
.
fields_and_pk
:
# Create regular model fields.
model_field
=
info
.
fields_and_pk
[
field_name
]
field_cls
=
lookup_class
(
self
.
_field_mapping
,
model_field
)
field_cls
=
self
.
_field_mapping
[
model_field
]
kwargs
=
get_field_kwargs
(
field_name
,
model_field
)
if
'choices'
in
kwargs
:
# Fields with choices get coerced into `ChoiceField`
...
...
rest_framework/settings.py
View file @
f4b1dcb1
...
...
@@ -45,6 +45,7 @@ DEFAULTS = {
),
'DEFAULT_THROTTLE_CLASSES'
:
(),
'DEFAULT_CONTENT_NEGOTIATION_CLASS'
:
'rest_framework.negotiation.DefaultContentNegotiation'
,
'DEFAULT_METADATA_CLASS'
:
'rest_framework.metadata.SimpleMetadata'
,
# Genric view behavior
'DEFAULT_MODEL_SERIALIZER_CLASS'
:
'rest_framework.serializers.ModelSerializer'
,
...
...
@@ -121,6 +122,7 @@ IMPORT_STRINGS = (
'DEFAULT_PERMISSION_CLASSES'
,
'DEFAULT_THROTTLE_CLASSES'
,
'DEFAULT_CONTENT_NEGOTIATION_CLASS'
,
'DEFAULT_METADATA_CLASS'
,
'DEFAULT_MODEL_SERIALIZER_CLASS'
,
'DEFAULT_PAGINATION_SERIALIZER_CLASS'
,
'DEFAULT_FILTER_BACKENDS'
,
...
...
rest_framework/utils/field_mapping.py
View file @
f4b1dcb1
...
...
@@ -9,16 +9,20 @@ from rest_framework.compat import clean_manytomany_helptext
import
inspect
def
lookup_class
(
mapping
,
instance
):
class
ClassLookupDict
(
object
):
"""
Takes a dictionary with classes as keys
, and an object
.
Traverses the object's inheritance hierarchy in method
resolution order, and returns the first matching value
Takes a dictionary with classes as keys.
Lookups against this object will traverses the object's inheritance
hierarchy in method
resolution order, and returns the first matching value
from the dictionary or raises a KeyError if nothing matches.
"""
for
cls
in
inspect
.
getmro
(
instance
.
__class__
):
if
cls
in
mapping
:
return
mapping
[
cls
]
def
__init__
(
self
,
mapping
):
self
.
mapping
=
mapping
def
__getitem__
(
self
,
key
):
for
cls
in
inspect
.
getmro
(
key
.
__class__
):
if
cls
in
self
.
mapping
:
return
self
.
mapping
[
cls
]
raise
KeyError
(
'Class
%
s not found in lookup.'
,
cls
.
__name__
)
...
...
rest_framework/views.py
View file @
f4b1dcb1
...
...
@@ -5,7 +5,6 @@ from __future__ import unicode_literals
from
django.core.exceptions
import
PermissionDenied
,
ValidationError
,
NON_FIELD_ERRORS
from
django.http
import
Http404
from
django.utils.datastructures
import
SortedDict
from
django.views.decorators.csrf
import
csrf_exempt
from
rest_framework
import
status
,
exceptions
from
rest_framework.compat
import
smart_text
,
HttpResponseBase
,
View
...
...
@@ -99,6 +98,7 @@ class APIView(View):
throttle_classes
=
api_settings
.
DEFAULT_THROTTLE_CLASSES
permission_classes
=
api_settings
.
DEFAULT_PERMISSION_CLASSES
content_negotiation_class
=
api_settings
.
DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class
=
api_settings
.
DEFAULT_METADATA_CLASS
# Allow dependancy injection of other settings to make testing easier.
settings
=
api_settings
...
...
@@ -418,22 +418,8 @@ class APIView(View):
def
options
(
self
,
request
,
*
args
,
**
kwargs
):
"""
Handler method for HTTP 'OPTIONS' request.
We may as well implement this as Django will otherwise provide
a less useful default implementation.
"""
return
Response
(
self
.
metadata
(
request
),
status
=
status
.
HTTP_200_OK
)
def
metadata
(
self
,
request
):
"""
Return a dictionary of metadata about the view.
Used to return responses for OPTIONS requests.
"""
# By default we can't provide any form-like information, however the
# generic views override this implementation and add additional
# information for POST and PUT methods, based on the serializer.
ret
=
SortedDict
()
ret
[
'name'
]
=
self
.
get_view_name
()
ret
[
'description'
]
=
self
.
get_view_description
()
ret
[
'renders'
]
=
[
renderer
.
media_type
for
renderer
in
self
.
renderer_classes
]
ret
[
'parses'
]
=
[
parser
.
media_type
for
parser
in
self
.
parser_classes
]
return
ret
if
self
.
metadata_class
is
None
:
return
self
.
http_method_not_allowed
(
request
,
*
args
,
**
kwargs
)
data
=
self
.
metadata_class
()
.
determine_metadata
(
request
,
self
)
return
Response
(
data
,
status
=
status
.
HTTP_200_OK
)
tests/test_metadata.py
0 → 100644
View file @
f4b1dcb1
from
__future__
import
unicode_literals
from
rest_framework
import
exceptions
,
serializers
,
views
from
rest_framework.request
import
Request
from
rest_framework.test
import
APIRequestFactory
import
pytest
request
=
Request
(
APIRequestFactory
()
.
options
(
'/'
))
class
TestMetadata
:
def
test_metadata
(
self
):
"""
OPTIONS requests to views should return a valid 200 response.
"""
class
ExampleView
(
views
.
APIView
):
"""Example view."""
pass
response
=
ExampleView
()
.
options
(
request
=
request
)
expected
=
{
'name'
:
'Example'
,
'description'
:
'Example view.'
,
'renders'
:
[
'application/json'
,
'text/html'
],
'parses'
:
[
'application/json'
,
'application/x-www-form-urlencoded'
,
'multipart/form-data'
]
}
assert
response
.
status_code
==
200
assert
response
.
data
==
expected
def
test_none_metadata
(
self
):
"""
OPTIONS requests to views where `metadata_class = None` should raise
a MethodNotAllowed exception, which will result in an HTTP 405 response.
"""
class
ExampleView
(
views
.
APIView
):
metadata_class
=
None
with
pytest
.
raises
(
exceptions
.
MethodNotAllowed
):
ExampleView
()
.
options
(
request
=
request
)
def
test_actions
(
self
):
"""
On generic views OPTIONS should return an 'actions' key with metadata
on the fields that may be supplied to PUT and POST requests.
"""
class
ExampleSerializer
(
serializers
.
Serializer
):
choice_field
=
serializers
.
ChoiceField
([
'red'
,
'green'
,
'blue'
])
integer_field
=
serializers
.
IntegerField
(
max_value
=
10
)
char_field
=
serializers
.
CharField
(
required
=
False
)
class
ExampleView
(
views
.
APIView
):
"""Example view."""
def
post
(
self
,
request
):
pass
def
get_serializer
(
self
):
return
ExampleSerializer
()
response
=
ExampleView
()
.
options
(
request
=
request
)
expected
=
{
'name'
:
'Example'
,
'description'
:
'Example view.'
,
'renders'
:
[
'application/json'
,
'text/html'
],
'parses'
:
[
'application/json'
,
'application/x-www-form-urlencoded'
,
'multipart/form-data'
],
'actions'
:
{
'POST'
:
{
'choice_field'
:
{
'type'
:
'choice'
,
'required'
:
True
,
'read_only'
:
False
,
'label'
:
'Choice field'
,
'choices'
:
[
{
'display_name'
:
'blue'
,
'value'
:
'blue'
},
{
'display_name'
:
'green'
,
'value'
:
'green'
},
{
'display_name'
:
'red'
,
'value'
:
'red'
}
]
},
'integer_field'
:
{
'type'
:
'integer'
,
'required'
:
True
,
'read_only'
:
False
,
'label'
:
'Integer field'
},
'char_field'
:
{
'type'
:
'string'
,
'required'
:
False
,
'read_only'
:
False
,
'label'
:
'Char field'
}
}
}
}
assert
response
.
status_code
==
200
assert
response
.
data
==
expected
def
test_global_permissions
(
self
):
"""
If a user does not have global permissions on an action, then any
metadata associated with it should not be included in OPTION responses.
"""
class
ExampleSerializer
(
serializers
.
Serializer
):
choice_field
=
serializers
.
ChoiceField
([
'red'
,
'green'
,
'blue'
])
integer_field
=
serializers
.
IntegerField
(
max_value
=
10
)
char_field
=
serializers
.
CharField
(
required
=
False
)
class
ExampleView
(
views
.
APIView
):
"""Example view."""
def
post
(
self
,
request
):
pass
def
put
(
self
,
request
):
pass
def
get_serializer
(
self
):
return
ExampleSerializer
()
def
check_permissions
(
self
,
request
):
if
request
.
method
==
'POST'
:
raise
exceptions
.
PermissionDenied
()
response
=
ExampleView
()
.
options
(
request
=
request
)
assert
response
.
status_code
==
200
assert
list
(
response
.
data
[
'actions'
]
.
keys
())
==
[
'PUT'
]
def
test_object_permissions
(
self
):
"""
If a user does not have object permissions on an action, then any
metadata associated with it should not be included in OPTION responses.
"""
class
ExampleSerializer
(
serializers
.
Serializer
):
choice_field
=
serializers
.
ChoiceField
([
'red'
,
'green'
,
'blue'
])
integer_field
=
serializers
.
IntegerField
(
max_value
=
10
)
char_field
=
serializers
.
CharField
(
required
=
False
)
class
ExampleView
(
views
.
APIView
):
"""Example view."""
def
post
(
self
,
request
):
pass
def
put
(
self
,
request
):
pass
def
get_serializer
(
self
):
return
ExampleSerializer
()
def
get_object
(
self
):
if
self
.
request
.
method
==
'PUT'
:
raise
exceptions
.
PermissionDenied
()
response
=
ExampleView
()
.
options
(
request
=
request
)
assert
response
.
status_code
==
200
assert
list
(
response
.
data
[
'actions'
]
.
keys
())
==
[
'POST'
]
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment