Commit 9da1ae81 by Sébastien Piquemal

merged + fixed broken test

parents 242327d3 5fd4c639
...@@ -30,6 +30,9 @@ Chris Pickett <bunchesofdonald> ...@@ -30,6 +30,9 @@ Chris Pickett <bunchesofdonald>
Ben Timby <btimby> Ben Timby <btimby>
Michele Lazzeri <michelelazzeri-nextage> Michele Lazzeri <michelelazzeri-nextage>
Camille Harang <mammique> Camille Harang <mammique>
Paul Oswald <poswald>
Sean C. Farley <scfarley>
Daniel Izquierdo <izquierdo>
THANKS TO: THANKS TO:
......
Release Notes Release Notes
============= =============
development 0.3.3
----------- -----
* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions. * Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.
* Use `staticfiles` for css files. * Use `staticfiles` for css files.
- Easier to override. Won't conflict with customised admin styles (eg grappelli) - 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. * Drop implied 'pk' filter if last arg in urlconf is unnamed.
- Too magical. Explict is better than implicit. - Too magical. Explict is better than implicit.
* Saner template variable autoescaping. * Saner template variable autoescaping.
* Tider setup.py * Tider setup.py
* Updated for URLObject 2.0
* Bugfixes: * Bugfixes:
- Bug with PerUserThrottling when user contains unicode chars. - Bug with PerUserThrottling when user contains unicode chars.
......
__version__ = '0.3.3-dev' __version__ = '0.3.3'
VERSION = __version__ # synonym VERSION = __version__ # synonym
...@@ -457,3 +457,11 @@ except ImportError: # python < 2.7 ...@@ -457,3 +457,11 @@ except ImportError: # python < 2.7
return decorator return decorator
unittest.skip = skip 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)
...@@ -13,7 +13,6 @@ from djangorestframework.renderers import BaseRenderer ...@@ -13,7 +13,6 @@ from djangorestframework.renderers import BaseRenderer
from djangorestframework.resources import Resource, FormResource, ModelResource from djangorestframework.resources import Resource, FormResource, ModelResource
from djangorestframework.response import Response, ImmediateResponse from djangorestframework.response import Response, ImmediateResponse
from djangorestframework.request import Request from djangorestframework.request import Request
from djangorestframework.utils import as_tuple, allowed_methods
__all__ = ( __all__ = (
...@@ -498,12 +497,12 @@ class PaginatorMixin(object): ...@@ -498,12 +497,12 @@ class PaginatorMixin(object):
""" """
Constructs a url used for getting the next/previous urls Constructs a url used for getting the next/previous urls
""" """
url = URLObject.parse(self.request.get_full_path()) url = URLObject(self.request.get_full_path())
url = url.set_query_param('page', page_number) url = url.set_query_param('page', str(page_number))
limit = self.get_limit() limit = self.get_limit()
if limit != self.limit: if limit != self.limit:
url = url.add_query_param('limit', limit) url = url.set_query_param('limit', str(limit))
return url return url
......
...@@ -379,7 +379,7 @@ class DocumentingHTMLRenderer(DocumentingTemplateRenderer): ...@@ -379,7 +379,7 @@ class DocumentingHTMLRenderer(DocumentingTemplateRenderer):
media_type = 'text/html' media_type = 'text/html'
format = 'html' format = 'html'
template = 'renderer.html' template = 'djangorestframework/api.html'
class DocumentingXHTMLRenderer(DocumentingTemplateRenderer): class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
...@@ -391,7 +391,7 @@ class DocumentingXHTMLRenderer(DocumentingTemplateRenderer): ...@@ -391,7 +391,7 @@ class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
media_type = 'application/xhtml+xml' media_type = 'application/xhtml+xml'
format = 'xhtml' format = 'xhtml'
template = 'renderer.html' template = 'djangorestframework/api.html'
class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
...@@ -403,7 +403,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): ...@@ -403,7 +403,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
media_type = 'text/plain' media_type = 'text/plain'
format = 'txt' format = 'txt'
template = 'renderer.txt' template = 'djangorestframework/api.txt'
DEFAULT_RENDERERS = ( DEFAULT_RENDERERS = (
......
...@@ -9,11 +9,9 @@ The wrapped request then offers a richer API, in particular : ...@@ -9,11 +9,9 @@ The wrapped request then offers a richer API, in particular :
- form overloading of HTTP method, content type and content - form overloading of HTTP method, content type and content
""" """
from django.http import HttpRequest
from djangorestframework.response import ImmediateResponse from djangorestframework.response import ImmediateResponse
from djangorestframework import status 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 djangorestframework.utils import as_tuple
from StringIO import StringIO from StringIO import StringIO
...@@ -105,7 +103,7 @@ class Request(object): ...@@ -105,7 +103,7 @@ class Request(object):
""" """
self._content_type = self.META.get('HTTP_CONTENT_TYPE', self.META.get('CONTENT_TYPE', '')) self._content_type = self.META.get('HTTP_CONTENT_TYPE', self.META.get('CONTENT_TYPE', ''))
self._perform_form_overloading() 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'): if not hasattr(self, '_method'):
self._method = self.request.method self._method = self.request.method
......
from django import forms 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 django.db import models
from djangorestframework.response import ImmediateResponse from djangorestframework.response import ImmediateResponse
from djangorestframework.reverse import reverse
from djangorestframework.serializer import Serializer, _SkipField from djangorestframework.serializer import Serializer, _SkipField
from djangorestframework.utils import as_tuple from djangorestframework.utils import as_tuple, reverse
class BaseResource(Serializer): class BaseResource(Serializer):
...@@ -354,7 +355,7 @@ class ModelResource(FormResource): ...@@ -354,7 +355,7 @@ class ModelResource(FormResource):
instance_attrs[param] = attr instance_attrs[param] = attr
try: try:
return reverse(self.view_callable[0], kwargs=instance_attrs) return reverse(self.view_callable[0], self.view.request, kwargs=instance_attrs)
except NoReverseMatch: except NoReverseMatch:
pass pass
raise _SkipField raise _SkipField
......
...@@ -6,13 +6,13 @@ from any view. It is a bit smarter than Django's `HttpResponse`, for it renders ...@@ -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`. 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 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 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. 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 `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. and immediately returning a response.
""" """
...@@ -31,8 +31,8 @@ class Response(SimpleTemplateResponse): ...@@ -31,8 +31,8 @@ class Response(SimpleTemplateResponse):
""" """
An HttpResponse that may include content that hasn't yet been serialized. An HttpResponse that may include content that hasn't yet been serialized.
Kwargs: Kwargs:
- content(object). The raw content, not yet serialized. This must be simple Python \ - content(object). The raw content, not yet serialized. This must be simple Python
data that renderers can handle (e.g.: `dict`, `str`, ...) data that renderers can handle (e.g.: `dict`, `str`, ...)
- renderers(list/tuple). The renderers to use for rendering the response content. - renderers(list/tuple). The renderers to use for rendering the response content.
""" """
...@@ -40,16 +40,17 @@ class Response(SimpleTemplateResponse): ...@@ -40,16 +40,17 @@ class Response(SimpleTemplateResponse):
_ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params _ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
_IGNORE_IE_ACCEPT_HEADER = True _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, # First argument taken by `SimpleTemplateResponse.__init__` is template_name,
# which we don't need # which we don't need
super(Response, self).__init__(None, status=status) super(Response, self).__init__(None, status=status)
# We need to store our content in raw content to avoid overriding HttpResponse's # We need to store our content in raw content to avoid overriding HttpResponse's
# `content` property # `content` property
self.raw_content = content self.raw_content = content
self.has_content_body = content is not None self.has_content_body = content is not None
self.request = request self.request = request
self.headers = headers and headers[:] or []
if renderers is not None: if renderers is not None:
self.renderers = renderers self.renderers = renderers
...@@ -64,7 +65,7 @@ class Response(SimpleTemplateResponse): ...@@ -64,7 +65,7 @@ class Response(SimpleTemplateResponse):
@property @property
def rendered_content(self): 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. selecting suitable renderer, setting response's actual content type, rendering data.
""" """
renderer, media_type = self._determine_renderer() renderer, media_type = self._determine_renderer()
...@@ -88,9 +89,9 @@ class Response(SimpleTemplateResponse): ...@@ -88,9 +89,9 @@ class Response(SimpleTemplateResponse):
def _determine_accept_list(self): def _determine_accept_list(self):
""" """
Returns a list of accepted media types. This list is determined from : Returns a list of accepted media types. This list is determined from :
1. overload with `_ACCEPT_QUERY_PARAM` 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. If those are useless, a default value is returned instead.
""" """
......
"""
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)
...@@ -146,7 +146,7 @@ class Serializer(object): ...@@ -146,7 +146,7 @@ class Serializer(object):
# then the second element of the tuple is the fields to # then the second element of the tuple is the fields to
# set on the related serializer # set on the related serializer
if isinstance(info, (list, tuple)): if isinstance(info, (list, tuple)):
class OnTheFlySerializer(Serializer): class OnTheFlySerializer(self.__class__):
fields = info fields = info
return OnTheFlySerializer return OnTheFlySerializer
......
...@@ -257,7 +257,7 @@ tfoot td { ...@@ -257,7 +257,7 @@ tfoot td {
color: #666; color: #666;
padding: 2px 5px; padding: 2px 5px;
font-size: 11px; 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-left: 1px solid #ddd;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
} }
...@@ -317,11 +317,11 @@ table thead th.sorted a { ...@@ -317,11 +317,11 @@ table thead th.sorted a {
} }
table thead th.ascending 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 { 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 */ /* ORDERABLE TABLES */
...@@ -332,7 +332,7 @@ table.orderable tbody tr td:hover { ...@@ -332,7 +332,7 @@ table.orderable tbody tr td:hover {
table.orderable tbody tr td:first-child { table.orderable tbody tr td:first-child {
padding-left: 14px; 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; background-repeat: repeat-y;
} }
...@@ -362,7 +362,7 @@ input[type=text], input[type=password], textarea, select, .vTextField { ...@@ -362,7 +362,7 @@ input[type=text], input[type=password], textarea, select, .vTextField {
/* FORM BUTTONS */ /* FORM BUTTONS */
.button, input[type=submit], input[type=button], .submit-row input { .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; padding: 3px 5px;
color: black; color: black;
border: 1px solid #bbb; border: 1px solid #bbb;
...@@ -370,31 +370,31 @@ input[type=text], input[type=password], textarea, select, .vTextField { ...@@ -370,31 +370,31 @@ input[type=text], input[type=password], textarea, select, .vTextField {
} }
.button:active, input[type=submit]:active, input[type=button]:active { .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; background-position: top;
} }
.button[disabled], input[type=submit][disabled], input[type=button][disabled] { .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; background-position: bottom;
opacity: 0.4; opacity: 0.4;
} }
.button.default, input[type=submit].default, .submit-row input.default { .button.default, input[type=submit].default, .submit-row input.default {
border: 2px solid #5b80b2; 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; font-weight: bold;
color: white; color: white;
float: right; float: right;
} }
.button.default:active, input[type=submit].default:active { .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; background-position: top;
} }
.button[disabled].default, input[type=submit][disabled].default, input[type=button][disabled].default { .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; background-position: bottom;
opacity: 0.4; opacity: 0.4;
} }
...@@ -431,7 +431,7 @@ input[type=text], input[type=password], textarea, select, .vTextField { ...@@ -431,7 +431,7 @@ input[type=text], input[type=password], textarea, select, .vTextField {
font-size: 11px; font-size: 11px;
text-align: left; text-align: left;
font-weight: bold; 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; color: white;
} }
...@@ -453,15 +453,15 @@ ul.messagelist li { ...@@ -453,15 +453,15 @@ ul.messagelist li {
margin: 0 0 3px 0; margin: 0 0 3px 0;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
color: #666; 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{ 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{ ul.messagelist li.error{
background-image: url(../img/admin/icon_error.gif); background-image: url(../../admin/img/admin/icon_error.gif);
} }
.errornote { .errornote {
...@@ -471,7 +471,7 @@ ul.messagelist li.error{ ...@@ -471,7 +471,7 @@ ul.messagelist li.error{
margin: 0 0 3px 0; margin: 0 0 3px 0;
border: 1px solid red; border: 1px solid red;
color: 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 { ul.errorlist {
...@@ -486,7 +486,7 @@ ul.errorlist { ...@@ -486,7 +486,7 @@ ul.errorlist {
margin: 0 0 3px 0; margin: 0 0 3px 0;
border: 1px solid red; border: 1px solid red;
color: white; 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 { .errorlist li a {
...@@ -522,7 +522,7 @@ div.system-message p.system-message-title { ...@@ -522,7 +522,7 @@ div.system-message p.system-message-title {
padding: 4px 5px 4px 25px; padding: 4px 5px 4px 25px;
margin: 0; margin: 0;
color: 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;
} }
.description { .description {
...@@ -533,7 +533,7 @@ div.system-message p.system-message-title { ...@@ -533,7 +533,7 @@ div.system-message p.system-message-title {
/* BREADCRUMBS */ /* BREADCRUMBS */
div.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; padding: 2px 8px 3px 8px;
font-size: 11px; font-size: 11px;
color: #999; color: #999;
...@@ -546,17 +546,17 @@ div.breadcrumbs { ...@@ -546,17 +546,17 @@ div.breadcrumbs {
.addlink { .addlink {
padding-left: 12px; 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 { .changelink {
padding-left: 12px; 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 { .deletelink {
padding-left: 12px; 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 { a.deletelink:link, a.deletelink:visited {
...@@ -591,14 +591,14 @@ a.deletelink:hover { ...@@ -591,14 +591,14 @@ a.deletelink:hover {
.object-tools li { .object-tools li {
display: block; display: block;
float: left; 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; padding: 0 0 0 8px;
margin-left: 2px; margin-left: 2px;
height: 16px; height: 16px;
} }
.object-tools li:hover { .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 { .object-tools a:link, .object-tools a:visited {
...@@ -607,29 +607,29 @@ a.deletelink:hover { ...@@ -607,29 +607,29 @@ a.deletelink:hover {
color: white; color: white;
padding: .1em 14px .1em 8px; padding: .1em 14px .1em 8px;
height: 14px; 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 { .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 { .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; padding-right: 28px;
} }
.object-tools a.viewsitelink:hover, .object-tools a.golink:hover { .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 { .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; padding-right: 28px;
} }
.object-tools a.addlink:hover { .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 */ /* OBJECT HISTORY */
...@@ -764,7 +764,7 @@ table#change-history tbody th { ...@@ -764,7 +764,7 @@ table#change-history tbody th {
} }
#content-related .module h2 { #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; color: #666;
} }
...@@ -910,7 +910,7 @@ fieldset.collapsed h2, fieldset.collapsed { ...@@ -910,7 +910,7 @@ fieldset.collapsed h2, fieldset.collapsed {
} }
fieldset.collapsed h2 { fieldset.collapsed h2 {
background-image: url(../img/admin/nav-bg.gif); background-image: url(../../admin/img/admin/nav-bg.gif);
background-position: bottom left; background-position: bottom left;
color: #999; color: #999;
} }
...@@ -931,7 +931,7 @@ fieldset.monospace textarea { ...@@ -931,7 +931,7 @@ fieldset.monospace textarea {
.submit-row { .submit-row {
padding: 5px 7px; padding: 5px 7px;
text-align: right; 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; border: 1px solid #ccc;
margin: 5px 0; margin: 5px 0;
overflow: hidden; overflow: hidden;
...@@ -950,7 +950,7 @@ fieldset.monospace textarea { ...@@ -950,7 +950,7 @@ fieldset.monospace textarea {
} }
.submit-row .deletelink { .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; padding-left: 14px;
} }
...@@ -1017,7 +1017,7 @@ fieldset.monospace textarea { ...@@ -1017,7 +1017,7 @@ fieldset.monospace textarea {
color: #666; color: #666;
padding: 3px 5px; padding: 3px 5px;
font-size: 11px; 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; border-bottom: 1px solid #ddd;
} }
...@@ -1102,7 +1102,7 @@ fieldset.monospace textarea { ...@@ -1102,7 +1102,7 @@ fieldset.monospace textarea {
color: #666; color: #666;
padding: 3px 5px; padding: 3px 5px;
border-bottom: 1px solid #ddd; 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 { .inline-group .tabular tr.add-row td {
...@@ -1113,7 +1113,7 @@ fieldset.monospace textarea { ...@@ -1113,7 +1113,7 @@ fieldset.monospace textarea {
.inline-group ul.tools a.add, .inline-group ul.tools a.add,
.inline-group div.add-row a, .inline-group div.add-row a,
.inline-group .tabular tr.add-row td 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; padding-left: 14px;
font-size: 11px; font-size: 11px;
outline: 0; /* Remove dotted border around link */ outline: 0; /* Remove dotted border around link */
......
{% extends "djangorestframework/base.html" %}
{# Override this template in your own templates directory to customize #}
\ No newline at end of file
...@@ -6,27 +6,35 @@ ...@@ -6,27 +6,35 @@
{% load static %} {% load static %}
<html xmlns="http://www.w3.org/1999/xhtml"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <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'/>
<title>Django REST framework - {{ name }}</title> {% 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> </head>
<body> <body class="{% block bodyclass %}{% endblock %}">
<div id="container"> <div id="container">
<div id="header"> <div id="header">
<div id="branding"> <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>
<div id="user-tools"> <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 %} {% 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> </div>
{% block nav-global %}{% endblock %}
</div> </div>
<div class="breadcrumbs"> <div class="breadcrumbs">
{% block 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 %}&rsaquo;{% endif %} <a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a> {% if not forloop.last %}&rsaquo;{% endif %}
{% endfor %} {% endfor %}
{% endblock %}
</div> </div>
<!-- Content -->
<div id="content" class="{% block coltype %}colM{% endblock %}"> <div id="content" class="{% block coltype %}colM{% endblock %}">
{% if 'OPTIONS' in allowed_methods %} {% if 'OPTIONS' in allowed_methods %}
...@@ -123,7 +131,12 @@ ...@@ -123,7 +131,12 @@
{% endif %} {% endif %}
</div> </div>
<!-- END content-main -->
</div> </div>
<!-- END Content -->
{% block footer %}<div id="footer"></div>{% endblock %}
</div> </div>
</body> </body>
</html> </html>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<html> <html>
<head> <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> </head>
<body class="login"> <body class="login">
......
...@@ -4,8 +4,7 @@ register = Library() ...@@ -4,8 +4,7 @@ register = Library()
def add_query_param(url, param): def add_query_param(url, param):
(key, sep, val) = param.partition('=') return unicode(URLObject(url).with_query(param))
return unicode(URLObject.parse(url) & (key, val))
register.filter('add_query_param', add_query_param) register.filter('add_query_param', add_query_param)
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url
from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.utils import simplejson as json from django.utils import simplejson as json
from djangorestframework.renderers import JSONRenderer
from djangorestframework.reverse import reverse
from djangorestframework.views import View from djangorestframework.views import View
from djangorestframework.response import Response from djangorestframework.response import Response
class MockView(View): class MyView(View):
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified""" """
permissions = () Mock resource which simply returns a URL, so that we can ensure
that reversed URLs are fully qualified.
"""
renderers = (JSONRenderer, )
def get(self, request): def get(self, request):
return Response(reverse('another')) return Response(reverse('myview', request))
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', MockView.as_view()), url(r'^myview$', MyView.as_view(), name='myview'),
url(r'^another$', MockView.as_view(), name='another'),
) )
class ReverseTests(TestCase): class ReverseTests(TestCase):
"""Tests for """ """
Tests for fully qualifed URLs when using `reverse`.
"""
urls = 'djangorestframework.tests.reverse' urls = 'djangorestframework.tests.reverse'
def test_reversed_urls_are_fully_qualified(self): def test_reversed_urls_are_fully_qualified(self):
response = self.client.get('/') response = self.client.get('/myview')
self.assertEqual(json.loads(response.content), 'http://testserver/another') self.assertEqual(json.loads(response.content), 'http://testserver/myview')
...@@ -46,8 +46,6 @@ class MockResource(ModelResource): ...@@ -46,8 +46,6 @@ class MockResource(ModelResource):
fields = ('foo', 'bar', 'baz') fields = ('foo', 'bar', 'baz')
urlpatterns = patterns('djangorestframework.utils.staticviews', 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/login$', 'api_login'),
url(r'^accounts/logout$', 'api_logout'), url(r'^accounts/logout$', 'api_logout'),
url(r'^mock/$', MockView.as_view()), url(r'^mock/$', MockView.as_view()),
...@@ -123,18 +121,6 @@ class ExtraViewsTests(TestCase): ...@@ -123,18 +121,6 @@ class ExtraViewsTests(TestCase):
"""Test the extra views djangorestframework provides""" """Test the extra views djangorestframework provides"""
urls = 'djangorestframework.tests.views' 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): def test_login_view(self):
"""Ensure the login view exists""" """Ensure the login view exists"""
response = self.client.get('/accounts/login') response = self.client.get('/accounts/login')
......
from django.conf.urls.defaults import patterns from django.conf.urls.defaults import patterns
from django.conf import settings
urlpatterns = patterns('djangorestframework.utils.staticviews', urlpatterns = patterns('djangorestframework.utils.staticviews',
(r'robots.txt', 'deny_robots'),
(r'^accounts/login/$', 'api_login'), (r'^accounts/login/$', 'api_login'),
(r'^accounts/logout/$', 'api_logout'), (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'),
)
import django
from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode
from django.utils.xmlutils import SimplerXMLGenerator 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 django.conf import settings
from djangorestframework.compat import StringIO from djangorestframework.compat import StringIO
...@@ -180,3 +181,21 @@ class XMLRenderer(): ...@@ -180,3 +181,21 @@ class XMLRenderer():
def dict2xml(input): def dict2xml(input):
return XMLRenderer().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))
...@@ -6,22 +6,13 @@ from django.template import RequestContext ...@@ -6,22 +6,13 @@ from django.template import RequestContext
import base64 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 # BLERGH
# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS # 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 # 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 # be making settings changes in order to accomodate django-rest-framework
@csrf_protect @csrf_protect
@never_cache @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, redirect_field_name=REDIRECT_FIELD_NAME,
authentication_form=AuthenticationForm): authentication_form=AuthenticationForm):
"""Displays the login form and handles the login action.""" """Displays the login form and handles the login action."""
...@@ -66,5 +57,5 @@ def api_login(request, template_name='api_login.html', ...@@ -66,5 +57,5 @@ def api_login(request, template_name='api_login.html',
}, context_instance=RequestContext(request)) }, 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) return logout(request, next_page, template_name, redirect_field_name)
...@@ -7,13 +7,12 @@ By setting or modifying class attributes on your view, you change it's predefine ...@@ -7,13 +7,12 @@ By setting or modifying class attributes on your view, you change it's predefine
import re 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.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe 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, apply_markdown 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.mixins import *
from djangorestframework.utils import allowed_methods from djangorestframework.utils import allowed_methods
from djangorestframework import resources, renderers, parsers, authentication, permissions, status from djangorestframework import resources, renderers, parsers, authentication, permissions, status
...@@ -163,6 +162,9 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -163,6 +162,9 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
return description return description
def markup_description(self, description): def markup_description(self, description):
"""
Apply HTML markup to the description of this view.
"""
if apply_markdown: if apply_markdown:
description = apply_markdown(description) description = apply_markdown(description)
else: else:
...@@ -171,11 +173,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -171,11 +173,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
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.
""" """
raise ImmediateResponse( content = {
{'detail': 'Method \'%s\' not allowed on this resource.' % request.method}, 'detail': "Method '%s' not allowed on this resource." % request.method
status=status.HTTP_405_METHOD_NOT_ALLOWED) }
raise ImmediateResponse(content, status.HTTP_405_METHOD_NOT_ALLOWED)
def initial(self, request, *args, **kargs): def initial(self, request, *args, **kargs):
""" """
...@@ -184,22 +188,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -184,22 +188,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
Required if you want to do things like set `request.upload_handlers` before Required if you want to do things like set `request.upload_handlers` before
the authentication and dispatch handling is run. the authentication and dispatch handling is run.
""" """
# Calls to 'reverse' will not be fully qualified unless we set the pass
# 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
def final(self, request, response, *args, **kargs): def final(self, request, response, *args, **kargs):
""" """
Returns an `HttpResponse`. This method is a hook for any code that needs to run Returns an `HttpResponse`. This method is a hook for any code that needs to run
after everything else in the view. after everything else in the view.
""" """
# Restore script_prefix.
set_script_prefix(self.orig_prefix)
# Always add these headers. # Always add these headers.
response['Allow'] = ', '.join(allowed_methods(self)) response['Allow'] = ', '.join(allowed_methods(self))
# sample to allow caching using Vary http header # sample to allow caching using Vary http header
...@@ -211,17 +206,12 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -211,17 +206,12 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
# all other authentication is CSRF exempt. # all other authentication is CSRF exempt.
@csrf_exempt @csrf_exempt
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.request = request self.request = self.create_request(request)
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
try: try:
# Get a custom request, built form the original request instance self.initial(request, *args, **kwargs)
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)
# Authenticate and check request has the relevant permissions # Authenticate and check request has the relevant permissions
self._check_permissions() self._check_permissions()
...@@ -231,7 +221,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -231,7 +221,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
handler = getattr(self, request.method.lower(), self.http_method_not_allowed) handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
else: else:
handler = self.http_method_not_allowed handler = self.http_method_not_allowed
# TODO: should we enforce HttpResponse, like Django does ? # TODO: should we enforce HttpResponse, like Django does ?
response = handler(request, *args, **kwargs) response = handler(request, *args, **kwargs)
...@@ -239,7 +229,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -239,7 +229,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
self.response = response = self.prepare_response(response) self.response = response = self.prepare_response(response)
# Pre-serialize filtering (eg filter complex objects into natively serializable types) # 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'): if hasattr(response, 'raw_content'):
response.raw_content = self.filter_response(response.raw_content) response.raw_content = self.filter_response(response.raw_content)
else: else:
......
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
...@@ -3,52 +3,70 @@ ...@@ -3,52 +3,70 @@
Setup 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, Django REST framework requires `django.contrib.staticfiles`_ to serve it's css.
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 you're using Django 1.2 you'll need to use the seperate
`if using the Django admin <https://docs.djangoproject.com/en/dev/howto/deployment/modpython/#serving-the-admin-files>`_. `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 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 from django.conf.urls.defaults import patterns
urlpatterns = patterns('djangorestframework.views', urlpatterns = patterns('djangorestframework.views',
(r'robots.txt', 'deny_robots'),
(r'favicon.ico', 'favicon'),
# Add your resources here # Add your resources here
(r'^accounts/login/$', 'api_login'), (r'^accounts/login/$', 'api_login'),
(r'^accounts/logout/$', 'api_logout'), (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
...@@ -40,8 +40,11 @@ Requirements ...@@ -40,8 +40,11 @@ Requirements
------------ ------------
* Python (2.5, 2.6, 2.7 supported) * 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 Installation
------------ ------------
...@@ -54,8 +57,6 @@ Or get the latest development version using git:: ...@@ -54,8 +57,6 @@ Or get the latest development version using git::
git clone git@github.com:tomchristie/django-rest-framework.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 Setup
----- -----
...@@ -114,3 +115,8 @@ Indices and tables ...@@ -114,3 +115,8 @@ Indices and tables
* :ref:`modindex` * :ref:`modindex`
* :ref:`search` * :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
:mod:`reverse`
================
.. automodule:: reverse
:members:
:mod:`utils`
==============
.. automodule:: utils
:members:
from django.core.urlresolvers import reverse
from djangorestframework.resources import ModelResource from djangorestframework.resources import ModelResource
from djangorestframework.reverse import reverse
from blogpost.models import BlogPost, Comment from blogpost.models import BlogPost, Comment
...@@ -12,7 +12,7 @@ class BlogPostResource(ModelResource): ...@@ -12,7 +12,7 @@ class BlogPostResource(ModelResource):
ordering = ('-created',) ordering = ('-created',)
def comments(self, instance): def comments(self, instance):
return reverse('comments', kwargs={'blogpost': instance.key}) return reverse('comments', request, kwargs={'blogpost': instance.key})
class CommentResource(ModelResource): class CommentResource(ModelResource):
...@@ -24,4 +24,4 @@ class CommentResource(ModelResource): ...@@ -24,4 +24,4 @@ class CommentResource(ModelResource):
ordering = ('-created',) ordering = ('-created',)
def blogpost(self, instance): def blogpost(self, instance):
return reverse('blog-post', kwargs={'key': instance.blogpost.key}) return reverse('blog-post', request, kwargs={'key': instance.blogpost.key})
"""Test a range of REST API usage of the example application. """Test a range of REST API usage of the example application.
""" """
from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.core.urlresolvers import reverse
from django.utils import simplejson as json from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.reverse import reverse
from djangorestframework.views import InstanceModelView, ListOrCreateModelView from djangorestframework.views import InstanceModelView, ListOrCreateModelView
from blogpost import models, urls from blogpost import models, urls
......
...@@ -2,9 +2,9 @@ from djangorestframework.compat import View # Use Django 1.3's django.views.gen ...@@ -2,9 +2,9 @@ from djangorestframework.compat import View # Use Django 1.3's django.views.gen
from djangorestframework.mixins import ResponseMixin from djangorestframework.mixins import ResponseMixin
from djangorestframework.renderers import DEFAULT_RENDERERS from djangorestframework.renderers import DEFAULT_RENDERERS
from djangorestframework.response import Response from djangorestframework.response import Response
from djangorestframework.reverse import reverse
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url
from django.core.urlresolvers import reverse
class ExampleView(ResponseMixin, View): class ExampleView(ResponseMixin, View):
...@@ -14,7 +14,7 @@ class ExampleView(ResponseMixin, View): ...@@ -14,7 +14,7 @@ class ExampleView(ResponseMixin, View):
def get(self, request): def get(self, request):
response = Response({'description': 'Some example content', 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) self.response = self.prepare_response(response)
return self.response return self.response
......
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from djangorestframework.reverse import reverse
from djangorestframework.views import View from djangorestframework.views import View
from djangorestframework.response import Response from djangorestframework.response import Response
from djangorestframework import status from djangorestframework import status
...@@ -41,7 +41,7 @@ class ObjectStoreRoot(View): ...@@ -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('.')] 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], 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)] 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): def post(self, request):
""" """
...@@ -51,8 +51,8 @@ class ObjectStoreRoot(View): ...@@ -51,8 +51,8 @@ class ObjectStoreRoot(View):
pathname = os.path.join(OBJECT_STORE_DIR, key) pathname = os.path.join(OBJECT_STORE_DIR, key)
pickle.dump(self.CONTENT, open(pathname, 'wb')) pickle.dump(self.CONTENT, open(pathname, 'wb'))
remove_oldest_files(OBJECT_STORE_DIR, MAX_FILES) remove_oldest_files(OBJECT_STORE_DIR, MAX_FILES)
self.headers['Location'] = reverse('stored-object', kwargs={'key':key}) url = reverse('stored-object', request, kwargs={'key':key})
return Response(self.CONTENT, status=status.HTTP_201_CREATED) return Response(self.CONTENT, status.HTTP_201_CREATED, {'Location': url})
class StoredObject(View): class StoredObject(View):
......
from djangorestframework.views import View from djangorestframework.views import View
from djangorestframework.response import Response from djangorestframework.response import Response
from djangorestframework.permissions import PerUserThrottling, IsAuthenticated from djangorestframework.permissions import PerUserThrottling, IsAuthenticated
from django.core.urlresolvers import reverse from djangorestframework.reverse import reverse
class PermissionsExampleView(View): class PermissionsExampleView(View):
...@@ -13,11 +13,11 @@ class PermissionsExampleView(View): ...@@ -13,11 +13,11 @@ class PermissionsExampleView(View):
return Response([ return Response([
{ {
'name': 'Throttling Example', 'name': 'Throttling Example',
'url': reverse('throttled-resource') 'url': reverse('throttled-resource', request)
}, },
{ {
'name': 'Logged in example', 'name': 'Logged in example',
'url': reverse('loggedin-resource') 'url': reverse('loggedin-resource', request)
}, },
]) ])
......
from __future__ import with_statement # for python 2.5 from __future__ import with_statement # for python 2.5
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from djangorestframework.resources import FormResource from djangorestframework.resources import FormResource
from djangorestframework.response import Response from djangorestframework.response import Response
from djangorestframework.renderers import BaseRenderer from djangorestframework.renderers import BaseRenderer
from djangorestframework.reverse import reverse
from djangorestframework.views import View from djangorestframework.views import View
from djangorestframework import status from djangorestframework import status
...@@ -61,7 +61,7 @@ class PygmentsRoot(View): ...@@ -61,7 +61,7 @@ class PygmentsRoot(View):
Return a list of all currently existing snippets. 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)] 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): def post(self, request):
""" """
...@@ -81,8 +81,8 @@ class PygmentsRoot(View): ...@@ -81,8 +81,8 @@ class PygmentsRoot(View):
remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES) remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES)
self.headers['Location'] = reverse('pygments-instance', args=[unique_id]) location = reverse('pygments-instance', request, args=[unique_id])
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_201_CREATED, headers={'Location': location})
class PygmentsInstance(View): class PygmentsInstance(View):
...@@ -98,7 +98,7 @@ class PygmentsInstance(View): ...@@ -98,7 +98,7 @@ class PygmentsInstance(View):
""" """
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id) pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
if not os.path.exists(pathname): 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()) return Response(open(pathname, 'r').read())
def delete(self, request, unique_id): def delete(self, request, unique_id):
...@@ -107,6 +107,7 @@ class PygmentsInstance(View): ...@@ -107,6 +107,7 @@ class PygmentsInstance(View):
""" """
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id) pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
if not os.path.exists(pathname): if not os.path.exists(pathname):
return Response(status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
return Response(os.remove(pathname)) os.remove(pathname)
return Response()
from django.core.urlresolvers import reverse from djangorestframework.reverse import reverse
from djangorestframework.views import View from djangorestframework.views import View
from djangorestframework.response import Response from djangorestframework.response import Response
from djangorestframework import status from djangorestframework import status
...@@ -14,9 +13,12 @@ class ExampleView(View): ...@@ -14,9 +13,12 @@ class ExampleView(View):
def get(self, request): 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): class AnotherExampleView(View):
...@@ -32,7 +34,7 @@ class AnotherExampleView(View): ...@@ -32,7 +34,7 @@ class AnotherExampleView(View):
Returns a simple string indicating which view the GET request was for. Returns a simple string indicating which view the GET request was for.
""" """
if int(num) > 2: 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) return Response("GET request to AnotherExampleResource %s" % num)
def post(self, request, num): def post(self, request, num):
...@@ -41,5 +43,5 @@ class AnotherExampleView(View): ...@@ -41,5 +43,5 @@ class AnotherExampleView(View):
Returns a simple string indicating what content was supplied. Returns a simple string indicating what content was supplied.
""" """
if int(num) > 2: 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))) return Response("POST request to AnotherExampleResource %s, with content: %s" % (num, repr(self.CONTENT)))
"""The root view for the examples provided with Django REST framework""" """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.views import View
from djangorestframework.response import Response from djangorestframework.response import Response
class Sandbox(View): 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) For example, to get the default representation using curl:
bash: curl -X GET http://api.django-rest-framework.org/ -H 'Accept: text/plain' # (Use plaintext documentation renderer)
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: The examples provided:
1. A basic example using the [Resource](http://django-rest-framework.org/library/resource.html) class. 1. A basic example using the [Resource][2] class.
2. A basic example using the [ModelResource](http://django-rest-framework.org/library/modelresource.html) class. 2. A basic example using the [ModelResource][3] 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). 3. An basic example using Django 1.3's [class based views][4] and
djangorestframework's [RendererMixin][5].
4. A generic object store API. 4. A generic object store API.
5. A code highlighting API. 5. A code highlighting API.
6. A blog posts and comments API. 6. A blog posts and comments API.
7. A basic example using permissions. 7. A basic example using permissions.
8. A basic example using enhanced request. 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): def get(self, request):
return Response([{'name': 'Simple Resource example', 'url': reverse('example-resource')}, return Response([
{'name': 'Simple ModelResource example', 'url': reverse('model-resource-root')}, {'name': 'Simple Resource example',
{'name': 'Simple Mixin-only example', 'url': reverse('mixin-view')}, 'url': reverse('example-resource', request)},
{'name': 'Object store API', 'url': reverse('object-store-root')}, {'name': 'Simple ModelResource example',
{'name': 'Code highlighting API', 'url': reverse('pygments-root')}, 'url': reverse('model-resource-root', request)},
{'name': 'Blog posts API', 'url': reverse('blog-posts-root')}, {'name': 'Simple Mixin-only example',
{'name': 'Permissions example', 'url': reverse('permissions-example')}, 'url': reverse('mixin-view', request)},
{'name': 'Simple request mixin example', 'url': reverse('request-example')} {'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)}
])
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment