Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #18072 -- Made more admin links use reverse() instead of hard-c…

…oded relative URLs.

Thanks kmike for the report and initial patch for the changelist->edit
object view link URL.

Other affected links include the delete object one and object history
one (in this case the change had been implemented in commit 5a9e127, this
commit adds admin-quoting of the object PK in a way similar to a222d6e.)

Refs #15294.
  • Loading branch information...
commit f51eab796d087439eedcb768cdd312517780940e 1 parent 515fd6a
Ramiro Morales ramiro authored
2  django/contrib/admin/templates/admin/change_form.html
@@ -29,7 +29,7 @@
29 29 {% if change %}{% if not is_popup %}
30 30 <ul class="object-tools">
31 31 {% block object-tools-items %}
32   - <li><a href="{% url opts|admin_urlname:'history' original.pk %}" class="historylink">{% trans "History" %}</a></li>
  32 + <li><a href="{% url opts|admin_urlname:'history' original.pk|admin_urlquote %}" class="historylink">{% trans "History" %}</a></li>
33 33 {% if has_absolute_url %}<li><a href="{% url 'admin:view_on_site' content_type_id original.pk %}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
34 34 {% endblock %}
35 35 </ul>
6 django/contrib/admin/templates/admin/submit_line.html
... ... @@ -1,8 +1,8 @@
1   -{% load i18n %}
  1 +{% load i18n admin_urls %}
2 2 <div class="submit-row">
3 3 {% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" {{ onclick_attrib }}/>{% endif %}
4   -{% if show_delete_link %}<p class="deletelink-box"><a href="delete/" class="deletelink">{% trans "Delete" %}</a></p>{% endif %}
  4 +{% if show_delete_link %}<p class="deletelink-box"><a href="{% url opts|admin_urlname:'delete' original.pk|admin_urlquote %}" class="deletelink">{% trans "Delete" %}</a></p>{% endif %}
5 5 {% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" {{ onclick_attrib }}/>{%endif%}
6   -{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" {{ onclick_attrib }} />{% endif %}
  6 +{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" {{ onclick_attrib }}/>{% endif %}
7 7 {% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" {{ onclick_attrib }}/>{% endif %}
8 8 </div>
6 django/contrib/admin/templatetags/admin_modify.py
@@ -28,7 +28,8 @@ def submit_row(context):
28 28 change = context['change']
29 29 is_popup = context['is_popup']
30 30 save_as = context['save_as']
31   - return {
  31 + ctx = {
  32 + 'opts': opts,
32 33 'onclick_attrib': (opts.get_ordered_objects() and change
33 34 and 'onclick="submitOrderForm();"' or ''),
34 35 'show_delete_link': (not is_popup and context['has_delete_permission']
@@ -40,6 +41,9 @@ def submit_row(context):
40 41 'is_popup': is_popup,
41 42 'show_save': True
42 43 }
  44 + if context.get('original') is not None:
  45 + ctx['original'] = context['original']
  46 + return ctx
43 47
44 48 @register.filter
45 49 def cell_count(inline_admin_form):
6 django/contrib/admin/util.py
@@ -48,9 +48,9 @@ def prepare_lookup_value(key, value):
48 48 def quote(s):
49 49 """
50 50 Ensure that primary key values do not confuse the admin URLs by escaping
51   - any '/', '_' and ':' characters. Similar to urllib.quote, except that the
52   - quoting is slightly different so that it doesn't get automatically
53   - unquoted by the Web browser.
  51 + any '/', '_' and ':' and similarly problematic characters.
  52 + Similar to urllib.quote, except that the quoting is slightly different so
  53 + that it doesn't get automatically unquoted by the Web browser.
54 54 """
55 55 if not isinstance(s, six.string_types):
56 56 return s
7 django/contrib/admin/views/main.py
@@ -3,6 +3,7 @@
3 3
4 4 from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
5 5 from django.core.paginator import InvalidPage
  6 +from django.core.urlresolvers import reverse
6 7 from django.db import models
7 8 from django.db.models.fields import FieldDoesNotExist
8 9 from django.utils.datastructures import SortedDict
@@ -376,4 +377,8 @@ def construct_search(field_name):
376 377 return qs
377 378
378 379 def url_for_result(self, result):
379   - return "%s/" % quote(getattr(result, self.pk_attname))
  380 + pk = getattr(result, self.pk_attname)
  381 + return reverse('admin:%s_%s_change' % (self.opts.app_label,
  382 + self.opts.module_name),
  383 + args=(quote(pk),),
  384 + current_app=self.model_admin.admin_site.name)
10 tests/regressiontests/admin_changelist/tests.py
@@ -6,6 +6,7 @@
6 6 from django.contrib.admin.options import IncorrectLookupParameters
7 7 from django.contrib.admin.views.main import ChangeList, SEARCH_VAR, ALL_VAR
8 8 from django.contrib.auth.models import User
  9 +from django.core.urlresolvers import reverse
9 10 from django.template import Context, Template
10 11 from django.test import TestCase
11 12 from django.test.client import RequestFactory
@@ -65,7 +66,8 @@ def test_result_list_empty_changelist_value(self):
65 66 template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
66 67 context = Context({'cl': cl})
67 68 table_output = template.render(context)
68   - row_html = '<tbody><tr class="row1"><th><a href="%d/">name</a></th><td class="nowrap">(None)</td></tr></tbody>' % new_child.id
  69 + link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
  70 + row_html = '<tbody><tr class="row1"><th><a href="%s">name</a></th><td class="nowrap">(None)</td></tr></tbody>' % link
69 71 self.assertFalse(table_output.find(row_html) == -1,
70 72 'Failed to find expected row element: %s' % table_output)
71 73
@@ -87,7 +89,8 @@ def test_result_list_html(self):
87 89 template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
88 90 context = Context({'cl': cl})
89 91 table_output = template.render(context)
90   - row_html = '<tbody><tr class="row1"><th><a href="%d/">name</a></th><td class="nowrap">Parent object</td></tr></tbody>' % new_child.id
  92 + link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
  93 + row_html = '<tbody><tr class="row1"><th><a href="%s">name</a></th><td class="nowrap">Parent object</td></tr></tbody>' % link
91 94 self.assertFalse(table_output.find(row_html) == -1,
92 95 'Failed to find expected row element: %s' % table_output)
93 96
@@ -425,7 +428,8 @@ def test_dynamic_list_display_links(self):
425 428 request = self._mocked_authenticated_request('/child/', superuser)
426 429 response = m.changelist_view(request)
427 430 for i in range(1, 10):
428   - self.assertContains(response, '<a href="%s/">%s</a>' % (i, i))
  431 + link = reverse('admin:admin_changelist_child_change', args=(i,))
  432 + self.assertContains(response, '<a href="%s">%s</a>' % (link, i))
429 433
430 434 list_display = m.get_list_display(request)
431 435 list_display_links = m.get_list_display_links(request, list_display)
7 tests/regressiontests/admin_custom_urls/fixtures/actions.json
@@ -40,12 +40,5 @@
40 40 "fields": {
41 41 "description": "An action with a name suspected of being a XSS attempt"
42 42 }
43   - },
44   - {
45   - "pk": "The name of an action",
46   - "model": "admin_custom_urls.action",
47   - "fields": {
48   - "description": "A generic action"
49   - }
50 43 }
51 44 ]
16 tests/regressiontests/admin_custom_urls/tests.py
... ... @@ -1,5 +1,6 @@
1 1 from __future__ import absolute_import, unicode_literals
2 2
  3 +from django.contrib.admin.util import quote
3 4 from django.core.urlresolvers import reverse
4 5 from django.template.response import TemplateResponse
5 6 from django.test import TestCase
@@ -67,7 +68,7 @@ def testAdminUrlsNoClash(self):
67 68
68 69 # Ditto, but use reverse() to build the URL
69 70 url = reverse('admin:%s_action_change' % Action._meta.app_label,
70   - args=('add',))
  71 + args=(quote('add'),))
71 72 response = self.client.get(url)
72 73 self.assertEqual(response.status_code, 200)
73 74 self.assertContains(response, 'Change action')
@@ -75,19 +76,8 @@ def testAdminUrlsNoClash(self):
75 76 # Should correctly get the change_view for the model instance with the
76 77 # funny-looking PK (the one wth a 'path/to/html/document.html' value)
77 78 url = reverse('admin:%s_action_change' % Action._meta.app_label,
78   - args=("path/to/html/document.html",))
  79 + args=(quote("path/to/html/document.html"),))
79 80 response = self.client.get(url)
80 81 self.assertEqual(response.status_code, 200)
81 82 self.assertContains(response, 'Change action')
82 83 self.assertContains(response, 'value="path/to/html/document.html"')
83   -
84   - def testChangeViewHistoryButton(self):
85   - url = reverse('admin:%s_action_change' % Action._meta.app_label,
86   - args=('The name of an action',))
87   - response = self.client.get(url)
88   - self.assertEqual(response.status_code, 200)
89   - expected_link = reverse('admin:%s_action_history' %
90   - Action._meta.app_label,
91   - args=('The name of an action',))
92   - self.assertContains(response, '<a href="%s" class="historylink"' %
93   - expected_link)
75 tests/regressiontests/admin_views/tests.py
@@ -260,19 +260,21 @@ def testChangeListSortingMultiple(self):
260 260 p1 = Person.objects.create(name="Chris", gender=1, alive=True)
261 261 p2 = Person.objects.create(name="Chris", gender=2, alive=True)
262 262 p3 = Person.objects.create(name="Bob", gender=1, alive=True)
263   - link = '<a href="%s/'
  263 + link1 = reverse('admin:admin_views_person_change', args=(p1.pk,))
  264 + link2 = reverse('admin:admin_views_person_change', args=(p2.pk,))
  265 + link3 = reverse('admin:admin_views_person_change', args=(p3.pk,))
264 266
265 267 # Sort by name, gender
266 268 # This hard-codes the URL because it'll fail if it runs against the
267 269 # 'admin2' custom admin (which doesn't have the Person model).
268 270 response = self.client.get('/test_admin/admin/admin_views/person/', {'o': '1.2'})
269   - self.assertContentBefore(response, link % p3.id, link % p1.id)
270   - self.assertContentBefore(response, link % p1.id, link % p2.id)
  271 + self.assertContentBefore(response, link3, link1)
  272 + self.assertContentBefore(response, link1, link2)
271 273
272 274 # Sort by gender descending, name
273 275 response = self.client.get('/test_admin/admin/admin_views/person/', {'o': '-2.1'})
274   - self.assertContentBefore(response, link % p2.id, link % p3.id)
275   - self.assertContentBefore(response, link % p3.id, link % p1.id)
  276 + self.assertContentBefore(response, link2, link3)
  277 + self.assertContentBefore(response, link3, link1)
276 278
277 279 def testChangeListSortingPreserveQuerySetOrdering(self):
278 280 """
@@ -284,37 +286,41 @@ def testChangeListSortingPreserveQuerySetOrdering(self):
284 286 p1 = Person.objects.create(name="Amy", gender=1, alive=True, age=80)
285 287 p2 = Person.objects.create(name="Bob", gender=1, alive=True, age=70)
286 288 p3 = Person.objects.create(name="Chris", gender=2, alive=False, age=60)
287   - link = '<a href="%s/'
  289 + link1 = reverse('admin:admin_views_person_change', args=(p1.pk,))
  290 + link2 = reverse('admin:admin_views_person_change', args=(p2.pk,))
  291 + link3 = reverse('admin:admin_views_person_change', args=(p3.pk,))
288 292
289 293 # This hard-codes the URL because it'll fail if it runs against the
290 294 # 'admin2' custom admin (which doesn't have the Person model).
291 295 response = self.client.get('/test_admin/admin/admin_views/person/', {})
292   - self.assertContentBefore(response, link % p3.id, link % p2.id)
293   - self.assertContentBefore(response, link % p2.id, link % p1.id)
  296 + self.assertContentBefore(response, link3, link2)
  297 + self.assertContentBefore(response, link2, link1)
294 298
295 299 def testChangeListSortingModelMeta(self):
296 300 # Test ordering on Model Meta is respected
297 301
298 302 l1 = Language.objects.create(iso='ur', name='Urdu')
299 303 l2 = Language.objects.create(iso='ar', name='Arabic')
300   - link = '<a href="%s/'
  304 + link1 = reverse('admin:admin_views_language_change', args=(quote(l1.pk),))
  305 + link2 = reverse('admin:admin_views_language_change', args=(quote(l2.pk),))
301 306
302 307 response = self.client.get('/test_admin/admin/admin_views/language/', {})
303   - self.assertContentBefore(response, link % l2.pk, link % l1.pk)
  308 + self.assertContentBefore(response, link2, link1)
304 309
305 310 # Test we can override with query string
306 311 response = self.client.get('/test_admin/admin/admin_views/language/', {'o':'-1'})
307   - self.assertContentBefore(response, link % l1.pk, link % l2.pk)
  312 + self.assertContentBefore(response, link1, link2)
308 313
309 314 def testChangeListSortingOverrideModelAdmin(self):
310 315 # Test ordering on Model Admin is respected, and overrides Model Meta
311 316 dt = datetime.datetime.now()
312 317 p1 = Podcast.objects.create(name="A", release_date=dt)
313 318 p2 = Podcast.objects.create(name="B", release_date=dt - datetime.timedelta(10))
  319 + link1 = reverse('admin:admin_views_podcast_change', args=(p1.pk,))
  320 + link2 = reverse('admin:admin_views_podcast_change', args=(p2.pk,))
314 321
315   - link = '<a href="%s/'
316 322 response = self.client.get('/test_admin/admin/admin_views/podcast/', {})
317   - self.assertContentBefore(response, link % p1.pk, link % p2.pk)
  323 + self.assertContentBefore(response, link1, link2)
318 324
319 325 def testMultipleSortSameField(self):
320 326 # Check that we get the columns we expect if we have two columns
@@ -322,14 +328,16 @@ def testMultipleSortSameField(self):
322 328 dt = datetime.datetime.now()
323 329 p1 = Podcast.objects.create(name="A", release_date=dt)
324 330 p2 = Podcast.objects.create(name="B", release_date=dt - datetime.timedelta(10))
  331 + link1 = reverse('admin:admin_views_podcast_change', args=(quote(p1.pk),))
  332 + link2 = reverse('admin:admin_views_podcast_change', args=(quote(p2.pk),))
325 333
326   - link = '<a href="%s/'
327 334 response = self.client.get('/test_admin/admin/admin_views/podcast/', {})
328   - self.assertContentBefore(response, link % p1.pk, link % p2.pk)
  335 + self.assertContentBefore(response, link1, link2)
329 336
330 337 p1 = ComplexSortedPerson.objects.create(name="Bob", age=10)
331 338 p2 = ComplexSortedPerson.objects.create(name="Amy", age=20)
332   - link = '<a href="%s/'
  339 + link1 = reverse('admin:admin_views_complexsortedperson_change', args=(p1.pk,))
  340 + link2 = reverse('admin:admin_views_complexsortedperson_change', args=(p2.pk,))
333 341
334 342 response = self.client.get('/test_admin/admin/admin_views/complexsortedperson/', {})
335 343 # Should have 5 columns (including action checkbox col)
@@ -342,7 +350,7 @@ def testMultipleSortSameField(self):
342 350 self.assertContentBefore(response, 'Name', 'Colored name')
343 351
344 352 # Check sorting - should be by name
345   - self.assertContentBefore(response, link % p2.id, link % p1.id)
  353 + self.assertContentBefore(response, link2, link1)
346 354
347 355 def testSortIndicatorsAdminOrder(self):
348 356 """
@@ -461,10 +469,12 @@ def testNamedGroupFieldChoicesChangeList(self):
461 469 for rows corresponding to instances of a model in which a named group
462 470 has been used in the choices option of a field.
463 471 """
  472 + link1 = reverse('admin:admin_views_fabric_change', args=(1,), current_app=self.urlbit)
  473 + link2 = reverse('admin:admin_views_fabric_change', args=(2,), current_app=self.urlbit)
464 474 response = self.client.get('/test_admin/%s/admin_views/fabric/' % self.urlbit)
465 475 fail_msg = "Changelist table isn't showing the right human-readable values set by a model field 'choices' option named group."
466   - self.assertContains(response, '<a href="1/">Horizontal</a>', msg_prefix=fail_msg, html=True)
467   - self.assertContains(response, '<a href="2/">Vertical</a>', msg_prefix=fail_msg, html=True)
  476 + self.assertContains(response, '<a href="%s">Horizontal</a>' % link1, msg_prefix=fail_msg, html=True)
  477 + self.assertContains(response, '<a href="%s">Vertical</a>' % link2, msg_prefix=fail_msg, html=True)
468 478
469 479 def testNamedGroupFieldChoicesFilter(self):
470 480 """
@@ -1371,9 +1381,12 @@ def test_get_change_view(self):
1371 1381 self.assertEqual(response.status_code, 200)
1372 1382
1373 1383 def test_changelist_to_changeform_link(self):
1374   - "The link from the changelist referring to the changeform of the object should be quoted"
1375   - response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/')
1376   - should_contain = """<th><a href="%s/">%s</a></th></tr>""" % (escape(quote(self.pk)), escape(self.pk))
  1384 + "Link to the changeform of the object in changelist should use reverse() and be quoted -- #18072"
  1385 + prefix = '/test_admin/admin/admin_views/modelwithstringprimarykey/'
  1386 + response = self.client.get(prefix)
  1387 + # this URL now comes through reverse(), thus iri_to_uri encoding
  1388 + pk_final_url = escape(iri_to_uri(quote(self.pk)))
  1389 + should_contain = """<th><a href="%s%s/">%s</a></th>""" % (prefix, pk_final_url, escape(self.pk))
1377 1390 self.assertContains(response, should_contain)
1378 1391
1379 1392 def test_recentactions_link(self):
@@ -1441,6 +1454,18 @@ def test_shortcut_view_with_escaping(self):
1441 1454 should_contain = '/%s/" class="viewsitelink">' % model.pk
1442 1455 self.assertContains(response, should_contain)
1443 1456
  1457 + def test_change_view_history_link(self):
  1458 + """Object history button link should work and contain the pk value quoted."""
  1459 + url = reverse('admin:%s_modelwithstringprimarykey_change' %
  1460 + ModelWithStringPrimaryKey._meta.app_label,
  1461 + args=(quote(self.pk),))
  1462 + response = self.client.get(url)
  1463 + self.assertEqual(response.status_code, 200)
  1464 + expected_link = reverse('admin:%s_modelwithstringprimarykey_history' %
  1465 + ModelWithStringPrimaryKey._meta.app_label,
  1466 + args=(quote(self.pk),))
  1467 + self.assertContains(response, '<a href="%s" class="historylink"' % expected_link)
  1468 +
1444 1469
1445 1470 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
1446 1471 class SecureViewTests(TestCase):
@@ -2023,12 +2048,14 @@ def test_pk_hidden_fields_with_list_display_links(self):
2023 2048 """
2024 2049 story1 = OtherStory.objects.create(title='The adventures of Guido', content='Once upon a time in Djangoland...')
2025 2050 story2 = OtherStory.objects.create(title='Crouching Tiger, Hidden Python', content='The Python was sneaking into...')
  2051 + link1 = reverse('admin:admin_views_otherstory_change', args=(story1.pk,))
  2052 + link2 = reverse('admin:admin_views_otherstory_change', args=(story2.pk,))
2026 2053 response = self.client.get('/test_admin/admin/admin_views/otherstory/')
2027 2054 self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table.
2028 2055 self.assertContains(response, 'id="id_form-1-id"', 1)
2029 2056 self.assertContains(response, '<div class="hiddenfields">\n<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id" /><input type="hidden" name="form-1-id" value="%d" id="id_form-1-id" />\n</div>' % (story2.id, story1.id), html=True)
2030   - self.assertContains(response, '<th><a href="%d/">%d</a></th>' % (story1.id, story1.id), 1)
2031   - self.assertContains(response, '<th><a href="%d/">%d</a></th>' % (story2.id, story2.id), 1)
  2057 + self.assertContains(response, '<th><a href="%s">%d</a></th>' % (link1, story1.id), 1)
  2058 + self.assertContains(response, '<th><a href="%s">%d</a></th>' % (link2, story2.id), 1)
2032 2059
2033 2060
2034 2061 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))

0 comments on commit f51eab7

Please sign in to comment.
Something went wrong with that request. Please try again.