3 from django.http import (HttpResponse, Http404, HttpResponseNotAllowed,
4 HttpResponseForbidden, HttpResponseServerError)
5 from django.views.debug import ExceptionReporter
6 from django.views.decorators.vary import vary_on_headers
7 from django.conf import settings
8 from django.core.mail import send_mail, EmailMessage
9 from django.db.models.query import QuerySet
10 from django.http import Http404
12 from emitters import Emitter
13 from handler import typemapper
14 from doc import HandlerMethod
15 from authentication import NoAuthentication
16 from utils import coerce_put_post, FormValidationError, HttpStatusCode
17 from utils import rc, format_error, translate_mime, MimerDataException
21 class Resource(object):
23 Resource. Create one for your URL mappings, just
24 like you would with Django. Takes one argument,
25 the handler. The second argument is optional, and
26 is an authentication handler. If not specified,
27 `NoAuthentication` will be used by default.
29 callmap = { 'GET': 'read', 'POST': 'create',
30 'PUT': 'update', 'DELETE': 'delete' }
32 def __init__(self, handler, authentication=None):
33 if not callable(handler):
34 raise AttributeError, "Handler not callable."
36 self.handler = handler()
38 if not authentication:
39 self.authentication = (NoAuthentication(),)
40 elif isinstance(authentication, (list, tuple)):
41 self.authentication = authentication
43 self.authentication = (authentication,)
46 self.email_errors = getattr(settings, 'PISTON_EMAIL_ERRORS', True)
47 self.display_errors = getattr(settings, 'PISTON_DISPLAY_ERRORS', True)
48 self.stream = getattr(settings, 'PISTON_STREAM_OUTPUT', False)
50 def determine_emitter(self, request, *args, **kwargs):
52 Function for determening which emitter to use
53 for output. It lives here so you can easily subclass
54 `Resource` in order to change how emission is detected.
56 You could also check for the `Accept` HTTP header here,
57 since that pretty much makes sense. Refer to `Mimer` for
60 em = kwargs.pop('emitter_format', None)
63 em = request.GET.get('format', 'json')
70 Gets the anonymous handler. Also tries to grab a class
71 if the `anonymous` value is a string, so that we can define
72 anonymous handlers that aren't defined yet (like, when
73 you're subclassing your basehandler into an anonymous one.)
75 if hasattr(self.handler, 'anonymous'):
76 anon = self.handler.anonymous
81 for klass in typemapper.keys():
82 if anon == klass.__name__:
87 def authenticate(self, request, rm):
88 actor, anonymous = False, True
90 for authenticator in self.authentication:
91 if not authenticator.is_authenticated(request):
92 if self.anonymous and \
93 rm in self.anonymous.allowed_methods:
95 actor, anonymous = self.anonymous(), True
97 actor, anonymous = authenticator.challenge, CHALLENGE
99 return self.handler, self.handler.is_anonymous
101 return actor, anonymous
103 @vary_on_headers('Authorization')
104 def __call__(self, request, *args, **kwargs):
106 NB: Sends a `Vary` header so we don't cache requests
107 that are different (OAuth stuff in `Authorization` header.)
109 rm = request.method.upper()
111 # Django's internal mechanism doesn't pick up
112 # PUT request, so we trick it a little here.
114 coerce_put_post(request)
116 actor, anonymous = self.authenticate(request, rm)
118 if anonymous is CHALLENGE:
123 # Translate nested datastructs into `request.data` here.
124 if rm in ('POST', 'PUT'):
126 translate_mime(request)
127 except MimerDataException:
128 return rc.BAD_REQUEST
130 if not rm in handler.allowed_methods:
131 return HttpResponseNotAllowed(handler.allowed_methods)
133 meth = getattr(handler, self.callmap.get(rm), None)
138 # Support emitter both through (?P<emitter_format>) and ?format=emitter.
139 em_format = self.determine_emitter(request, *args, **kwargs)
141 kwargs.pop('emitter_format', None)
143 # Clean up the request object a bit, since we might
144 # very well have `oauth_`-headers in there, and we
145 # don't want to pass these along to the handler.
146 request = self.cleanup_request(request)
149 result = meth(request, *args, **kwargs)
150 except FormValidationError, e:
151 resp = rc.BAD_REQUEST
152 resp.write(' '+str(e.form.errors))
156 result = rc.BAD_REQUEST
157 hm = HandlerMethod(meth)
160 msg = 'Method signature does not match.\n\n'
163 msg += 'Signature should be: %s' % sig
165 msg += 'Resource does not expect any parameters.'
167 if self.display_errors:
168 msg += '\n\nException was: %s' % str(e)
170 result.content = format_error(msg)
173 except HttpStatusCode, e:
177 On errors (like code errors), we'd like to be able to
178 give crash reports to both admins and also the calling
179 user. There's two setting parameters for this:
182 - `PISTON_EMAIL_ERRORS`: Will send a Django formatted
183 error email to people in `settings.ADMINS`.
184 - `PISTON_DISPLAY_ERRORS`: Will return a simple traceback
185 to the caller, so he can tell you what error they got.
187 If `PISTON_DISPLAY_ERRORS` is not enabled, the caller will
188 receive a basic "500 Internal Server Error" message.
190 exc_type, exc_value, tb = sys.exc_info()
191 rep = ExceptionReporter(request, exc_type, exc_value, tb.tb_next)
192 if self.email_errors:
193 self.email_exception(rep)
194 if self.display_errors:
195 return HttpResponseServerError(
196 format_error('\n'.join(rep.format_exception())))
200 emitter, ct = Emitter.get(em_format)
201 fields = handler.fields
202 if hasattr(handler, 'list_fields') and (
203 isinstance(result, list) or isinstance(result, QuerySet)):
204 fields = handler.list_fields
206 srl = emitter(result, typemapper, handler, fields, anonymous)
210 Decide whether or not we want a generator here,
211 or we just want to buffer up the entire result
212 before sending it to the client. Won't matter for
213 smaller datasets, but larger will have an impact.
215 if self.stream: stream = srl.stream_render(request)
216 else: stream = srl.render(request)
218 if not isinstance(stream, HttpResponse):
219 resp = HttpResponse(stream, mimetype=ct)
223 resp.streaming = self.stream
226 except HttpStatusCode, e:
230 def cleanup_request(request):
232 Removes `oauth_` keys from various dicts on the
233 request object, and returns the sanitized version.
235 for method_type in ('GET', 'PUT', 'POST', 'DELETE'):
236 block = getattr(request, method_type, { })
238 if True in [ k.startswith("oauth_") for k in block.keys() ]:
239 sanitized = block.copy()
241 for k in sanitized.keys():
242 if k.startswith("oauth_"):
245 setattr(request, method_type, sanitized)
251 def email_exception(self, reporter):
252 subject = "Piston crash report"
253 html = reporter.get_traceback_html()
255 message = EmailMessage(settings.EMAIL_SUBJECT_PREFIX+subject,
256 html, settings.SERVER_EMAIL,
257 [ admin[1] for admin in settings.ADMINS ])
259 message.content_subtype = 'html'
260 message.send(fail_silently=True)