pax_global_header00006660000000000000000000000064122457413610014517gustar00rootroot0000000000000052 comment=d782b82e476497aa98d68acf16b5b50ea167d21d django-conneg-0.9.4/000077500000000000000000000000001224574136100142425ustar00rootroot00000000000000django-conneg-0.9.4/.gitignore000066400000000000000000000000351224574136100162300ustar00rootroot00000000000000*.pyc *~ dist build MANIFEST django-conneg-0.9.4/MANIFEST.in000066400000000000000000000000551224574136100160000ustar00rootroot00000000000000include requirements.txt include README.rst django-conneg-0.9.4/README.rst000066400000000000000000000173371224574136100157440ustar00rootroot00000000000000Content-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/000077500000000000000000000000001224574136100154645ustar00rootroot00000000000000django-conneg-0.9.4/debian/README000066400000000000000000000003411224574136100163420ustar00rootroot00000000000000The 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/changelog000066400000000000000000000067021224574136100173430ustar00rootroot00000000000000python-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/compat000066400000000000000000000000021224574136100166620ustar00rootroot000000000000007 django-conneg-0.9.4/debian/control000066400000000000000000000014031224574136100170650ustar00rootroot00000000000000Source: 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/copyright000066400000000000000000000004711224574136100174210ustar00rootroot00000000000000Format-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/docs000066400000000000000000000000131224574136100163310ustar00rootroot00000000000000README.rst django-conneg-0.9.4/debian/pyversions000066400000000000000000000000051224574136100176230ustar00rootroot000000000000002.6- django-conneg-0.9.4/debian/rules000077500000000000000000000002741224574136100165470ustar00rootroot00000000000000#!/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/000077500000000000000000000000001224574136100167645ustar00rootroot00000000000000django-conneg-0.9.4/debian/source/format000066400000000000000000000000151224574136100201730ustar00rootroot000000000000003.0 (native) django-conneg-0.9.4/django_conneg/000077500000000000000000000000001224574136100170355ustar00rootroot00000000000000django-conneg-0.9.4/django_conneg/__init__.py000066400000000000000000000000261224574136100211440ustar00rootroot00000000000000__version__ = '0.9.4' django-conneg-0.9.4/django_conneg/conneg.py000066400000000000000000000120311224574136100206550ustar00rootroot00000000000000from 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.py000066400000000000000000000014511224574136100215550ustar00rootroot00000000000000from 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.py000066400000000000000000000110151224574136100203640ustar00rootroot00000000000000from __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.py000066400000000000000000000000001224574136100206600ustar00rootroot00000000000000django-conneg-0.9.4/django_conneg/support/000077500000000000000000000000001224574136100205515ustar00rootroot00000000000000django-conneg-0.9.4/django_conneg/support/__init__.py000066400000000000000000000000011224574136100226510ustar00rootroot00000000000000 django-conneg-0.9.4/django_conneg/support/middleware.py000066400000000000000000000122531224574136100232430ustar00rootroot00000000000000import 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/000077500000000000000000000000001224574136100210335ustar00rootroot00000000000000django-conneg-0.9.4/django_conneg/templates/conneg/000077500000000000000000000000001224574136100223045ustar00rootroot00000000000000django-conneg-0.9.4/django_conneg/templates/conneg/base.html000066400000000000000000000000311224574136100240760ustar00rootroot00000000000000{% extends "base.html" %}django-conneg-0.9.4/django_conneg/templates/conneg/error.html000066400000000000000000000004311224574136100243210ustar00rootroot00000000000000{% 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.txt000066400000000000000000000002151224574136100241740ustar00rootroot00000000000000{{ 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.html000066400000000000000000000003641224574136100251310ustar00rootroot00000000000000{% 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.txt000066400000000000000000000001341224574136100247770ustar00rootroot00000000000000Access to this resource is forbidden.{% if error.message %} {{ error.message }}{% endif %} django-conneg-0.9.4/django_conneg/templates/conneg/inactive_user.html000066400000000000000000000004041224574136100260300ustar00rootroot00000000000000{% 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.txt000066400000000000000000000001341224574136100257030ustar00rootroot00000000000000Access to this resource is forbidden.{% if error.message %} {{ error.message }}{% endif %} django-conneg-0.9.4/django_conneg/templates/conneg/not_acceptable.html000066400000000000000000000026361224574136100261440ustar00rootroot00000000000000{% 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:

{% for renderer in error.available_renderers %} {% endfor %}
Format name Format identifier Associated mimetypes
{{ renderer.name }} {{ renderer.format }} {% for mimetype in renderer.mimetypes %}{{ mimetype }}{% if not forloop.last %}, {% endif %}{% 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 %}
  1. {{ mediatype }}
  2. {% endfor %}
{% endblock %}django-conneg-0.9.4/django_conneg/templates/conneg/not_acceptable.txt000066400000000000000000000017621224574136100260160ustar00rootroot00000000000000Not 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.html000066400000000000000000000003651224574136100251710ustar00rootroot00000000000000{% 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.txt000066400000000000000000000001351224574136100250370ustar00rootroot00000000000000The requested resource does not exist.{% if error.message %} {{ error.message }}{% endif %} django-conneg-0.9.4/django_conneg/templates/conneg/service_unavailable.html000066400000000000000000000004611224574136100271760ustar00rootroot00000000000000{% 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.txt000066400000000000000000000002051224574136100270450ustar00rootroot00000000000000The 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.html000066400000000000000000000004561224574136100257200ustar00rootroot00000000000000{% 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.txt000066400000000000000000000002171224574136100255660ustar00rootroot00000000000000You 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.py000066400000000000000000000012061224574136100223050ustar00rootroot00000000000000import 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/000077500000000000000000000000001224574136100201775ustar00rootroot00000000000000django-conneg-0.9.4/django_conneg/tests/__init__.py000066400000000000000000000000761224574136100223130ustar00rootroot00000000000000from .basic_auth_middleware import * from .priorities import *django-conneg-0.9.4/django_conneg/tests/basic_auth_middleware.py000066400000000000000000000067051224574136100250600ustar00rootroot00000000000000import 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.py000066400000000000000000000054011224574136100227420ustar00rootroot00000000000000import 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/000077500000000000000000000000001224574136100221755ustar00rootroot00000000000000django-conneg-0.9.4/django_conneg/tests/templates/base.html000066400000000000000000000002711224574136100237750ustar00rootroot00000000000000 {% block title %}{% endblock %} {% block content %}{% endblock %} django-conneg-0.9.4/django_conneg/tests/urls.py000066400000000000000000000017241224574136100215420ustar00rootroot00000000000000from 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.py000066400000000000000000000005021224574136100205440ustar00rootroot00000000000000try: 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.py000066400000000000000000000435371224574136100205600ustar00rootroot00000000000000from __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.txt000066400000000000000000000000151224574136100175220ustar00rootroot00000000000000Django>=1.3 django-conneg-0.9.4/setup.py000066400000000000000000000056041224574136100157610ustar00rootroot00000000000000from 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.ini000066400000000000000000000001761224574136100155610ustar00rootroot00000000000000[tox] [testenv] deps = Django django-jenkins commands=django-admin.py jenkins --settings=django_conneg.test_settings