From e362344fbdc7c48d978cb87963b04953113a4348 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 22 Jan 2012 20:19:34 +0000 Subject: [PATCH 01/35] Fix tests to work on all supported configurations. --- djangorestframework/tests/mixins.py | 3 +- djangorestframework/tests/validators.py | 49 +++++++++++-------------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/djangorestframework/tests/mixins.py b/djangorestframework/tests/mixins.py index 72e0b8b07..88b13dd5f 100644 --- a/djangorestframework/tests/mixins.py +++ b/djangorestframework/tests/mixins.py @@ -41,8 +41,7 @@ class TestModelRead(TestModelsTestCase): mixin = ReadModelMixin() mixin.resource = GroupResource - with self.assertRaises(ErrorResponse): - response = mixin.get(request, 12345) + self.assertRaises(ErrorResponse, mixin.get, request, 12345) class TestModelCreation(TestModelsTestCase): diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py index 7b6432726..15d92231c 100644 --- a/djangorestframework/tests/validators.py +++ b/djangorestframework/tests/validators.py @@ -1,12 +1,9 @@ from django import forms from django.db import models from django.test import TestCase -from djangorestframework.compat import RequestFactory -from djangorestframework.resources import Resource, FormResource, ModelResource +from djangorestframework.resources import FormResource, ModelResource from djangorestframework.response import ErrorResponse from djangorestframework.views import View -from djangorestframework.resources import Resource - class TestDisabledValidations(TestCase): @@ -22,7 +19,7 @@ class TestDisabledValidations(TestCase): resource = DisabledFormResource view = MockView() - content = {'qwerty':'uiop'} + content = {'qwerty': 'uiop'} self.assertEqual(FormResource(view).validate_request(content, None), content) def test_disabled_form_validator_get_bound_form_returns_none(self): @@ -35,10 +32,9 @@ class TestDisabledValidations(TestCase): resource = DisabledFormResource view = MockView() - content = {'qwerty':'uiop'} + content = {'qwerty': 'uiop'} self.assertEqual(FormResource(view).get_bound_form(content), None) - def test_disabled_model_form_validator_returns_content_unchanged(self): """If the view's form is None and does not have a Resource with a model set then ModelFormValidator(view).validate_request(content, None) should just return the content unmodified.""" @@ -47,8 +43,8 @@ class TestDisabledValidations(TestCase): resource = ModelResource view = DisabledModelFormView() - content = {'qwerty':'uiop'} - self.assertEqual(ModelResource(view).get_bound_form(content), None)# + content = {'qwerty': 'uiop'} + self.assertEqual(ModelResource(view).get_bound_form(content), None) def test_disabled_model_form_validator_get_bound_form_returns_none(self): """If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None.""" @@ -56,9 +52,10 @@ class TestDisabledValidations(TestCase): resource = ModelResource view = DisabledModelFormView() - content = {'qwerty':'uiop'} + content = {'qwerty': 'uiop'} self.assertEqual(ModelResource(view).get_bound_form(content), None) + class TestNonFieldErrors(TestCase): """Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)""" @@ -72,7 +69,7 @@ class TestNonFieldErrors(TestCase): def clean(self): if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data: raise forms.ValidationError(self.ERROR_TEXT) - return self.cleaned_data #pragma: no cover + return self.cleaned_data class MockResource(FormResource): form = MockForm @@ -87,7 +84,7 @@ class TestNonFieldErrors(TestCase): except ErrorResponse, exc: self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]}) else: - self.fail('ErrorResponse was not raised') #pragma: no cover + self.fail('ErrorResponse was not raised') class TestFormValidation(TestCase): @@ -115,10 +112,9 @@ class TestFormValidation(TestCase): self.MockFormView = MockFormView self.MockModelFormView = MockModelFormView - def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator): """If the content is already valid and clean then validate(content) should just return the content unmodified.""" - content = {'qwerty':'uiop'} + content = {'qwerty': 'uiop'} self.assertEqual(validator.validate_request(content, None), content) def validation_failure_raises_response_exception(self, validator): @@ -143,7 +139,9 @@ class TestFormValidation(TestCase): raise errors on unexpected request data""" content = {'qwerty': 'uiop', 'extra': 'extra'} validator.allow_unknown_form_fields = True - self.assertDictEqual({'qwerty': u'uiop'}, validator.validate_request(content, None), "Resource didn't accept unknown fields.") + self.assertEqual({'qwerty': u'uiop'}, + validator.validate_request(content, None), + "Resource didn't accept unknown fields.") validator.allow_unknown_form_fields = False def validation_does_not_require_extra_fields_if_explicitly_set(self, validator): @@ -159,7 +157,7 @@ class TestFormValidation(TestCase): except ErrorResponse, exc: self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}}) else: - self.fail('ResourceException was not raised') #pragma: no cover + self.fail('ResourceException was not raised') def validation_failed_due_to_field_error_returns_appropriate_message(self, validator): """If validation fails due to a field error, ensure the response contains a single field error""" @@ -169,7 +167,7 @@ class TestFormValidation(TestCase): except ErrorResponse, exc: self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}}) else: - self.fail('ResourceException was not raised') #pragma: no cover + self.fail('ResourceException was not raised') def validation_failed_due_to_invalid_field_returns_appropriate_message(self, validator): """If validation fails due to an invalid field, ensure the response contains a single field error""" @@ -179,7 +177,7 @@ class TestFormValidation(TestCase): except ErrorResponse, exc: self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}}) else: - self.fail('ResourceException was not raised') #pragma: no cover + self.fail('ResourceException was not raised') def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator): """If validation for multiple reasons, ensure the response contains each error""" @@ -190,7 +188,7 @@ class TestFormValidation(TestCase): self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'], 'extra': ['This field does not exist.']}}) else: - self.fail('ResourceException was not raised') #pragma: no cover + self.fail('ResourceException was not raised') # Tests on FormResource @@ -209,7 +207,7 @@ class TestFormValidation(TestCase): def test_validation_allows_extra_fields_if_explicitly_set(self): validator = self.MockFormResource(self.MockFormView()) self.validation_allows_extra_fields_if_explicitly_set(validator) - + def test_validation_allows_unknown_fields_if_explicitly_allowed(self): validator = self.MockFormResource(self.MockFormView()) self.validation_allows_unknown_fields_if_explicitly_allowed(validator) @@ -294,22 +292,21 @@ class TestModelFormValidator(TestCase): self.validator = MockResource(MockView) - def test_property_fields_are_allowed_on_model_forms(self): """Validation on ModelForms may include property fields that exist on the Model to be included in the input.""" - content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'} + content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only'} self.assertEqual(self.validator.validate_request(content, None), content) def test_property_fields_are_not_required_on_model_forms(self): """Validation on ModelForms does not require property fields that exist on the Model to be included in the input.""" - content = {'qwerty':'example', 'uiop': 'example'} + content = {'qwerty': 'example', 'uiop': 'example'} self.assertEqual(self.validator.validate_request(content, None), content) def test_extra_fields_not_allowed_on_model_forms(self): """If some (otherwise valid) content includes fields that are not in the form then validation should fail. It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up broken clients more easily (eg submitting content with a misnamed field)""" - content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'} + content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'} self.assertRaises(ErrorResponse, self.validator.validate_request, content, None) def test_validate_requires_fields_on_model_forms(self): @@ -321,10 +318,8 @@ class TestModelFormValidator(TestCase): def test_validate_does_not_require_blankable_fields_on_model_forms(self): """Test standard ModelForm validation behaviour - fields with blank=True are not required.""" - content = {'qwerty':'example', 'readonly': 'read only'} + content = {'qwerty': 'example', 'readonly': 'read only'} self.validator.validate_request(content, None) def test_model_form_validator_uses_model_forms(self): self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm)) - - From 2dfc048e6f9c33652d9c6e50213524124a4701eb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 22 Jan 2012 20:47:01 +0000 Subject: [PATCH 02/35] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 454674403..9cec01cf2 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ Full documentation for the project is available at http://django-rest-framework. Issue tracking is on `GitHub `_. General questions should be taken to the `discussion group `_. -We also have a `Jenkins service `_ which runs our test suite. +We also have a `Jenkins service `_ which runs our test suite. Requirements: From 8bb427537cb0a8adc0a826bc8227d1b5225f905e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Jan 2012 09:06:30 +0000 Subject: [PATCH 03/35] Docstring tidy up --- djangorestframework/tests/authentication.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py index 1835c5236..303bf96be 100644 --- a/djangorestframework/tests/authentication.py +++ b/djangorestframework/tests/authentication.py @@ -11,7 +11,7 @@ import base64 class MockView(View): - permissions = ( permissions.IsAuthenticated, ) + permissions = (permissions.IsAuthenticated,) def post(self, request): return {'a': 1, 'b': 2, 'c': 3} @@ -74,24 +74,32 @@ class SessionAuthTests(TestCase): self.csrf_client.logout() def test_post_form_session_auth_failing_csrf(self): - """Ensure POSTing form over session authentication without CSRF token fails.""" + """ + Ensure POSTing form over session authentication without CSRF token fails. + """ self.csrf_client.login(username=self.username, password=self.password) response = self.csrf_client.post('/', {'example': 'example'}) self.assertEqual(response.status_code, 403) def test_post_form_session_auth_passing(self): - """Ensure POSTing form over session authentication with logged in user and CSRF token passes.""" + """ + Ensure POSTing form over session authentication with logged in user and CSRF token passes. + """ self.non_csrf_client.login(username=self.username, password=self.password) response = self.non_csrf_client.post('/', {'example': 'example'}) self.assertEqual(response.status_code, 200) def test_put_form_session_auth_passing(self): - """Ensure PUTting form over session authentication with logged in user and CSRF token passes.""" + """ + Ensure PUTting form over session authentication with logged in user and CSRF token passes. + """ self.non_csrf_client.login(username=self.username, password=self.password) response = self.non_csrf_client.put('/', {'example': 'example'}) self.assertEqual(response.status_code, 200) def test_post_form_session_auth_failing(self): - """Ensure POSTing form over session authentication without logged in user fails.""" + """ + Ensure POSTing form over session authentication without logged in user fails. + """ response = self.csrf_client.post('/', {'example': 'example'}) self.assertEqual(response.status_code, 403) From 2d8739115c8b82267f1006f636f05b116539e0dd Mon Sep 17 00:00:00 2001 From: Ben Timby Date: Mon, 23 Jan 2012 13:18:38 -0500 Subject: [PATCH 04/35] Fix for testcase test_with_content_type_args --- djangorestframework/tests/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index 84e4390b8..adb46f7fa 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -172,7 +172,7 @@ class RendererIntegrationTests(TestCase): self.assertEquals(resp.status_code, DUMMYSTATUS) _flat_repr = '{"foo": ["bar", "baz"]}' -_indented_repr = '{\n "foo": [\n "bar", \n "baz"\n ]\n}' +_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' class JSONRendererTests(TestCase): From 7fa3a214fbd33dac61d3f8836e2c4aeda3149647 Mon Sep 17 00:00:00 2001 From: Ben Timby Date: Mon, 23 Jan 2012 13:32:37 -0500 Subject: [PATCH 05/35] Refactored get_name()/get_description() --- djangorestframework/renderers.py | 47 ++++++----- djangorestframework/templates/renderer.html | 11 +-- djangorestframework/tests/description.py | 52 ++++++------ djangorestframework/utils/breadcrumbs.py | 4 +- djangorestframework/utils/description.py | 88 --------------------- djangorestframework/views.py | 73 ++++++++++++++--- 6 files changed, 119 insertions(+), 156 deletions(-) delete mode 100644 djangorestframework/utils/description.py diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index bb186b0a9..5338c3835 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -12,10 +12,9 @@ from django.template import RequestContext, loader from django.utils import simplejson as json -from djangorestframework.compat import apply_markdown, yaml +from djangorestframework.compat import yaml from djangorestframework.utils import dict2xml, url_resolves from djangorestframework.utils.breadcrumbs import get_breadcrumbs -from djangorestframework.utils.description import get_name, get_description from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches from djangorestframework import VERSION @@ -226,6 +225,7 @@ class DocumentingTemplateRenderer(BaseRenderer): return content + def _get_form_instance(self, view, method): """ Get a form, possibly bound to either the input or output data. @@ -261,6 +261,7 @@ class DocumentingTemplateRenderer(BaseRenderer): return form_instance + def _get_generic_content_form(self, view): """ Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms @@ -296,6 +297,20 @@ class DocumentingTemplateRenderer(BaseRenderer): # Okey doke, let's do it return GenericContentForm(view) + def get_name(self): + try: + return self.view.get_name() + except AttributeError: + return self.view.__doc__ + + def get_description(self, html=None): + if html is None: + html = bool('html' in self.format) + try: + return self.view.get_description(html) + except AttributeError: + return self.view.__doc__ + def render(self, obj=None, media_type=None): """ Renders *obj* using the :attr:`template` set on the class. @@ -316,15 +331,8 @@ class DocumentingTemplateRenderer(BaseRenderer): login_url = None logout_url = None - name = get_name(self.view) - description = get_description(self.view) - - markeddown = None - if apply_markdown: - try: - markeddown = apply_markdown(description) - except AttributeError: - markeddown = None + name = self.get_name() + description = self.get_description() breadcrumb_list = get_breadcrumbs(self.view.request.path) @@ -332,12 +340,11 @@ class DocumentingTemplateRenderer(BaseRenderer): context = RequestContext(self.view.request, { 'content': content, 'view': self.view, - 'request': self.view.request, # TODO: remove + 'request': self.view.request, # TODO: remove 'response': self.view.response, 'description': description, 'name': name, 'version': VERSION, - 'markeddown': markeddown, 'breadcrumblist': breadcrumb_list, 'available_formats': self.view._rendered_formats, 'put_form': put_form_instance, @@ -395,14 +402,12 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): template = 'renderer.txt' -DEFAULT_RENDERERS = ( - JSONRenderer, - JSONPRenderer, - DocumentingHTMLRenderer, - DocumentingXHTMLRenderer, - DocumentingPlainTextRenderer, - XMLRenderer -) +DEFAULT_RENDERERS = ( JSONRenderer, + JSONPRenderer, + DocumentingHTMLRenderer, + DocumentingXHTMLRenderer, + DocumentingPlainTextRenderer, + XMLRenderer ) if YAMLRenderer: DEFAULT_RENDERERS += (YAMLRenderer,) diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html index 0532d515e..3b33a6ff3 100644 --- a/djangorestframework/templates/renderer.html +++ b/djangorestframework/templates/renderer.html @@ -11,13 +11,8 @@ /* Custom styles */ .version{font-size:8px;} - {% if ADMIN_MEDIA_PREFIX %} - - - {% else %} - - - {% endif %} + + Django REST framework - {{ name }} @@ -50,7 +45,7 @@

{{ name }}

-

{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}

+

{% autoescape off %}{{ description }}{% endautoescape %}

{{ response.status }} {{ response.status_text }}{% autoescape off %}
 {% for key, val in response.headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
diff --git a/djangorestframework/tests/description.py b/djangorestframework/tests/description.py
index 56dcdfabb..212b4ca4d 100644
--- a/djangorestframework/tests/description.py
+++ b/djangorestframework/tests/description.py
@@ -1,7 +1,6 @@
 from django.test import TestCase
 from djangorestframework.views import View
 from djangorestframework.compat import apply_markdown
-from djangorestframework.utils.description import get_name, get_description
 
 # We check that docstrings get nicely un-indented.
 DESCRIPTION = """an example docstring
@@ -51,15 +50,15 @@ class TestViewNamesAndDescriptions(TestCase):
         """Ensure Resource names are based on the classname by default."""
         class MockView(View):
             pass
-        self.assertEquals(get_name(MockView()), 'Mock')
+        self.assertEquals(MockView().get_name(), 'Mock')
 
-    # This has been turned off now.
-    #def test_resource_name_can_be_set_explicitly(self):
-    #    """Ensure Resource names can be set using the 'name' class attribute."""
-    #    example = 'Some Other Name'
-    #    class MockView(View):
-    #        name = example
-    #    self.assertEquals(get_name(MockView()), example)
+    def test_resource_name_can_be_set_explicitly(self):
+        """Ensure Resource names can be set using the 'get_name' method."""
+        example = 'Some Other Name'
+        class MockView(View):
+            def get_name(self):
+                return example
+        self.assertEquals(MockView().get_name(), example)
 
     def test_resource_description_uses_docstring_by_default(self):
         """Ensure Resource names are based on the docstring by default."""
@@ -79,29 +78,30 @@ class TestViewNamesAndDescriptions(TestCase):
 
             # hash style header #"""
 
-        self.assertEquals(get_description(MockView()), DESCRIPTION)
+        self.assertEquals(MockView().get_description(), DESCRIPTION)
 
-    # This has been turned off now
-    #def test_resource_description_can_be_set_explicitly(self):
-    #    """Ensure Resource descriptions can be set using the 'description' class attribute."""
-    #    example = 'Some other description'
-    #    class MockView(View):
-    #        """docstring"""
-    #        description = example
-    #    self.assertEquals(get_description(MockView()), example)
+    def test_resource_description_can_be_set_explicitly(self):
+        """Ensure Resource descriptions can be set using the 'get_description' method."""
+        example = 'Some other description'
+        class MockView(View):
+            """docstring"""
+            def get_description(self):
+                return example
+        self.assertEquals(MockView().get_description(), example)
 
-    #def test_resource_description_does_not_require_docstring(self):
-    #    """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute."""
-    #    example = 'Some other description'
-    #    class MockView(View):
-    #        description = example
-    #    self.assertEquals(get_description(MockView()), example)
+    def test_resource_description_does_not_require_docstring(self):
+        """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'get_description' method."""
+        example = 'Some other description'
+        class MockView(View):
+            def get_description(self):
+                return example
+        self.assertEquals(MockView().get_description(), example)
 
     def test_resource_description_can_be_empty(self):
-        """Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string"""
+        """Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string."""
         class MockView(View):
             pass
-        self.assertEquals(get_description(MockView()), '')
+        self.assertEquals(MockView().get_description(), '')
 
     def test_markdown(self):
         """Ensure markdown to HTML works as expected"""
diff --git a/djangorestframework/utils/breadcrumbs.py b/djangorestframework/utils/breadcrumbs.py
index cfc63a479..85e13a5a4 100644
--- a/djangorestframework/utils/breadcrumbs.py
+++ b/djangorestframework/utils/breadcrumbs.py
@@ -1,6 +1,4 @@
 from django.core.urlresolvers import resolve
-from djangorestframework.utils.description import get_name
-
 
 def get_breadcrumbs(url):
     """Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
@@ -17,7 +15,7 @@ def get_breadcrumbs(url):
         else:
             # Check if this is a REST framework view, and if so add it to the breadcrumbs
             if isinstance(getattr(view, 'cls_instance', None), View):
-                breadcrumbs_list.insert(0, (get_name(view), url))
+                breadcrumbs_list.insert(0, (view.cls_instance.get_name(), url))
 
         if url == '':
             # All done
diff --git a/djangorestframework/utils/description.py b/djangorestframework/utils/description.py
deleted file mode 100644
index 096cf57ff..000000000
--- a/djangorestframework/utils/description.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""
-Get a descriptive name and description for a view.
-"""
-import re
-from djangorestframework.resources import Resource, FormResource, ModelResource
-
-
-# These a a bit Grungy, but they do the job.
-
-def get_name(view):
-    """
-    Return a name for the view.
-
-    If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'.
-    """
-
-    # If we're looking up the name of a view callable, as found by reverse,
-    # grok the class instance that we stored when as_view was called.
-    if getattr(view, 'cls_instance', None):
-        view = view.cls_instance
-
-    # If this view has a resource that's been overridden, then use that resource for the name
-    if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
-        name = view.resource.__name__
-
-        # Chomp of any non-descriptive trailing part of the resource class name
-        if name.endswith('Resource') and name != 'Resource':
-            name = name[:-len('Resource')]
-
-        # If the view has a descriptive suffix, eg '*** List', '*** Instance'
-        if getattr(view, '_suffix', None):
-            name += view._suffix
-
-    # Otherwise if it's a function view use the function's name
-    elif getattr(view, '__name__', None) is not None:
-        name = view.__name__
-
-    # If it's a view class with no resource then grok the name from the class name
-    elif getattr(view, '__class__', None) is not None:
-        name = view.__class__.__name__
-
-        # Chomp of any non-descriptive trailing part of the view class name
-        if name.endswith('View') and name != 'View':
-            name = name[:-len('View')]
-
-    # I ain't got nuthin fo' ya
-    else:
-        return ''
-
-    return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
-
-
-def get_description(view):
-    """
-    Provide a description for the view.
-
-    By default this is the view's docstring with nice unindention applied.
-    """
-
-    # If we're looking up the name of a view callable, as found by reverse,
-    # grok the class instance that we stored when as_view was called.
-    if getattr(view, 'cls_instance', None):
-        view = view.cls_instance
-
-    # If this view has a resource that's been overridden, then use the resource's doctring
-    if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
-        doc = view.resource.__doc__
-
-    # Otherwise use the view doctring
-    elif getattr(view, '__doc__', None):
-        doc = view.__doc__
-
-    # I ain't got nuthin fo' ya
-    else:
-        return ''
-
-    if not doc:
-        return ''
-
-    whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in doc.splitlines()[1:] if line.lstrip()]
-
-    # unindent the docstring if needed
-    if whitespace_counts:
-        whitespace_pattern = '^' + (' ' * min(whitespace_counts))
-        return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', doc)
-
-    # otherwise return it as-is
-    return doc
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 9f53868b3..9dd84bf2a 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -5,15 +5,17 @@ be subclassing in your implementation.
 By setting or modifying class attributes on your view, you change it's predefined behaviour.
 """
 
+import re
 from django.core.urlresolvers import set_script_prefix, get_script_prefix
 from django.http import HttpResponse
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
 from django.views.decorators.csrf import csrf_exempt
 
-from djangorestframework.compat import View as DjangoView
+from djangorestframework.compat import View as DjangoView, apply_markdown
 from djangorestframework.response import Response, ErrorResponse
 from djangorestframework.mixins import *
 from djangorestframework import resources, renderers, parsers, authentication, permissions, status
-from djangorestframework.utils.description import get_name, get_description
 
 
 __all__ = (
@@ -25,6 +27,7 @@ __all__ = (
 )
 
 
+
 class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
     """
     Handles incoming requests and maps them to REST operations.
@@ -47,13 +50,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
     List of parsers the resource can parse the request with.
     """
 
-    authentication = (authentication.UserLoggedInAuthentication,
-                      authentication.BasicAuthentication)
+    authentication = ( authentication.UserLoggedInAuthentication,
+                       authentication.BasicAuthentication )
     """
     List of all authenticating methods to attempt.
     """
 
-    permissions = (permissions.FullAnonAccess,)
+    permissions = ( permissions.FullAnonAccess, )
     """
     List of all permissions that must be checked.
     """
@@ -76,6 +79,59 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
         """
         return [method.upper() for method in self.http_method_names if hasattr(self, method)]
 
+    def get_name(self):
+        """
+        Return the resource or view class name for use as this view's name.
+        Override to customize.
+        """
+        # If this view has a resource that's been overridden, then use that resource for the name
+        if getattr(self, 'resource', None) not in (None, resources.Resource, resources.FormResource, resources.ModelResource):
+            name = self.resource.__name__
+
+            # Chomp of any non-descriptive trailing part of the resource class name
+            if name.endswith('Resource') and name != 'Resource':
+                name = name[:-len('Resource')]
+
+            # If the view has a descriptive suffix, eg '*** List', '*** Instance'
+            if getattr(self, '_suffix', None):
+                name += self._suffix
+        # If it's a view class with no resource then grok the name from the class name
+        elif getattr(self, '__class__', None) is not None:
+            name = self.__class__.__name__
+
+            # Chomp of any non-descriptive trailing part of the view class name
+            if name.endswith('View') and name != 'View':
+                name = name[:-len('View')]
+        else:
+            name = ''
+        return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
+
+    def get_description(self, html=False):
+        """
+        Return the resource or view docstring for use as this view's description.
+        Override to customize.
+        """
+        # If this view has a resource that's been overridden, then use the resource's doctring
+        if getattr(self, 'resource', None) not in (None, resources.Resource, resources.FormResource, resources.ModelResource):
+            doc = self.resource.__doc__
+        # Otherwise use the view doctring
+        elif getattr(self, '__doc__', None):
+            doc = self.__doc__
+        else:
+            doc = ''
+        whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in doc.splitlines()[1:] if line.lstrip()]
+        # unindent the docstring if needed
+        if whitespace_counts:
+            whitespace_pattern = '^' + (' ' * min(whitespace_counts))
+            doc = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', doc)
+        if doc and html:
+            if apply_markdown:
+                doc = apply_markdown(doc)
+            else:
+                doc = escape(doc)
+                doc = mark_safe(doc.replace('\n', '
')) + return doc + def http_method_not_allowed(self, request, *args, **kwargs): """ Return an HTTP 405 error if an operation is called which does not have a handler method. @@ -161,8 +217,8 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): def options(self, request, *args, **kwargs): response_obj = { - 'name': get_name(self), - 'description': get_description(self), + 'name': self.get_name(), + 'description': self.get_description(), 'renders': self._rendered_media_types, 'parses': self._parsed_media_types, } @@ -184,21 +240,18 @@ class ModelView(View): """ resource = resources.ModelResource - class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): """ A view which provides default operations for read/update/delete against a model instance. """ _suffix = 'Instance' - class ListModelView(ListModelMixin, ModelView): """ A view which provides default operations for list, against a model in the database. """ _suffix = 'List' - class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView): """ A view which provides default operations for list and create, against a model in the database. From 27590fea8b34f95fc4419524db18cc5053699bee Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 24 Jan 2012 17:53:54 +0000 Subject: [PATCH 06/35] Drop redundant comment. --- djangorestframework/status.py | 1 - 1 file changed, 1 deletion(-) diff --git a/djangorestframework/status.py b/djangorestframework/status.py index 9e2ef54cf..684c9b380 100644 --- a/djangorestframework/status.py +++ b/djangorestframework/status.py @@ -5,7 +5,6 @@ See RFC 2616 - Sec 10: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html Also see django.core.handlers.wsgi.STATUS_CODE_TEXT """ -# Verbose format HTTP_100_CONTINUE = 100 HTTP_101_SWITCHING_PROTOCOLS = 101 HTTP_200_OK = 200 From 863bbe7684c44921b779a69c6b4c2ff16a223bd8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 24 Jan 2012 18:13:01 +0000 Subject: [PATCH 07/35] Parse url when adding query param. --- djangorestframework/templatetags/add_query_param.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/templatetags/add_query_param.py b/djangorestframework/templatetags/add_query_param.py index ce175b810..117097303 100644 --- a/djangorestframework/templatetags/add_query_param.py +++ b/djangorestframework/templatetags/add_query_param.py @@ -5,7 +5,7 @@ register = Library() def add_query_param(url, param): (key, sep, val) = param.partition('=') - return unicode(URLObject(url) & (key, val)) + return unicode(URLObject.parse(url) & (key, val)) register.filter('add_query_param', add_query_param) From 049d417ebc2c88b6aeed7946efdb3baeab9a4432 Mon Sep 17 00:00:00 2001 From: Ben Timby Date: Tue, 24 Jan 2012 14:11:10 -0500 Subject: [PATCH 08/35] Reverted formatting --- djangorestframework/renderers.py | 18 +++++++++--------- djangorestframework/templates/renderer.html | 11 ++++++++--- djangorestframework/views.py | 10 ++++++---- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 5338c3835..bb0f789aa 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -225,7 +225,6 @@ class DocumentingTemplateRenderer(BaseRenderer): return content - def _get_form_instance(self, view, method): """ Get a form, possibly bound to either the input or output data. @@ -261,7 +260,6 @@ class DocumentingTemplateRenderer(BaseRenderer): return form_instance - def _get_generic_content_form(self, view): """ Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms @@ -340,7 +338,7 @@ class DocumentingTemplateRenderer(BaseRenderer): context = RequestContext(self.view.request, { 'content': content, 'view': self.view, - 'request': self.view.request, # TODO: remove + 'request': self.view.request, # TODO: remove 'response': self.view.response, 'description': description, 'name': name, @@ -402,12 +400,14 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): template = 'renderer.txt' -DEFAULT_RENDERERS = ( JSONRenderer, - JSONPRenderer, - DocumentingHTMLRenderer, - DocumentingXHTMLRenderer, - DocumentingPlainTextRenderer, - XMLRenderer ) +DEFAULT_RENDERERS = ( + JSONRenderer, + JSONPRenderer, + DocumentingHTMLRenderer, + DocumentingXHTMLRenderer, + DocumentingPlainTextRenderer, + XMLRenderer +) if YAMLRenderer: DEFAULT_RENDERERS += (YAMLRenderer,) diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html index 3b33a6ff3..ff761023f 100644 --- a/djangorestframework/templates/renderer.html +++ b/djangorestframework/templates/renderer.html @@ -11,9 +11,14 @@ /* Custom styles */ .version{font-size:8px;} - - - Django REST framework - {{ name }} + {% if ADMIN_MEDIA_PREFIX %} + + + {% else %} + + + {% endif %} + Django REST framework - {{ name }}
diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 9dd84bf2a..d89c2afd9 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -27,7 +27,6 @@ __all__ = ( ) - class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): """ Handles incoming requests and maps them to REST operations. @@ -50,13 +49,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): List of parsers the resource can parse the request with. """ - authentication = ( authentication.UserLoggedInAuthentication, - authentication.BasicAuthentication ) + authentication = (authentication.UserLoggedInAuthentication, + authentication.BasicAuthentication) """ List of all authenticating methods to attempt. """ - permissions = ( permissions.FullAnonAccess, ) + permissions = (permissions.FullAnonAccess,) """ List of all permissions that must be checked. """ @@ -240,18 +239,21 @@ class ModelView(View): """ resource = resources.ModelResource + class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): """ A view which provides default operations for read/update/delete against a model instance. """ _suffix = 'Instance' + class ListModelView(ListModelMixin, ModelView): """ A view which provides default operations for list, against a model in the database. """ _suffix = 'List' + class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView): """ A view which provides default operations for list and create, against a model in the database. From 1d9f24f60d1348c53e9a88cb605c7890b6c40146 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 24 Jan 2012 19:26:37 +0000 Subject: [PATCH 09/35] Fix UserLoggedInAuthentication for POST requests. Fixes #78. --- djangorestframework/authentication.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py index b61af32a2..f46a9c460 100644 --- a/djangorestframework/authentication.py +++ b/djangorestframework/authentication.py @@ -87,25 +87,12 @@ class UserLoggedInAuthentication(BaseAuthentication): Returns a :obj:`User` if the request session currently has a logged in user. Otherwise returns :const:`None`. """ - # TODO: Might be cleaner to switch this back to using request.POST, - # and let FormParser/MultiPartParser deal with the consequences. + self.view.DATA # Make sure our generic parsing runs first + if getattr(request, 'user', None) and request.user.is_active: # Enforce CSRF validation for session based authentication. - - # Temporarily replace request.POST with .DATA, to use our generic parsing. - # If DATA is not dict-like, use an empty dict. - if request.method.upper() == 'POST': - if hasattr(self.view.DATA, 'get'): - request._post = self.view.DATA - else: - request._post = {} - resp = CsrfViewMiddleware().process_view(request, None, (), {}) - # Replace request.POST - if request.method.upper() == 'POST': - del(request._post) - if resp is None: # csrf passed return request.user return None From 9ce864e63bd7e93e4602f687423f905749fbc0ea Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 24 Jan 2012 19:27:18 +0000 Subject: [PATCH 10/35] Cleanup authentication example --- examples/permissionsexample/views.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/permissionsexample/views.py b/examples/permissionsexample/views.py index 3f71e67bb..86f458f8e 100644 --- a/examples/permissionsexample/views.py +++ b/examples/permissionsexample/views.py @@ -2,14 +2,23 @@ from djangorestframework.views import View from djangorestframework.permissions import PerUserThrottling, IsAuthenticated from django.core.urlresolvers import reverse + class PermissionsExampleView(View): """ A container view for permissions examples. """ def get(self, request): - return [{'name': 'Throttling Example', 'url': reverse('throttled-resource')}, - {'name': 'Logged in example', 'url': reverse('loggedin-resource')},] + return [ + { + 'name': 'Throttling Example', + 'url': reverse('throttled-resource') + }, + { + 'name': 'Logged in example', + 'url': reverse('loggedin-resource') + }, + ] class ThrottlingExampleView(View): @@ -20,7 +29,7 @@ class ThrottlingExampleView(View): throttle will be applied until 60 seconds have passed since the first request. """ - permissions = ( PerUserThrottling, ) + permissions = (PerUserThrottling,) throttle = '10/min' def get(self, request): @@ -29,13 +38,15 @@ class ThrottlingExampleView(View): """ return "Successful response to GET request because throttle is not yet active." + class LoggedInExampleView(View): """ You can login with **'test', 'test'.** or use curl: - + `curl -X GET -H 'Accept: application/json' -u test:test http://localhost:8000/permissions-example` - """ + """ permissions = (IsAuthenticated, ) + def get(self, request): - return 'Logged in or not?' + return 'You have permission to view this resource' From 54caf6d7ba8b53cb74774535f2ecd8584a9b2cfd Mon Sep 17 00:00:00 2001 From: Ben Timby Date: Tue, 24 Jan 2012 14:34:29 -0500 Subject: [PATCH 11/35] Modified test case to pass regardless of JSON library --- djangorestframework/tests/renderers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index adb46f7fa..77b5430fa 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -1,3 +1,5 @@ +import re + from django.conf.urls.defaults import patterns, url from django.test import TestCase @@ -187,6 +189,8 @@ class JSONRendererTests(TestCase): obj = {'foo': ['bar', 'baz']} renderer = JSONRenderer(None) content = renderer.render(obj, 'application/json') + # Fix failing test case which depends on version of JSON library. + content = re.sub(' +\n', '\n', content) self.assertEquals(content, _flat_repr) def test_with_content_type_args(self): From d3ec860dd1fedf27042b3d08bf6c7e4f4adbddc9 Mon Sep 17 00:00:00 2001 From: Ben Timby Date: Tue, 24 Jan 2012 14:36:34 -0500 Subject: [PATCH 12/35] Modified test case to pass regardless of JSON library --- djangorestframework/tests/renderers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index adb46f7fa..77b5430fa 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -1,3 +1,5 @@ +import re + from django.conf.urls.defaults import patterns, url from django.test import TestCase @@ -187,6 +189,8 @@ class JSONRendererTests(TestCase): obj = {'foo': ['bar', 'baz']} renderer = JSONRenderer(None) content = renderer.render(obj, 'application/json') + # Fix failing test case which depends on version of JSON library. + content = re.sub(' +\n', '\n', content) self.assertEquals(content, _flat_repr) def test_with_content_type_args(self): From 25baa578f9f4f7ca30c7c744f9373923b63f9fba Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 24 Jan 2012 20:27:22 +0000 Subject: [PATCH 13/35] Added @btimby --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 14d86c263..11f6af5b8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,6 +27,7 @@ Natim Sebastian Żurek Benoit C Chris Pickett + THANKS TO: From 2c3f03c7cbecab59287788895a37272094ea49d3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 24 Jan 2012 22:04:22 +0000 Subject: [PATCH 14/35] No longer nameless --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 11f6af5b8..8d33f8501 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,7 +27,7 @@ Natim Sebastian Żurek Benoit C Chris Pickett - +Ben Timby THANKS TO: From c1fe5da85f936453a592fa683928672bb1911217 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 25 Jan 2012 20:39:01 +0000 Subject: [PATCH 15/35] Refactoring get_name/get_description --- djangorestframework/tests/renderers.py | 10 ++- djangorestframework/views.py | 104 ++++++++++++++++--------- 2 files changed, 77 insertions(+), 37 deletions(-) diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index 77b5430fa..9a02d0a9a 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -177,6 +177,13 @@ _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' +def strip_trailing_whitespace(content): + """ + Seems to be some inconsistencies re. trailing whitespace with + different versions of the json lib. + """ + return re.sub(' +\n', '\n', content) + class JSONRendererTests(TestCase): """ Tests specific to the JSON Renderer @@ -190,7 +197,6 @@ class JSONRendererTests(TestCase): renderer = JSONRenderer(None) content = renderer.render(obj, 'application/json') # Fix failing test case which depends on version of JSON library. - content = re.sub(' +\n', '\n', content) self.assertEquals(content, _flat_repr) def test_with_content_type_args(self): @@ -200,7 +206,7 @@ class JSONRendererTests(TestCase): obj = {'foo': ['bar', 'baz']} renderer = JSONRenderer(None) content = renderer.render(obj, 'application/json; indent=2') - self.assertEquals(content, _indented_repr) + self.assertEquals(strip_trailing_whitespace(content), _indented_repr) def test_render_and_parse(self): """ diff --git a/djangorestframework/views.py b/djangorestframework/views.py index d89c2afd9..88d81d25b 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -27,6 +27,46 @@ __all__ = ( ) +def _remove_trailing_string(content, trailing): + """ + Strip trailing component `trailing` from `content` if it exists. + Used when generating names from view/resource classes. + """ + if content.endswith(trailing) and content != trailing: + return content[:-len(trailing)] + return content + +def _remove_leading_indent(content): + """ + Remove leading indent from a block of text. + Used when generating descriptions from docstrings. + """ + whitespace_counts = [len(line) - len(line.lstrip(' ')) + for line in content.splitlines()[1:] if line.lstrip()] + + # unindent the content if needed + if whitespace_counts: + whitespace_pattern = '^' + (' ' * min(whitespace_counts)) + return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) + return content + +def _camelcase_to_spaces(content): + """ + Translate 'CamelCaseNames' to 'Camel Case Names'. + Used when generating names from view/resource classes. + """ + camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' + return re.sub(camelcase_boundry, ' \\1', content).strip() + + +_resource_classes = ( + None, + resources.Resource, + resources.FormResource, + resources.ModelResource +) + + class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): """ Handles incoming requests and maps them to REST operations. @@ -84,52 +124,46 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): Override to customize. """ # If this view has a resource that's been overridden, then use that resource for the name - if getattr(self, 'resource', None) not in (None, resources.Resource, resources.FormResource, resources.ModelResource): + if getattr(self, 'resource', None) not in _resource_classes: name = self.resource.__name__ + name = _remove_trailing_string(name, 'Resource') + name += getattr(self, '_suffix', '') - # Chomp of any non-descriptive trailing part of the resource class name - if name.endswith('Resource') and name != 'Resource': - name = name[:-len('Resource')] - - # If the view has a descriptive suffix, eg '*** List', '*** Instance' - if getattr(self, '_suffix', None): - name += self._suffix # If it's a view class with no resource then grok the name from the class name - elif getattr(self, '__class__', None) is not None: - name = self.__class__.__name__ - - # Chomp of any non-descriptive trailing part of the view class name - if name.endswith('View') and name != 'View': - name = name[:-len('View')] else: - name = '' - return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip() + name = self.__class__.__name__ + name = _remove_trailing_string(name, 'View') + + return _camelcase_to_spaces(name) def get_description(self, html=False): """ Return the resource or view docstring for use as this view's description. Override to customize. """ - # If this view has a resource that's been overridden, then use the resource's doctring - if getattr(self, 'resource', None) not in (None, resources.Resource, resources.FormResource, resources.ModelResource): - doc = self.resource.__doc__ - # Otherwise use the view doctring - elif getattr(self, '__doc__', None): - doc = self.__doc__ + + description = None + + # If this view has a resource that's been overridden, + # then try to use the resource's docstring + if getattr(self, 'resource', None) not in _resource_classes: + description = self.resource.__doc__ + + # Otherwise use the view docstring + if not description: + description = self.__doc__ or '' + + description = _remove_leading_indent(description) + + if html: + return self.markup_description(description) + return description + + def markup_description(self, description): + if apply_markdown: + return apply_markdown(description) else: - doc = '' - whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in doc.splitlines()[1:] if line.lstrip()] - # unindent the docstring if needed - if whitespace_counts: - whitespace_pattern = '^' + (' ' * min(whitespace_counts)) - doc = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', doc) - if doc and html: - if apply_markdown: - doc = apply_markdown(doc) - else: - doc = escape(doc) - doc = mark_safe(doc.replace('\n', '
')) - return doc + return mark_safe(escape(description).replace('\n', '
')) def http_method_not_allowed(self, request, *args, **kwargs): """ From cb6f62b2afdb00d04a251dafa1a8d7a6bf620737 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Fri, 27 Jan 2012 20:45:53 +0100 Subject: [PATCH 16/35] fixes #146 rest.ep.io now runs tip. --- examples/requirements-epio.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/requirements-epio.txt b/examples/requirements-epio.txt index 76ab5bd23..b49626763 100644 --- a/examples/requirements-epio.txt +++ b/examples/requirements-epio.txt @@ -1,3 +1,3 @@ Pygments==1.4 Markdown==2.0.3 -djangorestframework +git+git://github.com/tomchristie/django-rest-framework.git From 7af16dc762090a1184fd98fac9f4df9b733a18be Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sat, 28 Jan 2012 12:47:16 +0100 Subject: [PATCH 17/35] Thanks @michelelazzzeri-nextage, if you give your full name, it will be listed here explicitly. --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 8d33f8501..106dd14c9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,6 +28,7 @@ Sebastian Żurek Benoit C Chris Pickett Ben Timby + THANKS TO: From 76a8c11cf1aeed605d1da895a44fbd8630f8a30a Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sat, 28 Jan 2012 13:37:47 +0100 Subject: [PATCH 18/35] Preparing release notes for taggging and uploading to pypi. --- RELEASES | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/RELEASES b/RELEASES index 85d26c711..d41ea1859 100644 --- a/RELEASES +++ b/RELEASES @@ -1,3 +1,15 @@ +0.3.2 + +* Bugfixes: + * Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115) + * serialize_model method in serializer.py may cause wrong value (#73) + * Fix Error when clicking OPTIONS button (#146) + * And many other fixes + +0.3.1 + +* [not documented] + 0.3.0 * JSONP Support From a9e0159481a1b52e396bb0edee5f03563226e6da Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sat, 28 Jan 2012 13:38:29 +0100 Subject: [PATCH 19/35] prepare for tagging --- djangorestframework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py index 55f92cc4b..a5ba90e23 100644 --- a/djangorestframework/__init__.py +++ b/djangorestframework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.3.2-dev' +__version__ = '0.3.2' VERSION = __version__ # synonym From cc226e7e67b13a25c59a727581f4fd780f5d553a Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sat, 28 Jan 2012 14:06:34 +0100 Subject: [PATCH 20/35] moving forward --- djangorestframework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py index a5ba90e23..55f92cc4b 100644 --- a/djangorestframework/__init__.py +++ b/djangorestframework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.3.2' +__version__ = '0.3.2-dev' VERSION = __version__ # synonym From bbfa404e4679f4229e44fd7e641e62fdd2e7bdd5 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sat, 28 Jan 2012 15:27:06 +0100 Subject: [PATCH 21/35] Fix silly error. This makes more sense. --- djangorestframework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py index 55f92cc4b..0aaa29152 100644 --- a/djangorestframework/__init__.py +++ b/djangorestframework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.3.2-dev' +__version__ = '0.3.3-dev' VERSION = __version__ # synonym From 22ee89f0f3fa295a265547336f837261bb919f34 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 28 Jan 2012 14:38:06 +0000 Subject: [PATCH 22/35] Tidy up auto-escaping. --- djangorestframework/templates/renderer.html | 2 +- djangorestframework/templates/renderer.txt | 4 ++-- djangorestframework/views.py | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html index ff761023f..5faa8b3e1 100644 --- a/djangorestframework/templates/renderer.html +++ b/djangorestframework/templates/renderer.html @@ -50,7 +50,7 @@

{{ name }}

-

{% autoescape off %}{{ description }}{% endautoescape %}

+

{{ description }}

{{ response.status }} {{ response.status_text }}{% autoescape off %}
 {% for key, val in response.headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
diff --git a/djangorestframework/templates/renderer.txt b/djangorestframework/templates/renderer.txt
index 5be8c1175..b584952cb 100644
--- a/djangorestframework/templates/renderer.txt
+++ b/djangorestframework/templates/renderer.txt
@@ -1,8 +1,8 @@
-{{ name }}
+{% autoescape off %}{{ name }}
 
 {{ description }}
 
-{% autoescape off %}HTTP/1.0 {{ response.status }} {{ response.status_text }}
+HTTP/1.0 {{ response.status }} {{ response.status_text }}
 {% for key, val in response.headers.items %}{{ key }}: {{ val }}
 {% endfor %}
 {{ content }}{% endautoescape %}
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 88d81d25b..32d2437c1 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -36,6 +36,7 @@ def _remove_trailing_string(content, trailing):
         return content[:-len(trailing)]
     return content
 
+
 def _remove_leading_indent(content):
     """
     Remove leading indent from a block of text.
@@ -50,6 +51,7 @@ def _remove_leading_indent(content):
         return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
     return content
 
+
 def _camelcase_to_spaces(content):
     """
     Translate 'CamelCaseNames' to 'Camel Case Names'.
@@ -161,9 +163,10 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
 
     def markup_description(self, description):
         if apply_markdown:
-            return apply_markdown(description)
+            description = apply_markdown(description)
         else:
-            return mark_safe(escape(description).replace('\n', '
')) + description = escape(description).replace('\n', '
') + return mark_safe(description) def http_method_not_allowed(self, request, *args, **kwargs): """ From f34ed6d1f3d8c1c4c5059beba944aa386e9a15ab Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 28 Jan 2012 17:45:13 +0000 Subject: [PATCH 23/35] 0.3.2 Release notes --- RELEASES | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASES b/RELEASES index d41ea1859..facdfee2b 100644 --- a/RELEASES +++ b/RELEASES @@ -1,3 +1,7 @@ +0.3.3 (dev) + +* Saner template varible autoescaping + 0.3.2 * Bugfixes: @@ -5,6 +9,10 @@ * serialize_model method in serializer.py may cause wrong value (#73) * Fix Error when clicking OPTIONS button (#146) * And many other fixes +* Remove short status codes + - Zen of Python: "There should be one-- and preferably only one --obvious way to do it." +* get_name, get_description become methods on the view - makes them overridable. +* Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering 0.3.1 From 765ec0b76ea22c857190ee6a25e9e6615e5b0c5e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 28 Jan 2012 18:53:55 +0000 Subject: [PATCH 24/35] Use `staticfiles` for serving css. Fixes #116. --- .../static/css/djangorestframework.css | 1152 +++++++++++++++++ djangorestframework/templates/renderer.html | 29 +- examples/settings.py | 13 +- examples/urls.py | 2 + 4 files changed, 1167 insertions(+), 29 deletions(-) create mode 100644 djangorestframework/static/css/djangorestframework.css diff --git a/djangorestframework/static/css/djangorestframework.css b/djangorestframework/static/css/djangorestframework.css new file mode 100644 index 000000000..8fc4bace5 --- /dev/null +++ b/djangorestframework/static/css/djangorestframework.css @@ -0,0 +1,1152 @@ +/********************** admin 'base.css' ************************/ + +body { + margin: 0; + padding: 0; + font-size: 12px; + font-family: "Lucida Grande","DejaVu Sans","Bitstream Vera Sans",Verdana,Arial,sans-serif; + color: #333; + background: #fff; +} + +/* LINKS */ + +a:link, a:visited { + color: #5b80b2; + text-decoration: none; +} + +a:hover { + color: #036; +} + +a img { + border: none; +} + +a.section:link, a.section:visited { + color: white; + text-decoration: none; +} + +/* GLOBAL DEFAULTS */ + +p, ol, ul, dl { + margin: .2em 0 .8em 0; +} + +p { + padding: 0; + line-height: 140%; +} + +h1,h2,h3,h4,h5 { + font-weight: bold; +} + +h1 { + font-size: 18px; + color: #666; + padding: 0 6px 0 0; + margin: 0 0 .2em 0; +} + +h2 { + font-size: 16px; + margin: 1em 0 .5em 0; +} + +h2.subhead { + font-weight: normal; + margin-top: 0; +} + +h3 { + font-size: 14px; + margin: .8em 0 .3em 0; + color: #666; + font-weight: bold; +} + +h4 { + font-size: 12px; + margin: 1em 0 .8em 0; + padding-bottom: 3px; +} + +h5 { + font-size: 10px; + margin: 1.5em 0 .5em 0; + color: #666; + text-transform: uppercase; + letter-spacing: 1px; +} + +ul li { + list-style-type: square; + padding: 1px 0; +} + +ul.plainlist { + margin-left: 0 !important; +} + +ul.plainlist li { + list-style-type: none; +} + +li ul { + margin-bottom: 0; +} + +li, dt, dd { + font-size: 11px; + line-height: 14px; +} + +dt { + font-weight: bold; + margin-top: 4px; +} + +dd { + margin-left: 0; +} + +form { + margin: 0; + padding: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +blockquote { + font-size: 11px; + color: #777; + margin-left: 2px; + padding-left: 10px; + border-left: 5px solid #ddd; +} + +code, pre { + font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; + background: inherit; + color: #666; + font-size: 11px; +} + +pre.literal-block { + margin: 10px; + background: #eee; + padding: 6px 8px; +} + +code strong { + color: #930; +} + +hr { + clear: both; + color: #eee; + background-color: #eee; + height: 1px; + border: none; + margin: 0; + padding: 0; + font-size: 1px; + line-height: 1px; +} + +/* TEXT STYLES & MODIFIERS */ + +.small { + font-size: 11px; +} + +.tiny { + font-size: 10px; +} + +p.tiny { + margin-top: -2px; +} + +.mini { + font-size: 9px; +} + +p.mini { + margin-top: -3px; +} + +.help, p.help { + font-size: 10px !important; + color: #999; +} + +p img, h1 img, h2 img, h3 img, h4 img, td img { + vertical-align: middle; +} + +.quiet, a.quiet:link, a.quiet:visited { + color: #999 !important; + font-weight: normal !important; +} + +.quiet strong { + font-weight: bold !important; +} + +.float-right { + float: right; +} + +.float-left { + float: left; +} + +.clear { + clear: both; +} + +.align-left { + text-align: left; +} + +.align-right { + text-align: right; +} + +.example { + margin: 10px 0; + padding: 5px 10px; + background: #efefef; +} + +.nowrap { + white-space: nowrap; +} + +/* TABLES */ + +table { + border-collapse: collapse; + border-color: #ccc; +} + +td, th { + font-size: 11px; + line-height: 13px; + border-bottom: 1px solid #eee; + vertical-align: top; + padding: 5px; + font-family: "Lucida Grande", Verdana, Arial, sans-serif; +} + +th { + text-align: left; + font-size: 12px; + font-weight: bold; +} + +thead th, +tfoot td { + color: #666; + padding: 2px 5px; + font-size: 11px; + background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; + border-left: 1px solid #ddd; + border-bottom: 1px solid #ddd; +} + +tfoot td { + border-bottom: none; + border-top: 1px solid #ddd; +} + +thead th:first-child, +tfoot td:first-child { + border-left: none !important; +} + +thead th.optional { + font-weight: normal !important; +} + +fieldset table { + border-right: 1px solid #eee; +} + +tr.row-label td { + font-size: 9px; + padding-top: 2px; + padding-bottom: 0; + border-bottom: none; + color: #666; + margin-top: -1px; +} + +tr.alt { + background: #f6f6f6; +} + +.row1 { + background: #EDF3FE; +} + +.row2 { + background: white; +} + +/* SORTABLE TABLES */ + +thead th a:link, thead th a:visited { + color: #666; + display: block; +} + +table thead th.sorted { + background-position: bottom left !important; +} + +table thead th.sorted a { + padding-right: 13px; +} + +table thead th.ascending a { + background: url(../img/admin/arrow-up.gif) right .4em no-repeat; +} + +table thead th.descending a { + background: url(../img/admin/arrow-down.gif) right .4em no-repeat; +} + +/* ORDERABLE TABLES */ + +table.orderable tbody tr td:hover { + cursor: move; +} + +table.orderable tbody tr td:first-child { + padding-left: 14px; + background-image: url(../img/admin/nav-bg-grabber.gif); + background-repeat: repeat-y; +} + +table.orderable-initalized .order-cell, body>tr>td.order-cell { + display: none; +} + +/* FORM DEFAULTS */ + +input, textarea, select, .form-row p { + margin: 2px 0; + padding: 2px 3px; + vertical-align: middle; + font-family: "Lucida Grande", Verdana, Arial, sans-serif; + font-weight: normal; + font-size: 11px; +} + +textarea { + vertical-align: top !important; +} + +input[type=text], input[type=password], textarea, select, .vTextField { + border: 1px solid #ccc; +} + +/* FORM BUTTONS */ + +.button, input[type=submit], input[type=button], .submit-row input { + background: white url(../img/admin/nav-bg.gif) bottom repeat-x; + padding: 3px 5px; + color: black; + border: 1px solid #bbb; + border-color: #ddd #aaa #aaa #ddd; +} + +.button:active, input[type=submit]:active, input[type=button]:active { + background-image: url(../img/admin/nav-bg-reverse.gif); + background-position: top; +} + +.button[disabled], input[type=submit][disabled], input[type=button][disabled] { + background-image: url(../img/admin/nav-bg.gif); + background-position: bottom; + opacity: 0.4; +} + +.button.default, input[type=submit].default, .submit-row input.default { + border: 2px solid #5b80b2; + background: #7CA0C7 url(../img/admin/default-bg.gif) bottom repeat-x; + font-weight: bold; + color: white; + float: right; +} + +.button.default:active, input[type=submit].default:active { + background-image: url(../img/admin/default-bg-reverse.gif); + background-position: top; +} + +.button[disabled].default, input[type=submit][disabled].default, input[type=button][disabled].default { + background-image: url(../img/admin/default-bg.gif); + background-position: bottom; + opacity: 0.4; +} + + +/* MODULES */ + +.module { + border: 1px solid #ccc; + margin-bottom: 5px; + background: white; +} + +.module p, .module ul, .module h3, .module h4, .module dl, .module pre { + padding-left: 10px; + padding-right: 10px; +} + +.module blockquote { + margin-left: 12px; +} + +.module ul, .module ol { + margin-left: 1.5em; +} + +.module h3 { + margin-top: .6em; +} + +.module h2, .module caption, .inline-group h2 { + margin: 0; + padding: 2px 5px 3px 5px; + font-size: 11px; + text-align: left; + font-weight: bold; + background: #7CA0C7 url(../img/admin/default-bg.gif) top left repeat-x; + color: white; +} + +.module table { + border-collapse: collapse; +} + +/* MESSAGES & ERRORS */ + +ul.messagelist { + padding: 0 0 5px 0; + margin: 0; +} + +ul.messagelist li { + font-size: 12px; + display: block; + padding: 4px 5px 4px 25px; + margin: 0 0 3px 0; + border-bottom: 1px solid #ddd; + color: #666; + background: #ffc url(../img/admin/icon_success.gif) 5px .3em no-repeat; +} + +ul.messagelist li.warning{ + background-image: url(../img/admin/icon_alert.gif); +} + +ul.messagelist li.error{ + background-image: url(../img/admin/icon_error.gif); +} + +.errornote { + font-size: 12px !important; + display: block; + padding: 4px 5px 4px 25px; + margin: 0 0 3px 0; + border: 1px solid red; + color: red; + background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat; +} + +ul.errorlist { + margin: 0 !important; + padding: 0 !important; +} + +.errorlist li { + font-size: 12px !important; + display: block; + padding: 4px 5px 4px 25px; + margin: 0 0 3px 0; + border: 1px solid red; + color: white; + background: red url(../img/admin/icon_alert.gif) 5px .3em no-repeat; +} + +.errorlist li a { + color: white; + text-decoration: underline; +} + +td ul.errorlist { + margin: 0 !important; + padding: 0 !important; +} + +td ul.errorlist li { + margin: 0 !important; +} + +.errors { + background: #ffc; +} + +.errors input, .errors select, .errors textarea { + border: 1px solid red; +} + +div.system-message { + background: #ffc; + margin: 10px; + padding: 6px 8px; + font-size: .8em; +} + +div.system-message p.system-message-title { + padding: 4px 5px 4px 25px; + margin: 0; + color: red; + background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat; +} + +.description { + font-size: 12px; + padding: 5px 0 0 12px; +} + +/* BREADCRUMBS */ + +div.breadcrumbs { + background: white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; + padding: 2px 8px 3px 8px; + font-size: 11px; + color: #999; + border-top: 1px solid white; + border-bottom: 1px solid #ccc; + text-align: left; +} + +/* ACTION ICONS */ + +.addlink { + padding-left: 12px; + background: url(../img/admin/icon_addlink.gif) 0 .2em no-repeat; +} + +.changelink { + padding-left: 12px; + background: url(../img/admin/icon_changelink.gif) 0 .2em no-repeat; +} + +.deletelink { + padding-left: 12px; + background: url(../img/admin/icon_deletelink.gif) 0 .25em no-repeat; +} + +a.deletelink:link, a.deletelink:visited { + color: #CC3434; +} + +a.deletelink:hover { + color: #993333; +} + +/* OBJECT TOOLS */ + +.object-tools { + font-size: 10px; + font-weight: bold; + font-family: Arial,Helvetica,sans-serif; + padding-left: 0; + float: right; + position: relative; + margin-top: -2.4em; + margin-bottom: -2em; +} + +.form-row .object-tools { + margin-top: 5px; + margin-bottom: 5px; + float: none; + height: 2em; + padding-left: 3.5em; +} + +.object-tools li { + display: block; + float: left; + background: url(../img/admin/tool-left.gif) 0 0 no-repeat; + padding: 0 0 0 8px; + margin-left: 2px; + height: 16px; +} + +.object-tools li:hover { + background: url(../img/admin/tool-left_over.gif) 0 0 no-repeat; +} + +.object-tools a:link, .object-tools a:visited { + display: block; + float: left; + color: white; + padding: .1em 14px .1em 8px; + height: 14px; + background: #999 url(../img/admin/tool-right.gif) 100% 0 no-repeat; +} + +.object-tools a:hover, .object-tools li:hover a { + background: #5b80b2 url(../img/admin/tool-right_over.gif) 100% 0 no-repeat; +} + +.object-tools a.viewsitelink, .object-tools a.golink { + background: #999 url(../img/admin/tooltag-arrowright.gif) top right no-repeat; + padding-right: 28px; +} + +.object-tools a.viewsitelink:hover, .object-tools a.golink:hover { + background: #5b80b2 url(../img/admin/tooltag-arrowright_over.gif) top right no-repeat; +} + +.object-tools a.addlink { + background: #999 url(../img/admin/tooltag-add.gif) top right no-repeat; + padding-right: 28px; +} + +.object-tools a.addlink:hover { + background: #5b80b2 url(../img/admin/tooltag-add_over.gif) top right no-repeat; +} + +/* OBJECT HISTORY */ + +table#change-history { + width: 100%; +} + +table#change-history tbody th { + width: 16em; +} + +/* PAGE STRUCTURE */ + +#container { + position: relative; + width: 100%; + min-width: 760px; + padding: 0; +} + +#content { + margin: 10px 15px; +} + +#header { + width: 100%; +} + +#content-main { + float: left; + width: 100%; +} + +#content-related { + float: right; + width: 18em; + position: relative; + margin-right: -19em; +} + +#footer { + clear: both; + padding: 10px; +} + +/* COLUMN TYPES */ + +.colMS { + margin-right: 20em !important; +} + +.colSM { + margin-left: 20em !important; +} + +.colSM #content-related { + float: left; + margin-right: 0; + margin-left: -19em; +} + +.colSM #content-main { + float: right; +} + +.popup .colM { + width: 95%; +} + +.subcol { + float: left; + width: 46%; + margin-right: 15px; +} + +.dashboard #content { + width: 500px; +} + +/* HEADER */ + +#header { + background: #417690; + color: #ffc; + overflow: hidden; +} + +#header a:link, #header a:visited { + color: white; +} + +#header a:hover { + text-decoration: underline; +} + +#branding h1 { + padding: 0 10px; + font-size: 18px; + margin: 8px 0; + font-weight: normal; + color: #f4f379; +} + +#branding h2 { + padding: 0 10px; + font-size: 14px; + margin: -8px 0 8px 0; + font-weight: normal; + color: #ffc; +} + +#user-tools { + position: absolute; + top: 0; + right: 0; + padding: 1.2em 10px; + font-size: 11px; + text-align: right; +} + +/* SIDEBAR */ + +#content-related h3 { + font-size: 12px; + color: #666; + margin-bottom: 3px; +} + +#content-related h4 { + font-size: 11px; +} + +#content-related .module h2 { + background: #eee url(../img/admin/nav-bg.gif) bottom left repeat-x; + color: #666; +} + +/********************** admin 'forms.css' ************************/ + +/* FORM ROWS */ + +.form-row { + overflow: hidden; + padding: 8px 12px; + font-size: 11px; + border-bottom: 1px solid #eee; +} + +.form-row img, .form-row input { + vertical-align: middle; +} + +form .form-row p { + padding-left: 0; + font-size: 11px; +} + +/* FORM LABELS */ + +form h4 { + margin: 0 !important; + padding: 0 !important; + border: none !important; +} + +label { + font-weight: normal !important; + color: #666; + font-size: 12px; +} + +.required label, label.required { + font-weight: bold !important; + color: #333 !important; +} + +/* RADIO BUTTONS */ + +form ul.radiolist li { + list-style-type: none; +} + +form ul.radiolist label { + float: none; + display: inline; +} + +form ul.inline { + margin-left: 0; + padding: 0; +} + +form ul.inline li { + float: left; + padding-right: 7px; +} + +/* ALIGNED FIELDSETS */ + +.aligned label { + display: block; + padding: 3px 10px 0 0; + float: left; + width: 8em; +} + +.aligned ul label { + display: inline; + float: none; + width: auto; +} + +.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { + width: 350px; +} + +form .aligned p, form .aligned ul { + margin-left: 7em; + padding-left: 30px; +} + +form .aligned table p { + margin-left: 0; + padding-left: 0; +} + +form .aligned p.help { + padding-left: 38px; +} + +.aligned .vCheckboxLabel { + float: none !important; + display: inline; + padding-left: 4px; +} + +.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField { + width: 610px; +} + +.checkbox-row p.help { + margin-left: 0; + padding-left: 0 !important; +} + +fieldset .field-box { + float: left; + margin-right: 20px; +} + +/* WIDE FIELDSETS */ + +.wide label { + width: 15em !important; +} + +form .wide p { + margin-left: 15em; +} + +form .wide p.help { + padding-left: 38px; +} + +.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { + width: 450px; +} + +/* COLLAPSED FIELDSETS */ + +fieldset.collapsed * { + display: none; +} + +fieldset.collapsed h2, fieldset.collapsed { + display: block !important; +} + +fieldset.collapsed h2 { + background-image: url(../img/admin/nav-bg.gif); + background-position: bottom left; + color: #999; +} + +fieldset.collapsed .collapse-toggle { + background: transparent; + display: inline !important; +} + +/* MONOSPACE TEXTAREAS */ + +fieldset.monospace textarea { + font-family: "Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace; +} + +/* SUBMIT ROW */ + +.submit-row { + padding: 5px 7px; + text-align: right; + background: white url(../img/admin/nav-bg.gif) 0 100% repeat-x; + border: 1px solid #ccc; + margin: 5px 0; + overflow: hidden; +} + +.submit-row input { + margin: 0 0 0 5px; +} + +.submit-row p { + margin: 0.3em; +} + +.submit-row p.deletelink-box { + float: left; +} + +.submit-row .deletelink { + background: url(../img/admin/icon_deletelink.gif) 0 50% no-repeat; + padding-left: 14px; +} + +/* CUSTOM FORM FIELDS */ + +.vSelectMultipleField { + vertical-align: top !important; +} + +.vCheckboxField { + border: none; +} + +.vDateField, .vTimeField { + margin-right: 2px; +} + +.vURLField { + width: 30em; +} + +.vLargeTextField, .vXMLLargeTextField { + width: 48em; +} + +.flatpages-flatpage #id_content { + height: 40.2em; +} + +.module table .vPositiveSmallIntegerField { + width: 2.2em; +} + +.vTextField { + width: 20em; +} + +.vIntegerField { + width: 5em; +} + +.vForeignKeyRawIdAdminField { + width: 5em; +} + +/* INLINES */ + +.inline-group { + padding: 0; + border: 1px solid #ccc; + margin: 10px 0; +} + +.inline-group .aligned label { + width: 8em; +} + +.inline-related { + position: relative; +} + +.inline-related h3 { + margin: 0; + color: #666; + padding: 3px 5px; + font-size: 11px; + background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; + border-bottom: 1px solid #ddd; +} + +.inline-related h3 span.delete { + float: right; +} + +.inline-related h3 span.delete label { + margin-left: 2px; + font-size: 11px; +} + +.inline-related fieldset { + margin: 0; + background: #fff; + border: none; +} + +.inline-related fieldset.module h3 { + margin: 0; + padding: 2px 5px 3px 5px; + font-size: 11px; + text-align: left; + font-weight: bold; + background: #bcd; + color: #fff; +} + +.inline-group .tabular fieldset.module { + border: none; + border-bottom: 1px solid #ddd; +} + +.inline-related.tabular fieldset.module table { + width: 100%; +} + +.last-related fieldset { + border: none; +} + +.inline-group .tabular tr.has_original td { + padding-top: 2em; +} + +.inline-group .tabular tr td.original { + padding: 2px 0 0 0; + width: 0; + _position: relative; +} + +.inline-group .tabular th.original { + width: 0px; + padding: 0; +} + +.inline-group .tabular td.original p { + position: absolute; + left: 0; + height: 1.1em; + padding: 2px 7px; + overflow: hidden; + font-size: 9px; + font-weight: bold; + color: #666; + _width: 700px; +} + +.inline-group ul.tools { + padding: 0; + margin: 0; + list-style: none; +} + +.inline-group ul.tools li { + display: inline; + padding: 0 5px; +} + +.inline-group div.add-row, +.inline-group .tabular tr.add-row td { + color: #666; + padding: 3px 5px; + border-bottom: 1px solid #ddd; + background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; +} + +.inline-group .tabular tr.add-row td { + padding: 4px 5px 3px; + border-bottom: none; +} + +.inline-group ul.tools a.add, +.inline-group div.add-row a, +.inline-group .tabular tr.add-row td a { + background: url(../img/admin/icon_addlink.gif) 0 50% no-repeat; + padding-left: 14px; + font-size: 11px; + outline: 0; /* Remove dotted border around link */ +} + +.empty-form { + display: none; +} + +/* IE7 specific bug fixes */ + +.submit-row input { + float: right; +} + +/* Overrides specific to REST framework */ + +#site-name a { + color: #F4F379 !important; +} + +.errorlist { + display: inline !important; +} + +.errorlist li { + display: inline !important; + background: white !important; + color: black !important; + border: 0 !important; +} + +/* Custom styles */ +.version { + font-size: 8px; +} diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html index 5faa8b3e1..e396a58f5 100644 --- a/djangorestframework/templates/renderer.html +++ b/djangorestframework/templates/renderer.html @@ -1,25 +1,14 @@ -{% load urlize_quoted_links %}{% load add_query_param %} + +{% load urlize_quoted_links %} +{% load add_query_param %} +{% load static %} - - - {% if ADMIN_MEDIA_PREFIX %} - - - {% else %} - - - {% endif %} - Django REST framework - {{ name }} - + + + Django REST framework - {{ name }} +
@@ -34,7 +23,7 @@ diff --git a/examples/settings.py b/examples/settings.py index e12b7f3fe..a8846b1b3 100644 --- a/examples/settings.py +++ b/examples/settings.py @@ -53,16 +53,10 @@ MEDIA_ROOT = os.path.join(os.getenv('EPIO_DATA_DIRECTORY', '.'), 'media') # trailing slash if there is a path component (optional in other cases). # Examples: "http://media.lawrence.com", "http://example.com/media/" # NOTE: None of the djangorestframework examples serve media content via MEDIA_URL. -MEDIA_URL = '' +MEDIA_URL = '/uploads/' + +STATIC_URL = '/static/' -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". -# NOTE: djangorestframework does not require the admin app to be installed, -# but it does require the admin media be served. Django's test server will do -# this for you automatically, but in production you'll want to make sure you -# serve the admin media from somewhere. -ADMIN_MEDIA_PREFIX = '/static/admin' # Make this unique, and don't share it with anybody. SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu' @@ -102,6 +96,7 @@ INSTALLED_APPS = ( 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', + 'django.contrib.staticfiles', 'django.contrib.messages', 'djangorestframework', diff --git a/examples/urls.py b/examples/urls.py index 08d97a14a..bd4087fdd 100644 --- a/examples/urls.py +++ b/examples/urls.py @@ -1,6 +1,7 @@ from django.conf.urls.defaults import patterns, include, url from django.conf import settings from sandbox.views import Sandbox +from django.contrib.staticfiles.urls import staticfiles_urlpatterns urlpatterns = patterns('', (r'^$', Sandbox.as_view()), @@ -15,3 +16,4 @@ urlpatterns = patterns('', (r'^', include('djangorestframework.urls')), ) +urlpatterns += staticfiles_urlpatterns() From c0674e36d498b7b727e3d5a260b5cdf4104b4d26 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 28 Jan 2012 19:06:40 +0000 Subject: [PATCH 25/35] Drop implicit 'pk' on last arg in urlconf. (Too magical). --- djangorestframework/mixins.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 7f0870f85..2ea5e8404 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -507,11 +507,6 @@ class ModelMixin(object): if BaseRenderer._FORMAT_QUERY_PARAM in tmp: del tmp[BaseRenderer._FORMAT_QUERY_PARAM] - if args: - # If we have any no kwargs then assume the last arg represents the - # primrary key. Otherwise assume the kwargs uniquely identify the - # model. - tmp.update({'pk': args[-1]}) return Q(**tmp) def get_instance_data(self, model, content, **kwargs): From f115be29e0c65adc474ed71aa94112aa7d5511c0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 28 Jan 2012 19:10:07 +0000 Subject: [PATCH 26/35] Latest changelog. --- RELEASES | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASES b/RELEASES index facdfee2b..8e67e7269 100644 --- a/RELEASES +++ b/RELEASES @@ -1,6 +1,10 @@ -0.3.3 (dev) +development -* Saner template varible autoescaping +* Saner template varible autoescaping. +* Use `staticfiles` for css files. + - Easier to override. Won't conflict with customised admin styles (eg grappelli) +* Drop implied 'pk' filter if last arg in urlconf is unnamed. + - Too magical. Explict is better than implicit. 0.3.2 From 289e421543830caa153a8ceb4f9685edced84fad Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 29 Jan 2012 13:46:04 +0100 Subject: [PATCH 27/35] Add Michele's full name. --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 106dd14c9..2466de043 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,7 +28,7 @@ Sebastian Żurek Benoit C Chris Pickett Ben Timby - +Michele Lazzeri THANKS TO: From c7a805603405e21897b1788408beb5165685a2ec Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 29 Jan 2012 13:19:56 +0000 Subject: [PATCH 28/35] Use named args in mixin tests. --- djangorestframework/tests/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangorestframework/tests/mixins.py b/djangorestframework/tests/mixins.py index 88b13dd5f..a7512efc7 100644 --- a/djangorestframework/tests/mixins.py +++ b/djangorestframework/tests/mixins.py @@ -30,7 +30,7 @@ class TestModelRead(TestModelsTestCase): mixin = ReadModelMixin() mixin.resource = GroupResource - response = mixin.get(request, group.id) + response = mixin.get(request, id=group.id) self.assertEquals(group.name, response.name) def test_read_404(self): @@ -41,7 +41,7 @@ class TestModelRead(TestModelsTestCase): mixin = ReadModelMixin() mixin.resource = GroupResource - self.assertRaises(ErrorResponse, mixin.get, request, 12345) + self.assertRaises(ErrorResponse, mixin.get, request, id=12345) class TestModelCreation(TestModelsTestCase): From 278b3576f311ad49a95b8efb997a194e5211df57 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 30 Jan 2012 14:13:57 +0000 Subject: [PATCH 29/35] Fixes #148. Thanks @dvinegla. --- djangorestframework/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index 945023cec..d832c5feb 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -188,7 +188,7 @@ class PerUserThrottling(BaseThrottle): def get_cache_key(self): if self.auth.is_authenticated(): - ident = str(self.auth) + ident = unicode(self.auth) else: ident = self.view.request.META.get('REMOTE_ADDR', None) return 'throttle_user_%s' % ident From 9746bb34cccd3158c0b13c0559342d9f88c07fdb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 30 Jan 2012 14:14:48 +0000 Subject: [PATCH 30/35] Update RELEASES --- RELEASES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASES b/RELEASES index 8e67e7269..5dd0ce779 100644 --- a/RELEASES +++ b/RELEASES @@ -5,6 +5,8 @@ development - Easier to override. Won't conflict with customised admin styles (eg grappelli) * Drop implied 'pk' filter if last arg in urlconf is unnamed. - Too magical. Explict is better than implicit. +* Bugfixes: + - Bug with PerUserThrottling when user contains unicode chars. 0.3.2 From 50c359c551728b8cdbb963a9f233c635065effb0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 30 Jan 2012 15:54:38 +0000 Subject: [PATCH 31/35] Refs #148 --- djangorestframework/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index d832c5feb..dfe55ce94 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -188,7 +188,7 @@ class PerUserThrottling(BaseThrottle): def get_cache_key(self): if self.auth.is_authenticated(): - ident = unicode(self.auth) + ident = self.auth.id else: ident = self.view.request.META.get('REMOTE_ADDR', None) return 'throttle_user_%s' % ident From 243d80ccb0a6df6ce727356f1ffbd5a55098dfb9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 30 Jan 2012 16:05:13 +0000 Subject: [PATCH 32/35] Django 1.2 needs django-staticfiles --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 2e6ab204c..7601cd02d 100644 --- a/tox.ini +++ b/tox.ini @@ -34,6 +34,7 @@ deps= django==1.2.4 coverage==3.4 URLObject>=0.6.0 + django-staticfiles>=1.1.2 unittest-xml-reporting==1.2 Pyyaml==3.10 # Optional packages: @@ -45,6 +46,7 @@ deps= django==1.2.4 coverage==3.4 URLObject>=0.6.0 + django-staticfiles>=1.1.2 unittest-xml-reporting==1.2 Pyyaml==3.10 # Optional packages: @@ -56,6 +58,7 @@ deps= django==1.2.4 coverage==3.4 URLObject>=0.6.0 + django-staticfiles>=1.1.2 unittest-xml-reporting==1.2 Pyyaml==3.10 # Optional packages: From 39d4a39441485bb5783fb0cc400a38a8feab14f6 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Mon, 30 Jan 2012 18:29:54 +0100 Subject: [PATCH 33/35] renamed to CHANGELOG.rst and included release notes in index.rst --- RELEASES => CHANGELOG.rst | 14 ++++++++++++++ docs/index.rst | 2 ++ 2 files changed, 16 insertions(+) rename RELEASES => CHANGELOG.rst (96%) diff --git a/RELEASES b/CHANGELOG.rst similarity index 96% rename from RELEASES rename to CHANGELOG.rst index 5dd0ce779..152f22671 100644 --- a/RELEASES +++ b/CHANGELOG.rst @@ -1,4 +1,8 @@ +Release Notes +============= + development +----------- * Saner template varible autoescaping. * Use `staticfiles` for css files. @@ -9,6 +13,7 @@ development - Bug with PerUserThrottling when user contains unicode chars. 0.3.2 +----- * Bugfixes: * Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115) @@ -21,15 +26,18 @@ development * Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering 0.3.1 +----- * [not documented] 0.3.0 +----- * JSONP Support * Bugfixes, including support for latest markdown release 0.2.4 +----- * Fix broken IsAdminUser permission. * OPTIONS support. @@ -37,20 +45,24 @@ development * Drop mentions of Blog, BitBucket. 0.2.3 +----- * Fix some throttling bugs. * ``X-Throttle`` header on throttling. * Support for nesting resources on related models. 0.2.2 +----- * Throttling support complete. 0.2.1 +----- * Couple of simple bugfixes over 0.2.0 0.2.0 +----- * Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear. The public API has been massively cleaned up. Expect it to be fairly stable from here on in. @@ -75,9 +87,11 @@ development You can reuse these mixin classes individually without using the ``View`` class. 0.1.1 +----- * Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1. 0.1.0 +----- * Initial release. diff --git a/docs/index.rst b/docs/index.rst index 0221ea052..ecc1f1182 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -105,6 +105,8 @@ The following example exposes your `MyModel` model through an api. It will provi contents +.. include:: ../CHANGELOG.rst + Indices and tables ------------------ From 497d6df5e578b3ee750c65de7265b87fce2b0c7c Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Mon, 30 Jan 2012 21:24:20 +0100 Subject: [PATCH 34/35] Fix typo. --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 152f22671..9da9df2e1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Release Notes development ----------- -* Saner template varible autoescaping. +* Saner template variable autoescaping. * Use `staticfiles` for css files. - Easier to override. Won't conflict with customised admin styles (eg grappelli) * Drop implied 'pk' filter if last arg in urlconf is unnamed. From b2fcfffb3bdaed89d39ee563c58dc0ede5e857ac Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 31 Jan 2012 09:05:52 +0000 Subject: [PATCH 35/35] django-staticfiles for Django 1.2 compatability --- djangorestframework/runtests/settings.py | 6 ++++++ tox.ini | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/djangorestframework/runtests/settings.py b/djangorestframework/runtests/settings.py index 07855fa7a..25f771114 100644 --- a/djangorestframework/runtests/settings.py +++ b/djangorestframework/runtests/settings.py @@ -97,6 +97,12 @@ INSTALLED_APPS = ( 'djangorestframework', ) +import django + +if django.VERSION < (1, 3): + INSTALLED_APPS += ('staticfiles',) + + # OAuth support is optional, so we only test oauth if it's installed. try: import oauth_provider diff --git a/tox.ini b/tox.ini index 7601cd02d..8c3af2567 100644 --- a/tox.ini +++ b/tox.ini @@ -32,9 +32,9 @@ commands= basepython=python2.5 deps= django==1.2.4 + django-staticfiles>=1.1.2 coverage==3.4 URLObject>=0.6.0 - django-staticfiles>=1.1.2 unittest-xml-reporting==1.2 Pyyaml==3.10 # Optional packages: @@ -44,9 +44,9 @@ deps= basepython=python2.6 deps= django==1.2.4 + django-staticfiles>=1.1.2 coverage==3.4 URLObject>=0.6.0 - django-staticfiles>=1.1.2 unittest-xml-reporting==1.2 Pyyaml==3.10 # Optional packages: @@ -56,9 +56,9 @@ deps= basepython=python2.7 deps= django==1.2.4 + django-staticfiles>=1.1.2 coverage==3.4 URLObject>=0.6.0 - django-staticfiles>=1.1.2 unittest-xml-reporting==1.2 Pyyaml==3.10 # Optional packages: @@ -138,6 +138,7 @@ commands= python examples/runtests.py deps= django==1.2.4 + django-staticfiles>=1.1.2 coverage==3.4 URLObject>=0.6.0 wsgiref==0.1.2 @@ -153,6 +154,7 @@ commands= python examples/runtests.py deps= django==1.2.4 + django-staticfiles>=1.1.2 coverage==3.4 URLObject>=0.6.0 wsgiref==0.1.2 @@ -168,6 +170,7 @@ commands= python examples/runtests.py deps= django==1.2.4 + django-staticfiles>=1.1.2 coverage==3.4 URLObject>=0.6.0 wsgiref==0.1.2