diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 124f3307af8c..81b57f33aada 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -10,6 +10,7 @@ from django.core.exceptions import ValidationError from django.core.validators import URLValidator from django.db.models import CASCADE, UUIDField +from django.forms.widgets import Select from django.urls import reverse from django.urls.exceptions import NoReverseMatch from django.utils.html import smart_urlquote @@ -284,16 +285,18 @@ def __init__( if can_add_related is None: can_add_related = admin_site.is_registered(rel.model) self.can_add_related = can_add_related - # XXX: The UX does not support multiple selected values. - multiple = getattr(widget, "allow_multiple_selected", False) if not isinstance(widget, AutocompleteMixin): self.attrs["data-context"] = "available-source" - self.can_change_related = not multiple and can_change_related + # Only single-select Select widgets are supported. + supported = not getattr( + widget, "allow_multiple_selected", False + ) and isinstance(widget, Select) + self.can_change_related = supported 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 - self.can_view_related = not multiple and can_view_related + self.can_delete_related = supported and not cascade and can_delete_related + self.can_view_related = supported and can_view_related # To check if the related object is registered with this AdminSite. self.admin_site = admin_site self.use_fieldset = True diff --git a/django/contrib/redirects/admin.py b/django/contrib/redirects/admin.py index 39400ad26523..beeca84086a5 100644 --- a/django/contrib/redirects/admin.py +++ b/django/contrib/redirects/admin.py @@ -7,4 +7,3 @@ class RedirectAdmin(admin.ModelAdmin): list_display = ("old_path", "new_path") list_filter = ("site",) search_fields = ("old_path", "new_path") - radio_fields = {"site": admin.VERTICAL} diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt index 8abe53833d93..a0326d7608c5 100644 --- a/docs/ref/forms/models.txt +++ b/docs/ref/forms/models.txt @@ -1,15 +1,188 @@ -==================== -Model Form Functions -==================== - -Model Form API reference. For introductory material about model forms, see the -:doc:`/topics/forms/modelforms` topic guide. +=========== +Model forms +=========== .. module:: django.forms.models - :synopsis: Django's functions for building model forms and formsets. + :synopsis: ModelForm API reference for inner ``Meta`` class and factory + functions + +.. currentmodule:: django.forms + +``ModelForm`` API reference. For introductory material about using a +``ModelForm``, see the :doc:`/topics/forms/modelforms` topic guide. + +Model form ``Meta`` API +======================= + +.. class:: ModelFormOptions + +The ``_meta`` API is used to build forms that reflect a Django model. It is +accessible through the ``_meta`` attribute of each model form, and is an +``django.forms.models.ModelFormOptions`` instance. + +The structure of the generated form can be customized by defining metadata +options as attributes of an inner ``Meta`` class. For example:: + + from django.forms import ModelForm + from myapp.models import Book + + + class BookForm(ModelForm): + class Meta: + model = Book + fields = ["title", "author"] + help_texts = { + "title": "The title of the book", + "author": "The author of the book", + } + # ... other attributes + +Required attributes are :attr:`~ModelFormOptions.model`, and either +:attr:`~ModelFormOptions.fields` or :attr:`~ModelFormOptions.exclude`. All +other ``Meta`` attributes are optional. + +Optional attributes, other than :attr:`~ModelFormOptions.localized_fields` and +:attr:`~ModelFormOptions.formfield_callback`, expect a dictionary that maps a +model field name to a value. Any field that is not defined in the dictionary +falls back to the field's default value. + +.. admonition:: Invalid field names + + Invalid or excluded field names in an optional dictionary attribute have no + effect, since fields that are not included are not accessed. + +.. admonition:: Invalid Meta class attributes + + You may define any attribute on a ``Meta`` class. Typos or incorrect + attribute names do not raise an error. + +``error_messages`` +------------------ + +.. attribute:: ModelFormOptions.error_messages + + A dictionary that maps a model field name to a dictionary of error message + keys (``null``, ``blank``, ``invalid``, ``unique``, etc.) mapped to custom + error messages. + + When a field is not specified, Django will fall back on the error messages + defined in that model field's :attr:`django.db.models.Field.error_messages` + and then finally on the default error messages for that field type. + +``exclude`` +----------- + +.. attribute:: ModelFormOptions.exclude + + A tuple or list of :attr:`~ModelFormOptions.model` field names to be + excluded from the form. + + Either :attr:`~ModelFormOptions.fields` or + :attr:`~ModelFormOptions.exclude` must be set. If neither are set, an + :class:`~django.core.exceptions.ImproperlyConfigured` exception will be + raised. If :attr:`~ModelFormOptions.exclude` is set and + :attr:`~ModelFormOptions.fields` is unset, all model fields, except for + those specified in :attr:`~ModelFormOptions.exclude`, are included in the + form. + +``field_classes`` +----------------- + +.. attribute:: ModelFormOptions.field_classes + + A dictionary that maps a model field name to a :class:`~django.forms.Field` + class, which overrides the ``form_class`` used in the model field's + :meth:`.Field.formfield` method. + + When a field is not specified, Django will fall back on the model field's + :ref:`default field class `. + +``fields`` +---------- + +.. attribute:: ModelFormOptions.fields + + A tuple or list of :attr:`~ModelFormOptions.model` field names to be + included in the form. The value ``'__all__'`` can be used to specify that + all fields should be included. + + If any field is specified in :attr:`~ModelFormOptions.exclude`, this will + not be included in the form despite being specified in + :attr:`~ModelFormOptions.fields`. + + Either :attr:`~ModelFormOptions.fields` or + :attr:`~ModelFormOptions.exclude` must be set. If neither are set, an + :class:`~django.core.exceptions.ImproperlyConfigured` exception will be + raised. + +``formfield_callback`` +---------------------- + +.. attribute:: ModelFormOptions.formfield_callback + + A function or callable that takes a model field and returns a + :class:`django.forms.Field` object. + +``help_texts`` +-------------- + +.. attribute:: ModelFormOptions.help_texts + + A dictionary that maps a model field name to a help text string. + + When a field is not specified, Django will fall back on that model field's + :attr:`~django.db.models.Field.help_text`. + +``labels`` +---------- + +.. attribute:: ModelFormOptions.labels + + A dictionary that maps a model field names to a label string. + + When a field is not specified, Django will fall back on that model field's + :attr:`~django.db.models.Field.verbose_name` and then the field's attribute + name. + +``localized_fields`` +-------------------- + +.. attribute:: ModelFormOptions.localized_fields + + A tuple or list of :attr:`~ModelFormOptions.model` field names to be + localized. The value ``'__all__'`` can be used to specify that all fields + should be localized. + + By default, form fields are not localized, see + :ref:`enabling localization of fields + ` for more details. + +``model`` +--------- + +.. attribute:: ModelFormOptions.model + + Required. The :class:`django.db.models.Model` to be used for the + :class:`~django.forms.ModelForm`. + +``widgets`` +----------- + +.. attribute:: ModelFormOptions.widgets + + A dictionary that maps a model field name to a + :class:`django.forms.Widget`. + + When a field is not specified, Django will fall back on the default widget + for that particular type of :class:`django.db.models.Field`. + +Model form factory functions +============================ + +.. currentmodule:: django.forms.models ``modelform_factory`` -===================== +--------------------- .. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None, labels=None, help_texts=None, error_messages=None, field_classes=None) @@ -51,7 +224,7 @@ Model Form API reference. For introductory material about model forms, see the in an :exc:`~django.core.exceptions.ImproperlyConfigured` exception. ``modelformset_factory`` -======================== +------------------------ .. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None, can_delete_extra=True, renderer=None, edit_only=False) @@ -74,7 +247,7 @@ Model Form API reference. For introductory material about model forms, see the See :ref:`model-formsets` for example usage. ``inlineformset_factory`` -========================= +------------------------- .. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None, can_delete_extra=True, renderer=None, edit_only=False) diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 611dc83439f4..a673a179f6ba 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -38,6 +38,8 @@ For example: >>> article = Article.objects.get(pk=1) >>> form = ArticleForm(instance=article) +.. _model-form-field-types: + Field types ----------- @@ -683,6 +685,8 @@ the field declaratively and setting its ``validators`` parameter:: See the :doc:`form field documentation ` for more information on fields and their arguments. +.. _modelforms-enabling-localization-of-fields: + Enabling localization of fields ------------------------------- diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 9a5c846bdd1e..7588c2cc32a1 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -978,6 +978,21 @@ def test_data_model_ref_when_model_name_is_camel_case(self): """ self.assertHTMLEqual(output, expected) + def test_non_select_widget_cant_change_delete_related(self): + main_band = Event._meta.get_field("main_band") + widget = widgets.AdminRadioSelect() + wrapper = widgets.RelatedFieldWidgetWrapper( + widget, + main_band, + 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) + @override_settings(ROOT_URLCONF="admin_widgets.urls") class AdminWidgetSeleniumTestCase(AdminSeleniumTestCase):