Skip to content

Loading…

Redirect after Login: Specify list of domains which are safe to redirect... #724

Closed
wants to merge 12 commits into from

10 participants

@cato-

... to

aaugustin and others added some commits
@aaugustin aaugustin Accepted None in tzname().
This is required by the tzinfo API, see Python's docs.

Also made _get_timezone_name deterministic.
5a3d949
@ramiro ramiro Mention backward relationships in aggregate docs.
Thanks Anssi and Marc Tamlyn for reviewing.

Fixes #19803.
0560bfb
@aaugustin aaugustin Merge pull request #719 from JonLoy/ticket_19808
Fixed #19808 -- Typo in example
c4841b3
@akaariai akaariai Removed try-except in django.db.close_connection()
The reason was that the except clause needed to remove a connection
from the django.db.connections dict, but other parts of Django do not
expect this to happen. In addition the except clause was silently
swallowing the exception messages.

Refs #19707, special thanks to Carl Meyer for pointing out that this
approach should be taken.
fafee74
@carljm carljm Fix admindocs on Python 3, where None cannot be sorted with strings.
This fixes two tests in admin_views which were failing on Python 3, but only if
the tests were run with docutils installed.
3a002db
@hirokiky hirokiky Fixed #18558 -- Added url property to HttpResponseRedirect*
Thanks coolRR for the report.
e94f405
@claudep claudep Fixed #19693 -- Made truncatewords_html handle self-closing tags
Thanks sneawo for the report and Jonathan Loy for the patch.
ac4faa6
@claudep claudep Fixed #8404 -- Isolated auth password-related tests from custom templ…
…ates
142ec8b
@aaugustin

text isn't defined here.

Django member

Doh... Sorry :-(

claudep and others added some commits
@claudep claudep Fixed a misnamed variable introduced in commit 142ec8b
Refs #8404.
f1029b3
@mvantellingen mvantellingen Fixed #19819 - Improved template filter errors handling.
Wrap the Parser.compile_filter method call with a try/except and call the
newly added Parser.compile_filter_error(). Overwrite this method in the
DebugParser to throw the correct error.

Since this error was otherwise catched by the compile_function try/except
block the debugger highlighted the wrong line.
138de53
@claudep claudep Fixed #19823 -- Fixed memcached code example in cache docs 668d0b8
@cato- cato- Redirect after Login: Specify list of domains which are safe to redir…
…ect to
c4d94a5
@apollo13
Django member

Is there any ticket for this change? With the new security release it might make sense to reuse allowed_hosts for this?

@cato-

There is no ticket. Using the ALLOWED_HOSTS setting with the current default (['*']) would render the check useless

@timgraham
Django member

Recommend creating a ticket so this doesn't get lost.

@timgraham
Django member

Closing in absence of a ticket.

@timgraham timgraham closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 11, 2013
  1. @aaugustin

    Accepted None in tzname().

    aaugustin committed
    This is required by the tzinfo API, see Python's docs.
    
    Also made _get_timezone_name deterministic.
Commits on Feb 12, 2013
  1. @ramiro

    Mention backward relationships in aggregate docs.

    ramiro committed
    Thanks Anssi and Marc Tamlyn for reviewing.
    
    Fixes #19803.
  2. @aaugustin

    Merge pull request #719 from JonLoy/ticket_19808

    aaugustin committed
    Fixed #19808 -- Typo in example
  3. @akaariai

    Removed try-except in django.db.close_connection()

    akaariai committed
    The reason was that the except clause needed to remove a connection
    from the django.db.connections dict, but other parts of Django do not
    expect this to happen. In addition the except clause was silently
    swallowing the exception messages.
    
    Refs #19707, special thanks to Carl Meyer for pointing out that this
    approach should be taken.
Commits on Feb 13, 2013
  1. @carljm

    Fix admindocs on Python 3, where None cannot be sorted with strings.

    carljm committed
    This fixes two tests in admin_views which were failing on Python 3, but only if
    the tests were run with docutils installed.
  2. @hirokiky @claudep

    Fixed #18558 -- Added url property to HttpResponseRedirect*

    hirokiky committed with claudep
    Thanks coolRR for the report.
  3. @claudep

    Fixed #19693 -- Made truncatewords_html handle self-closing tags

    claudep committed
    Thanks sneawo for the report and Jonathan Loy for the patch.
  4. @claudep
Commits on Feb 14, 2013
  1. @claudep
  2. @mvantellingen @aaugustin

    Fixed #19819 - Improved template filter errors handling.

    mvantellingen committed with aaugustin
    Wrap the Parser.compile_filter method call with a try/except and call the
    newly added Parser.compile_filter_error(). Overwrite this method in the
    DebugParser to throw the correct error.
    
    Since this error was otherwise catched by the compile_function try/except
    block the debugger highlighted the wrong line.
  3. @claudep
  4. @cato-
This page is out of date. Refresh to see the latest.
View
1 AUTHORS
@@ -306,6 +306,7 @@ answer newbie questions, and generally made Django that much better:
Garth Kidd <http://www.deadlybloodyserious.com/>
kilian <kilian.cavalotti@lip6.fr>
Sune Kirkeby <http://ibofobi.dk/>
+ Hiroki Kiyohara <hirokiky@gmail.com>
Bastian Kleineidam <calvin@debian.org>
Cameron Knight (ckknight)
Nena Kojadin <nena@kiberpipa.org>
View
4 django/contrib/admindocs/templates/admin_doc/template_filter_index.html
@@ -22,7 +22,7 @@
<h2>{% firstof library.grouper "Built-in filters" %}</h2>
{% if library.grouper %}<p class="small quiet">To use these filters, put <code>{% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %}</code> in your template before using the filter.</p><hr />{% endif %}
{% for filter in library.list|dictsort:"name" %}
- <h3 id="{{ library.grouper|default_if_none:"built_in" }}-{{ filter.name }}">{{ filter.name }}</h3>
+ <h3 id="{{ library.grouper|default:"built_in" }}-{{ filter.name }}">{{ filter.name }}</h3>
{{ filter.title }}
{{ filter.body }}
{% if not forloop.last %}<hr />{% endif %}
@@ -43,7 +43,7 @@ <h3 id="{{ library.grouper|default_if_none:"built_in" }}-{{ filter.name }}">{{ f
<h2>{% firstof library.grouper "Built-in filters" %}</h2>
<ul>
{% for filter in library.list|dictsort:"name" %}
- <li><a href="#{{ library.grouper|default_if_none:"built_in" }}-{{ filter.name }}">{{ filter.name }}</a></li>
+ <li><a href="#{{ library.grouper|default:"built_in" }}-{{ filter.name }}">{{ filter.name }}</a></li>
{% endfor %}
</ul>
</div>
View
4 django/contrib/admindocs/templates/admin_doc/template_tag_index.html
@@ -22,7 +22,7 @@
<h2>{% firstof library.grouper "Built-in tags" %}</h2>
{% if library.grouper %}<p class="small quiet">To use these tags, put <code>{% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %}</code> in your template before using the tag.</p><hr />{% endif %}
{% for tag in library.list|dictsort:"name" %}
- <h3 id="{{ library.grouper|default_if_none:"built_in" }}-{{ tag.name }}">{{ tag.name }}</h3>
+ <h3 id="{{ library.grouper|default:"built_in" }}-{{ tag.name }}">{{ tag.name }}</h3>
<h4>{{ tag.title|striptags }}</h4>
{{ tag.body }}
{% if not forloop.last %}<hr />{% endif %}
@@ -43,7 +43,7 @@ <h3 id="{{ library.grouper|default_if_none:"built_in" }}-{{ tag.name }}">{{ tag.
<h2>{% firstof library.grouper "Built-in tags" %}</h2>
<ul>
{% for tag in library.list|dictsort:"name" %}
- <li><a href="#{{ library.grouper|default_if_none:"built_in" }}-{{ tag.name }}">{{ tag.name }}</a></li>
+ <li><a href="#{{ library.grouper|default:"built_in" }}-{{ tag.name }}">{{ tag.name }}</a></li>
{% endfor %}
</ul>
</div>
View
4 django/contrib/admindocs/views.py
@@ -62,7 +62,7 @@ def template_tag_index(request):
for key in metadata:
metadata[key] = utils.parse_rst(metadata[key], 'tag', _('tag:') + tag_name)
if library in template.builtins:
- tag_library = None
+ tag_library = ''
else:
tag_library = module_name.split('.')[-1]
tags.append({
@@ -97,7 +97,7 @@ def template_filter_index(request):
for key in metadata:
metadata[key] = utils.parse_rst(metadata[key], 'filter', _('filter:') + filter_name)
if library in template.builtins:
- tag_library = None
+ tag_library = ''
else:
tag_library = module_name.split('.')[-1]
filters.append({
View
2 django/contrib/auth/tests/decorators.py
@@ -34,7 +34,7 @@ def testLoginRequired(self, view_url='/login_required/', login_url='/login/'):
"""
response = self.client.get(view_url)
self.assertEqual(response.status_code, 302)
- self.assertTrue(login_url in response['Location'])
+ self.assertTrue(login_url in response.url)
self.login()
response = self.client.get(view_url)
self.assertEqual(response.status_code, 200)
View
45 django/contrib/auth/tests/views.py
@@ -1,3 +1,4 @@
+import itertools
import os
import re
@@ -46,11 +47,13 @@ def login(self, password='password'):
'password': password,
})
self.assertEqual(response.status_code, 302)
- self.assertTrue(response['Location'].endswith(settings.LOGIN_REDIRECT_URL))
+ self.assertTrue(response.url.endswith(settings.LOGIN_REDIRECT_URL))
self.assertTrue(SESSION_KEY in self.client.session)
- def assertContainsEscaped(self, response, text, **kwargs):
- return self.assertContains(response, escape(force_text(text)), **kwargs)
+ def assertFormError(self, response, error):
+ """Assert that error is found in response.context['form'] errors"""
+ form_errors = list(itertools.chain(*response.context['form'].errors.values()))
+ self.assertIn(force_text(error), form_errors)
@skipIfCustomUser
@@ -87,7 +90,7 @@ def test_email_not_found(self):
response = self.client.get('/password_reset/')
self.assertEqual(response.status_code, 200)
response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
- self.assertContainsEscaped(response, PasswordResetForm.error_messages['unknown'])
+ self.assertFormError(response, PasswordResetForm.error_messages['unknown'])
self.assertEqual(len(mail.outbox), 0)
def test_email_found(self):
@@ -214,7 +217,7 @@ def test_confirm_different_passwords(self):
url, path = self._test_confirm_start()
response = self.client.post(path, {'new_password1': 'anewpassword',
'new_password2': 'x'})
- self.assertContainsEscaped(response, SetPasswordForm.error_messages['password_mismatch'])
+ self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
@override_settings(AUTH_USER_MODEL='auth.CustomUser')
@@ -248,7 +251,7 @@ def fail_login(self, password='password'):
'username': 'testclient',
'password': password,
})
- self.assertContainsEscaped(response, AuthenticationForm.error_messages['invalid_login'] % {
+ self.assertFormError(response, AuthenticationForm.error_messages['invalid_login'] % {
'username': User._meta.get_field('username').verbose_name
})
@@ -262,7 +265,7 @@ def test_password_change_fails_with_invalid_old_password(self):
'new_password1': 'password1',
'new_password2': 'password1',
})
- self.assertContainsEscaped(response, PasswordChangeForm.error_messages['password_incorrect'])
+ self.assertFormError(response, PasswordChangeForm.error_messages['password_incorrect'])
def test_password_change_fails_with_mismatched_passwords(self):
self.login()
@@ -271,7 +274,7 @@ def test_password_change_fails_with_mismatched_passwords(self):
'new_password1': 'password1',
'new_password2': 'donuts',
})
- self.assertContainsEscaped(response, SetPasswordForm.error_messages['password_mismatch'])
+ self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
def test_password_change_succeeds(self):
self.login()
@@ -281,7 +284,7 @@ def test_password_change_succeeds(self):
'new_password2': 'password1',
})
self.assertEqual(response.status_code, 302)
- self.assertTrue(response['Location'].endswith('/password_change/done/'))
+ self.assertTrue(response.url.endswith('/password_change/done/'))
self.fail_login()
self.login(password='password1')
@@ -293,13 +296,13 @@ def test_password_change_done_succeeds(self):
'new_password2': 'password1',
})
self.assertEqual(response.status_code, 302)
- self.assertTrue(response['Location'].endswith('/password_change/done/'))
+ self.assertTrue(response.url.endswith('/password_change/done/'))
def test_password_change_done_fails(self):
with self.settings(LOGIN_URL='/login/'):
response = self.client.get('/password_change/done/')
self.assertEqual(response.status_code, 302)
- self.assertTrue(response['Location'].endswith('/login/?next=/password_change/done/'))
+ self.assertTrue(response.url.endswith('/login/?next=/password_change/done/'))
@skipIfCustomUser
@@ -336,7 +339,7 @@ def test_security_check(self, password='password'):
'password': password,
})
self.assertEqual(response.status_code, 302)
- self.assertFalse(bad_url in response['Location'],
+ self.assertFalse(bad_url in response.url,
"%s should be blocked" % bad_url)
# These URLs *should* still pass the security check
@@ -357,7 +360,7 @@ def test_security_check(self, password='password'):
'password': password,
})
self.assertEqual(response.status_code, 302)
- self.assertTrue(good_url in response['Location'],
+ self.assertTrue(good_url in response.url,
"%s should be allowed" % good_url)
@@ -376,7 +379,7 @@ def get_login_required_url(self, login_url):
settings.LOGIN_URL = login_url
response = self.client.get('/login_required/')
self.assertEqual(response.status_code, 302)
- return response['Location']
+ return response.url
def test_standard_login_url(self):
login_url = '/login/'
@@ -444,11 +447,11 @@ def test_logout_with_overridden_redirect_url(self):
self.login()
response = self.client.get('/logout/next_page/')
self.assertEqual(response.status_code, 302)
- self.assertTrue(response['Location'].endswith('/somewhere/'))
+ self.assertTrue(response.url.endswith('/somewhere/'))
response = self.client.get('/logout/next_page/?next=/login/')
self.assertEqual(response.status_code, 302)
- self.assertTrue(response['Location'].endswith('/login/'))
+ self.assertTrue(response.url.endswith('/login/'))
self.confirm_logged_out()
@@ -457,7 +460,7 @@ def test_logout_with_next_page_specified(self):
self.login()
response = self.client.get('/logout/next_page/')
self.assertEqual(response.status_code, 302)
- self.assertTrue(response['Location'].endswith('/somewhere/'))
+ self.assertTrue(response.url.endswith('/somewhere/'))
self.confirm_logged_out()
def test_logout_with_redirect_argument(self):
@@ -465,7 +468,7 @@ def test_logout_with_redirect_argument(self):
self.login()
response = self.client.get('/logout/?next=/login/')
self.assertEqual(response.status_code, 302)
- self.assertTrue(response['Location'].endswith('/login/'))
+ self.assertTrue(response.url.endswith('/login/'))
self.confirm_logged_out()
def test_logout_with_custom_redirect_argument(self):
@@ -473,7 +476,7 @@ def test_logout_with_custom_redirect_argument(self):
self.login()
response = self.client.get('/logout/custom_query/?follow=/somewhere/')
self.assertEqual(response.status_code, 302)
- self.assertTrue(response['Location'].endswith('/somewhere/'))
+ self.assertTrue(response.url.endswith('/somewhere/'))
self.confirm_logged_out()
def test_security_check(self, password='password'):
@@ -492,7 +495,7 @@ def test_security_check(self, password='password'):
self.login()
response = self.client.get(nasty_url)
self.assertEqual(response.status_code, 302)
- self.assertFalse(bad_url in response['Location'],
+ self.assertFalse(bad_url in response.url,
"%s should be blocked" % bad_url)
self.confirm_logged_out()
@@ -512,6 +515,6 @@ def test_security_check(self, password='password'):
self.login()
response = self.client.get(safe_url)
self.assertEqual(response.status_code, 302)
- self.assertTrue(good_url in response['Location'],
+ self.assertTrue(good_url in response.url,
"%s should be allowed" % good_url)
self.confirm_logged_out()
View
8 django/contrib/auth/views.py
@@ -28,7 +28,8 @@
def login(request, template_name='registration/login.html',
redirect_field_name=REDIRECT_FIELD_NAME,
authentication_form=AuthenticationForm,
- current_app=None, extra_context=None):
+ current_app=None, extra_context=None,
+ redirect_hosts=None):
"""
Displays the login form and handles the login action.
"""
@@ -39,7 +40,10 @@ def login(request, template_name='registration/login.html',
if form.is_valid():
# Ensure the user-originating redirection url is safe.
- if not is_safe_url(url=redirect_to, host=request.get_host()):
+ host_list = [request.get_host(),]
+ if redirect_hosts:
+ host_list.extend(redirect_hosts)
+ if not is_safe_url(url=redirect_to, host=redirect_hosts):
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
# Okay, security check complete. Log the user in.
View
36 django/contrib/formtools/tests/wizard/namedwizardtests/tests.py
@@ -21,7 +21,7 @@ def setUp(self):
def test_initial_call(self):
response = self.client.get(reverse('%s_start' % self.wizard_urlname))
self.assertEqual(response.status_code, 302)
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
wizard = response.context['wizard']
self.assertEqual(wizard['steps'].current, 'form1')
@@ -40,7 +40,7 @@ def test_initial_call_with_params(self):
self.assertEqual(response.status_code, 302)
# Test for proper redirect GET parameters
- location = response['Location']
+ location = response.url
self.assertNotEqual(location.find('?'), -1)
querydict = QueryDict(location[location.find('?') + 1:])
self.assertEqual(dict(querydict.items()), get_params)
@@ -60,7 +60,7 @@ def test_form_post_success(self):
response = self.client.post(
reverse(self.wizard_urlname, kwargs={'step': 'form1'}),
self.wizard_step_data[0])
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
wizard = response.context['wizard']
@@ -79,7 +79,7 @@ def test_form_stepback(self):
response = self.client.post(
reverse(self.wizard_urlname, kwargs={'step': 'form1'}),
self.wizard_step_data[0])
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['wizard']['steps'].current, 'form2')
@@ -88,7 +88,7 @@ def test_form_stepback(self):
reverse(self.wizard_urlname, kwargs={
'step': response.context['wizard']['steps'].current
}), {'wizard_goto_step': response.context['wizard']['steps'].prev})
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['wizard']['steps'].current, 'form1')
@@ -116,7 +116,7 @@ def test_form_finish(self):
reverse(self.wizard_urlname,
kwargs={'step': response.context['wizard']['steps'].current}),
self.wizard_step_data[0])
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['wizard']['steps'].current, 'form2')
@@ -128,7 +128,7 @@ def test_form_finish(self):
reverse(self.wizard_urlname,
kwargs={'step': response.context['wizard']['steps'].current}),
post_data)
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['wizard']['steps'].current, 'form3')
@@ -137,7 +137,7 @@ def test_form_finish(self):
reverse(self.wizard_urlname,
kwargs={'step': response.context['wizard']['steps'].current}),
self.wizard_step_data[2])
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['wizard']['steps'].current, 'form4')
@@ -146,7 +146,7 @@ def test_form_finish(self):
reverse(self.wizard_urlname,
kwargs={'step': response.context['wizard']['steps'].current}),
self.wizard_step_data[3])
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
all_data = response.context['form_list']
@@ -169,7 +169,7 @@ def test_cleaned_data(self):
reverse(self.wizard_urlname,
kwargs={'step': response.context['wizard']['steps'].current}),
self.wizard_step_data[0])
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
post_data = self.wizard_step_data[1]
@@ -178,7 +178,7 @@ def test_cleaned_data(self):
reverse(self.wizard_urlname,
kwargs={'step': response.context['wizard']['steps'].current}),
post_data)
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
step2_url = reverse(self.wizard_urlname, kwargs={'step': 'form2'})
@@ -194,14 +194,14 @@ def test_cleaned_data(self):
reverse(self.wizard_urlname,
kwargs={'step': response.context['wizard']['steps'].current}),
self.wizard_step_data[2])
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse(self.wizard_urlname,
kwargs={'step': response.context['wizard']['steps'].current}),
self.wizard_step_data[3])
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
all_data = response.context['all_cleaned_data']
@@ -227,7 +227,7 @@ def test_manipulated_data(self):
reverse(self.wizard_urlname,
kwargs={'step': response.context['wizard']['steps'].current}),
self.wizard_step_data[0])
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
post_data = self.wizard_step_data[1]
@@ -237,14 +237,14 @@ def test_manipulated_data(self):
reverse(self.wizard_urlname,
kwargs={'step': response.context['wizard']['steps'].current}),
post_data)
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse(self.wizard_urlname,
kwargs={'step': response.context['wizard']['steps'].current}),
self.wizard_step_data[2])
- loc = response['Location']
+ loc = response.url
response = self.client.get(loc)
self.assertEqual(response.status_code, 200, loc)
@@ -263,7 +263,7 @@ def test_form_reset(self):
response = self.client.post(
reverse(self.wizard_urlname, kwargs={'step': 'form1'}),
self.wizard_step_data[0])
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['wizard']['steps'].current, 'form2')
@@ -271,7 +271,7 @@ def test_form_reset(self):
'%s?reset=1' % reverse('%s_start' % self.wizard_urlname))
self.assertEqual(response.status_code, 302)
- response = self.client.get(response['Location'])
+ response = self.client.get(response.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['wizard']['steps'].current, 'form1')
View
13 django/db/__init__.py
@@ -45,14 +45,11 @@ def close_connection(**kwargs):
# Avoid circular imports
from django.db import transaction
for conn in connections:
- try:
- transaction.abort(conn)
- connections[conn].close()
- except Exception:
- # The connection's state is unknown, so it has to be
- # abandoned. This could happen for example if the network
- # connection has a failure.
- del connections[conn]
+ # If an error happens here the connection will be left in broken
+ # state. Once a good db connection is again available, the
+ # connection state will be cleaned up.
+ transaction.abort(conn)
+ connections[conn].close()
signals.request_finished.connect(close_connection)
# Register an event that resets connection.queries
View
3 django/db/utils.py
@@ -99,9 +99,6 @@ def __getitem__(self, alias):
def __setitem__(self, key, value):
setattr(self._connections, key, value)
- def __delitem__(self, key):
- delattr(self._connections, key)
-
def __iter__(self):
return iter(self.databases)
View
2 django/http/response.py
@@ -392,6 +392,8 @@ def __init__(self, redirect_to, *args, **kwargs):
super(HttpResponseRedirectBase, self).__init__(*args, **kwargs)
self['Location'] = iri_to_uri(redirect_to)
+ url = property(lambda self: self['Location'])
+
class HttpResponseRedirect(HttpResponseRedirectBase):
status_code = 302
View
9 django/template/base.py
@@ -250,7 +250,11 @@ def parse(self, parse_until=None):
elif token.token_type == 1: # TOKEN_VAR
if not token.contents:
self.empty_variable(token)
- filter_expression = self.compile_filter(token.contents)
+ try:
+ filter_expression = self.compile_filter(token.contents)
+ except TemplateSyntaxError as e:
+ if not self.compile_filter_error(token, e):
+ raise
var_node = self.create_variable_node(filter_expression)
self.extend_nodelist(nodelist, var_node, token)
elif token.token_type == 2: # TOKEN_BLOCK
@@ -330,6 +334,9 @@ def invalid_block_tag(self, token, command, parse_until=None):
def unclosed_block_tag(self, parse_until):
raise self.error(None, "Unclosed tags: %s " % ', '.join(parse_until))
+ def compile_filter_error(self, token, e):
+ pass
+
def compile_function_error(self, token, e):
pass
View
4 django/template/debug.py
@@ -64,6 +64,10 @@ def unclosed_block_tag(self, parse_until):
msg = "Unclosed tag '%s'. Looking for one of: %s " % (command, ', '.join(parse_until))
raise self.source_error(source, msg)
+ def compile_filter_error(self, token, e):
+ if not hasattr(e, 'django_template_source'):
+ e.django_template_source = token.source
+
def compile_function_error(self, token, e):
if not hasattr(e, 'django_template_source'):
e.django_template_source = token.source
View
2 django/test/client.py
@@ -580,7 +580,7 @@ def _handle_redirects(self, response, **extra):
response.redirect_chain = []
while response.status_code in (301, 302, 303, 307):
- url = response['Location']
+ url = response.url
redirect_chain = response.redirect_chain
redirect_chain.append((url, response.status_code))
View
2 django/test/testcases.py
@@ -601,7 +601,7 @@ def assertRedirects(self, response, expected_url, status_code=302,
" code was %d (expected %d)" %
(response.status_code, status_code))
- url = response['Location']
+ url = response.url
scheme, netloc, path, query, fragment = urlsplit(url)
redirect_response = response.client.get(path, QueryDict(query))
View
9 django/utils/http.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
import calendar
+import collections
import datetime
import re
import sys
@@ -238,4 +239,10 @@ def is_safe_url(url, host=None):
if not url:
return False
netloc = urllib_parse.urlparse(url)[1]
- return not netloc or netloc == host
+ if not netloc:
+ return True
+
+ if isinstance(host, collections.Iterable):
+ return netloc in host
+ else:
+ return netloc == host
View
2 django/utils/text.py
@@ -24,7 +24,7 @@
# Set up regular expressions
re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U|re.S)
-re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>', re.S)
+re_tag = re.compile(r'<(/)?([^ ]+?)(?:(\s*/)| .*?)?>', re.S)
def wrap(text, width):
View
6 django/utils/timezone.py
@@ -80,7 +80,8 @@ def dst(self, dt):
return ZERO
def tzname(self, dt):
- return _time.tzname[self._isdst(dt)]
+ is_dst = False if dt is None else self._isdst(dt)
+ return _time.tzname[is_dst]
def _isdst(self, dt):
tt = (dt.year, dt.month, dt.day,
@@ -145,8 +146,7 @@ def _get_timezone_name(timezone):
return timezone.zone
except AttributeError:
# for regular tzinfo objects
- local_now = datetime.now(timezone)
- return timezone.tzname(local_now)
+ return timezone.tzname(None)
# Timezone selection functions.
View
4 django/utils/tzinfo.py
@@ -71,9 +71,9 @@ def dst(self, dt):
return timedelta(0)
def tzname(self, dt):
+ is_dst = False if dt is None else self._isdst(dt)
try:
- return force_text(time.tzname[self._isdst(dt)],
- DEFAULT_LOCALE_ENCODING)
+ return force_text(time.tzname[is_dst], DEFAULT_LOCALE_ENCODING)
except UnicodeDecodeError:
return None
View
7 docs/ref/request-response.txt
@@ -746,6 +746,13 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in
domain (e.g. ``'/search/'``). See :class:`HttpResponse` for other optional
constructor arguments. Note that this returns an HTTP status code 302.
+ .. attribute:: HttpResponseRedirect.url
+
+ .. versionadded:: 1.6
+
+ This read-only attribute represents the URL the response will redirect
+ to (equivalent to the ``Location`` response header).
+
.. class:: HttpResponsePermanentRedirect
Like :class:`HttpResponseRedirect`, but it returns a permanent redirect
View
4 docs/releases/1.6.txt
@@ -68,6 +68,10 @@ Minor features
:class:`~django.views.generic.edit.DeletionMixin` is now interpolated with
its ``object``\'s ``__dict__``.
+* :class:`~django.http.HttpResponseRedirect` and
+ :class:`~django.http.HttpResponsePermanentRedirect` now provide an ``url``
+ attribute (equivalent to the URL the response will redirect to).
+
Backwards incompatible changes in 1.6
=====================================
View
7 docs/topics/auth/default.txt
@@ -560,7 +560,7 @@ Most built-in authentication views provide a URL name for easier reference. See
patterns.
-.. function:: login(request, [template_name, redirect_field_name, authentication_form])
+.. function:: login(request, [template_name, redirect_field_name, authentication_form, redirect_hosts])
**URL name:** ``login``
@@ -652,6 +652,11 @@ patterns.
provide a ``get_user`` method which returns the authenticated user object
(this method is only ever called after successful form validation).
+ If you need to redirect the user to an URL on a domain different from the
+ current one, you have to specify which domains are safe to redirect to.
+ You can pass a list of safe domains with to the ``redirect_hosts``
+ parameter.
+
.. _forms documentation: ../forms/
.. _site framework docs: ../sites/
View
2 docs/topics/cache.txt
@@ -137,7 +137,7 @@ on the IP addresses 172.19.26.240 (port 11211), 172.19.26.242 (port 11212), and
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': [
'172.19.26.240:11211',
- '172.19.26.242:11211',
+ '172.19.26.242:11212',
'172.19.26.244:11213',
]
}
View
53 docs/topics/db/aggregation.txt
@@ -21,14 +21,12 @@ used to track the inventory for a series of online bookstores:
class Author(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()
- friends = models.ManyToManyField('self', blank=True)
class Publisher(models.Model):
name = models.CharField(max_length=300)
num_awards = models.IntegerField()
class Book(models.Model):
- isbn = models.CharField(max_length=9)
name = models.CharField(max_length=300)
pages = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
@@ -40,6 +38,7 @@ used to track the inventory for a series of online bookstores:
class Store(models.Model):
name = models.CharField(max_length=300)
books = models.ManyToManyField(Book)
+ registered_users = models.PositiveIntegerField()
Cheat sheet
===========
@@ -64,6 +63,9 @@ In a hurry? Here's how to do common aggregate queries, assuming the models above
>>> Book.objects.all().aggregate(Max('price'))
{'price__max': Decimal('81.20')}
+ # All the following queries involve traversing the Book<->Publisher
+ # many-to-many relationship backward
+
# Each publisher, each with a count of books as a "num_books" attribute.
>>> from django.db.models import Count
>>> pubs = Publisher.objects.annotate(num_books=Count('book'))
@@ -73,7 +75,6 @@ In a hurry? Here's how to do common aggregate queries, assuming the models above
73
# The top 5 publishers, in order by number of books.
- >>> from django.db.models import Count
>>> pubs = Publisher.objects.annotate(num_books=Count('book')).order_by('-num_books')[:5]
>>> pubs[0].num_books
1323
@@ -169,7 +170,7 @@ specify the annotation::
Unlike ``aggregate()``, ``annotate()`` is *not* a terminal clause. The output
of the ``annotate()`` clause is a ``QuerySet``; this ``QuerySet`` can be
modified using any other ``QuerySet`` operation, including ``filter()``,
-``order_by``, or even additional calls to ``annotate()``.
+``order_by()``, or even additional calls to ``annotate()``.
Joins and aggregates
====================
@@ -205,6 +206,50 @@ issue the query::
>>> Store.objects.aggregate(youngest_age=Min('books__authors__age'))
+Following relationships backwards
+---------------------------------
+
+In a way similar to :ref:`lookups-that-span-relationships`, aggregations and
+annotations on fields of models or models that are related to the one you are
+querying can include traversing "reverse" relationships. The lowercase name
+of related models and double-underscores are used here too.
+
+For example, we can ask for all publishers, annotated with their respective
+total book stock counters (note how we use `'book'` to specify the
+Publisher->Book reverse foreign key hop)::
+
+ >>> from django.db.models import Count, Min, Sum, Max, Avg
+ >>> Publisher.objects.annotate(Count('book'))
+
+(Every Publisher in the resulting QuerySet will have an extra attribute called
+``book__count``.)
+
+We can also ask for the oldest book of any of those managed by every publisher::
+
+ >>> Publisher.objects.aggregate(oldest_pubdate=Min('book__pubdate'))
+
+(The resulting dictionary will have a key called ``'oldest_pubdate'``. If no
+such alias was specified, it would be the rather long ``'book__pubdate__min'``.)
+
+This doesn't apply just to foreign keys. It also works with many-to-many
+relations. For example, we can ask for every author, annotated with the total
+number of pages considering all the books he/she has (co-)authored (note how we
+use `'book'` to specify the Author->Book reverse many-to-many hop)::
+
+ >>> Author.objects.annotate(total_pages=Sum('book__pages'))
+
+(Every Author in the resulting QuerySet will have an extra attribute called
+``total_pages``. If no such alias was specified, it would be the rather long
+``book__pages__sum``.)
+
+Or ask for the average rating of all the books written by author(s) we have on
+file::
+
+ >>> Author.objects.aggregate(average_rating=Avg('book__rating'))
+
+(The resulting dictionary will have a key called ``'average__rating'``. If no
+such alias was specified, it would be the rather long ``'book__rating__avg'``.)
+
Aggregations and other QuerySet clauses
=======================================
View
2 tests/regressiontests/admin_views/tests.py
@@ -1641,7 +1641,7 @@ def test_shortcut_view_only_available_to_staff(self):
response = self.client.get(shortcut_url, follow=False)
# Can't use self.assertRedirects() because User.get_absolute_url() is silly.
self.assertEqual(response.status_code, 302)
- self.assertEqual(response['Location'], 'http://example.com/users/super/')
+ self.assertEqual(response.url, 'http://example.com/users/super/')
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
View
22 tests/regressiontests/generic_views/base.py
@@ -329,66 +329,66 @@ def test_permanent_redirect(self):
"Default is a permanent redirect"
response = RedirectView.as_view(url='/bar/')(self.rf.get('/foo/'))
self.assertEqual(response.status_code, 301)
- self.assertEqual(response['Location'], '/bar/')
+ self.assertEqual(response.url, '/bar/')
def test_temporary_redirect(self):
"Permanent redirects are an option"
response = RedirectView.as_view(url='/bar/', permanent=False)(self.rf.get('/foo/'))
self.assertEqual(response.status_code, 302)
- self.assertEqual(response['Location'], '/bar/')
+ self.assertEqual(response.url, '/bar/')
def test_include_args(self):
"GET arguments can be included in the redirected URL"
response = RedirectView.as_view(url='/bar/')(self.rf.get('/foo/'))
self.assertEqual(response.status_code, 301)
- self.assertEqual(response['Location'], '/bar/')
+ self.assertEqual(response.url, '/bar/')
response = RedirectView.as_view(url='/bar/', query_string=True)(self.rf.get('/foo/?pork=spam'))
self.assertEqual(response.status_code, 301)
- self.assertEqual(response['Location'], '/bar/?pork=spam')
+ self.assertEqual(response.url, '/bar/?pork=spam')
def test_include_urlencoded_args(self):
"GET arguments can be URL-encoded when included in the redirected URL"
response = RedirectView.as_view(url='/bar/', query_string=True)(
self.rf.get('/foo/?unicode=%E2%9C%93'))
self.assertEqual(response.status_code, 301)
- self.assertEqual(response['Location'], '/bar/?unicode=%E2%9C%93')
+ self.assertEqual(response.url, '/bar/?unicode=%E2%9C%93')
def test_parameter_substitution(self):
"Redirection URLs can be parameterized"
response = RedirectView.as_view(url='/bar/%(object_id)d/')(self.rf.get('/foo/42/'), object_id=42)
self.assertEqual(response.status_code, 301)
- self.assertEqual(response['Location'], '/bar/42/')
+ self.assertEqual(response.url, '/bar/42/')
def test_redirect_POST(self):
"Default is a permanent redirect"
response = RedirectView.as_view(url='/bar/')(self.rf.post('/foo/'))
self.assertEqual(response.status_code, 301)
- self.assertEqual(response['Location'], '/bar/')
+ self.assertEqual(response.url, '/bar/')
def test_redirect_HEAD(self):
"Default is a permanent redirect"
response = RedirectView.as_view(url='/bar/')(self.rf.head('/foo/'))
self.assertEqual(response.status_code, 301)
- self.assertEqual(response['Location'], '/bar/')
+ self.assertEqual(response.url, '/bar/')
def test_redirect_OPTIONS(self):
"Default is a permanent redirect"
response = RedirectView.as_view(url='/bar/')(self.rf.options('/foo/'))
self.assertEqual(response.status_code, 301)
- self.assertEqual(response['Location'], '/bar/')
+ self.assertEqual(response.url, '/bar/')
def test_redirect_PUT(self):
"Default is a permanent redirect"
response = RedirectView.as_view(url='/bar/')(self.rf.put('/foo/'))
self.assertEqual(response.status_code, 301)
- self.assertEqual(response['Location'], '/bar/')
+ self.assertEqual(response.url, '/bar/')
def test_redirect_DELETE(self):
"Default is a permanent redirect"
response = RedirectView.as_view(url='/bar/')(self.rf.delete('/foo/'))
self.assertEqual(response.status_code, 301)
- self.assertEqual(response['Location'], '/bar/')
+ self.assertEqual(response.url, '/bar/')
def test_redirect_when_meta_contains_no_query_string(self):
"regression for #16705"
View
2 tests/regressiontests/httpwrappers/tests.py
@@ -410,6 +410,8 @@ def test_redirect(self):
content='The resource has temporarily moved',
content_type='text/html')
self.assertContains(response, 'The resource has temporarily moved', status_code=302)
+ # Test that url attribute is right
+ self.assertEqual(response.url, response['Location'])
def test_not_modified(self):
response = HttpResponseNotModified()
View
20 tests/regressiontests/middleware/tests.py
@@ -69,7 +69,7 @@ def test_append_slash_redirect(self):
request = self._get_request('slash')
r = CommonMiddleware().process_request(request)
self.assertEqual(r.status_code, 301)
- self.assertEqual(r['Location'], 'http://testserver/middleware/slash/')
+ self.assertEqual(r.url, 'http://testserver/middleware/slash/')
@override_settings(APPEND_SLASH=True, DEBUG=True)
def test_append_slash_no_redirect_on_POST_in_DEBUG(self):
@@ -101,7 +101,7 @@ def test_append_slash_quoted(self):
r = CommonMiddleware().process_request(request)
self.assertEqual(r.status_code, 301)
self.assertEqual(
- r['Location'],
+ r.url,
'http://testserver/middleware/needsquoting%23/')
@override_settings(APPEND_SLASH=False, PREPEND_WWW=True)
@@ -110,7 +110,7 @@ def test_prepend_www(self):
r = CommonMiddleware().process_request(request)
self.assertEqual(r.status_code, 301)
self.assertEqual(
- r['Location'],
+ r.url,
'http://www.testserver/middleware/path/')
@override_settings(APPEND_SLASH=True, PREPEND_WWW=True)
@@ -118,7 +118,7 @@ def test_prepend_www_append_slash_have_slash(self):
request = self._get_request('slash/')
r = CommonMiddleware().process_request(request)
self.assertEqual(r.status_code, 301)
- self.assertEqual(r['Location'],
+ self.assertEqual(r.url,
'http://www.testserver/middleware/slash/')
@override_settings(APPEND_SLASH=True, PREPEND_WWW=True)
@@ -126,7 +126,7 @@ def test_prepend_www_append_slash_slashless(self):
request = self._get_request('slash')
r = CommonMiddleware().process_request(request)
self.assertEqual(r.status_code, 301)
- self.assertEqual(r['Location'],
+ self.assertEqual(r.url,
'http://www.testserver/middleware/slash/')
@@ -171,7 +171,7 @@ def test_append_slash_redirect_custom_urlconf(self):
self.assertFalse(r is None,
"CommonMiddlware failed to return APPEND_SLASH redirect using request.urlconf")
self.assertEqual(r.status_code, 301)
- self.assertEqual(r['Location'], 'http://testserver/middleware/customurlconf/slash/')
+ self.assertEqual(r.url, 'http://testserver/middleware/customurlconf/slash/')
@override_settings(APPEND_SLASH=True, DEBUG=True)
def test_append_slash_no_redirect_on_POST_in_DEBUG_custom_urlconf(self):
@@ -208,7 +208,7 @@ def test_append_slash_quoted_custom_urlconf(self):
"CommonMiddlware failed to return APPEND_SLASH redirect using request.urlconf")
self.assertEqual(r.status_code, 301)
self.assertEqual(
- r['Location'],
+ r.url,
'http://testserver/middleware/customurlconf/needsquoting%23/')
@override_settings(APPEND_SLASH=False, PREPEND_WWW=True)
@@ -218,7 +218,7 @@ def test_prepend_www_custom_urlconf(self):
r = CommonMiddleware().process_request(request)
self.assertEqual(r.status_code, 301)
self.assertEqual(
- r['Location'],
+ r.url,
'http://www.testserver/middleware/customurlconf/path/')
@override_settings(APPEND_SLASH=True, PREPEND_WWW=True)
@@ -227,7 +227,7 @@ def test_prepend_www_append_slash_have_slash_custom_urlconf(self):
request.urlconf = 'regressiontests.middleware.extra_urls'
r = CommonMiddleware().process_request(request)
self.assertEqual(r.status_code, 301)
- self.assertEqual(r['Location'],
+ self.assertEqual(r.url,
'http://www.testserver/middleware/customurlconf/slash/')
@override_settings(APPEND_SLASH=True, PREPEND_WWW=True)
@@ -236,7 +236,7 @@ def test_prepend_www_append_slash_slashless_custom_urlconf(self):
request.urlconf = 'regressiontests.middleware.extra_urls'
r = CommonMiddleware().process_request(request)
self.assertEqual(r.status_code, 301)
- self.assertEqual(r['Location'],
+ self.assertEqual(r.url,
'http://www.testserver/middleware/customurlconf/slash/')
# Legacy tests for the 404 error reporting via email (to be removed in 1.8)
View
18 tests/regressiontests/requests/tests.py
@@ -548,9 +548,6 @@ def test_request_finished_db_state(self):
'This test will close the connection, in-memory '
'sqlite connections must not be closed.')
def test_request_finished_failed_connection(self):
- # See comments in test_request_finished_db_state() for the self.client
- # usage.
- response = self.client.get('/')
conn = connections[DEFAULT_DB_ALIAS]
conn.enter_transaction_management()
conn.managed(True)
@@ -560,9 +557,14 @@ def test_request_finished_failed_connection(self):
def fail_horribly():
raise Exception("Horrible failure!")
conn._rollback = fail_horribly
- signals.request_finished.send(sender=response._handler_class)
- # As even rollback wasn't possible the connection wrapper itself was
- # abandoned. Accessing the connections[alias] will create a new
- # connection wrapper, whch must be different than the original one.
- self.assertIsNot(conn, connections[DEFAULT_DB_ALIAS])
+ try:
+ with self.assertRaises(Exception):
+ signals.request_finished.send(sender=self.__class__)
+ # The connection's state wasn't cleaned up
+ self.assertTrue(len(connection.transaction_state), 1)
+ finally:
+ del conn._rollback
+ # The connection will be cleaned on next request where the conn
+ # works again.
+ signals.request_finished.send(sender=self.__class__)
self.assertEqual(len(connection.transaction_state), 0)
View
12 tests/regressiontests/templates/parser.py
@@ -4,8 +4,10 @@
from __future__ import unicode_literals
from django.template import (TokenParser, FilterExpression, Parser, Variable,
- TemplateSyntaxError)
+ Template, TemplateSyntaxError)
+from django.test.utils import override_settings
from django.utils.unittest import TestCase
+from django.utils import six
class ParserTests(TestCase):
@@ -83,3 +85,11 @@ def test_variable_parsing(self):
self.assertRaises(TemplateSyntaxError,
Variable, "article._hidden"
)
+
+ @override_settings(DEBUG=True, TEMPLATE_DEBUG=True)
+ def test_compile_filter_error(self):
+ # regression test for #19819
+ msg = "Could not parse the remainder: '@bar' from 'foo@bar'"
+ with six.assertRaisesRegex(self, TemplateSyntaxError, msg) as cm:
+ Template("{% if 1 %}{{ foo@bar }}{% endif %}")
+ self.assertEqual(cm.exception.django_template_source[1], (10, 23))
View
16 tests/regressiontests/urlpatterns_reverse/tests.py
@@ -270,31 +270,31 @@ def get_absolute_url(self):
res = redirect(FakeObj())
self.assertTrue(isinstance(res, HttpResponseRedirect))
- self.assertEqual(res['Location'], '/hi-there/')
+ self.assertEqual(res.url, '/hi-there/')
res = redirect(FakeObj(), permanent=True)
self.assertTrue(isinstance(res, HttpResponsePermanentRedirect))
- self.assertEqual(res['Location'], '/hi-there/')
+ self.assertEqual(res.url, '/hi-there/')
def test_redirect_to_view_name(self):
res = redirect('hardcoded2')
- self.assertEqual(res['Location'], '/hardcoded/doc.pdf')
+ self.assertEqual(res.url, '/hardcoded/doc.pdf')
res = redirect('places', 1)
- self.assertEqual(res['Location'], '/places/1/')
+ self.assertEqual(res.url, '/places/1/')
res = redirect('headlines', year='2008', month='02', day='17')
- self.assertEqual(res['Location'], '/headlines/2008.02.17/')
+ self.assertEqual(res.url, '/headlines/2008.02.17/')
self.assertRaises(NoReverseMatch, redirect, 'not-a-view')
def test_redirect_to_url(self):
res = redirect('/foo/')
- self.assertEqual(res['Location'], '/foo/')
+ self.assertEqual(res.url, '/foo/')
res = redirect('http://example.com/')
- self.assertEqual(res['Location'], 'http://example.com/')
+ self.assertEqual(res.url, 'http://example.com/')
def test_redirect_view_object(self):
from .views import absolute_kwargs_view
res = redirect(absolute_kwargs_view)
- self.assertEqual(res['Location'], '/absolute_arg_view/')
+ self.assertEqual(res.url, '/absolute_arg_view/')
self.assertRaises(NoReverseMatch, redirect, absolute_kwargs_view, wrong_argument=None)
View
27 tests/regressiontests/utils/text.py
@@ -55,22 +55,33 @@ def test_truncate_words(self):
truncator.words(4, '[snip]'))
def test_truncate_html_words(self):
- truncator = text.Truncator('<p><strong><em>The quick brown fox jumped '
- 'over the lazy dog.</em></strong></p>')
- self.assertEqual('<p><strong><em>The quick brown fox jumped over the '
- 'lazy dog.</em></strong></p>', truncator.words(10, html=True))
- self.assertEqual('<p><strong><em>The quick brown fox...</em>'
+ truncator = text.Truncator('<p id="par"><strong><em>The quick brown fox'
+ ' jumped over the lazy dog.</em></strong></p>')
+ self.assertEqual('<p id="par"><strong><em>The quick brown fox jumped over'
+ ' the lazy dog.</em></strong></p>', truncator.words(10, html=True))
+ self.assertEqual('<p id="par"><strong><em>The quick brown fox...</em>'
'</strong></p>', truncator.words(4, html=True))
- self.assertEqual('<p><strong><em>The quick brown fox....</em>'
+ self.assertEqual('<p id="par"><strong><em>The quick brown fox....</em>'
'</strong></p>', truncator.words(4, '....', html=True))
- self.assertEqual('<p><strong><em>The quick brown fox</em></strong>'
- '</p>', truncator.words(4, '', html=True))
+ self.assertEqual('<p id="par"><strong><em>The quick brown fox</em>'
+ '</strong></p>', truncator.words(4, '', html=True))
+
# Test with new line inside tag
truncator = text.Truncator('<p>The quick <a href="xyz.html"\n'
'id="mylink">brown fox</a> jumped over the lazy dog.</p>')
self.assertEqual('<p>The quick <a href="xyz.html"\n'
'id="mylink">brown...</a></p>', truncator.words(3, '...', html=True))
+ # Test self-closing tags
+ truncator = text.Truncator('<br/>The <hr />quick brown fox jumped over'
+ ' the lazy dog.')
+ self.assertEqual('<br/>The <hr />quick brown...',
+ truncator.words(3, '...', html=True ))
+ truncator = text.Truncator('<br>The <hr/>quick <em>brown fox</em> '
+ 'jumped over the lazy dog.')
+ self.assertEqual('<br>The <hr/>quick <em>brown...</em>',
+ truncator.words(3, '...', html=True ))
+
def test_wrap(self):
digits = '1234 67 9'
self.assertEqual(text.wrap(digits, 100), '1234 67 9')
View
2 tests/regressiontests/views/tests/i18n.py
@@ -44,7 +44,7 @@ def test_setlang_unsafe_next(self):
lang_code, lang_name = settings.LANGUAGES[0]
post_data = dict(language=lang_code, next='//unsafe/redirection/')
response = self.client.post('/views/i18n/setlang/', data=post_data)
- self.assertEqual(response['Location'], 'http://testserver/')
+ self.assertEqual(response.url, 'http://testserver/')
self.assertEqual(self.client.session['django_language'], lang_code)
def test_setlang_reversal(self):
Something went wrong with that request. Please try again.