Source

django-piston / piston / resource.py

The branch 'django1.4' does not exist.
Full commit
Jesper Nøhr e1d7a29 

Andi Albrecht 3a0d021 
Michael Richards… 0407e88 

Jesper Nøhr e1d7a29 
Jesper Nøhr c2b087f 
Jesper Nøhr e1d7a29 

James Emerton 4de2991 
Jesper Nøhr 5c2a4cd 
Jesper Nøhr e1d7a29 
Jesper Nøhr 4775806 
Jesper Nøhr 7e872e4 
Jesper Nøhr e1d7a29 
Jesper Nøhr 6acc5f8 
Jesper Nøhr 88a4cda 
Adam Lowry 5f2e107 
Jesper Nøhr 4775806 
Jesper Nøhr 4874292 

Jesper Nøhr 4775806 
Jesper Nøhr f50c817 






Alexander Ljungb… 4b24b73 
Jesper Nøhr 4775806 
Alexander Ljungb… 4b24b73 
Jesper Nøhr 4775806 


Alexander Ljungb… 4b24b73 
Jesper Nøhr 4775806 
Jesper Nøhr 87c0fcf 
Alexander Ljungb… 4b24b73 
Jesper Nøhr abe924e 
Jesper Nøhr 4874292 
Jesper Nøhr a7e8d64 
Jesper Nøhr 4874292 
Jesper Nøhr abe924e 
Jesper Nøhr 4874292 
Alexander Ljungb… 4b24b73 
Jesper Nøhr e1d7a29 


Jesper Nøhr b0a1571 

Jesper Nøhr 92f9c3e 









Jesper Nøhr 4682426 
Alexander Ljungb… 4b24b73 
Jesper Nøhr 92f9c3e 



mdorn 0210cf9 










Jesper Nøhr c64c757 









Alexander Ljungb… 4b24b73 
Jesper Nøhr c64c757 





Alexander Ljungb… 4b24b73 
Jesper Nøhr c64c757 
Alexander Ljungb… 4b24b73 
Jesper Nøhr 4874292 












Alexander Ljungb… 4b24b73 
Jesper Nøhr 4874292 
Alexander Ljungb… 4b24b73 
Jesper Nøhr c2b087f 
Jesper Nøhr 4775806 
Jesper Nøhr c2b087f 



Jesper Nøhr abecaaa 

Matthew Marshall 9cf7ef7 




Jesper Nøhr 4874292 
Jesper Nøhr abecaaa 
Jesper Nøhr 4874292 

Jesper Nøhr ac2ebcf 
Jesper Nøhr 4874292 
Alexander Ljungb… 4b24b73 
Jesper Nøhr 88a4cda 
Adam Lowry 5f2e107 




Alexander Ljungb… 4b24b73 
Alexander Ljungb… 7cace08 



Alexander Ljungb… 4b24b73 
Jesper Nøhr ac2ebcf 

Alexander Ljungb… 4b24b73 
Emanuele Rocca 5cbbead 
Mads Sülau Valst… 0764bde 
Jesper Nøhr 4775806 
Jesper Nøhr 23ebc37 

Jesper Nøhr 92f9c3e 
Jesper Nøhr 4682426 

Alexander Ljungb… 4b24b73 
Jesper Nøhr 835cd78 



Alexander Ljungb… 4b24b73 
Jesper Nøhr 4830140 

Matt Cordes 25272db 
Ionel Maries Cri… ad479ae 
Jesper Nøhr e1d7a29 
ion.scerbatiuc 04d0f8c 

Jesper Nøhr c9fe89f 

Jesper Nøhr dc0ee00 
Jesper Nøhr c9fe89f 
ion.scerbatiuc 04d0f8c 




Matt Cordes fde50de 




Andi Albrecht 3a0d021 
Matt Cordes fde50de 
Andi Albrecht 3a0d021 




Matt Cordes fde50de 
Andi Albrecht 3a0d021 
James Emerton 4de2991 
Michael Richards… 3e043a1 
Jesper Nøhr 2ac6494 
Jesper Nøhr b0a1571 








Jesper Nøhr 1a329d4 
Matt Cordes fde50de 
Jesper Nøhr 1a329d4 

Jesper Nøhr b0a1571 



Jesper Nøhr 2ac6494 
Jesper Nøhr 2de9bcc 
Mads Sülau Valst… 0764bde 
Jesper Nøhr 835cd78 
Andi Albrecht 3a0d021 









Jesper Nøhr 835cd78 




Jesper Nøhr e1d7a29 
Jesper Nøhr 835cd78 



Alexander Ljungb… 4b24b73 
Jesper Nøhr 835cd78 


Alexander Ljungb… 4b24b73 
Jesper Nøhr 835cd78 


Alexander Ljungb… 4b24b73 


Jesper Nøhr e1d7a29 






Alexander Ljungb… 4b24b73 
Jesper Nøhr e1d7a29 

Matt Cordes 25272db 

Ionel Maries Cri… ad479ae 
Matt Cordes 25272db 
Matt Cordes 9a91918 
Matt Cordes 25272db 




















Matt Cordes 9a91918 
Matt Cordes 25272db 




























import sys, inspect

import django
from django.http import (HttpResponse, Http404, HttpResponseNotAllowed,
    HttpResponseForbidden, HttpResponseServerError)
from django.views.debug import ExceptionReporter
from django.views.decorators.vary import vary_on_headers
from django.conf import settings
from django.core.mail import send_mail, EmailMessage
from django.db.models.query import QuerySet
from django.http import Http404

from emitters import Emitter
from handler import typemapper
from doc import HandlerMethod
from authentication import NoAuthentication
from utils import coerce_put_post, FormValidationError, HttpStatusCode
from utils import rc, format_error, translate_mime, MimerDataException

CHALLENGE = object()

class Resource(object):
    """
    Resource. Create one for your URL mappings, just
    like you would with Django. Takes one argument,
    the handler. The second argument is optional, and
    is an authentication handler. If not specified,
    `NoAuthentication` will be used by default.
    """
    callmap = { 'GET': 'read', 'POST': 'create',
                'PUT': 'update', 'DELETE': 'delete' }

    def __init__(self, handler, authentication=None):
        if not callable(handler):
            raise AttributeError, "Handler not callable."

        self.handler = handler()
        self.csrf_exempt = getattr(self.handler, 'csrf_exempt', True)

        if not authentication:
            self.authentication = (NoAuthentication(),)
        elif isinstance(authentication, (list, tuple)):
            self.authentication = authentication
        else:
            self.authentication = (authentication,)

        # Erroring
        self.email_errors = getattr(settings, 'PISTON_EMAIL_ERRORS', True)
        self.display_errors = getattr(settings, 'PISTON_DISPLAY_ERRORS', True)
        self.stream = getattr(settings, 'PISTON_STREAM_OUTPUT', False)

    def determine_emitter(self, request, *args, **kwargs):
        """
        Function for determening which emitter to use
        for output. It lives here so you can easily subclass
        `Resource` in order to change how emission is detected.

        You could also check for the `Accept` HTTP header here,
        since that pretty much makes sense. Refer to `Mimer` for
        that as well.
        """
        em = kwargs.pop('emitter_format', None)

        if not em:
            em = request.GET.get('format', 'json')

        return em

    def form_validation_response(self, e):
        """
        Method to return form validation error information. 
        You will probably want to override this in your own
        `Resource` subclass.
        """
        resp = rc.BAD_REQUEST
        resp.write(' '+str(e.form.errors))
        return resp

    @property
    def anonymous(self):
        """
        Gets the anonymous handler. Also tries to grab a class
        if the `anonymous` value is a string, so that we can define
        anonymous handlers that aren't defined yet (like, when
        you're subclassing your basehandler into an anonymous one.)
        """
        if hasattr(self.handler, 'anonymous'):
            anon = self.handler.anonymous

            if callable(anon):
                return anon

            for klass in typemapper.keys():
                if anon == klass.__name__:
                    return klass

        return None

    def authenticate(self, request, rm):
        actor, anonymous = False, True

        for authenticator in self.authentication:
            if not authenticator.is_authenticated(request):
                if self.anonymous and \
                    rm in self.anonymous.allowed_methods:

                    actor, anonymous = self.anonymous(), True
                else:
                    actor, anonymous = authenticator.challenge, CHALLENGE
            else:
                return self.handler, self.handler.is_anonymous

        return actor, anonymous

    @vary_on_headers('Authorization')
    def __call__(self, request, *args, **kwargs):
        """
        NB: Sends a `Vary` header so we don't cache requests
        that are different (OAuth stuff in `Authorization` header.)
        """
        rm = request.method.upper()

        # Django's internal mechanism doesn't pick up
        # PUT request, so we trick it a little here.
        if rm == "PUT":
            coerce_put_post(request)

        actor, anonymous = self.authenticate(request, rm)

        if anonymous is CHALLENGE:
            return actor()
        else:
            handler = actor

        # Translate nested datastructs into `request.data` here.
        if rm in ('POST', 'PUT'):
            try:
                translate_mime(request)
            except MimerDataException:
                return rc.BAD_REQUEST
            if not hasattr(request, 'data'):
                if rm == 'POST':
                    request.data = request.POST
                else:
                    request.data = request.PUT

        if not rm in handler.allowed_methods:
            return HttpResponseNotAllowed(handler.allowed_methods)

        meth = getattr(handler, self.callmap.get(rm, ''), None)
        if not meth:
            raise Http404

        # Support emitter both through (?P<emitter_format>) and ?format=emitter.
        em_format = self.determine_emitter(request, *args, **kwargs)

        kwargs.pop('emitter_format', None)

        # Clean up the request object a bit, since we might
        # very well have `oauth_`-headers in there, and we
        # don't want to pass these along to the handler.
        request = self.cleanup_request(request)

        try:
            result = meth(request, *args, **kwargs)
        except Exception, e:
            result = self.error_handler(e, request, meth, em_format)

        try:
            emitter, ct = Emitter.get(em_format)
            fields = handler.fields

            if hasattr(handler, 'list_fields') and isinstance(result, (list, tuple, QuerySet)):
                fields = handler.list_fields
        except ValueError:
            result = rc.BAD_REQUEST
            result.content = "Invalid output format specified '%s'." % em_format
            return result

        status_code = 200

        # If we're looking at a response object which contains non-string
        # content, then assume we should use the emitter to format that 
        # content
        if self._use_emitter(result):
            status_code = result.status_code
            # Note: We can't use result.content here because that
            # method attempts to convert the content into a string
            # which we don't want.  when
            # _is_string/_base_content_is_iter is False _container is
            # the raw data
            result = result._container

        srl = emitter(result, typemapper, handler, fields, anonymous)

        try:
            """
            Decide whether or not we want a generator here,
            or we just want to buffer up the entire result
            before sending it to the client. Won't matter for
            smaller datasets, but larger will have an impact.
            """
            if self.stream: stream = srl.stream_render(request)
            else: stream = srl.render(request)

            if not isinstance(stream, HttpResponse):
                resp = HttpResponse(stream, mimetype=ct, status=status_code)
            else:
                resp = stream

            resp.streaming = self.stream

            return resp
        except HttpStatusCode, e:
            return e.response

    @staticmethod
    def _use_emitter(result):
        """True iff result is a HttpResponse and contains non-string content."""
        if not isinstance(result, HttpResponse):
            return False
        elif django.VERSION >= (1, 4):
            return not result._base_content_is_iter
        else:
            return result._is_string

    @staticmethod
    def cleanup_request(request):
        """
        Removes `oauth_` keys from various dicts on the
        request object, and returns the sanitized version.
        """
        for method_type in ('GET', 'PUT', 'POST', 'DELETE'):
            block = getattr(request, method_type, { })

            if True in [ k.startswith("oauth_") for k in block.keys() ]:
                sanitized = block.copy()

                for k in sanitized.keys():
                    if k.startswith("oauth_"):
                        sanitized.pop(k)

                setattr(request, method_type, sanitized)

        return request

    # --

    def email_exception(self, reporter):
        subject = "Piston crash report"
        html = reporter.get_traceback_html()

        message = EmailMessage(settings.EMAIL_SUBJECT_PREFIX+subject,
                                html, settings.SERVER_EMAIL,
                                [ admin[1] for admin in settings.ADMINS ])

        message.content_subtype = 'html'
        message.send(fail_silently=True)


    def error_handler(self, e, request, meth, em_format):
        """
        Override this method to add handling of errors customized for your 
        needs
        """
        if isinstance(e, FormValidationError):
            return self.form_validation_response(e)

        elif isinstance(e, TypeError):
            result = rc.BAD_REQUEST
            hm = HandlerMethod(meth)
            sig = hm.signature

            msg = 'Method signature does not match.\n\n'

            if sig:
                msg += 'Signature should be: %s' % sig
            else:
                msg += 'Resource does not expect any parameters.'

            if self.display_errors:
                msg += '\n\nException was: %s' % str(e)

            result.content = format_error(msg)
            return result
        elif isinstance(e, Http404):
            return rc.NOT_FOUND

        elif isinstance(e, HttpStatusCode):
            return e.response
 
        else: 
            """
            On errors (like code errors), we'd like to be able to
            give crash reports to both admins and also the calling
            user. There's two setting parameters for this:

            Parameters::
             - `PISTON_EMAIL_ERRORS`: Will send a Django formatted
               error email to people in `settings.ADMINS`.
             - `PISTON_DISPLAY_ERRORS`: Will return a simple traceback
               to the caller, so he can tell you what error they got.

            If `PISTON_DISPLAY_ERRORS` is not enabled, the caller will
            receive a basic "500 Internal Server Error" message.
            """
            exc_type, exc_value, tb = sys.exc_info()
            rep = ExceptionReporter(request, exc_type, exc_value, tb.tb_next)
            if self.email_errors:
                self.email_exception(rep)
            if self.display_errors:
                return HttpResponseServerError(
                    format_error('\n'.join(rep.format_exception())))
            else:
                raise