X-Git-Url: https://git.mdrn.pl/wolnelektury.git/blobdiff_plain/d0ab1d8908cadac9f51a17e2fe9e1193f14e28cc..0e87ae0739ed3e72301b7b718098f97a7f06a5d8:/apps/piston/resource.py diff --git a/apps/piston/resource.py b/apps/piston/resource.py new file mode 100644 index 000000000..40f065d05 --- /dev/null +++ b/apps/piston/resource.py @@ -0,0 +1,260 @@ +import sys, inspect + +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() + + 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 + + @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 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) 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 FormValidationError, e: + resp = rc.BAD_REQUEST + resp.write(' '+str(e.form.errors)) + + return resp + except TypeError, e: + 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) + except Http404: + return rc.NOT_FOUND + except HttpStatusCode, e: + return e.response + except Exception, e: + """ + 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 + + emitter, ct = Emitter.get(em_format) + fields = handler.fields + if hasattr(handler, 'list_fields') and ( + isinstance(result, list) or isinstance(result, QuerySet)): + fields = handler.list_fields + + 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) + else: + resp = stream + + resp.streaming = self.stream + + return resp + except HttpStatusCode, e: + return e.response + + @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)