aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AUTHORS3
-rw-r--r--CHANGELOG.rst7
-rw-r--r--djangorestframework/__init__.py2
-rw-r--r--djangorestframework/compat.py8
-rw-r--r--djangorestframework/mixins.py7
-rw-r--r--djangorestframework/renderers.py6
-rw-r--r--djangorestframework/request.py6
-rw-r--r--djangorestframework/resources.py7
-rw-r--r--djangorestframework/response.py19
-rw-r--r--djangorestframework/reverse.py23
-rw-r--r--djangorestframework/serializer.py2
-rw-r--r--djangorestframework/static/djangorestframework/css/style.css (renamed from djangorestframework/static/css/djangorestframework.css)72
-rw-r--r--djangorestframework/static/favicon.icobin1286 -> 0 bytes
-rw-r--r--djangorestframework/static/robots.txt2
-rw-r--r--djangorestframework/templates/djangorestframework/api.html3
-rw-r--r--djangorestframework/templates/djangorestframework/api.txt (renamed from djangorestframework/templates/renderer.txt)0
-rw-r--r--djangorestframework/templates/djangorestframework/base.html (renamed from djangorestframework/templates/renderer.html)21
-rw-r--r--djangorestframework/templates/djangorestframework/login.html (renamed from djangorestframework/templates/api_login.html)2
-rw-r--r--djangorestframework/templatetags/add_query_param.py3
-rw-r--r--djangorestframework/tests/reverse.py25
-rw-r--r--djangorestframework/tests/views.py14
-rw-r--r--djangorestframework/urls.py10
-rw-r--r--djangorestframework/utils/__init__.py21
-rw-r--r--djangorestframework/utils/staticviews.py13
-rw-r--r--djangorestframework/views.py40
-rw-r--r--docs/howto/reverse.rst39
-rw-r--r--docs/howto/setup.rst66
-rw-r--r--docs/index.rst14
-rw-r--r--docs/library/reverse.rst5
-rw-r--r--docs/library/utils.rst5
-rw-r--r--examples/blogpost/resources.py6
-rw-r--r--examples/blogpost/tests.py3
-rw-r--r--examples/mixin/urls.py4
-rw-r--r--examples/objectstore/views.py8
-rw-r--r--examples/permissionsexample/views.py6
-rw-r--r--examples/pygments_api/views.py15
-rw-r--r--examples/resourceexample/views.py14
-rw-r--r--examples/sandbox/views.py65
38 files changed, 349 insertions, 217 deletions
diff --git a/AUTHORS b/AUTHORS
index 7fae4578..67d1ea39 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -30,6 +30,9 @@ Chris Pickett <bunchesofdonald>
Ben Timby <btimby>
Michele Lazzeri <michelelazzeri-nextage>
Camille Harang <mammique>
+Paul Oswald <poswald>
+Sean C. Farley <scfarley>
+Daniel Izquierdo <izquierdo>
THANKS TO:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 010bf6c0..ddc3ac17 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,16 +1,19 @@
Release Notes
=============
-development
------------
+0.3.3
+-----
* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.
* Use `staticfiles` for css files.
- Easier to override. Won't conflict with customised admin styles (eg grappelli)
+* Templates are now nicely namespaced.
+ - Allows easier overriding.
* Drop implied 'pk' filter if last arg in urlconf is unnamed.
- Too magical. Explict is better than implicit.
* Saner template variable autoescaping.
* Tider setup.py
+* Updated for URLObject 2.0
* Bugfixes:
- Bug with PerUserThrottling when user contains unicode chars.
diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py
index 0aaa2915..efe7f566 100644
--- a/djangorestframework/__init__.py
+++ b/djangorestframework/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '0.3.3-dev'
+__version__ = '0.3.3'
VERSION = __version__ # synonym
diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py
index 7690316c..b818b446 100644
--- a/djangorestframework/compat.py
+++ b/djangorestframework/compat.py
@@ -457,3 +457,11 @@ except ImportError: # python < 2.7
return decorator
unittest.skip = skip
+
+# reverse_lazy (Django 1.4 onwards)
+try:
+ from django.core.urlresolvers import reverse_lazy
+except:
+ from django.core.urlresolvers import reverse
+ from django.utils.functional import lazy
+ reverse_lazy = lazy(reverse, str)
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index cf746839..51c859cd 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -13,7 +13,6 @@ from djangorestframework.renderers import BaseRenderer
from djangorestframework.resources import Resource, FormResource, ModelResource
from djangorestframework.response import Response, ImmediateResponse
from djangorestframework.request import Request
-from djangorestframework.utils import as_tuple, allowed_methods
__all__ = (
@@ -498,12 +497,12 @@ class PaginatorMixin(object):
"""
Constructs a url used for getting the next/previous urls
"""
- url = URLObject.parse(self.request.get_full_path())
- url = url.set_query_param('page', page_number)
+ url = URLObject(self.request.get_full_path())
+ url = url.set_query_param('page', str(page_number))
limit = self.get_limit()
if limit != self.limit:
- url = url.add_query_param('limit', limit)
+ url = url.set_query_param('limit', str(limit))
return url
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py
index 2cc9cc88..d24bcfce 100644
--- a/djangorestframework/renderers.py
+++ b/djangorestframework/renderers.py
@@ -379,7 +379,7 @@ class DocumentingHTMLRenderer(DocumentingTemplateRenderer):
media_type = 'text/html'
format = 'html'
- template = 'renderer.html'
+ template = 'djangorestframework/api.html'
class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
@@ -391,7 +391,7 @@ class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
media_type = 'application/xhtml+xml'
format = 'xhtml'
- template = 'renderer.html'
+ template = 'djangorestframework/api.html'
class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
@@ -403,7 +403,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
media_type = 'text/plain'
format = 'txt'
- template = 'renderer.txt'
+ template = 'djangorestframework/api.txt'
DEFAULT_RENDERERS = (
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
index d4ea1e01..e8f2b8c3 100644
--- a/djangorestframework/request.py
+++ b/djangorestframework/request.py
@@ -9,11 +9,9 @@ The wrapped request then offers a richer API, in particular :
- form overloading of HTTP method, content type and content
"""
-from django.http import HttpRequest
-
from djangorestframework.response import ImmediateResponse
from djangorestframework import status
-from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
+from djangorestframework.utils.mediatypes import is_form_media_type
from djangorestframework.utils import as_tuple
from StringIO import StringIO
@@ -105,7 +103,7 @@ class Request(object):
"""
self._content_type = self.META.get('HTTP_CONTENT_TYPE', self.META.get('CONTENT_TYPE', ''))
self._perform_form_overloading()
- # if the HTTP method was not overloaded, we take the raw HTTP method
+ # if the HTTP method was not overloaded, we take the raw HTTP method
if not hasattr(self, '_method'):
self._method = self.request.method
diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py
index 15b3579d..8ee49f82 100644
--- a/djangorestframework/resources.py
+++ b/djangorestframework/resources.py
@@ -1,10 +1,11 @@
from django import forms
-from django.core.urlresolvers import reverse, get_urlconf, get_resolver, NoReverseMatch
+from django.core.urlresolvers import get_urlconf, get_resolver, NoReverseMatch
from django.db import models
from djangorestframework.response import ImmediateResponse
+from djangorestframework.reverse import reverse
from djangorestframework.serializer import Serializer, _SkipField
-from djangorestframework.utils import as_tuple
+from djangorestframework.utils import as_tuple, reverse
class BaseResource(Serializer):
@@ -354,7 +355,7 @@ class ModelResource(FormResource):
instance_attrs[param] = attr
try:
- return reverse(self.view_callable[0], kwargs=instance_attrs)
+ return reverse(self.view_callable[0], self.view.request, kwargs=instance_attrs)
except NoReverseMatch:
pass
raise _SkipField
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
index a352531f..50087079 100644
--- a/djangorestframework/response.py
+++ b/djangorestframework/response.py
@@ -6,13 +6,13 @@ from any view. It is a bit smarter than Django's `HttpResponse`, for it renders
its content to a serial format by using a list of :mod:`renderers`.
To determine the content type to which it must render, default behaviour is to use standard
-HTTP Accept header content negotiation. But `Response` also supports overriding the content type
+HTTP Accept header content negotiation. But `Response` also supports overriding the content type
by specifying an ``_accept=`` parameter in the URL. Also, `Response` will ignore `Accept` headers
from Internet Explorer user agents and use a sensible browser `Accept` header instead.
`ImmediateResponse` is an exception that inherits from `Response`. It can be used
-to abort the request handling (i.e. ``View.get``, ``View.put``, ...),
+to abort the request handling (i.e. ``View.get``, ``View.put``, ...),
and immediately returning a response.
"""
@@ -31,8 +31,8 @@ class Response(SimpleTemplateResponse):
"""
An HttpResponse that may include content that hasn't yet been serialized.
- Kwargs:
- - content(object). The raw content, not yet serialized. This must be simple Python \
+ Kwargs:
+ - content(object). The raw content, not yet serialized. This must be simple Python
data that renderers can handle (e.g.: `dict`, `str`, ...)
- renderers(list/tuple). The renderers to use for rendering the response content.
"""
@@ -40,16 +40,17 @@ class Response(SimpleTemplateResponse):
_ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
_IGNORE_IE_ACCEPT_HEADER = True
- def __init__(self, content=None, status=None, request=None, renderers=None):
+ def __init__(self, content=None, status=None, request=None, renderers=None, headers=None):
# First argument taken by `SimpleTemplateResponse.__init__` is template_name,
# which we don't need
super(Response, self).__init__(None, status=status)
# We need to store our content in raw content to avoid overriding HttpResponse's
# `content` property
- self.raw_content = content
+ self.raw_content = content
self.has_content_body = content is not None
self.request = request
+ self.headers = headers and headers[:] or []
if renderers is not None:
self.renderers = renderers
@@ -64,7 +65,7 @@ class Response(SimpleTemplateResponse):
@property
def rendered_content(self):
"""
- The final rendered content. Accessing this attribute triggers the complete rendering cycle :
+ The final rendered content. Accessing this attribute triggers the complete rendering cycle :
selecting suitable renderer, setting response's actual content type, rendering data.
"""
renderer, media_type = self._determine_renderer()
@@ -88,9 +89,9 @@ class Response(SimpleTemplateResponse):
def _determine_accept_list(self):
"""
Returns a list of accepted media types. This list is determined from :
-
+
1. overload with `_ACCEPT_QUERY_PARAM`
- 2. `Accept` header of the request
+ 2. `Accept` header of the request
If those are useless, a default value is returned instead.
"""
diff --git a/djangorestframework/reverse.py b/djangorestframework/reverse.py
new file mode 100644
index 00000000..ad06f966
--- /dev/null
+++ b/djangorestframework/reverse.py
@@ -0,0 +1,23 @@
+"""
+Provide reverse functions that return fully qualified URLs
+"""
+from django.core.urlresolvers import reverse as django_reverse
+from djangorestframework.compat import reverse_lazy as django_reverse_lazy
+
+
+def reverse(viewname, request, *args, **kwargs):
+ """
+ Do the same as `django.core.urlresolvers.reverse` but using
+ *request* to build a fully qualified URL.
+ """
+ url = django_reverse(viewname, *args, **kwargs)
+ return request.build_absolute_uri(url)
+
+
+def reverse_lazy(viewname, request, *args, **kwargs):
+ """
+ Do the same as `django.core.urlresolvers.reverse_lazy` but using
+ *request* to build a fully qualified URL.
+ """
+ url = django_reverse_lazy(viewname, *args, **kwargs)
+ return request.build_absolute_uri(url)
diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py
index 71c0d93a..b0c02675 100644
--- a/djangorestframework/serializer.py
+++ b/djangorestframework/serializer.py
@@ -146,7 +146,7 @@ class Serializer(object):
# then the second element of the tuple is the fields to
# set on the related serializer
if isinstance(info, (list, tuple)):
- class OnTheFlySerializer(Serializer):
+ class OnTheFlySerializer(self.__class__):
fields = info
return OnTheFlySerializer
diff --git a/djangorestframework/static/css/djangorestframework.css b/djangorestframework/static/djangorestframework/css/style.css
index 1e75b8e8..d47aa16a 100644
--- a/djangorestframework/static/css/djangorestframework.css
+++ b/djangorestframework/static/djangorestframework/css/style.css
@@ -257,7 +257,7 @@ tfoot td {
color: #666;
padding: 2px 5px;
font-size: 11px;
- background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
+ background: #e1e1e1 url(../../admin/img/admin/nav-bg.gif) top left repeat-x;
border-left: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
@@ -317,11 +317,11 @@ table thead th.sorted a {
}
table thead th.ascending a {
- background: url(../img/admin/arrow-up.gif) right .4em no-repeat;
+ background: url(../../admin/img/admin/arrow-up.gif) right .4em no-repeat;
}
table thead th.descending a {
- background: url(../img/admin/arrow-down.gif) right .4em no-repeat;
+ background: url(../../admin/img/admin/arrow-down.gif) right .4em no-repeat;
}
/* ORDERABLE TABLES */
@@ -332,7 +332,7 @@ table.orderable tbody tr td:hover {
table.orderable tbody tr td:first-child {
padding-left: 14px;
- background-image: url(../img/admin/nav-bg-grabber.gif);
+ background-image: url(../../admin/img/admin/nav-bg-grabber.gif);
background-repeat: repeat-y;
}
@@ -362,7 +362,7 @@ input[type=text], input[type=password], textarea, select, .vTextField {
/* FORM BUTTONS */
.button, input[type=submit], input[type=button], .submit-row input {
- background: white url(../img/admin/nav-bg.gif) bottom repeat-x;
+ background: white url(../../admin/img/admin/nav-bg.gif) bottom repeat-x;
padding: 3px 5px;
color: black;
border: 1px solid #bbb;
@@ -370,31 +370,31 @@ input[type=text], input[type=password], textarea, select, .vTextField {
}
.button:active, input[type=submit]:active, input[type=button]:active {
- background-image: url(../img/admin/nav-bg-reverse.gif);
+ background-image: url(../../admin/img/admin/nav-bg-reverse.gif);
background-position: top;
}
.button[disabled], input[type=submit][disabled], input[type=button][disabled] {
- background-image: url(../img/admin/nav-bg.gif);
+ background-image: url(../../admin/img/admin/nav-bg.gif);
background-position: bottom;
opacity: 0.4;
}
.button.default, input[type=submit].default, .submit-row input.default {
border: 2px solid #5b80b2;
- background: #7CA0C7 url(../img/admin/default-bg.gif) bottom repeat-x;
+ background: #7CA0C7 url(../../admin/img/admin/default-bg.gif) bottom repeat-x;
font-weight: bold;
color: white;
float: right;
}
.button.default:active, input[type=submit].default:active {
- background-image: url(../img/admin/default-bg-reverse.gif);
+ background-image: url(../../admin/img/admin/default-bg-reverse.gif);
background-position: top;
}
.button[disabled].default, input[type=submit][disabled].default, input[type=button][disabled].default {
- background-image: url(../img/admin/default-bg.gif);
+ background-image: url(../../admin/img/admin/default-bg.gif);
background-position: bottom;
opacity: 0.4;
}
@@ -431,7 +431,7 @@ input[type=text], input[type=password], textarea, select, .vTextField {
font-size: 11px;
text-align: left;
font-weight: bold;
- background: #7CA0C7 url(../img/admin/default-bg.gif) top left repeat-x;
+ background: #7CA0C7 url(../../admin/img/admin/default-bg.gif) top left repeat-x;
color: white;
}
@@ -453,15 +453,15 @@ ul.messagelist li {
margin: 0 0 3px 0;
border-bottom: 1px solid #ddd;
color: #666;
- background: #ffc url(../img/admin/icon_success.gif) 5px .3em no-repeat;
+ background: #ffc url(../../admin/img/admin/icon_success.gif) 5px .3em no-repeat;
}
ul.messagelist li.warning{
- background-image: url(../img/admin/icon_alert.gif);
+ background-image: url(../../admin/img/admin/icon_alert.gif);
}
ul.messagelist li.error{
- background-image: url(../img/admin/icon_error.gif);
+ background-image: url(../../admin/img/admin/icon_error.gif);
}
.errornote {
@@ -471,7 +471,7 @@ ul.messagelist li.error{
margin: 0 0 3px 0;
border: 1px solid red;
color: red;
- background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat;
+ background: #ffc url(../../admin/img/admin/icon_error.gif) 5px .3em no-repeat;
}
ul.errorlist {
@@ -486,7 +486,7 @@ ul.errorlist {
margin: 0 0 3px 0;
border: 1px solid red;
color: white;
- background: red url(../img/admin/icon_alert.gif) 5px .3em no-repeat;
+ background: red url(../../admin/img/admin/icon_alert.gif) 5px .3em no-repeat;
}
.errorlist li a {
@@ -522,7 +522,7 @@ div.system-message p.system-message-title {
padding: 4px 5px 4px 25px;
margin: 0;
color: red;
- background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat;
+ background: #ffc url(../../admin/img/admin/icon_error.gif) 5px .3em no-repeat;
}
.description {
@@ -533,7 +533,7 @@ div.system-message p.system-message-title {
/* BREADCRUMBS */
div.breadcrumbs {
- background: white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x;
+ background: white url(../../admin/img/admin/nav-bg-reverse.gif) 0 -10px repeat-x;
padding: 2px 8px 3px 8px;
font-size: 11px;
color: #999;
@@ -546,17 +546,17 @@ div.breadcrumbs {
.addlink {
padding-left: 12px;
- background: url(../img/admin/icon_addlink.gif) 0 .2em no-repeat;
+ background: url(../../admin/img/admin/icon_addlink.gif) 0 .2em no-repeat;
}
.changelink {
padding-left: 12px;
- background: url(../img/admin/icon_changelink.gif) 0 .2em no-repeat;
+ background: url(../../admin/img/admin/icon_changelink.gif) 0 .2em no-repeat;
}
.deletelink {
padding-left: 12px;
- background: url(../img/admin/icon_deletelink.gif) 0 .25em no-repeat;
+ background: url(../../admin/img/admin/icon_deletelink.gif) 0 .25em no-repeat;
}
a.deletelink:link, a.deletelink:visited {
@@ -591,14 +591,14 @@ a.deletelink:hover {
.object-tools li {
display: block;
float: left;
- background: url(../img/admin/tool-left.gif) 0 0 no-repeat;
+ background: url(../../admin/img/admin/tool-left.gif) 0 0 no-repeat;
padding: 0 0 0 8px;
margin-left: 2px;
height: 16px;
}
.object-tools li:hover {
- background: url(../img/admin/tool-left_over.gif) 0 0 no-repeat;
+ background: url(../../admin/img/admin/tool-left_over.gif) 0 0 no-repeat;
}
.object-tools a:link, .object-tools a:visited {
@@ -607,29 +607,29 @@ a.deletelink:hover {
color: white;
padding: .1em 14px .1em 8px;
height: 14px;
- background: #999 url(../img/admin/tool-right.gif) 100% 0 no-repeat;
+ background: #999 url(../../admin/img/admin/tool-right.gif) 100% 0 no-repeat;
}
.object-tools a:hover, .object-tools li:hover a {
- background: #5b80b2 url(../img/admin/tool-right_over.gif) 100% 0 no-repeat;
+ background: #5b80b2 url(../../admin/img/admin/tool-right_over.gif) 100% 0 no-repeat;
}
.object-tools a.viewsitelink, .object-tools a.golink {
- background: #999 url(../img/admin/tooltag-arrowright.gif) top right no-repeat;
+ background: #999 url(../../admin/img/admin/tooltag-arrowright.gif) top right no-repeat;
padding-right: 28px;
}
.object-tools a.viewsitelink:hover, .object-tools a.golink:hover {
- background: #5b80b2 url(../img/admin/tooltag-arrowright_over.gif) top right no-repeat;
+ background: #5b80b2 url(../../admin/img/admin/tooltag-arrowright_over.gif) top right no-repeat;
}
.object-tools a.addlink {
- background: #999 url(../img/admin/tooltag-add.gif) top right no-repeat;
+ background: #999 url(../../admin/img/admin/tooltag-add.gif) top right no-repeat;
padding-right: 28px;
}
.object-tools a.addlink:hover {
- background: #5b80b2 url(../img/admin/tooltag-add_over.gif) top right no-repeat;
+ background: #5b80b2 url(../../admin/img/admin/tooltag-add_over.gif) top right no-repeat;
}
/* OBJECT HISTORY */
@@ -764,7 +764,7 @@ table#change-history tbody th {
}
#content-related .module h2 {
- background: #eee url(../img/admin/nav-bg.gif) bottom left repeat-x;
+ background: #eee url(../../admin/img/admin/nav-bg.gif) bottom left repeat-x;
color: #666;
}
@@ -910,7 +910,7 @@ fieldset.collapsed h2, fieldset.collapsed {
}
fieldset.collapsed h2 {
- background-image: url(../img/admin/nav-bg.gif);
+ background-image: url(../../admin/img/admin/nav-bg.gif);
background-position: bottom left;
color: #999;
}
@@ -931,7 +931,7 @@ fieldset.monospace textarea {
.submit-row {
padding: 5px 7px;
text-align: right;
- background: white url(../img/admin/nav-bg.gif) 0 100% repeat-x;
+ background: white url(../../admin/img/admin/nav-bg.gif) 0 100% repeat-x;
border: 1px solid #ccc;
margin: 5px 0;
overflow: hidden;
@@ -950,7 +950,7 @@ fieldset.monospace textarea {
}
.submit-row .deletelink {
- background: url(../img/admin/icon_deletelink.gif) 0 50% no-repeat;
+ background: url(../../admin/img/admin/icon_deletelink.gif) 0 50% no-repeat;
padding-left: 14px;
}
@@ -1017,7 +1017,7 @@ fieldset.monospace textarea {
color: #666;
padding: 3px 5px;
font-size: 11px;
- background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
+ background: #e1e1e1 url(../../admin/img/admin/nav-bg.gif) top left repeat-x;
border-bottom: 1px solid #ddd;
}
@@ -1102,7 +1102,7 @@ fieldset.monospace textarea {
color: #666;
padding: 3px 5px;
border-bottom: 1px solid #ddd;
- background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
+ background: #e1e1e1 url(../../admin/img/admin/nav-bg.gif) top left repeat-x;
}
.inline-group .tabular tr.add-row td {
@@ -1113,7 +1113,7 @@ fieldset.monospace textarea {
.inline-group ul.tools a.add,
.inline-group div.add-row a,
.inline-group .tabular tr.add-row td a {
- background: url(../img/admin/icon_addlink.gif) 0 50% no-repeat;
+ background: url(../../admin/img/admin/icon_addlink.gif) 0 50% no-repeat;
padding-left: 14px;
font-size: 11px;
outline: 0; /* Remove dotted border around link */
diff --git a/djangorestframework/static/favicon.ico b/djangorestframework/static/favicon.ico
deleted file mode 100644
index 17080106..00000000
--- a/djangorestframework/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/djangorestframework/static/robots.txt b/djangorestframework/static/robots.txt
deleted file mode 100644
index 1f53798b..00000000
--- a/djangorestframework/static/robots.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-User-agent: *
-Disallow: /
diff --git a/djangorestframework/templates/djangorestframework/api.html b/djangorestframework/templates/djangorestframework/api.html
new file mode 100644
index 00000000..fd9bcc98
--- /dev/null
+++ b/djangorestframework/templates/djangorestframework/api.html
@@ -0,0 +1,3 @@
+{% extends "djangorestframework/base.html" %}
+
+{# Override this template in your own templates directory to customize #} \ No newline at end of file
diff --git a/djangorestframework/templates/renderer.txt b/djangorestframework/templates/djangorestframework/api.txt
index b584952c..b584952c 100644
--- a/djangorestframework/templates/renderer.txt
+++ b/djangorestframework/templates/djangorestframework/api.txt
diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/djangorestframework/base.html
index 808d9664..fa913c33 100644
--- a/djangorestframework/templates/renderer.html
+++ b/djangorestframework/templates/djangorestframework/base.html
@@ -6,27 +6,35 @@
{% load static %}
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
- <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}css/djangorestframework.css'/>
- <title>Django REST framework - {{ name }}</title>
+ <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}djangorestframework/css/style.css'/>
+ {% block extrastyle %}{% endblock %}
+ <title>{% block title %}Django REST framework - {{ name }}{% endblock %}</title>
+ {% block extrahead %}{% endblock %}
+ {% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE" />{% endblock %}
</head>
- <body>
+ <body class="{% block bodyclass %}{% endblock %}">
<div id="container">
<div id="header">
<div id="branding">
- <h1 id="site-name"><a href='http://django-rest-framework.org'>Django REST framework</a> <span class="version"> v {{ version }}</span></h1>
+ <h1 id="site-name">{% block branding %}<a href='http://django-rest-framework.org'>Django REST framework</a> <span class="version"> v {{ version }}</span>{% endblock %}</h1>
</div>
<div id="user-tools">
{% if user.is_active %}Welcome, {{ user }}.{% if logout_url %} <a href='{{ logout_url }}'>Log out</a>{% endif %}{% else %}Anonymous {% if login_url %}<a href='{{ login_url }}'>Log in</a>{% endif %}{% endif %}
+ {% block userlinks %}{% endblock %}
</div>
+ {% block nav-global %}{% endblock %}
</div>
<div class="breadcrumbs">
+ {% block breadcrumbs %}
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
<a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a> {% if not forloop.last %}&rsaquo;{% endif %}
{% endfor %}
+ {% endblock %}
</div>
+ <!-- Content -->
<div id="content" class="{% block coltype %}colM{% endblock %}">
{% if 'OPTIONS' in allowed_methods %}
@@ -123,7 +131,12 @@
{% endif %}
</div>
+ <!-- END content-main -->
+
</div>
+ <!-- END Content -->
+
+ {% block footer %}<div id="footer"></div>{% endblock %}
</div>
</body>
</html>
diff --git a/djangorestframework/templates/api_login.html b/djangorestframework/templates/djangorestframework/login.html
index 016a4e10..07929f0c 100644
--- a/djangorestframework/templates/api_login.html
+++ b/djangorestframework/templates/djangorestframework/login.html
@@ -2,7 +2,7 @@
<html>
<head>
- <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}css/djangorestframework.css'/>
+ <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}djangorestframework/css/style.css'/>
</head>
<body class="login">
diff --git a/djangorestframework/templatetags/add_query_param.py b/djangorestframework/templatetags/add_query_param.py
index 11709730..4cf0133b 100644
--- a/djangorestframework/templatetags/add_query_param.py
+++ b/djangorestframework/templatetags/add_query_param.py
@@ -4,8 +4,7 @@ register = Library()
def add_query_param(url, param):
- (key, sep, val) = param.partition('=')
- return unicode(URLObject.parse(url) & (key, val))
+ return unicode(URLObject(url).with_query(param))
register.filter('add_query_param', add_query_param)
diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py
index c49caca0..56e1ed5b 100644
--- a/djangorestframework/tests/reverse.py
+++ b/djangorestframework/tests/reverse.py
@@ -1,29 +1,34 @@
from django.conf.urls.defaults import patterns, url
-from django.core.urlresolvers import reverse
from django.test import TestCase
from django.utils import simplejson as json
+from djangorestframework.renderers import JSONRenderer
+from djangorestframework.reverse import reverse
from djangorestframework.views import View
from djangorestframework.response import Response
-class MockView(View):
- """Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
- permissions = ()
+class MyView(View):
+ """
+ Mock resource which simply returns a URL, so that we can ensure
+ that reversed URLs are fully qualified.
+ """
+ renderers = (JSONRenderer, )
def get(self, request):
- return Response(reverse('another'))
+ return Response(reverse('myview', request))
urlpatterns = patterns('',
- url(r'^$', MockView.as_view()),
- url(r'^another$', MockView.as_view(), name='another'),
+ url(r'^myview$', MyView.as_view(), name='myview'),
)
class ReverseTests(TestCase):
- """Tests for """
+ """
+ Tests for fully qualifed URLs when using `reverse`.
+ """
urls = 'djangorestframework.tests.reverse'
def test_reversed_urls_are_fully_qualified(self):
- response = self.client.get('/')
- self.assertEqual(json.loads(response.content), 'http://testserver/another')
+ response = self.client.get('/myview')
+ self.assertEqual(json.loads(response.content), 'http://testserver/myview')
diff --git a/djangorestframework/tests/views.py b/djangorestframework/tests/views.py
index ab5d75d6..d4189087 100644
--- a/djangorestframework/tests/views.py
+++ b/djangorestframework/tests/views.py
@@ -46,8 +46,6 @@ class MockResource(ModelResource):
fields = ('foo', 'bar', 'baz')
urlpatterns = patterns('djangorestframework.utils.staticviews',
- url(r'^robots.txt$', 'deny_robots'),
- url(r'^favicon.ico$', 'favicon'),
url(r'^accounts/login$', 'api_login'),
url(r'^accounts/logout$', 'api_logout'),
url(r'^mock/$', MockView.as_view()),
@@ -123,18 +121,6 @@ class ExtraViewsTests(TestCase):
"""Test the extra views djangorestframework provides"""
urls = 'djangorestframework.tests.views'
- def test_robots_view(self):
- """Ensure the robots view exists"""
- response = self.client.get('/robots.txt')
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response['Content-Type'], 'text/plain')
-
- def test_favicon_view(self):
- """Ensure the favicon view exists"""
- response = self.client.get('/favicon.ico')
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response['Content-Type'], 'image/vnd.microsoft.icon')
-
def test_login_view(self):
"""Ensure the login view exists"""
response = self.client.get('/accounts/login')
diff --git a/djangorestframework/urls.py b/djangorestframework/urls.py
index 827aac9c..5c797bcd 100644
--- a/djangorestframework/urls.py
+++ b/djangorestframework/urls.py
@@ -1,16 +1,6 @@
from django.conf.urls.defaults import patterns
-from django.conf import settings
urlpatterns = patterns('djangorestframework.utils.staticviews',
- (r'robots.txt', 'deny_robots'),
(r'^accounts/login/$', 'api_login'),
(r'^accounts/logout/$', 'api_logout'),
)
-
-# Only serve favicon in production because otherwise chrome users will pretty much
-# permanantly have the django-rest-framework favicon whenever they navigate to
-# 127.0.0.1:8000 or whatever, which gets annoying
-if not settings.DEBUG:
- urlpatterns += patterns('djangorestframework.utils.staticviews',
- (r'favicon.ico', 'favicon'),
- )
diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py
index fbe55474..afef4f19 100644
--- a/djangorestframework/utils/__init__.py
+++ b/djangorestframework/utils/__init__.py
@@ -1,6 +1,7 @@
+import django
from django.utils.encoding import smart_unicode
from django.utils.xmlutils import SimplerXMLGenerator
-from django.core.urlresolvers import resolve
+from django.core.urlresolvers import resolve, reverse as django_reverse
from django.conf import settings
from djangorestframework.compat import StringIO
@@ -180,3 +181,21 @@ class XMLRenderer():
def dict2xml(input):
return XMLRenderer().dict2xml(input)
+
+
+def reverse(viewname, request, *args, **kwargs):
+ """
+ Do the same as :py:func:`django.core.urlresolvers.reverse` but using
+ *request* to build a fully qualified URL.
+ """
+ return request.build_absolute_uri(django_reverse(viewname, *args, **kwargs))
+
+if django.VERSION >= (1, 4):
+ from django.core.urlresolvers import reverse_lazy as django_reverse_lazy
+
+ def reverse_lazy(viewname, request, *args, **kwargs):
+ """
+ Do the same as :py:func:`django.core.urlresolvers.reverse_lazy` but using
+ *request* to build a fully qualified URL.
+ """
+ return request.build_absolute_uri(django_reverse_lazy(viewname, *args, **kwargs))
diff --git a/djangorestframework/utils/staticviews.py b/djangorestframework/utils/staticviews.py
index 12a36f6c..7cbc0b9b 100644
--- a/djangorestframework/utils/staticviews.py
+++ b/djangorestframework/utils/staticviews.py
@@ -6,22 +6,13 @@ from django.template import RequestContext
import base64
-def deny_robots(request):
- return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain')
-
-
-def favicon(request):
- data = 'AAABAAEAEREAAAEAIADwBAAAFgAAACgAAAARAAAAIgAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADLy8tLy8vL3svLy1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy8vLBsvLywkAAAAATkZFS1xUVPqhn57/y8vL0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmVlQ/GxcXiy8vL88vLy4FdVlXzTkZF/2RdXP/Ly8vty8vLtMvLy5DLy8vty8vLxgAAAAAAAAAAAAAAAAAAAABORkUJTkZF4lNMS/+Lh4f/cWtq/05GRf9ORkX/Vk9O/3JtbP+Ef3//Vk9O/2ljYv/Ly8v5y8vLCQAAAAAAAAAAAAAAAE5GRQlORkX2TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/UElI/8PDw5cAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRZZORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vLvQAAAAAAAAAAAAAAAAAAAADLy8tIaWNi805GRf9ORkX/YVpZ/396eV7Ly8t7qaen9lZOTu5ORkX/TkZF/25oZ//Ly8v/y8vLycvLy0gAAAAATkZFSGNcXPpORkX/TkZF/05GRf+ysLDzTkZFe1NLSv6Oior/raur805GRf9ORkX/TkZF/2hiYf+npaX/y8vL5wAAAABORkXnTkZF/05GRf9ORkX/VU1M/8vLy/9PR0b1TkZF/1VNTP/Ly8uQT0dG+E5GRf9ORkX/TkZF/1hRUP3Ly8tmAAAAAE5GRWBORkXkTkZF/05GRf9ORkX/t7a2/355eOpORkX/TkZFkISAf1BORkX/TkZF/05GRf9XT075TkZFZgAAAAAAAAAAAAAAAAAAAABORkXDTkZF/05GRf9lX17/ubi4/8vLy/+2tbT/Yltb/05GRf9ORkX/a2Vk/8vLy5MAAAAAAAAAAAAAAAAAAAAAAAAAAFNLSqNORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vL+cvLyw8AAAAAAAAAAAAAAABORkUSTkZF+U5GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/1BJSP/CwsLmy8vLDwAAAAAAAAAAAAAAAE5GRRJORkXtTkZF9FFJSJ1ORkXJTkZF/05GRf9ORkX/ZF5d9k5GRZ9ORkXtTkZF5HFsaxUAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRQxORkUJAAAAAAAAAABORkXhTkZF/2JbWv7Ly8tgAAAAAAAAAABORkUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRWBORkX2TkZFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAP9/gAD+P4AA4AOAAMADgADAA4AAwAOAAMMBgACCAIAAAAGAAIBDgADAA4AAwAOAAMADgADAB4AA/H+AAP7/gAA='
- return HttpResponse(base64.b64decode(data), mimetype='image/vnd.microsoft.icon')
-
-
# BLERGH
# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS
# to add ADMIN_MEDIA_PREFIX to the RequestContext. I don't like this but really really want users to not have to
# be making settings changes in order to accomodate django-rest-framework
@csrf_protect
@never_cache
-def api_login(request, template_name='api_login.html',
+def api_login(request, template_name='djangorestframework/login.html',
redirect_field_name=REDIRECT_FIELD_NAME,
authentication_form=AuthenticationForm):
"""Displays the login form and handles the login action."""
@@ -66,5 +57,5 @@ def api_login(request, template_name='api_login.html',
}, context_instance=RequestContext(request))
-def api_logout(request, next_page=None, template_name='api_login.html', redirect_field_name=REDIRECT_FIELD_NAME):
+def api_logout(request, next_page=None, template_name='djangorestframework/login.html', redirect_field_name=REDIRECT_FIELD_NAME):
return logout(request, next_page, template_name, redirect_field_name)
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 93e2d3a3..6bfc4192 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -7,13 +7,12 @@ By setting or modifying class attributes on your view, you change it's predefine
import re
from django.core.urlresolvers import set_script_prefix, get_script_prefix
-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 djangorestframework.compat import View as DjangoView, apply_markdown
-from djangorestframework.response import Response, ImmediateResponse
+from djangorestframework.response import ImmediateResponse
from djangorestframework.mixins import *
from djangorestframework.utils import allowed_methods
from djangorestframework import resources, renderers, parsers, authentication, permissions, status
@@ -163,6 +162,9 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
return description
def markup_description(self, description):
+ """
+ Apply HTML markup to the description of this view.
+ """
if apply_markdown:
description = apply_markdown(description)
else:
@@ -171,11 +173,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
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.
"""
- raise ImmediateResponse(
- {'detail': 'Method \'%s\' not allowed on this resource.' % request.method},
- status=status.HTTP_405_METHOD_NOT_ALLOWED)
+ content = {
+ 'detail': "Method '%s' not allowed on this resource." % request.method
+ }
+ raise ImmediateResponse(content, status.HTTP_405_METHOD_NOT_ALLOWED)
def initial(self, request, *args, **kargs):
"""
@@ -184,22 +188,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
Required if you want to do things like set `request.upload_handlers` before
the authentication and dispatch handling is run.
"""
- # Calls to 'reverse' will not be fully qualified unless we set the
- # scheme/host/port here.
- self.orig_prefix = get_script_prefix()
- if not (self.orig_prefix.startswith('http:') or self.orig_prefix.startswith('https:')):
- prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
- set_script_prefix(prefix + self.orig_prefix)
- return request
+ pass
def final(self, request, response, *args, **kargs):
"""
Returns an `HttpResponse`. This method is a hook for any code that needs to run
after everything else in the view.
"""
- # Restore script_prefix.
- set_script_prefix(self.orig_prefix)
-
# Always add these headers.
response['Allow'] = ', '.join(allowed_methods(self))
# sample to allow caching using Vary http header
@@ -211,17 +206,12 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
# all other authentication is CSRF exempt.
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
- self.request = request
+ self.request = self.create_request(request)
self.args = args
self.kwargs = kwargs
try:
- # Get a custom request, built form the original request instance
- self.request = request = self.create_request(request)
-
- # `initial` is the opportunity to temper with the request,
- # even completely replace it.
- self.request = request = self.initial(request, *args, **kwargs)
+ self.initial(request, *args, **kwargs)
# Authenticate and check request has the relevant permissions
self._check_permissions()
@@ -231,7 +221,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
-
+
# TODO: should we enforce HttpResponse, like Django does ?
response = handler(request, *args, **kwargs)
@@ -239,7 +229,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
self.response = response = self.prepare_response(response)
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
- # TODO: ugly hack to handle both HttpResponse and Response.
+ # TODO: ugly hack to handle both HttpResponse and Response.
if hasattr(response, 'raw_content'):
response.raw_content = self.filter_response(response.raw_content)
else:
diff --git a/docs/howto/reverse.rst b/docs/howto/reverse.rst
new file mode 100644
index 00000000..73b8fa4d
--- /dev/null
+++ b/docs/howto/reverse.rst
@@ -0,0 +1,39 @@
+Returning URIs from your Web APIs
+=================================
+
+As a rule, it's probably better practice to return absolute URIs from you web APIs, e.g. "http://example.com/foobar", rather than returning relative URIs, e.g. "/foobar".
+
+The advantages of doing so are:
+
+* It's more explicit.
+* It leaves less work for your API clients.
+* There's no ambiguity about the meaning of the string when it's found in representations such as JSON that do not have a native URI type.
+* It allows us to easily do things like markup HTML representations with hyperlinks.
+
+Django REST framework provides two utility functions to make it simpler to return absolute URIs from your Web API.
+
+There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink its output for you, which makes browsing the API much easier.
+
+reverse(viewname, request, ...)
+-------------------------------
+
+The :py:func:`~reverse.reverse` function has the same behavior as `django.core.urlresolvers.reverse`_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port::
+
+ from djangorestframework.reverse import reverse
+ from djangorestframework.views import View
+
+ class MyView(View):
+ def get(self, request):
+ context = {
+ 'url': reverse('year-summary', request, args=[1945])
+ }
+
+ return Response(context)
+
+reverse_lazy(viewname, request, ...)
+------------------------------------
+
+The :py:func:`~reverse.reverse_lazy` function has the same behavior as `django.core.urlresolvers.reverse_lazy`_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port.
+
+.. _django.core.urlresolvers.reverse: https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse
+.. _django.core.urlresolvers.reverse_lazy: https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse-lazy
diff --git a/docs/howto/setup.rst b/docs/howto/setup.rst
index f14e0499..0af1449c 100644
--- a/docs/howto/setup.rst
+++ b/docs/howto/setup.rst
@@ -3,52 +3,70 @@
Setup
=====
-Installing into site-packages
------------------------------
+Templates
+---------
-If you need to manually install Django REST framework to your ``site-packages`` directory, run the ``setup.py`` script::
+Django REST framework uses a few templates for the HTML and plain text
+documenting renderers. You'll need to ensure ``TEMPLATE_LOADERS`` setting
+contains ``'django.template.loaders.app_directories.Loader'``.
+This will already be the case by default.
- python setup.py install
+You may customize the templates by creating a new template called
+``djangorestframework/api.html`` in your project, which should extend
+``djangorestframework/base.html`` and override the appropriate
+block tags. For example::
-Template Loaders
-----------------
+ {% extends "djangorestframework/base.html" %}
-Django REST framework uses a few templates for the HTML and plain text documenting renderers.
+ {% block title %}My API{% endblock %}
-* Ensure ``TEMPLATE_LOADERS`` setting contains ``'django.template.loaders.app_directories.Loader'``.
+ {% block branding %}
+ <h1 id="site-name">My API</h1>
+ {% endblock %}
-This will be the case by default so you shouldn't normally need to do anything here.
-Admin Styling
--------------
+Styling
+-------
-Django REST framework uses the admin media for styling. When running using Django's testserver this is automatically served for you,
-but once you move onto a production server, you'll want to make sure you serve the admin media separately, exactly as you would do
-`if using the Django admin <https://docs.djangoproject.com/en/dev/howto/deployment/modpython/#serving-the-admin-files>`_.
+Django REST framework requires `django.contrib.staticfiles`_ to serve it's css.
+If you're using Django 1.2 you'll need to use the seperate
+`django-staticfiles`_ package instead.
+
+You can override the styling by creating a file in your top-level static
+directory named ``djangorestframework/css/style.css``
-* Ensure that the ``ADMIN_MEDIA_PREFIX`` is set appropriately and that you are serving the admin media.
- (Django's testserver will automatically serve the admin media for you)
Markdown
--------
-The Python `markdown library <http://www.freewisdom.org/projects/python-markdown/>`_ is not required but comes recommended.
+`Python markdown`_ is not required but comes recommended.
+
+If markdown is installed your :class:`.Resource` descriptions can include
+`markdown formatting`_ which will be rendered by the self-documenting API.
+
+YAML
+----
+
+YAML support is optional, and requires `PyYAML`_.
-If markdown is installed your :class:`.Resource` descriptions can include `markdown style formatting
-<http://daringfireball.net/projects/markdown/syntax>`_ which will be rendered by the HTML documenting renderer.
-robots.txt, favicon, login/logout
----------------------------------
+Login / Logout
+--------------
-Django REST framework comes with a few views that can be useful including a deny robots view, a favicon view, and api login and logout views::
+Django REST framework includes login and logout views that are useful if
+you're using the self-documenting API::
from django.conf.urls.defaults import patterns
urlpatterns = patterns('djangorestframework.views',
- (r'robots.txt', 'deny_robots'),
- (r'favicon.ico', 'favicon'),
# Add your resources here
(r'^accounts/login/$', 'api_login'),
(r'^accounts/logout/$', 'api_logout'),
)
+.. _django.contrib.staticfiles: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/
+.. _django-staticfiles: http://pypi.python.org/pypi/django-staticfiles/
+.. _URLObject: http://pypi.python.org/pypi/URLObject/
+.. _Python markdown: http://www.freewisdom.org/projects/python-markdown/
+.. _markdown formatting: http://daringfireball.net/projects/markdown/syntax
+.. _PyYAML: http://pypi.python.org/pypi/PyYAML \ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
index ecc1f118..b969c4a3 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -40,8 +40,11 @@ Requirements
------------
* Python (2.5, 2.6, 2.7 supported)
-* Django (1.2, 1.3, 1.4-alpha supported)
-
+* Django (1.2, 1.3, 1.4 supported)
+* `django.contrib.staticfiles`_ (or `django-staticfiles`_ for Django 1.2)
+* `URLObject`_ >= 2.0.0
+* `Markdown`_ >= 2.1.0 (Optional)
+* `PyYAML`_ >= 3.10 (Optional)
Installation
------------
@@ -54,8 +57,6 @@ Or get the latest development version using git::
git clone git@github.com:tomchristie/django-rest-framework.git
-Or you can `download the current release <http://pypi.python.org/pypi/djangorestframework>`_.
-
Setup
-----
@@ -114,3 +115,8 @@ Indices and tables
* :ref:`modindex`
* :ref:`search`
+.. _django.contrib.staticfiles: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/
+.. _django-staticfiles: http://pypi.python.org/pypi/django-staticfiles/
+.. _URLObject: http://pypi.python.org/pypi/URLObject/
+.. _Markdown: http://pypi.python.org/pypi/Markdown/
+.. _PyYAML: http://pypi.python.org/pypi/PyYAML
diff --git a/docs/library/reverse.rst b/docs/library/reverse.rst
new file mode 100644
index 00000000..a2c29c48
--- /dev/null
+++ b/docs/library/reverse.rst
@@ -0,0 +1,5 @@
+:mod:`reverse`
+================
+
+.. automodule:: reverse
+ :members:
diff --git a/docs/library/utils.rst b/docs/library/utils.rst
new file mode 100644
index 00000000..653f24fd
--- /dev/null
+++ b/docs/library/utils.rst
@@ -0,0 +1,5 @@
+:mod:`utils`
+==============
+
+.. automodule:: utils
+ :members:
diff --git a/examples/blogpost/resources.py b/examples/blogpost/resources.py
index 5a3c1ce2..d11c5615 100644
--- a/examples/blogpost/resources.py
+++ b/examples/blogpost/resources.py
@@ -1,5 +1,5 @@
-from django.core.urlresolvers import reverse
from djangorestframework.resources import ModelResource
+from djangorestframework.reverse import reverse
from blogpost.models import BlogPost, Comment
@@ -12,7 +12,7 @@ class BlogPostResource(ModelResource):
ordering = ('-created',)
def comments(self, instance):
- return reverse('comments', kwargs={'blogpost': instance.key})
+ return reverse('comments', request, kwargs={'blogpost': instance.key})
class CommentResource(ModelResource):
@@ -24,4 +24,4 @@ class CommentResource(ModelResource):
ordering = ('-created',)
def blogpost(self, instance):
- return reverse('blog-post', kwargs={'key': instance.blogpost.key})
+ return reverse('blog-post', request, kwargs={'key': instance.blogpost.key})
diff --git a/examples/blogpost/tests.py b/examples/blogpost/tests.py
index 5aa4f89f..23f1ac21 100644
--- a/examples/blogpost/tests.py
+++ b/examples/blogpost/tests.py
@@ -1,12 +1,11 @@
"""Test a range of REST API usage of the example application.
"""
-from django.core.urlresolvers import reverse
from django.test import TestCase
-from django.core.urlresolvers import reverse
from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory
+from djangorestframework.reverse import reverse
from djangorestframework.views import InstanceModelView, ListOrCreateModelView
from blogpost import models, urls
diff --git a/examples/mixin/urls.py b/examples/mixin/urls.py
index 58cf370c..102f2c12 100644
--- a/examples/mixin/urls.py
+++ b/examples/mixin/urls.py
@@ -2,9 +2,9 @@ from djangorestframework.compat import View # Use Django 1.3's django.views.gen
from djangorestframework.mixins import ResponseMixin
from djangorestframework.renderers import DEFAULT_RENDERERS
from djangorestframework.response import Response
+from djangorestframework.reverse import reverse
from django.conf.urls.defaults import patterns, url
-from django.core.urlresolvers import reverse
class ExampleView(ResponseMixin, View):
@@ -14,7 +14,7 @@ class ExampleView(ResponseMixin, View):
def get(self, request):
response = Response({'description': 'Some example content',
- 'url': reverse('mixin-view')}, status=200)
+ 'url': reverse('mixin-view', request)}, status=200)
self.response = self.prepare_response(response)
return self.response
diff --git a/examples/objectstore/views.py b/examples/objectstore/views.py
index ae545394..b48bfac2 100644
--- a/examples/objectstore/views.py
+++ b/examples/objectstore/views.py
@@ -1,6 +1,6 @@
from django.conf import settings
-from django.core.urlresolvers import reverse
+from djangorestframework.reverse import reverse
from djangorestframework.views import View
from djangorestframework.response import Response
from djangorestframework import status
@@ -41,7 +41,7 @@ class ObjectStoreRoot(View):
filepaths = [os.path.join(OBJECT_STORE_DIR, file) for file in os.listdir(OBJECT_STORE_DIR) if not file.startswith('.')]
ctime_sorted_basenames = [item[0] for item in sorted([(os.path.basename(path), os.path.getctime(path)) for path in filepaths],
key=operator.itemgetter(1), reverse=True)]
- return Response([reverse('stored-object', kwargs={'key':key}) for key in ctime_sorted_basenames])
+ return Response([reverse('stored-object', request, kwargs={'key':key}) for key in ctime_sorted_basenames])
def post(self, request):
"""
@@ -51,8 +51,8 @@ class ObjectStoreRoot(View):
pathname = os.path.join(OBJECT_STORE_DIR, key)
pickle.dump(self.CONTENT, open(pathname, 'wb'))
remove_oldest_files(OBJECT_STORE_DIR, MAX_FILES)
- self.headers['Location'] = reverse('stored-object', kwargs={'key':key})
- return Response(self.CONTENT, status=status.HTTP_201_CREATED)
+ url = reverse('stored-object', request, kwargs={'key':key})
+ return Response(self.CONTENT, status.HTTP_201_CREATED, {'Location': url})
class StoredObject(View):
diff --git a/examples/permissionsexample/views.py b/examples/permissionsexample/views.py
index bcf6619c..13384c9f 100644
--- a/examples/permissionsexample/views.py
+++ b/examples/permissionsexample/views.py
@@ -1,7 +1,7 @@
from djangorestframework.views import View
from djangorestframework.response import Response
from djangorestframework.permissions import PerUserThrottling, IsAuthenticated
-from django.core.urlresolvers import reverse
+from djangorestframework.reverse import reverse
class PermissionsExampleView(View):
@@ -13,11 +13,11 @@ class PermissionsExampleView(View):
return Response([
{
'name': 'Throttling Example',
- 'url': reverse('throttled-resource')
+ 'url': reverse('throttled-resource', request)
},
{
'name': 'Logged in example',
- 'url': reverse('loggedin-resource')
+ 'url': reverse('loggedin-resource', request)
},
])
diff --git a/examples/pygments_api/views.py b/examples/pygments_api/views.py
index 852b6730..75d36fea 100644
--- a/examples/pygments_api/views.py
+++ b/examples/pygments_api/views.py
@@ -1,10 +1,10 @@
from __future__ import with_statement # for python 2.5
from django.conf import settings
-from django.core.urlresolvers import reverse
from djangorestframework.resources import FormResource
from djangorestframework.response import Response
from djangorestframework.renderers import BaseRenderer
+from djangorestframework.reverse import reverse
from djangorestframework.views import View
from djangorestframework import status
@@ -61,7 +61,7 @@ class PygmentsRoot(View):
Return a list of all currently existing snippets.
"""
unique_ids = [os.path.split(f)[1] for f in list_dir_sorted_by_ctime(HIGHLIGHTED_CODE_DIR)]
- return Response([reverse('pygments-instance', args=[unique_id]) for unique_id in unique_ids])
+ return Response([reverse('pygments-instance', request, args=[unique_id]) for unique_id in unique_ids])
def post(self, request):
"""
@@ -81,8 +81,8 @@ class PygmentsRoot(View):
remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES)
- self.headers['Location'] = reverse('pygments-instance', args=[unique_id])
- return Response(status=status.HTTP_201_CREATED)
+ location = reverse('pygments-instance', request, args=[unique_id])
+ return Response(status=status.HTTP_201_CREATED, headers={'Location': location})
class PygmentsInstance(View):
@@ -98,7 +98,7 @@ class PygmentsInstance(View):
"""
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
if not os.path.exists(pathname):
- return Response(status.HTTP_404_NOT_FOUND)
+ return Response(status=status.HTTP_404_NOT_FOUND)
return Response(open(pathname, 'r').read())
def delete(self, request, unique_id):
@@ -107,6 +107,7 @@ class PygmentsInstance(View):
"""
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
if not os.path.exists(pathname):
- return Response(status.HTTP_404_NOT_FOUND)
- return Response(os.remove(pathname))
+ return Response(status=status.HTTP_404_NOT_FOUND)
+ os.remove(pathname)
+ return Response()
diff --git a/examples/resourceexample/views.py b/examples/resourceexample/views.py
index 44c4176a..8e7be302 100644
--- a/examples/resourceexample/views.py
+++ b/examples/resourceexample/views.py
@@ -1,5 +1,4 @@
-from django.core.urlresolvers import reverse
-
+from djangorestframework.reverse import reverse
from djangorestframework.views import View
from djangorestframework.response import Response
from djangorestframework import status
@@ -14,9 +13,12 @@ class ExampleView(View):
def get(self, request):
"""
- Handle GET requests, returning a list of URLs pointing to 3 other views.
+ Handle GET requests, returning a list of URLs pointing to
+ three other views.
"""
- return Response({"Some other resources": [reverse('another-example', kwargs={'num':num}) for num in range(3)]})
+ urls = [reverse('another-example', request, kwargs={'num': num})
+ for num in range(3)]
+ return Response({"Some other resources": urls})
class AnotherExampleView(View):
@@ -32,7 +34,7 @@ class AnotherExampleView(View):
Returns a simple string indicating which view the GET request was for.
"""
if int(num) > 2:
- return Response(status.HTTP_404_NOT_FOUND)
+ return Response(status=status.HTTP_404_NOT_FOUND)
return Response("GET request to AnotherExampleResource %s" % num)
def post(self, request, num):
@@ -41,5 +43,5 @@ class AnotherExampleView(View):
Returns a simple string indicating what content was supplied.
"""
if int(num) > 2:
- return Response(status.HTTP_404_NOT_FOUND)
+ return Response(status=status.HTTP_404_NOT_FOUND)
return Response("POST request to AnotherExampleResource %s, with content: %s" % (num, repr(self.CONTENT)))
diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py
index 49b59b40..a9b82447 100644
--- a/examples/sandbox/views.py
+++ b/examples/sandbox/views.py
@@ -1,40 +1,67 @@
"""The root view for the examples provided with Django REST framework"""
-from django.core.urlresolvers import reverse
+from djangorestframework.reverse import reverse
from djangorestframework.views import View
from djangorestframework.response import Response
class Sandbox(View):
- """This is the sandbox for the examples provided with [Django REST framework](http://django-rest-framework.org).
+ """
+ This is the sandbox for the examples provided with
+ [Django REST framework][1].
- These examples are provided to help you get a better idea of some of the features of RESTful APIs created using the framework.
+ These examples are provided to help you get a better idea of some of the
+ features of RESTful APIs created using the framework.
- All the example APIs allow anonymous access, and can be navigated either through the browser or from the command line...
+ All the example APIs allow anonymous access, and can be navigated either
+ through the browser or from the command line.
- bash: curl -X GET http://api.django-rest-framework.org/ # (Use default renderer)
- bash: curl -X GET http://api.django-rest-framework.org/ -H 'Accept: text/plain' # (Use plaintext documentation renderer)
+ For example, to get the default representation using curl:
+
+ bash: curl -X GET http://rest.ep.io/
+
+ Or, to get the plaintext documentation represention:
+
+ bash: curl -X GET http://rest.ep.io/ -H 'Accept: text/plain'
The examples provided:
- 1. A basic example using the [Resource](http://django-rest-framework.org/library/resource.html) class.
- 2. A basic example using the [ModelResource](http://django-rest-framework.org/library/modelresource.html) class.
- 3. An basic example using Django 1.3's [class based views](http://docs.djangoproject.com/en/dev/topics/class-based-views/) and djangorestframework's [RendererMixin](http://django-rest-framework.org/library/renderers.html).
+ 1. A basic example using the [Resource][2] class.
+ 2. A basic example using the [ModelResource][3] class.
+ 3. An basic example using Django 1.3's [class based views][4] and
+ djangorestframework's [RendererMixin][5].
4. A generic object store API.
5. A code highlighting API.
6. A blog posts and comments API.
7. A basic example using permissions.
8. A basic example using enhanced request.
- Please feel free to browse, create, edit and delete the resources in these examples."""
+ Please feel free to browse, create, edit and delete the resources in
+ these examples.
+
+ [1]: http://django-rest-framework.org
+ [2]: http://django-rest-framework.org/library/resource.html
+ [3]: http://django-rest-framework.org/library/modelresource.html
+ [4]: http://docs.djangoproject.com/en/dev/topics/class-based-views/
+ [5]: http://django-rest-framework.org/library/renderers.html
+ """
def get(self, request):
- return Response([{'name': 'Simple Resource example', 'url': reverse('example-resource')},
- {'name': 'Simple ModelResource example', 'url': reverse('model-resource-root')},
- {'name': 'Simple Mixin-only example', 'url': reverse('mixin-view')},
- {'name': 'Object store API', 'url': reverse('object-store-root')},
- {'name': 'Code highlighting API', 'url': reverse('pygments-root')},
- {'name': 'Blog posts API', 'url': reverse('blog-posts-root')},
- {'name': 'Permissions example', 'url': reverse('permissions-example')},
- {'name': 'Simple request mixin example', 'url': reverse('request-example')}
- ])
+ return Response([
+ {'name': 'Simple Resource example',
+ 'url': reverse('example-resource', request)},
+ {'name': 'Simple ModelResource example',
+ 'url': reverse('model-resource-root', request)},
+ {'name': 'Simple Mixin-only example',
+ 'url': reverse('mixin-view', request)},
+ {'name': 'Object store API'
+ 'url': reverse('object-store-root', request)},
+ {'name': 'Code highlighting API',
+ 'url': reverse('pygments-root', request)},
+ {'name': 'Blog posts API',
+ 'url': reverse('blog-posts-root', request)},
+ {'name': 'Permissions example',
+ 'url': reverse('permissions-example', request)},
+ {'name': 'Simple request mixin example',
+ 'url': reverse('request-example', request)}
+ ])