aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AUTHORS6
-rw-r--r--MANIFEST.in5
-rw-r--r--README111
-rw-r--r--djangorestframework/__init__.py4
-rw-r--r--djangorestframework/authentication.py12
-rw-r--r--djangorestframework/mixins.py17
-rw-r--r--djangorestframework/parsers.py5
-rw-r--r--djangorestframework/permissions.py145
-rw-r--r--djangorestframework/renderers.py6
-rw-r--r--djangorestframework/resources.py123
-rw-r--r--djangorestframework/runtests/runtests.py28
-rw-r--r--djangorestframework/serializer.py310
-rw-r--r--djangorestframework/templates/renderer.html2
-rw-r--r--djangorestframework/tests/files.py12
-rw-r--r--djangorestframework/tests/parsers.py22
-rw-r--r--djangorestframework/tests/renderers.py25
-rw-r--r--djangorestframework/tests/resources.py31
-rw-r--r--djangorestframework/tests/serializer.py117
-rw-r--r--djangorestframework/tests/throttling.py164
-rw-r--r--djangorestframework/views.py20
-rw-r--r--docs/examples/blogpost.rst28
-rw-r--r--docs/examples/modelviews.rst (renamed from docs/examples/modelresources.rst)28
-rw-r--r--docs/examples/views.rst (renamed from docs/examples/resources.rst)18
-rw-r--r--docs/howto/alternativeframeworks.rst37
-rw-r--r--docs/howto/setup.rst7
-rw-r--r--docs/howto/usingcurl.rst6
-rw-r--r--docs/index.rst69
-rw-r--r--docs/library/serializer.rst5
-rw-r--r--examples/blogpost/models.py3
-rw-r--r--examples/blogpost/resources.py27
-rw-r--r--examples/blogpost/tests.py164
-rw-r--r--examples/blogpost/urls.py29
-rw-r--r--examples/modelresourceexample/resources.py7
-rw-r--r--examples/modelresourceexample/urls.py10
-rw-r--r--examples/permissionsexample/__init__.py (renamed from examples/modelresourceexample/views.py)0
-rw-r--r--examples/permissionsexample/urls.py6
-rw-r--r--examples/permissionsexample/views.py20
-rw-r--r--examples/pygments_api/views.py11
-rw-r--r--examples/resourceexample/urls.py6
-rw-r--r--examples/resourceexample/views.py33
-rw-r--r--examples/sandbox/views.py4
-rw-r--r--examples/urls.py1
-rw-r--r--setup.py18
43 files changed, 1215 insertions, 487 deletions
diff --git a/AUTHORS b/AUTHORS
index 103423ab..a85b7ff8 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,11 +1,13 @@
Tom Christie <tomchristie> - tom@tomchristie.com, @thisneonsoul
Paul Bagwell <pbgwl> - Suggestions & bugfixes.
-Marko Tibold <markotibold> - Contributions & Providing the Hudson CI Server.
+Marko Tibold <markotibold> - Contributions & Providing the Jenkins CI Server.
Sébastien Piquemal <sebpiq> - Contributions.
Carmen Wick <cwick> - Bugfixes.
Alex Ehlke <aehlke> - Design Contributions.
+Alen Mujezinovic <flashingpumpkin> - Contributions.
THANKS TO:
+
Jesper Noehr <jespern> & the django-piston contributors for providing the starting point for this project.
-And of course, to the Django core team and the Django community at large.
+And of course, to the Django core team and the Django community at large. You guys rock.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..fc9ce976
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,5 @@
+recursive-include djangorestframework/static *.ico *.txt
+recursive-include djangorestframework/templates *.txt *.html
+recursive-include examples .keep *.py *.txt
+recursive-include docs *.py *.rst *.html *.txt
+include AUTHORS LICENSE requirements.txt tox.ini
diff --git a/README b/README
index 70df13b8..3c740486 100644
--- a/README
+++ b/README
@@ -1,36 +1,99 @@
-# To install django-rest-framework in a virtualenv environment...
+General Notes
+-------------
-hg clone https://tomchristie@bitbucket.org/tomchristie/django-rest-framework
-cd django-rest-framework/
-virtualenv --no-site-packages --distribute --python=python2.6 env
-source ./env/bin/activate
-pip install -r requirements.txt # django, coverage
+To install django-rest-framework in a virtualenv environment
-# To run the tests...
+ hg clone https://tomchristie@bitbucket.org/tomchristie/django-rest-framework
+ cd django-rest-framework/
+ virtualenv --no-site-packages --distribute --python=python2.6 env
+ source ./env/bin/activate
+ pip install -r requirements.txt # django, coverage
-export PYTHONPATH=. # Ensure djangorestframework is on the PYTHONPATH
-python djangorestframework/runtests/runtests.py
+To run the tests
-# To run the test coverage report...
+ export PYTHONPATH=. # Ensure djangorestframework is on the PYTHONPATH
+ python djangorestframework/runtests/runtests.py
-export PYTHONPATH=. # Ensure djangorestframework is on the PYTHONPATH
-python djangorestframework/runtests/runcoverage.py
-# To run the examples...
+To run the test coverage report
-pip install -r examples/requirements.txt # pygments, httplib2, markdown
-cd examples
-export PYTHONPATH=..
-python manage.py syncdb
-python manage.py runserver
+ export PYTHONPATH=. # Ensure djangorestframework is on the PYTHONPATH
+ python djangorestframework/runtests/runcoverage.py
-# To build the documentation...
-pip install -r docs/requirements.txt # sphinx
-sphinx-build -c docs -b html -d docs/build docs html
+To run the examples
-# To run the tests against the full set of supported configurations
+ pip install -r examples/requirements.txt # pygments, httplib2, markdown
+ cd examples
+ export PYTHONPATH=..
+ python manage.py syncdb
+ python manage.py runserver
-deactivate # Ensure we are not currently running in a virtualenv
-tox
+
+To build the documentation
+
+ pip install -r docs/requirements.txt # sphinx
+ sphinx-build -c docs -b html -d docs/build docs html
+
+
+To run the tests against the full set of supported configurations
+
+ deactivate # Ensure we are not currently running in a virtualenv
+ tox
+
+
+To create the sdist packages
+
+ python setup.py sdist --formats=gztar,zip
+
+
+
+Release Notes
+=============
+
+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.
+
+ * `Resource` becomes decoupled into `View` and `Resource`, your views should now inherit from `View`, not `Resource`.
+
+ * The handler functions on views .get() .put() .post() etc, no longer have the `content` and `auth` args.
+ Use `self.CONTENT` inside a view to access the deserialized, validated content.
+ Use `self.user` inside a view to access the authenticated user.
+
+ * `allowed_methods` and `anon_allowed_methods` are now defunct. if a method is defined, it's available.
+ The `permissions` attribute on a `View` is now used to provide generic permissions checking.
+ Use permission classes such as `FullAnonAccess`, `IsAuthenticated` or `IsUserOrIsAnonReadOnly` to set the permissions.
+
+ * The `authenticators` class becomes `authentication`. Class names change to Authentication.
+
+ * The `emitters` class becomes `renderers`. Class names change to Renderers.
+
+ * `ResponseException` becomes `ErrorResponse`.
+
+ * The mixin classes have been nicely refactored, the basic mixins are now `RequestMixin`, `ResponseMixin`, `AuthMixin`, and `ResourceMixin`
+ 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. \ No newline at end of file
diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py
index 26482435..b1ef6dda 100644
--- a/djangorestframework/__init__.py
+++ b/djangorestframework/__init__.py
@@ -1 +1,3 @@
-VERSION="0.2.0"
+__version__ = '0.2.3'
+
+VERSION = __version__ # synonym
diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py
index 7d6e2114..be22103e 100644
--- a/djangorestframework/authentication.py
+++ b/djangorestframework/authentication.py
@@ -13,13 +13,13 @@ from djangorestframework.utils import as_tuple
import base64
__all__ = (
- 'BaseAuthenticaton',
- 'BasicAuthenticaton',
- 'UserLoggedInAuthenticaton'
+ 'BaseAuthentication',
+ 'BasicAuthentication',
+ 'UserLoggedInAuthentication'
)
-class BaseAuthenticaton(object):
+class BaseAuthentication(object):
"""
All authentication classes should extend BaseAuthentication.
"""
@@ -47,7 +47,7 @@ class BaseAuthenticaton(object):
return None
-class BasicAuthenticaton(BaseAuthenticaton):
+class BasicAuthentication(BaseAuthentication):
"""
Use HTTP Basic authentication.
"""
@@ -78,7 +78,7 @@ class BasicAuthenticaton(BaseAuthenticaton):
return None
-class UserLoggedInAuthenticaton(BaseAuthenticaton):
+class UserLoggedInAuthentication(BaseAuthentication):
"""
Use Django's session framework for authentication.
"""
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 11e3bb38..910d06ae 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -466,7 +466,7 @@ class InstanceMixin(object):
# We do a little dance when we store the view callable...
# we need to store it wrapped in a 1-tuple, so that inspect will treat it
# as a function when we later look it up (rather than turning it into a method).
- # This makes sure our URL reversing works ok.
+ # This makes sure our URL reversing works ok.
resource.view_callable = (view,)
return view
@@ -479,6 +479,7 @@ class ReadModelMixin(object):
"""
def get(self, request, *args, **kwargs):
model = self.resource.model
+
try:
if args:
# If we have any none kwargs then assume the last represents the primrary key
@@ -498,6 +499,7 @@ class CreateModelMixin(object):
"""
def post(self, request, *args, **kwargs):
model = self.resource.model
+
# translated 'related_field' kwargs into 'related_field_id'
for related_name in [field.name for field in model._meta.fields if isinstance(field, RelatedField)]:
if kwargs.has_key(related_name):
@@ -522,6 +524,7 @@ class UpdateModelMixin(object):
"""
def put(self, request, *args, **kwargs):
model = self.resource.model
+
# TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
try:
if args:
@@ -547,6 +550,7 @@ class DeleteModelMixin(object):
"""
def delete(self, request, *args, **kwargs):
model = self.resource.model
+
try:
if args:
# If we have any none kwargs then assume the last represents the primrary key
@@ -581,8 +585,15 @@ class ListModelMixin(object):
queryset = None
def get(self, request, *args, **kwargs):
- queryset = self.queryset if self.queryset else self.resource.model.objects.all()
- ordering = getattr(self.resource, 'ordering', None)
+ model = self.resource.model
+
+ queryset = self.queryset if self.queryset else model.objects.all()
+
+ if hasattr(self, 'resource'):
+ ordering = getattr(self.resource, 'ordering', None)
+ else:
+ ordering = None
+
if ordering:
args = as_tuple(ordering)
queryset = queryset.order_by(*args)
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
index 726e09e9..3346a26e 100644
--- a/djangorestframework/parsers.py
+++ b/djangorestframework/parsers.py
@@ -11,12 +11,11 @@ We need a method to be able to:
and multipart/form-data. (eg also handle multipart/json)
"""
+from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.utils import simplejson as json
from djangorestframework import status
-from djangorestframework.compat import parse_qs
from djangorestframework.response import ErrorResponse
-from djangorestframework.utils import as_tuple
from djangorestframework.utils.mediatypes import media_type_matches
__all__ = (
@@ -117,7 +116,7 @@ class FormParser(BaseParser):
`data` will be a :class:`QueryDict` containing all the form parameters.
`files` will always be :const:`None`.
"""
- data = parse_qs(stream.read(), keep_blank_values=True)
+ data = QueryDict(stream.read())
return (data, None)
diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py
index 1f6151f8..7dcabcf0 100644
--- a/djangorestframework/permissions.py
+++ b/djangorestframework/permissions.py
@@ -1,6 +1,6 @@
"""
The :mod:`permissions` module bundles a set of permission classes that are used
-for checking if a request passes a certain set of constraints. You can assign a permision
+for checking if a request passes a certain set of constraints. You can assign a permission
class to your view by setting your View's :attr:`permissions` class attribute.
"""
@@ -15,7 +15,9 @@ __all__ = (
'IsAuthenticated',
'IsAdminUser',
'IsUserOrIsAnonReadOnly',
- 'PerUserThrottling'
+ 'PerUserThrottling',
+ 'PerViewThrottling',
+ 'PerResourceThrottling'
)
@@ -24,12 +26,11 @@ _403_FORBIDDEN_RESPONSE = ErrorResponse(
{'detail': 'You do not have permission to access this resource. ' +
'You may need to login or otherwise authenticate the request.'})
-_503_THROTTLED_RESPONSE = ErrorResponse(
+_503_SERVICE_UNAVAILABLE = ErrorResponse(
status.HTTP_503_SERVICE_UNAVAILABLE,
{'detail': 'request was throttled'})
-
class BasePermission(object):
"""
A base class from which all permission classes should inherit.
@@ -88,37 +89,131 @@ class IsUserOrIsAnonReadOnly(BasePermission):
raise _403_FORBIDDEN_RESPONSE
-class PerUserThrottling(BasePermission):
+class BaseThrottle(BasePermission):
"""
- Rate throttling of requests on a per-user basis.
+ Rate throttling of requests.
- The rate (requests / seconds) is set by a :attr:`throttle` attribute on the ``View`` class.
- The attribute is a two tuple of the form (number of requests, duration in seconds).
+ The rate (requests / seconds) is set by a :attr:`throttle` attribute
+ on the :class:`.View` class. The attribute is a string of the form 'number of
+ requests/period'.
- The user id will be used as a unique identifier if the user is authenticated.
- For anonymous requests, the IP address of the client will be used.
+ Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day')
Previous request information used for throttling is stored in the cache.
+ """
+
+ attr_name = 'throttle'
+ default = '0/sec'
+ timer = time.time
+
+ def get_cache_key(self):
+ """
+ Should return a unique cache-key which can be used for throttling.
+ Muse be overridden.
+ """
+ pass
+
+ def check_permission(self, auth):
+ """
+ Check the throttling.
+ Return `None` or raise an :exc:`.ErrorResponse`.
+ """
+ num, period = getattr(self.view, self.attr_name, self.default).split('/')
+ self.num_requests = int(num)
+ self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
+ self.auth = auth
+ self.check_throttle()
+
+ def check_throttle(self):
+ """
+ Implement the check to see if the request should be throttled.
+
+ On success calls :meth:`throttle_success`.
+ On failure calls :meth:`throttle_failure`.
+ """
+ self.key = self.get_cache_key()
+ self.history = cache.get(self.key, [])
+ self.now = self.timer()
+
+ # Drop any requests from the history which have now passed the
+ # throttle duration
+ while self.history and self.history[-1] <= self.now - self.duration:
+ self.history.pop()
+ if len(self.history) >= self.num_requests:
+ self.throttle_failure()
+ else:
+ self.throttle_success()
+
+ def throttle_success(self):
+ """
+ Inserts the current request's timestamp along with the key
+ into the cache.
+ """
+ self.history.insert(0, self.now)
+ cache.set(self.key, self.history, self.duration)
+ header = 'status=SUCCESS; next=%s sec' % self.next()
+ self.view.add_header('X-Throttle', header)
+
+ def throttle_failure(self):
+ """
+ Called when a request to the API has failed due to throttling.
+ Raises a '503 service unavailable' response.
+ """
+ header = 'status=FAILURE; next=%s sec' % self.next()
+ self.view.add_header('X-Throttle', header)
+ raise _503_SERVICE_UNAVAILABLE
+
+ def next(self):
+ """
+ Returns the recommended next request time in seconds.
+ """
+ if self.history:
+ remaining_duration = self.duration - (self.now - self.history[-1])
+ else:
+ remaining_duration = self.duration
+
+ available_requests = self.num_requests - len(self.history) + 1
+
+ return '%.2f' % (remaining_duration / float(available_requests))
+
+
+class PerUserThrottling(BaseThrottle):
"""
+ Limits the rate of API calls that may be made by a given user.
- def check_permission(self, user):
- (num_requests, duration) = getattr(self.view, 'throttle', (0, 0))
+ The user id will be used as a unique identifier if the user is
+ authenticated. For anonymous requests, the IP address of the client will
+ be used.
+ """
- if user.is_authenticated():
- ident = str(auth)
+ def get_cache_key(self):
+ if self.auth.is_authenticated():
+ ident = str(self.auth)
else:
ident = self.view.request.META.get('REMOTE_ADDR', None)
+ return 'throttle_user_%s' % ident
- key = 'throttle_%s' % ident
- history = cache.get(key, [])
- now = time.time()
-
- # Drop any requests from the history which have now passed the throttle duration
- while history and history[0] < now - duration:
- history.pop()
- if len(history) >= num_requests:
- raise _503_THROTTLED_RESPONSE
+class PerViewThrottling(BaseThrottle):
+ """
+ Limits the rate of API calls that may be used on a given view.
+
+ The class name of the view is used as a unique identifier to
+ throttle against.
+ """
+
+ def get_cache_key(self):
+ return 'throttle_view_%s' % self.view.__class__.__name__
+
+
+class PerResourceThrottling(BaseThrottle):
+ """
+ Limits the rate of API calls that may be used against all views on
+ a given resource.
+
+ The class name of the resource is used as a unique identifier to
+ throttle against.
+ """
- history.insert(0, now)
- cache.set(key, history, duration)
+ def get_cache_key(self):
+ return 'throttle_resource_%s' % self.view.resource.__class__.__name__
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py
index 9834ba5e..13cd52f5 100644
--- a/djangorestframework/renderers.py
+++ b/djangorestframework/renderers.py
@@ -17,6 +17,7 @@ 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
from decimal import Decimal
import re
@@ -108,6 +109,7 @@ class XMLRenderer(BaseRenderer):
"""
Renderer which serializes to XML.
"""
+
media_type = 'application/xml'
def render(self, obj=None, media_type=None):
@@ -181,7 +183,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
# Get the form instance if we have one bound to the input
form_instance = None
- if method == view.method.lower():
+ if method == getattr(view, 'method', view.request.method).lower():
form_instance = getattr(view, 'bound_form_instance', None)
if not form_instance and hasattr(view, 'get_bound_form'):
@@ -251,6 +253,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
The context used in the template contains all the information
needed to self-document the response to this request.
"""
+
content = self._get_content(self.view, self.view.request, obj, media_type)
put_form_instance = self._get_form_instance(self.view, 'put')
@@ -283,6 +286,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
'response': self.view.response,
'description': description,
'name': name,
+ 'version': VERSION,
'markeddown': markeddown,
'breadcrumblist': breadcrumb_list,
'available_media_types': self.view._rendered_media_types,
diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py
index adf5c1c3..08f9e0ae 100644
--- a/djangorestframework/resources.py
+++ b/djangorestframework/resources.py
@@ -6,6 +6,7 @@ from django.db.models.fields.related import RelatedField
from django.utils.encoding import smart_unicode
from djangorestframework.response import ErrorResponse
+from djangorestframework.serializer import Serializer
from djangorestframework.utils import as_tuple
import decimal
@@ -13,105 +14,9 @@ import inspect
import re
-# TODO: _IgnoreFieldException
-# Map model classes to resource classes
-#_model_to_resource = {}
-
-def _model_to_dict(instance, resource=None):
- """
- Given a model instance, return a ``dict`` representing the model.
-
- The implementation is similar to Django's ``django.forms.model_to_dict``, except:
-
- * It doesn't coerce related objects into primary keys.
- * It doesn't drop ``editable=False`` fields.
- * It also supports attribute or method fields on the instance or resource.
- """
- opts = instance._meta
- data = {}
-
- #print [rel.name for rel in opts.get_all_related_objects()]
- #related = [rel.get_accessor_name() for rel in opts.get_all_related_objects()]
- #print [getattr(instance, rel) for rel in related]
- #if resource.fields:
- # fields = resource.fields
- #else:
- # fields = set(opts.fields + opts.many_to_many)
-
- fields = resource and resource.fields or ()
- include = resource and resource.include or ()
- exclude = resource and resource.exclude or ()
-
- extra_fields = fields and list(resource.fields) or []
-
- # Model fields
- for f in opts.fields + opts.many_to_many:
- if fields and not f.name in fields:
- continue
- if exclude and f.name in exclude:
- continue
- if isinstance(f, models.ForeignKey):
- data[f.name] = getattr(instance, f.name)
- else:
- data[f.name] = f.value_from_object(instance)
-
- if extra_fields and f.name in extra_fields:
- extra_fields.remove(f.name)
-
- # Method fields
- for fname in extra_fields:
- if hasattr(resource, fname):
- # check the resource first, to allow it to override fields
- obj = getattr(resource, fname)
- # if it's a method like foo(self, instance), then call it
- if inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) == 2:
- obj = obj(instance)
- elif hasattr(instance, fname):
- # now check the object instance
- obj = getattr(instance, fname)
- else:
- continue
-
- # TODO: It would be nicer if this didn't recurse here.
- # Let's keep _model_to_dict flat, and _object_to_data recursive.
- data[fname] = _object_to_data(obj)
-
- return data
-
-
-def _object_to_data(obj, resource=None):
- """
- Convert an object into a serializable representation.
- """
- if isinstance(obj, dict):
- # dictionaries
- # TODO: apply same _model_to_dict logic fields/exclude here
- return dict([ (key, _object_to_data(val)) for key, val in obj.iteritems() ])
- if isinstance(obj, (tuple, list, set, QuerySet)):
- # basic iterables
- return [_object_to_data(item, resource) for item in obj]
- if isinstance(obj, models.Manager):
- # Manager objects
- return [_object_to_data(item, resource) for item in obj.all()]
- if isinstance(obj, models.Model):
- # Model instances
- return _object_to_data(_model_to_dict(obj, resource))
- if isinstance(obj, decimal.Decimal):
- # Decimals (force to string representation)
- return str(obj)
- if inspect.isfunction(obj) and not inspect.getargspec(obj)[0]:
- # function with no args
- return _object_to_data(obj(), resource)
- if inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1:
- # bound method
- return _object_to_data(obj(), resource)
-
- return smart_unicode(obj, strings_only=True)
-
-
-class BaseResource(object):
+class BaseResource(Serializer):
"""
Base class for all Resource classes, which simply defines the interface they provide.
"""
@@ -119,7 +24,8 @@ class BaseResource(object):
include = None
exclude = None
- def __init__(self, view):
+ def __init__(self, view=None, depth=None, stack=[], **kwargs):
+ super(BaseResource, self).__init__(depth, stack, **kwargs)
self.view = view
def validate_request(self, data, files=None):
@@ -133,7 +39,7 @@ class BaseResource(object):
"""
Given the response content, filter it into a serializable object.
"""
- return _object_to_data(obj, self)
+ return self.serialize(obj)
class Resource(BaseResource):
@@ -223,15 +129,13 @@ class FormResource(Resource):
# In addition to regular validation we also ensure no additional fields are being passed in...
unknown_fields = seen_fields_set - (form_fields_set | allowed_extra_fields_set)
- unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept')) # TODO: Ugh.
-
+ unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept', '_method')) # TODO: Ugh.
+
# Check using both regular validation, and our stricter no additional fields rule
if bound_form.is_valid() and not unknown_fields:
# Validation succeeded...
cleaned_data = bound_form.cleaned_data
- cleaned_data.update(bound_form.files)
-
# Add in any extra fields to the cleaned content...
for key in (allowed_extra_fields_set & seen_fields_set) - set(cleaned_data.keys()):
cleaned_data[key] = data[key]
@@ -282,7 +186,7 @@ class FormResource(Resource):
"""
# A form on the view overrides a form on the resource.
- form = getattr(self.view, 'form', self.form)
+ form = getattr(self.view, 'form', None) or self.form
# Use the requested method or determine the request method
if method is None and hasattr(self.view, 'request') and hasattr(self.view, 'method'):
@@ -299,7 +203,7 @@ class FormResource(Resource):
if not form:
return None
- if data is not None:
+ if data is not None or files is not None:
return form(data, files)
return form()
@@ -375,8 +279,8 @@ class ModelResource(FormResource):
"""
super(ModelResource, self).__init__(view)
- if getattr(view, 'model', None):
- self.model = view.model
+ self.model = getattr(view, 'model', None) or self.model
+
def validate_request(self, data, files=None):
"""
@@ -437,6 +341,9 @@ class ModelResource(FormResource):
This method can be overridden if you need to set the resource url reversing explicitly.
"""
+ if not hasattr(self, 'view_callable'):
+ raise NoReverseMatch
+
# dis does teh magicks...
urlconf = get_urlconf()
resolver = get_resolver(urlconf)
@@ -458,7 +365,7 @@ class ModelResource(FormResource):
if isinstance(attr, models.Model):
instance_attrs[param] = attr.pk
else:
- instance_attrs[param] = attr
+ instance_attrs[param] = attr
try:
return reverse(self.view_callable[0], kwargs=instance_attrs)
diff --git a/djangorestframework/runtests/runtests.py b/djangorestframework/runtests/runtests.py
index a3cdfa67..1da918f5 100644
--- a/djangorestframework/runtests/runtests.py
+++ b/djangorestframework/runtests/runtests.py
@@ -13,21 +13,27 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'djangorestframework.runtests.settings'
from django.conf import settings
from django.test.utils import get_runner
+def usage():
+ return """
+ Usage: python runtests.py [UnitTestClass].[method]
+
+ You can pass the Class name of the `UnitTestClass` you want to test.
+
+ Append a method name if you only want to test a specific method of that class.
+ """
+
def main():
TestRunner = get_runner(settings)
- if hasattr(TestRunner, 'func_name'):
- # Pre 1.2 test runners were just functions,
- # and did not support the 'failfast' option.
- import warnings
- warnings.warn(
- 'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
- DeprecationWarning
- )
- failures = TestRunner(['djangorestframework'])
+ test_runner = TestRunner()
+ if len(sys.argv) == 2:
+ test_case = '.' + sys.argv[1]
+ elif len(sys.argv) == 1:
+ test_case = ''
else:
- test_runner = TestRunner()
- failures = test_runner.run_tests(['djangorestframework'])
+ print usage()
+ sys.exit(1)
+ failures = test_runner.run_tests(['djangorestframework' + test_case])
sys.exit(failures)
diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py
new file mode 100644
index 00000000..da8036e9
--- /dev/null
+++ b/djangorestframework/serializer.py
@@ -0,0 +1,310 @@
+"""
+Customizable serialization.
+"""
+from django.db import models
+from django.db.models.query import QuerySet
+from django.db.models.fields.related import RelatedField
+from django.utils.encoding import smart_unicode, is_protected_type
+
+import decimal
+import inspect
+import types
+
+
+# We register serializer classes, so that we can refer to them by their
+# class names, if there are cyclical serialization heirachys.
+_serializers = {}
+
+
+def _field_to_tuple(field):
+ """
+ Convert an item in the `fields` attribute into a 2-tuple.
+ """
+ if isinstance(field, (tuple, list)):
+ return (field[0], field[1])
+ return (field, None)
+
+def _fields_to_list(fields):
+ """
+ Return a list of field names.
+ """
+ return [_field_to_tuple(field)[0] for field in fields or ()]
+
+def _fields_to_dict(fields):
+ """
+ Return a `dict` of field name -> None, or tuple of fields, or Serializer class
+ """
+ return dict([_field_to_tuple(field) for field in fields or ()])
+
+
+class _SkipField(Exception):
+ """
+ Signals that a serialized field should be ignored.
+ We use this mechanism as the default behavior for ensuring
+ that we don't infinitely recurse when dealing with nested data.
+ """
+ pass
+
+
+class _RegisterSerializer(type):
+ """
+ Metaclass to register serializers.
+ """
+ def __new__(cls, name, bases, attrs):
+ # Build the class and register it.
+ ret = super(_RegisterSerializer, cls).__new__(cls, name, bases, attrs)
+ _serializers[name] = ret
+ return ret
+
+
+class Serializer(object):
+ """
+ Converts python objects into plain old native types suitable for
+ serialization. In particular it handles models and querysets.
+
+ The output format is specified by setting a number of attributes
+ on the class.
+
+ You may also override any of the serialization methods, to provide
+ for more flexible behavior.
+
+ Valid output types include anything that may be directly rendered into
+ json, xml etc...
+ """
+ __metaclass__ = _RegisterSerializer
+
+ fields = ()
+ """
+ Specify the fields to be serialized on a model or dict.
+ Overrides `include` and `exclude`.
+ """
+
+ include = ()
+ """
+ Fields to add to the default set to be serialized on a model/dict.
+ """
+
+ exclude = ()
+ """
+ Fields to remove from the default set to be serialized on a model/dict.
+ """
+
+ rename = {}
+ """
+ A dict of key->name to use for the field keys.
+ """
+
+ related_serializer = None
+ """
+ The default serializer class to use for any related models.
+ """
+
+ depth = None
+ """
+ The maximum depth to serialize to, or `None`.
+ """
+
+
+ def __init__(self, depth=None, stack=[], **kwargs):
+ self.depth = depth or self.depth
+ self.stack = stack
+
+
+ def get_fields(self, obj):
+ """
+ Return the set of field names/keys to use for a model instance/dict.
+ """
+ fields = self.fields
+
+ # If `fields` is not set, we use the default fields and modify
+ # them with `include` and `exclude`
+ if not fields:
+ default = self.get_default_fields(obj)
+ include = self.include or ()
+ exclude = self.exclude or ()
+ fields = set(default + list(include)) - set(exclude)
+
+ else:
+ fields = _fields_to_list(self.fields)
+
+ return fields
+
+
+ def get_default_fields(self, obj):
+ """
+ Return the default list of field names/keys for a model instance/dict.
+ These are used if `fields` is not given.
+ """
+ if isinstance(obj, models.Model):
+ opts = obj._meta
+ return [field.name for field in opts.fields + opts.many_to_many]
+ else:
+ return obj.keys()
+
+
+ def get_related_serializer(self, key):
+ info = _fields_to_dict(self.fields).get(key, None)
+
+ # If an element in `fields` is a 2-tuple of (str, tuple)
+ # then the second element of the tuple is the fields to
+ # set on the related serializer
+ if isinstance(info, (list, tuple)):
+ class OnTheFlySerializer(Serializer):
+ fields = info
+ return OnTheFlySerializer
+
+ # If an element in `fields` is a 2-tuple of (str, Serializer)
+ # then the second element of the tuple is the Serializer
+ # class to use for that field.
+ elif isinstance(info, type) and issubclass(info, Serializer):
+ return info
+
+ # If an element in `fields` is a 2-tuple of (str, str)
+ # then the second element of the tuple is the name of the Serializer
+ # class to use for that field.
+ #
+ # Black magic to deal with cyclical Serializer dependancies.
+ # Similar to what Django does for cyclically related models.
+ elif isinstance(info, str) and info in _serializers:
+ return _serializers[info]
+
+ # Otherwise use `related_serializer` or fall back to `Serializer`
+ return getattr(self, 'related_serializer') or Serializer
+
+
+ def serialize_key(self, key):
+ """
+ Keys serialize to their string value,
+ unless they exist in the `rename` dict.
+ """
+ return getattr(self.rename, key, key)
+
+
+ def serialize_val(self, key, obj):
+ """
+ Convert a model field or dict value into a serializable representation.
+ """
+ related_serializer = self.get_related_serializer(key)
+
+ if self.depth is None:
+ depth = None
+ elif self.depth <= 0:
+ return self.serialize_max_depth(obj)
+ else:
+ depth = self.depth - 1
+
+ if any([obj is elem for elem in self.stack]):
+ return self.serialize_recursion(obj)
+ else:
+ stack = self.stack[:]
+ stack.append(obj)
+
+ return related_serializer(depth=depth, stack=stack).serialize(obj)
+
+
+ def serialize_max_depth(self, obj):
+ """
+ Determine how objects should be serialized once `depth` is exceeded.
+ The default behavior is to ignore the field.
+ """
+ raise _SkipField
+
+
+ def serialize_recursion(self, obj):
+ """
+ Determine how objects should be serialized if recursion occurs.
+ The default behavior is to ignore the field.
+ """
+ raise _SkipField
+
+
+ def serialize_model(self, instance):
+ """
+ Given a model instance or dict, serialize it to a dict..
+ """
+ data = {}
+
+ fields = self.get_fields(instance)
+
+ # serialize each required field
+ for fname in fields:
+ if hasattr(self, fname):
+ # check for a method 'fname' on self first
+ meth = getattr(self, fname)
+ if inspect.ismethod(meth) and len(inspect.getargspec(meth)[0]) == 2:
+ obj = meth(instance)
+ elif hasattr(instance, fname):
+ # now check for an attribute 'fname' on the instance
+ obj = getattr(instance, fname)
+ elif fname in instance:
+ # finally check for a key 'fname' on the instance
+ obj = instance[fname]
+ else:
+ continue
+
+ try:
+ key = self.serialize_key(fname)
+ val = self.serialize_val(fname, obj)
+ data[key] = val
+ except _SkipField:
+ pass
+
+ return data
+
+
+ def serialize_iter(self, obj):
+ """
+ Convert iterables into a serializable representation.
+ """
+ return [self.serialize(item) for item in obj]
+
+
+ def serialize_func(self, obj):
+ """
+ Convert no-arg methods and functions into a serializable representation.
+ """
+ return self.serialize(obj())
+
+
+ def serialize_manager(self, obj):
+ """
+ Convert a model manager into a serializable representation.
+ """
+ return self.serialize_iter(obj.all())
+
+
+ def serialize_fallback(self, obj):
+ """
+ Convert any unhandled object into a serializable representation.
+ """
+ return smart_unicode(obj, strings_only=True)
+
+
+ def serialize(self, obj):
+ """
+ Convert any object into a serializable representation.
+ """
+
+ if isinstance(obj, (dict, models.Model)):
+ # Model instances & dictionaries
+ return self.serialize_model(obj)
+ elif isinstance(obj, (tuple, list, set, QuerySet, types.GeneratorType)):
+ # basic iterables
+ return self.serialize_iter(obj)
+ elif isinstance(obj, models.Manager):
+ # Manager objects
+ return self.serialize_manager(obj)
+ elif inspect.isfunction(obj) and not inspect.getargspec(obj)[0]:
+ # function with no args
+ return self.serialize_func(obj)
+ elif inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1:
+ # bound method
+ return self.serialize_func(obj)
+
+ # Protected types are passed through as is.
+ # (i.e. Primitives like None, numbers, dates, and Decimals.)
+ if is_protected_type(obj):
+ return obj
+
+ # All other values are converted to string.
+ return self.serialize_fallback(obj)
diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html
index 94748d28..97d3837a 100644
--- a/djangorestframework/templates/renderer.html
+++ b/djangorestframework/templates/renderer.html
@@ -18,7 +18,7 @@
<div id="header">
<div id="branding">
- <h1 id="site-name"><a href='http://django-rest-framework.org'>Django REST framework</a></h1>
+ <h1 id="site-name"><a href='http://django-rest-framework.org'>Django REST framework</a> <small>{{ version }}</small></h1>
</div>
<div id="user-tools">
{% if user.is_active %}Welcome, {{ user }}.{% if logout_url %} <a href='{{ logout_url }}'>Log out</a>{% endif %}{% else %}Anonymous {% if login_url %}<a href='{{ login_url }}'>Log in</a>{% endif %}{% endif %}
diff --git a/djangorestframework/tests/files.py b/djangorestframework/tests/files.py
index 25aad9b4..992d3cba 100644
--- a/djangorestframework/tests/files.py
+++ b/djangorestframework/tests/files.py
@@ -12,20 +12,16 @@ class UploadFilesTests(TestCase):
def test_upload_file(self):
-
class FileForm(forms.Form):
- file = forms.FileField
-
- class MockResource(FormResource):
- form = FileForm
+ file = forms.FileField()
class MockView(View):
permissions = ()
- resource = MockResource
+ form = FileForm
def post(self, request, *args, **kwargs):
- return {'FILE_NAME': self.CONTENT['file'][0].name,
- 'FILE_CONTENT': self.CONTENT['file'][0].read()}
+ return {'FILE_NAME': self.CONTENT['file'].name,
+ 'FILE_CONTENT': self.CONTENT['file'].read()}
file = StringIO.StringIO('stuff')
file.name = 'stuff.txt'
diff --git a/djangorestframework/tests/parsers.py b/djangorestframework/tests/parsers.py
index 3ab1a61c..deba688e 100644
--- a/djangorestframework/tests/parsers.py
+++ b/djangorestframework/tests/parsers.py
@@ -131,3 +131,25 @@
# self.assertEqual(data['key1'], 'val1')
# self.assertEqual(files['file1'].read(), 'blablabla')
+from StringIO import StringIO
+from cgi import parse_qs
+from django import forms
+from django.test import TestCase
+from djangorestframework.parsers import FormParser
+
+class Form(forms.Form):
+ field1 = forms.CharField(max_length=3)
+ field2 = forms.CharField()
+
+class TestFormParser(TestCase):
+ def setUp(self):
+ self.string = "field1=abc&field2=defghijk"
+
+ def test_parse(self):
+ """ Make sure the `QueryDict` works OK """
+ parser = FormParser(None)
+
+ stream = StringIO(self.string)
+ (data, files) = parser.parse(stream)
+
+ self.assertEqual(Form(data).is_valid(), True)
diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py
index c9108764..569eb640 100644
--- a/djangorestframework/tests/renderers.py
+++ b/djangorestframework/tests/renderers.py
@@ -1,12 +1,16 @@
from django.conf.urls.defaults import patterns, url
from django import http
from django.test import TestCase
+
from djangorestframework.compat import View as DjangoView
from djangorestframework.renderers import BaseRenderer, JSONRenderer
+from djangorestframework.parsers import JSONParser
from djangorestframework.mixins import ResponseMixin
from djangorestframework.response import Response
from djangorestframework.utils.mediatypes import add_media_type_param
+from StringIO import StringIO
+
DUMMYSTATUS = 200
DUMMYCONTENT = 'dummycontent'
@@ -100,14 +104,35 @@ class JSONRendererTests(TestCase):
"""
Tests specific to the JSON Renderer
"""
+
def test_without_content_type_args(self):
+ """
+ Test basic JSON rendering.
+ """
obj = {'foo':['bar','baz']}
renderer = JSONRenderer(None)
content = renderer.render(obj, 'application/json')
self.assertEquals(content, _flat_repr)
def test_with_content_type_args(self):
+ """
+ Test JSON rendering with additional content type arguments supplied.
+ """
obj = {'foo':['bar','baz']}
renderer = JSONRenderer(None)
content = renderer.render(obj, 'application/json; indent=2')
self.assertEquals(content, _indented_repr)
+
+ def test_render_and_parse(self):
+ """
+ Test rendering and then parsing returns the original object.
+ IE obj -> render -> parse -> obj.
+ """
+ obj = {'foo':['bar','baz']}
+
+ renderer = JSONRenderer(None)
+ parser = JSONParser(None)
+
+ content = renderer.render(obj, 'application/json')
+ (data, files) = parser.parse(StringIO(content))
+ self.assertEquals(obj, data)
diff --git a/djangorestframework/tests/resources.py b/djangorestframework/tests/resources.py
deleted file mode 100644
index fd1226be..00000000
--- a/djangorestframework/tests/resources.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""Tests for the resource module"""
-from django.test import TestCase
-from djangorestframework.resources import _object_to_data
-
-import datetime
-import decimal
-
-class TestObjectToData(TestCase):
- """Tests for the _object_to_data function"""
-
- def test_decimal(self):
- """Decimals need to be converted to a string representation."""
- self.assertEquals(_object_to_data(decimal.Decimal('1.5')), '1.5')
-
- def test_function(self):
- """Functions with no arguments should be called."""
- def foo():
- return 1
- self.assertEquals(_object_to_data(foo), 1)
-
- def test_method(self):
- """Methods with only a ``self`` argument should be called."""
- class Foo(object):
- def foo(self):
- return 1
- self.assertEquals(_object_to_data(Foo().foo), 1)
-
- def test_datetime(self):
- """datetime objects are left as-is."""
- now = datetime.datetime.now()
- self.assertEquals(_object_to_data(now), now) \ No newline at end of file
diff --git a/djangorestframework/tests/serializer.py b/djangorestframework/tests/serializer.py
new file mode 100644
index 00000000..9f629050
--- /dev/null
+++ b/djangorestframework/tests/serializer.py
@@ -0,0 +1,117 @@
+"""Tests for the resource module"""
+from django.test import TestCase
+from djangorestframework.serializer import Serializer
+
+from django.db import models
+
+import datetime
+import decimal
+
+class TestObjectToData(TestCase):
+ """
+ Tests for the Serializer class.
+ """
+
+ def setUp(self):
+ self.serializer = Serializer()
+ self.serialize = self.serializer.serialize
+
+ def test_decimal(self):
+ """Decimals need to be converted to a string representation."""
+ self.assertEquals(self.serialize(decimal.Decimal('1.5')), decimal.Decimal('1.5'))
+
+ def test_function(self):
+ """Functions with no arguments should be called."""
+ def foo():
+ return 1
+ self.assertEquals(self.serialize(foo), 1)
+
+ def test_method(self):
+ """Methods with only a ``self`` argument should be called."""
+ class Foo(object):
+ def foo(self):
+ return 1
+ self.assertEquals(self.serialize(Foo().foo), 1)
+
+ def test_datetime(self):
+ """
+ datetime objects are left as-is.
+ """
+ now = datetime.datetime.now()
+ self.assertEquals(self.serialize(now), now)
+
+
+class TestFieldNesting(TestCase):
+ """
+ Test nesting the fields in the Serializer class
+ """
+ def setUp(self):
+ self.serializer = Serializer()
+ self.serialize = self.serializer.serialize
+
+ class M1(models.Model):
+ field1 = models.CharField()
+ field2 = models.CharField()
+
+ class M2(models.Model):
+ field = models.OneToOneField(M1)
+
+ class M3(models.Model):
+ field = models.ForeignKey(M1)
+
+ self.m1 = M1(field1='foo', field2='bar')
+ self.m2 = M2(field=self.m1)
+ self.m3 = M3(field=self.m1)
+
+
+ def test_tuple_nesting(self):
+ """
+ Test tuple nesting on `fields` attr
+ """
+ class SerializerM2(Serializer):
+ fields = (('field', ('field1',)),)
+
+ class SerializerM3(Serializer):
+ fields = (('field', ('field2',)),)
+
+ self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
+ self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
+
+
+ def test_serializer_class_nesting(self):
+ """
+ Test related model serialization
+ """
+ class NestedM2(Serializer):
+ fields = ('field1', )
+
+ class NestedM3(Serializer):
+ fields = ('field2', )
+
+ class SerializerM2(Serializer):
+ fields = [('field', NestedM2)]
+
+ class SerializerM3(Serializer):
+ fields = [('field', NestedM3)]
+
+ self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
+ self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
+
+ def test_serializer_classname_nesting(self):
+ """
+ Test related model serialization
+ """
+ class SerializerM2(Serializer):
+ fields = [('field', 'NestedM2')]
+
+ class SerializerM3(Serializer):
+ fields = [('field', 'NestedM3')]
+
+ class NestedM2(Serializer):
+ fields = ('field1', )
+
+ class NestedM3(Serializer):
+ fields = ('field2', )
+
+ self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
+ self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
diff --git a/djangorestframework/tests/throttling.py b/djangorestframework/tests/throttling.py
index a8f08b18..b620ee24 100644
--- a/djangorestframework/tests/throttling.py
+++ b/djangorestframework/tests/throttling.py
@@ -1,38 +1,148 @@
-from django.conf.urls.defaults import patterns
+"""
+Tests for the throttling implementations in the permissions module.
+"""
+
from django.test import TestCase
-from django.utils import simplejson as json
+from django.contrib.auth.models import User
+from django.core.cache import cache
from djangorestframework.compat import RequestFactory
from djangorestframework.views import View
-from djangorestframework.permissions import PerUserThrottling
-
+from djangorestframework.permissions import PerUserThrottling, PerViewThrottling, PerResourceThrottling
+from djangorestframework.resources import FormResource
class MockView(View):
permissions = ( PerUserThrottling, )
- throttle = (3, 1) # 3 requests per second
+ throttle = '3/sec'
def get(self, request):
return 'foo'
-urlpatterns = patterns('',
- (r'^$', MockView.as_view()),
-)
-
-
-#class ThrottlingTests(TestCase):
-# """Basic authentication"""
-# urls = 'djangorestframework.tests.throttling'
-#
-# def test_requests_are_throttled(self):
-# """Ensure request rate is limited"""
-# for dummy in range(3):
-# response = self.client.get('/')
-# response = self.client.get('/')
-#
-# def test_request_throttling_is_per_user(self):
-# """Ensure request rate is only limited per user, not globally"""
-# pass
-#
-# def test_request_throttling_expires(self):
-# """Ensure request rate is limited for a limited duration only"""
-# pass
+class MockView_PerViewThrottling(MockView):
+ permissions = ( PerViewThrottling, )
+
+class MockView_PerResourceThrottling(MockView):
+ permissions = ( PerResourceThrottling, )
+ resource = FormResource
+
+class MockView_MinuteThrottling(MockView):
+ throttle = '3/min'
+
+
+
+class ThrottlingTests(TestCase):
+ urls = 'djangorestframework.tests.throttling'
+
+ def setUp(self):
+ """
+ Reset the cache so that no throttles will be active
+ """
+ cache.clear()
+ self.factory = RequestFactory()
+
+ def test_requests_are_throttled(self):
+ """
+ Ensure request rate is limited
+ """
+ request = self.factory.get('/')
+ for dummy in range(4):
+ response = MockView.as_view()(request)
+ self.assertEqual(503, response.status_code)
+
+ def set_throttle_timer(self, view, value):
+ """
+ Explicitly set the timer, overriding time.time()
+ """
+ view.permissions[0].timer = lambda self: value
+
+ def test_request_throttling_expires(self):
+ """
+ Ensure request rate is limited for a limited duration only
+ """
+ self.set_throttle_timer(MockView, 0)
+
+ request = self.factory.get('/')
+ for dummy in range(4):
+ response = MockView.as_view()(request)
+ self.assertEqual(503, response.status_code)
+
+ # Advance the timer by one second
+ self.set_throttle_timer(MockView, 1)
+
+ response = MockView.as_view()(request)
+ self.assertEqual(200, response.status_code)
+
+ def ensure_is_throttled(self, view, expect):
+ request = self.factory.get('/')
+ request.user = User.objects.create(username='a')
+ for dummy in range(3):
+ view.as_view()(request)
+ request.user = User.objects.create(username='b')
+ response = view.as_view()(request)
+ self.assertEqual(expect, response.status_code)
+
+ def test_request_throttling_is_per_user(self):
+ """
+ Ensure request rate is only limited per user, not globally for
+ PerUserThrottles
+ """
+ self.ensure_is_throttled(MockView, 200)
+
+ def test_request_throttling_is_per_view(self):
+ """
+ Ensure request rate is limited globally per View for PerViewThrottles
+ """
+ self.ensure_is_throttled(MockView_PerViewThrottling, 503)
+
+ def test_request_throttling_is_per_resource(self):
+ """
+ Ensure request rate is limited globally per Resource for PerResourceThrottles
+ """
+ self.ensure_is_throttled(MockView_PerResourceThrottling, 503)
+
+
+ def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
+ """
+ Ensure the response returns an X-Throttle field with status and next attributes
+ set properly.
+ """
+ request = self.factory.get('/')
+ for timer, expect in expected_headers:
+ self.set_throttle_timer(view, timer)
+ response = view.as_view()(request)
+ self.assertEquals(response['X-Throttle'], expect)
+
+ def test_seconds_fields(self):
+ """
+ Ensure for second based throttles.
+ """
+ self.ensure_response_header_contains_proper_throttle_field(MockView,
+ ((0, 'status=SUCCESS; next=0.33 sec'),
+ (0, 'status=SUCCESS; next=0.50 sec'),
+ (0, 'status=SUCCESS; next=1.00 sec'),
+ (0, 'status=FAILURE; next=1.00 sec')
+ ))
+
+ def test_minutes_fields(self):
+ """
+ Ensure for minute based throttles.
+ """
+ self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling,
+ ((0, 'status=SUCCESS; next=20.00 sec'),
+ (0, 'status=SUCCESS; next=30.00 sec'),
+ (0, 'status=SUCCESS; next=60.00 sec'),
+ (0, 'status=FAILURE; next=60.00 sec')
+ ))
+
+ def test_next_rate_remains_constant_if_followed(self):
+ """
+ If a client follows the recommended next request rate,
+ the throttling rate should stay constant.
+ """
+ self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling,
+ ((0, 'status=SUCCESS; next=20.00 sec'),
+ (20, 'status=SUCCESS; next=20.00 sec'),
+ (40, 'status=SUCCESS; next=20.00 sec'),
+ (60, 'status=SUCCESS; next=20.00 sec'),
+ (80, 'status=SUCCESS; next=20.00 sec')
+ ))
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 5b3cc855..18d064e1 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -56,15 +56,15 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
"""
List of all authenticating methods to attempt.
"""
- authentication = ( authentication.UserLoggedInAuthenticaton,
- authentication.BasicAuthenticaton )
+ authentication = ( authentication.UserLoggedInAuthentication,
+ authentication.BasicAuthentication )
"""
List of all permissions that must be checked.
"""
permissions = ( permissions.FullAnonAccess, )
-
+
@classmethod
def as_view(cls, **initkwargs):
"""
@@ -101,6 +101,14 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
"""
pass
+
+ def add_header(self, field, value):
+ """
+ Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class.
+ """
+ self.headers[field] = value
+
+
# Note: session based authentication is explicitly CSRF validated,
# all other authentication is CSRF exempt.
@csrf_exempt
@@ -108,6 +116,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
self.request = request
self.args = args
self.kwargs = kwargs
+ self.headers = {}
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
@@ -149,7 +158,10 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
response.headers['Allow'] = ', '.join(self.allowed_methods)
response.headers['Vary'] = 'Authenticate, Accept'
-
+
+ # merge with headers possibly set at some point in the view
+ response.headers.update(self.headers)
+
return self.render(response)
diff --git a/docs/examples/blogpost.rst b/docs/examples/blogpost.rst
index 9d762f52..be91913d 100644
--- a/docs/examples/blogpost.rst
+++ b/docs/examples/blogpost.rst
@@ -8,26 +8,32 @@ Blog Posts API
The models
----------
+In this example we're working from two related models:
+
``models.py``
.. include:: ../../examples/blogpost/models.py
:literal:
-URL configuration
------------------
+Creating the resources
+----------------------
+
+We need to create two resources that we map to our two existing models, in order to describe how the models should be serialized.
+Our resource descriptions will typically go into a module called something like 'resources.py'
-``urls.py``
+``resources.py``
-.. include:: ../../examples/blogpost/urls.py
+.. include:: ../../examples/blogpost/resources.py
:literal:
-Creating the resources
-----------------------
+Creating views for our resources
+--------------------------------
-Once we have some existing models there's very little we need to do to create the corresponding resources. We simply create a base resource and an instance resource for each model we're working with.
-django-rest-framework will provide the default operations on the resources all the usual input validation that Django's models can give us for free.
+Once we've created the resources there's very little we need to do to create the API.
+For each resource we'll create a base view, and an instance view.
+The generic views :class:`.ListOrCreateModelView` and :class:`.InstanceModelView` provide default operations for listing, creating and updating our models via the API, and also automatically provide input validation using default ModelForms for each model.
-#``views.py``
+``urls.py``
-#.. include:: ../../examples/blogpost/views.py
-# :literal: \ No newline at end of file
+.. include:: ../../examples/blogpost/urls.py
+ :literal:
diff --git a/docs/examples/modelresources.rst b/docs/examples/modelviews.rst
index 676656a7..c60c9f24 100644
--- a/docs/examples/modelresources.rst
+++ b/docs/examples/modelviews.rst
@@ -1,7 +1,7 @@
-.. _modelresources:
+.. _modelviews:
-Getting Started - Model Resources
----------------------------------
+Getting Started - Model Views
+-----------------------------
.. note::
@@ -15,28 +15,28 @@ Getting Started - Model Resources
Often you'll want parts of your API to directly map to existing django models. Django REST framework handles this nicely for you in a couple of ways:
-#. It automatically provides suitable create/read/update/delete methods for your resources.
+#. It automatically provides suitable create/read/update/delete methods for your views.
#. Input validation occurs automatically, by using appropriate `ModelForms <http://docs.djangoproject.com/en/dev/topics/forms/modelforms/>`_.
-We'll start of defining two resources in our urlconf again.
+Here's the model we're working from in this example:
-``urls.py``
+``models.py``
-.. include:: ../../examples/modelresourceexample/urls.py
+.. include:: ../../examples/modelresourceexample/models.py
:literal:
-Here's the models we're working from in this example. It's usually a good idea to make sure you provide the :func:`get_absolute_url()` `permalink <http://docs.djangoproject.com/en/dev/ref/models/instances/#get-absolute-url>`_ for all models you want to expose via the API.
+To add an API for the model, first we need to create a Resource for the model.
-``models.py``
+``resources.py``
-.. include:: ../../examples/modelresourceexample/models.py
+.. include:: ../../examples/modelresourceexample/resources.py
:literal:
-Now that we've got some models and a urlconf, there's very little code to write. We'll create a :class:`.ModelResource` to map to instances of our models, and a top level :class:`.RootModelResource` to list the existing instances and to create new instances.
+Then we simply map a couple of views to the Resource in our urlconf.
-``views.py``
+``urls.py``
-.. include:: ../../examples/modelresourceexample/views.py
+.. include:: ../../examples/modelresourceexample/urls.py
:literal:
And we're done. We've now got a fully browseable API, which supports multiple input and output media types, and has all the nice automatic field validation that Django gives us for free.
@@ -56,5 +56,3 @@ Or access it from the command line using curl:
# Demonstrates API's input validation using JSON input
bash: curl -X POST -H 'Content-Type: application/json' --data-binary '{"foo":true}' http://api.django-rest-framework.org/model-resource-example/
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}
-
-We could also have added the handler methods :meth:`.Resource.get()`, :meth:`.Resource.post()` etc... seen in the last example, but Django REST framework provides nice default implementations for us that do exactly what we'd expect them to.
diff --git a/docs/examples/resources.rst b/docs/examples/views.rst
index f3242421..59e13976 100644
--- a/docs/examples/resources.rst
+++ b/docs/examples/views.rst
@@ -1,7 +1,7 @@
-.. _resources:
+.. _views:
-Getting Started - Resources
----------------------------
+Getting Started - Views
+-----------------------
.. note::
@@ -15,12 +15,12 @@ Getting Started - Resources
We're going to start off with a simple example, that demonstrates a few things:
-#. Creating resources.
-#. Linking resources.
-#. Writing method handlers on resources.
-#. Adding form validation to resources.
+#. Creating views.
+#. Linking views.
+#. Writing method handlers on views.
+#. Adding form validation to views.
-First we'll define two resources in our urlconf.
+First we'll define two views in our urlconf.
``urls.py``
@@ -34,7 +34,7 @@ Now we'll add a form that we'll use for input validation. This is completely op
.. include:: ../../examples/resourceexample/forms.py
:literal:
-Now we'll write our resources. The first is a read only resource that links to three instances of the second. The second resource just has some stub handler methods to help us see that our example is working.
+Now we'll write our views. The first is a read only view that links to three instances of the second. The second view just has some stub handler methods to help us see that our example is working.
``views.py``
diff --git a/docs/howto/alternativeframeworks.rst b/docs/howto/alternativeframeworks.rst
index c6eba1dd..dc8d1ea6 100644
--- a/docs/howto/alternativeframeworks.rst
+++ b/docs/howto/alternativeframeworks.rst
@@ -1,6 +1,35 @@
-Alternative Frameworks
-======================
+Alternative frameworks & Why Django REST framework
+==================================================
-#. `django-piston <https://bitbucket.org/jespern/django-piston/wiki/Home>`_ is excellent, and has a great community behind it. This project is based on piston code in parts.
+Alternative frameworks
+----------------------
-#. `django-tasypie <https://github.com/toastdriven/django-tastypie>`_ is also well worth looking at.
+There are a number of alternative REST frameworks for Django:
+
+* `django-piston <https://bitbucket.org/jespern/django-piston/wiki/Home>`_ is very mature, and has a large community behind it. This project was originally based on piston code in parts.
+* `django-tasypie <https://github.com/toastdriven/django-tastypie>`_ is also very good, and has a very active and helpful developer community and maintainers.
+* Other interesting projects include `dagny <https://github.com/zacharyvoase/dagny>`_ and `dj-webmachine <http://benoitc.github.com/dj-webmachine/>`_
+
+
+Why use Django REST framework?
+------------------------------
+
+The big benefits of using Django REST framework come down to:
+
+1. It's based on Django's class based views, which makes it simple, modular, and future-proof.
+2. It stays as close as possible to Django idioms and language throughout.
+3. The browse-able API makes working with the APIs extremely quick and easy.
+
+
+Why was this project created?
+-----------------------------
+
+For me the browse-able API is the most important aspect of Django REST framework.
+
+I wanted to show that Web APIs could easily be made Web browse-able,
+and demonstrate how much better browse-able Web APIs are to work with.
+
+Being able to navigate and use a Web API directly in the browser is a huge win over only having command line and programmatic
+access to the API. It enables the API to be properly self-describing, and it makes it much much quicker and easier to work with.
+There's no fundamental reason why the Web APIs we're creating shouldn't be able to render to HTML as well as JSON/XML/whatever,
+and I really think that more Web API frameworks *in whatever language* ought to be taking a similar approach.
diff --git a/docs/howto/setup.rst b/docs/howto/setup.rst
index e59ea90e..b4fbc037 100644
--- a/docs/howto/setup.rst
+++ b/docs/howto/setup.rst
@@ -3,6 +3,13 @@
Setup
=====
+Installing into site-packages
+-----------------------------
+
+If you need to manually install Django REST framework to your ``site-packages`` directory, run the ``setup.py`` script::
+
+ python setup.py install
+
Template Loaders
----------------
diff --git a/docs/howto/usingcurl.rst b/docs/howto/usingcurl.rst
index e7edfef9..2ad2c764 100644
--- a/docs/howto/usingcurl.rst
+++ b/docs/howto/usingcurl.rst
@@ -23,4 +23,8 @@ There are a few things that can be helpful to remember when using CURL with djan
#. Or any other content type::
- curl -X PUT -H 'Content-Type: application/json' --data-binary '{"foo":"bar"}' http://example.com/my-api/some-resource/ \ No newline at end of file
+ curl -X PUT -H 'Content-Type: application/json' --data-binary '{"foo":"bar"}' http://example.com/my-api/some-resource/
+
+#. You can use basic authentication to send the username and password::
+
+ curl -X GET -H 'Accept: application/json' -u <user>:<password> http://example.com/my-api/ \ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
index 3b4e9c49..8a285271 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -30,15 +30,11 @@ Resources
* The ``djangorestframework`` package is `available on PyPI <http://pypi.python.org/pypi/djangorestframework>`_.
* We have an active `discussion group <http://groups.google.com/group/django-rest-framework>`_ and a `project blog <http://blog.django-rest-framework.org>`_.
-* Bug reports are handled on the `issue tracker <https://bitbucket.org/tomchristie/django-rest-framework/issues?sort=version>`_.
-* There is a `Jenkins CI server <http://datacenter.tibold.nl/job/djangorestframework/>`_ which tracks test status and coverage reporting. (Thanks Marko!)
-* Get with in touch with `@thisneonsoul <https://twitter.com/thisneonsoul>`_ on twitter.
+* Bug reports are handled on the `issue tracker <https://github.com/tomchristie/django-rest-framework/issues>`_.
+* There is a `Jenkins CI server <http://jenkins.tibold.nl/job/djangorestframework/>`_ which tracks test status and coverage reporting. (Thanks Marko!)
Any and all questions, thoughts, bug reports and contributions are *hugely appreciated*.
-We'd like for this to be a real community driven effort, so come say hi, get involved, and get forking! (See: `Forking a Bitbucket Repository
-<http://confluence.atlassian.com/display/BITBUCKET/Forking+a+Bitbucket+Repository>`_, or `Fork A GitHub Repo <http://help.github.com/fork-a-repo/>`_)
-
Requirements
------------
@@ -46,8 +42,8 @@ Requirements
* Django (1.2, 1.3 supported)
-Installation & Setup
---------------------
+Installation
+------------
You can install Django REST framework using ``pip`` or ``easy_install``::
@@ -59,61 +55,51 @@ Or get the latest development version using mercurial or git::
hg clone https://bitbucket.org/tomchristie/django-rest-framework
git clone git@github.com:tomchristie/django-rest-framework.git
-Or you can download the current release:
-
-* `django-rest-framework-0.1.tar.gz <https://bitbucket.org/tomchristie/django-rest-framework/downloads/django-rest-framework-0.1.tar.gz>`_
-* `django-rest-framework-0.1.zip <https://bitbucket.org/tomchristie/django-rest-framework/downloads/django-rest-framework-0.1.zip>`_
-
-and then install Django REST framework to your ``site-packages`` directory, by running the ``setup.py`` script::
+Or you can `download the current release <http://pypi.python.org/pypi/djangorestframework>`_.
- python setup.py install
+Setup
+-----
-**To add django-rest-framework to a Django project:**
+To add Django REST framework to a Django project:
* Ensure that the ``djangorestframework`` directory is on your ``PYTHONPATH``.
* Add ``djangorestframework`` to your ``INSTALLED_APPS``.
-For more information take a look at the :ref:`setup` section.
+For more information on settings take a look at the :ref:`setup` section.
Getting Started
---------------
-Using Django REST framework can be as simple as adding a few lines to your urlconf and adding a `permalink <http://docs.djangoproject.com/en/dev/ref/models/instances/#get-absolute-url>`_ to your model.
+Using Django REST framework can be as simple as adding a few lines to your urlconf.
-`urls.py`::
+``urls.py``::
from django.conf.urls.defaults import patterns, url
- from djangorestframework import ModelResource, RootModelResource
- from models import MyModel
+ from djangorestframework.resources import ModelResource
+ from djangorestframework.views import ListOrCreateModelView, InstanceModelView
+ from myapp.models import MyModel
+
+ class MyResource(ModelResource):
+ model = MyModel
urlpatterns = patterns('',
- url(r'^$', RootModelResource.as_view(model=MyModel)),
- url(r'^(?P<pk>[^/]+)/$', ModelResource.as_view(model=MyModel), name='my-model'),
- )
-
-`models.py`::
-
- class MyModel(models.Model):
-
- # (Rest of model definition...)
-
- @models.permalink
- def get_absolute_url(self):
- return ('my-model', (self.pk,))
+ url(r'^$', ListOrCreateModelView.as_view(resource=MyResource)),
+ url(r'^(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=MyResource)),
+ )
Django REST framework comes with two "getting started" examples.
-#. :ref:`resources`
-#. :ref:`modelresources`
+#. :ref:`views`
+#. :ref:`modelviews`
Examples
--------
There are a few real world web API examples included with Django REST framework.
-#. :ref:`objectstore` - Using :class:`.Resource` for resources that do not map to models.
-#. :ref:`codehighlighting` - Using :class:`.Resource` with forms for input validation.
-#. :ref:`blogposts` - Using :class:`.ModelResource` for resources that map directly to models.
+#. :ref:`objectstore` - Using :class:`views.View` classes for APIs that do not map to models.
+#. :ref:`codehighlighting` - Using :class:`views.View` classes with forms for input validation.
+#. :ref:`blogposts` - Using :class:`views.ModelView` classes for APIs that map directly to models.
All the examples are freely available for testing in the sandbox:
@@ -148,6 +134,7 @@ Library Reference
library/renderers
library/resource
library/response
+ library/serializer
library/status
library/views
@@ -157,8 +144,8 @@ Examples Reference
.. toctree::
:maxdepth: 1
- examples/resources
- examples/modelresources
+ examples/views
+ examples/modelviews
examples/objectstore
examples/pygments
examples/blogpost
diff --git a/docs/library/serializer.rst b/docs/library/serializer.rst
new file mode 100644
index 00000000..63dd3308
--- /dev/null
+++ b/docs/library/serializer.rst
@@ -0,0 +1,5 @@
+:mod:`serializer`
+=================
+
+.. automodule:: serializer
+ :members:
diff --git a/examples/blogpost/models.py b/examples/blogpost/models.py
index c4925a15..d77f530d 100644
--- a/examples/blogpost/models.py
+++ b/examples/blogpost/models.py
@@ -22,6 +22,9 @@ class BlogPost(models.Model):
slug = models.SlugField(editable=False, default='')
def save(self, *args, **kwargs):
+ """
+ For the purposes of the sandbox, limit the maximum number of stored models.
+ """
self.slug = slugify(self.title)
super(self.__class__, self).save(*args, **kwargs)
for obj in self.__class__.objects.order_by('-created')[MAX_POSTS:]:
diff --git a/examples/blogpost/resources.py b/examples/blogpost/resources.py
new file mode 100644
index 00000000..9b91ed73
--- /dev/null
+++ b/examples/blogpost/resources.py
@@ -0,0 +1,27 @@
+from django.core.urlresolvers import reverse
+from djangorestframework.resources import ModelResource
+from blogpost.models import BlogPost, Comment
+
+
+class BlogPostResource(ModelResource):
+ """
+ A Blog Post has a *title* and *content*, and can be associated with zero or more comments.
+ """
+ model = BlogPost
+ fields = ('created', 'title', 'slug', 'content', 'url', 'comments')
+ ordering = ('-created',)
+
+ def comments(self, instance):
+ return reverse('comments', kwargs={'blogpost': instance.key})
+
+
+class CommentResource(ModelResource):
+ """
+ A Comment is associated with a given Blog Post and has a *username* and *comment*, and optionally a *rating*.
+ """
+ model = Comment
+ fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
+ ordering = ('-created',)
+
+ def blogpost(self, instance):
+ return reverse('blog-post', kwargs={'key': instance.blogpost.key}) \ No newline at end of file
diff --git a/examples/blogpost/tests.py b/examples/blogpost/tests.py
index 9b9a682f..e55f0f90 100644
--- a/examples/blogpost/tests.py
+++ b/examples/blogpost/tests.py
@@ -7,79 +7,80 @@ from django.core.urlresolvers import reverse
from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory
-
-from blogpost import views, models
-import blogpost
-
-
-class AcceptHeaderTests(TestCase):
- """Test correct behaviour of the Accept header as specified by RFC 2616:
-
- http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1"""
-
- def assert_accept_mimetype(self, mimetype, expect=None):
- """Assert that a request with given mimetype in the accept header,
- gives a response with the appropriate content-type."""
- if expect is None:
- expect = mimetype
-
- resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype)
-
- self.assertEquals(resp['content-type'], expect)
-
-
- def dont_test_accept_json(self):
- """Ensure server responds with Content-Type of JSON when requested."""
- self.assert_accept_mimetype('application/json')
-
- def dont_test_accept_xml(self):
- """Ensure server responds with Content-Type of XML when requested."""
- self.assert_accept_mimetype('application/xml')
-
- def dont_test_accept_json_when_prefered_to_xml(self):
- """Ensure server responds with Content-Type of JSON when it is the client's prefered choice."""
- self.assert_accept_mimetype('application/json;q=0.9, application/xml;q=0.1', expect='application/json')
-
- def dont_test_accept_xml_when_prefered_to_json(self):
- """Ensure server responds with Content-Type of XML when it is the client's prefered choice."""
- self.assert_accept_mimetype('application/json;q=0.1, application/xml;q=0.9', expect='application/xml')
-
- def dont_test_default_json_prefered(self):
- """Ensure server responds with JSON in preference to XML."""
- self.assert_accept_mimetype('application/json,application/xml', expect='application/json')
-
- def dont_test_accept_generic_subtype_format(self):
- """Ensure server responds with an appropriate type, when the subtype is left generic."""
- self.assert_accept_mimetype('text/*', expect='text/html')
-
- def dont_test_accept_generic_type_format(self):
- """Ensure server responds with an appropriate type, when the type and subtype are left generic."""
- self.assert_accept_mimetype('*/*', expect='application/json')
-
- def dont_test_invalid_accept_header_returns_406(self):
- """Ensure server returns a 406 (not acceptable) response if we set the Accept header to junk."""
- resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT='invalid/invalid')
- self.assertNotEquals(resp['content-type'], 'invalid/invalid')
- self.assertEquals(resp.status_code, 406)
-
- def dont_test_prefer_specific_over_generic(self): # This test is broken right now
- """More specific accept types have precedence over less specific types."""
- self.assert_accept_mimetype('application/xml, */*', expect='application/xml')
- self.assert_accept_mimetype('*/*, application/xml', expect='application/xml')
-
-
-class AllowedMethodsTests(TestCase):
- """Basic tests to check that only allowed operations may be performed on a Resource"""
-
- def dont_test_reading_a_read_only_resource_is_allowed(self):
- """GET requests on a read only resource should default to a 200 (OK) response"""
- resp = self.client.get(reverse(views.RootResource))
- self.assertEquals(resp.status_code, 200)
-
- def dont_test_writing_to_read_only_resource_is_not_allowed(self):
- """PUT requests on a read only resource should default to a 405 (method not allowed) response"""
- resp = self.client.put(reverse(views.RootResource), {})
- self.assertEquals(resp.status_code, 405)
+from djangorestframework.views import InstanceModelView, ListOrCreateModelView
+
+from blogpost import models, urls
+#import blogpost
+
+
+# class AcceptHeaderTests(TestCase):
+# """Test correct behaviour of the Accept header as specified by RFC 2616:
+#
+# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1"""
+#
+# def assert_accept_mimetype(self, mimetype, expect=None):
+# """Assert that a request with given mimetype in the accept header,
+# gives a response with the appropriate content-type."""
+# if expect is None:
+# expect = mimetype
+#
+# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype)
+#
+# self.assertEquals(resp['content-type'], expect)
+#
+#
+# def dont_test_accept_json(self):
+# """Ensure server responds with Content-Type of JSON when requested."""
+# self.assert_accept_mimetype('application/json')
+#
+# def dont_test_accept_xml(self):
+# """Ensure server responds with Content-Type of XML when requested."""
+# self.assert_accept_mimetype('application/xml')
+#
+# def dont_test_accept_json_when_prefered_to_xml(self):
+# """Ensure server responds with Content-Type of JSON when it is the client's prefered choice."""
+# self.assert_accept_mimetype('application/json;q=0.9, application/xml;q=0.1', expect='application/json')
+#
+# def dont_test_accept_xml_when_prefered_to_json(self):
+# """Ensure server responds with Content-Type of XML when it is the client's prefered choice."""
+# self.assert_accept_mimetype('application/json;q=0.1, application/xml;q=0.9', expect='application/xml')
+#
+# def dont_test_default_json_prefered(self):
+# """Ensure server responds with JSON in preference to XML."""
+# self.assert_accept_mimetype('application/json,application/xml', expect='application/json')
+#
+# def dont_test_accept_generic_subtype_format(self):
+# """Ensure server responds with an appropriate type, when the subtype is left generic."""
+# self.assert_accept_mimetype('text/*', expect='text/html')
+#
+# def dont_test_accept_generic_type_format(self):
+# """Ensure server responds with an appropriate type, when the type and subtype are left generic."""
+# self.assert_accept_mimetype('*/*', expect='application/json')
+#
+# def dont_test_invalid_accept_header_returns_406(self):
+# """Ensure server returns a 406 (not acceptable) response if we set the Accept header to junk."""
+# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT='invalid/invalid')
+# self.assertNotEquals(resp['content-type'], 'invalid/invalid')
+# self.assertEquals(resp.status_code, 406)
+#
+# def dont_test_prefer_specific_over_generic(self): # This test is broken right now
+# """More specific accept types have precedence over less specific types."""
+# self.assert_accept_mimetype('application/xml, */*', expect='application/xml')
+# self.assert_accept_mimetype('*/*, application/xml', expect='application/xml')
+#
+#
+# class AllowedMethodsTests(TestCase):
+# """Basic tests to check that only allowed operations may be performed on a Resource"""
+#
+# def dont_test_reading_a_read_only_resource_is_allowed(self):
+# """GET requests on a read only resource should default to a 200 (OK) response"""
+# resp = self.client.get(reverse(views.RootResource))
+# self.assertEquals(resp.status_code, 200)
+#
+# def dont_test_writing_to_read_only_resource_is_not_allowed(self):
+# """PUT requests on a read only resource should default to a 405 (method not allowed) response"""
+# resp = self.client.put(reverse(views.RootResource), {})
+# self.assertEquals(resp.status_code, 405)
#
# def test_reading_write_only_not_allowed(self):
# resp = self.client.get(reverse(views.WriteOnlyResource))
@@ -178,32 +179,33 @@ class TestRotation(TestCase):
models.BlogPost.objects.all().delete()
def test_get_to_root(self):
- '''Simple test to demonstrate how the requestfactory needs to be used'''
+ '''Simple get to the *root* url of blogposts'''
request = self.factory.get('/blog-post')
- view = views.BlogPosts.as_view()
+ view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
response = view(request)
self.assertEqual(response.status_code, 200)
def test_blogposts_not_exceed_MAX_POSTS(self):
'''Posting blog-posts should not result in more than MAX_POSTS items stored.'''
- for post in range(views.MAX_POSTS + 5):
+ for post in range(models.MAX_POSTS + 5):
form_data = {'title': 'This is post #%s' % post, 'content': 'This is the content of post #%s' % post}
request = self.factory.post('/blog-post', data=form_data)
- view = views.BlogPosts.as_view()
+ view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
view(request)
- self.assertEquals(len(models.BlogPost.objects.all()),views.MAX_POSTS)
+ self.assertEquals(len(models.BlogPost.objects.all()),models.MAX_POSTS)
def test_fifo_behaviour(self):
'''It's fine that the Blogposts are capped off at MAX_POSTS. But we want to make sure we see FIFO behaviour.'''
for post in range(15):
form_data = {'title': '%s' % post, 'content': 'This is the content of post #%s' % post}
request = self.factory.post('/blog-post', data=form_data)
- view = views.BlogPosts.as_view()
+ view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
view(request)
request = self.factory.get('/blog-post')
- view = views.BlogPosts.as_view()
+ view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
response = view(request)
response_posts = json.loads(response.content)
response_titles = [d['title'] for d in response_posts]
- self.assertEquals(response_titles, ['%s' % i for i in range(views.MAX_POSTS - 5, views.MAX_POSTS + 5)])
+ response_titles.reverse()
+ self.assertEquals(response_titles, ['%s' % i for i in range(models.MAX_POSTS - 5, models.MAX_POSTS + 5)])
\ No newline at end of file
diff --git a/examples/blogpost/urls.py b/examples/blogpost/urls.py
index c677b8fa..e9bd2754 100644
--- a/examples/blogpost/urls.py
+++ b/examples/blogpost/urls.py
@@ -1,36 +1,11 @@
from django.conf.urls.defaults import patterns, url
-from django.core.urlresolvers import reverse
-
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
-from djangorestframework.resources import ModelResource
-
-from blogpost.models import BlogPost, Comment
-
-
-class BlogPostResource(ModelResource):
- """
- A Blog Post has a *title* and *content*, and can be associated with zero or more comments.
- """
- model = BlogPost
- fields = ('created', 'title', 'slug', 'content', 'url', 'comments')
- ordering = ('-created',)
-
- def comments(self, instance):
- return reverse('comments', kwargs={'blogpost': instance.key})
-
-
-class CommentResource(ModelResource):
- """
- A Comment is associated with a given Blog Post and has a *username* and *comment*, and optionally a *rating*.
- """
- model = Comment
- fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
- ordering = ('-created',)
+from blogpost.resources import BlogPostResource, CommentResource
urlpatterns = patterns('',
url(r'^$', ListOrCreateModelView.as_view(resource=BlogPostResource), name='blog-posts-root'),
- url(r'^(?P<key>[^/]+)/$', InstanceModelView.as_view(resource=BlogPostResource)),
+ url(r'^(?P<key>[^/]+)/$', InstanceModelView.as_view(resource=BlogPostResource), name='blog-post'),
url(r'^(?P<blogpost>[^/]+)/comments/$', ListOrCreateModelView.as_view(resource=CommentResource), name='comments'),
url(r'^(?P<blogpost>[^/]+)/comments/(?P<id>[^/]+)/$', InstanceModelView.as_view(resource=CommentResource)),
)
diff --git a/examples/modelresourceexample/resources.py b/examples/modelresourceexample/resources.py
new file mode 100644
index 00000000..634ea6b3
--- /dev/null
+++ b/examples/modelresourceexample/resources.py
@@ -0,0 +1,7 @@
+from djangorestframework.resources import ModelResource
+from modelresourceexample.models import MyModel
+
+class MyModelResource(ModelResource):
+ model = MyModel
+ fields = ('foo', 'bar', 'baz', 'url')
+ ordering = ('created',)
diff --git a/examples/modelresourceexample/urls.py b/examples/modelresourceexample/urls.py
index bb71ddd3..b6a16542 100644
--- a/examples/modelresourceexample/urls.py
+++ b/examples/modelresourceexample/urls.py
@@ -1,14 +1,8 @@
from django.conf.urls.defaults import patterns, url
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
-from djangorestframework.resources import ModelResource
-from modelresourceexample.models import MyModel
-
-class MyModelResource(ModelResource):
- model = MyModel
- fields = ('foo', 'bar', 'baz', 'url')
- ordering = ('created',)
+from modelresourceexample.resources import MyModelResource
urlpatterns = patterns('',
url(r'^$', ListOrCreateModelView.as_view(resource=MyModelResource), name='model-resource-root'),
- url(r'^([0-9]+)/$', InstanceModelView.as_view(resource=MyModelResource)),
+ url(r'^(?P<pk>[0-9]+)/$', InstanceModelView.as_view(resource=MyModelResource)),
)
diff --git a/examples/modelresourceexample/views.py b/examples/permissionsexample/__init__.py
index e69de29b..e69de29b 100644
--- a/examples/modelresourceexample/views.py
+++ b/examples/permissionsexample/__init__.py
diff --git a/examples/permissionsexample/urls.py b/examples/permissionsexample/urls.py
new file mode 100644
index 00000000..d17f5159
--- /dev/null
+++ b/examples/permissionsexample/urls.py
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import patterns, url
+from permissionsexample.views import ThrottlingExampleView
+
+urlpatterns = patterns('',
+ url(r'^$', ThrottlingExampleView.as_view(), name='throttled-resource'),
+)
diff --git a/examples/permissionsexample/views.py b/examples/permissionsexample/views.py
new file mode 100644
index 00000000..20e7cba7
--- /dev/null
+++ b/examples/permissionsexample/views.py
@@ -0,0 +1,20 @@
+from djangorestframework.views import View
+from djangorestframework.permissions import PerUserThrottling
+
+
+class ThrottlingExampleView(View):
+ """
+ A basic read-only View that has a **per-user throttle** of 10 requests per minute.
+
+ If a user exceeds the 10 requests limit within a period of one minute, the
+ throttle will be applied until 60 seconds have passed since the first request.
+ """
+
+ permissions = ( PerUserThrottling, )
+ throttle = '10/min'
+
+ def get(self, request):
+ """
+ Handle GET requests.
+ """
+ return "Successful response to GET request because throttle is not yet active." \ No newline at end of file
diff --git a/examples/pygments_api/views.py b/examples/pygments_api/views.py
index 76647107..e50029f6 100644
--- a/examples/pygments_api/views.py
+++ b/examples/pygments_api/views.py
@@ -46,19 +46,12 @@ class HTMLRenderer(BaseRenderer):
media_type = 'text/html'
-
-class PygmentsFormResource(FormResource):
- """
- """
- form = PygmentsForm
-
-
class PygmentsRoot(View):
"""
- This example demonstrates a simple RESTful Web API aound the awesome pygments library.
+ This example demonstrates a simple RESTful Web API around the awesome pygments library.
This top level resource is used to create highlighted code snippets, and to list all the existing code snippets.
"""
- resource = PygmentsFormResource
+ form = PygmentsForm
def get(self, request):
"""
diff --git a/examples/resourceexample/urls.py b/examples/resourceexample/urls.py
index cb6435bb..6e141f3c 100644
--- a/examples/resourceexample/urls.py
+++ b/examples/resourceexample/urls.py
@@ -1,7 +1,7 @@
from django.conf.urls.defaults import patterns, url
-from resourceexample.views import ExampleResource, AnotherExampleResource
+from resourceexample.views import ExampleView, AnotherExampleView
urlpatterns = patterns('',
- url(r'^$', ExampleResource.as_view(), name='example-resource'),
- url(r'^(?P<num>[0-9]+)/$', AnotherExampleResource.as_view(), name='another-example-resource'),
+ url(r'^$', ExampleView.as_view(), name='example-resource'),
+ url(r'^(?P<num>[0-9]+)/$', AnotherExampleView.as_view(), name='another-example'),
)
diff --git a/examples/resourceexample/views.py b/examples/resourceexample/views.py
index 29651fbf..990c7834 100644
--- a/examples/resourceexample/views.py
+++ b/examples/resourceexample/views.py
@@ -1,42 +1,45 @@
from django.core.urlresolvers import reverse
from djangorestframework.views import View
-from djangorestframework.resources import FormResource
from djangorestframework.response import Response
from djangorestframework import status
from resourceexample.forms import MyForm
-class MyFormValidation(FormResource):
- """
- A resource which applies form validation on the input.
- """
- form = MyForm
-
-class ExampleResource(View):
+class ExampleView(View):
"""
- A basic read-only resource that points to 3 other resources.
+ A basic read-only view that points to 3 other views.
"""
def get(self, request):
- return {"Some other resources": [reverse('another-example-resource', kwargs={'num':num}) for num in range(3)]}
+ """
+ Handle GET requests, returning a list of URLs pointing to 3 other views.
+ """
+ return {"Some other resources": [reverse('another-example', kwargs={'num':num}) for num in range(3)]}
-class AnotherExampleResource(View):
+class AnotherExampleView(View):
"""
- A basic GET-able/POST-able resource.
+ A basic view, that can be handle GET and POST requests.
+ Applies some simple form validation on POST requests.
"""
- resource = MyFormValidation
+ form = MyForm
def get(self, request, num):
- """Handle GET requests"""
+ """
+ Handle GET requests.
+ Returns a simple string indicating which view the GET request was for.
+ """
if int(num) > 2:
return Response(status.HTTP_404_NOT_FOUND)
return "GET request to AnotherExampleResource %s" % num
def post(self, request, num):
- """Handle POST requests"""
+ """
+ Handle POST requests, with form validation.
+ Returns a simple string indicating what content was supplied.
+ """
if int(num) > 2:
return Response(status.HTTP_404_NOT_FOUND)
return "POST request to AnotherExampleResource %s, with content: %s" % (num, repr(self.CONTENT))
diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py
index 1c55c28f..1e326f43 100644
--- a/examples/sandbox/views.py
+++ b/examples/sandbox/views.py
@@ -31,4 +31,6 @@ class Sandbox(View):
{'name': 'Simple Mixin-only example', 'url': reverse('mixin-view')},
{'name': 'Object store API', 'url': reverse('object-store-root')},
{'name': 'Code highlighting API', 'url': reverse('pygments-root')},
- {'name': 'Blog posts API', 'url': reverse('blog-posts-root')}]
+ {'name': 'Blog posts API', 'url': reverse('blog-posts-root')},
+ {'name': 'Permissions example', 'url': reverse('throttled-resource')}
+ ]
diff --git a/examples/urls.py b/examples/urls.py
index cf4d4042..08d97a14 100644
--- a/examples/urls.py
+++ b/examples/urls.py
@@ -10,6 +10,7 @@ urlpatterns = patterns('',
(r'^object-store/', include('objectstore.urls')),
(r'^pygments/', include('pygments_api.urls')),
(r'^blog-post/', include('blogpost.urls')),
+ (r'^permissions-example/', include('permissionsexample.urls')),
(r'^', include('djangorestframework.urls')),
)
diff --git a/setup.py b/setup.py
index e5dc4e0e..1d738328 100644
--- a/setup.py
+++ b/setup.py
@@ -3,13 +3,19 @@
from setuptools import setup
+import os, re
+
+path = os.path.join(os.path.dirname(__file__), 'djangorestframework', '__init__.py')
+init_py = open(path).read()
+VERSION = re.match("__version__ = '([^']+)'", init_py).group(1)
+
setup(
- name = "djangorestframework",
- version = "0.1",
- url = 'https://bitbucket.org/tomchristie/django-rest-framework/wiki/Home',
- download_url = 'https://bitbucket.org/tomchristie/django-rest-framework/downloads',
+ name = 'djangorestframework',
+ version = VERSION,
+ url = 'http://django-rest-framework.org',
+ download_url = 'http://pypi.python.org/pypi/djangorestframework/',
license = 'BSD',
- description = "A lightweight REST framework for Django.",
+ description = 'A lightweight REST framework for Django.',
author = 'Tom Christie',
author_email = 'tom@tomchristie.com',
packages = ['djangorestframework',
@@ -31,5 +37,3 @@ setup(
'Topic :: Internet :: WWW/HTTP',
]
)
-
-