Django

Code

Ticket #2333: browser.patch

File browser.patch, 13.6 kB (added by russellm, 2 years ago)

A browser for testing purposes

  • django/test/browser.py

    old new  
     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

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
  • django/template/defaulttags.py

    old new  
    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

    old new  
    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

    old new  
    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    """