pax_global_header 0000666 0000000 0000000 00000000064 12245741361 0014517 g ustar 00root root 0000000 0000000 52 comment=d782b82e476497aa98d68acf16b5b50ea167d21d
django-conneg-0.9.4/ 0000775 0000000 0000000 00000000000 12245741361 0014242 5 ustar 00root root 0000000 0000000 django-conneg-0.9.4/.gitignore 0000664 0000000 0000000 00000000035 12245741361 0016230 0 ustar 00root root 0000000 0000000 *.pyc
*~
dist
build
MANIFEST
django-conneg-0.9.4/MANIFEST.in 0000664 0000000 0000000 00000000055 12245741361 0016000 0 ustar 00root root 0000000 0000000 include requirements.txt
include README.rst
django-conneg-0.9.4/README.rst 0000664 0000000 0000000 00000017337 12245741361 0015744 0 ustar 00root root 0000000 0000000 Content-negotiation framework for Django
========================================
This project provides a simple and extensible framework for producing views
that content-negotiate in Django.
Prerequisites
-------------
This library depends on Django 1.3, which you can install using your package
manager on recent distributions, or using pip::
pip install -r requirements.txt
``pip`` is called ``pip-python`` on Fedora. It is generally provided by a
``python-pip`` package.
Using
-----
To define a view, do something like this::
from django_conneg.views import ContentNegotiatedView
class IndexView(ContentNegotiatedView):
def get(self, request):
context = {
# Build context here
}
# Call render, passing a template name (without file extension)
return self.render(request, context, 'index')
This will then look for a renderer that can provide a representation that
matches what was asked for in the Accept header.
By default ContentNegotiatedView provides no renderers, so the above snippet
would always return a 405 Not Acceptable to tell the user-agent that it
couldn't provide a response in a suggested format.
To define a renderer on a view, do something like this::
import json
from django.http import HttpResponse
from django_conneg.decorators import renderer
class JSONView(ContentNegotiatedView):
@renderer(format='json', mimetypes=('application/json',), name='JSON')
def render_json(self, request, context, template_name):
# Very simplistic, and will fail when it encounters 'non-primitives'
# like Django Model objects, Forms, etc.
return HttpResponse(json.dumps(context), mimetype='application/json')
.. note::
``django-conneg`` already provides a slightly more sophisticated JSONView;
see below for more information.
You can render to a particular format by calling ``render_to_format()`` on the
view::
class IndexView(ContentNegotiatedView):
def get(self, request):
# ...
if some_condition:
return self.render_to_format(request, context, 'index', 'html')
else:
return self.render(request, context, 'index')
Forcing a particular renderer from the client
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default, a client can request a particular set of renderers be tried by
using the ``format`` query or POST parameter::
GET /some-view/?format=json,yaml
The formats correspond to the ``format`` argument to the ``@renderer``
decorator.
To change the name of the parameter used, override
``_format_override_parameter`` on the view class::
class MyView(ContentNegotiatedView):
_format_override_parameter = 'output'
Providing fallback renderers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sometimes you might want to provide a response in some format even if the
those in the Accept header can't be honoured. This is useful when providing
error responses in a different format to the client's expected format. To do
this, set the ``_force_fallback_format`` attribute to the name of the format::
class MyView(ContentNegotiatedView):
_force_fallback_format = 'html'
If a client doesn't provide an Accept header, then you can specify a default
format with ``_default_format``::
class MyView(ContentNegotiatedView):
_default_format = 'html'
Built-in renderer views
~~~~~~~~~~~~~~~~~~~~~~~
``django_conneg`` includes the following built-in renderers in the
``django_conneg.views`` module:
* ``HTMLView`` (renders a ``.html`` template with media type ``text/html``)
* ``TextView`` (renders a ``.txt`` template with media type ``text/plain``)
* ``JSONView`` (coerces the context to JavaScript primitives and returns as ``application/json``)
* ``JSONPView`` (as ``JSONView``, but wraps in a callback and returns as ``application/javascript``)
Using these, you could define a view that renders to both HTML and JSON like this::
from django_conneg.views import HTMLView
class IndexView(JSONView, HTMLView):
def get(self, request):
# ...
return self.render(request, context, 'index')
Accessing renderer details
--------------------------
The renderer used to construct a response is exposed as a ``renderer``
attribute on the response object::
class IndexView(JSONView, HTMLView):
def get(self, request):
# ...
response = self.render(request, context, 'index')
response['X-Renderer-Format'] = response.renderer.format
return response
Renderer priorities
-------------------
Some user-agents might specify various media types with equal levels of
desirability. For example, previous versions of Safari and Chrome `used to send
`_
an ``Accept`` header like this::
application/xml,application/xhtml+xml,text/html;q=0.9,
text/plain;q=0.8,image/png,*/*;q=0.5
Without any additional hints it would be non-deterministic as to whether XML or
XHTML is served.
By passing a ``priority`` argument to the ``@renderer`` decorator you can
specify an ordering of renderers for such ambiguous situations::
class IndexView(ContentNegotiatedView):
@renderer(format='xml', mimetypes=('application/xml',), name='XML', priority=0)
def render_xml(request, context, template_name):
# ...
@renderer(format='html', mimetypes=('application/xhtml+xml','text/html), name='HTML', priority=1)
def render_html(request, context, template_name):
# ...
As higher-numbered priorities are preferred, this will result in HTML always
being prefered over XML in ambiguous situations.
By default, ``django-conneg``'s built-in renderers have a priority of 0, except
for ``HTMLView`` and ``TextView``, which each have a priority of 1 for the
reason given above.
Improved 40x response handling
------------------------------
Django provides a couple of useful exceptions, ``Http404`` and
``PermissionDenied``, which you may want to use in your application. However,
it's only possible to customise the 404 site-wide (either by providing a
``404.html`` template, or by setting ``handler404`` in your urlconf), and
until Django 1.4 comes out, PermissionDenied will always result in a very
spartan error page.
``django-conneg`` provides an ``ErrorCatchingView`` which you can use as a
mixin to customise the rendering of responses for these error situations::
from django_conneg.views import HTMLView, ErrorCatchingView
class IndexView(HTMLView, ErrorCatchingView):
# ...
You can then customise error responses in one of the following ways:
* overriding the ``conneg/(forbidden|not_found|not_acceptable).(html|txt) templates
* overriding ``error_403``, ``error_404`` or ``error_406`` methods on the view
* overriding the ``error_template_names`` attribute to specify a non-standard template name:
In the latter case, you can do something like::
import httplib
from django.util.datastructures import MergeDict
from django_conneg.views import HTMLView, ErrorCatchingView
class IndexView(HTMLView, ErrorCatchingView):
# Provide a view-specific 404 page. Use MergeDict to use django_conneg's
# defaults for other types of errors.
error_template_names = MergeDict({httplib.NOT_FOUND: 'foo/404'},
ErrorCatchingView.error_template_names)
# ...
Running the tests
-----------------
``django-conneg`` has a modest test suite. To run it, head to the root of the
repository and run::
django-admin test --settings=django_conneg.test_settings --pythonpath=.
If you don't have Django, you'll need to install it as detailed in the
Prerequisites section above.
django-conneg-0.9.4/debian/ 0000775 0000000 0000000 00000000000 12245741361 0015464 5 ustar 00root root 0000000 0000000 django-conneg-0.9.4/debian/README 0000664 0000000 0000000 00000000341 12245741361 0016342 0 ustar 00root root 0000000 0000000 The Debian Package python-django-conneg
----------------------------
Native source package from .
-- Alexander Dutton Mon, 19 Dec 2011 14:14:33 +0000
django-conneg-0.9.4/debian/changelog 0000664 0000000 0000000 00000006702 12245741361 0017343 0 ustar 00root root 0000000 0000000 python-django-conneg (0.9.4) unstable; urgency=low
* Python 3 support
* format can now be passed from urlconf param
* BasicAuthMiddleware improvements
-- Alexander Dutton Thu, 28 Nov 2013 22:23:19 +0000
python-django-conneg (0.9.3) unstable; urgency=low
* URL in setup.py now points at the right repository.
-- Alexander Dutton Fri, 23 Nov 2012 09:10:41 +0000
python-django-conneg (0.9.2) unstable; urgency=low
* Fixed test stage of Debian package (new settings path)
* Backported to mock < 0.7 (as is on squeeze)
-- Alexander Dutton Fri, 16 Nov 2012 15:32:47 +0000
python-django-conneg (0.9.1) unstable; urgency=low
* BasicAuthMiddleware now returns 401 if supplied with invalid credentials,
and will return 403 for inactive users.
* Tests for BasicAuthMiddleware
-- Alexander Dutton Fri, 16 Nov 2012 15:12:45 +0000
python-django-conneg (0.9) unstable; urgency=low
* More generic handling of HTTP-based exceptions
* No longer catches and re-raises Exception
* Datetimes coerced to UTC before JSON serialization
-- Alexander Dutton Fri, 9 Nov 2012 14:16:15 +0000
python-django-conneg (0.8) unstable; urgency=low
* Tidied some methods into better places and added deprecation warnings.
* Added test param to renderer decorator to give an indication of whether a
particular renderer will succeed.
* renderer decorator now creates callable Renderer objects
* Lists of renderers can now be combined
* Some conneg stuff factored out from views into new conneg.Conneg class
* ErrorCatchingView now default behaviour; provides default templates for
403 and 404 errors in HTML, JSON and plain text
* Undocumented attributes view._renderers{,_by_mimetype,_by_format} now
available as view.conneg.renderers[…]
* renderer details now placed in context (useful for links to other formats
when creating templates)
-- Alexander Dutton Fri, 28 Sep 2012 13:08:01 +0100
python-django-conneg (0.7.4) unstable; urgency=low
* Fixed the raise statement in error_500 so that the original traceback is
preserved.
* Allow multiple fallback formats.
* Added support for HEAD requests.
-- Alexander Dutton Fri, 4 May 2012 14:08:39 +0100
python-django-conneg (0.7.3) unstable; urgency=low
* Template names in ErrorCatchingView are now configurable.
-- Alexander Dutton Tue, 6 Mar 2012 14:52:12 +0000
python-django-conneg (0.7.2) unstable; urgency=low
* Exception messages are now passed through to the template by
ErrorCatchingView and displayed appropriately.
-- Alexander Dutton Tue, 6 Mar 2012 13:56:44 +0000
python-django-conneg (0.7.1) unstable; urgency=low
* Added ErrorCatchingView for providing content-negotiated responses to
common error scenarios.
* JSONView now indents two spaces by default.
-- Alexander Dutton Tue, 6 Mar 2012 11:44:21 +0000
python-django-conneg (0.6) unstable; urgency=low
* Fixed renderer priority handling
-- Alexander Dutton Thu, 26 Jan 2012 09:31:38 +0000
python-django-conneg (0.5) unstable; urgency=low
* Initial Release.
-- Alexander Dutton Mon, 19 Dec 2011 14:14:33 +0000
django-conneg-0.9.4/debian/compat 0000664 0000000 0000000 00000000002 12245741361 0016662 0 ustar 00root root 0000000 0000000 7
django-conneg-0.9.4/debian/control 0000664 0000000 0000000 00000001403 12245741361 0017065 0 ustar 00root root 0000000 0000000 Source: python-django-conneg
Section: python
Priority: extra
Maintainer: Alexander Dutton
Build-Depends: debhelper (>= 7.0.50~),
python-all,
python-support,
python-mock,
python-django (>= 1.3)
Standards-Version: 3.9.1
X-Python-Version: >= 2.6
Homepage: https://github.com/ox-it/django-conneg
Vcs-Git: git://github.com/ox-it/django-conneg.git
Package: python-django-conneg
Section: python
Architecture: all
Depends: ${misc:Depends},
${python:Depends},
python-django (>= 1.3)
Description: Class-based views for returning content-negotiated responses
django-conneg provides a simple and extensible framework for producing
views that content-negotiate in Django.
django-conneg-0.9.4/debian/copyright 0000664 0000000 0000000 00000000471 12245741361 0017421 0 ustar 00root root 0000000 0000000 Format-Specification: http://anonscm.debian.org/viewvc/dep/web/deps/dep5/copyright-format.xml?revision=248
Upstream-Contact: IT Services, University of Oxford
Source: https://github.com/ox-it/django-conneg
Files: *
Copyright: 2012, University of Oxford
License: BSD
django-conneg-0.9.4/debian/docs 0000664 0000000 0000000 00000000013 12245741361 0016331 0 ustar 00root root 0000000 0000000 README.rst
django-conneg-0.9.4/debian/pyversions 0000664 0000000 0000000 00000000005 12245741361 0017623 0 ustar 00root root 0000000 0000000 2.6-
django-conneg-0.9.4/debian/rules 0000775 0000000 0000000 00000000274 12245741361 0016547 0 ustar 00root root 0000000 0000000 #!/usr/bin/make -f
%:
dh $@ --with python2
override_dh_auto_test:
django-admin test --settings=django_conneg.test_settings --pythonpath=.
overridge_dh_clean:
dh_clean
rm -rf build
django-conneg-0.9.4/debian/source/ 0000775 0000000 0000000 00000000000 12245741361 0016764 5 ustar 00root root 0000000 0000000 django-conneg-0.9.4/debian/source/format 0000664 0000000 0000000 00000000015 12245741361 0020173 0 ustar 00root root 0000000 0000000 3.0 (native)
django-conneg-0.9.4/django_conneg/ 0000775 0000000 0000000 00000000000 12245741361 0017035 5 ustar 00root root 0000000 0000000 django-conneg-0.9.4/django_conneg/__init__.py 0000664 0000000 0000000 00000000026 12245741361 0021144 0 ustar 00root root 0000000 0000000 __version__ = '0.9.4'
django-conneg-0.9.4/django_conneg/conneg.py 0000664 0000000 0000000 00000012031 12245741361 0020655 0 ustar 00root root 0000000 0000000 from collections import defaultdict
import inspect
import weakref
from django_conneg.http import MediaType
class Renderer(object):
def __init__(self, func, format, mimetypes=(), priority=0, name=None, test=None, instance=None, owner=None):
self.func = func
self.test = test or (lambda s,r,c,t: True)
if instance:
self.func = func.__get__(instance, owner)
self.test = test.__get__(instance, owner)
self.is_renderer = True
self.format = format
self.mimetypes = set(MediaType(mimetype, priority) for mimetype in mimetypes)
self.name = name
self.priority = priority
self.is_bound = instance is not None
def __get__(self, instance, owner=None):
return Renderer(self.func, self.format, self.mimetypes, self.priority, self.name, self.test, instance, owner)
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
@property
def __name__(self):
return self.func.__name__
@__name__.setter
def __name__(self, name):
self.func.__name__ = name
def __repr__(self):
if self.is_bound:
return "".format(self.func.im_class.__name__ or '?',
self.func.__name__,
self.func.__self__)
else:
return "".format(self.func.__name__)
class Conneg(object):
_memo_by_class = weakref.WeakKeyDictionary()
def __init__(self, renderers=None, obj=None):
self.renderers_by_format = defaultdict(list)
self.renderers_by_mimetype = defaultdict(list)
if renderers is not None:
renderers = list(renderers)
elif obj:
cls = type(obj) if not isinstance(obj, type) else obj
renderers = self._memo_by_class.get(cls)
if renderers is None:
# This is about as much memoization as we can do. We keep
# the renderers unbound for now
renderers = []
for name in dir(cls):
try:
value = getattr(obj, name)
except AttributeError:
continue
if isinstance(value, Renderer):
renderers.append(value)
self._memo_by_class[cls] = renderers
# Bind the renderers to this instance. See
# http://stackoverflow.com/a/1015405/613023 for an explanation.
renderers = [r.__get__(obj, cls) for r in renderers]
for renderer in renderers:
if renderer.mimetypes is not None:
for mimetype in renderer.mimetypes:
self.renderers_by_mimetype[mimetype].append(renderer)
self.renderers_by_format[renderer.format].append(renderer)
# Order all the renderers by priority
renderers.sort(key=lambda renderer:-renderer.priority)
self.renderers = tuple(renderers)
def get_renderers(self, request, context=None, template_name=None,
accept_header=None, formats=None, default_format=None, fallback_formats=None,
early=False):
"""
Returns a list of renderer functions in the order they should be tried.
Tries the format override parameter first, then the Accept header. If
neither is present, attempt to fall back to self._default_format. If
a fallback format has been specified, we try that last.
If early is true, don't test renderers to see whether they can handle
a serialization. This is useful if we're trying to find all relevant
serializers before we've built a context which they will accept.
"""
if formats:
renderers, seen_formats = [], set()
for format in formats:
if format in self.renderers_by_format and format not in seen_formats:
renderers.extend(self.renderers_by_format[format])
seen_formats.add(format)
elif accept_header:
accepts = MediaType.parse_accept_header(accept_header)
renderers = MediaType.resolve(accepts, self.renderers)
elif default_format:
renderers = self.renderers_by_format[default_format]
else:
renderers = []
fallback_formats = fallback_formats if isinstance(fallback_formats, (list, tuple)) else (fallback_formats,)
for format in fallback_formats:
for renderer in self.renderers_by_format[format]:
if renderer not in renderers:
renderers.append(renderer)
if not early and context is not None and template_name:
renderers = [r for r in renderers if r.test(request, context, template_name)]
return renderers
def __add__(self, other):
if not isinstance(other, Conneg):
other = Conneg(obj=other)
return Conneg(self.renderers + other.renderers) django-conneg-0.9.4/django_conneg/decorators.py 0000664 0000000 0000000 00000001451 12245741361 0021555 0 ustar 00root root 0000000 0000000 from django_conneg.conneg import Renderer
def renderer(format, mimetypes=(), priority=0, name=None, test=None):
"""
Decorates a view method to say that it renders a particular format and mimetypes.
Use as:
@renderer(format="foo")
def render_foo(self, request, context, template_name): ...
or
@renderer(format="foo", mimetypes=("application/x-foo",))
def render_foo(self, request, context, template_name): ...
The former case will inherit mimetypes from the previous renderer for that
format in the MRO. Where there isn't one, it will default to the empty
tuple.
Takes an optional priority argument to resolve ties between renderers.
"""
def g(f):
return Renderer(f, format, mimetypes, priority, name, test)
return g
django-conneg-0.9.4/django_conneg/http.py 0000664 0000000 0000000 00000011015 12245741361 0020364 0 ustar 00root root 0000000 0000000 from __future__ import unicode_literals
import re
from django.http import HttpResponseRedirect
class HttpResponseSeeOther(HttpResponseRedirect):
status_code = 303
class HttpResponseTemporaryRedirect(HttpResponseRedirect):
status_code = 307
class HttpError(Exception):
def __init__(self, status_code=None, message=None):
if status_code:
self.status_code = status_code
super(HttpError, self).__init__(message)
class HttpNotAcceptable(HttpError):
status_code = 406
def __init__(self, tried_mimetypes):
self.tried_mimetypes = tried_mimetypes
class HttpBadRequest(HttpError):
status_code = 400
class MediaType(object):
"""
Represents a parsed internet media type.
"""
_MEDIA_TYPE_RE = re.compile(r'(\*/\*)|(?P[^/]+)/(\*|((?P[^+]+)\+)?(?P.+))')
def __init__(self, value, priority=0):
value = str(value).strip()
media_type = value.split(';')
media_type, params = media_type[0].strip(), dict((i.strip() for i in p.split('=', 1)) for p in media_type[1:] if '=' in p)
mt = self._MEDIA_TYPE_RE.match(media_type)
if not mt:
raise ValueError("Not a correctly formatted internet media type (%r)" % media_type)
mt = mt.groupdict()
try:
self.quality = float(params.pop('q', 1))
except ValueError:
self.quality = 1
self.type = mt.get('type'), mt.get('subtype'), mt.get('subsubtype')
self.specifity = len([t for t in self.type if t])
self.params = params
self.value = value
self.priority = priority
def __str__(self):
return self.value
def __gt__(self, other):
if self.quality != other.quality:
return self.quality > other.quality
if self.specifity != other.specifity:
return self.specifity > other.specifity
for key in other.params:
if self.params.get(key) != other.params[key]:
return False
return len(self.params) > len(other.params)
def __lt__(self, other):
return other > self
def __eq__(self, other):
return self.quality == other.quality and self.type == other.type and self.params == other.params
def __hash__(self):
return hash(hash(self.quality) + hash(self.type) + hash(tuple(sorted(self.params.items()))))
def __ne__(self, other):
return not self.__eq__(other)
def equivalent(self, other):
"""
Returns whether two MediaTypes have the same overall specifity.
"""
return not (self > other or self < other)
def __cmp__(self, other):
if self > other:
return 1
elif self < other:
return -1
else:
return 0
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.value)
def provides(self, imt):
"""
Returns True iff the self is at least as specific as other.
Examples:
application/xhtml+xml provides application/xml, application/*, */*
text/html provides text/*, but not application/xhtml+xml or application/html
"""
return self.type[:imt.specifity] == imt.type[:imt.specifity]
@classmethod
def resolve(cls, accept, available_renderers):
"""
Resolves a list of accepted MediaTypes and available renderers to the preferred renderer.
Call as MediaType.resolve([MediaType], [renderer]).
"""
assert isinstance(available_renderers, tuple)
accept = sorted(accept)
renderers, seen = [], set()
accept_groups = [[accept.pop()]]
for imt in accept:
if imt.equivalent(accept_groups[-1][0]):
accept_groups[-1].append(imt)
else:
accept_groups.append([imt])
for accept_group in accept_groups:
for renderer in available_renderers:
if renderer in seen:
continue
for mimetype in renderer.mimetypes:
for imt in accept_group:
if mimetype.provides(imt):
renderers.append(renderer)
seen.add(renderer)
break
return renderers
@classmethod
def parse_accept_header(cls, accept):
media_types = []
for media_type in accept.split(','):
try:
media_types.append(MediaType(media_type))
except ValueError:
pass
return media_types
django-conneg-0.9.4/django_conneg/models.py 0000664 0000000 0000000 00000000000 12245741361 0020660 0 ustar 00root root 0000000 0000000 django-conneg-0.9.4/django_conneg/support/ 0000775 0000000 0000000 00000000000 12245741361 0020551 5 ustar 00root root 0000000 0000000 django-conneg-0.9.4/django_conneg/support/__init__.py 0000664 0000000 0000000 00000000001 12245741361 0022651 0 ustar 00root root 0000000 0000000
django-conneg-0.9.4/django_conneg/support/middleware.py 0000664 0000000 0000000 00000012253 12245741361 0023243 0 ustar 00root root 0000000 0000000 import base64
try: # Python 3
from http.client import UNAUTHORIZED, FORBIDDEN, FOUND
import urllib.parse as urllib_parse
except ImportError: # Python 2.x
from httplib import UNAUTHORIZED, FORBIDDEN, FOUND
import urlparse as urllib_parse
from django.conf import settings
from django.contrib.auth import authenticate
from django_conneg.http import MediaType
from django_conneg.views import HTMLView, JSONPView, TextView
class UnauthorizedView(HTMLView, JSONPView, TextView):
_force_fallback_format = 'txt'
template_name = 'conneg/unauthorized'
def get(self, request):
self.context.update({'status_code': UNAUTHORIZED,
'error': 'You need to be authenticated to perform this request.'})
return self.render()
post = put = delete = get
class InactiveUserView(HTMLView, JSONPView, TextView):
_force_fallback_format = 'txt'
template_name = 'conneg/inactive_user'
def get(self, request):
self.context.update({'status_code': FORBIDDEN,
'error': 'Your account is inactive.'})
return self.render()
post = put = delete = get
class BasicAuthMiddleware(object):
"""
Sets request.user if there are valid basic auth credentials on the
request, and turns @login_required redirects into 401 responses for
non-HTML responses.
"""
allow_http = getattr(settings, 'BASIC_AUTH_ALLOW_HTTP', False) or settings.DEBUG
unauthorized_view = staticmethod(UnauthorizedView.as_view())
inactive_user_view = staticmethod(InactiveUserView.as_view())
def process_request(self, request):
# Ignore if user already authenticated
if request.user.is_authenticated():
return
# Don't do anything for unsecure requests, unless DEBUG is on
if not self.allow_http and not request.is_secure():
return
# Parse the username and password out of the Authorization
# HTTP header and set request.user if we find an active user.
# We don't use auth.login, as the authorization is only valid
# for this one request.
authorization = request.META.get('HTTP_AUTHORIZATION')
if not authorization or not authorization.startswith('Basic '):
return
try:
credentials = base64.b64decode(authorization[6:].encode('utf-8')).decode('utf-8').split(':', 1)
except TypeError:
return
if len(credentials) != 2:
return
user = authenticate(username=credentials[0], password=credentials[1])
if user and user.is_active:
request.user = user
elif user and not user.is_active:
return self.inactive_user_view(request)
else:
return self.unauthorized_view(request)
def process_response(self, request, response):
"""
Adds WWW-Authenticate: Basic headers to 401 responses, and rewrites
redirects the login page to be 401 responses if it's a non-browser
agent.
"""
process = False
# Don't do anything for unsecure requests, unless DEBUG is on
if not self.allow_http and not request.is_secure():
return response
if response.status_code == UNAUTHORIZED:
pass
elif response.status_code == FOUND:
location = urllib_parse.urlparse(response['Location'])
if location.path != settings.LOGIN_URL:
# If it wasn't a redirect to the login page, we don't touch it.
return response
elif not self.is_agent_a_robot(request):
# We don't touch requests made in order to be shown to humans.
return response
realm = getattr(settings, 'BASIC_AUTH_REALM', request.META.get('HTTP_HOST', 'restricted'))
if response.status_code == FOUND:
response = self.unauthorized_view(request)
authenticate = response.get('WWW-Authenticate', None)
if authenticate:
authenticate = 'Basic realm="%s", %s' % (realm, authenticate)
else:
authenticate = 'Basic realm="%s"' % realm
response['WWW-Authenticate'] = authenticate
return response
def is_agent_a_robot(self, request):
if request.META.get('HTTP_ORIGIN'):
# A CORS request (from JavaScript)
return True
if request.META.get('HTTP_X_REQUESTED_WITH'):
# An AJAX request (from JavaScript)
return True
accept = sorted(MediaType.parse_accept_header(request.META.get('HTTP_ACCEPT', '')), reverse=True)
if accept and accept[0].type in (('text', 'html', None), ('application', 'xml', 'xhtml')):
# Agents whose first preference is for HTML are presumably trying
# to show it to a human.
return False
if 'MSIE' in request.META.get('HTTP_USER_AGENT', ''):
# We'll assume that IE (which doesn't set a proper Accept header)
# is making a request on behalf of a human. It does seem to set an
# Origin header when using XDomainRequest, and can set
# X-Requested-With if the request is being made from JavaScript.
return False
return True
django-conneg-0.9.4/django_conneg/templates/ 0000775 0000000 0000000 00000000000 12245741361 0021033 5 ustar 00root root 0000000 0000000 django-conneg-0.9.4/django_conneg/templates/conneg/ 0000775 0000000 0000000 00000000000 12245741361 0022304 5 ustar 00root root 0000000 0000000 django-conneg-0.9.4/django_conneg/templates/conneg/base.html 0000664 0000000 0000000 00000000031 12245741361 0024076 0 ustar 00root root 0000000 0000000 {% extends "base.html" %} django-conneg-0.9.4/django_conneg/templates/conneg/error.html 0000664 0000000 0000000 00000000431 12245741361 0024321 0 ustar 00root root 0000000 0000000 {% extends "conneg/base.html" %}
{% block title %}Forbidden{% endblock %}
{% block content %}
{{ status_code }} {{ status_message }}
There was an error dealing with your request.
{% if error.message %}{{ error.message }}
{% endif %}
{% endblock %} django-conneg-0.9.4/django_conneg/templates/conneg/error.txt 0000664 0000000 0000000 00000000215 12245741361 0024174 0 ustar 00root root 0000000 0000000 {{ status_code }} {{ status_message }}
There was an error dealing with your request.
{% if error.message %}{{ error.message }}{% endif %} django-conneg-0.9.4/django_conneg/templates/conneg/forbidden.html 0000664 0000000 0000000 00000000364 12245741361 0025131 0 ustar 00root root 0000000 0000000 {% extends "conneg/base.html" %}
{% block title %}Forbidden{% endblock %}
{% block content %}
Forbidden
Access to this resource is forbidden.
{% if error.message %}{{ error.message }}
{% endif %}
{% endblock %} django-conneg-0.9.4/django_conneg/templates/conneg/forbidden.txt 0000664 0000000 0000000 00000000134 12245741361 0024777 0 ustar 00root root 0000000 0000000 Access to this resource is forbidden.{% if error.message %}
{{ error.message }}{% endif %}
django-conneg-0.9.4/django_conneg/templates/conneg/inactive_user.html 0000664 0000000 0000000 00000000404 12245741361 0026030 0 ustar 00root root 0000000 0000000 {% extends "conneg/base.html" %}
{% block title %}Forbidden{% endblock %}
{% block content %}
Forbidden (inactive user)
Access to this resource is forbidden.
{% if error.message %}{{ error.message }}
{% endif %}
{% endblock %} django-conneg-0.9.4/django_conneg/templates/conneg/inactive_user.txt 0000664 0000000 0000000 00000000134 12245741361 0025703 0 ustar 00root root 0000000 0000000 Access to this resource is forbidden.{% if error.message %}
{{ error.message }}{% endif %}
django-conneg-0.9.4/django_conneg/templates/conneg/not_acceptable.html 0000664 0000000 0000000 00000002636 12245741361 0026144 0 ustar 00root root 0000000 0000000 {% extends "conneg/base.html" %}
{% block title %}Not Acceptable{% endblock %}
{% block content %}
Not Acceptable
Of the media types specified in your request, none is supported as a response type. This resource supports the following response formats:
Format name |
Format identifier |
Associated mimetypes |
{% for renderer in error.available_renderers %}
{{ renderer.name }} |
{{ renderer.format }} |
{% for mimetype in renderer.mimetypes %}{{ mimetype }}{% if not forloop.last %}, {% endif %}{% endfor%} |
{% endfor %}
You can specify a media type using the Accept header, or by providing a {{ error.format_parameter_name }} parameter in the query string or request body, containing a single — or comma-separated list of — format identifiers.
For this request, you provided the following Accept header:
{{ error.accept_header }}
The site interpreted this to mean that we should attempt to return serializations in the following order:
{% for mediatype in error.accept_header_parsed %}
- {{ mediatype }}
{% endfor %}
{% endblock %} django-conneg-0.9.4/django_conneg/templates/conneg/not_acceptable.txt 0000664 0000000 0000000 00000001762 12245741361 0026016 0 ustar 00root root 0000000 0000000 Not Acceptable
Of the media types specified in your request, none is supported as a response
type. This resource supports the following response formats:
Format name | Format identifier | Associated mimetypes
-------------+--------------------+-------------------------------------------{% for renderer in error.available_renderers %}
{{ renderer.name|ljust:12 }} | {{ renderer.format|ljust:18 }} | {{ renderer.mimetypes|join:", " }}{% endfor %}
You can specify a media type using the Accept header, or by providing a
{{ error.format_parameter_name }} parameter in the query string or request body, containing a single - or
comma-separated list of - format identifiers.
For this request, you provided the following Accept header:
{{ error.accept_header }}
The site interpreted this to mean that we should attempt to return
serializations in the following order:
{% for mediatype in error.accept_header_parsed %}
{{ forloop.counter|rjust:2 }}. {{ mediatype }}{% endfor %}
django-conneg-0.9.4/django_conneg/templates/conneg/not_found.html 0000664 0000000 0000000 00000000365 12245741361 0025171 0 ustar 00root root 0000000 0000000 {% extends "conneg/base.html" %}
{% block title %}Not Found{% endblock %}
{% block content %}
Not Found
The requested resource does not exist.
{% if error.message %}{{ error.message }}
{% endif %}
{% endblock %} django-conneg-0.9.4/django_conneg/templates/conneg/not_found.txt 0000664 0000000 0000000 00000000135 12245741361 0025037 0 ustar 00root root 0000000 0000000 The requested resource does not exist.{% if error.message %}
{{ error.message }}{% endif %}
django-conneg-0.9.4/django_conneg/templates/conneg/service_unavailable.html 0000664 0000000 0000000 00000000461 12245741361 0027176 0 ustar 00root root 0000000 0000000 {% extends "conneg/base.html" %}
{% block title %}Service Unavailable{% endblock %}
{% block content %}
Service Unavailable
The requested resource is not available at the moment. Please try again later.
{% if error.message %}{{ error.message }}
{% endif %}
{% endblock %} django-conneg-0.9.4/django_conneg/templates/conneg/service_unavailable.txt 0000664 0000000 0000000 00000000205 12245741361 0027045 0 ustar 00root root 0000000 0000000 The requested resource is not available at the moment. Please try again later.{% if error.message %}
{{ error.message }}{% endif %}
django-conneg-0.9.4/django_conneg/templates/conneg/unauthorized.html 0000664 0000000 0000000 00000000456 12245741361 0025720 0 ustar 00root root 0000000 0000000 {% extends "conneg/base.html" %}
{% block title %}Unauthorized{% endblock %}
{% block content %}
Unauthorized
You need to be authenticated to perform this request.
See the WWW-Authenticate header for more details about supported
authentication schemes.
{% endblock %} django-conneg-0.9.4/django_conneg/templates/conneg/unauthorized.txt 0000664 0000000 0000000 00000000217 12245741361 0025566 0 ustar 00root root 0000000 0000000 You need to be authenticated to perform this request.
See the WWW-Authenticate header for more details about supported
authentication schemes. django-conneg-0.9.4/django_conneg/test_settings.py 0000664 0000000 0000000 00000001206 12245741361 0022305 0 ustar 00root root 0000000 0000000 import imp
INSTALLED_APPS = (
'django_conneg',
'django_conneg.tests',
)
SECRET_KEY = 'test secret key'
# Use django_jenkins if it's installed.
try:
imp.find_module('django_jenkins')
except ImportError:
pass
else:
INSTALLED_APPS += ('django_jenkins',)
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3'}}
LOGIN_URL = '/login/'
ROOT_URLCONF = 'django_conneg.tests.urls'
BASIC_AUTH_ALLOW_HTTP = True
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django_conneg.support.middleware.BasicAuthMiddleware',
)
django-conneg-0.9.4/django_conneg/tests/ 0000775 0000000 0000000 00000000000 12245741361 0020177 5 ustar 00root root 0000000 0000000 django-conneg-0.9.4/django_conneg/tests/__init__.py 0000664 0000000 0000000 00000000076 12245741361 0022313 0 ustar 00root root 0000000 0000000 from .basic_auth_middleware import *
from .priorities import * django-conneg-0.9.4/django_conneg/tests/basic_auth_middleware.py 0000664 0000000 0000000 00000006705 12245741361 0025060 0 ustar 00root root 0000000 0000000 import base64
try:
from http.client import OK, FORBIDDEN, FOUND, UNAUTHORIZED
except ImportError:
from httplib import OK, FORBIDDEN, FOUND, UNAUTHORIZED
from django.contrib.auth.models import User
from django.test import TestCase
from django.test.utils import override_settings
import mock
test_username = 'username'
test_password = 'password'
def mocked_authenticate(username, password):
if username == test_username and password == test_password:
return User(username='active')
def mocked_authenticate_inactive(username, password):
if username == test_username and password == test_password:
return User(username='inactive', is_active=False)
def basic_auth(username, password):
return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode(':'.join([username, password]).encode('utf-8')).decode('utf-8')}
@override_settings(LOGIN_URL='/login/',
MIDDLEWARE_CLASSES=('django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django_conneg.support.middleware.BasicAuthMiddleware'),
BASIC_AUTH_ALLOW_HTTP = True)
class BasicAuthTestCase(TestCase):
urls = 'django_conneg.tests.urls'
def testOptionalWithout(self):
response = self.client.get('/optional-auth/')
self.assertEqual(response.status_code, OK)
self.assertFalse(response.is_authenticated)
@mock.patch('django_conneg.support.middleware.authenticate', mocked_authenticate)
def testOptionalWithCorrect(self):
response = self.client.get('/optional-auth/',
**basic_auth(test_username, test_password))
self.assertEqual(response.status_code, OK)
self.assertTrue(response.is_authenticated)
@mock.patch('django_conneg.support.middleware.authenticate', mocked_authenticate)
def testOptionalWithIncorrect(self):
response = self.client.get('/optional-auth/',
**basic_auth(test_username, 'not-the-password'))
self.assertEqual(response.status_code, UNAUTHORIZED)
self.assertEqual(response['WWW-Authenticate'], 'Basic realm="restricted"')
def testRequiredWithoutHTML(self):
response = self.client.get('/login-required/', HTTP_ACCEPT='text/html')
self.assertEqual(response.status_code, FOUND)
self.assertEqual(response['Location'],
'http://testserver/login/?next=/login-required/')
def testRequiredWithoutJSON(self):
response = self.client.get('/login-required/', HTTP_ACCEPT='application/json')
self.assertEqual(response.status_code, UNAUTHORIZED)
self.assertEqual(response['WWW-Authenticate'], 'Basic realm="restricted"')
@mock.patch('django_conneg.support.middleware.authenticate', mocked_authenticate)
def testRequiredWithCorrect(self):
response = self.client.get('/login-required/',
**basic_auth(test_username, test_password))
self.assertEqual(response.status_code, OK)
self.assertTrue(response.is_authenticated)
@mock.patch('django_conneg.support.middleware.authenticate', mocked_authenticate_inactive)
def testRequiredWithCorrectInactive(self):
response = self.client.get('/login-required/',
**basic_auth(test_username, test_password))
self.assertEqual(response.status_code, FORBIDDEN)
django-conneg-0.9.4/django_conneg/tests/priorities.py 0000664 0000000 0000000 00000005401 12245741361 0022742 0 ustar 00root root 0000000 0000000 import itertools
import unittest
from django.http import HttpResponse
from django_conneg import http, views, decorators
class PriorityTestCase(unittest.TestCase):
mimetypes = ('text/plain', 'application/xml', 'text/html', 'application/json')
def getRenderer(self, format, mimetypes, name, priority):
if not isinstance(mimetypes, tuple):
mimetypes = (mimetypes,)
def renderer(request, context, template_name):
return HttpResponse('', mimetype=mimetypes[0])
renderer.__name__ = 'render_%s' % mimetypes[0].replace('/', '_')
renderer = decorators.renderer(format=format,
mimetypes=mimetypes,
priority=priority)(renderer)
return renderer
def getTestView(self, priorities):
members = {}
for i, (mimetype, priority) in enumerate(priorities.items()):
members['render_%d' % i] = self.getRenderer(str(i), mimetype, str(i), priority)
TestView = type('TestView',
(views.ContentNegotiatedView,),
members)
return TestView
def testEqualQuality(self):
accept_header = ', '.join(self.mimetypes)
accept = http.MediaType.parse_accept_header(accept_header)
for mimetypes in itertools.permutations(self.mimetypes):
renderers = tuple(self.getRenderer(str(i), mimetype, str(i), -i) for i, mimetype in enumerate(mimetypes))
renderers = http.MediaType.resolve(accept, renderers)
for renderer, mimetype in zip(renderers, mimetypes):
self.assertEqual(next(iter(renderer.mimetypes)), http.MediaType(mimetype))
def testEqualQualityView(self):
accept_header = ', '.join(self.mimetypes)
accept = http.MediaType.parse_accept_header(accept_header)
for mimetypes in itertools.permutations(self.mimetypes):
priorities = dict((mimetype, -i) for i, mimetype in enumerate(mimetypes))
test_view = self.getTestView(priorities).as_view()
renderers = http.MediaType.resolve(accept, test_view.conneg.renderers)
for renderer, mimetype in zip(renderers, mimetypes):
self.assertEqual(next(iter(renderer.mimetypes)), http.MediaType(mimetype))
def testPrioritySorting(self):
for mimetypes in itertools.permutations(self.mimetypes):
priorities = dict((mimetype, -i) for i, mimetype in enumerate(mimetypes))
test_view = self.getTestView(priorities).as_view()
renderer_priorities = [renderer.priority for renderer in test_view.conneg.renderers]
self.assertEqual(renderer_priorities, sorted(renderer_priorities, reverse=True))
if __name__ == '__main__':
unittest.main()
django-conneg-0.9.4/django_conneg/tests/templates/ 0000775 0000000 0000000 00000000000 12245741361 0022175 5 ustar 00root root 0000000 0000000 django-conneg-0.9.4/django_conneg/tests/templates/base.html 0000664 0000000 0000000 00000000271 12245741361 0023775 0 ustar 00root root 0000000 0000000
{% block title %}{% endblock %}
{% block content %}{% endblock %}
django-conneg-0.9.4/django_conneg/tests/urls.py 0000664 0000000 0000000 00000001724 12245741361 0021542 0 ustar 00root root 0000000 0000000 from django.conf.urls.defaults import patterns, url
from django.conf import settings
from django.http import HttpResponse, HttpResponseRedirect
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
from django_conneg.views import HTMLView, JSONView
class OptionalAuthView(HTMLView, JSONView):
_force_fallback_format = ('html', 'json')
def get(self, request):
response = self.render()
response.is_authenticated = request.user.is_authenticated()
return response
class LoginRequiredView(HTMLView, JSONView):
_force_fallback_format = ('html', 'json')
@method_decorator(login_required)
def get(self, request):
response = self.render()
response.is_authenticated = request.user.is_authenticated()
return response
urlpatterns = patterns('',
url(r'^optional-auth/$', OptionalAuthView.as_view()),
url(r'^login-required/$', LoginRequiredView.as_view()),
) django-conneg-0.9.4/django_conneg/utils.py 0000664 0000000 0000000 00000000502 12245741361 0020544 0 ustar 00root root 0000000 0000000 try:
from pytz import utc
except ImportError:
import datetime
class _UTC(datetime.tzinfo):
def utcoffset(self, dt):
return datetime.timedelta(0)
def dst(self, dt):
return datetime.timedelta(0)
def tzname(self, dt):
return "UTC"
utc = _UTC() django-conneg-0.9.4/django_conneg/views.py 0000664 0000000 0000000 00000043537 12245741361 0020560 0 ustar 00root root 0000000 0000000 from __future__ import unicode_literals
import datetime
try: # Python < 3
import httplib as http_client
import urlparse as urllib_parse
from urllib import urlencode
str_types = (unicode, str)
except ImportError: # Python >= 3
import http.client as http_client
import urllib.parse as urllib_parse
from urllib.parse import urlencode
str_types = (str,)
unicode = str
import inspect
import itertools
import logging
import sys
import time
import urllib
import warnings
from django.core import exceptions
from django.views.generic import View
from django.utils.decorators import classonlymethod
from django import http
from django.template import RequestContext, TemplateDoesNotExist
from django.shortcuts import render_to_response
from django.utils.cache import patch_vary_headers
from django_conneg.conneg import Conneg
from django_conneg.decorators import renderer
from django_conneg.http import MediaType, HttpError, HttpNotAcceptable
from django_conneg.utils import utc
logger = logging.getLogger(__name__)
class BaseContentNegotiatedView(View):
conneg = None
context = None
_default_format = None
_force_fallback_format = None
_format_override_parameter = 'format'
_format_url_parameter = 'format'
template_name = None
@classonlymethod
def as_view(cls, **initkwargs):
view = super(BaseContentNegotiatedView, cls).as_view(**initkwargs)
view.conneg = Conneg(obj=cls)
return view
def dispatch(self, request, *args, **kwargs):
# This is handy for the view to work out what renderers will
# be attempted, and to manipulate the list if necessary.
# Also handy for middleware to check whether the view was a
# BaseContentNegotiatedView, and which renderers were preferred.
if self.context is None:
self.context = {'additional_headers': {}}
format_url_parameter = kwargs.pop(self._format_url_parameter, None)
if format_url_parameter:
self.format_override = [format_url_parameter]
elif request.REQUEST.get(self._format_override_parameter):
self.format_override = request.REQUEST[self._format_override_parameter].split(',')
else:
self.format_override = None
self.request = request
self.args = args
self.kwargs = kwargs
self.conneg = Conneg(obj=self)
self.set_renderers(request)
return super(BaseContentNegotiatedView, self).dispatch(request, *args, **kwargs)
def set_renderers(self, request=None, context=None, template_name=None, early=False):
"""
Makes sure that the renderers attribute on the request is up
to date. renderers_for_view keeps track of the view that
is attempting to render the request, so that if the request
has been delegated to another view we know to recalculate
the applicable renderers. When called multiple times on the
same view this will be very low-cost for subsequent calls.
"""
request, context, template_name = self.get_render_params(request, context, template_name)
args = (self.conneg, context, template_name,
self._default_format, self._force_fallback_format, self._format_override_parameter)
if getattr(request, 'renderers_for_args', None) != args:
fallback_formats = self._force_fallback_format or ()
if not isinstance(fallback_formats, (list, tuple)):
fallback_formats = (fallback_formats,)
request.renderers = self.conneg.get_renderers(request=request,
context=context,
template_name=template_name,
accept_header=request.META.get('HTTP_ACCEPT'),
formats=self.format_override,
default_format=self._default_format,
fallback_formats=fallback_formats,
early=early)
request.renderers_for_view = args
self.context['renderers'] = [self.renderer_for_context(request, r) for r in self.conneg.renderers]
return request.renderers
def get_render_params(self, request, context, template_name):
if not template_name:
template_name = self.template_name
if isinstance(template_name, str_types) and template_name.endswith('.html'):
template_name = template_name[:-5]
return request or self.request, context or self.context, template_name
def render(self, request=None, context=None, template_name=None):
"""
Returns a HttpResponse of the right media type as specified by the
request.
context can contain status_code and additional_headers members, to set
the HTTP status code and headers of the request, respectively.
template_name should lack a file-type suffix (e.g. '.html', as
renderers will append this as necessary.
"""
request, context, template_name = self.get_render_params(request, context, template_name)
self.set_renderers()
status_code = context.pop('status_code', http_client.OK)
additional_headers = context.pop('additional_headers', {})
for renderer in request.renderers:
response = renderer(request, context, template_name)
if response is NotImplemented:
continue
response.status_code = status_code
response.renderer = renderer
break
else:
tried_mimetypes = list(itertools.chain(*[r.mimetypes for r in request.renderers]))
response = self.http_not_acceptable(request, tried_mimetypes)
response.renderer = None
for key, value in additional_headers.items():
response[key] = value
# We're doing content-negotiation, so tell the user-agent that the
# response will vary depending on the accept header.
patch_vary_headers(response, ('Accept',))
return response
def http_not_acceptable(self, request, tried_mimetypes, *args, **kwargs):
response = http.HttpResponse("""\
Your Accept header didn't contain any supported media ranges.
Supported ranges are:
* %s\n""" % '\n * '.join(sorted('%s (%s; %s)' % (f.name, ", ".join(m.value for m in f.mimetypes), f.format) for f in request.renderers if not any(m in tried_mimetypes for m in f.mimetypes))), mimetype="text/plain")
response.status_code = http_client.NOT_ACCEPTABLE
return response
def head(self, request, *args, **kwargs):
handle_get = getattr(self, 'get', None)
if handle_get:
response = handle_get(request, *args, **kwargs)
response.content = ''
return response
else:
return self.http_method_not_allowed(request, *args, **kwargs)
def options(self, request, *args, **kwargs):
response = http.HttpResponse()
response['Accept'] = ','.join(m.upper() for m in sorted(self.http_method_names) if hasattr(self, m))
return response
@classmethod
def parse_accept_header(cls, accept):
warnings.warn("The parse_accept_header method has moved to django_conneg.http.MediaType")
return MediaType.parse_accept_header(accept)
def render_to_format(self, request=None, context=None, template_name=None, format=None):
request, context, template_name = self.get_render_params(request, context, template_name)
self.set_renderers()
status_code = context.pop('status_code', http_client.OK)
additional_headers = context.pop('additional_headers', {})
for renderer in self.conneg.renderers_by_format.get(format, ()):
response = renderer(request, context, template_name)
if response is not NotImplemented:
break
else:
response = self.http_not_acceptable(request, ())
renderer = None
response.status_code = status_code
response.renderer = renderer
for key, value in additional_headers.items():
response[key] = value
return response
def join_template_name(self, template_name, extension):
"""
Appends an extension to a template_name or list of template_names.
"""
if template_name is None:
return None
if isinstance(template_name, (list, tuple)):
return tuple('.'.join([n, extension]) for n in template_name)
if isinstance(template_name, str_types):
return '.'.join([template_name, extension])
raise AssertionError('template_name not of correct type: %r' % type(template_name))
def renderer_for_context(self, request, renderer):
return {'name': renderer.name,
'priority': renderer.priority,
'mimetypes': [m.value for m in renderer.mimetypes],
'format': renderer.format,
'url': self.url_for_format(request, renderer.format)}
def url_for_format(self, request, format):
qs = urllib_parse.parse_qs(request.META.get('QUERY_STRING', ''))
qs['format'] = [format]
return '?{0}'.format(urlencode(qs, True))
class ContentNegotiatedView(BaseContentNegotiatedView):
@property
def error_view(self):
if not hasattr(self, '_error_view'):
self._error_view = ErrorView.as_view()
return self._error_view
error_template_names = {http_client.NOT_FOUND: ('conneg/not_found', '404'),
http_client.FORBIDDEN: ('conneg/forbidden', '403'),
http_client.NOT_ACCEPTABLE: ('conneg/not_acceptable',),
http_client.BAD_REQUEST: ('conneg/bad_request', '400'),
http_client.SERVICE_UNAVAILABLE: ('conneg/service_unavailable', '503'),
'default': ('conneg/error',)}
def dispatch(self, request, *args, **kwargs):
try:
return super(ContentNegotiatedView, self).dispatch(request, *args, **kwargs)
except http.Http404 as e:
return self.error(request, e, args, kwargs, http_client.NOT_FOUND)
except exceptions.PermissionDenied as e:
return self.error(request, e, args, kwargs, http_client.FORBIDDEN)
except HttpError as e:
return self.error(request, e, args, kwargs, e.status_code)
def http_not_acceptable(self, request, tried_mimetypes, *args, **kwargs):
raise HttpNotAcceptable(tried_mimetypes)
def error(self, request, exception, args, kwargs, status_code):
method_name = 'error_%d' % status_code
method = getattr(self, method_name, None)
# See if we've got a dedicated handler for this status code
if callable(method):
return method(request, exception, *args, **kwargs)
# Otherwise, if it's an HttpError, try to render it to an
# appropriate template
context = {'error': {'status_code': status_code,
'status_message': http_client.responses.get(status_code)}}
if isinstance(exception, HttpError) and exception.args:
context['error']['message'] = exception.args[0]
template_names = self.error_template_names.get(status_code,
self.error_template_names['default'])
return self.error_view(request, context, template_names)
def error_406(self, request, exception, *args, **kwargs):
accept_header_parsed = MediaType.parse_accept_header(request.META.get('HTTP_ACCEPT', ''))
accept_header_parsed.sort(reverse=True)
accept_header_parsed = map(unicode, accept_header_parsed)
context = {'error': {'status_code': http_client.NOT_ACCEPTABLE,
'tried_mimetypes': exception.tried_mimetypes,
'available_renderers': [self.renderer_for_context(request, r) for r in self.conneg.renderers],
'format_parameter_name': self._format_override_parameter,
'format_parameter': request.REQUEST.get(self._format_override_parameter),
'format_parameter_parsed': request.REQUEST.get(self._format_override_parameter, '').split(','),
'accept_header': request.META.get('HTTP_ACCEPT'),
'accept_header_parsed': accept_header_parsed}}
return self.error_view(request, context,
self.error_template_names[http_client.NOT_ACCEPTABLE])
# For backwards compatibility
ErrorCatchingView = ContentNegotiatedView
class HTMLView(ContentNegotiatedView):
_default_format = 'html'
@renderer(format="html", mimetypes=('text/html', 'application/xhtml+xml'), priority=1, name='HTML')
def render_html(self, request, context, template_name):
template_name = self.join_template_name(template_name, 'html')
if template_name is None:
return NotImplemented
try:
return render_to_response(template_name,
context, context_instance=RequestContext(request),
mimetype='text/html')
except TemplateDoesNotExist:
return NotImplemented
class TextView(ContentNegotiatedView):
@renderer(format="txt", mimetypes=('text/plain',), priority=1, name='Plain text')
def render_text(self, request, context, template_name):
template_name = self.join_template_name(template_name, 'txt')
if template_name is None:
return NotImplemented
try:
return render_to_response(template_name,
context, context_instance=RequestContext(request),
mimetype='text/plain')
except TemplateDoesNotExist:
return NotImplemented
try:
import json
except ImportError:
try:
import simplejson as json
except ImportError:
pass
# Only define if json is available.
if 'json' in locals():
class JSONView(ContentNegotiatedView):
_json_indent = 2
def preprocess_context_for_json(self, context):
return context
def simplify_for_json(self, value):
if inspect.ismethod(getattr(value, 'simplify_for_json', None)):
return value.simplify_for_json(self.simplify_for_json)
if isinstance(value, datetime.datetime):
if value.tzinfo:
value = value.astimezone(utc)
return time.mktime(value.timetuple()) * 1000
if isinstance(value, (list, tuple)):
items = []
for item in value:
item = self.simplify_for_json(item)
if item is not NotImplemented:
items.append(item)
return items
if isinstance(value, dict):
items = {}
for key, item in value.items():
item = self.simplify_for_json(item)
if item is not NotImplemented:
items[unicode(key)] = item
return items
elif type(value) in (int, float, bool):
return value
elif type(value) in str_types:
return unicode(value)
elif value is None:
return value
else:
logger.warning("Failed to simplify object of type %r", type(value))
return NotImplemented
def simplify(self, value):
warnings.warn("JSONView.simplify() has been renamed to simplify_for_json")
return self.simplify_for_json(value)
@renderer(format='json', mimetypes=('application/json',), name='JSON')
def render_json(self, request, context, template_name):
context = self.preprocess_context_for_json(context)
return http.HttpResponse(json.dumps(self.simplify_for_json(context), indent=self._json_indent),
mimetype="application/json")
class JSONPView(JSONView):
# The query parameter to look for the callback name
_default_jsonp_callback_parameter = 'callback'
# The default callback name if none is provided
_default_jsonp_callback = 'callback'
# Overridden to return JSONP if there's a callback parameter
@renderer(format='json', mimetypes=('application/json',), name='JSON')
def render_json(self, request, context, template_name):
if self._default_jsonp_callback_parameter in request.GET:
return self.render_js(request, context, template_name)
else:
return super(JSONPView, self).render_json(request, context, template_name)
@renderer(format='js', mimetypes=('text/javascript', 'application/javascript'), name='JavaScript (JSONP)')
def render_js(self, request, context, template_name):
context = self.preprocess_context_for_json(context)
callback_name = request.GET.get(self._default_jsonp_callback_parameter,
self._default_jsonp_callback)
return http.HttpResponse('%s(%s);' % (callback_name, json.dumps(self.simplify_for_json(context), indent=self._json_indent)),
mimetype="application/javascript")
class ErrorView(HTMLView, JSONPView, TextView):
_force_fallback_format = ('html', 'json')
def get(self, request, context, template_name):
self.context.update(context)
self.template_name = template_name
self.context['error']['response'] = http_client.responses[context['error']['status_code']]
self.context['status_code'] = context['error']['status_code']
return self.render()
post = delete = put = get
django-conneg-0.9.4/requirements.txt 0000664 0000000 0000000 00000000015 12245741361 0017522 0 ustar 00root root 0000000 0000000 Django>=1.3
django-conneg-0.9.4/setup.py 0000664 0000000 0000000 00000005604 12245741361 0015761 0 ustar 00root root 0000000 0000000 from distutils.core import setup
from distutils.command.install import INSTALL_SCHEMES
import os
from django_conneg import __version__
#################################
# BEGIN borrowed from Django #
# licensed under the BSD #
# http://www.djangoproject.com/ #
#################################
def fullsplit(path, result=None):
"""
Split a pathname into components (the opposite of os.path.join) in a
platform-neutral way.
"""
if result is None:
result = []
head, tail = os.path.split(path)
if head == '':
return [tail] + result
if head == path:
return result
return fullsplit(head, [tail] + result)
# Tell distutils to put the data_files in platform-specific installation
# locations. See here for an explanation:
# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb
for scheme in INSTALL_SCHEMES.values():
scheme['data'] = scheme['purelib']
# Compile the list of packages available, because distutils doesn't have
# an easy way to do this.
packages, data_files = [], []
root_dir = os.path.dirname(__file__)
if root_dir != '':
os.chdir(root_dir)
for dirpath, dirnames, filenames in os.walk('django_conneg'):
# Ignore dirnames that start with '.'
for i, dirname in enumerate(dirnames):
if dirname.startswith('.'): del dirnames[i]
if '__init__.py' in filenames:
packages.append('.'.join(fullsplit(dirpath)))
elif filenames:
data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]])
#################################
# END borrowed from Django #
#################################
# Idea borrowed from http://cburgmer.posterous.com/pip-requirementstxt-and-setuppy
install_requires, dependency_links = [], []
for line in open('requirements.txt'):
line = line.strip()
if line.startswith('-e'):
dependency_links.append(line[2:].strip())
elif line:
install_requires.append(line)
setup(
name='django-conneg',
description="An implementation of content-negotiating class-based views for Django",
author='IT Services, University of Oxford',
author_email='infodev@it.ox.ac.uk',
version=__version__,
packages=packages,
license='BSD',
url='https://github.com/ox-it/django-conneg',
long_description=open('README.rst').read(),
classifiers=['Development Status :: 4 - Beta',
'Framework :: Django',
'License :: OSI Approved :: BSD License',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content'],
keywords=['REST', 'University of Oxford', 'content negotiation', 'Accept header', 'Django'],
data_files=data_files,
install_requires=install_requires,
dependency_links=dependency_links,
)
django-conneg-0.9.4/tox.ini 0000664 0000000 0000000 00000000176 12245741361 0015561 0 ustar 00root root 0000000 0000000 [tox]
[testenv]
deps =
Django
django-jenkins
commands=django-admin.py jenkins --settings=django_conneg.test_settings