diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py
index 88867d5..014c819 100644
a
|
b
|
class LoginURLSettings(AuthViewsTestCase):
|
265 | 265 | querystring = QueryDict('', mutable=True) |
266 | 266 | querystring['next'] = '/login_required/' |
267 | 267 | self.assertEqual(login_required_url, |
268 | | 'http://testserver%s?%s' % (login_url, querystring.urlencode())) |
| 268 | 'http://testserver%s?%s' % (login_url, querystring.urlencode('/'))) |
269 | 269 | |
270 | 270 | def test_remote_login_url(self): |
271 | 271 | login_url = 'http://remote.example.com/login' |
… |
… |
class LoginURLSettings(AuthViewsTestCase):
|
273 | 273 | querystring = QueryDict('', mutable=True) |
274 | 274 | querystring['next'] = 'http://testserver/login_required/' |
275 | 275 | self.assertEqual(login_required_url, |
276 | | '%s?%s' % (login_url, querystring.urlencode())) |
| 276 | '%s?%s' % (login_url, querystring.urlencode('/'))) |
277 | 277 | |
278 | 278 | def test_https_login_url(self): |
279 | 279 | login_url = 'https:///login/' |
… |
… |
class LoginURLSettings(AuthViewsTestCase):
|
281 | 281 | querystring = QueryDict('', mutable=True) |
282 | 282 | querystring['next'] = 'http://testserver/login_required/' |
283 | 283 | self.assertEqual(login_required_url, |
284 | | '%s?%s' % (login_url, querystring.urlencode())) |
| 284 | '%s?%s' % (login_url, querystring.urlencode('/'))) |
285 | 285 | |
286 | 286 | def test_login_url_with_querystring(self): |
287 | 287 | login_url = '/login/?pretty=1' |
… |
… |
class LoginURLSettings(AuthViewsTestCase):
|
289 | 289 | querystring = QueryDict('pretty=1', mutable=True) |
290 | 290 | querystring['next'] = '/login_required/' |
291 | 291 | self.assertEqual(login_required_url, 'http://testserver/login/?%s' % |
292 | | querystring.urlencode()) |
| 292 | querystring.urlencode('/')) |
293 | 293 | |
294 | 294 | def test_remote_login_url_with_next_querystring(self): |
295 | 295 | login_url = 'http://remote.example.com/login/' |
… |
… |
class LoginURLSettings(AuthViewsTestCase):
|
298 | 298 | querystring = QueryDict('', mutable=True) |
299 | 299 | querystring['next'] = 'http://testserver/login_required/' |
300 | 300 | self.assertEqual(login_required_url, '%s?%s' % (login_url, |
301 | | querystring.urlencode())) |
302 | | |
| 301 | querystring.urlencode('/'))) |
| 302 | |
| 303 | |
303 | 304 | class LogoutTest(AuthViewsTestCase): |
304 | 305 | urls = 'django.contrib.auth.tests.urls' |
305 | 306 | |
diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
index d1f0e06..06bf8f4 100644
a
|
b
|
def redirect_to_login(next, login_url=None,
|
108 | 108 | if redirect_field_name: |
109 | 109 | querystring = QueryDict(login_url_parts[4], mutable=True) |
110 | 110 | querystring[redirect_field_name] = next |
111 | | login_url_parts[4] = querystring.urlencode() |
| 111 | login_url_parts[4] = querystring.urlencode(safe='/') |
112 | 112 | |
113 | 113 | return HttpResponseRedirect(urlparse.urlunparse(login_url_parts)) |
114 | 114 | |
diff --git a/django/http/__init__.py b/django/http/__init__.py
index 42027f0..6526c63 100644
a
|
b
|
import os
|
3 | 3 | import re |
4 | 4 | import time |
5 | 5 | from pprint import pformat |
6 | | from urllib import urlencode |
| 6 | from urllib import urlencode, quote |
7 | 7 | from urlparse import urljoin |
8 | 8 | try: |
9 | 9 | from cStringIO import StringIO |
… |
… |
class QueryDict(MultiValueDict):
|
363 | 363 | """Returns a mutable copy of this object.""" |
364 | 364 | return self.__deepcopy__({}) |
365 | 365 | |
366 | | def urlencode(self): |
| 366 | def urlencode(self, safe=None): |
| 367 | """ |
| 368 | Returns an encoded string of all query string arguments. |
| 369 | |
| 370 | :arg safe: Used to specify characters which do not require quoting, e.g.:: |
| 371 | |
| 372 | >>> q = QueryDict('', mutable=True) |
| 373 | >>> q['next'] = '/a&b/' |
| 374 | >>> q.urlencode() |
| 375 | 'next=%2Fa%26b%2F' |
| 376 | >>> q.urlencode(safe='/') |
| 377 | 'next=/a%26b/' |
| 378 | |
| 379 | """ |
367 | 380 | output = [] |
| 381 | if safe: |
| 382 | encode = lambda k, v: u'%s=%s' % ((quote(k, safe), quote(v, safe))) |
| 383 | else: |
| 384 | encode = lambda k, v: urlencode({k: v}) |
368 | 385 | for k, list_ in self.lists(): |
369 | 386 | k = smart_str(k, self.encoding) |
370 | | output.extend([urlencode({k: smart_str(v, self.encoding)}) for v in list_]) |
| 387 | output.extend([encode(k, smart_str(v, self.encoding)) for v in list_]) |
371 | 388 | return '&'.join(output) |
372 | 389 | |
373 | 390 | class CompatCookie(SimpleCookie): |
diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
index cc89229..036cb8d 100644
a
|
b
|
In addition, ``QueryDict`` has the following methods:
|
430 | 430 | Like :meth:`items()`, except it includes all values, as a list, for each |
431 | 431 | member of the dictionary. For example:: |
432 | 432 | |
433 | | >>> q = QueryDict('a=1&a=2&a=3') |
434 | | >>> q.lists() |
435 | | [(u'a', [u'1', u'2', u'3'])] |
| 433 | >>> q = QueryDict('a=1&a=2&a=3') |
| 434 | >>> q.lists() |
| 435 | [(u'a', [u'1', u'2', u'3'])] |
436 | 436 | |
437 | | .. method:: QueryDict.urlencode() |
| 437 | .. method:: QueryDict.urlencode([safe]) |
438 | 438 | |
439 | | Returns a string of the data in query-string format. |
440 | | Example: ``"a=2&b=3&b=5"``. |
| 439 | Returns a string of the data in query-string format. Example:: |
| 440 | |
| 441 | >>> q = QueryDict('a=2&b=3&b=5') |
| 442 | >>> q.urlencode() |
| 443 | 'a=2&b=3&b=5' |
| 444 | |
| 445 | .. versionadded:: 1.3 |
| 446 | |
| 447 | Optionally, urlencode can be passed characters which |
| 448 | do not require encoding. For example:: |
| 449 | |
| 450 | >>> q = QueryDict('/a&b/') |
| 451 | >>> q.urlencode(safe='/') |
| 452 | 'next=/a%26b/' |
441 | 453 | |
442 | 454 | HttpResponse objects |
443 | 455 | ==================== |
diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py
index 6d45252..65b06a1 100644
a
|
b
|
class QueryDictTests(unittest.TestCase):
|
71 | 71 | |
72 | 72 | self.assertEqual(q.urlencode(), 'foo=bar') |
73 | 73 | |
| 74 | def test_urlencode(self): |
| 75 | q = QueryDict('', mutable=True) |
| 76 | q['next'] = '/a&b/' |
| 77 | self.assertEqual(q.urlencode(), 'next=%2Fa%26b%2F') |
| 78 | self.assertEqual(q.urlencode(safe='/'), 'next=/a%26b/') |
| 79 | q = QueryDict('', mutable=True) |
| 80 | q['next'] = u'/t\xebst&key/' |
| 81 | self.assertEqual(q.urlencode(), 'next=%2Ft%C3%ABst%26key%2F') |
| 82 | self.assertEqual(q.urlencode(safe='/'), 'next=/t%C3%ABst%26key/') |
| 83 | |
74 | 84 | def test_mutable_copy(self): |
75 | 85 | """A copy of a QueryDict is mutable.""" |
76 | 86 | q = QueryDict('').copy() |