Django

Code

root/django/branches/gis/django/test/client.py

Revision 8215, 11.8 kB (checked in by jbronn, 4 months ago)

gis: Merged revisions 7981-8001,8003-8011,8013-8033,8035-8036,8038-8039,8041-8063,8065-8076,8078-8139,8141-8154,8156-8214 via svnmerge from trunk.

  • Property svn:eol-style set to native
Line 
1 import urllib
2 import sys
3 import os
4 try:
5     from cStringIO import StringIO
6 except ImportError:
7     from StringIO import StringIO
8
9 from django.conf import settings
10 from django.contrib.auth import authenticate, login
11 from django.core.handlers.base import BaseHandler
12 from django.core.handlers.wsgi import WSGIRequest
13 from django.core.signals import got_request_exception
14 from django.dispatch import dispatcher
15 from django.http import SimpleCookie, HttpRequest
16 from django.template import TemplateDoesNotExist
17 from django.test import signals
18 from django.utils.functional import curry
19 from django.utils.encoding import smart_str
20 from django.utils.http import urlencode
21 from django.utils.itercompat import is_iterable
22
23 BOUNDARY = 'BoUnDaRyStRiNg'
24 MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY
25
26
27 class FakePayload(object):
28     """
29     A wrapper around StringIO that restricts what can be read since data from
30     the network can't be seeked and cannot be read outside of its content
31     length. This makes sure that views can't do anything under the test client
32     that wouldn't work in Real Life.
33     """
34     def __init__(self, content):
35         self.__content = StringIO(content)
36         self.__len = len(content)
37
38     def read(self, num_bytes=None):
39         if num_bytes is None:
40             num_bytes = self.__len or 1
41         assert self.__len >= num_bytes, "Cannot read more than the available bytes from the HTTP incoming data."
42         content = self.__content.read(num_bytes)
43         self.__len -= num_bytes
44         return content
45
46
47 class ClientHandler(BaseHandler):
48     """
49     A HTTP Handler that can be used for testing purposes.
50     Uses the WSGI interface to compose requests, but returns
51     the raw HttpResponse object
52     """
53     def __call__(self, environ):
54         from django.conf import settings
55         from django.core import signals
56
57         # Set up middleware if needed. We couldn't do this earlier, because
58         # settings weren't available.
59         if self._request_middleware is None:
60             self.load_middleware()
61
62         dispatcher.send(signal=signals.request_started)
63         try:
64             request = WSGIRequest(environ)
65             response = self.get_response(request)
66
67             # Apply response middleware.
68             for middleware_method in self._response_middleware:
69                 response = middleware_method(request, response)
70             response = self.apply_response_fixes(request, response)
71         finally:
72             dispatcher.send(signal=signals.request_finished)
73
74         return response
75
76 def store_rendered_templates(store, signal, sender, template, context):
77     """
78     Stores templates and contexts that are rendered.
79     """
80     store.setdefault('template',[]).append(template)
81     store.setdefault('context',[]).append(context)
82
83 def encode_multipart(boundary, data):
84     """
85     Encodes multipart POST data from a dictionary of form values.
86
87     The key will be used as the form data name; the value will be transmitted
88     as content. If the value is a file, the contents of the file will be sent
89     as an application/octet-stream; otherwise, str(value) will be sent.
90     """
91     lines = []
92     to_str = lambda s: smart_str(s, settings.DEFAULT_CHARSET)
93
94     # Not by any means perfect, but good enough for our purposes.
95     is_file = lambda thing: hasattr(thing, "read") and callable(thing.read)
96
97     # Each bit of the multipart form data could be either a form value or a
98     # file, or a *list* of form values and/or files. Remember that HTTP field
99     # names can be duplicated!
100     for (key, value) in data.items():
101         if is_file(value):
102             lines.extend(encode_file(boundary, key, value))
103         elif not isinstance(value, basestring) and is_iterable(value):
104             for item in value:
105                 if is_file(item):
106                     lines.extend(encode_file(boundary, key, item))
107                 else:
108                     lines.extend([
109                         '--' + boundary,
110                         'Content-Disposition: form-data; name="%s"' % to_str(key),
111                         '',
112                         to_str(item)
113                     ])
114         else:
115             lines.extend([
116                 '--' + boundary,
117                 'Content-Disposition: form-data; name="%s"' % to_str(key),
118                 '',
119                 to_str(value)
120             ])
121
122     lines.extend([
123         '--' + boundary + '--',
124         '',
125     ])
126     return '\r\n'.join(lines)
127
128 def encode_file(boundary, key, file):
129     to_str = lambda s: smart_str(s, settings.DEFAULT_CHARSET)
130     return [
131         '--' + boundary,
132         'Content-Disposition: form-data; name="%s"; filename="%s"' \
133             % (to_str(key), to_str(os.path.basename(file.name))),
134         'Content-Type: application/octet-stream',
135         '',
136         file.read()
137     ]
138    
139 class Client:
140     """
141     A class that can act as a client for testing purposes.
142
143     It allows the user to compose GET and POST requests, and
144     obtain the response that the server gave to those requests.
145     The server Response objects are annotated with the details
146     of the contexts and templates that were rendered during the
147     process of serving the request.
148
149     Client objects are stateful - they will retain cookie (and
150     thus session) details for the lifetime of the Client instance.
151
152     This is not intended as a replacement for Twill/Selenium or
153     the like - it is here to allow testing against the
154     contexts and templates produced by a view, rather than the
155     HTML rendered to the end-user.
156     """
157     def __init__(self, **defaults):
158         self.handler = ClientHandler()
159         self.defaults = defaults
160         self.cookies = SimpleCookie()
161         self.exc_info = None
162
163     def store_exc_info(self, *args, **kwargs):
164         """
165         Stores exceptions when they are generated by a view.
166         """
167         self.exc_info = sys.exc_info()
168
169     def _session(self):
170         """
171         Obtains the current session variables.
172         """
173         if 'django.contrib.sessions' in settings.INSTALLED_APPS:
174             engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
175             cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
176             if cookie:
177                 return engine.SessionStore(cookie.value)
178         return {}
179     session = property(_session)
180
181     def request(self, **request):
182         """
183         The master request method. Composes the environment dictionary
184         and passes to the handler, returning the result of the handler.
185         Assumes defaults for the query environment, which can be overridden
186         using the arguments to the request.
187         """
188         environ = {
189             'HTTP_COOKIE':      self.cookies,
190             'PATH_INFO':         '/',
191             'QUERY_STRING':      '',
192             'REQUEST_METHOD':    'GET',
193             'SCRIPT_NAME':       '',
194             'SERVER_NAME':       'testserver',
195             'SERVER_PORT':       80,
196             'SERVER_PROTOCOL':   'HTTP/1.1',
197         }
198         environ.update(self.defaults)
199         environ.update(request)
200
201         # Curry a data dictionary into an instance of the template renderer
202         # callback function.
203         data = {}
204         on_template_render = curry(store_rendered_templates, data)
205         dispatcher.connect(on_template_render, signal=signals.template_rendered)
206
207         # Capture exceptions created by the handler.
208         dispatcher.connect(self.store_exc_info, signal=got_request_exception)
209
210         try:
211             response = self.handler(environ)
212         except TemplateDoesNotExist, e:
213             # If the view raises an exception, Django will attempt to show
214             # the 500.html template. If that template is not available,
215             # we should ignore the error in favor of re-raising the
216             # underlying exception that caused the 500 error. Any other
217             # template found to be missing during view error handling
218             # should be reported as-is.
219             if e.args != ('500.html',):
220                 raise
221
222         # Look for a signalled exception, clear the current context
223         # exception data, then re-raise the signalled exception.
224         # Also make sure that the signalled exception is cleared from
225         # the local cache!
226         if self.exc_info:
227             exc_info = self.exc_info
228             self.exc_info = None
229             raise exc_info[1], None, exc_info[2]
230
231         # Save the client and request that stimulated the response.
232         response.client = self
233         response.request = request
234
235         # Add any rendered template detail to the response.
236         # If there was only one template rendered (the most likely case),
237         # flatten the list to a single element.
238         for detail in ('template', 'context'):
239             if data.get(detail):
240                 if len(data[detail]) == 1:
241                     setattr(response, detail, data[detail][0]);
242                 else:
243                     setattr(response, detail, data[detail])
244             else:
245                 setattr(response, detail, None)
246
247         # Update persistent cookie data.
248         if response.cookies:
249             self.cookies.update(response.cookies)
250
251         return response
252
253     def get(self, path, data={}, **extra):
254         """
255         Requests a response from the server using GET.
256         """
257         r = {
258             'CONTENT_LENGTH':  None,
259             'CONTENT_TYPE':    'text/html; charset=utf-8',
260             'PATH_INFO':       urllib.unquote(path),
261             'QUERY_STRING':    urlencode(data, doseq=True),
262             'REQUEST_METHOD': 'GET',
263         }
264         r.update(extra)
265
266         return self.request(**r)
267
268     def post(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
269         """
270         Requests a response from the server using POST.
271         """
272         if content_type is MULTIPART_CONTENT:
273             post_data = encode_multipart(BOUNDARY, data)
274         else:
275             post_data = data
276
277         r = {
278             'CONTENT_LENGTH': len(post_data),
279             'CONTENT_TYPE':   content_type,
280             'PATH_INFO':      urllib.unquote(path),
281             'REQUEST_METHOD': 'POST',
282             'wsgi.input':     FakePayload(post_data),
283         }
284         r.update(extra)
285
286         return self.request(**r)
287
288     def login(self, **credentials):
289         """
290         Sets the Client to appear as if it has successfully logged into a site.
291
292         Returns True if login is possible; False if the provided credentials
293         are incorrect, or the user is inactive, or if the sessions framework is
294         not available.
295         """
296         user = authenticate(**credentials)
297         if user and user.is_active \
298                 and 'django.contrib.sessions' in settings.INSTALLED_APPS:
299             engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
300
301             # Create a fake request to store login details.
302             request = HttpRequest()
303             request.session = engine.SessionStore()
304             login(request, user)
305
306             # Set the cookie to represent the session.
307             session_cookie = settings.SESSION_COOKIE_NAME
308             self.cookies[session_cookie] = request.session.session_key
309             cookie_data = {
310                 'max-age': None,
311                 'path': '/',
312                 'domain': settings.SESSION_COOKIE_DOMAIN,
313                 'secure': settings.SESSION_COOKIE_SECURE or None,
314                 'expires': None,
315             }
316             self.cookies[session_cookie].update(cookie_data)
317
318             # Save the session values.
319             request.session.save()
320
321             return True
322         else:
323             return False
324
325     def logout(self):
326         """
327         Removes the authenticated user's cookies.
328
329         Causes the authenticated user to be logged out.
330         """
331         session = __import__(settings.SESSION_ENGINE, {}, {}, ['']).SessionStore()
332         session.delete(session_key=self.cookies[settings.SESSION_COOKIE_NAME].value)
333         self.cookies = SimpleCookie()
Note: See TracBrowser for help on using the browser.