mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-23 01:57:00 +03:00
merge
This commit is contained in:
commit
279fa0d371
2
AUTHORS
2
AUTHORS
|
@ -27,6 +27,8 @@ Natim <natim>
|
||||||
Sebastian Żurek <sebzur>
|
Sebastian Żurek <sebzur>
|
||||||
Benoit C <dzen>
|
Benoit C <dzen>
|
||||||
Chris Pickett <bunchesofdonald>
|
Chris Pickett <bunchesofdonald>
|
||||||
|
Ben Timby <btimby>
|
||||||
|
Michele Lazzeri <michelelazzeri-nextage>
|
||||||
|
|
||||||
THANKS TO:
|
THANKS TO:
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,43 @@
|
||||||
|
Release Notes
|
||||||
|
=============
|
||||||
|
|
||||||
|
development
|
||||||
|
-----------
|
||||||
|
|
||||||
|
* Saner template variable autoescaping.
|
||||||
|
* Use `staticfiles` for css files.
|
||||||
|
- Easier to override. Won't conflict with customised admin styles (eg grappelli)
|
||||||
|
* Drop implied 'pk' filter if last arg in urlconf is unnamed.
|
||||||
|
- Too magical. Explict is better than implicit.
|
||||||
|
* Bugfixes:
|
||||||
|
- Bug with PerUserThrottling when user contains unicode chars.
|
||||||
|
|
||||||
|
0.3.2
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Bugfixes:
|
||||||
|
* Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115)
|
||||||
|
* serialize_model method in serializer.py may cause wrong value (#73)
|
||||||
|
* Fix Error when clicking OPTIONS button (#146)
|
||||||
|
* And many other fixes
|
||||||
|
* Remove short status codes
|
||||||
|
- Zen of Python: "There should be one-- and preferably only one --obvious way to do it."
|
||||||
|
* get_name, get_description become methods on the view - makes them overridable.
|
||||||
|
* Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering
|
||||||
|
|
||||||
|
0.3.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* [not documented]
|
||||||
|
|
||||||
0.3.0
|
0.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
* JSONP Support
|
* JSONP Support
|
||||||
* Bugfixes, including support for latest markdown release
|
* Bugfixes, including support for latest markdown release
|
||||||
|
|
||||||
0.2.4
|
0.2.4
|
||||||
|
-----
|
||||||
|
|
||||||
* Fix broken IsAdminUser permission.
|
* Fix broken IsAdminUser permission.
|
||||||
* OPTIONS support.
|
* OPTIONS support.
|
||||||
|
@ -11,20 +45,24 @@
|
||||||
* Drop mentions of Blog, BitBucket.
|
* Drop mentions of Blog, BitBucket.
|
||||||
|
|
||||||
0.2.3
|
0.2.3
|
||||||
|
-----
|
||||||
|
|
||||||
* Fix some throttling bugs.
|
* Fix some throttling bugs.
|
||||||
* ``X-Throttle`` header on throttling.
|
* ``X-Throttle`` header on throttling.
|
||||||
* Support for nesting resources on related models.
|
* Support for nesting resources on related models.
|
||||||
|
|
||||||
0.2.2
|
0.2.2
|
||||||
|
-----
|
||||||
|
|
||||||
* Throttling support complete.
|
* Throttling support complete.
|
||||||
|
|
||||||
0.2.1
|
0.2.1
|
||||||
|
-----
|
||||||
|
|
||||||
* Couple of simple bugfixes over 0.2.0
|
* Couple of simple bugfixes over 0.2.0
|
||||||
|
|
||||||
0.2.0
|
0.2.0
|
||||||
|
-----
|
||||||
|
|
||||||
* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
|
* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
|
||||||
The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
|
The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
|
||||||
|
@ -49,9 +87,11 @@
|
||||||
You can reuse these mixin classes individually without using the ``View`` class.
|
You can reuse these mixin classes individually without using the ``View`` class.
|
||||||
|
|
||||||
0.1.1
|
0.1.1
|
||||||
|
-----
|
||||||
|
|
||||||
* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
|
* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
|
||||||
|
|
||||||
0.1.0
|
0.1.0
|
||||||
|
-----
|
||||||
|
|
||||||
* Initial release.
|
* Initial release.
|
|
@ -16,7 +16,7 @@ Full documentation for the project is available at http://django-rest-framework.
|
||||||
Issue tracking is on `GitHub <https://github.com/tomchristie/django-rest-framework/issues>`_.
|
Issue tracking is on `GitHub <https://github.com/tomchristie/django-rest-framework/issues>`_.
|
||||||
General questions should be taken to the `discussion group <http://groups.google.com/group/django-rest-framework>`_.
|
General questions should be taken to the `discussion group <http://groups.google.com/group/django-rest-framework>`_.
|
||||||
|
|
||||||
We also have a `Jenkins service <http://jenkins.tibold.nl/job/djangorestframework/>`_ which runs our test suite.
|
We also have a `Jenkins service <http://jenkins.tibold.nl/job/djangorestframework1/>`_ which runs our test suite.
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
__version__ = '0.3.2-dev'
|
__version__ = '0.3.3-dev'
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
VERSION = __version__ # synonym
|
||||||
|
|
|
@ -87,25 +87,12 @@ class UserLoggedInAuthentication(BaseAuthentication):
|
||||||
Returns a :obj:`User` if the request session currently has a logged in user.
|
Returns a :obj:`User` if the request session currently has a logged in user.
|
||||||
Otherwise returns :const:`None`.
|
Otherwise returns :const:`None`.
|
||||||
"""
|
"""
|
||||||
# TODO: Might be cleaner to switch this back to using request.POST,
|
request.DATA # Make sure our generic parsing runs first
|
||||||
# and let FormParser/MultiPartParser deal with the consequences.
|
|
||||||
if getattr(request, 'user', None) and request.user.is_active:
|
if getattr(request, 'user', None) and request.user.is_active:
|
||||||
# Enforce CSRF validation for session based authentication.
|
# Enforce CSRF validation for session based authentication.
|
||||||
|
|
||||||
# Temporarily replace request.POST with .DATA, to use our generic parsing.
|
|
||||||
# If DATA is not dict-like, use an empty dict.
|
|
||||||
if request.method.upper() == 'POST':
|
|
||||||
if hasattr(request.DATA, 'get'):
|
|
||||||
request._post = request.DATA
|
|
||||||
else:
|
|
||||||
request._post = {}
|
|
||||||
|
|
||||||
resp = CsrfViewMiddleware().process_view(request, None, (), {})
|
resp = CsrfViewMiddleware().process_view(request, None, (), {})
|
||||||
|
|
||||||
# Replace request.POST
|
|
||||||
if request.method.upper() == 'POST':
|
|
||||||
del(request._post)
|
|
||||||
|
|
||||||
if resp is None: # csrf passed
|
if resp is None: # csrf passed
|
||||||
return request.user
|
return request.user
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -384,11 +384,6 @@ class ModelMixin(object):
|
||||||
if BaseRenderer._FORMAT_QUERY_PARAM in tmp:
|
if BaseRenderer._FORMAT_QUERY_PARAM in tmp:
|
||||||
del tmp[BaseRenderer._FORMAT_QUERY_PARAM]
|
del tmp[BaseRenderer._FORMAT_QUERY_PARAM]
|
||||||
|
|
||||||
if args:
|
|
||||||
# If we have any no kwargs then assume the last arg represents the
|
|
||||||
# primrary key. Otherwise assume the kwargs uniquely identify the
|
|
||||||
# model.
|
|
||||||
tmp.update({'pk': args[-1]})
|
|
||||||
return Q(**tmp)
|
return Q(**tmp)
|
||||||
|
|
||||||
def get_instance_data(self, model, content, **kwargs):
|
def get_instance_data(self, model, content, **kwargs):
|
||||||
|
|
|
@ -188,7 +188,7 @@ class PerUserThrottling(BaseThrottle):
|
||||||
|
|
||||||
def get_cache_key(self):
|
def get_cache_key(self):
|
||||||
if self.auth.is_authenticated():
|
if self.auth.is_authenticated():
|
||||||
ident = str(self.auth)
|
ident = self.auth.id
|
||||||
else:
|
else:
|
||||||
ident = self.view.request.META.get('REMOTE_ADDR', None)
|
ident = self.view.request.META.get('REMOTE_ADDR', None)
|
||||||
return 'throttle_user_%s' % ident
|
return 'throttle_user_%s' % ident
|
||||||
|
|
|
@ -12,10 +12,9 @@ from django.template import RequestContext, loader
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
|
|
||||||
from djangorestframework.compat import apply_markdown, yaml
|
from djangorestframework.compat import yaml
|
||||||
from djangorestframework.utils import dict2xml, url_resolves
|
from djangorestframework.utils import dict2xml, url_resolves
|
||||||
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
|
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
|
||||||
from djangorestframework.utils.description import get_name, get_description
|
|
||||||
from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
|
from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
|
||||||
from djangorestframework import VERSION
|
from djangorestframework import VERSION
|
||||||
|
|
||||||
|
@ -296,6 +295,20 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
# Okey doke, let's do it
|
# Okey doke, let's do it
|
||||||
return GenericContentForm(view.request)
|
return GenericContentForm(view.request)
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
try:
|
||||||
|
return self.view.get_name()
|
||||||
|
except AttributeError:
|
||||||
|
return self.view.__doc__
|
||||||
|
|
||||||
|
def get_description(self, html=None):
|
||||||
|
if html is None:
|
||||||
|
html = bool('html' in self.format)
|
||||||
|
try:
|
||||||
|
return self.view.get_description(html)
|
||||||
|
except AttributeError:
|
||||||
|
return self.view.__doc__
|
||||||
|
|
||||||
def render(self, obj=None, media_type=None):
|
def render(self, obj=None, media_type=None):
|
||||||
"""
|
"""
|
||||||
Renders *obj* using the :attr:`template` set on the class.
|
Renders *obj* using the :attr:`template` set on the class.
|
||||||
|
@ -316,15 +329,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
login_url = None
|
login_url = None
|
||||||
logout_url = None
|
logout_url = None
|
||||||
|
|
||||||
name = get_name(self.view)
|
name = self.get_name()
|
||||||
description = get_description(self.view)
|
description = self.get_description()
|
||||||
|
|
||||||
markeddown = None
|
|
||||||
if apply_markdown:
|
|
||||||
try:
|
|
||||||
markeddown = apply_markdown(description)
|
|
||||||
except AttributeError:
|
|
||||||
markeddown = None
|
|
||||||
|
|
||||||
breadcrumb_list = get_breadcrumbs(self.view.request.path)
|
breadcrumb_list = get_breadcrumbs(self.view.request.path)
|
||||||
|
|
||||||
|
@ -337,7 +343,6 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
'description': description,
|
'description': description,
|
||||||
'name': name,
|
'name': name,
|
||||||
'version': VERSION,
|
'version': VERSION,
|
||||||
'markeddown': markeddown,
|
|
||||||
'breadcrumblist': breadcrumb_list,
|
'breadcrumblist': breadcrumb_list,
|
||||||
'available_formats': self.view._rendered_formats,
|
'available_formats': self.view._rendered_formats,
|
||||||
'put_form': put_form_instance,
|
'put_form': put_form_instance,
|
||||||
|
|
|
@ -97,6 +97,12 @@ INSTALLED_APPS = (
|
||||||
'djangorestframework',
|
'djangorestframework',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import django
|
||||||
|
|
||||||
|
if django.VERSION < (1, 3):
|
||||||
|
INSTALLED_APPS += ('staticfiles',)
|
||||||
|
|
||||||
|
|
||||||
# OAuth support is optional, so we only test oauth if it's installed.
|
# OAuth support is optional, so we only test oauth if it's installed.
|
||||||
try:
|
try:
|
||||||
import oauth_provider
|
import oauth_provider
|
||||||
|
|
1152
djangorestframework/static/css/djangorestframework.css
Normal file
1152
djangorestframework/static/css/djangorestframework.css
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -5,7 +5,6 @@ See RFC 2616 - Sec 10: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||||
Also see django.core.handlers.wsgi.STATUS_CODE_TEXT
|
Also see django.core.handlers.wsgi.STATUS_CODE_TEXT
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Verbose format
|
|
||||||
HTTP_100_CONTINUE = 100
|
HTTP_100_CONTINUE = 100
|
||||||
HTTP_101_SWITCHING_PROTOCOLS = 101
|
HTTP_101_SWITCHING_PROTOCOLS = 101
|
||||||
HTTP_200_OK = 200
|
HTTP_200_OK = 200
|
||||||
|
|
|
@ -1,25 +1,14 @@
|
||||||
{% load urlize_quoted_links %}{% load add_query_param %}<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
{% load urlize_quoted_links %}
|
||||||
|
{% load add_query_param %}
|
||||||
|
{% load static %}
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}css/djangorestframework.css'/>
|
||||||
/* Override some of the Django admin styling */
|
<title>Django REST framework - {{ name }}</title>
|
||||||
#site-name a {color: #F4F379 !important;}
|
</head>
|
||||||
.errorlist {display: inline !important}
|
|
||||||
.errorlist li {display: inline !important; background: white !important; color: black !important; border: 0 !important;}
|
|
||||||
/* Custom styles */
|
|
||||||
.version{font-size:8px;}
|
|
||||||
</style>
|
|
||||||
{% if ADMIN_MEDIA_PREFIX %}
|
|
||||||
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/base.css'/>
|
|
||||||
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/forms.css'/>
|
|
||||||
{% else %}
|
|
||||||
<link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/base.css'/>
|
|
||||||
<link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/forms.css'/>
|
|
||||||
{% endif %}
|
|
||||||
<title>Django REST framework - {{ name }}</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
|
|
||||||
|
@ -34,7 +23,7 @@
|
||||||
|
|
||||||
<div class="breadcrumbs">
|
<div class="breadcrumbs">
|
||||||
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
|
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
|
||||||
<a href="{{breadcrumb_url}}">{{breadcrumb_name}}</a> {% if not forloop.last %}›{% endif %}
|
<a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a> {% if not forloop.last %}›{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -50,7 +39,7 @@
|
||||||
|
|
||||||
<div class='content-main'>
|
<div class='content-main'>
|
||||||
<h1>{{ name }}</h1>
|
<h1>{{ name }}</h1>
|
||||||
<p>{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}</p>
|
<p>{{ description }}</p>
|
||||||
<div class='module'>
|
<div class='module'>
|
||||||
<pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %}
|
<pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %}
|
||||||
{% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
|
{% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{{ name }}
|
{% autoescape off %}{{ name }}
|
||||||
|
|
||||||
{{ description }}
|
{{ description }}
|
||||||
|
|
||||||
{% autoescape off %}HTTP/1.0 {{ response.status }} {{ response.status_text }}
|
HTTP/1.0 {{ response.status }} {{ response.status_text }}
|
||||||
{% for key, val in response.headers.items %}{{ key }}: {{ val }}
|
{% for key, val in response.headers.items %}{{ key }}: {{ val }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ content }}{% endautoescape %}
|
{{ content }}{% endautoescape %}
|
||||||
|
|
|
@ -5,7 +5,7 @@ register = Library()
|
||||||
|
|
||||||
def add_query_param(url, param):
|
def add_query_param(url, param):
|
||||||
(key, sep, val) = param.partition('=')
|
(key, sep, val) = param.partition('=')
|
||||||
return unicode(URLObject(url) & (key, val))
|
return unicode(URLObject.parse(url) & (key, val))
|
||||||
|
|
||||||
|
|
||||||
register.filter('add_query_param', add_query_param)
|
register.filter('add_query_param', add_query_param)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import base64
|
||||||
|
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
permissions = ( permissions.IsAuthenticated, )
|
permissions = (permissions.IsAuthenticated,)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
return {'a': 1, 'b': 2, 'c': 3}
|
return {'a': 1, 'b': 2, 'c': 3}
|
||||||
|
@ -74,24 +74,32 @@ class SessionAuthTests(TestCase):
|
||||||
self.csrf_client.logout()
|
self.csrf_client.logout()
|
||||||
|
|
||||||
def test_post_form_session_auth_failing_csrf(self):
|
def test_post_form_session_auth_failing_csrf(self):
|
||||||
"""Ensure POSTing form over session authentication without CSRF token fails."""
|
"""
|
||||||
|
Ensure POSTing form over session authentication without CSRF token fails.
|
||||||
|
"""
|
||||||
self.csrf_client.login(username=self.username, password=self.password)
|
self.csrf_client.login(username=self.username, password=self.password)
|
||||||
response = self.csrf_client.post('/', {'example': 'example'})
|
response = self.csrf_client.post('/', {'example': 'example'})
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_post_form_session_auth_passing(self):
|
def test_post_form_session_auth_passing(self):
|
||||||
"""Ensure POSTing form over session authentication with logged in user and CSRF token passes."""
|
"""
|
||||||
|
Ensure POSTing form over session authentication with logged in user and CSRF token passes.
|
||||||
|
"""
|
||||||
self.non_csrf_client.login(username=self.username, password=self.password)
|
self.non_csrf_client.login(username=self.username, password=self.password)
|
||||||
response = self.non_csrf_client.post('/', {'example': 'example'})
|
response = self.non_csrf_client.post('/', {'example': 'example'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_put_form_session_auth_passing(self):
|
def test_put_form_session_auth_passing(self):
|
||||||
"""Ensure PUTting form over session authentication with logged in user and CSRF token passes."""
|
"""
|
||||||
|
Ensure PUTting form over session authentication with logged in user and CSRF token passes.
|
||||||
|
"""
|
||||||
self.non_csrf_client.login(username=self.username, password=self.password)
|
self.non_csrf_client.login(username=self.username, password=self.password)
|
||||||
response = self.non_csrf_client.put('/', {'example': 'example'})
|
response = self.non_csrf_client.put('/', {'example': 'example'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_post_form_session_auth_failing(self):
|
def test_post_form_session_auth_failing(self):
|
||||||
"""Ensure POSTing form over session authentication without logged in user fails."""
|
"""
|
||||||
|
Ensure POSTing form over session authentication without logged in user fails.
|
||||||
|
"""
|
||||||
response = self.csrf_client.post('/', {'example': 'example'})
|
response = self.csrf_client.post('/', {'example': 'example'})
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
from djangorestframework.compat import apply_markdown
|
from djangorestframework.compat import apply_markdown
|
||||||
from djangorestframework.utils.description import get_name, get_description
|
|
||||||
|
|
||||||
# We check that docstrings get nicely un-indented.
|
# We check that docstrings get nicely un-indented.
|
||||||
DESCRIPTION = """an example docstring
|
DESCRIPTION = """an example docstring
|
||||||
|
@ -51,15 +50,15 @@ class TestViewNamesAndDescriptions(TestCase):
|
||||||
"""Ensure Resource names are based on the classname by default."""
|
"""Ensure Resource names are based on the classname by default."""
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
pass
|
pass
|
||||||
self.assertEquals(get_name(MockView()), 'Mock')
|
self.assertEquals(MockView().get_name(), 'Mock')
|
||||||
|
|
||||||
# This has been turned off now.
|
def test_resource_name_can_be_set_explicitly(self):
|
||||||
#def test_resource_name_can_be_set_explicitly(self):
|
"""Ensure Resource names can be set using the 'get_name' method."""
|
||||||
# """Ensure Resource names can be set using the 'name' class attribute."""
|
example = 'Some Other Name'
|
||||||
# example = 'Some Other Name'
|
class MockView(View):
|
||||||
# class MockView(View):
|
def get_name(self):
|
||||||
# name = example
|
return example
|
||||||
# self.assertEquals(get_name(MockView()), example)
|
self.assertEquals(MockView().get_name(), example)
|
||||||
|
|
||||||
def test_resource_description_uses_docstring_by_default(self):
|
def test_resource_description_uses_docstring_by_default(self):
|
||||||
"""Ensure Resource names are based on the docstring by default."""
|
"""Ensure Resource names are based on the docstring by default."""
|
||||||
|
@ -79,29 +78,30 @@ class TestViewNamesAndDescriptions(TestCase):
|
||||||
|
|
||||||
# hash style header #"""
|
# hash style header #"""
|
||||||
|
|
||||||
self.assertEquals(get_description(MockView()), DESCRIPTION)
|
self.assertEquals(MockView().get_description(), DESCRIPTION)
|
||||||
|
|
||||||
# This has been turned off now
|
def test_resource_description_can_be_set_explicitly(self):
|
||||||
#def test_resource_description_can_be_set_explicitly(self):
|
"""Ensure Resource descriptions can be set using the 'get_description' method."""
|
||||||
# """Ensure Resource descriptions can be set using the 'description' class attribute."""
|
example = 'Some other description'
|
||||||
# example = 'Some other description'
|
class MockView(View):
|
||||||
# class MockView(View):
|
"""docstring"""
|
||||||
# """docstring"""
|
def get_description(self):
|
||||||
# description = example
|
return example
|
||||||
# self.assertEquals(get_description(MockView()), example)
|
self.assertEquals(MockView().get_description(), example)
|
||||||
|
|
||||||
#def test_resource_description_does_not_require_docstring(self):
|
def test_resource_description_does_not_require_docstring(self):
|
||||||
# """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute."""
|
"""Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'get_description' method."""
|
||||||
# example = 'Some other description'
|
example = 'Some other description'
|
||||||
# class MockView(View):
|
class MockView(View):
|
||||||
# description = example
|
def get_description(self):
|
||||||
# self.assertEquals(get_description(MockView()), example)
|
return example
|
||||||
|
self.assertEquals(MockView().get_description(), example)
|
||||||
|
|
||||||
def test_resource_description_can_be_empty(self):
|
def test_resource_description_can_be_empty(self):
|
||||||
"""Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string"""
|
"""Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string."""
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
pass
|
pass
|
||||||
self.assertEquals(get_description(MockView()), '')
|
self.assertEquals(MockView().get_description(), '')
|
||||||
|
|
||||||
def test_markdown(self):
|
def test_markdown(self):
|
||||||
"""Ensure markdown to HTML works as expected"""
|
"""Ensure markdown to HTML works as expected"""
|
||||||
|
|
|
@ -30,7 +30,7 @@ class TestModelRead(TestModelsTestCase):
|
||||||
mixin = ReadModelMixin()
|
mixin = ReadModelMixin()
|
||||||
mixin.resource = GroupResource
|
mixin.resource = GroupResource
|
||||||
|
|
||||||
response = mixin.get(request, group.id)
|
response = mixin.get(request, id=group.id)
|
||||||
self.assertEquals(group.name, response.name)
|
self.assertEquals(group.name, response.name)
|
||||||
|
|
||||||
def test_read_404(self):
|
def test_read_404(self):
|
||||||
|
@ -41,8 +41,7 @@ class TestModelRead(TestModelsTestCase):
|
||||||
mixin = ReadModelMixin()
|
mixin = ReadModelMixin()
|
||||||
mixin.resource = GroupResource
|
mixin.resource = GroupResource
|
||||||
|
|
||||||
with self.assertRaises(ErrorResponse):
|
self.assertRaises(ErrorResponse, mixin.get, request, id=12345)
|
||||||
response = mixin.get(request, 12345)
|
|
||||||
|
|
||||||
|
|
||||||
class TestModelCreation(TestModelsTestCase):
|
class TestModelCreation(TestModelsTestCase):
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import re
|
||||||
|
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
@ -174,6 +176,12 @@ class RendererIntegrationTests(TestCase):
|
||||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
_flat_repr = '{"foo": ["bar", "baz"]}'
|
||||||
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
|
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
|
||||||
|
|
||||||
|
def strip_trailing_whitespace(content):
|
||||||
|
"""
|
||||||
|
Seems to be some inconsistencies re. trailing whitespace with
|
||||||
|
different versions of the json lib.
|
||||||
|
"""
|
||||||
|
return re.sub(' +\n', '\n', content)
|
||||||
|
|
||||||
class JSONRendererTests(TestCase):
|
class JSONRendererTests(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -187,6 +195,7 @@ class JSONRendererTests(TestCase):
|
||||||
obj = {'foo': ['bar', 'baz']}
|
obj = {'foo': ['bar', 'baz']}
|
||||||
renderer = JSONRenderer(None)
|
renderer = JSONRenderer(None)
|
||||||
content = renderer.render(obj, 'application/json')
|
content = renderer.render(obj, 'application/json')
|
||||||
|
# Fix failing test case which depends on version of JSON library.
|
||||||
self.assertEquals(content, _flat_repr)
|
self.assertEquals(content, _flat_repr)
|
||||||
|
|
||||||
def test_with_content_type_args(self):
|
def test_with_content_type_args(self):
|
||||||
|
@ -196,7 +205,7 @@ class JSONRendererTests(TestCase):
|
||||||
obj = {'foo': ['bar', 'baz']}
|
obj = {'foo': ['bar', 'baz']}
|
||||||
renderer = JSONRenderer(None)
|
renderer = JSONRenderer(None)
|
||||||
content = renderer.render(obj, 'application/json; indent=2')
|
content = renderer.render(obj, 'application/json; indent=2')
|
||||||
self.assertEquals(content, _indented_repr)
|
self.assertEquals(strip_trailing_whitespace(content), _indented_repr)
|
||||||
|
|
||||||
def test_render_and_parse(self):
|
def test_render_and_parse(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.resources import FormResource, ModelResource
|
||||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
|
||||||
from djangorestframework.response import ErrorResponse
|
from djangorestframework.response import ErrorResponse
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
from djangorestframework.resources import Resource
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestDisabledValidations(TestCase):
|
class TestDisabledValidations(TestCase):
|
||||||
|
@ -22,7 +19,7 @@ class TestDisabledValidations(TestCase):
|
||||||
resource = DisabledFormResource
|
resource = DisabledFormResource
|
||||||
|
|
||||||
view = MockView()
|
view = MockView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty': 'uiop'}
|
||||||
self.assertEqual(FormResource(view).validate_request(content, None), content)
|
self.assertEqual(FormResource(view).validate_request(content, None), content)
|
||||||
|
|
||||||
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
||||||
|
@ -35,10 +32,9 @@ class TestDisabledValidations(TestCase):
|
||||||
resource = DisabledFormResource
|
resource = DisabledFormResource
|
||||||
|
|
||||||
view = MockView()
|
view = MockView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty': 'uiop'}
|
||||||
self.assertEqual(FormResource(view).get_bound_form(content), None)
|
self.assertEqual(FormResource(view).get_bound_form(content), None)
|
||||||
|
|
||||||
|
|
||||||
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
||||||
"""If the view's form is None and does not have a Resource with a model set then
|
"""If the view's form is None and does not have a Resource with a model set then
|
||||||
ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
|
ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
|
||||||
|
@ -47,8 +43,8 @@ class TestDisabledValidations(TestCase):
|
||||||
resource = ModelResource
|
resource = ModelResource
|
||||||
|
|
||||||
view = DisabledModelFormView()
|
view = DisabledModelFormView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty': 'uiop'}
|
||||||
self.assertEqual(ModelResource(view).get_bound_form(content), None)#
|
self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
||||||
|
|
||||||
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
|
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
|
||||||
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
||||||
|
@ -56,9 +52,10 @@ class TestDisabledValidations(TestCase):
|
||||||
resource = ModelResource
|
resource = ModelResource
|
||||||
|
|
||||||
view = DisabledModelFormView()
|
view = DisabledModelFormView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty': 'uiop'}
|
||||||
self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
||||||
|
|
||||||
|
|
||||||
class TestNonFieldErrors(TestCase):
|
class TestNonFieldErrors(TestCase):
|
||||||
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
||||||
|
|
||||||
|
@ -72,7 +69,7 @@ class TestNonFieldErrors(TestCase):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
|
if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
|
||||||
raise forms.ValidationError(self.ERROR_TEXT)
|
raise forms.ValidationError(self.ERROR_TEXT)
|
||||||
return self.cleaned_data #pragma: no cover
|
return self.cleaned_data
|
||||||
|
|
||||||
class MockResource(FormResource):
|
class MockResource(FormResource):
|
||||||
form = MockForm
|
form = MockForm
|
||||||
|
@ -87,7 +84,7 @@ class TestNonFieldErrors(TestCase):
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
||||||
else:
|
else:
|
||||||
self.fail('ErrorResponse was not raised') #pragma: no cover
|
self.fail('ErrorResponse was not raised')
|
||||||
|
|
||||||
|
|
||||||
class TestFormValidation(TestCase):
|
class TestFormValidation(TestCase):
|
||||||
|
@ -115,10 +112,9 @@ class TestFormValidation(TestCase):
|
||||||
self.MockFormView = MockFormView
|
self.MockFormView = MockFormView
|
||||||
self.MockModelFormView = MockModelFormView
|
self.MockModelFormView = MockModelFormView
|
||||||
|
|
||||||
|
|
||||||
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
|
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
|
||||||
"""If the content is already valid and clean then validate(content) should just return the content unmodified."""
|
"""If the content is already valid and clean then validate(content) should just return the content unmodified."""
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty': 'uiop'}
|
||||||
self.assertEqual(validator.validate_request(content, None), content)
|
self.assertEqual(validator.validate_request(content, None), content)
|
||||||
|
|
||||||
def validation_failure_raises_response_exception(self, validator):
|
def validation_failure_raises_response_exception(self, validator):
|
||||||
|
@ -143,7 +139,9 @@ class TestFormValidation(TestCase):
|
||||||
raise errors on unexpected request data"""
|
raise errors on unexpected request data"""
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
validator.allow_unknown_form_fields = True
|
validator.allow_unknown_form_fields = True
|
||||||
self.assertDictEqual({'qwerty': u'uiop'}, validator.validate_request(content, None), "Resource didn't accept unknown fields.")
|
self.assertEqual({'qwerty': u'uiop'},
|
||||||
|
validator.validate_request(content, None),
|
||||||
|
"Resource didn't accept unknown fields.")
|
||||||
validator.allow_unknown_form_fields = False
|
validator.allow_unknown_form_fields = False
|
||||||
|
|
||||||
def validation_does_not_require_extra_fields_if_explicitly_set(self, validator):
|
def validation_does_not_require_extra_fields_if_explicitly_set(self, validator):
|
||||||
|
@ -159,7 +157,7 @@ class TestFormValidation(TestCase):
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised') #pragma: no cover
|
self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
def validation_failed_due_to_field_error_returns_appropriate_message(self, validator):
|
def validation_failed_due_to_field_error_returns_appropriate_message(self, validator):
|
||||||
"""If validation fails due to a field error, ensure the response contains a single field error"""
|
"""If validation fails due to a field error, ensure the response contains a single field error"""
|
||||||
|
@ -169,7 +167,7 @@ class TestFormValidation(TestCase):
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised') #pragma: no cover
|
self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
def validation_failed_due_to_invalid_field_returns_appropriate_message(self, validator):
|
def validation_failed_due_to_invalid_field_returns_appropriate_message(self, validator):
|
||||||
"""If validation fails due to an invalid field, ensure the response contains a single field error"""
|
"""If validation fails due to an invalid field, ensure the response contains a single field error"""
|
||||||
|
@ -179,7 +177,7 @@ class TestFormValidation(TestCase):
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
|
self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised') #pragma: no cover
|
self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator):
|
def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator):
|
||||||
"""If validation for multiple reasons, ensure the response contains each error"""
|
"""If validation for multiple reasons, ensure the response contains each error"""
|
||||||
|
@ -190,7 +188,7 @@ class TestFormValidation(TestCase):
|
||||||
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
|
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
|
||||||
'extra': ['This field does not exist.']}})
|
'extra': ['This field does not exist.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised') #pragma: no cover
|
self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
# Tests on FormResource
|
# Tests on FormResource
|
||||||
|
|
||||||
|
@ -209,7 +207,7 @@ class TestFormValidation(TestCase):
|
||||||
def test_validation_allows_extra_fields_if_explicitly_set(self):
|
def test_validation_allows_extra_fields_if_explicitly_set(self):
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
validator = self.MockFormResource(self.MockFormView())
|
||||||
self.validation_allows_extra_fields_if_explicitly_set(validator)
|
self.validation_allows_extra_fields_if_explicitly_set(validator)
|
||||||
|
|
||||||
def test_validation_allows_unknown_fields_if_explicitly_allowed(self):
|
def test_validation_allows_unknown_fields_if_explicitly_allowed(self):
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
validator = self.MockFormResource(self.MockFormView())
|
||||||
self.validation_allows_unknown_fields_if_explicitly_allowed(validator)
|
self.validation_allows_unknown_fields_if_explicitly_allowed(validator)
|
||||||
|
@ -294,22 +292,21 @@ class TestModelFormValidator(TestCase):
|
||||||
|
|
||||||
self.validator = MockResource(MockView)
|
self.validator = MockResource(MockView)
|
||||||
|
|
||||||
|
|
||||||
def test_property_fields_are_allowed_on_model_forms(self):
|
def test_property_fields_are_allowed_on_model_forms(self):
|
||||||
"""Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
|
"""Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
|
||||||
content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'}
|
content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only'}
|
||||||
self.assertEqual(self.validator.validate_request(content, None), content)
|
self.assertEqual(self.validator.validate_request(content, None), content)
|
||||||
|
|
||||||
def test_property_fields_are_not_required_on_model_forms(self):
|
def test_property_fields_are_not_required_on_model_forms(self):
|
||||||
"""Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
|
"""Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
|
||||||
content = {'qwerty':'example', 'uiop': 'example'}
|
content = {'qwerty': 'example', 'uiop': 'example'}
|
||||||
self.assertEqual(self.validator.validate_request(content, None), content)
|
self.assertEqual(self.validator.validate_request(content, None), content)
|
||||||
|
|
||||||
def test_extra_fields_not_allowed_on_model_forms(self):
|
def test_extra_fields_not_allowed_on_model_forms(self):
|
||||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
|
content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'}
|
||||||
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
||||||
|
|
||||||
def test_validate_requires_fields_on_model_forms(self):
|
def test_validate_requires_fields_on_model_forms(self):
|
||||||
|
@ -321,10 +318,8 @@ class TestModelFormValidator(TestCase):
|
||||||
|
|
||||||
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
||||||
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
||||||
content = {'qwerty':'example', 'readonly': 'read only'}
|
content = {'qwerty': 'example', 'readonly': 'read only'}
|
||||||
self.validator.validate_request(content, None)
|
self.validator.validate_request(content, None)
|
||||||
|
|
||||||
def test_model_form_validator_uses_model_forms(self):
|
def test_model_form_validator_uses_model_forms(self):
|
||||||
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
|
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
from django.core.urlresolvers import resolve
|
from django.core.urlresolvers import resolve
|
||||||
from djangorestframework.utils.description import get_name
|
|
||||||
|
|
||||||
|
|
||||||
def get_breadcrumbs(url):
|
def get_breadcrumbs(url):
|
||||||
"""Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
|
"""Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
|
||||||
|
@ -17,7 +15,7 @@ def get_breadcrumbs(url):
|
||||||
else:
|
else:
|
||||||
# Check if this is a REST framework view, and if so add it to the breadcrumbs
|
# Check if this is a REST framework view, and if so add it to the breadcrumbs
|
||||||
if isinstance(getattr(view, 'cls_instance', None), View):
|
if isinstance(getattr(view, 'cls_instance', None), View):
|
||||||
breadcrumbs_list.insert(0, (get_name(view), url))
|
breadcrumbs_list.insert(0, (view.cls_instance.get_name(), url))
|
||||||
|
|
||||||
if url == '':
|
if url == '':
|
||||||
# All done
|
# All done
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
"""
|
|
||||||
Get a descriptive name and description for a view.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
|
||||||
|
|
||||||
|
|
||||||
# These a a bit Grungy, but they do the job.
|
|
||||||
|
|
||||||
def get_name(view):
|
|
||||||
"""
|
|
||||||
Return a name for the view.
|
|
||||||
|
|
||||||
If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# If we're looking up the name of a view callable, as found by reverse,
|
|
||||||
# grok the class instance that we stored when as_view was called.
|
|
||||||
if getattr(view, 'cls_instance', None):
|
|
||||||
view = view.cls_instance
|
|
||||||
|
|
||||||
# If this view has a resource that's been overridden, then use that resource for the name
|
|
||||||
if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
|
|
||||||
name = view.resource.__name__
|
|
||||||
|
|
||||||
# Chomp of any non-descriptive trailing part of the resource class name
|
|
||||||
if name.endswith('Resource') and name != 'Resource':
|
|
||||||
name = name[:-len('Resource')]
|
|
||||||
|
|
||||||
# If the view has a descriptive suffix, eg '*** List', '*** Instance'
|
|
||||||
if getattr(view, '_suffix', None):
|
|
||||||
name += view._suffix
|
|
||||||
|
|
||||||
# Otherwise if it's a function view use the function's name
|
|
||||||
elif getattr(view, '__name__', None) is not None:
|
|
||||||
name = view.__name__
|
|
||||||
|
|
||||||
# If it's a view class with no resource then grok the name from the class name
|
|
||||||
elif getattr(view, '__class__', None) is not None:
|
|
||||||
name = view.__class__.__name__
|
|
||||||
|
|
||||||
# Chomp of any non-descriptive trailing part of the view class name
|
|
||||||
if name.endswith('View') and name != 'View':
|
|
||||||
name = name[:-len('View')]
|
|
||||||
|
|
||||||
# I ain't got nuthin fo' ya
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def get_description(view):
|
|
||||||
"""
|
|
||||||
Provide a description for the view.
|
|
||||||
|
|
||||||
By default this is the view's docstring with nice unindention applied.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# If we're looking up the name of a view callable, as found by reverse,
|
|
||||||
# grok the class instance that we stored when as_view was called.
|
|
||||||
if getattr(view, 'cls_instance', None):
|
|
||||||
view = view.cls_instance
|
|
||||||
|
|
||||||
# If this view has a resource that's been overridden, then use the resource's doctring
|
|
||||||
if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
|
|
||||||
doc = view.resource.__doc__
|
|
||||||
|
|
||||||
# Otherwise use the view doctring
|
|
||||||
elif getattr(view, '__doc__', None):
|
|
||||||
doc = view.__doc__
|
|
||||||
|
|
||||||
# I ain't got nuthin fo' ya
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
if not doc:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in doc.splitlines()[1:] if line.lstrip()]
|
|
||||||
|
|
||||||
# unindent the docstring if needed
|
|
||||||
if whitespace_counts:
|
|
||||||
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
|
||||||
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', doc)
|
|
||||||
|
|
||||||
# otherwise return it as-is
|
|
||||||
return doc
|
|
|
@ -5,15 +5,17 @@ be subclassing in your implementation.
|
||||||
By setting or modifying class attributes on your view, you change it's predefined behaviour.
|
By setting or modifying class attributes on your view, you change it's predefined behaviour.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
from django.core.urlresolvers import set_script_prefix, get_script_prefix
|
from django.core.urlresolvers import set_script_prefix, get_script_prefix
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.utils.html import escape
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from djangorestframework.compat import View as DjangoView
|
from djangorestframework.compat import View as DjangoView, apply_markdown
|
||||||
from djangorestframework.response import Response, ErrorResponse
|
from djangorestframework.response import Response, ErrorResponse
|
||||||
from djangorestframework.mixins import *
|
from djangorestframework.mixins import *
|
||||||
from djangorestframework import resources, renderers, parsers, authentication, permissions, status
|
from djangorestframework import resources, renderers, parsers, authentication, permissions, status
|
||||||
from djangorestframework.utils.description import get_name, get_description
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -25,6 +27,48 @@ __all__ = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_trailing_string(content, trailing):
|
||||||
|
"""
|
||||||
|
Strip trailing component `trailing` from `content` if it exists.
|
||||||
|
Used when generating names from view/resource classes.
|
||||||
|
"""
|
||||||
|
if content.endswith(trailing) and content != trailing:
|
||||||
|
return content[:-len(trailing)]
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_leading_indent(content):
|
||||||
|
"""
|
||||||
|
Remove leading indent from a block of text.
|
||||||
|
Used when generating descriptions from docstrings.
|
||||||
|
"""
|
||||||
|
whitespace_counts = [len(line) - len(line.lstrip(' '))
|
||||||
|
for line in content.splitlines()[1:] if line.lstrip()]
|
||||||
|
|
||||||
|
# unindent the content if needed
|
||||||
|
if whitespace_counts:
|
||||||
|
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
||||||
|
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def _camelcase_to_spaces(content):
|
||||||
|
"""
|
||||||
|
Translate 'CamelCaseNames' to 'Camel Case Names'.
|
||||||
|
Used when generating names from view/resource classes.
|
||||||
|
"""
|
||||||
|
camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
|
||||||
|
return re.sub(camelcase_boundry, ' \\1', content).strip()
|
||||||
|
|
||||||
|
|
||||||
|
_resource_classes = (
|
||||||
|
None,
|
||||||
|
resources.Resource,
|
||||||
|
resources.FormResource,
|
||||||
|
resources.ModelResource
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
"""
|
"""
|
||||||
Handles incoming requests and maps them to REST operations.
|
Handles incoming requests and maps them to REST operations.
|
||||||
|
@ -48,7 +92,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
authentication = (authentication.UserLoggedInAuthentication,
|
authentication = (authentication.UserLoggedInAuthentication,
|
||||||
authentication.BasicAuthentication)
|
authentication.BasicAuthentication)
|
||||||
"""
|
"""
|
||||||
List of all authenticating methods to attempt.
|
List of all authenticating methods to attempt.
|
||||||
"""
|
"""
|
||||||
|
@ -76,6 +120,54 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
"""
|
"""
|
||||||
return [method.upper() for method in self.http_method_names if hasattr(self, method)]
|
return [method.upper() for method in self.http_method_names if hasattr(self, method)]
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
"""
|
||||||
|
Return the resource or view class name for use as this view's name.
|
||||||
|
Override to customize.
|
||||||
|
"""
|
||||||
|
# If this view has a resource that's been overridden, then use that resource for the name
|
||||||
|
if getattr(self, 'resource', None) not in _resource_classes:
|
||||||
|
name = self.resource.__name__
|
||||||
|
name = _remove_trailing_string(name, 'Resource')
|
||||||
|
name += getattr(self, '_suffix', '')
|
||||||
|
|
||||||
|
# If it's a view class with no resource then grok the name from the class name
|
||||||
|
else:
|
||||||
|
name = self.__class__.__name__
|
||||||
|
name = _remove_trailing_string(name, 'View')
|
||||||
|
|
||||||
|
return _camelcase_to_spaces(name)
|
||||||
|
|
||||||
|
def get_description(self, html=False):
|
||||||
|
"""
|
||||||
|
Return the resource or view docstring for use as this view's description.
|
||||||
|
Override to customize.
|
||||||
|
"""
|
||||||
|
|
||||||
|
description = None
|
||||||
|
|
||||||
|
# If this view has a resource that's been overridden,
|
||||||
|
# then try to use the resource's docstring
|
||||||
|
if getattr(self, 'resource', None) not in _resource_classes:
|
||||||
|
description = self.resource.__doc__
|
||||||
|
|
||||||
|
# Otherwise use the view docstring
|
||||||
|
if not description:
|
||||||
|
description = self.__doc__ or ''
|
||||||
|
|
||||||
|
description = _remove_leading_indent(description)
|
||||||
|
|
||||||
|
if html:
|
||||||
|
return self.markup_description(description)
|
||||||
|
return description
|
||||||
|
|
||||||
|
def markup_description(self, description):
|
||||||
|
if apply_markdown:
|
||||||
|
description = apply_markdown(description)
|
||||||
|
else:
|
||||||
|
description = escape(description).replace('\n', '<br />')
|
||||||
|
return mark_safe(description)
|
||||||
|
|
||||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return an HTTP 405 error if an operation is called which does not have a handler method.
|
Return an HTTP 405 error if an operation is called which does not have a handler method.
|
||||||
|
@ -164,8 +256,8 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
|
|
||||||
def options(self, request, *args, **kwargs):
|
def options(self, request, *args, **kwargs):
|
||||||
response_obj = {
|
response_obj = {
|
||||||
'name': get_name(self),
|
'name': self.get_name(),
|
||||||
'description': get_description(self),
|
'description': self.get_description(),
|
||||||
'renders': self._rendered_media_types,
|
'renders': self._rendered_media_types,
|
||||||
'parses': request._parsed_media_types,
|
'parses': request._parsed_media_types,
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,8 @@ The following example exposes your `MyModel` model through an api. It will provi
|
||||||
|
|
||||||
contents
|
contents
|
||||||
|
|
||||||
|
.. include:: ../CHANGELOG.rst
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,23 @@ from djangorestframework.views import View
|
||||||
from djangorestframework.permissions import PerUserThrottling, IsAuthenticated
|
from djangorestframework.permissions import PerUserThrottling, IsAuthenticated
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
|
||||||
class PermissionsExampleView(View):
|
class PermissionsExampleView(View):
|
||||||
"""
|
"""
|
||||||
A container view for permissions examples.
|
A container view for permissions examples.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return [{'name': 'Throttling Example', 'url': reverse('throttled-resource')},
|
return [
|
||||||
{'name': 'Logged in example', 'url': reverse('loggedin-resource')},]
|
{
|
||||||
|
'name': 'Throttling Example',
|
||||||
|
'url': reverse('throttled-resource')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Logged in example',
|
||||||
|
'url': reverse('loggedin-resource')
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ThrottlingExampleView(View):
|
class ThrottlingExampleView(View):
|
||||||
|
@ -20,7 +29,7 @@ class ThrottlingExampleView(View):
|
||||||
throttle will be applied until 60 seconds have passed since the first request.
|
throttle will be applied until 60 seconds have passed since the first request.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
permissions = ( PerUserThrottling, )
|
permissions = (PerUserThrottling,)
|
||||||
throttle = '10/min'
|
throttle = '10/min'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
@ -29,13 +38,15 @@ class ThrottlingExampleView(View):
|
||||||
"""
|
"""
|
||||||
return "Successful response to GET request because throttle is not yet active."
|
return "Successful response to GET request because throttle is not yet active."
|
||||||
|
|
||||||
|
|
||||||
class LoggedInExampleView(View):
|
class LoggedInExampleView(View):
|
||||||
"""
|
"""
|
||||||
You can login with **'test', 'test'.** or use curl:
|
You can login with **'test', 'test'.** or use curl:
|
||||||
|
|
||||||
`curl -X GET -H 'Accept: application/json' -u test:test http://localhost:8000/permissions-example`
|
`curl -X GET -H 'Accept: application/json' -u test:test http://localhost:8000/permissions-example`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
permissions = (IsAuthenticated, )
|
permissions = (IsAuthenticated, )
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return 'Logged in or not?'
|
return 'You have permission to view this resource'
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
Pygments==1.4
|
Pygments==1.4
|
||||||
Markdown==2.0.3
|
Markdown==2.0.3
|
||||||
djangorestframework
|
git+git://github.com/tomchristie/django-rest-framework.git
|
||||||
|
|
|
@ -53,16 +53,10 @@ MEDIA_ROOT = os.path.join(os.getenv('EPIO_DATA_DIRECTORY', '.'), 'media')
|
||||||
# trailing slash if there is a path component (optional in other cases).
|
# trailing slash if there is a path component (optional in other cases).
|
||||||
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
||||||
# NOTE: None of the djangorestframework examples serve media content via MEDIA_URL.
|
# NOTE: None of the djangorestframework examples serve media content via MEDIA_URL.
|
||||||
MEDIA_URL = ''
|
MEDIA_URL = '/uploads/'
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
|
||||||
# trailing slash.
|
|
||||||
# Examples: "http://foo.com/media/", "/media/".
|
|
||||||
# NOTE: djangorestframework does not require the admin app to be installed,
|
|
||||||
# but it does require the admin media be served. Django's test server will do
|
|
||||||
# this for you automatically, but in production you'll want to make sure you
|
|
||||||
# serve the admin media from somewhere.
|
|
||||||
ADMIN_MEDIA_PREFIX = '/static/admin'
|
|
||||||
|
|
||||||
# Make this unique, and don't share it with anybody.
|
# Make this unique, and don't share it with anybody.
|
||||||
SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu'
|
SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu'
|
||||||
|
@ -102,6 +96,7 @@ INSTALLED_APPS = (
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.sites',
|
'django.contrib.sites',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
|
|
||||||
'djangorestframework',
|
'djangorestframework',
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.conf.urls.defaults import patterns, include, url
|
from django.conf.urls.defaults import patterns, include, url
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from sandbox.views import Sandbox
|
from sandbox.views import Sandbox
|
||||||
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
(r'^$', Sandbox.as_view()),
|
(r'^$', Sandbox.as_view()),
|
||||||
|
@ -16,3 +17,4 @@ urlpatterns = patterns('',
|
||||||
(r'^', include('djangorestframework.urls')),
|
(r'^', include('djangorestframework.urls')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
urlpatterns += staticfiles_urlpatterns()
|
||||||
|
|
6
tox.ini
6
tox.ini
|
@ -32,6 +32,7 @@ commands=
|
||||||
basepython=python2.5
|
basepython=python2.5
|
||||||
deps=
|
deps=
|
||||||
django==1.2.4
|
django==1.2.4
|
||||||
|
django-staticfiles>=1.1.2
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
URLObject>=0.6.0
|
URLObject>=0.6.0
|
||||||
unittest-xml-reporting==1.2
|
unittest-xml-reporting==1.2
|
||||||
|
@ -43,6 +44,7 @@ deps=
|
||||||
basepython=python2.6
|
basepython=python2.6
|
||||||
deps=
|
deps=
|
||||||
django==1.2.4
|
django==1.2.4
|
||||||
|
django-staticfiles>=1.1.2
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
URLObject>=0.6.0
|
URLObject>=0.6.0
|
||||||
unittest-xml-reporting==1.2
|
unittest-xml-reporting==1.2
|
||||||
|
@ -54,6 +56,7 @@ deps=
|
||||||
basepython=python2.7
|
basepython=python2.7
|
||||||
deps=
|
deps=
|
||||||
django==1.2.4
|
django==1.2.4
|
||||||
|
django-staticfiles>=1.1.2
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
URLObject>=0.6.0
|
URLObject>=0.6.0
|
||||||
unittest-xml-reporting==1.2
|
unittest-xml-reporting==1.2
|
||||||
|
@ -135,6 +138,7 @@ commands=
|
||||||
python examples/runtests.py
|
python examples/runtests.py
|
||||||
deps=
|
deps=
|
||||||
django==1.2.4
|
django==1.2.4
|
||||||
|
django-staticfiles>=1.1.2
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
URLObject>=0.6.0
|
URLObject>=0.6.0
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
|
@ -150,6 +154,7 @@ commands=
|
||||||
python examples/runtests.py
|
python examples/runtests.py
|
||||||
deps=
|
deps=
|
||||||
django==1.2.4
|
django==1.2.4
|
||||||
|
django-staticfiles>=1.1.2
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
URLObject>=0.6.0
|
URLObject>=0.6.0
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
|
@ -165,6 +170,7 @@ commands=
|
||||||
python examples/runtests.py
|
python examples/runtests.py
|
||||||
deps=
|
deps=
|
||||||
django==1.2.4
|
django==1.2.4
|
||||||
|
django-staticfiles>=1.1.2
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
URLObject>=0.6.0
|
URLObject>=0.6.0
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
|
|
Loading…
Reference in New Issue
Block a user