mirror of https://github.com/django/django.git
Fixed #13165 -- Added edit and delete links to admin foreign key widgets.
Thanks to Collin Anderson for the review and suggestions and Tim for the final review.
This commit is contained in:
parent
48ad288679
commit
07988744b3
|
@ -188,11 +188,16 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
|
||||||
# OneToOneField with parent_link=True or a M2M intermediary.
|
# OneToOneField with parent_link=True or a M2M intermediary.
|
||||||
if formfield and db_field.name not in self.raw_id_fields:
|
if formfield and db_field.name not in self.raw_id_fields:
|
||||||
related_modeladmin = self.admin_site._registry.get(db_field.rel.to)
|
related_modeladmin = self.admin_site._registry.get(db_field.rel.to)
|
||||||
can_add_related = bool(related_modeladmin and
|
wrapper_kwargs = {}
|
||||||
related_modeladmin.has_add_permission(request))
|
if related_modeladmin:
|
||||||
|
wrapper_kwargs.update(
|
||||||
|
can_add_related=related_modeladmin.has_add_permission(request),
|
||||||
|
can_change_related=related_modeladmin.has_change_permission(request),
|
||||||
|
can_delete_related=related_modeladmin.has_delete_permission(request),
|
||||||
|
)
|
||||||
formfield.widget = widgets.RelatedFieldWidgetWrapper(
|
formfield.widget = widgets.RelatedFieldWidgetWrapper(
|
||||||
formfield.widget, db_field.rel, self.admin_site,
|
formfield.widget, db_field.rel, self.admin_site, **wrapper_kwargs
|
||||||
can_add_related=can_add_related)
|
)
|
||||||
|
|
||||||
return formfield
|
return formfield
|
||||||
|
|
||||||
|
@ -703,17 +708,18 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
from django.contrib.admin.views.main import ChangeList
|
from django.contrib.admin.views.main import ChangeList
|
||||||
return ChangeList
|
return ChangeList
|
||||||
|
|
||||||
def get_object(self, request, object_id):
|
def get_object(self, request, object_id, from_field=None):
|
||||||
"""
|
"""
|
||||||
Returns an instance matching the primary key provided. ``None`` is
|
Returns an instance matching the field and value provided, the primary
|
||||||
returned if no match is found (or the object_id failed validation
|
key is used if no field is provided. Returns ``None`` if no match is
|
||||||
against the primary key field).
|
found or the object_id fails validation.
|
||||||
"""
|
"""
|
||||||
queryset = self.get_queryset(request)
|
queryset = self.get_queryset(request)
|
||||||
model = queryset.model
|
model = queryset.model
|
||||||
|
field = model._meta.pk if from_field is None else model._meta.get_field(from_field)
|
||||||
try:
|
try:
|
||||||
object_id = model._meta.pk.to_python(object_id)
|
object_id = field.to_python(object_id)
|
||||||
return queryset.get(pk=object_id)
|
return queryset.get(**{field.name: object_id})
|
||||||
except (model.DoesNotExist, ValidationError, ValueError):
|
except (model.DoesNotExist, ValidationError, ValueError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -1186,6 +1192,19 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
Determines the HttpResponse for the change_view stage.
|
Determines the HttpResponse for the change_view stage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if IS_POPUP_VAR in request.POST:
|
||||||
|
to_field = request.POST.get(TO_FIELD_VAR)
|
||||||
|
attr = str(to_field) if to_field else obj._meta.pk.attname
|
||||||
|
# Retrieve the `object_id` from the resolved pattern arguments.
|
||||||
|
value = request.resolver_match.args[0]
|
||||||
|
new_value = obj.serializable_value(attr)
|
||||||
|
return SimpleTemplateResponse('admin/popup_response.html', {
|
||||||
|
'action': 'change',
|
||||||
|
'value': escape(value),
|
||||||
|
'obj': escapejs(obj),
|
||||||
|
'new_value': escape(new_value),
|
||||||
|
})
|
||||||
|
|
||||||
opts = self.model._meta
|
opts = self.model._meta
|
||||||
pk_value = obj._get_pk_val()
|
pk_value = obj._get_pk_val()
|
||||||
preserved_filters = self.get_preserved_filters(request)
|
preserved_filters = self.get_preserved_filters(request)
|
||||||
|
@ -1324,17 +1343,23 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
self.message_user(request, msg, messages.WARNING)
|
self.message_user(request, msg, messages.WARNING)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def response_delete(self, request, obj_display):
|
def response_delete(self, request, obj_display, obj_id):
|
||||||
"""
|
"""
|
||||||
Determines the HttpResponse for the delete_view stage.
|
Determines the HttpResponse for the delete_view stage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
opts = self.model._meta
|
opts = self.model._meta
|
||||||
|
|
||||||
|
if IS_POPUP_VAR in request.POST:
|
||||||
|
return SimpleTemplateResponse('admin/popup_response.html', {
|
||||||
|
'action': 'delete',
|
||||||
|
'value': escape(obj_id),
|
||||||
|
})
|
||||||
|
|
||||||
self.message_user(request,
|
self.message_user(request,
|
||||||
_('The %(name)s "%(obj)s" was deleted successfully.') % {
|
_('The %(name)s "%(obj)s" was deleted successfully.') % {
|
||||||
'name': force_text(opts.verbose_name),
|
'name': force_text(opts.verbose_name),
|
||||||
'obj': force_text(obj_display)
|
'obj': force_text(obj_display),
|
||||||
}, messages.SUCCESS)
|
}, messages.SUCCESS)
|
||||||
|
|
||||||
if self.has_change_permission(request, None):
|
if self.has_change_permission(request, None):
|
||||||
|
@ -1355,6 +1380,10 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
app_label = opts.app_label
|
app_label = opts.app_label
|
||||||
|
|
||||||
request.current_app = self.admin_site.name
|
request.current_app = self.admin_site.name
|
||||||
|
context.update(
|
||||||
|
to_field_var=TO_FIELD_VAR,
|
||||||
|
is_popup_var=IS_POPUP_VAR,
|
||||||
|
)
|
||||||
|
|
||||||
return TemplateResponse(request,
|
return TemplateResponse(request,
|
||||||
self.delete_confirmation_template or [
|
self.delete_confirmation_template or [
|
||||||
|
@ -1409,7 +1438,7 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
obj = None
|
obj = None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
obj = self.get_object(request, unquote(object_id))
|
obj = self.get_object(request, unquote(object_id), to_field)
|
||||||
|
|
||||||
if not self.has_change_permission(request, obj):
|
if not self.has_change_permission(request, obj):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
@ -1654,7 +1683,11 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
opts = self.model._meta
|
opts = self.model._meta
|
||||||
app_label = opts.app_label
|
app_label = opts.app_label
|
||||||
|
|
||||||
obj = self.get_object(request, unquote(object_id))
|
to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
|
||||||
|
if to_field and not self.to_field_allowed(request, to_field):
|
||||||
|
raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field)
|
||||||
|
|
||||||
|
obj = self.get_object(request, unquote(object_id), to_field)
|
||||||
|
|
||||||
if not self.has_delete_permission(request, obj):
|
if not self.has_delete_permission(request, obj):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
@ -1676,10 +1709,12 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
if perms_needed:
|
if perms_needed:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
obj_display = force_text(obj)
|
obj_display = force_text(obj)
|
||||||
|
attr = str(to_field) if to_field else opts.pk.attname
|
||||||
|
obj_id = obj.serializable_value(attr)
|
||||||
self.log_deletion(request, obj, obj_display)
|
self.log_deletion(request, obj, obj_display)
|
||||||
self.delete_model(request, obj)
|
self.delete_model(request, obj)
|
||||||
|
|
||||||
return self.response_delete(request, obj_display)
|
return self.response_delete(request, obj_display, obj_id)
|
||||||
|
|
||||||
object_name = force_text(opts.verbose_name)
|
object_name = force_text(opts.verbose_name)
|
||||||
|
|
||||||
|
@ -1700,6 +1735,9 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
opts=opts,
|
opts=opts,
|
||||||
app_label=app_label,
|
app_label=app_label,
|
||||||
preserved_filters=self.get_preserved_filters(request),
|
preserved_filters=self.get_preserved_filters(request),
|
||||||
|
is_popup=(IS_POPUP_VAR in request.POST or
|
||||||
|
IS_POPUP_VAR in request.GET),
|
||||||
|
to_field=to_field,
|
||||||
)
|
)
|
||||||
context.update(extra_context or {})
|
context.update(extra_context or {})
|
||||||
|
|
||||||
|
|
|
@ -576,3 +576,13 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
border-top: 1px solid #ddd;
|
border-top: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* RELATED WIDGET WRAPPER */
|
||||||
|
|
||||||
|
.related-widget-wrapper-link {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-widget-wrapper-link:link {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
|
@ -56,11 +56,16 @@ function dismissRelatedLookupPopup(win, chosenId) {
|
||||||
win.close();
|
win.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAddAnotherPopup(triggeringLink) {
|
function showRelatedObjectPopup(triggeringLink) {
|
||||||
return showAdminPopup(triggeringLink, /^add_/);
|
var name = triggeringLink.id.replace(/^(change|add|delete)_/, '');
|
||||||
|
name = id_to_windowname(name);
|
||||||
|
var href = triggeringLink.href;
|
||||||
|
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
|
||||||
|
win.focus();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissAddAnotherPopup(win, newId, newRepr) {
|
function dismissAddRelatedObjectPopup(win, newId, newRepr) {
|
||||||
// newId and newRepr are expected to have previously been escaped by
|
// newId and newRepr are expected to have previously been escaped by
|
||||||
// django.utils.html.escape.
|
// django.utils.html.escape.
|
||||||
newId = html_unescape(newId);
|
newId = html_unescape(newId);
|
||||||
|
@ -81,6 +86,8 @@ function dismissAddAnotherPopup(win, newId, newRepr) {
|
||||||
elem.value = newId;
|
elem.value = newId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Trigger a change event to update related links if required.
|
||||||
|
django.jQuery(elem).trigger('change');
|
||||||
} else {
|
} else {
|
||||||
var toId = name + "_to";
|
var toId = name + "_to";
|
||||||
o = new Option(newRepr, newId);
|
o = new Option(newRepr, newId);
|
||||||
|
@ -89,3 +96,35 @@ function dismissAddAnotherPopup(win, newId, newRepr) {
|
||||||
}
|
}
|
||||||
win.close();
|
win.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) {
|
||||||
|
objId = html_unescape(objId);
|
||||||
|
newRepr = html_unescape(newRepr);
|
||||||
|
var id = windowname_to_id(win.name).replace(/^edit_/, '');
|
||||||
|
var selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
|
||||||
|
var selects = django.jQuery(selectsSelector);
|
||||||
|
selects.find('option').each(function() {
|
||||||
|
if (this.value == objId) {
|
||||||
|
this.innerHTML = newRepr;
|
||||||
|
this.value = newId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
win.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
function dismissDeleteRelatedObjectPopup(win, objId) {
|
||||||
|
objId = html_unescape(objId);
|
||||||
|
var id = windowname_to_id(win.name).replace(/^delete_/, '');
|
||||||
|
var selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
|
||||||
|
var selects = django.jQuery(selectsSelector);
|
||||||
|
selects.find('option').each(function() {
|
||||||
|
if (this.value == objId) {
|
||||||
|
django.jQuery(this).remove();
|
||||||
|
}
|
||||||
|
}).trigger('change');
|
||||||
|
win.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Kept for backward compatibility
|
||||||
|
showAddAnotherPopup = showRelatedObjectPopup;
|
||||||
|
dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
django.jQuery(function($){
|
||||||
|
function updateLinks() {
|
||||||
|
var $this = $(this);
|
||||||
|
var siblings = $this.nextAll('.change-related, .delete-related');
|
||||||
|
if (!siblings.length) return;
|
||||||
|
var value = $this.val();
|
||||||
|
if (value) {
|
||||||
|
siblings.each(function(){
|
||||||
|
var elm = $(this);
|
||||||
|
elm.attr('href', elm.attr('data-href-template').replace('__fk__', value));
|
||||||
|
});
|
||||||
|
} else siblings.removeAttr('href');
|
||||||
|
}
|
||||||
|
var container = $(document);
|
||||||
|
container.on('change', '.related-widget-wrapper select', updateLinks);
|
||||||
|
container.find('.related-widget-wrapper select').each(updateLinks);
|
||||||
|
container.on('click', '.related-widget-wrapper-link', function(event){
|
||||||
|
if (this.href) {
|
||||||
|
showRelatedObjectPopup(this);
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
});
|
|
@ -36,6 +36,8 @@
|
||||||
<form action="" method="post">{% csrf_token %}
|
<form action="" method="post">{% csrf_token %}
|
||||||
<div>
|
<div>
|
||||||
<input type="hidden" name="post" value="yes" />
|
<input type="hidden" name="post" value="yes" />
|
||||||
|
{% 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 %}
|
||||||
<input type="submit" value="{% trans "Yes, I'm sure" %}" />
|
<input type="submit" value="{% trans "Yes, I'm sure" %}" />
|
||||||
<a href="#" onclick="window.history.back(); return false;" class="button cancel-link">{% trans "No, take me back" %}</a>
|
<a href="#" onclick="window.history.back(); return false;" class="button cancel-link">{% trans "No, take me back" %}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,13 @@
|
||||||
<head><title></title></head>
|
<head><title></title></head>
|
||||||
<body>
|
<body>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
opener.dismissAddAnotherPopup(window, "{{ value }}", "{{ obj }}");
|
{% if action == 'change' %}
|
||||||
|
opener.dismissChangeRelatedObjectPopup(window, "{{ value }}", "{{ obj }}", "{{ new_value }}");
|
||||||
|
{% elif action == 'delete' %}
|
||||||
|
opener.dismissDeleteRelatedObjectPopup(window, "{{ value }}");
|
||||||
|
{% else %}
|
||||||
|
opener.dismissAddRelatedObjectPopup(window, "{{ value }}", "{{ obj }}");
|
||||||
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
{% load i18n admin_static %}
|
||||||
|
<div class="related-widget-wrapper">
|
||||||
|
{{ widget }}
|
||||||
|
{% block links %}
|
||||||
|
{% if can_change_related %}
|
||||||
|
<a class="related-widget-wrapper-link change-related" id="change_id_{{ name }}"
|
||||||
|
data-href-template="{{ change_related_template_url }}?{{ url_params }}"
|
||||||
|
title="{% blocktrans %}Change selected {{ model }}{% endblocktrans %}">
|
||||||
|
<img src="{% static 'admin/img/icon_changelink.gif' %}" width="10" height="10"
|
||||||
|
alt="{% trans 'Change' %}"/>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_add_related %}
|
||||||
|
<a class="related-widget-wrapper-link add-related" id="add_id_{{ name }}"
|
||||||
|
href="{{ add_related_url }}?{{ url_params }}"
|
||||||
|
title="{% blocktrans %}Add another {{ model }}{% endblocktrans %}">
|
||||||
|
<img src="{% static 'admin/img/icon_addlink.gif' %}" width="10" height="10"
|
||||||
|
alt="{% trans 'Add' %}"/>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_delete_related %}
|
||||||
|
<a class="related-widget-wrapper-link delete-related" id="delete_id_{{ name }}"
|
||||||
|
data-href-template="{{ delete_related_template_url }}?{{ url_params }}"
|
||||||
|
title="{% blocktrans %}Delete selected {{ model }}{% endblocktrans %}">
|
||||||
|
<img src="{% static 'admin/img/icon_deletelink.gif' %}" width="10" height="10"
|
||||||
|
alt="{% trans 'Delete' %}"/>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
|
@ -8,8 +8,10 @@ import copy
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.admin.templatetags.admin_static import static
|
from django.contrib.admin.templatetags.admin_static import static
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.forms.widgets import RadioFieldRenderer
|
from django.db.models.deletion import CASCADE
|
||||||
|
from django.forms.widgets import Media, RadioFieldRenderer
|
||||||
from django.forms.utils import flatatt
|
from django.forms.utils import flatatt
|
||||||
|
from django.template.loader import render_to_string
|
||||||
from django.utils.html import escape, format_html, format_html_join, smart_urlquote
|
from django.utils.html import escape, format_html, format_html_join, smart_urlquote
|
||||||
from django.utils.text import Truncator
|
from django.utils.text import Truncator
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
@ -232,7 +234,10 @@ class RelatedFieldWidgetWrapper(forms.Widget):
|
||||||
This class is a wrapper to a given widget to add the add icon for the
|
This class is a wrapper to a given widget to add the add icon for the
|
||||||
admin interface.
|
admin interface.
|
||||||
"""
|
"""
|
||||||
def __init__(self, widget, rel, admin_site, can_add_related=None):
|
template = 'admin/related_widget_wrapper.html'
|
||||||
|
|
||||||
|
def __init__(self, widget, rel, admin_site, can_add_related=None,
|
||||||
|
can_change_related=False, can_delete_related=False):
|
||||||
self.needs_multipart_form = widget.needs_multipart_form
|
self.needs_multipart_form = widget.needs_multipart_form
|
||||||
self.attrs = widget.attrs
|
self.attrs = widget.attrs
|
||||||
self.choices = widget.choices
|
self.choices = widget.choices
|
||||||
|
@ -243,6 +248,12 @@ class RelatedFieldWidgetWrapper(forms.Widget):
|
||||||
if can_add_related is None:
|
if can_add_related is None:
|
||||||
can_add_related = rel.to in admin_site._registry
|
can_add_related = rel.to in admin_site._registry
|
||||||
self.can_add_related = can_add_related
|
self.can_add_related = can_add_related
|
||||||
|
# XXX: The UX does not support multiple selected values.
|
||||||
|
multiple = getattr(widget, 'allow_multiple_selected', False)
|
||||||
|
self.can_change_related = not multiple and can_change_related
|
||||||
|
# XXX: The deletion UX can be confusing when dealing with cascading deletion.
|
||||||
|
cascade = getattr(rel, 'on_delete', None) is CASCADE
|
||||||
|
self.can_delete_related = not multiple and not cascade and can_delete_related
|
||||||
# so we can check if the related object is registered with this AdminSite
|
# so we can check if the related object is registered with this AdminSite
|
||||||
self.admin_site = admin_site
|
self.admin_site = admin_site
|
||||||
|
|
||||||
|
@ -259,22 +270,47 @@ class RelatedFieldWidgetWrapper(forms.Widget):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media(self):
|
def media(self):
|
||||||
return self.widget.media
|
media = Media(js=['admin/js/related-widget-wrapper.js'])
|
||||||
|
return self.widget.media + media
|
||||||
|
|
||||||
|
def get_related_url(self, info, action, *args):
|
||||||
|
return reverse("admin:%s_%s_%s" % (info + (action,)),
|
||||||
|
current_app=self.admin_site.name, args=args)
|
||||||
|
|
||||||
def render(self, name, value, *args, **kwargs):
|
def render(self, name, value, *args, **kwargs):
|
||||||
from django.contrib.admin.views.main import TO_FIELD_VAR
|
from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR
|
||||||
|
rel_opts = self.rel.to._meta
|
||||||
|
info = (rel_opts.app_label, rel_opts.model_name)
|
||||||
self.widget.choices = self.choices
|
self.widget.choices = self.choices
|
||||||
output = [self.widget.render(name, value, *args, **kwargs)]
|
url_params = '&'.join("%s=%s" % param for param in [
|
||||||
|
(TO_FIELD_VAR, self.rel.get_related_field().name),
|
||||||
|
(IS_POPUP_VAR, 1),
|
||||||
|
])
|
||||||
|
context = {
|
||||||
|
'widget': self.widget.render(name, value, *args, **kwargs),
|
||||||
|
'name': name,
|
||||||
|
'url_params': url_params,
|
||||||
|
'model': rel_opts.verbose_name,
|
||||||
|
}
|
||||||
|
if self.can_change_related:
|
||||||
|
change_related_template_url = self.get_related_url(info, 'change', '__fk__')
|
||||||
|
context.update(
|
||||||
|
can_change_related=True,
|
||||||
|
change_related_template_url=change_related_template_url,
|
||||||
|
)
|
||||||
if self.can_add_related:
|
if self.can_add_related:
|
||||||
rel_to = self.rel.to
|
add_related_url = self.get_related_url(info, 'add')
|
||||||
info = (rel_to._meta.app_label, rel_to._meta.model_name)
|
context.update(
|
||||||
related_url = reverse('admin:%s_%s_add' % info, current_app=self.admin_site.name)
|
can_add_related=True,
|
||||||
url_params = '?%s=%s' % (TO_FIELD_VAR, self.rel.get_related_field().name)
|
add_related_url=add_related_url,
|
||||||
# TODO: "add_id_" is hard-coded here. This should instead use the
|
)
|
||||||
# correct API to determine the ID dynamically.
|
if self.can_delete_related:
|
||||||
output.append('<a href="%s%s" class="add-another" id="add_id_%s" title="%s"></a>'
|
delete_related_template_url = self.get_related_url(info, 'delete', '__fk__')
|
||||||
% (related_url, url_params, name, _('Add Another')))
|
context.update(
|
||||||
return mark_safe(''.join(output))
|
can_delete_related=True,
|
||||||
|
delete_related_template_url=delete_related_template_url,
|
||||||
|
)
|
||||||
|
return mark_safe(render_to_string(self.template, context))
|
||||||
|
|
||||||
def build_attrs(self, extra_attrs=None, **kwargs):
|
def build_attrs(self, extra_attrs=None, **kwargs):
|
||||||
"Helper function for building an attribute dictionary."
|
"Helper function for building an attribute dictionary."
|
||||||
|
|
|
@ -1736,7 +1736,7 @@ templates used by the :class:`ModelAdmin` views:
|
||||||
been saved. You can override it to change the default
|
been saved. You can override it to change the default
|
||||||
behavior after the object has been changed.
|
behavior after the object has been changed.
|
||||||
|
|
||||||
.. method:: ModelAdmin.response_delete(request, obj_display)
|
.. method:: ModelAdmin.response_delete(request, obj_display, obj_id)
|
||||||
|
|
||||||
.. versionadded:: 1.7
|
.. versionadded:: 1.7
|
||||||
|
|
||||||
|
@ -1750,6 +1750,13 @@ templates used by the :class:`ModelAdmin` views:
|
||||||
``obj_display`` is a string with the name of the deleted
|
``obj_display`` is a string with the name of the deleted
|
||||||
object.
|
object.
|
||||||
|
|
||||||
|
``obj_id`` is the serialized identifier used to retrieve the object to be
|
||||||
|
deleted.
|
||||||
|
|
||||||
|
.. versionadded:: 1.8
|
||||||
|
|
||||||
|
The ``obj_id`` parameter was added.
|
||||||
|
|
||||||
.. method:: ModelAdmin.get_changeform_initial_data(request)
|
.. method:: ModelAdmin.get_changeform_initial_data(request)
|
||||||
|
|
||||||
.. versionadded:: 1.7
|
.. versionadded:: 1.7
|
||||||
|
|
|
@ -161,6 +161,9 @@ Minor features
|
||||||
its value from :meth:`~django.contrib.admin.AdminSite.has_permission`,
|
its value from :meth:`~django.contrib.admin.AdminSite.has_permission`,
|
||||||
indicates whether the user may access the site.
|
indicates whether the user may access the site.
|
||||||
|
|
||||||
|
* Foreign key dropdowns now have buttons for changing or deleting related
|
||||||
|
objects using a popup.
|
||||||
|
|
||||||
:mod:`django.contrib.admindocs`
|
:mod:`django.contrib.admindocs`
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -913,6 +916,23 @@ those writing third-party backends in updating their code:
|
||||||
``data_type_check_constraints`` attributes have moved from the
|
``data_type_check_constraints`` attributes have moved from the
|
||||||
``DatabaseCreation`` class to ``DatabaseWrapper``.
|
``DatabaseCreation`` class to ``DatabaseWrapper``.
|
||||||
|
|
||||||
|
:mod:`django.contrib.admin`
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* ``AdminSite`` no longer takes an ``app_name`` argument and its ``app_name``
|
||||||
|
attribute has been removed. The application name is always ``admin`` (as
|
||||||
|
opposed to the instance name which you can still customize using
|
||||||
|
``AdminSite(name="...")``.
|
||||||
|
|
||||||
|
* The ``ModelAdmin.get_object()`` method (private API) now takes a third
|
||||||
|
argument named ``from_field`` in order to specify which field should match
|
||||||
|
the provided ``object_id``.
|
||||||
|
|
||||||
|
* The :meth:`ModelAdmin.response_delete()
|
||||||
|
<django.contrib.admin.ModelAdmin.response_delete>` method
|
||||||
|
now takes a second argument named ``obj_id`` which is the serialized
|
||||||
|
identifier used to retrieve the object before deletion.
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -945,11 +965,6 @@ Miscellaneous
|
||||||
``'username'``, using the the ``'unique'`` key in its
|
``'username'``, using the the ``'unique'`` key in its
|
||||||
:attr:`~django.forms.Field.error_messages` argument.
|
:attr:`~django.forms.Field.error_messages` argument.
|
||||||
|
|
||||||
* ``AdminSite`` no longer takes an ``app_name`` argument and its ``app_name``
|
|
||||||
attribute has been removed. The application name is always ``admin`` (as
|
|
||||||
opposed to the instance name which you can still customize using
|
|
||||||
``AdminSite(name="...")``.
|
|
||||||
|
|
||||||
* The block ``usertools`` in the ``base.html`` template of
|
* The block ``usertools`` in the ``base.html`` template of
|
||||||
:mod:`django.contrib.admin` now requires the ``has_permission`` context
|
:mod:`django.contrib.admin` now requires the ``has_permission`` context
|
||||||
variable to be set. If you have any custom admin views that use this
|
variable to be set. If you have any custom admin views that use this
|
||||||
|
|
|
@ -50,7 +50,7 @@ class AdminCustomUrlsTest(TestCase):
|
||||||
}
|
}
|
||||||
response = self.client.post('/admin/admin_custom_urls/action/!add/', post_data)
|
response = self.client.post('/admin/admin_custom_urls/action/!add/', post_data)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, 'dismissAddAnotherPopup')
|
self.assertContains(response, 'dismissAddRelatedObjectPopup')
|
||||||
self.assertContains(response, 'Action added through a popup')
|
self.assertContains(response, 'Action added through a popup')
|
||||||
|
|
||||||
def test_admin_URLs_no_clash(self):
|
def test_admin_URLs_no_clash(self):
|
||||||
|
|
|
@ -53,6 +53,7 @@ callable_year.admin_order_field = 'date'
|
||||||
|
|
||||||
class ArticleInline(admin.TabularInline):
|
class ArticleInline(admin.TabularInline):
|
||||||
model = Article
|
model = Article
|
||||||
|
fk_name = 'section'
|
||||||
prepopulated_fields = {
|
prepopulated_fields = {
|
||||||
'title': ('content',)
|
'title': ('content',)
|
||||||
}
|
}
|
||||||
|
@ -93,7 +94,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||||
}),
|
}),
|
||||||
('Some other fields', {
|
('Some other fields', {
|
||||||
'classes': ('wide',),
|
'classes': ('wide',),
|
||||||
'fields': ('date', 'section')
|
'fields': ('date', 'section', 'sub_section')
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ class Article(models.Model):
|
||||||
content = models.TextField()
|
content = models.TextField()
|
||||||
date = models.DateTimeField()
|
date = models.DateTimeField()
|
||||||
section = models.ForeignKey(Section, null=True, blank=True)
|
section = models.ForeignKey(Section, null=True, blank=True)
|
||||||
|
sub_section = models.ForeignKey(Section, null=True, blank=True, on_delete=models.SET_NULL, related_name='+')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
@ -545,7 +546,7 @@ class Pizza(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Album(models.Model):
|
class Album(models.Model):
|
||||||
owner = models.ForeignKey(User)
|
owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
title = models.CharField(max_length=30)
|
title = models.CharField(max_length=30)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -180,7 +180,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
|
||||||
}
|
}
|
||||||
response = self.client.post('/test_admin/%s/admin_views/article/add/' % self.urlbit, post_data)
|
response = self.client.post('/test_admin/%s/admin_views/article/add/' % self.urlbit, post_data)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, 'dismissAddAnotherPopup')
|
self.assertContains(response, 'dismissAddRelatedObjectPopup')
|
||||||
self.assertContains(response, 'title with a new\\u000Aline')
|
self.assertContains(response, 'title with a new\\u000Aline')
|
||||||
|
|
||||||
# Post data for edit inline
|
# Post data for edit inline
|
||||||
|
@ -648,8 +648,8 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
|
||||||
response = self.client.get("/test_admin/admin/admin_views/referencedbyinline/", {TO_FIELD_VAR: 'name'})
|
response = self.client.get("/test_admin/admin/admin_views/referencedbyinline/", {TO_FIELD_VAR: 'name'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# We also want to prevent the add and change view from leaking a
|
# We also want to prevent the add, change, and delete views from
|
||||||
# disallowed field value.
|
# leaking a disallowed field value.
|
||||||
with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls:
|
with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls:
|
||||||
response = self.client.post("/test_admin/admin/admin_views/section/add/", {TO_FIELD_VAR: 'name'})
|
response = self.client.post("/test_admin/admin/admin_views/section/add/", {TO_FIELD_VAR: 'name'})
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
@ -661,6 +661,11 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(len(calls), 1)
|
self.assertEqual(len(calls), 1)
|
||||||
|
|
||||||
|
with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls:
|
||||||
|
response = self.client.post("/test_admin/admin/admin_views/section/%d/delete/" % section.pk, {TO_FIELD_VAR: 'name'})
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
|
||||||
def test_allowed_filtering_15103(self):
|
def test_allowed_filtering_15103(self):
|
||||||
"""
|
"""
|
||||||
Regressions test for ticket 15103 - filtering on fields defined in a
|
Regressions test for ticket 15103 - filtering on fields defined in a
|
||||||
|
@ -1472,21 +1477,75 @@ class AdminViewPermissionsTest(TestCase):
|
||||||
login_url = reverse('admin:login') + '?next=/test_admin/admin/'
|
login_url = reverse('admin:login') + '?next=/test_admin/admin/'
|
||||||
# Set up and log in user.
|
# Set up and log in user.
|
||||||
url = '/test_admin/admin/admin_views/article/add/'
|
url = '/test_admin/admin/admin_views/article/add/'
|
||||||
add_link_text = ' class="add-another"'
|
add_link_text = 'add_id_section'
|
||||||
self.client.get('/test_admin/admin/')
|
|
||||||
self.client.post(login_url, self.adduser_login)
|
self.client.post(login_url, self.adduser_login)
|
||||||
# The add user can't add sections yet, so they shouldn't see the "add
|
# The user can't add sections yet, so they shouldn't see the "add
|
||||||
# section" link.
|
# section" link.
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
self.assertNotContains(response, add_link_text)
|
self.assertNotContains(response, add_link_text)
|
||||||
# Allow the add user to add sections too. Now they can see the "add
|
# Allow the user to add sections too. Now they can see the "add
|
||||||
# section" link.
|
# section" link.
|
||||||
add_user = User.objects.get(username='adduser')
|
user = User.objects.get(username='adduser')
|
||||||
perm = get_perm(Section, get_permission_codename('add', Section._meta))
|
perm = get_perm(Section, get_permission_codename('add', Section._meta))
|
||||||
add_user.user_permissions.add(perm)
|
user.user_permissions.add(perm)
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
self.assertContains(response, add_link_text)
|
self.assertContains(response, add_link_text)
|
||||||
|
|
||||||
|
def test_conditionally_show_change_section_link(self):
|
||||||
|
"""
|
||||||
|
The foreign key widget should only show the "change related" button if
|
||||||
|
the user has permission to change that related item.
|
||||||
|
"""
|
||||||
|
def get_change_related(response):
|
||||||
|
return response.context['adminform'].form.fields['section'].widget.can_change_related
|
||||||
|
|
||||||
|
login_url = reverse('admin:login')
|
||||||
|
# Set up and log in user.
|
||||||
|
url = '/test_admin/admin/admin_views/article/add/'
|
||||||
|
change_link_text = 'change_id_section'
|
||||||
|
self.client.post(login_url, self.adduser_login)
|
||||||
|
# The user can't change sections yet, so they shouldn't see the "change
|
||||||
|
# section" link.
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertFalse(get_change_related(response))
|
||||||
|
self.assertNotContains(response, change_link_text)
|
||||||
|
# Allow the user to change sections too. Now they can see the "change
|
||||||
|
# section" link.
|
||||||
|
user = User.objects.get(username='adduser')
|
||||||
|
perm = get_perm(Section, get_permission_codename('change', Section._meta))
|
||||||
|
user.user_permissions.add(perm)
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertTrue(get_change_related(response))
|
||||||
|
self.assertContains(response, change_link_text)
|
||||||
|
|
||||||
|
def test_conditionally_show_delete_section_link(self):
|
||||||
|
"""
|
||||||
|
The foreign key widget should only show the "delete related" button if
|
||||||
|
the user has permission to delete that related item.
|
||||||
|
"""
|
||||||
|
def get_delete_related(response):
|
||||||
|
return response.context['adminform'].form.fields['sub_section'].widget.can_delete_related
|
||||||
|
|
||||||
|
login_url = reverse('admin:login')
|
||||||
|
# Set up and log in user.
|
||||||
|
url = '/test_admin/admin/admin_views/article/add/'
|
||||||
|
delete_link_text = 'delete_id_sub_section'
|
||||||
|
self.client.get('/test_admin/admin/')
|
||||||
|
self.client.post(login_url, self.adduser_login)
|
||||||
|
# The user can't delete sections yet, so they shouldn't see the "delete
|
||||||
|
# section" link.
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertFalse(get_delete_related(response))
|
||||||
|
self.assertNotContains(response, delete_link_text)
|
||||||
|
# Allow the user to delete sections too. Now they can see the "delete
|
||||||
|
# section" link.
|
||||||
|
user = User.objects.get(username='adduser')
|
||||||
|
perm = get_perm(Section, get_permission_codename('delete', Section._meta))
|
||||||
|
user.user_permissions.add(perm)
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertTrue(get_delete_related(response))
|
||||||
|
self.assertContains(response, delete_link_text)
|
||||||
|
|
||||||
def test_custom_model_admin_templates(self):
|
def test_custom_model_admin_templates(self):
|
||||||
login_url = reverse('admin:login') + '?next=/test_admin/admin/'
|
login_url = reverse('admin:login') + '?next=/test_admin/admin/'
|
||||||
self.client.get('/test_admin/admin/')
|
self.client.get('/test_admin/admin/')
|
||||||
|
@ -4140,12 +4199,12 @@ class UserAdminTest(TestCase):
|
||||||
self.assertEqual(adminform.form.errors['password2'],
|
self.assertEqual(adminform.form.errors['password2'],
|
||||||
["The two password fields didn't match."])
|
["The two password fields didn't match."])
|
||||||
|
|
||||||
def test_user_fk_popup(self):
|
def test_user_fk_add_popup(self):
|
||||||
"""Quick user addition in a FK popup shouldn't invoke view for further user customization"""
|
"""User addition through a FK popup should return the appropriate JavaScript response."""
|
||||||
response = self.client.get('/test_admin/admin/admin_views/album/add/')
|
response = self.client.get('/test_admin/admin/admin_views/album/add/')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, '/test_admin/admin/auth/user/add')
|
self.assertContains(response, '/test_admin/admin/auth/user/add')
|
||||||
self.assertContains(response, 'class="add-another" id="add_id_owner"')
|
self.assertContains(response, 'class="related-widget-wrapper-link add-related" id="add_id_owner"')
|
||||||
response = self.client.get('/test_admin/admin/auth/user/add/?_popup=1')
|
response = self.client.get('/test_admin/admin/auth/user/add/?_popup=1')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertNotContains(response, 'name="_continue"')
|
self.assertNotContains(response, 'name="_continue"')
|
||||||
|
@ -4159,7 +4218,52 @@ class UserAdminTest(TestCase):
|
||||||
}
|
}
|
||||||
response = self.client.post('/test_admin/admin/auth/user/add/?_popup=1', data, follow=True)
|
response = self.client.post('/test_admin/admin/auth/user/add/?_popup=1', data, follow=True)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, 'dismissAddAnotherPopup')
|
self.assertContains(response, 'dismissAddRelatedObjectPopup')
|
||||||
|
|
||||||
|
def test_user_fk_change_popup(self):
|
||||||
|
"""User change through a FK popup should return the appropriate JavaScript response."""
|
||||||
|
response = self.client.get('/test_admin/admin/admin_views/album/add/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, '/test_admin/admin/auth/user/__fk__/')
|
||||||
|
self.assertContains(response, 'class="related-widget-wrapper-link change-related" id="change_id_owner"')
|
||||||
|
user = User.objects.get(username='changeuser')
|
||||||
|
url = "/test_admin/admin/auth/user/%s/?_popup=1" % user.pk
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertNotContains(response, 'name="_continue"')
|
||||||
|
self.assertNotContains(response, 'name="_addanother"')
|
||||||
|
data = {
|
||||||
|
'username': 'newuser',
|
||||||
|
'password1': 'newpassword',
|
||||||
|
'password2': 'newpassword',
|
||||||
|
'last_login_0': '2007-05-30',
|
||||||
|
'last_login_1': '13:20:10',
|
||||||
|
'date_joined_0': '2007-05-30',
|
||||||
|
'date_joined_1': '13:20:10',
|
||||||
|
'_popup': '1',
|
||||||
|
'_save': '1',
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data, follow=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, 'dismissChangeRelatedObjectPopup')
|
||||||
|
|
||||||
|
def test_user_fk_delete_popup(self):
|
||||||
|
"""User deletion through a FK popup should return the appropriate JavaScript response."""
|
||||||
|
response = self.client.get('/test_admin/admin/admin_views/album/add/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, '/test_admin/admin/auth/user/__fk__/delete/')
|
||||||
|
self.assertContains(response, 'class="related-widget-wrapper-link change-related" id="change_id_owner"')
|
||||||
|
user = User.objects.get(username='changeuser')
|
||||||
|
url = "/test_admin/admin/auth/user/%s/delete/?_popup=1" % user.pk
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = {
|
||||||
|
'post': 'yes',
|
||||||
|
'_popup': '1',
|
||||||
|
}
|
||||||
|
response = self.client.post(url, data, follow=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, 'dismissDeleteRelatedObjectPopup')
|
||||||
|
|
||||||
def test_save_add_another_button(self):
|
def test_save_add_another_button(self):
|
||||||
user_count = User.objects.count()
|
user_count = User.objects.count()
|
||||||
|
|
|
@ -108,7 +108,8 @@ class Individual(models.Model):
|
||||||
related instances (rendering will be called programmatically in this case).
|
related instances (rendering will be called programmatically in this case).
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=20)
|
name = models.CharField(max_length=20)
|
||||||
parent = models.ForeignKey('self', null=True)
|
parent = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
|
||||||
|
soulmate = models.ForeignKey('self', null=True, on_delete=models.CASCADE, related_name='soulmates')
|
||||||
|
|
||||||
|
|
||||||
class Company(models.Model):
|
class Company(models.Model):
|
||||||
|
|
|
@ -510,6 +510,32 @@ class RelatedFieldWidgetWrapperTests(DjangoTestCase):
|
||||||
w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
|
w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
|
||||||
self.assertFalse(w.can_add_related)
|
self.assertFalse(w.can_add_related)
|
||||||
|
|
||||||
|
def test_select_multiple_widget_cant_change_delete_related(self):
|
||||||
|
rel = models.Individual._meta.get_field('parent').rel
|
||||||
|
widget = forms.SelectMultiple()
|
||||||
|
wrapper = widgets.RelatedFieldWidgetWrapper(
|
||||||
|
widget, rel, widget_admin_site,
|
||||||
|
can_add_related=True,
|
||||||
|
can_change_related=True,
|
||||||
|
can_delete_related=True,
|
||||||
|
)
|
||||||
|
self.assertTrue(wrapper.can_add_related)
|
||||||
|
self.assertFalse(wrapper.can_change_related)
|
||||||
|
self.assertFalse(wrapper.can_delete_related)
|
||||||
|
|
||||||
|
def test_on_delete_cascade_rel_cant_delete_related(self):
|
||||||
|
rel = models.Individual._meta.get_field('soulmate').rel
|
||||||
|
widget = forms.Select()
|
||||||
|
wrapper = widgets.RelatedFieldWidgetWrapper(
|
||||||
|
widget, rel, widget_admin_site,
|
||||||
|
can_add_related=True,
|
||||||
|
can_change_related=True,
|
||||||
|
can_delete_related=True,
|
||||||
|
)
|
||||||
|
self.assertTrue(wrapper.can_add_related)
|
||||||
|
self.assertTrue(wrapper.can_change_related)
|
||||||
|
self.assertFalse(wrapper.can_delete_related)
|
||||||
|
|
||||||
|
|
||||||
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
|
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
|
||||||
ROOT_URLCONF='admin_widgets.urls')
|
ROOT_URLCONF='admin_widgets.urls')
|
||||||
|
@ -1134,9 +1160,27 @@ class RelatedFieldWidgetSeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
|
||||||
# The field now contains the new user
|
# The field now contains the new user
|
||||||
self.wait_for('#id_user option[value="newuser"]')
|
self.wait_for('#id_user option[value="newuser"]')
|
||||||
|
|
||||||
|
# Click the Change User button to change it
|
||||||
|
self.selenium.find_element_by_id('change_id_user').click()
|
||||||
|
self.selenium.switch_to_window('id_user')
|
||||||
|
self.wait_page_loaded()
|
||||||
|
|
||||||
|
username_field = self.selenium.find_element_by_id('id_username')
|
||||||
|
username_value = 'changednewuser'
|
||||||
|
username_field.clear()
|
||||||
|
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)
|
||||||
|
# Wait up to 2 seconds for the new option to show up after clicking save in the popup.
|
||||||
|
self.selenium.implicitly_wait(2)
|
||||||
|
self.selenium.find_element_by_css_selector('#id_user option[value=changednewuser]')
|
||||||
|
self.selenium.implicitly_wait(0)
|
||||||
|
|
||||||
# Go ahead and submit the form to make sure it works
|
# Go ahead and submit the form to make sure it works
|
||||||
self.selenium.find_element_by_css_selector(save_button_css_selector).click()
|
self.selenium.find_element_by_css_selector(save_button_css_selector).click()
|
||||||
self.wait_for_text('li.success', 'The profile "newuser" was added successfully.')
|
self.wait_for_text('li.success', 'The profile "changednewuser" was added successfully.')
|
||||||
profiles = models.Profile.objects.all()
|
profiles = models.Profile.objects.all()
|
||||||
self.assertEqual(len(profiles), 1)
|
self.assertEqual(len(profiles), 1)
|
||||||
self.assertEqual(profiles[0].user.username, username_value)
|
self.assertEqual(profiles[0].user.username, username_value)
|
||||||
|
|
|
@ -355,30 +355,30 @@ class ModelAdminTests(TestCase):
|
||||||
form = ma.get_form(request)()
|
form = ma.get_form(request)()
|
||||||
|
|
||||||
self.assertHTMLEqual(str(form["main_band"]),
|
self.assertHTMLEqual(str(form["main_band"]),
|
||||||
'<select name="main_band" id="id_main_band">\n'
|
'<div class="related-widget-wrapper">'
|
||||||
'<option value="" selected="selected">---------</option>\n'
|
'<select name="main_band" id="id_main_band">'
|
||||||
'<option value="%d">The Beatles</option>\n'
|
'<option value="" selected="selected">---------</option>'
|
||||||
'<option value="%d">The Doors</option>\n'
|
'<option value="%d">The Beatles</option>'
|
||||||
'</select>' % (band2.id, self.band.id))
|
'<option value="%d">The Doors</option>'
|
||||||
|
'</select></div>' % (band2.id, self.band.id))
|
||||||
|
|
||||||
class AdminConcertForm(forms.ModelForm):
|
class AdminConcertForm(forms.ModelForm):
|
||||||
pass
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(AdminConcertForm, self).__init__(*args, **kwargs)
|
super(AdminConcertForm, self).__init__(*args, **kwargs)
|
||||||
self.fields["main_band"].queryset = Band.objects.filter(name='The Doors')
|
self.fields["main_band"].queryset = Band.objects.filter(name='The Doors')
|
||||||
|
|
||||||
class ConcertAdmin(ModelAdmin):
|
class ConcertAdminWithForm(ModelAdmin):
|
||||||
form = AdminConcertForm
|
form = AdminConcertForm
|
||||||
|
|
||||||
ma = ConcertAdmin(Concert, self.site)
|
ma = ConcertAdminWithForm(Concert, self.site)
|
||||||
form = ma.get_form(request)()
|
form = ma.get_form(request)()
|
||||||
|
|
||||||
self.assertHTMLEqual(str(form["main_band"]),
|
self.assertHTMLEqual(str(form["main_band"]),
|
||||||
'<select name="main_band" id="id_main_band">\n'
|
'<div class="related-widget-wrapper">'
|
||||||
'<option value="" selected="selected">---------</option>\n'
|
'<select name="main_band" id="id_main_band">'
|
||||||
'<option value="%d">The Doors</option>\n'
|
'<option value="" selected="selected">---------</option>'
|
||||||
'</select>' % self.band.id)
|
'<option value="%d">The Doors</option>'
|
||||||
|
'</select></div>' % self.band.id)
|
||||||
|
|
||||||
def test_regression_for_ticket_15820(self):
|
def test_regression_for_ticket_15820(self):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue