mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-23 01:57:00 +03:00
merge
This commit is contained in:
commit
279fa0d371
2
AUTHORS
2
AUTHORS
|
@ -27,6 +27,8 @@ Natim <natim>
|
|||
Sebastian Żurek <sebzur>
|
||||
Benoit C <dzen>
|
||||
Chris Pickett <bunchesofdonald>
|
||||
Ben Timby <btimby>
|
||||
Michele Lazzeri <michelelazzeri-nextage>
|
||||
|
||||
THANKS TO:
|
||||
|
||||
|
|
|
@ -1,9 +1,43 @@
|
|||
Release Notes
|
||||
=============
|
||||
|
||||
development
|
||||
-----------
|
||||
|
||||
* 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.
|
||||
- Too magical. Explict is better than implicit.
|
||||
* Bugfixes:
|
||||
- Bug with PerUserThrottling when user contains unicode chars.
|
||||
|
||||
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
|
||||
* 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
|
||||
-----
|
||||
|
||||
* [not documented]
|
||||
|
||||
0.3.0
|
||||
-----
|
||||
|
||||
* JSONP Support
|
||||
* Bugfixes, including support for latest markdown release
|
||||
|
||||
0.2.4
|
||||
-----
|
||||
|
||||
* Fix broken IsAdminUser permission.
|
||||
* OPTIONS support.
|
||||
|
@ -11,20 +45,24 @@
|
|||
* 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.
|
||||
|
@ -49,9 +87,11 @@
|
|||
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.
|
|
@ -16,7 +16,7 @@ Full documentation for the project is available at http://django-rest-framework.
|
|||
Issue tracking is on `GitHub <https://github.com/tomchristie/django-rest-framework/issues>`_.
|
||||
General questions should be taken to the `discussion group <http://groups.google.com/group/django-rest-framework>`_.
|
||||
|
||||
We also have a `Jenkins service <http://jenkins.tibold.nl/job/djangorestframework/>`_ which runs our test suite.
|
||||
We also have a `Jenkins service <http://jenkins.tibold.nl/job/djangorestframework1/>`_ which runs our test suite.
|
||||
|
||||
Requirements:
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
__version__ = '0.3.2-dev'
|
||||
__version__ = '0.3.3-dev'
|
||||
|
||||
VERSION = __version__ # synonym
|
||||
|
|
|
@ -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.
|
||||
request.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(request.DATA, 'get'):
|
||||
request._post = request.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
|
||||
|
|
|
@ -384,11 +384,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):
|
||||
|
|
|
@ -188,7 +188,7 @@ class PerUserThrottling(BaseThrottle):
|
|||
|
||||
def get_cache_key(self):
|
||||
if self.auth.is_authenticated():
|
||||
ident = str(self.auth)
|
||||
ident = self.auth.id
|
||||
else:
|
||||
ident = self.view.request.META.get('REMOTE_ADDR', None)
|
||||
return 'throttle_user_%s' % ident
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -296,6 +295,20 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
|||
# Okey doke, let's do it
|
||||
return GenericContentForm(view.request)
|
||||
|
||||
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 +329,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)
|
||||
|
||||
|
@ -337,7 +343,6 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
|||
'description': description,
|
||||
'name': name,
|
||||
'version': VERSION,
|
||||
'markeddown': markeddown,
|
||||
'breadcrumblist': breadcrumb_list,
|
||||
'available_formats': self.view._rendered_formats,
|
||||
'put_form': put_form_instance,
|
||||
|
|
|
@ -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
|
||||
|
|
1152
djangorestframework/static/css/djangorestframework.css
Normal file
1152
djangorestframework/static/css/djangorestframework.css
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -1,25 +1,14 @@
|
|||
{% load urlize_quoted_links %}{% load add_query_param %}<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
{% load urlize_quoted_links %}
|
||||
{% load add_query_param %}
|
||||
{% load static %}
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<style>
|
||||
/* Override some of the Django admin styling */
|
||||
#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;}
|
||||
</style>
|
||||
{% if ADMIN_MEDIA_PREFIX %}
|
||||
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/base.css'/>
|
||||
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/forms.css'/>
|
||||
{% else %}
|
||||
<link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/base.css'/>
|
||||
<link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/forms.css'/>
|
||||
{% endif %}
|
||||
<title>Django REST framework - {{ name }}</title>
|
||||
</head>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}css/djangorestframework.css'/>
|
||||
<title>Django REST framework - {{ name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
|
||||
|
@ -34,7 +23,7 @@
|
|||
|
||||
<div class="breadcrumbs">
|
||||
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
|
||||
<a href="{{breadcrumb_url}}">{{breadcrumb_name}}</a> {% if not forloop.last %}›{% endif %}
|
||||
<a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a> {% if not forloop.last %}›{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
@ -50,7 +39,7 @@
|
|||
|
||||
<div class='content-main'>
|
||||
<h1>{{ name }}</h1>
|
||||
<p>{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}</p>
|
||||
<p>{{ description }}</p>
|
||||
<div class='module'>
|
||||
<pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %}
|
||||
{% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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,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, id=12345)
|
||||
|
||||
|
||||
class TestModelCreation(TestModelsTestCase):
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import re
|
||||
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
from django.test import TestCase
|
||||
|
||||
|
@ -174,6 +176,12 @@ class RendererIntegrationTests(TestCase):
|
|||
_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):
|
||||
"""
|
||||
|
@ -187,6 +195,7 @@ 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.
|
||||
self.assertEquals(content, _flat_repr)
|
||||
|
||||
def test_with_content_type_args(self):
|
||||
|
@ -196,7 +205,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):
|
||||
"""
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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,48 @@ __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.
|
||||
|
@ -48,7 +92,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
|||
"""
|
||||
|
||||
authentication = (authentication.UserLoggedInAuthentication,
|
||||
authentication.BasicAuthentication)
|
||||
authentication.BasicAuthentication)
|
||||
"""
|
||||
List of all authenticating methods to attempt.
|
||||
"""
|
||||
|
@ -76,6 +120,54 @@ 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 _resource_classes:
|
||||
name = self.resource.__name__
|
||||
name = _remove_trailing_string(name, 'Resource')
|
||||
name += getattr(self, '_suffix', '')
|
||||
|
||||
# If it's a view class with no resource then grok the name from the class name
|
||||
else:
|
||||
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.
|
||||
"""
|
||||
|
||||
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:
|
||||
description = apply_markdown(description)
|
||||
else:
|
||||
description = escape(description).replace('\n', '<br />')
|
||||
return mark_safe(description)
|
||||
|
||||
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.
|
||||
|
@ -164,8 +256,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': request._parsed_media_types,
|
||||
}
|
||||
|
|
|
@ -105,6 +105,8 @@ The following example exposes your `MyModel` model through an api. It will provi
|
|||
|
||||
contents
|
||||
|
||||
.. include:: ../CHANGELOG.rst
|
||||
|
||||
Indices and tables
|
||||
------------------
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
Pygments==1.4
|
||||
Markdown==2.0.3
|
||||
djangorestframework
|
||||
git+git://github.com/tomchristie/django-rest-framework.git
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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()),
|
||||
|
@ -16,3 +17,4 @@ urlpatterns = patterns('',
|
|||
(r'^', include('djangorestframework.urls')),
|
||||
)
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
|
6
tox.ini
6
tox.ini
|
@ -32,6 +32,7 @@ commands=
|
|||
basepython=python2.5
|
||||
deps=
|
||||
django==1.2.4
|
||||
django-staticfiles>=1.1.2
|
||||
coverage==3.4
|
||||
URLObject>=0.6.0
|
||||
unittest-xml-reporting==1.2
|
||||
|
@ -43,6 +44,7 @@ deps=
|
|||
basepython=python2.6
|
||||
deps=
|
||||
django==1.2.4
|
||||
django-staticfiles>=1.1.2
|
||||
coverage==3.4
|
||||
URLObject>=0.6.0
|
||||
unittest-xml-reporting==1.2
|
||||
|
@ -54,6 +56,7 @@ deps=
|
|||
basepython=python2.7
|
||||
deps=
|
||||
django==1.2.4
|
||||
django-staticfiles>=1.1.2
|
||||
coverage==3.4
|
||||
URLObject>=0.6.0
|
||||
unittest-xml-reporting==1.2
|
||||
|
@ -135,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
|
||||
|
@ -150,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
|
||||
|
@ -165,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
|
||||
|
|
Loading…
Reference in New Issue
Block a user