Loading django/contrib/admin/options.py +53 −15 Original line number Diff line number Diff line Loading @@ -188,11 +188,16 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): # OneToOneField with parent_link=True or a M2M intermediary. if formfield and db_field.name not in self.raw_id_fields: related_modeladmin = self.admin_site._registry.get(db_field.rel.to) can_add_related = bool(related_modeladmin and related_modeladmin.has_add_permission(request)) wrapper_kwargs = {} 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, db_field.rel, self.admin_site, can_add_related=can_add_related) formfield.widget, db_field.rel, self.admin_site, **wrapper_kwargs ) return formfield Loading Loading @@ -703,17 +708,18 @@ class ModelAdmin(BaseModelAdmin): from django.contrib.admin.views.main import 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 returned if no match is found (or the object_id failed validation against the primary key field). Returns an instance matching the field and value provided, the primary key is used if no field is provided. Returns ``None`` if no match is found or the object_id fails validation. """ queryset = self.get_queryset(request) model = queryset.model field = model._meta.pk if from_field is None else model._meta.get_field(from_field) try: object_id = model._meta.pk.to_python(object_id) return queryset.get(pk=object_id) object_id = field.to_python(object_id) return queryset.get(**{field.name: object_id}) except (model.DoesNotExist, ValidationError, ValueError): return None Loading Loading @@ -1186,6 +1192,19 @@ class ModelAdmin(BaseModelAdmin): 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 pk_value = obj._get_pk_val() preserved_filters = self.get_preserved_filters(request) Loading Loading @@ -1324,17 +1343,23 @@ class ModelAdmin(BaseModelAdmin): self.message_user(request, msg, messages.WARNING) 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. """ 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, _('The %(name)s "%(obj)s" was deleted successfully.') % { 'name': force_text(opts.verbose_name), 'obj': force_text(obj_display) 'obj': force_text(obj_display), }, messages.SUCCESS) if self.has_change_permission(request, None): Loading @@ -1355,6 +1380,10 @@ class ModelAdmin(BaseModelAdmin): app_label = opts.app_label request.current_app = self.admin_site.name context.update( to_field_var=TO_FIELD_VAR, is_popup_var=IS_POPUP_VAR, ) return TemplateResponse(request, self.delete_confirmation_template or [ Loading Loading @@ -1409,7 +1438,7 @@ class ModelAdmin(BaseModelAdmin): obj = None 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): raise PermissionDenied Loading Loading @@ -1654,7 +1683,11 @@ class ModelAdmin(BaseModelAdmin): opts = self.model._meta 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): raise PermissionDenied Loading @@ -1676,10 +1709,12 @@ class ModelAdmin(BaseModelAdmin): if perms_needed: raise PermissionDenied 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.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) Loading @@ -1700,6 +1735,9 @@ class ModelAdmin(BaseModelAdmin): opts=opts, app_label=app_label, 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 {}) Loading django/contrib/admin/static/admin/css/widgets.css +10 −0 Original line number Diff line number Diff line Loading @@ -576,3 +576,13 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover { font-size: 11px; border-top: 1px solid #ddd; } /* RELATED WIDGET WRAPPER */ .related-widget-wrapper-link { opacity: 0.3; } .related-widget-wrapper-link:link { opacity: 1; } django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js +42 −3 Original line number Diff line number Diff line Loading @@ -56,11 +56,16 @@ function dismissRelatedLookupPopup(win, chosenId) { win.close(); } function showAddAnotherPopup(triggeringLink) { return showAdminPopup(triggeringLink, /^add_/); function showRelatedObjectPopup(triggeringLink) { 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 // django.utils.html.escape. newId = html_unescape(newId); Loading @@ -81,6 +86,8 @@ function dismissAddAnotherPopup(win, newId, newRepr) { elem.value = newId; } } // Trigger a change event to update related links if required. django.jQuery(elem).trigger('change'); } else { var toId = name + "_to"; o = new Option(newRepr, newId); Loading @@ -89,3 +96,35 @@ function dismissAddAnotherPopup(win, newId, newRepr) { } 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; django/contrib/admin/static/admin/js/related-widget-wrapper.js 0 → 100644 +23 −0 Original line number Diff line number Diff line 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(); }); }); django/contrib/admin/templates/admin/delete_confirmation.html +2 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,8 @@ <form action="" method="post">{% csrf_token %} <div> <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" %}" /> <a href="#" onclick="window.history.back(); return false;" class="button cancel-link">{% trans "No, take me back" %}</a> </div> Loading Loading
django/contrib/admin/options.py +53 −15 Original line number Diff line number Diff line Loading @@ -188,11 +188,16 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): # OneToOneField with parent_link=True or a M2M intermediary. if formfield and db_field.name not in self.raw_id_fields: related_modeladmin = self.admin_site._registry.get(db_field.rel.to) can_add_related = bool(related_modeladmin and related_modeladmin.has_add_permission(request)) wrapper_kwargs = {} 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, db_field.rel, self.admin_site, can_add_related=can_add_related) formfield.widget, db_field.rel, self.admin_site, **wrapper_kwargs ) return formfield Loading Loading @@ -703,17 +708,18 @@ class ModelAdmin(BaseModelAdmin): from django.contrib.admin.views.main import 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 returned if no match is found (or the object_id failed validation against the primary key field). Returns an instance matching the field and value provided, the primary key is used if no field is provided. Returns ``None`` if no match is found or the object_id fails validation. """ queryset = self.get_queryset(request) model = queryset.model field = model._meta.pk if from_field is None else model._meta.get_field(from_field) try: object_id = model._meta.pk.to_python(object_id) return queryset.get(pk=object_id) object_id = field.to_python(object_id) return queryset.get(**{field.name: object_id}) except (model.DoesNotExist, ValidationError, ValueError): return None Loading Loading @@ -1186,6 +1192,19 @@ class ModelAdmin(BaseModelAdmin): 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 pk_value = obj._get_pk_val() preserved_filters = self.get_preserved_filters(request) Loading Loading @@ -1324,17 +1343,23 @@ class ModelAdmin(BaseModelAdmin): self.message_user(request, msg, messages.WARNING) 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. """ 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, _('The %(name)s "%(obj)s" was deleted successfully.') % { 'name': force_text(opts.verbose_name), 'obj': force_text(obj_display) 'obj': force_text(obj_display), }, messages.SUCCESS) if self.has_change_permission(request, None): Loading @@ -1355,6 +1380,10 @@ class ModelAdmin(BaseModelAdmin): app_label = opts.app_label request.current_app = self.admin_site.name context.update( to_field_var=TO_FIELD_VAR, is_popup_var=IS_POPUP_VAR, ) return TemplateResponse(request, self.delete_confirmation_template or [ Loading Loading @@ -1409,7 +1438,7 @@ class ModelAdmin(BaseModelAdmin): obj = None 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): raise PermissionDenied Loading Loading @@ -1654,7 +1683,11 @@ class ModelAdmin(BaseModelAdmin): opts = self.model._meta 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): raise PermissionDenied Loading @@ -1676,10 +1709,12 @@ class ModelAdmin(BaseModelAdmin): if perms_needed: raise PermissionDenied 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.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) Loading @@ -1700,6 +1735,9 @@ class ModelAdmin(BaseModelAdmin): opts=opts, app_label=app_label, 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 {}) Loading
django/contrib/admin/static/admin/css/widgets.css +10 −0 Original line number Diff line number Diff line Loading @@ -576,3 +576,13 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover { font-size: 11px; border-top: 1px solid #ddd; } /* RELATED WIDGET WRAPPER */ .related-widget-wrapper-link { opacity: 0.3; } .related-widget-wrapper-link:link { opacity: 1; }
django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js +42 −3 Original line number Diff line number Diff line Loading @@ -56,11 +56,16 @@ function dismissRelatedLookupPopup(win, chosenId) { win.close(); } function showAddAnotherPopup(triggeringLink) { return showAdminPopup(triggeringLink, /^add_/); function showRelatedObjectPopup(triggeringLink) { 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 // django.utils.html.escape. newId = html_unescape(newId); Loading @@ -81,6 +86,8 @@ function dismissAddAnotherPopup(win, newId, newRepr) { elem.value = newId; } } // Trigger a change event to update related links if required. django.jQuery(elem).trigger('change'); } else { var toId = name + "_to"; o = new Option(newRepr, newId); Loading @@ -89,3 +96,35 @@ function dismissAddAnotherPopup(win, newId, newRepr) { } 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;
django/contrib/admin/static/admin/js/related-widget-wrapper.js 0 → 100644 +23 −0 Original line number Diff line number Diff line 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(); }); });
django/contrib/admin/templates/admin/delete_confirmation.html +2 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,8 @@ <form action="" method="post">{% csrf_token %} <div> <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" %}" /> <a href="#" onclick="window.history.back(); return false;" class="button cancel-link">{% trans "No, take me back" %}</a> </div> Loading