Code

Ticket #11797: test_client_form_parsing.diff

File test_client_form_parsing.diff, 11.0 KB (added by Rupe, 4 years ago)

updated to the current trunk and improved

Line 
1Index: django/test/client.py
2===================================================================
3--- django/test/client.py       (revision 13063)
4+++ django/test/client.py       (working copy)
5@@ -1,5 +1,6 @@
6 import urllib
7 from urlparse import urlparse, urlunparse, urlsplit
8+from HTMLParser import HTMLParser
9 import sys
10 import os
11 import re
12@@ -13,7 +14,7 @@
13 from django.core.handlers.base import BaseHandler
14 from django.core.handlers.wsgi import WSGIRequest
15 from django.core.signals import got_request_exception
16-from django.http import SimpleCookie, HttpRequest, QueryDict
17+from django.http import SimpleCookie, HttpRequest, HttpResponse, QueryDict
18 from django.template import TemplateDoesNotExist
19 from django.test import signals
20 from django.utils.functional import curry
21@@ -84,6 +85,116 @@
22 
23         return response
24 
25+
26+class FormHTMLParser(HTMLParser):
27+    """
28+    Exctracts the list of forms and fields with their values to
29+    facilitate re-submission
30+    """
31+
32+    _tag_list = ['form', 'input', 'textarea', 'select', 'option']
33+
34+    def __init__(self):
35+        self._curTagName = None
36+        self._curTagType = None
37+        self.forms = None
38+        self.forms_count = 0
39+        HTMLParser.__init__(self)
40+
41+    def handle_starttag(self, tag, attrs):
42+        if tag in self._tag_list:
43+            attrs = dict(attrs)
44+            self._curTagType = tag
45+
46+            if tag == 'form':
47+                if self.forms is None:
48+                    self.forms = {}
49+                    self.forms_count = 0
50+                # We can use either the name or id here because it will
51+                # not be passed back to the server. It's only used for
52+                # easier (more plain) reference in testing code.
53+                if attrs.has_key('name'):
54+                    self._curForm = attrs['name']
55+                elif attrs.has_key('id'):
56+                    self._curForm = attrs['id']
57+                else:
58+                    # ? Should this be a string (for consistancy) or a real number?
59+                    self._curForm = str(self.forms_count)
60+                self.forms[self._curForm] = {}
61+                self.forms_count = self.forms_count + 1
62+
63+            elif tag == 'input' and attrs['type'] not in ('checkbox', 'radio'):
64+                if attrs.has_key('name'):
65+                    if not attrs.has_key('value'):
66+                        attrs['value'] = ''
67+                    self.forms[self._curForm][attrs['name']] = attrs['value']
68+                    self._curTagName = attrs['name']
69+
70+            elif tag == 'input' and attrs['type'] in ('checkbox', 'radio'):
71+                if attrs.has_key('checked'):
72+                    if not attrs.has_key('value'):
73+                        attrs['value'] = 'on'
74+                    if self.forms[self._curForm].has_key(attrs['name']):
75+                        self.forms[self._curForm][attrs['name']].append(attrs['value'])
76+                    else:
77+                        self.forms[self._curForm][attrs['name']] = [attrs['value']]
78+                    self._curTagName = attrs['name']
79+
80+            elif tag == 'textarea':
81+                self.forms[self._curForm][attrs['name']] = ''
82+                self._curTagName = attrs['name']
83+
84+            elif tag == 'select':
85+                self.forms[self._curForm][attrs['name']] = []
86+                self._curTagName = attrs['name']
87+
88+            elif tag == 'option':
89+                if attrs.has_key('selected'):
90+                    self.forms[self._curForm][self._curTagName].append(attrs['value'])
91+
92+    def handle_data(self, data):
93+        if self._curTagName:
94+            if self._curTagType == 'textarea':
95+                # This is to ensure that there is always just a \r\n, as is the observed
96+                # behavior of web browsers.
97+                self.forms[self._curForm][self._curTagName] = data.replace('\r\n', '\n').replace('\n', '\r\n')
98+
99+    def handle_endtag(self, tag):
100+        if self._curTagName and tag != 'option':
101+            self._curTagName = None
102+
103+class FieldNotFound(KeyError):
104+    "Raised when a requested field or form is not found"
105+    pass
106+
107+def castHttpResponse(curResponse):
108+
109+    class _TestHttpResponse(curResponse.__class__):
110+
111+        def form_data(self, search_name):
112+            """
113+            Returns the entire set of form values associated with form of the given field or form name
114+            """
115+            if getattr(self, "_form_data", None) is None:
116+                self._form_data = self._get_form_data()
117+            if self._form_data.has_key(search_name):
118+                return self._form_data[search_name]
119+            for form_name, data in self._form_data.items():
120+                if data.has_key(search_name):
121+                    return data
122+            raise FieldNotFound("A field or form with the name %s was not found." % search_name)
123+
124+        def _get_form_data(self):
125+            curParser = FormHTMLParser()
126+            curParser.feed(self.content)
127+            curParser.close()
128+            return curParser.forms
129+   
130+    curResponse.__class__ = _TestHttpResponse
131+   
132+    return curResponse
133+
134+
135 def store_rendered_templates(store, signal, sender, template, context, **kwargs):
136     """
137     Stores templates and contexts that are rendered.
138@@ -246,6 +357,9 @@
139                 exc_info = self.exc_info
140                 self.exc_info = None
141                 raise exc_info[1], None, exc_info[2]
142+           
143+            # Add the form field processing to response object
144+            response = castHttpResponse(response)
145 
146             # Save the client and request that stimulated the response.
147             response.client = self
148Index: docs/topics/testing.txt
149===================================================================
150--- docs/topics/testing.txt     (revision 13063)
151+++ docs/topics/testing.txt     (working copy)
152@@ -836,6 +836,27 @@
153             >>> response = client.get('/foo/')
154             >>> response.context['name']
155             'Arthur'
156+   
157+    .. method:: form_data(name)
158+   
159+       .. versionadded:: 1.2
160+       
161+       Returns a dictionary of values for a given form. If you give a name to your
162+       buttons their values will also be returned. You can supply either the name or
163+       id of the form or the name of any field or button and you will receive all
164+       values for the form.
165+       
166+           >>> response = client.get('/foo/')
167+           >>> response.form_data('subject_field')
168+           {'subject_field': 'Please visit my site', 'email_field': 'joe@xyz.com'}
169+       
170+       The resulting dictionary can then be used to easily change form data and
171+       submit the form.
172+       
173+           >>> my_form_data = response.form_data('subject_field')
174+           >>> my_form_data['email_field'] = 'mark@mycompany.com'
175+           >>> response = client.post('/foo/', my_form_data)
176+
177 
178     .. attribute:: request
179 
180Index: tests/regressiontests/test_client_regress/models.py
181===================================================================
182--- tests/regressiontests/test_client_regress/models.py (revision 13063)
183+++ tests/regressiontests/test_client_regress/models.py (working copy)
184@@ -821,3 +821,35 @@
185         response = self.client.post("/test_client_regress/parse_unicode_json/", json,
186                                     content_type="application/json; charset=koi8-r")
187         self.assertEqual(response.content, json.encode('koi8-r'))
188+
189+class FormProcessingTests(TestCase):
190+    def test_extraction(self):
191+        "The form values can be extracted, submitted, and re-extracted reliably."
192+        # Regression test for #11797
193+        from django.test.client import FieldNotFound
194+
195+        response = self.client.get('/test_client_regress/form_processing/')
196+        self.failUnlessEqual(response.status_code, 200, 'Failed to retrieve the form test page. (Status code: %s)' % response.status_code)
197+
198+        curVals = response.form_data('frmTest')
199+        curVals2 = response.form_data('choiceFld')
200+
201+        self.assert_(curVals == curVals2, 'The form name did not return the same data as the field name.')
202+
203+        self.assertRaises(FieldNotFound, response.form_data, 'badField')
204+       
205+        response = self.client.post('/test_client_regress/form_processing/', curVals)
206+        self.failUnlessEqual(response.status_code, 200, 'Failed to retrieve the form test page. (Status code: %s)' % response.status_code)
207+       self.assert_(response.context['form'].errors == {}, 'There was a problem detected in the view: %s' % response.context['form'].errors)
208+
209+       curVals2 = response.form_data('charFld')
210+               
211+        self.assert_(curVals == curVals2, 'The test client did not return the same data after submission.')
212+
213+       curVals2['MultiChoiceFld'] = ['pt', 'es']
214+        response = self.client.post('/test_client_regress/form_processing/', curVals2)
215+        self.failUnlessEqual(response.status_code, 200, 'Failed to retrieve the form test page. (Status code: %s)' % response.status_code)
216+       self.assert_(response.context['form'].errors == {}, 'There was a problem detected in the view: %s' % response.context['form'].errors)
217+
218+       curVals3 = response.form_data('rChoiceFld')
219+       self.assert_(curVals != curVals3 and curVals3 == curVals2, 'The test client did not properly return the modification data after submission.')
220Index: tests/regressiontests/test_client_regress/urls.py
221===================================================================
222--- tests/regressiontests/test_client_regress/urls.py   (revision 13063)
223+++ tests/regressiontests/test_client_regress/urls.py   (working copy)
224@@ -24,4 +24,5 @@
225     (r'^request_methods/$', views.request_methods_view),
226     (r'^check_unicode/$', views.return_unicode),
227     (r'^parse_unicode_json/$', views.return_json_file),
228+    (r'^form_processing/$', views.test_form_proc),
229 )
230Index: tests/regressiontests/test_client_regress/views.py
231===================================================================
232--- tests/regressiontests/test_client_regress/views.py  (revision 13063)
233+++ tests/regressiontests/test_client_regress/views.py  (working copy)
234@@ -86,3 +86,14 @@
235                             mimetype='application/json; charset=' + charset)
236     response['Content-Disposition'] = 'attachment; filename=testfile.json'
237     return response
238+
239+def test_form_proc(request):
240+    "A view that captures and returns the form fields processed by the test client"       
241+    from forms import TestFormProcForm
242+       
243+    if request.method == 'POST':
244+        form = TestFormProcForm(request.POST)       
245+        form.is_valid()       
246+    else:
247+        form = TestFormProcForm()       
248+    return render_to_response('form_view.html', {'form': form})
249Index: tests/templates/form_view.html
250===================================================================
251--- tests/templates/form_view.html      (revision 13063)
252+++ tests/templates/form_view.html      (working copy)
253@@ -2,7 +2,7 @@
254 {% block title %}Submit data{% endblock %}
255 {% block content %}
256 <h1>{{ message }}</h1>
257-<form method='post' action='.'>
258+<form method='post' action='.' name="frmTest">
259 {% if form.errors %}
260 <p class='warning'>Please correct the errors below:</p>
261 {% endif %}