Ticket #2333: browser.patch

File browser.patch, 13.6 KB (added by russellm, 9 years ago)

A browser for testing purposes

  • django/test/browser.py

     
     1from cStringIO import StringIO
     2from django.contrib.admin.views.decorators import LOGIN_FORM_KEY, _encode_post_data
     3from django.core.handlers.base import BaseHandler
     4from django.core.handlers.wsgi import WSGIRequest
     5from django.dispatch import dispatcher
     6from django.http import urlencode, SimpleCookie
     7from django.template import signals
     8from django.utils.functional import curry
     9
     10class BrowserHandler(BaseHandler):
     11    """
     12    A HTTP Handler that can be used for testing purposes.
     13    Uses the WSGI interface to compose requests, but returns
     14    the raw HttpResponse object
     15    """
     16    def __call__(self, environ):
     17        from django.conf import settings
     18        from django.core import signals
     19
     20        # Set up middleware if needed. We couldn't do this earlier, because
     21        # settings weren't available.
     22        if self._request_middleware is None:
     23            self.load_middleware()
     24
     25        dispatcher.send(signal=signals.request_started)
     26        try:
     27            request = WSGIRequest(environ)
     28            response = self.get_response(request.path, request)
     29
     30            # Apply response middleware
     31            for middleware_method in self._response_middleware:
     32                response = middleware_method(request, response)
     33
     34        finally:
     35            dispatcher.send(signal=signals.request_finished)
     36       
     37        return response
     38
     39def store_rendered_templates(store, signal, sender, template, context):
     40    "A utility function for storing templates and contexts that are rendered"
     41    store.setdefault('template',[]).append(template)
     42    store.setdefault('context',[]).append(context)
     43
     44def encode_multipart(boundary, data):
     45    """
     46    A simple method for encoding multipart POST data from a dictionary of
     47    form values.
     48   
     49    The key will be used as the form data name; the value will be transmitted
     50    as content. If the value is a file, the contents of the file will be sent
     51    as an application/octet-stream; otherwise, str(value) will be sent.
     52    """
     53    lines = []
     54    for (key, value) in data.items():
     55        if isinstance(value, file):
     56            lines.append('--' + boundary)
     57            lines.append('Content-Disposition: form-data; name="%s"' % key)
     58            lines.append('')
     59            lines.append('--' + boundary)
     60            lines.append('Content-Disposition: form-data; name="%s_file"; filename="%s"' % (key, value.name))
     61            lines.append('Content-Type: application/octet-stream')
     62            lines.append('')
     63            lines.append(value.read())
     64        else:
     65            lines.append('--' + boundary)
     66            lines.append('Content-Disposition: form-data; name="%s"' % key)
     67            lines.append('')
     68            lines.append(str(value))
     69       
     70    lines.append('--' + boundary + '--')
     71    lines.append('')
     72    return '\r\n'.join(lines)
     73
     74class Browser:
     75    """
     76    A class that can act as a browser for testing purposes.
     77     
     78    It allows the user to compose GET and POST requests, and
     79    obtain the response that the server gave to those requests.
     80    The server Response objects are annotated with the details
     81    of the contexts and templates that were rendered during the
     82    process of serving the request.
     83
     84    Browser objects are stateful - they will retain cookie (and
     85    thus session) details for the lifetime of the Browser instance.
     86   
     87    This is not intended as a replacement for Twill/Selenium or
     88    the like - it is here to allow testing against the
     89    contexts and templates produced by a view, rather than the
     90    HTML rendered to the end-user.
     91    """
     92    def __init__(self, **defaults):
     93        self.handler = TestHandler()
     94        self.defaults = defaults
     95        self.cookie = SimpleCookie()
     96       
     97    def request(self, **request):
     98        """
     99        The master request method. Composes the environment dictionary
     100        and passes to the handler, returning the result of the handler.
     101        Assumes defaults for the query environment, which can be overridden
     102        using the arguments to the request.
     103        """
     104
     105
     106        # Set up the base request environment
     107        environ = {
     108            'HTTP_COOKIE':      self.cookie,
     109            'PATH_INFO':         '/',
     110            'QUERY_STRING':      '',
     111            'REQUEST_METHOD':    'GET',
     112            'SCRIPT_NAME':       None,
     113            'SERVER_NAME':       'testserver',
     114            'SERVER_PORT':       80,
     115            'SERVER_PROTOCOL':   'HTTP/1.1',
     116        }
     117       
     118        # Appy any default or request specific environment updates
     119        environ.update(self.defaults)
     120        environ.update(request)       
     121
     122        # Curry a data dictionary into an instance of
     123        # the template renderer callback function
     124        data = {}
     125        on_template_render = curry(store_rendered_templates, data)
     126       
     127        # Connect the curried function to the template_rendered signal
     128        dispatcher.connect(on_template_render, signal=signals.template_rendered)
     129
     130        # Handle the request
     131        response = self.handler(environ)
     132       
     133        # Add any rendered template detail to the response
     134        # If there was only one template rendered (the most likely case),
     135        # flatten the list to a single element
     136        for detail in ('template', 'context'):
     137            if data.get(detail):
     138                if len(data[detail]) == 1:
     139                    setattr(response, detail, data[detail][0]);
     140                else:
     141                    setattr(response, detail, data[detail])
     142            else:
     143                setattr(response, detail, None)
     144       
     145        # If the response requested a new cookie be set, store it
     146        if response.cookies:
     147            self.cookie.update(response.cookies)
     148        return response
     149       
     150    def get(self, path, data={}, **extra):
     151        "Request a response from the server using GET."
     152        r = {
     153            'CONTENT_LENGTH':  None,
     154            'CONTENT_TYPE':    'text/html; charset=utf-8',
     155            'PATH_INFO':       path,
     156            'QUERY_STRING':    urlencode(data),
     157            'REQUEST_METHOD': 'GET',
     158        }
     159        r.update(extra)
     160       
     161        return self.request(**r)
     162   
     163    def post(self, path, data={}, **extra):
     164        "Request a response from the server using POST."
     165       
     166        BOUNDARY = 'BoUnDaRyStRiNg'
     167
     168        encoded = encode_multipart(BOUNDARY, data)
     169        stream = StringIO(encoded)
     170        r = {
     171            'CONTENT_LENGTH': len(encoded),
     172            'CONTENT_TYPE':   'multipart/form-data; boundary=%s' % BOUNDARY,
     173            'PATH_INFO':      path,
     174            'REQUEST_METHOD': 'POST',
     175            'wsgi.input':     stream,
     176        }
     177        r.update(extra)
     178       
     179        return self.request(**r)
     180
     181    def login(self, path, username, password, **extra):
     182        """
     183        A specialized sequence of GET and POST to log into a view that
     184        is protected by @login_required or a similar access decorator.
     185       
     186        path should be the URL of the login page, or of any page that
     187        is login protected.
     188       
     189        Returns True if login was successful; False if otherwise.       
     190        """
     191        # First, GET the login page.
     192        # This is required to establish the session.
     193        response = self.get(path)
     194        if response.status_code != 200:
     195            return False
     196
     197        # Set up the block of form data required by the login page.
     198        form_data = {
     199            'username': username,
     200            'password': password,
     201            'this_is_the_login_form':1,
     202            'post_data':_encode_post_data({LOGIN_FORM_KEY: 1})
     203        }
     204        response = self.post('/admin/', data=form_data, **extra)
     205       
     206        # login page should response 200 (if you requested the login
     207        # page specifically), or 302 (if you requested a login
     208        # protected page, to which the login can redirect).
     209        return response.status_code in (200,302)
  • django/views/debug.py

     
    117117            'function': '?',
    118118            'lineno': '?',
    119119        }]
    120     t = Template(TECHNICAL_500_TEMPLATE)
     120    t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 Template')
    121121    c = Context({
    122122        'exception_type': exc_type.__name__,
    123123        'exception_value': exc_value,
     
    143143            # tried exists but is an empty list. The URLconf must've been empty.
    144144            return empty_urlconf(request)
    145145
    146     t = Template(TECHNICAL_404_TEMPLATE)
     146    t = Template(TECHNICAL_404_TEMPLATE, name='Technical 404 Template')
    147147    c = Context({
    148148        'root_urlconf': settings.ROOT_URLCONF,
    149149        'urlpatterns': tried,
     
    156156
    157157def empty_urlconf(request):
    158158    "Create an empty URLconf 404 error response."
    159     t = Template(EMPTY_URLCONF_TEMPLATE)
     159    t = Template(EMPTY_URLCONF_TEMPLATE, name='Empty URLConf Template')
    160160    c = Context({
    161161        'project_name': settings.SETTINGS_MODULE.split('.')[0]
    162162    })
  • django/views/static.py

     
    8282    try:
    8383        t = loader.get_template('static/directory_index')
    8484    except TemplateDoesNotExist:
    85         t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE)
     85        t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default Directory Index Template')
    8686    files = []
    8787    for f in os.listdir(fullpath):
    8888        if not f.startswith('.'):
  • django/template/__init__.py

     
    6060from django.template.context import Context, RequestContext, ContextPopException
    6161from django.utils.functional import curry
    6262from django.utils.text import smart_split
     63from django.dispatch import dispatcher
     64from django.template import signals
    6365
    6466__all__ = ('Template', 'Context', 'RequestContext', 'compile_string')
    6567
     
    137139        return self.source
    138140
    139141class Template(object):
    140     def __init__(self, template_string, origin=None):
     142    def __init__(self, template_string, origin=None, name='<Unknown Template>'):
    141143        "Compilation stage"
    142144        if settings.TEMPLATE_DEBUG and origin == None:
    143145            origin = StringOrigin(template_string)
    144146            # Could do some crazy stack-frame stuff to record where this string
    145147            # came from...
    146148        self.nodelist = compile_string(template_string, origin)
     149        self.name = name
    147150
    148151    def __iter__(self):
    149152        for node in self.nodelist:
     
    152155
    153156    def render(self, context):
    154157        "Display stage -- can be called many times"
     158        dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context)
    155159        return self.nodelist.render(context)
    156160
    157161def compile_string(template_string, origin):
  • django/template/signals.py

     
     1template_rendered=object()
     2 No newline at end of file
  • django/template/defaulttags.py

     
    251251            output = ''
    252252        if self.parsed:
    253253            try:
    254                 t = Template(output)
     254                t = Template(output, name=self.filepath)
    255255                return t.render(context)
    256256            except TemplateSyntaxError, e:
    257257                if settings.DEBUG:
  • django/template/loader_tags.py

     
    5555        except TemplateDoesNotExist:
    5656            raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent
    5757        else:
    58             return get_template_from_string(source, origin)
     58            return get_template_from_string(source, origin, parent)
    5959
    6060    def render(self, context):
    6161        compiled_parent = self.get_parent(context)
  • django/template/loader.py

     
    7676    Returns a compiled Template object for the given template name,
    7777    handling template inheritance recursively.
    7878    """
    79     return get_template_from_string(*find_template_source(template_name))
     79    source, origin = find_template_source(template_name)
     80    template = get_template_from_string(source, origin, template_name)
     81    return template
    8082
    81 def get_template_from_string(source, origin=None):
     83def get_template_from_string(source, origin=None, name=None):
    8284    """
    8385    Returns a compiled Template object for the given template code,
    8486    handling template inheritance recursively.
    8587    """
    86     return Template(source, origin)
     88    return Template(source, origin, name)
    8789
    8890def render_to_string(template_name, dictionary=None, context_instance=None):
    8991    """
Back to Top