diff options
38 files changed, 349 insertions, 217 deletions
@@ -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 Binary files differdeleted file mode 100644 index 17080106..00000000 --- a/djangorestframework/static/favicon.ico +++ /dev/null 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 %}›{% 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)} + ]) |
