fixes #815: books arrangement
[wolnelektury.git] / apps / piston / resource.py
1 import sys, inspect
2
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
11
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
18
19 CHALLENGE = object()
20
21 class Resource(object):
22     """
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.
28     """
29     callmap = { 'GET': 'read', 'POST': 'create',
30                 'PUT': 'update', 'DELETE': 'delete' }
31
32     def __init__(self, handler, authentication=None):
33         if not callable(handler):
34             raise AttributeError, "Handler not callable."
35
36         self.handler = handler()
37
38         if not authentication:
39             self.authentication = (NoAuthentication(),)
40         elif isinstance(authentication, (list, tuple)):
41             self.authentication = authentication
42         else:
43             self.authentication = (authentication,)
44
45         # Erroring
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)
49
50     def determine_emitter(self, request, *args, **kwargs):
51         """
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.
55
56         You could also check for the `Accept` HTTP header here,
57         since that pretty much makes sense. Refer to `Mimer` for
58         that as well.
59         """
60         em = kwargs.pop('emitter_format', None)
61
62         if not em:
63             em = request.GET.get('format', 'json')
64
65         return em
66
67     @property
68     def anonymous(self):
69         """
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.)
74         """
75         if hasattr(self.handler, 'anonymous'):
76             anon = self.handler.anonymous
77
78             if callable(anon):
79                 return anon
80
81             for klass in typemapper.keys():
82                 if anon == klass.__name__:
83                     return klass
84
85         return None
86
87     def authenticate(self, request, rm):
88         actor, anonymous = False, True
89
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:
94
95                     actor, anonymous = self.anonymous(), True
96                 else:
97                     actor, anonymous = authenticator.challenge, CHALLENGE
98             else:
99                 return self.handler, self.handler.is_anonymous
100
101         return actor, anonymous
102
103     @vary_on_headers('Authorization')
104     def __call__(self, request, *args, **kwargs):
105         """
106         NB: Sends a `Vary` header so we don't cache requests
107         that are different (OAuth stuff in `Authorization` header.)
108         """
109         rm = request.method.upper()
110
111         # Django's internal mechanism doesn't pick up
112         # PUT request, so we trick it a little here.
113         if rm == "PUT":
114             coerce_put_post(request)
115
116         actor, anonymous = self.authenticate(request, rm)
117
118         if anonymous is CHALLENGE:
119             return actor()
120         else:
121             handler = actor
122
123         # Translate nested datastructs into `request.data` here.
124         if rm in ('POST', 'PUT'):
125             try:
126                 translate_mime(request)
127             except MimerDataException:
128                 return rc.BAD_REQUEST
129
130         if not rm in handler.allowed_methods:
131             return HttpResponseNotAllowed(handler.allowed_methods)
132
133         meth = getattr(handler, self.callmap.get(rm), None)
134
135         if not meth:
136             raise Http404
137
138         # Support emitter both through (?P<emitter_format>) and ?format=emitter.
139         em_format = self.determine_emitter(request, *args, **kwargs)
140
141         kwargs.pop('emitter_format', None)
142
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)
147
148         try:
149             result = meth(request, *args, **kwargs)
150         except FormValidationError, e:
151             resp = rc.BAD_REQUEST
152             resp.write(' '+str(e.form.errors))
153
154             return resp
155         except TypeError, e:
156             result = rc.BAD_REQUEST
157             hm = HandlerMethod(meth)
158             sig = hm.signature
159
160             msg = 'Method signature does not match.\n\n'
161
162             if sig:
163                 msg += 'Signature should be: %s' % sig
164             else:
165                 msg += 'Resource does not expect any parameters.'
166
167             if self.display_errors:
168                 msg += '\n\nException was: %s' % str(e)
169
170             result.content = format_error(msg)
171         except Http404:
172             return rc.NOT_FOUND
173         except HttpStatusCode, e:
174             return e.response
175         except Exception, e:
176             """
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:
180
181             Parameters::
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.
186
187             If `PISTON_DISPLAY_ERRORS` is not enabled, the caller will
188             receive a basic "500 Internal Server Error" message.
189             """
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())))
197             else:
198                 raise
199
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
205
206         srl = emitter(result, typemapper, handler, fields, anonymous)
207
208         try:
209             """
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.
214             """
215             if self.stream: stream = srl.stream_render(request)
216             else: stream = srl.render(request)
217
218             if not isinstance(stream, HttpResponse):
219                 resp = HttpResponse(stream, mimetype=ct)
220             else:
221                 resp = stream
222
223             resp.streaming = self.stream
224
225             return resp
226         except HttpStatusCode, e:
227             return e.response
228
229     @staticmethod
230     def cleanup_request(request):
231         """
232         Removes `oauth_` keys from various dicts on the
233         request object, and returns the sanitized version.
234         """
235         for method_type in ('GET', 'PUT', 'POST', 'DELETE'):
236             block = getattr(request, method_type, { })
237
238             if True in [ k.startswith("oauth_") for k in block.keys() ]:
239                 sanitized = block.copy()
240
241                 for k in sanitized.keys():
242                     if k.startswith("oauth_"):
243                         sanitized.pop(k)
244
245                 setattr(request, method_type, sanitized)
246
247         return request
248
249     # --
250
251     def email_exception(self, reporter):
252         subject = "Piston crash report"
253         html = reporter.get_traceback_html()
254
255         message = EmailMessage(settings.EMAIL_SUBJECT_PREFIX+subject,
256                                 html, settings.SERVER_EMAIL,
257                                 [ admin[1] for admin in settings.ADMINS ])
258
259         message.content_subtype = 'html'
260         message.send(fail_silently=True)