Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #20836 -- Ensure that the ForeignKey's to_field attribute is pr…

…operly considered by the admin's interface when creating related objects.

Many thanks to Collin Anderson for the report and patch and to Peter Sheats for the test.
  • Loading branch information...
commit 55a11683f7b094ae4fd0b9fa030d18a12657ba98 1 parent 4e784f3
@jphalip jphalip authored
View
1  AUTHORS
@@ -538,6 +538,7 @@ answer newbie questions, and generally made Django that much better:
Aleksandra Sendecka <asendecka@hauru.eu>
serbaut@gmail.com
John Shaffer <jshaffer2112@gmail.com>
+ Peter Sheats <sheats@gmail.com>
Pete Shinners <pete@shinners.org>
Leo Shklovskii
jason.sidabras@gmail.com
View
18 django/contrib/admin/options.py
@@ -44,6 +44,7 @@
IS_POPUP_VAR = '_popup'
+TO_FIELD_VAR = '_to_field'
HORIZONTAL, VERTICAL = 1, 2
# returns the <ul> class for a given radio_admin field
@@ -932,6 +933,8 @@ def render_change_form(self, request, context, add=False, change=False, form_url
'content_type_id': ContentType.objects.get_for_model(self.model).id,
'save_as': self.save_as,
'save_on_top': self.save_on_top,
+ 'to_field_var': TO_FIELD_VAR,
+ 'is_popup_var': IS_POPUP_VAR
})
if add and self.add_form_template is not None:
form_template = self.add_form_template
@@ -951,13 +954,20 @@ def response_add(self, request, obj, post_url_continue=None):
opts = obj._meta
pk_value = obj._get_pk_val()
preserved_filters = self.get_preserved_filters(request)
-
msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
# Here, we distinguish between different save types by checking for
# the presence of keys in request.POST.
+
if IS_POPUP_VAR in request.POST:
+ to_field = request.POST.get(TO_FIELD_VAR)
+ if to_field:
+ attr = str(to_field)
+ else:
+ attr = obj._meta.pk.attname
+ value = obj.serializable_value(attr)
return SimpleTemplateResponse('admin/popup_response.html', {
- 'pk_value': escape(pk_value),
+ 'pk_value': escape(pk_value), # for possible backwards-compatibility
+ 'value': escape(value),
'obj': escapejs(obj)
})
@@ -988,6 +998,7 @@ def response_change(self, request, obj):
"""
Determines the HttpResponse for the change_view stage.
"""
+
opts = self.model._meta
pk_value = obj._get_pk_val()
preserved_filters = self.get_preserved_filters(request)
@@ -1224,6 +1235,7 @@ def add_view(self, request, form_url='', extra_context=None):
title=_('Add %s') % force_text(opts.verbose_name),
adminform=adminForm,
is_popup=IS_POPUP_VAR in request.REQUEST,
+ to_field=request.REQUEST.get(TO_FIELD_VAR),
media=media,
inline_admin_formsets=inline_admin_formsets,
errors=helpers.AdminErrorList(form, formsets),
@@ -1297,6 +1309,7 @@ def change_view(self, request, object_id, form_url='', extra_context=None):
object_id=object_id,
original=obj,
is_popup=IS_POPUP_VAR in request.REQUEST,
+ to_field=request.REQUEST.get(TO_FIELD_VAR),
media=media,
inline_admin_formsets=inline_admin_formsets,
errors=helpers.AdminErrorList(form, formsets),
@@ -1443,6 +1456,7 @@ def changelist_view(self, request, extra_context=None):
selection_note_all=selection_note_all % {'total_count': cl.result_count},
title=cl.title,
is_popup=cl.is_popup,
+ to_field=cl.to_field,
cl=cl,
media=media,
has_add_permission=self.has_add_permission(request),
View
3  django/contrib/admin/templates/admin/change_form.html
@@ -39,7 +39,8 @@
{% endblock %}
<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.model_name }}_form">{% csrf_token %}{% block form_top %}{% endblock %}
<div>
-{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
+{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1" />{% endif %}
+{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}" />{% endif %}
{% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %}
{% if errors %}
<p class="errornote">
View
2  django/contrib/admin/templates/admin/change_list.html
@@ -54,7 +54,7 @@
{% block object-tools-items %}
<li>
{% url cl.opts|admin_urlname:'add' as add_url %}
- <a href="{% add_preserved_filters add_url is_popup %}" class="addlink">
+ <a href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink">
{% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
</a>
</li>
View
2  django/contrib/admin/templates/admin/popup_response.html
@@ -3,7 +3,7 @@
<head><title></title></head>
<body>
<script type="text/javascript">
- opener.dismissAddAnotherPopup(window, "{{ pk_value }}", "{{ obj }}");
+ opener.dismissAddAnotherPopup(window, "{{ value }}", "{{ obj }}");
</script>
</body>
</html>
View
5 django/contrib/admin/templatetags/admin_urls.py
@@ -22,7 +22,7 @@ def admin_urlquote(value):
@register.simple_tag(takes_context=True)
-def add_preserved_filters(context, url, popup=False):
+def add_preserved_filters(context, url, popup=False, to_field=None):
opts = context.get('opts')
preserved_filters = context.get('preserved_filters')
@@ -48,6 +48,9 @@ def add_preserved_filters(context, url, popup=False):
if popup:
from django.contrib.admin.options import IS_POPUP_VAR
merged_qs[IS_POPUP_VAR] = 1
+ if to_field:
+ from django.contrib.admin.options import TO_FIELD_VAR
+ merged_qs[TO_FIELD_VAR] = to_field
merged_qs.update(parsed_qs)
View
3  django/contrib/admin/views/main.py
@@ -15,7 +15,7 @@
from django.contrib.admin import FieldListFilter
from django.contrib.admin.exceptions import DisallowedModelAdminLookup
-from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR
+from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR, TO_FIELD_VAR
from django.contrib.admin.util import (quote, get_fields_from_path,
lookup_needs_distinct, prepare_lookup_value)
@@ -25,7 +25,6 @@
ORDER_TYPE_VAR = 'ot'
PAGE_VAR = 'p'
SEARCH_VAR = 'q'
-TO_FIELD_VAR = 't'
ERROR_FLAG = 'e'
IGNORED_PARAMS = (
View
6 django/contrib/admin/widgets.py
@@ -248,16 +248,18 @@ def media(self):
return self.widget.media
def render(self, name, value, *args, **kwargs):
+ from django.contrib.admin.views.main import TO_FIELD_VAR
rel_to = self.rel.to
info = (rel_to._meta.app_label, rel_to._meta.model_name)
self.widget.choices = self.choices
output = [self.widget.render(name, value, *args, **kwargs)]
if self.can_add_related:
related_url = reverse('admin:%s_%s_add' % info, current_app=self.admin_site.name)
+ url_params = '?%s=%s' % (TO_FIELD_VAR, self.rel.get_related_field().name)
# TODO: "add_id_" is hard-coded here. This should instead use the
# correct API to determine the ID dynamically.
- output.append('<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> '
- % (related_url, name))
+ output.append('<a href="%s%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> '
+ % (related_url, url_params, name))
output.append('<img src="%s" width="10" height="10" alt="%s"/></a>'
% (static('admin/img/icon_addlink.gif'), _('Add Another')))
return mark_safe(''.join(output))
View
8 tests/admin_widgets/models.py
@@ -130,3 +130,11 @@ class School(models.Model):
def __str__(self):
return self.name
+
+
+@python_2_unicode_compatible
+class Profile(models.Model):
+ user = models.ForeignKey('auth.User', 'username')
+
+ def __str__(self):
+ return self.user.username
View
53 tests/admin_widgets/tests.py
@@ -387,7 +387,7 @@ def test_render(self):
w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site)
self.assertHTMLEqual(
w.render('test', band.pk, attrs={}),
- '<input type="text" name="test" value="%(bandpk)s" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/band/?t=id" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_STATIC_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Linkin Park</strong>' % dict(admin_static_prefix(), bandpk=band.pk)
+ '<input type="text" name="test" value="%(bandpk)s" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/band/?_to_field=id" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_STATIC_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Linkin Park</strong>' % dict(admin_static_prefix(), bandpk=band.pk)
)
def test_relations_to_non_primary_key(self):
@@ -402,7 +402,7 @@ def test_relations_to_non_primary_key(self):
w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site)
self.assertHTMLEqual(
w.render('test', core.parent_id, attrs={}),
- '<input type="text" name="test" value="86" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_STATIC_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Apple</strong>' % admin_static_prefix()
+ '<input type="text" name="test" value="86" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/inventory/?_to_field=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_STATIC_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Apple</strong>' % admin_static_prefix()
)
def test_fk_related_model_not_in_admin(self):
@@ -444,7 +444,7 @@ def test_proper_manager_for_label_lookup(self):
)
self.assertHTMLEqual(
w.render('test', child_of_hidden.parent_id, attrs={}),
- '<input type="text" name="test" value="93" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_STATIC_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Hidden</strong>' % admin_static_prefix()
+ '<input type="text" name="test" value="93" class="vForeignKeyRawIdAdminField" /><a href="/widget_admin/admin_widgets/inventory/?_to_field=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_STATIC_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Hidden</strong>' % admin_static_prefix()
)
@@ -498,7 +498,6 @@ def test_no_can_add_related(self):
self.assertFalse(w.can_add_related)
-
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class DateTimePickerSeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
@@ -953,3 +952,49 @@ class AdminRawIdWidgetSeleniumChromeTests(AdminRawIdWidgetSeleniumFirefoxTests):
class AdminRawIdWidgetSeleniumIETests(AdminRawIdWidgetSeleniumFirefoxTests):
webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class RelatedFieldWidgetSeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
+ available_apps = ['admin_widgets'] + AdminSeleniumWebDriverTestCase.available_apps
+ fixtures = ['admin-widgets-users.xml']
+ urls = "admin_widgets.urls"
+ webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
+
+ def test_foreign_key_using_to_field(self):
+ self.admin_login(username='super', password='secret', login_url='/')
+ self.selenium.get('%s%s' % (
+ self.live_server_url,
+ '/admin_widgets/profile/add/'))
+
+ main_window = self.selenium.current_window_handle
+ # Click the Add User button to add new
+ self.selenium.find_element_by_id('add_id_user').click()
+ self.selenium.switch_to_window('id_user')
+ self.wait_page_loaded()
+ password_field = self.selenium.find_element_by_id('id_password')
+ password_field.send_keys('password')
+
+ username_field = self.selenium.find_element_by_id('id_username')
+ username_value = 'newuser'
+ username_field.send_keys(username_value)
+
+ save_button_css_selector = '.submit-row > input[type=submit]'
+ self.selenium.find_element_by_css_selector(save_button_css_selector).click()
+ self.selenium.switch_to_window(main_window)
+ user_select = self.selenium.find_element_by_id('id_user')
+ new_option = user_select.find_elements_by_tag_name('option')[-1]
+ self.assertEqual(username_value, new_option.get_attribute('value'))
+
+ # Go ahead and submit the form to make sure it works
+ self.selenium.find_element_by_css_selector(save_button_css_selector).click()
+ self.wait_page_loaded()
+ profiles = models.Profile.objects.all()
+ self.assertEqual(len(profiles), 1)
+ self.assertEqual(profiles[0].user.username, username_value)
+
+class RelatedFieldWidgetSeleniumChromeTests(RelatedFieldWidgetSeleniumFirefoxTests):
+ webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver'
+
+class RelatedFieldWidgetSeleniumIETests(RelatedFieldWidgetSeleniumFirefoxTests):
+ webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'
View
2  tests/admin_widgets/widgetadmin.py
@@ -43,3 +43,5 @@ class SchoolAdmin(admin.ModelAdmin):
site.register(models.Advisor)
site.register(models.School, SchoolAdmin)
+
+site.register(models.Profile)
Please sign in to comment.
Something went wrong with that request. Please try again.