From dd680d7a0ae997f2e562db383f23b56624e7ea98 Mon Sep 17 00:00:00 2001 From: Alen Mujezinovic Date: Fri, 27 Jan 2012 12:56:05 +0000 Subject: [PATCH 01/44] Added get_renderers method to ResponseMixin --- djangorestframework/mixins.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 7f0870f85..70492d840 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -183,7 +183,7 @@ class RequestMixin(object): return parser.parse(stream) raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, - {'error': 'Unsupported media type in request \'%s\'.' % + {'error': 'Unsupported media type in request \'%s\'.' % content_type}) @property @@ -222,6 +222,13 @@ class ResponseMixin(object): Should be a tuple/list of classes as described in the :mod:`renderers` module. """ + def get_renderers(self): + """ + Return an iterable of available renderers. Override if you want to change + this list at runtime, say depending on what settings you have enabled. + """ + return self.renderers + # TODO: wrap this behavior around dispatch(), ensuring it works # out of the box with existing Django classes that use render_to_response. def render(self, response): @@ -283,7 +290,7 @@ class ResponseMixin(object): # attempting more specific media types first # NB. The inner loop here isn't as bad as it first looks :) # Worst case is we're looping over len(accept_list) * len(self.renderers) - renderers = [renderer_cls(self) for renderer_cls in self.renderers] + renderers = [renderer_cls(self) for renderer_cls in self.get_renderers()] for accepted_media_type_lst in order_by_precedence(accept_list): for renderer in renderers: From e0f7d2cd9f9ef1f9788d402b6a2861946d4c2fc9 Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Wed, 15 Feb 2012 10:21:02 +0900 Subject: [PATCH 02/44] Add empty {% blocks %} to the template to give people a chance to override --- .../templates/base_renderer.html | 142 ++++++++++++++++++ djangorestframework/templates/renderer.html | 130 +--------------- 2 files changed, 144 insertions(+), 128 deletions(-) create mode 100644 djangorestframework/templates/base_renderer.html diff --git a/djangorestframework/templates/base_renderer.html b/djangorestframework/templates/base_renderer.html new file mode 100644 index 000000000..1d4ae92ae --- /dev/null +++ b/djangorestframework/templates/base_renderer.html @@ -0,0 +1,142 @@ + + +{% load urlize_quoted_links %} +{% load add_query_param %} +{% load static %} + + + + {% block extrastyle %}{% endblock %} + {% block title %}Django REST framework - {{ name }}{% endblock %} + {% block extrahead %}{% endblock %} + {% block blockbots %}{% endblock %} + + +
+ + + + + + +
+ + {% if 'OPTIONS' in view.allowed_methods %} +
+ {% csrf_token %} + + +
+ {% endif %} + +
+

{{ name }}

+

{{ description }}

+
+
{{ response.status }} {{ response.status_text }}{% autoescape off %}
+{% for key, val in response.headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
+{% endfor %}
+{{ content|urlize_quoted_links }}
{% endautoescape %}
+ + {% if 'GET' in view.allowed_methods %} +
+
+

GET {{ name }}

+
+ GET + {% for format in available_formats %} + {% with FORMAT_PARAM|add:"="|add:format as param %} + [{{ format }}] + {% endwith %} + {% endfor %} +
+
+
+ {% endif %} + + {# Only display the POST/PUT/DELETE forms if method tunneling via POST forms is enabled and the user has permissions on this view. #} + {% if METHOD_PARAM and response.status != 403 %} + + {% if 'POST' in view.allowed_methods %} +
+
+

POST {{ name }}

+ {% csrf_token %} + {{ post_form.non_field_errors }} + {% for field in post_form %} +
+ {{ field.label_tag }} + {{ field }} + {{ field.help_text }} + {{ field.errors }} +
+ {% endfor %} +
+ +
+
+
+ {% endif %} + + {% if 'PUT' in view.allowed_methods %} +
+
+

PUT {{ name }}

+ + {% csrf_token %} + {{ put_form.non_field_errors }} + {% for field in put_form %} +
+ {{ field.label_tag }} + {{ field }} + {{ field.help_text }} + {{ field.errors }} +
+ {% endfor %} +
+ +
+
+
+ {% endif %} + + {% if 'DELETE' in view.allowed_methods %} +
+
+

DELETE {{ name }}

+ {% csrf_token %} + +
+ +
+
+
+ {% endif %} + + {% endif %} +
+ + +
+ + + {% block footer %}{% endblock %} +
+ + diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html index bda49e6f8..adb42c7f2 100644 --- a/djangorestframework/templates/renderer.html +++ b/djangorestframework/templates/renderer.html @@ -1,129 +1,3 @@ - - -{% load urlize_quoted_links %} -{% load add_query_param %} -{% load static %} - - - - Django REST framework - {{ name }} - - -
+{% extends "base_renderer.html" %} - - - - -
- - {% if 'OPTIONS' in view.allowed_methods %} -
- {% csrf_token %} - - -
- {% endif %} - -
-

{{ name }}

-

{{ description }}

-
-
{{ response.status }} {{ response.status_text }}{% autoescape off %}
-{% for key, val in response.headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
-{% endfor %}
-{{ content|urlize_quoted_links }}
{% endautoescape %}
- - {% if 'GET' in view.allowed_methods %} -
-
-

GET {{ name }}

-
- GET - {% for format in available_formats %} - {% with FORMAT_PARAM|add:"="|add:format as param %} - [{{ format }}] - {% endwith %} - {% endfor %} -
-
-
- {% endif %} - - {# Only display the POST/PUT/DELETE forms if method tunneling via POST forms is enabled and the user has permissions on this view. #} - {% if METHOD_PARAM and response.status != 403 %} - - {% if 'POST' in view.allowed_methods %} -
-
-

POST {{ name }}

- {% csrf_token %} - {{ post_form.non_field_errors }} - {% for field in post_form %} -
- {{ field.label_tag }} - {{ field }} - {{ field.help_text }} - {{ field.errors }} -
- {% endfor %} -
- -
-
-
- {% endif %} - - {% if 'PUT' in view.allowed_methods %} -
-
-

PUT {{ name }}

- - {% csrf_token %} - {{ put_form.non_field_errors }} - {% for field in put_form %} -
- {{ field.label_tag }} - {{ field }} - {{ field.help_text }} - {{ field.errors }} -
- {% endfor %} -
- -
-
-
- {% endif %} - - {% if 'DELETE' in view.allowed_methods %} -
-
-

DELETE {{ name }}

- {% csrf_token %} - -
- -
-
-
- {% endif %} - - {% endif %} -
-
-
- - +{# Override this template in your own templates directory to customize #} \ No newline at end of file From cbd0752740b4dfb054179f68ca330b8ac212ec77 Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Wed, 15 Feb 2012 10:30:56 +0900 Subject: [PATCH 03/44] Move the templates into a named directory --- djangorestframework/renderers.py | 6 +++--- .../templates/{ => djangorestframework}/api_login.html | 0 .../templates/{ => djangorestframework}/base_renderer.html | 0 .../templates/{ => djangorestframework}/renderer.html | 2 +- .../templates/{ => djangorestframework}/renderer.txt | 0 djangorestframework/utils/staticviews.py | 4 ++-- 6 files changed, 6 insertions(+), 6 deletions(-) rename djangorestframework/templates/{ => djangorestframework}/api_login.html (100%) rename djangorestframework/templates/{ => djangorestframework}/base_renderer.html (100%) rename djangorestframework/templates/{ => djangorestframework}/renderer.html (57%) rename djangorestframework/templates/{ => djangorestframework}/renderer.txt (100%) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index bb0f789aa..33c549fde 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -373,7 +373,7 @@ class DocumentingHTMLRenderer(DocumentingTemplateRenderer): media_type = 'text/html' format = 'html' - template = 'renderer.html' + template = 'djangorestframework/renderer.html' class DocumentingXHTMLRenderer(DocumentingTemplateRenderer): @@ -385,7 +385,7 @@ class DocumentingXHTMLRenderer(DocumentingTemplateRenderer): media_type = 'application/xhtml+xml' format = 'xhtml' - template = 'renderer.html' + template = 'djangorestframework/renderer.html' class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): @@ -397,7 +397,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): media_type = 'text/plain' format = 'txt' - template = 'renderer.txt' + template = 'djangorestframework/renderer.txt' DEFAULT_RENDERERS = ( diff --git a/djangorestframework/templates/api_login.html b/djangorestframework/templates/djangorestframework/api_login.html similarity index 100% rename from djangorestframework/templates/api_login.html rename to djangorestframework/templates/djangorestframework/api_login.html diff --git a/djangorestframework/templates/base_renderer.html b/djangorestframework/templates/djangorestframework/base_renderer.html similarity index 100% rename from djangorestframework/templates/base_renderer.html rename to djangorestframework/templates/djangorestframework/base_renderer.html diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/djangorestframework/renderer.html similarity index 57% rename from djangorestframework/templates/renderer.html rename to djangorestframework/templates/djangorestframework/renderer.html index adb42c7f2..01ca55435 100644 --- a/djangorestframework/templates/renderer.html +++ b/djangorestframework/templates/djangorestframework/renderer.html @@ -1,3 +1,3 @@ -{% extends "base_renderer.html" %} +{% extends "djangorestframework/base_renderer.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/renderer.txt similarity index 100% rename from djangorestframework/templates/renderer.txt rename to djangorestframework/templates/djangorestframework/renderer.txt diff --git a/djangorestframework/utils/staticviews.py b/djangorestframework/utils/staticviews.py index 9bae0ee78..caa63ccd3 100644 --- a/djangorestframework/utils/staticviews.py +++ b/djangorestframework/utils/staticviews.py @@ -12,7 +12,7 @@ import base64 # 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/api_login.html', redirect_field_name=REDIRECT_FIELD_NAME, authentication_form=AuthenticationForm): """Displays the login form and handles the login action.""" @@ -57,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/api_login.html', redirect_field_name=REDIRECT_FIELD_NAME): return logout(request, next_page, template_name, redirect_field_name) From bf717eff6420af78a41e1359cdd7b9e409076281 Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Wed, 15 Feb 2012 10:31:56 +0900 Subject: [PATCH 04/44] Documentation on how to override the template; Closes Issue #165 --- docs/howto/setup.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/howto/setup.rst b/docs/howto/setup.rst index 22f98f0c6..d018c72d4 100644 --- a/docs/howto/setup.rst +++ b/docs/howto/setup.rst @@ -29,6 +29,19 @@ but once you move onto a production server, you'll want to make sure you serve t * 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) +You may customize the templates by creating a new template called ``djangorestframework/renderer.html`` +in your project, extend ``djangorestframework/base_renderer.html`` and override the +appropriate ``{% block tags %}``. For example:: + + {% extends "djangorestframework/base_renderer.html" %} + + {% block title %}My API{% endblock %} + + {% block branding %} +

My API

+ {% endblock %} + + Markdown -------- From 50198935af1f772835a3c26943cbb245295c60e5 Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Mon, 20 Feb 2012 22:16:51 +0900 Subject: [PATCH 05/44] Rename templates to 'base.html', 'api.html', 'api.txt' and 'login.html' --- djangorestframework/renderers.py | 6 +++--- .../djangorestframework/{renderer.html => api.html} | 2 +- .../templates/djangorestframework/{renderer.txt => api.txt} | 0 .../djangorestframework/{base_renderer.html => base.html} | 0 .../djangorestframework/{api_login.html => login.html} | 0 djangorestframework/utils/staticviews.py | 4 ++-- docs/howto/setup.rst | 6 +++--- 7 files changed, 9 insertions(+), 9 deletions(-) rename djangorestframework/templates/djangorestframework/{renderer.html => api.html} (57%) rename djangorestframework/templates/djangorestframework/{renderer.txt => api.txt} (100%) rename djangorestframework/templates/djangorestframework/{base_renderer.html => base.html} (100%) rename djangorestframework/templates/djangorestframework/{api_login.html => login.html} (100%) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 33c549fde..de9a01ec4 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -373,7 +373,7 @@ class DocumentingHTMLRenderer(DocumentingTemplateRenderer): media_type = 'text/html' format = 'html' - template = 'djangorestframework/renderer.html' + template = 'djangorestframework/api.html' class DocumentingXHTMLRenderer(DocumentingTemplateRenderer): @@ -385,7 +385,7 @@ class DocumentingXHTMLRenderer(DocumentingTemplateRenderer): media_type = 'application/xhtml+xml' format = 'xhtml' - template = 'djangorestframework/renderer.html' + template = 'djangorestframework/api.html' class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): @@ -397,7 +397,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): media_type = 'text/plain' format = 'txt' - template = 'djangorestframework/renderer.txt' + template = 'djangorestframework/api.txt' DEFAULT_RENDERERS = ( diff --git a/djangorestframework/templates/djangorestframework/renderer.html b/djangorestframework/templates/djangorestframework/api.html similarity index 57% rename from djangorestframework/templates/djangorestframework/renderer.html rename to djangorestframework/templates/djangorestframework/api.html index 01ca55435..fd9bcc983 100644 --- a/djangorestframework/templates/djangorestframework/renderer.html +++ b/djangorestframework/templates/djangorestframework/api.html @@ -1,3 +1,3 @@ -{% extends "djangorestframework/base_renderer.html" %} +{% 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/djangorestframework/renderer.txt b/djangorestframework/templates/djangorestframework/api.txt similarity index 100% rename from djangorestframework/templates/djangorestframework/renderer.txt rename to djangorestframework/templates/djangorestframework/api.txt diff --git a/djangorestframework/templates/djangorestframework/base_renderer.html b/djangorestframework/templates/djangorestframework/base.html similarity index 100% rename from djangorestframework/templates/djangorestframework/base_renderer.html rename to djangorestframework/templates/djangorestframework/base.html diff --git a/djangorestframework/templates/djangorestframework/api_login.html b/djangorestframework/templates/djangorestframework/login.html similarity index 100% rename from djangorestframework/templates/djangorestframework/api_login.html rename to djangorestframework/templates/djangorestframework/login.html diff --git a/djangorestframework/utils/staticviews.py b/djangorestframework/utils/staticviews.py index caa63ccd3..7cbc0b9b8 100644 --- a/djangorestframework/utils/staticviews.py +++ b/djangorestframework/utils/staticviews.py @@ -12,7 +12,7 @@ import base64 # be making settings changes in order to accomodate django-rest-framework @csrf_protect @never_cache -def api_login(request, template_name='djangorestframework/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.""" @@ -57,5 +57,5 @@ def api_login(request, template_name='djangorestframework/api_login.html', }, context_instance=RequestContext(request)) -def api_logout(request, next_page=None, template_name='djangorestframework/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/docs/howto/setup.rst b/docs/howto/setup.rst index d018c72d4..64b58262c 100644 --- a/docs/howto/setup.rst +++ b/docs/howto/setup.rst @@ -29,11 +29,11 @@ but once you move onto a production server, you'll want to make sure you serve t * 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) -You may customize the templates by creating a new template called ``djangorestframework/renderer.html`` -in your project, extend ``djangorestframework/base_renderer.html`` and override the +You may customize the templates by creating a new template called ``djangorestframework/api.html`` +in your project, extend ``djangorestframework/base.html`` and override the appropriate ``{% block tags %}``. For example:: - {% extends "djangorestframework/base_renderer.html" %} + {% extends "djangorestframework/base.html" %} {% block title %}My API{% endblock %} From 45cf5260acc4e03c6557367b5621f943d1b22d41 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Feb 2012 15:27:35 +0000 Subject: [PATCH 06/44] Update CHANGELOG.rst --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 010bf6c01..f225e2c3a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,8 @@ development * 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. From 0954765e910ec9fb01f9ac0285a3a51cde586949 Mon Sep 17 00:00:00 2001 From: "Sean C. Farley" Date: Mon, 20 Feb 2012 12:56:27 -0500 Subject: [PATCH 07/44] Chase changes to URLObject's API in v2.0.0 URLObject v2.0.0 removed parse() from its API. --- djangorestframework/mixins.py | 2 +- djangorestframework/templatetags/add_query_param.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index cb0b913a7..c10ac1edc 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -685,7 +685,7 @@ class PaginatorMixin(object): """ 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) limit = self.get_limit() diff --git a/djangorestframework/templatetags/add_query_param.py b/djangorestframework/templatetags/add_query_param.py index 117097303..4cf0133be 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) From b9c8b72875e696689256fc8942d99a45e69cca28 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Feb 2012 19:57:13 +0000 Subject: [PATCH 08/44] Added @scfarley --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index e79cac9cb..c6bb56f3c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -31,6 +31,7 @@ Ben Timby Michele Lazzeri Camille Harang Paul Oswald +Sean C. Farley THANKS TO: From 36d3db1447f57f0f33a5c1d1d266b2b2499697f8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Feb 2012 19:58:41 +0000 Subject: [PATCH 09/44] Update ChangeLog - URLObject 2.0.0 --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f225e2c3a..1e2af1c19 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,7 @@ development - 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. From 9fb77938613ab81c61f3eb8f57952feeac69bac4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Feb 2012 20:07:14 +0000 Subject: [PATCH 10/44] Fix for URLObject 2.0.0 --- djangorestframework/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index c10ac1edc..f38277eb4 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -686,11 +686,11 @@ class PaginatorMixin(object): Constructs a url used for getting the next/previous urls """ 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() if limit != self.limit: - url = url.add_query_param('limit', limit) + url = url.set_query_param('limit', str(limit)) return url From 6c5fb562574d4c42a2ecc3eb9109e56599551a11 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Feb 2012 20:31:41 +0000 Subject: [PATCH 11/44] Update docs --- docs/howto/setup.rst | 74 ++++++++++++++++++++++++-------------------- docs/index.rst | 14 ++++++--- 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/docs/howto/setup.rst b/docs/howto/setup.rst index 64b58262c..0af1449cb 100644 --- a/docs/howto/setup.rst +++ b/docs/howto/setup.rst @@ -3,35 +3,18 @@ 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 - -Template Loaders ----------------- - -Django REST framework uses a few templates for the HTML and plain text documenting renderers. - -* Ensure ``TEMPLATE_LOADERS`` setting contains ``'django.template.loaders.app_directories.Loader'``. - -This will be the case by default so you shouldn't normally need to do anything here. - -Admin 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 `_. - -* 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) - -You may customize the templates by creating a new template called ``djangorestframework/api.html`` -in your project, extend ``djangorestframework/base.html`` and override the -appropriate ``{% block tags %}``. For example:: +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:: {% extends "djangorestframework/base.html" %} @@ -42,19 +25,36 @@ appropriate ``{% block tags %}``. For example:: {% endblock %} +Styling +------- + +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`` + + Markdown -------- -The Python `markdown library `_ 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 style formatting -`_ which will be rendered by the HTML documenting renderer. +If markdown is installed your :class:`.Resource` descriptions can include +`markdown formatting`_ which will be rendered by the self-documenting API. -login/logout ---------------------------------- +YAML +---- -Django REST framework comes with a few views that can be useful including an api -login and logout views:: +YAML support is optional, and requires `PyYAML`_. + + +Login / Logout +-------------- + +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 @@ -64,3 +64,9 @@ login and logout views:: (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 ecc1f1182..b969c4a38 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 `_. - 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 From 54a02c1fb9e9f1036e94aa662f1c286398a93ef7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Feb 2012 20:33:07 +0000 Subject: [PATCH 12/44] Version 0.3.3 --- CHANGELOG.rst | 4 ++-- djangorestframework/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1e2af1c19..ddc3ac17c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,8 @@ Release Notes ============= -development ------------ +0.3.3 +----- * Added DjangoModelPermissions class to support `django.contrib.auth` style permissions. * Use `staticfiles` for css files. diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py index 0aaa29152..efe7f5663 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 From 5a5f24f263c218f4f04a6ea54bfc48a2794e152b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 21 Feb 2012 13:38:21 +0000 Subject: [PATCH 13/44] Gotta love dynamic languages --- djangorestframework/serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py index 71c0d93ae..b0c026753 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 From d808d55b5ca9ae2e45418aca718ee21a9beb84f9 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Thu, 16 Feb 2012 11:03:05 +0900 Subject: [PATCH 14/44] Create a custom reverse() function (not implemented yet) --- djangorestframework/urlresolvers.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 djangorestframework/urlresolvers.py diff --git a/djangorestframework/urlresolvers.py b/djangorestframework/urlresolvers.py new file mode 100644 index 000000000..0fd88ec1a --- /dev/null +++ b/djangorestframework/urlresolvers.py @@ -0,0 +1,4 @@ +from django.core.urlresolvers import reverse + +def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): + raise NotImplementedError From 12e34814e99e0fd99184def782ae6f49a6120ab3 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Thu, 16 Feb 2012 11:03:42 +0900 Subject: [PATCH 15/44] Test the custom reverse() function instead of the one provided by Django --- djangorestframework/tests/reverse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py index 2d1ca79e6..0b74ab06d 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -1,8 +1,8 @@ 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.urlresolvers import reverse from djangorestframework.views import View From af4851f7a873a07677c51fb9615cd26c5eb0cb89 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 18:05:12 +0900 Subject: [PATCH 16/44] Move the new `reverse' function from urlresolvers to utils --- djangorestframework/tests/reverse.py | 2 +- djangorestframework/urlresolvers.py | 4 ---- djangorestframework/utils/__init__.py | 5 ++++- 3 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 djangorestframework/urlresolvers.py diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py index 0b74ab06d..02dbe9db4 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -2,7 +2,7 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.utils import simplejson as json -from djangorestframework.urlresolvers import reverse +from djangorestframework.utils import reverse from djangorestframework.views import View diff --git a/djangorestframework/urlresolvers.py b/djangorestframework/urlresolvers.py deleted file mode 100644 index 0fd88ec1a..000000000 --- a/djangorestframework/urlresolvers.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.core.urlresolvers import reverse - -def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): - raise NotImplementedError diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index 634d0d68c..ba2f6e05b 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -1,6 +1,6 @@ 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 from django.conf import settings from djangorestframework.compat import StringIO @@ -173,3 +173,6 @@ class XMLRenderer(): def dict2xml(input): return XMLRenderer().dict2xml(input) + +def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): + raise NotImplementedError From f5badcf80c16bd70d8273d08c24554e18056d587 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 18:33:45 +0900 Subject: [PATCH 17/44] Stop using set_script_prefix --- djangorestframework/views.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 32d2437c1..3e8bf3566 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -181,20 +181,12 @@ 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 def final(self, request, response, *args, **kargs): """ 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.headers['Allow'] = ', '.join(self.allowed_methods) # sample to allow caching using Vary http header From a29ca6c2d0fe376c6c3826660b4488ebfd70bfe3 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 18:34:31 +0900 Subject: [PATCH 18/44] Pass the request object to `reverse' in the reverse tests --- djangorestframework/tests/reverse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py index 02dbe9db4..3ac04d8c0 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -11,7 +11,7 @@ class MockView(View): permissions = () def get(self, request): - return reverse('another') + return reverse('another', request) urlpatterns = patterns('', url(r'^$', MockView.as_view()), From 13110a386c9d72feee8cc11f0f45024ba1d8a9f2 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 18:46:47 +0900 Subject: [PATCH 19/44] Implement a custom reverse that builds absolute URLs with a request object --- djangorestframework/utils/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index ba2f6e05b..f72e0046d 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -1,6 +1,6 @@ from django.utils.encoding import smart_unicode from django.utils.xmlutils import SimplerXMLGenerator -from django.core.urlresolvers import resolve, reverse +from django.core.urlresolvers import resolve, reverse as django_reverse from django.conf import settings from djangorestframework.compat import StringIO @@ -174,5 +174,6 @@ class XMLRenderer(): def dict2xml(input): return XMLRenderer().dict2xml(input) -def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): - raise NotImplementedError + +def reverse(viewname, request, *args, **kwargs): + return request.build_absolute_uri(django_reverse(viewname, *args, **kwargs)) From b600b5075fe766bb6f5d3ac10845f53c789b6139 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 18:58:42 +0900 Subject: [PATCH 20/44] Update resources.py to use the new custom `reverse()' --- djangorestframework/resources.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py index cc338cc05..5e126defe 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -1,10 +1,10 @@ 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 ErrorResponse from djangorestframework.serializer import Serializer, _SkipField -from djangorestframework.utils import as_tuple +from djangorestframework.utils import as_tuple, reverse class BaseResource(Serializer): @@ -354,7 +354,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 From c7e7279d979a346b5d1c950cc960183013799c41 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Mon, 20 Feb 2012 19:28:50 +0900 Subject: [PATCH 21/44] Update examples to use the new custom `reverse()' This fixes #167 except for the docs --- examples/blogpost/resources.py | 6 +++--- examples/blogpost/tests.py | 3 +-- examples/mixin/urls.py | 4 ++-- examples/objectstore/views.py | 6 +++--- examples/permissionsexample/views.py | 6 +++--- examples/pygments_api/views.py | 6 +++--- examples/resourceexample/views.py | 5 ++--- examples/sandbox/views.py | 16 ++++++++-------- 8 files changed, 25 insertions(+), 27 deletions(-) diff --git a/examples/blogpost/resources.py b/examples/blogpost/resources.py index 5a3c1ce2b..d4e0594d8 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.utils 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 5aa4f89f4..9f72e6862 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.utils 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 a3da3b2cc..1f8c93b2c 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.utils 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(200, {'description': 'Some example content', - 'url': reverse('mixin-view')}) + 'url': reverse('mixin-view', request)}) return self.render(response) diff --git a/examples/objectstore/views.py b/examples/objectstore/views.py index d85ed9f44..61d51b1be 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.utils 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 [reverse('stored-object', kwargs={'key':key}) for key in ctime_sorted_basenames] + return [reverse('stored-object', request, kwargs={'key':key}) for key in ctime_sorted_basenames] def post(self, request): """ @@ -51,7 +51,7 @@ 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) - return Response(status.HTTP_201_CREATED, self.CONTENT, {'Location': reverse('stored-object', kwargs={'key':key})}) + return Response(status.HTTP_201_CREATED, self.CONTENT, {'Location': reverse('stored-object', request, kwargs={'key':key})}) class StoredObject(View): diff --git a/examples/permissionsexample/views.py b/examples/permissionsexample/views.py index 86f458f8e..83ef0fd9a 100644 --- a/examples/permissionsexample/views.py +++ b/examples/permissionsexample/views.py @@ -1,6 +1,6 @@ from djangorestframework.views import View from djangorestframework.permissions import PerUserThrottling, IsAuthenticated -from django.core.urlresolvers import reverse +from djangorestframework.utils import reverse class PermissionsExampleView(View): @@ -12,11 +12,11 @@ class PermissionsExampleView(View): return [ { '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 ffea60ae3..01710bd50 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.utils 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 [reverse('pygments-instance', args=[unique_id]) for unique_id in unique_ids] + return [reverse('pygments-instance', request, args=[unique_id]) for unique_id in unique_ids] def post(self, request): """ @@ -81,7 +81,7 @@ class PygmentsRoot(View): remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES) - return Response(status.HTTP_201_CREATED, headers={'Location': reverse('pygments-instance', args=[unique_id])}) + return Response(status.HTTP_201_CREATED, headers={'Location': reverse('pygments-instance', request, args=[unique_id])}) class PygmentsInstance(View): diff --git a/examples/resourceexample/views.py b/examples/resourceexample/views.py index e6b5eeb89..2f623c39a 100644 --- a/examples/resourceexample/views.py +++ b/examples/resourceexample/views.py @@ -1,5 +1,4 @@ -from django.core.urlresolvers import reverse - +from djangorestframework.utils import reverse from djangorestframework.views import View from djangorestframework.response import Response from djangorestframework import status @@ -16,7 +15,7 @@ class ExampleView(View): """ Handle GET requests, returning a list of URLs pointing to 3 other views. """ - return {"Some other resources": [reverse('another-example', kwargs={'num':num}) for num in range(3)]} + return {"Some other resources": [reverse('another-example', request, kwargs={'num':num}) for num in range(3)]} class AnotherExampleView(View): diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py index f7a3542d7..3872b5037 100644 --- a/examples/sandbox/views.py +++ b/examples/sandbox/views.py @@ -1,6 +1,6 @@ """The root view for the examples provided with Django REST framework""" -from django.core.urlresolvers import reverse +from djangorestframework.utils import reverse from djangorestframework.views import View @@ -27,11 +27,11 @@ class Sandbox(View): Please feel free to browse, create, edit and delete the resources in these examples.""" def get(self, request): - return [{'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')} + return [{'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)} ] From 86d470796e077e8c3c2253bba26cd7550f5d8f33 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Tue, 21 Feb 2012 15:23:31 +0900 Subject: [PATCH 22/44] Add a HOWTO page for the custom `reverse()' --- docs/howto/reverse.rst | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/howto/reverse.rst diff --git a/docs/howto/reverse.rst b/docs/howto/reverse.rst new file mode 100644 index 000000000..e4efbbcab --- /dev/null +++ b/docs/howto/reverse.rst @@ -0,0 +1,47 @@ +Returning URIs from your Web APIs +================================= + + "The central feature that distinguishes the REST architectural style from + other network-based styles is its emphasis on a uniform interface between + components." + + -- Roy Fielding, Architectural Styles and the Design of Network-based Software Architectures + +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:`~utils.reverse` function has the same behavior as :py:func:`django.core.urlresolvers.reverse` [1]_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port:: + + from djangorestframework.utils 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:`~utils.reverse_lazy` function has the same behavior as :py:func:`django.core.urlresolvers.reverse_lazy` [2]_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port. + +.. rubric:: Footnotes + +.. [1] https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse +.. [2] https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse-lazy From 8f5b093c0d0db7b50bd197abfc8dc137f794b8e7 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Tue, 21 Feb 2012 15:29:47 +0900 Subject: [PATCH 23/44] Include the `utils' module in the docs --- docs/library/utils.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/library/utils.rst diff --git a/docs/library/utils.rst b/docs/library/utils.rst new file mode 100644 index 000000000..653f24fde --- /dev/null +++ b/docs/library/utils.rst @@ -0,0 +1,5 @@ +:mod:`utils` +============== + +.. automodule:: utils + :members: From 6f4fab81d2926af47e75e0c1bae4aad17c6bca91 Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Tue, 21 Feb 2012 15:30:00 +0900 Subject: [PATCH 24/44] Add a docstring for `reverse()' --- djangorestframework/utils/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index f72e0046d..a0519e517 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -176,4 +176,8 @@ def 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)) From f17f3886f42c2068e6e8a41c6e5fe6bfe1408add Mon Sep 17 00:00:00 2001 From: Daniel Izquierdo Date: Tue, 21 Feb 2012 15:55:39 +0900 Subject: [PATCH 25/44] Implement `reverse_lazy()' --- djangorestframework/utils/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index a0519e517..fc8bae924 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -1,3 +1,4 @@ +import django from django.utils.encoding import smart_unicode from django.utils.xmlutils import SimplerXMLGenerator from django.core.urlresolvers import resolve, reverse as django_reverse @@ -181,3 +182,13 @@ def reverse(viewname, request, *args, **kwargs): *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)) From 288e7f7c40bf97b88e39b59f1aba7740de8b353c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 21 Feb 2012 14:16:47 +0000 Subject: [PATCH 26/44] Added @izquierdo. Thanks! --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index c6bb56f3c..67d1ea398 100644 --- a/AUTHORS +++ b/AUTHORS @@ -32,6 +32,7 @@ Michele Lazzeri Camille Harang Paul Oswald Sean C. Farley +Daniel Izquierdo THANKS TO: From b7c06dd8e37fa88ec055831b650967d76e1b78af Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 21 Feb 2012 14:53:54 +0000 Subject: [PATCH 27/44] Update djangorestframework/views.py --- djangorestframework/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 3e8bf3566..bcdb50391 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -181,7 +181,7 @@ 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. """ - return request + pass def final(self, request, response, *args, **kargs): """ From ca9465f11e3310b7b1e18cd893e96b69963f68c9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 21 Feb 2012 20:47:55 +0000 Subject: [PATCH 28/44] reverse gets it's own module --- djangorestframework/compat.py | 8 ++++++++ djangorestframework/resources.py | 3 ++- djangorestframework/reverse.py | 23 +++++++++++++++++++++++ djangorestframework/tests/reverse.py | 25 +++++++++++++++---------- djangorestframework/utils/__init__.py | 20 +------------------- docs/howto/reverse.rst | 18 +++++------------- docs/library/reverse.rst | 5 +++++ docs/library/utils.rst | 5 ----- examples/blogpost/resources.py | 2 +- examples/blogpost/tests.py | 2 +- examples/mixin/urls.py | 2 +- examples/objectstore/views.py | 2 +- examples/permissionsexample/views.py | 2 +- examples/pygments_api/views.py | 2 +- examples/resourceexample/views.py | 2 +- examples/sandbox/views.py | 2 +- 16 files changed, 67 insertions(+), 56 deletions(-) create mode 100644 djangorestframework/reverse.py create mode 100644 docs/library/reverse.rst delete mode 100644 docs/library/utils.rst diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index 7690316c9..b818b4462 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/resources.py b/djangorestframework/resources.py index 5e126defe..2b84ee09d 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -3,8 +3,9 @@ from django.core.urlresolvers import get_urlconf, get_resolver, NoReverseMatch from django.db import models from djangorestframework.response import ErrorResponse +from djangorestframework.reverse import reverse from djangorestframework.serializer import Serializer, _SkipField -from djangorestframework.utils import as_tuple, reverse +from djangorestframework.utils import as_tuple class BaseResource(Serializer): diff --git a/djangorestframework/reverse.py b/djangorestframework/reverse.py new file mode 100644 index 000000000..ad06f9664 --- /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/tests/reverse.py b/djangorestframework/tests/reverse.py index 3ac04d8c0..9c0aee796 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -2,27 +2,32 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.utils import simplejson as json -from djangorestframework.utils import reverse +from djangorestframework.renderers import JSONRenderer +from djangorestframework.reverse import reverse from djangorestframework.views import View -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 reverse('another', request) + return 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/utils/__init__.py b/djangorestframework/utils/__init__.py index fc8bae924..537d42523 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -1,7 +1,7 @@ import django from django.utils.encoding import smart_unicode from django.utils.xmlutils import SimplerXMLGenerator -from django.core.urlresolvers import resolve, reverse as django_reverse +from django.core.urlresolvers import resolve from django.conf import settings from djangorestframework.compat import StringIO @@ -174,21 +174,3 @@ 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/docs/howto/reverse.rst b/docs/howto/reverse.rst index e4efbbcab..73b8fa4df 100644 --- a/docs/howto/reverse.rst +++ b/docs/howto/reverse.rst @@ -1,12 +1,6 @@ Returning URIs from your Web APIs ================================= - "The central feature that distinguishes the REST architectural style from - other network-based styles is its emphasis on a uniform interface between - components." - - -- Roy Fielding, Architectural Styles and the Design of Network-based Software Architectures - 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: @@ -23,9 +17,9 @@ There's no requirement for you to use them, but if you do then the self-describi reverse(viewname, request, ...) ------------------------------- -The :py:func:`~utils.reverse` function has the same behavior as :py:func:`django.core.urlresolvers.reverse` [1]_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port:: +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.utils import reverse + from djangorestframework.reverse import reverse from djangorestframework.views import View class MyView(View): @@ -39,9 +33,7 @@ The :py:func:`~utils.reverse` function has the same behavior as :py:func:`django reverse_lazy(viewname, request, ...) ------------------------------------ -The :py:func:`~utils.reverse_lazy` function has the same behavior as :py:func:`django.core.urlresolvers.reverse_lazy` [2]_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port. +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. -.. rubric:: Footnotes - -.. [1] https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse -.. [2] https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse-lazy +.. _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/library/reverse.rst b/docs/library/reverse.rst new file mode 100644 index 000000000..a2c29c488 --- /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 deleted file mode 100644 index 653f24fde..000000000 --- a/docs/library/utils.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`utils` -============== - -.. automodule:: utils - :members: diff --git a/examples/blogpost/resources.py b/examples/blogpost/resources.py index d4e0594d8..d11c5615c 100644 --- a/examples/blogpost/resources.py +++ b/examples/blogpost/resources.py @@ -1,5 +1,5 @@ from djangorestframework.resources import ModelResource -from djangorestframework.utils import reverse +from djangorestframework.reverse import reverse from blogpost.models import BlogPost, Comment diff --git a/examples/blogpost/tests.py b/examples/blogpost/tests.py index 9f72e6862..23f1ac21d 100644 --- a/examples/blogpost/tests.py +++ b/examples/blogpost/tests.py @@ -5,7 +5,7 @@ from django.test import TestCase from django.utils import simplejson as json from djangorestframework.compat import RequestFactory -from djangorestframework.utils import reverse +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 1f8c93b2c..9652cfda4 100644 --- a/examples/mixin/urls.py +++ b/examples/mixin/urls.py @@ -2,7 +2,7 @@ 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.utils import reverse +from djangorestframework.reverse import reverse from django.conf.urls.defaults import patterns, url diff --git a/examples/objectstore/views.py b/examples/objectstore/views.py index 61d51b1be..dd7114820 100644 --- a/examples/objectstore/views.py +++ b/examples/objectstore/views.py @@ -1,6 +1,6 @@ from django.conf import settings -from djangorestframework.utils import reverse +from djangorestframework.reverse import reverse from djangorestframework.views import View from djangorestframework.response import Response from djangorestframework import status diff --git a/examples/permissionsexample/views.py b/examples/permissionsexample/views.py index 83ef0fd9a..b0c1f02ef 100644 --- a/examples/permissionsexample/views.py +++ b/examples/permissionsexample/views.py @@ -1,6 +1,6 @@ from djangorestframework.views import View from djangorestframework.permissions import PerUserThrottling, IsAuthenticated -from djangorestframework.utils import reverse +from djangorestframework.reverse import reverse class PermissionsExampleView(View): diff --git a/examples/pygments_api/views.py b/examples/pygments_api/views.py index 01710bd50..b53966813 100644 --- a/examples/pygments_api/views.py +++ b/examples/pygments_api/views.py @@ -4,7 +4,7 @@ from django.conf import settings from djangorestframework.resources import FormResource from djangorestframework.response import Response from djangorestframework.renderers import BaseRenderer -from djangorestframework.utils import reverse +from djangorestframework.reverse import reverse from djangorestframework.views import View from djangorestframework import status diff --git a/examples/resourceexample/views.py b/examples/resourceexample/views.py index 2f623c39a..1b5b8e9c3 100644 --- a/examples/resourceexample/views.py +++ b/examples/resourceexample/views.py @@ -1,4 +1,4 @@ -from djangorestframework.utils import reverse +from djangorestframework.reverse import reverse from djangorestframework.views import View from djangorestframework.response import Response from djangorestframework import status diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py index 3872b5037..78e00f463 100644 --- a/examples/sandbox/views.py +++ b/examples/sandbox/views.py @@ -1,6 +1,6 @@ """The root view for the examples provided with Django REST framework""" -from djangorestframework.utils import reverse +from djangorestframework.reverse import reverse from djangorestframework.views import View From 9277f438cb85e8205cfe0149142d2f2b4d11a31c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 21 Feb 2012 22:09:05 +0000 Subject: [PATCH 29/44] Fix YAML parser bug --- djangorestframework/parsers.py | 44 ++++++++++++++++---------------- djangorestframework/renderers.py | 35 ++++++++++++------------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index c8a014aef..099abe9a0 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -92,28 +92,25 @@ class JSONParser(BaseParser): {'detail': 'JSON parse error - %s' % unicode(exc)}) -if yaml: - class YAMLParser(BaseParser): +class YAMLParser(BaseParser): + """ + Parses YAML-serialized data. + """ + + media_type = 'application/yaml' + + def parse(self, stream): """ - Parses YAML-serialized data. + Returns a 2-tuple of `(data, files)`. + + `data` will be an object which is the parsed content of the response. + `files` will always be `None`. """ - - media_type = 'application/yaml' - - def parse(self, stream): - """ - Returns a 2-tuple of `(data, files)`. - - `data` will be an object which is the parsed content of the response. - `files` will always be `None`. - """ - try: - return (yaml.safe_load(stream), None) - except ValueError, exc: - raise ErrorResponse(status.HTTP_400_BAD_REQUEST, - {'detail': 'YAML parse error - %s' % unicode(exc)}) -else: - YAMLParser = None + try: + return (yaml.safe_load(stream), None) + except (ValueError, yaml.parser.ParserError), exc: + content = {'detail': 'YAML parse error - %s' % unicode(exc)} + raise ErrorResponse(status.HTTP_400_BAD_REQUEST, content) class PlainTextParser(BaseParser): @@ -248,5 +245,8 @@ DEFAULT_PARSERS = ( XMLParser ) -if YAMLParser: - DEFAULT_PARSERS += (YAMLParser,) +if yaml: + DEFAULT_PARSERS += (YAMLParser, ) +else: + YAMLParser = None + diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index de9a01ec4..71c381f7e 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -152,25 +152,22 @@ class XMLRenderer(BaseRenderer): return dict2xml(obj) -if yaml: - class YAMLRenderer(BaseRenderer): +class YAMLRenderer(BaseRenderer): + """ + Renderer which serializes to YAML. + """ + + media_type = 'application/yaml' + format = 'yaml' + + def render(self, obj=None, media_type=None): """ - Renderer which serializes to YAML. + Renders *obj* into serialized YAML. """ + if obj is None: + return '' - media_type = 'application/yaml' - format = 'yaml' - - def render(self, obj=None, media_type=None): - """ - Renders *obj* into serialized YAML. - """ - if obj is None: - return '' - - return yaml.safe_dump(obj) -else: - YAMLRenderer = None + return yaml.safe_dump(obj) class TemplateRenderer(BaseRenderer): @@ -409,5 +406,7 @@ DEFAULT_RENDERERS = ( XMLRenderer ) -if YAMLRenderer: - DEFAULT_RENDERERS += (YAMLRenderer,) +if yaml: + DEFAULT_RENDERERS += (YAMLRenderer, ) +else: + YAMLRenderer = None From b074754b54adf172cd2d102e2a326a7f322cf2ef Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 21 Feb 2012 22:13:41 +0000 Subject: [PATCH 30/44] Remove ADMIN_MEDIA_PREFIX everywhere --- djangorestframework/renderers.py | 1 - djangorestframework/runtests/settings.py | 5 ---- djangorestframework/utils/__init__.py | 37 ------------------------ djangorestframework/utils/staticviews.py | 1 - 4 files changed, 44 deletions(-) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 71c381f7e..75957c6dd 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -348,7 +348,6 @@ class DocumentingTemplateRenderer(BaseRenderer): 'logout_url': logout_url, 'FORMAT_PARAM': self._FORMAT_QUERY_PARAM, 'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None), - 'ADMIN_MEDIA_PREFIX': getattr(settings, 'ADMIN_MEDIA_PREFIX', None), }) ret = template.render(context) diff --git a/djangorestframework/runtests/settings.py b/djangorestframework/runtests/settings.py index f54a554b3..7cb3e27b2 100644 --- a/djangorestframework/runtests/settings.py +++ b/djangorestframework/runtests/settings.py @@ -53,11 +53,6 @@ MEDIA_ROOT = '' # Examples: "http://media.lawrence.com", "http://example.com/media/" MEDIA_URL = '' -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/media/' - # Make this unique, and don't share it with anybody. SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy' diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index 537d42523..ca16b2617 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -9,11 +9,6 @@ from djangorestframework.compat import StringIO import re import xml.etree.ElementTree as ET - -#def admin_media_prefix(request): -# """Adds the ADMIN_MEDIA_PREFIX to the request context.""" -# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX} - from mediatypes import media_type_matches, is_form_media_type from mediatypes import add_media_type_param, get_media_type_params, order_by_precedence @@ -49,38 +44,6 @@ def url_resolves(url): return True -# From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml -#class object_dict(dict): -# """object view of dict, you can -# >>> a = object_dict() -# >>> a.fish = 'fish' -# >>> a['fish'] -# 'fish' -# >>> a['water'] = 'water' -# >>> a.water -# 'water' -# >>> a.test = {'value': 1} -# >>> a.test2 = object_dict({'name': 'test2', 'value': 2}) -# >>> a.test, a.test2.name, a.test2.value -# (1, 'test2', 2) -# """ -# def __init__(self, initd=None): -# if initd is None: -# initd = {} -# dict.__init__(self, initd) -# -# def __getattr__(self, item): -# d = self.__getitem__(item) -# # if value is the only key in object, you can omit it -# if isinstance(d, dict) and 'value' in d and len(d) == 1: -# return d['value'] -# else: -# return d -# -# def __setattr__(self, item, value): -# self.__setitem__(item, value) - - # From xml2dict class XML2Dict(object): diff --git a/djangorestframework/utils/staticviews.py b/djangorestframework/utils/staticviews.py index 7cbc0b9b8..101df4e96 100644 --- a/djangorestframework/utils/staticviews.py +++ b/djangorestframework/utils/staticviews.py @@ -53,7 +53,6 @@ def api_login(request, template_name='djangorestframework/login.html', redirect_field_name: redirect_to, #'site': current_site, #'site_name': current_site.name, - 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX, }, context_instance=RequestContext(request)) From 66eabe8bd1a539f92c3d677565d69edc29a1721b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 21 Feb 2012 22:50:41 +0000 Subject: [PATCH 31/44] Remove staticviews. Use standard login/logout --- djangorestframework/renderers.py | 4 +--- .../templates/djangorestframework/base.html | 11 +++++++++-- .../templates/djangorestframework/login.html | 2 +- djangorestframework/tests/accept.py | 15 +++++++++++++-- djangorestframework/tests/oauthentication.py | 2 +- djangorestframework/tests/renderers.py | 3 ++- djangorestframework/tests/views.py | 12 ++++++------ djangorestframework/urls.py | 11 +++++++---- docs/howto/setup.rst | 17 +++++++++-------- docs/index.rst | 6 ++++++ examples/urls.py | 5 ++--- 11 files changed, 57 insertions(+), 31 deletions(-) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 75957c6dd..d9aa4028e 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -335,7 +335,7 @@ class DocumentingTemplateRenderer(BaseRenderer): context = RequestContext(self.view.request, { 'content': content, 'view': self.view, - 'request': self.view.request, # TODO: remove + 'request': self.view.request, 'response': self.view.response, 'description': description, 'name': name, @@ -344,8 +344,6 @@ class DocumentingTemplateRenderer(BaseRenderer): 'available_formats': self.view._rendered_formats, 'put_form': put_form_instance, 'post_form': post_form_instance, - 'login_url': login_url, - 'logout_url': logout_url, 'FORMAT_PARAM': self._FORMAT_QUERY_PARAM, 'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None), }) diff --git a/djangorestframework/templates/djangorestframework/base.html b/djangorestframework/templates/djangorestframework/base.html index 1d4ae92ae..00ecf8c3c 100644 --- a/djangorestframework/templates/djangorestframework/base.html +++ b/djangorestframework/templates/djangorestframework/base.html @@ -20,8 +20,15 @@

{% block branding %}Django REST framework v {{ version }}{% endblock %}

- {% if user.is_active %}Welcome, {{ user }}.{% if logout_url %} Log out{% endif %}{% else %}Anonymous {% if login_url %}Log in{% endif %}{% endif %} - {% block userlinks %}{% endblock %} + {% block userlinks %} + {% if user.is_active %} + Welcome, {{ user }}. + Log out + {% else %} + Anonymous + Log in + {% endif %} + {% endblock %}
{% block nav-global %}{% endblock %} diff --git a/djangorestframework/templates/djangorestframework/login.html b/djangorestframework/templates/djangorestframework/login.html index 07929f0c7..248744dff 100644 --- a/djangorestframework/templates/djangorestframework/login.html +++ b/djangorestframework/templates/djangorestframework/login.html @@ -17,7 +17,7 @@
-
+ {% csrf_token %}
{{ form.username }} diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py index d66f6fb03..21aba589b 100644 --- a/djangorestframework/tests/accept.py +++ b/djangorestframework/tests/accept.py @@ -1,3 +1,4 @@ +from django.conf.urls.defaults import patterns, url, include from django.test import TestCase from djangorestframework.compat import RequestFactory from djangorestframework.views import View @@ -13,9 +14,19 @@ SAFARI_5_0_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-ca) AppleWebKit/5 OPERA_11_0_MSIE_USER_AGENT = 'Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; pl) Opera 11.00' OPERA_11_0_OPERA_USER_AGENT = 'Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00' + +urlpatterns = patterns('', + url(r'^api', include('djangorestframework.urls', namespace='djangorestframework')) +) + + class UserAgentMungingTest(TestCase): - """We need to fake up the accept headers when we deal with MSIE. Blergh. - http://www.gethifi.com/blog/browser-rest-http-accept-headers""" + """ + We need to fake up the accept headers when we deal with MSIE. Blergh. + http://www.gethifi.com/blog/browser-rest-http-accept-headers + """ + + urls = 'djangorestframework.tests.accept' def setUp(self): diff --git a/djangorestframework/tests/oauthentication.py b/djangorestframework/tests/oauthentication.py index b4bcf2fa8..29f2c44ea 100644 --- a/djangorestframework/tests/oauthentication.py +++ b/djangorestframework/tests/oauthentication.py @@ -27,7 +27,7 @@ else: urlpatterns = patterns('', url(r'^$', oauth_required(ClientView.as_view())), url(r'^oauth/', include('oauth_provider.urls')), - url(r'^accounts/login/$', 'djangorestframework.utils.staticviews.api_login'), + url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')), ) diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index 9a02d0a9a..3ed5ab28f 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -1,6 +1,6 @@ import re -from django.conf.urls.defaults import patterns, url +from django.conf.urls.defaults import patterns, url, include from django.test import TestCase from djangorestframework import status @@ -73,6 +73,7 @@ urlpatterns = patterns('', url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderers=[JSONPRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), + url(r'^api', include('djangorestframework.urls', namespace='djangorestframework')) ) diff --git a/djangorestframework/tests/views.py b/djangorestframework/tests/views.py index d41890878..418b4b164 100644 --- a/djangorestframework/tests/views.py +++ b/djangorestframework/tests/views.py @@ -1,4 +1,5 @@ -from django.conf.urls.defaults import patterns, url +from django.core.urlresolvers import reverse +from django.conf.urls.defaults import patterns, url, include from django.http import HttpResponse from django.test import TestCase from django.test import Client @@ -45,14 +46,13 @@ class MockResource(ModelResource): model = MockResourceModel fields = ('foo', 'bar', 'baz') -urlpatterns = patterns('djangorestframework.utils.staticviews', - url(r'^accounts/login$', 'api_login'), - url(r'^accounts/logout$', 'api_logout'), +urlpatterns = patterns('', url(r'^mock/$', MockView.as_view()), url(r'^mock/final/$', MockViewFinal.as_view()), url(r'^resourcemock/$', ResourceMockView.as_view()), url(r'^model/$', ListOrCreateModelView.as_view(resource=MockResource)), url(r'^model/(?P[^/]+)/$', InstanceModelView.as_view(resource=MockResource)), + url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')), ) class BaseViewTests(TestCase): @@ -123,13 +123,13 @@ class ExtraViewsTests(TestCase): def test_login_view(self): """Ensure the login view exists""" - response = self.client.get('/accounts/login') + response = self.client.get(reverse('djangorestframework:login')) self.assertEqual(response.status_code, 200) self.assertEqual(response['Content-Type'].split(';')[0], 'text/html') def test_logout_view(self): """Ensure the logout view exists""" - response = self.client.get('/accounts/logout') + response = self.client.get(reverse('djangorestframework:logout')) self.assertEqual(response.status_code, 200) self.assertEqual(response['Content-Type'].split(';')[0], 'text/html') diff --git a/djangorestframework/urls.py b/djangorestframework/urls.py index 5c797bcdb..3fa813eae 100644 --- a/djangorestframework/urls.py +++ b/djangorestframework/urls.py @@ -1,6 +1,9 @@ -from django.conf.urls.defaults import patterns +from django.conf.urls.defaults import patterns, url -urlpatterns = patterns('djangorestframework.utils.staticviews', - (r'^accounts/login/$', 'api_login'), - (r'^accounts/logout/$', 'api_logout'), + +template_name = {'template_name': 'djangorestframework/login.html'} + +urlpatterns = patterns('django.contrib.auth.views', + url(r'^login/$', 'login', template_name, name='login'), + url(r'^logout/$', 'logout', template_name, name='logout'), ) diff --git a/docs/howto/setup.rst b/docs/howto/setup.rst index 0af1449cb..f01270601 100644 --- a/docs/howto/setup.rst +++ b/docs/howto/setup.rst @@ -53,16 +53,17 @@ YAML support is optional, and requires `PyYAML`_. Login / Logout -------------- -Django REST framework includes login and logout views that are useful if -you're using the self-documenting API:: +Django REST framework includes login and logout views that are needed if +you're using the self-documenting API. - from django.conf.urls.defaults import patterns +Make sure you include the following in your `urlconf`:: - urlpatterns = patterns('djangorestframework.views', - # Add your resources here - (r'^accounts/login/$', 'api_login'), - (r'^accounts/logout/$', 'api_logout'), - ) + from django.conf.urls.defaults import patterns, url + + urlpatterns = patterns('', + ... + url(r'^restframework', include('djangorestframework.urls', namespace='djangorestframework')) + ) .. _django.contrib.staticfiles: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/ .. _django-staticfiles: http://pypi.python.org/pypi/django-staticfiles/ diff --git a/docs/index.rst b/docs/index.rst index b969c4a38..a6745fca5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -64,6 +64,12 @@ To add Django REST framework to a Django project: * Ensure that the ``djangorestframework`` directory is on your ``PYTHONPATH``. * Add ``djangorestframework`` to your ``INSTALLED_APPS``. +* Add the following to your URLconf. (To include the REST framework Login/Logout views.):: + + urlpatterns = patterns('', + ... + url(r'^restframework', include('djangorestframework.urls', namespace='djangorestframework')) + ) For more information on settings take a look at the :ref:`setup` section. diff --git a/examples/urls.py b/examples/urls.py index 33297b550..fda7942fb 100644 --- a/examples/urls.py +++ b/examples/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import patterns, include +from django.conf.urls.defaults import patterns, include, url from sandbox.views import Sandbox try: from django.contrib.staticfiles.urls import staticfiles_urlpatterns @@ -15,8 +15,7 @@ urlpatterns = patterns('', (r'^pygments/', include('pygments_api.urls')), (r'^blog-post/', include('blogpost.urls')), (r'^permissions-example/', include('permissionsexample.urls')), - - (r'^', include('djangorestframework.urls')), + url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')), ) urlpatterns += staticfiles_urlpatterns() From 44b56ed059fa2963cf13ecdd4796b3ac45e800b6 Mon Sep 17 00:00:00 2001 From: Can Yavuz Date: Wed, 22 Feb 2012 11:17:37 +0100 Subject: [PATCH 32/44] let the XML parser fail gracefully on malformed XML --- djangorestframework/compat.py | 6 ++++++ djangorestframework/parsers.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index b818b4462..e81b428f3 100644 --- a/djangorestframework/compat.py +++ b/djangorestframework/compat.py @@ -465,3 +465,9 @@ except: from django.core.urlresolvers import reverse from django.utils.functional import lazy reverse_lazy = lazy(reverse, str) + +# xml.etree.parse only throws ParseError for python >= 2.7 +try: + from xml.etree import ParseError as ETParseError +except ImportError: # python < 2.7 + ETParseError = None diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index 099abe9a0..7481666b3 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -20,6 +20,8 @@ from djangorestframework.compat import yaml from djangorestframework.response import ErrorResponse from djangorestframework.utils.mediatypes import media_type_matches from xml.etree import ElementTree as ET +from djangorestframework.compat import ETParseError +from xml.parsers.expat import ExpatError import datetime import decimal @@ -185,7 +187,11 @@ class XMLParser(BaseParser): `data` will simply be a string representing the body of the request. `files` will always be `None`. """ - tree = ET.parse(stream) + try: + tree = ET.parse(stream) + except (ExpatError, ETParseError, ValueError), exc: + content = {'detail': 'XML parse error - %s' % unicode(exc)} + raise ErrorResponse(status.HTTP_400_BAD_REQUEST, content) data = self._xml_convert(tree.getroot()) return (data, None) From b2b1a0354f64a4f4d5e5d36fa50d2d286e15918e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 22 Feb 2012 12:55:13 +0000 Subject: [PATCH 33/44] Added @tschan. Thanks! --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 67d1ea398..243ebfdf8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -33,6 +33,7 @@ Camille Harang Paul Oswald Sean C. Farley Daniel Izquierdo +Can Yavuz THANKS TO: From 8e0b9e55ecb0733369918d4562ba38ba505cdfe8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 22 Feb 2012 13:09:09 +0000 Subject: [PATCH 34/44] Properly catch MultiPartParserError during .parse() --- djangorestframework/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index 7481666b3..a19582b7c 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -167,10 +167,10 @@ class MultiPartParser(BaseParser): upload_handlers = self.view.request._get_upload_handlers() try: django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers) + return django_parser.parse() except MultiPartParserError, exc: raise ErrorResponse(status.HTTP_400_BAD_REQUEST, {'detail': 'multipart parse error - %s' % unicode(exc)}) - return django_parser.parse() class XMLParser(BaseParser): From 2b59df004a5bb7449aa4c07277ac846c330a79f7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 23 Feb 2012 08:58:10 +0000 Subject: [PATCH 35/44] reverse takes request as a kwarg for compatibility with django's reverse --- djangorestframework/compat.py | 22 +++------- djangorestframework/resources.py | 14 ++++-- djangorestframework/reverse.py | 19 ++++---- djangorestframework/tests/reverse.py | 2 +- examples/blogpost/resources.py | 8 +++- examples/mixin/urls.py | 7 +-- examples/modelresourceexample/models.py | 3 +- examples/modelresourceexample/resources.py | 1 + examples/modelresourceexample/urls.py | 2 +- examples/objectstore/views.py | 51 +++++++++++++++------- examples/pygments_api/views.py | 10 +++-- examples/resourceexample/views.py | 2 +- examples/sandbox/views.py | 14 +++--- 13 files changed, 88 insertions(+), 67 deletions(-) diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index e81b428f3..83d26f1ff 100644 --- a/djangorestframework/compat.py +++ b/djangorestframework/compat.py @@ -214,18 +214,15 @@ else: REASON_NO_CSRF_COOKIE = "CSRF cookie not set." REASON_BAD_TOKEN = "CSRF token missing or incorrect." - def _get_failure_view(): """ Returns the view to be used for CSRF rejections """ return get_callable(settings.CSRF_FAILURE_VIEW) - def _get_new_csrf_key(): return hashlib.md5("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest() - def get_token(request): """ Returns the the CSRF token required for a POST form. The token is an @@ -239,7 +236,6 @@ else: request.META["CSRF_COOKIE_USED"] = True return request.META.get("CSRF_COOKIE", None) - def _sanitize_token(token): # Allow only alphanum, and ensure we return a 'str' for the sake of the post # processing middleware. @@ -432,12 +428,13 @@ try: except ImportError: yaml = None + import unittest try: import unittest.skip -except ImportError: # python < 2.7 +except ImportError: # python < 2.7 from unittest import TestCase - import functools + import functools def skip(reason): # Pasted from py27/lib/unittest/case.py @@ -448,26 +445,19 @@ except ImportError: # python < 2.7 if not (isinstance(test_item, type) and issubclass(test_item, TestCase)): @functools.wraps(test_item) def skip_wrapper(*args, **kwargs): - pass + pass test_item = skip_wrapper test_item.__unittest_skip__ = True test_item.__unittest_skip_why__ = reason return test_item 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) # xml.etree.parse only throws ParseError for python >= 2.7 try: from xml.etree import ParseError as ETParseError -except ImportError: # python < 2.7 +except ImportError: # python < 2.7 ETParseError = None diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py index 2b84ee09d..71c4d2b95 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -10,7 +10,8 @@ from djangorestframework.utils import as_tuple class BaseResource(Serializer): """ - Base class for all Resource classes, which simply defines the interface they provide. + Base class for all Resource classes, which simply defines the interface + they provide. """ fields = None include = None @@ -19,11 +20,13 @@ class BaseResource(Serializer): def __init__(self, view=None, depth=None, stack=[], **kwargs): super(BaseResource, self).__init__(depth, stack, **kwargs) self.view = view + self.request = view.request def validate_request(self, data, files=None): """ Given the request content return the cleaned, validated content. - Typically raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure. + Typically raises a :exc:`response.ErrorResponse` with status code 400 + (Bad Request) on failure. """ return data @@ -37,7 +40,8 @@ class BaseResource(Serializer): class Resource(BaseResource): """ A Resource determines how a python object maps to some serializable data. - Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets. + Objects that a resource can act on include plain Python object instances, + Django Models, and Django QuerySets. """ # The model attribute refers to the Django Model which this Resource maps to. @@ -355,7 +359,9 @@ class ModelResource(FormResource): instance_attrs[param] = attr try: - return reverse(self.view_callable[0], self.view.request, kwargs=instance_attrs) + return reverse(self.view_callable[0], + kwargs=instance_attrs, + request=self.view.request) except NoReverseMatch: pass raise _SkipField diff --git a/djangorestframework/reverse.py b/djangorestframework/reverse.py index ad06f9664..056431576 100644 --- a/djangorestframework/reverse.py +++ b/djangorestframework/reverse.py @@ -2,22 +2,19 @@ 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 +from django.utils.functional import lazy def reverse(viewname, request, *args, **kwargs): """ - Do the same as `django.core.urlresolvers.reverse` but using - *request* to build a fully qualified URL. + Same as `django.core.urlresolvers.reverse`, but optionally takes a request + and returns a fully qualified URL, using the request to get the base URL. """ + request = kwargs.pop('request', None) url = django_reverse(viewname, *args, **kwargs) - return request.build_absolute_uri(url) + if request: + return request.build_absolute_uri(url) + return 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) +reverse_lazy = lazy(reverse, str) diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py index 9c0aee796..3dd13ef83 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -15,7 +15,7 @@ class MyView(View): renderers = (JSONRenderer, ) def get(self, request): - return reverse('myview', request) + return reverse('myview', request=request) urlpatterns = patterns('', url(r'^myview$', MyView.as_view(), name='myview'), diff --git a/examples/blogpost/resources.py b/examples/blogpost/resources.py index d11c5615c..ac00a7342 100644 --- a/examples/blogpost/resources.py +++ b/examples/blogpost/resources.py @@ -12,7 +12,9 @@ class BlogPostResource(ModelResource): ordering = ('-created',) def comments(self, instance): - return reverse('comments', request, kwargs={'blogpost': instance.key}) + return reverse('comments', + kwargs={'blogpost': instance.key}, + request=self.request) class CommentResource(ModelResource): @@ -24,4 +26,6 @@ class CommentResource(ModelResource): ordering = ('-created',) def blogpost(self, instance): - return reverse('blog-post', request, kwargs={'key': instance.blogpost.key}) + return reverse('blog-post', + kwargs={'key': instance.blogpost.key}, + request=self.request) diff --git a/examples/mixin/urls.py b/examples/mixin/urls.py index 9652cfda4..900c532e0 100644 --- a/examples/mixin/urls.py +++ b/examples/mixin/urls.py @@ -9,16 +9,17 @@ from django.conf.urls.defaults import patterns, url class ExampleView(ResponseMixin, View): """An example view using Django 1.3's class based views. - Uses djangorestframework's RendererMixin to provide support for multiple output formats.""" + Uses djangorestframework's RendererMixin to provide support for multiple + output formats.""" renderers = DEFAULT_RENDERERS def get(self, request): + url = reverse('mixin-view', request=request) response = Response(200, {'description': 'Some example content', - 'url': reverse('mixin-view', request)}) + 'url': url}) return self.render(response) urlpatterns = patterns('', url(r'^$', ExampleView.as_view(), name='mixin-view'), ) - diff --git a/examples/modelresourceexample/models.py b/examples/modelresourceexample/models.py index ff0179c88..11f3eae22 100644 --- a/examples/modelresourceexample/models.py +++ b/examples/modelresourceexample/models.py @@ -2,6 +2,7 @@ from django.db import models MAX_INSTANCES = 10 + class MyModel(models.Model): foo = models.BooleanField() bar = models.IntegerField(help_text='Must be an integer.') @@ -15,5 +16,3 @@ class MyModel(models.Model): super(MyModel, self).save(*args, **kwargs) while MyModel.objects.all().count() > MAX_INSTANCES: MyModel.objects.all().order_by('-created')[0].delete() - - diff --git a/examples/modelresourceexample/resources.py b/examples/modelresourceexample/resources.py index 634ea6b30..05090f8f3 100644 --- a/examples/modelresourceexample/resources.py +++ b/examples/modelresourceexample/resources.py @@ -1,6 +1,7 @@ from djangorestframework.resources import ModelResource from modelresourceexample.models import MyModel + class MyModelResource(ModelResource): model = MyModel fields = ('foo', 'bar', 'baz', 'url') diff --git a/examples/modelresourceexample/urls.py b/examples/modelresourceexample/urls.py index b6a16542a..c680dc655 100644 --- a/examples/modelresourceexample/urls.py +++ b/examples/modelresourceexample/urls.py @@ -4,5 +4,5 @@ from modelresourceexample.resources import MyModelResource urlpatterns = patterns('', url(r'^$', ListOrCreateModelView.as_view(resource=MyModelResource), name='model-resource-root'), - url(r'^(?P[0-9]+)/$', InstanceModelView.as_view(resource=MyModelResource)), + url(r'^(?P[0-9]+)/$', InstanceModelView.as_view(resource=MyModelResource), name='model-resource-instance'), ) diff --git a/examples/objectstore/views.py b/examples/objectstore/views.py index dd7114820..880bd3fcb 100644 --- a/examples/objectstore/views.py +++ b/examples/objectstore/views.py @@ -28,6 +28,20 @@ def remove_oldest_files(dir, max_files): [os.remove(path) for path in ctime_sorted_paths[max_files:]] +def get_filename(key): + """ + Given a stored object's key returns the file's path. + """ + return os.path.join(OBJECT_STORE_DIR, key) + + +def get_file_url(key, request): + """ + Given a stored object's key returns the URL for the object. + """ + return reverse('stored-object', kwargs={'key': key}, request=request) + + class ObjectStoreRoot(View): """ Root of the Object Store API. @@ -38,20 +52,24 @@ class ObjectStoreRoot(View): """ Return a list of all the stored object URLs. (Ordered by creation time, newest first) """ - 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], key=operator.itemgetter(1), reverse=True)] - return [reverse('stored-object', request, kwargs={'key':key}) for key in ctime_sorted_basenames] + return [get_file_url(key, request) for key in ctime_sorted_basenames] def post(self, request): """ Create a new stored object, with a unique key. """ key = str(uuid.uuid1()) - pathname = os.path.join(OBJECT_STORE_DIR, key) - pickle.dump(self.CONTENT, open(pathname, 'wb')) + filename = get_filename(key) + pickle.dump(self.CONTENT, open(filename, 'wb')) + remove_oldest_files(OBJECT_STORE_DIR, MAX_FILES) - return Response(status.HTTP_201_CREATED, self.CONTENT, {'Location': reverse('stored-object', request, kwargs={'key':key})}) + url = get_file_url(key, request) + return Response(status.HTTP_201_CREATED, self.CONTENT, {'Location': url}) class StoredObject(View): @@ -59,29 +77,30 @@ class StoredObject(View): Represents a stored object. The object may be any picklable content. """ - def get(self, request, key): """ - Return a stored object, by unpickling the contents of a locally stored file. + Return a stored object, by unpickling the contents of a locally + stored file. """ - pathname = os.path.join(OBJECT_STORE_DIR, key) - if not os.path.exists(pathname): + filename = get_filename(key) + if not os.path.exists(filename): return Response(status.HTTP_404_NOT_FOUND) - return pickle.load(open(pathname, 'rb')) + return pickle.load(open(filename, 'rb')) def put(self, request, key): """ - Update/create a stored object, by pickling the request content to a locally stored file. + Update/create a stored object, by pickling the request content to a + locally stored file. """ - pathname = os.path.join(OBJECT_STORE_DIR, key) - pickle.dump(self.CONTENT, open(pathname, 'wb')) + filename = get_filename(key) + pickle.dump(self.CONTENT, open(filename, 'wb')) return self.CONTENT def delete(self, request, key): """ Delete a stored object, by removing it's pickled file. """ - pathname = os.path.join(OBJECT_STORE_DIR, key) - if not os.path.exists(pathname): + filename = get_filename(key) + if not os.path.exists(filename): return Response(status.HTTP_404_NOT_FOUND) - os.remove(pathname) + os.remove(filename) diff --git a/examples/pygments_api/views.py b/examples/pygments_api/views.py index b53966813..3dd55115c 100644 --- a/examples/pygments_api/views.py +++ b/examples/pygments_api/views.py @@ -30,9 +30,13 @@ def list_dir_sorted_by_ctime(dir): """ Return a list of files sorted by creation time """ - filepaths = [os.path.join(dir, file) for file in os.listdir(dir) if not file.startswith('.')] - return [item[0] for item in sorted( [(path, os.path.getctime(path)) for path in filepaths], - key=operator.itemgetter(1), reverse=False) ] + filepaths = [os.path.join(dir, file) + for file in os.listdir(dir) + if not file.startswith('.')] + ctimes = [(path, os.path.getctime(path)) for path in filepaths] + ctimes = sorted(ctimes, key=operator.itemgetter(1), reverse=False) + return [filepath for filepath, ctime in ctimes] + def remove_oldest_files(dir, max_files): """ diff --git a/examples/resourceexample/views.py b/examples/resourceexample/views.py index 1b5b8e9c3..fcd0b2737 100644 --- a/examples/resourceexample/views.py +++ b/examples/resourceexample/views.py @@ -15,7 +15,7 @@ class ExampleView(View): """ Handle GET requests, returning a list of URLs pointing to 3 other views. """ - return {"Some other resources": [reverse('another-example', request, kwargs={'num':num}) for num in range(3)]} + return {"Some other resources": [reverse('another-example', kwargs={'num':num}, request=request) for num in range(3)]} class AnotherExampleView(View): diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py index 78e00f463..8e3b3a10a 100644 --- a/examples/sandbox/views.py +++ b/examples/sandbox/views.py @@ -27,11 +27,11 @@ class Sandbox(View): Please feel free to browse, create, edit and delete the resources in these examples.""" def get(self, request): - return [{'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)} + return [{'name': 'Simple Resource example', 'url': reverse('example-resource', request=request)}, + {'name': 'Simple ModelResource example', 'url': reverse('model-resource-root', request=request)}, + {'name': 'Simple Mixin-only example', 'url': reverse('mixin-view', request=request)}, + {'name': 'Object store API', 'url': reverse('object-store-root', request=request)}, + {'name': 'Code highlighting API', 'url': reverse('pygments-root', request=request)}, + {'name': 'Blog posts API', 'url': reverse('blog-posts-root', request=request)}, + {'name': 'Permissions example', 'url': reverse('permissions-example', request=request)} ] From e15494a172f19d9388c0888fb566907aad95de0c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 23 Feb 2012 09:21:01 +0000 Subject: [PATCH 36/44] Remove InstanceMixin auto-url magicks. --- djangorestframework/mixins.py | 31 ++---------- djangorestframework/resources.py | 55 ++-------------------- djangorestframework/reverse.py | 2 +- djangorestframework/tests/modelviews.py | 5 +- djangorestframework/views.py | 3 +- examples/blogpost/models.py | 3 +- examples/blogpost/resources.py | 5 ++ examples/modelresourceexample/resources.py | 6 +++ examples/modelresourceexample/urls.py | 7 ++- examples/resourceexample/views.py | 6 ++- 10 files changed, 35 insertions(+), 88 deletions(-) diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index f38277eb4..6c8f8179d 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -25,14 +25,13 @@ __all__ = ( 'ResponseMixin', 'AuthMixin', 'ResourceMixin', - # Reverse URL lookup behavior - 'InstanceMixin', # Model behavior mixins 'ReadModelMixin', 'CreateModelMixin', 'UpdateModelMixin', 'DeleteModelMixin', - 'ListModelMixin' + 'ListModelMixin', + 'PaginatorMixin' ) @@ -444,30 +443,6 @@ class ResourceMixin(object): else: return None -########## - - -class InstanceMixin(object): - """ - `Mixin` class that is used to identify a `View` class as being the canonical identifier - for the resources it is mapped to. - """ - - @classmethod - def as_view(cls, **initkwargs): - """ - Store the callable object on the resource class that has been associated with this view. - """ - view = super(InstanceMixin, cls).as_view(**initkwargs) - resource = getattr(cls(**initkwargs), 'resource', None) - if resource: - # We do a little dance when we store the view callable... - # we need to store it wrapped in a 1-tuple, so that inspect will treat it - # as a function when we later look it up (rather than turning it into a method). - # This makes sure our URL reversing works ok. - resource.view_callable = (view,) - return view - ########## Model Mixins ########## @@ -599,7 +574,7 @@ class CreateModelMixin(ModelMixin): manager.through(**data).save() headers = {} - if hasattr(instance, 'get_absolute_url'): + if hasattr(self.resource, 'url'): headers['Location'] = self.resource(self).url(instance) return Response(status.HTTP_201_CREATED, instance, headers) diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py index 71c4d2b95..f170eb45a 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -1,10 +1,7 @@ from django import forms -from django.core.urlresolvers import get_urlconf, get_resolver, NoReverseMatch -from django.db import models from djangorestframework.response import ErrorResponse -from djangorestframework.reverse import reverse -from djangorestframework.serializer import Serializer, _SkipField +from djangorestframework.serializer import Serializer from djangorestframework.utils import as_tuple @@ -20,7 +17,7 @@ class BaseResource(Serializer): def __init__(self, view=None, depth=None, stack=[], **kwargs): super(BaseResource, self).__init__(depth, stack, **kwargs) self.view = view - self.request = view.request + self.request = getattr(view, 'request', None) def validate_request(self, data, files=None): """ @@ -224,9 +221,6 @@ class ModelResource(FormResource): Also provides a :meth:`get_bound_form` method which may be used by some renderers. """ - # Auto-register new ModelResource classes into _model_to_resource - #__metaclass__ = _RegisterModelResource - form = None """ The form class that should be used for request validation. @@ -260,7 +254,7 @@ class ModelResource(FormResource): The list of fields to exclude. This is only used if :attr:`fields` is not set. """ - include = ('url',) + include = () """ The list of extra fields to include. This is only used if :attr:`fields` is not set. """ @@ -323,49 +317,6 @@ class ModelResource(FormResource): return form() - def url(self, instance): - """ - Attempts to reverse resolve the url of the given model *instance* for this resource. - - Requires a ``View`` with :class:`mixins.InstanceMixin` to have been created for this resource. - - This method can be overridden if you need to set the resource url reversing explicitly. - """ - - if not hasattr(self, 'view_callable'): - raise _SkipField - - # dis does teh magicks... - urlconf = get_urlconf() - resolver = get_resolver(urlconf) - - possibilities = resolver.reverse_dict.getlist(self.view_callable[0]) - for tuple_item in possibilities: - possibility = tuple_item[0] - # pattern = tuple_item[1] - # Note: defaults = tuple_item[2] for django >= 1.3 - for result, params in possibility: - - #instance_attrs = dict([ (param, getattr(instance, param)) for param in params if hasattr(instance, param) ]) - - instance_attrs = {} - for param in params: - if not hasattr(instance, param): - continue - attr = getattr(instance, param) - if isinstance(attr, models.Model): - instance_attrs[param] = attr.pk - else: - instance_attrs[param] = attr - - try: - return reverse(self.view_callable[0], - kwargs=instance_attrs, - request=self.view.request) - except NoReverseMatch: - pass - raise _SkipField - @property def _model_fields_set(self): """ diff --git a/djangorestframework/reverse.py b/djangorestframework/reverse.py index 056431576..ba663f98f 100644 --- a/djangorestframework/reverse.py +++ b/djangorestframework/reverse.py @@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse as django_reverse from django.utils.functional import lazy -def reverse(viewname, request, *args, **kwargs): +def reverse(viewname, *args, **kwargs): """ Same as `django.core.urlresolvers.reverse`, but optionally takes a request and returns a fully qualified URL, using the request to get the base URL. diff --git a/djangorestframework/tests/modelviews.py b/djangorestframework/tests/modelviews.py index 031e65c50..ccd8513fd 100644 --- a/djangorestframework/tests/modelviews.py +++ b/djangorestframework/tests/modelviews.py @@ -1,5 +1,4 @@ from django.conf.urls.defaults import patterns, url -from django.test import TestCase from django.forms import ModelForm from django.contrib.auth.models import Group, User from djangorestframework.resources import ModelResource @@ -7,18 +6,22 @@ from djangorestframework.views import ListOrCreateModelView, InstanceModelView from djangorestframework.tests.models import CustomUser from djangorestframework.tests.testcases import TestModelsTestCase + class GroupResource(ModelResource): model = Group + class UserForm(ModelForm): class Meta: model = User exclude = ('last_login', 'date_joined') + class UserResource(ModelResource): model = User form = UserForm + class CustomUserResource(ModelResource): model = CustomUser diff --git a/djangorestframework/views.py b/djangorestframework/views.py index bcdb50391..3657fd645 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -6,7 +6,6 @@ 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 @@ -269,7 +268,7 @@ class ModelView(View): resource = resources.ModelResource -class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): +class InstanceModelView(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): """ A view which provides default operations for read/update/delete against a model instance. """ diff --git a/examples/blogpost/models.py b/examples/blogpost/models.py index d77f530d5..10732ab41 100644 --- a/examples/blogpost/models.py +++ b/examples/blogpost/models.py @@ -2,6 +2,7 @@ from django.db import models from django.template.defaultfilters import slugify import uuid + def uuid_str(): return str(uuid.uuid1()) @@ -14,6 +15,7 @@ RATING_CHOICES = ((0, 'Awful'), MAX_POSTS = 10 + class BlogPost(models.Model): key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False) title = models.CharField(max_length=128) @@ -37,4 +39,3 @@ class Comment(models.Model): comment = models.TextField() rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='How did you rate this post?') created = models.DateTimeField(auto_now_add=True) - diff --git a/examples/blogpost/resources.py b/examples/blogpost/resources.py index ac00a7342..c3166bcf3 100644 --- a/examples/blogpost/resources.py +++ b/examples/blogpost/resources.py @@ -11,6 +11,11 @@ class BlogPostResource(ModelResource): fields = ('created', 'title', 'slug', 'content', 'url', 'comments') ordering = ('-created',) + def url(self, instance): + return reverse('blog-post', + kwargs={'key': instance.key}, + request=self.request) + def comments(self, instance): return reverse('comments', kwargs={'blogpost': instance.key}, diff --git a/examples/modelresourceexample/resources.py b/examples/modelresourceexample/resources.py index 05090f8f3..b74b05721 100644 --- a/examples/modelresourceexample/resources.py +++ b/examples/modelresourceexample/resources.py @@ -1,4 +1,5 @@ from djangorestframework.resources import ModelResource +from djangorestframework.reverse import reverse from modelresourceexample.models import MyModel @@ -6,3 +7,8 @@ class MyModelResource(ModelResource): model = MyModel fields = ('foo', 'bar', 'baz', 'url') ordering = ('created',) + + def url(self, instance): + return reverse('model-resource-instance', + kwargs={'id': instance.id}, + request=self.request) diff --git a/examples/modelresourceexample/urls.py b/examples/modelresourceexample/urls.py index c680dc655..c5e1f874b 100644 --- a/examples/modelresourceexample/urls.py +++ b/examples/modelresourceexample/urls.py @@ -2,7 +2,10 @@ from django.conf.urls.defaults import patterns, url from djangorestframework.views import ListOrCreateModelView, InstanceModelView from modelresourceexample.resources import MyModelResource +my_model_list = ListOrCreateModelView.as_view(resource=MyModelResource) +my_model_instance = InstanceModelView.as_view(resource=MyModelResource) + urlpatterns = patterns('', - url(r'^$', ListOrCreateModelView.as_view(resource=MyModelResource), name='model-resource-root'), - url(r'^(?P[0-9]+)/$', InstanceModelView.as_view(resource=MyModelResource), name='model-resource-instance'), + url(r'^$', my_model_list, name='model-resource-root'), + url(r'^(?P[0-9]+)/$', my_model_instance, name='model-resource-instance'), ) diff --git a/examples/resourceexample/views.py b/examples/resourceexample/views.py index fcd0b2737..0db238044 100644 --- a/examples/resourceexample/views.py +++ b/examples/resourceexample/views.py @@ -15,7 +15,11 @@ class ExampleView(View): """ Handle GET requests, returning a list of URLs pointing to 3 other views. """ - return {"Some other resources": [reverse('another-example', kwargs={'num':num}, request=request) for num in range(3)]} + resource_urls = [reverse('another-example', + kwargs={'num': num}, + request=request) + for num in range(3)] + return {"Some other resources": resource_urls} class AnotherExampleView(View): From afdda88b13bb168d91cee267799a7e8d9bf26366 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 23 Feb 2012 09:22:37 +0000 Subject: [PATCH 37/44] Remove unused 'staticviews' --- djangorestframework/utils/staticviews.py | 60 ------------------------ 1 file changed, 60 deletions(-) delete mode 100644 djangorestframework/utils/staticviews.py diff --git a/djangorestframework/utils/staticviews.py b/djangorestframework/utils/staticviews.py deleted file mode 100644 index 101df4e96..000000000 --- a/djangorestframework/utils/staticviews.py +++ /dev/null @@ -1,60 +0,0 @@ -from django.contrib.auth.views import * -from django.conf import settings -from django.http import HttpResponse -from django.shortcuts import render_to_response -from django.template import RequestContext -import base64 - - -# 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='djangorestframework/login.html', - redirect_field_name=REDIRECT_FIELD_NAME, - authentication_form=AuthenticationForm): - """Displays the login form and handles the login action.""" - - redirect_to = request.REQUEST.get(redirect_field_name, '') - - if request.method == "POST": - form = authentication_form(data=request.POST) - if form.is_valid(): - # Light security check -- make sure redirect_to isn't garbage. - if not redirect_to or ' ' in redirect_to: - redirect_to = settings.LOGIN_REDIRECT_URL - - # Heavier security check -- redirects to http://example.com should - # not be allowed, but things like /view/?param=http://example.com - # should be allowed. This regex checks if there is a '//' *before* a - # question mark. - elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to): - redirect_to = settings.LOGIN_REDIRECT_URL - - # Okay, security checks complete. Log the user in. - auth_login(request, form.get_user()) - - if request.session.test_cookie_worked(): - request.session.delete_test_cookie() - - return HttpResponseRedirect(redirect_to) - - else: - form = authentication_form(request) - - request.session.set_test_cookie() - - #current_site = get_current_site(request) - - return render_to_response(template_name, { - 'form': form, - redirect_field_name: redirect_to, - #'site': current_site, - #'site_name': current_site.name, - }, context_instance=RequestContext(request)) - - -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) From 718381523543d226c30f374dd234a535f2a7a5f7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 23 Feb 2012 09:29:51 +0000 Subject: [PATCH 38/44] Version 0.4.0-dev --- djangorestframework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py index efe7f5663..46dd608fc 100644 --- a/djangorestframework/__init__.py +++ b/djangorestframework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.3.3' +__version__ = '0.4.0-dev' VERSION = __version__ # synonym From 98c16e6da84a9938c73ed9e0ff8c6b42d24b098f Mon Sep 17 00:00:00 2001 From: Shawn Lewis Date: Thu, 23 Feb 2012 16:26:02 -0800 Subject: [PATCH 39/44] Test for issue #178. This failing test shows that the serializer implementation breaks when a related serializer is passed in via include rather than via fields. --- djangorestframework/tests/serializer.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/djangorestframework/tests/serializer.py b/djangorestframework/tests/serializer.py index e85806106..834a60d09 100644 --- a/djangorestframework/tests/serializer.py +++ b/djangorestframework/tests/serializer.py @@ -104,6 +104,27 @@ class TestFieldNesting(TestCase): self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}}) self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}}) + def test_serializer_no_fields(self): + """ + Test related serializer works when the fields attr isn't present. Fix for + #178. + """ + class NestedM2(Serializer): + fields = ('field1', ) + + class NestedM3(Serializer): + fields = ('field2', ) + + class SerializerM2(Serializer): + include = [('field', NestedM2)] + exclude = ('id', ) + + class SerializerM3(Serializer): + fields = [('field', NestedM3)] + + self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}}) + self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}}) + def test_serializer_classname_nesting(self): """ Test related model serialization From 9c92f96ce2299667ef3393c6d63ffe7a83c89e4a Mon Sep 17 00:00:00 2001 From: Shawn Lewis Date: Thu, 23 Feb 2012 16:30:44 -0800 Subject: [PATCH 40/44] Fix for #178. Related serializers passed in via include now work as expected. --- djangorestframework/serializer.py | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py index b0c026753..5dea37e81 100644 --- a/djangorestframework/serializer.py +++ b/djangorestframework/serializer.py @@ -25,16 +25,9 @@ def _field_to_tuple(field): def _fields_to_list(fields): """ - Return a list of field names. + Return a list of field tuples. """ - return [_field_to_tuple(field)[0] for field in fields or ()] - - -def _fields_to_dict(fields): - """ - Return a `dict` of field name -> None, or tuple of fields, or Serializer class - """ - return dict([_field_to_tuple(field) for field in fields or ()]) + return [_field_to_tuple(field) for field in fields or ()] class _SkipField(Exception): @@ -110,9 +103,6 @@ class Serializer(object): self.stack = stack def get_fields(self, obj): - """ - Return the set of field names/keys to use for a model instance/dict. - """ fields = self.fields # If `fields` is not set, we use the default fields and modify @@ -123,9 +113,6 @@ class Serializer(object): exclude = self.exclude or () fields = set(default + list(include)) - set(exclude) - else: - fields = _fields_to_list(self.fields) - return fields def get_default_fields(self, obj): @@ -139,9 +126,7 @@ class Serializer(object): else: return obj.keys() - def get_related_serializer(self, key): - info = _fields_to_dict(self.fields).get(key, None) - + def get_related_serializer(self, info): # If an element in `fields` is a 2-tuple of (str, tuple) # then the second element of the tuple is the fields to # set on the related serializer @@ -175,11 +160,11 @@ class Serializer(object): """ return self.rename.get(smart_str(key), smart_str(key)) - def serialize_val(self, key, obj): + def serialize_val(self, key, obj, related_info): """ Convert a model field or dict value into a serializable representation. """ - related_serializer = self.get_related_serializer(key) + related_serializer = self.get_related_serializer(related_info) if self.depth is None: depth = None @@ -219,7 +204,7 @@ class Serializer(object): fields = self.get_fields(instance) # serialize each required field - for fname in fields: + for fname, related_info in _fields_to_list(fields): try: # we first check for a method 'fname' on self, # 'fname's signature must be 'def fname(self, instance)' @@ -237,7 +222,7 @@ class Serializer(object): continue key = self.serialize_key(fname) - val = self.serialize_val(fname, obj) + val = self.serialize_val(fname, obj, related_info) data[key] = val except _SkipField: pass From 2fe6913b1abfa9ca2f5e6df69faba5fc2c2c266d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 24 Feb 2012 13:13:11 +0000 Subject: [PATCH 41/44] Added @shawnlewis. Thanks! --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 243ebfdf8..47a31d05d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -34,6 +34,7 @@ Paul Oswald Sean C. Farley Daniel Izquierdo Can Yavuz +Shawn Lewis THANKS TO: From 1751655927735ecd8a096889014f4e0beeba101a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 27 Feb 2012 10:06:20 +0000 Subject: [PATCH 42/44] Update docs/requirements.txt --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 77cdf485a..46a671494 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Documentation requires Django & Sphinx, and their dependencies... -Django==1.2.4 +Django>=1.2.4 Jinja2==2.5.5 Pygments==1.4 Sphinx==1.0.7 From 55317b03725dcfba5c3fd5b06b7c507fff69ff13 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 29 Feb 2012 21:32:10 +0100 Subject: [PATCH 43/44] Fixes broken permissions-example. reverse takes `request` as a kwarg. --- examples/permissionsexample/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/permissionsexample/views.py b/examples/permissionsexample/views.py index b0c1f02ef..5aeae7eb2 100644 --- a/examples/permissionsexample/views.py +++ b/examples/permissionsexample/views.py @@ -12,11 +12,11 @@ class PermissionsExampleView(View): return [ { 'name': 'Throttling Example', - 'url': reverse('throttled-resource', request) + 'url': reverse('throttled-resource', request=request) }, { 'name': 'Logged in example', - 'url': reverse('loggedin-resource', request) + 'url': reverse('loggedin-resource', request=request) }, ] From e53c819cc7a5567f2c29375550e9ff62ec20d472 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Tue, 27 Mar 2012 23:40:33 +0200 Subject: [PATCH 44/44] Fix broken pygments test. --- examples/pygments_api/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/pygments_api/views.py b/examples/pygments_api/views.py index 3dd55115c..f41fa739e 100644 --- a/examples/pygments_api/views.py +++ b/examples/pygments_api/views.py @@ -65,7 +65,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 [reverse('pygments-instance', request, args=[unique_id]) for unique_id in unique_ids] + return [reverse('pygments-instance', request=request, args=[unique_id]) for unique_id in unique_ids] def post(self, request): """ @@ -85,7 +85,7 @@ class PygmentsRoot(View): remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES) - return Response(status.HTTP_201_CREATED, headers={'Location': reverse('pygments-instance', request, args=[unique_id])}) + return Response(status.HTTP_201_CREATED, headers={'Location': reverse('pygments-instance', request=request, args=[unique_id])}) class PygmentsInstance(View):