Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions django/contrib/admin/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion django/contrib/redirects/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
193 changes: 183 additions & 10 deletions docs/ref/forms/models.txt
Original file line number Diff line number Diff line change
@@ -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 <model-form-field-types>`.

``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
<modelforms-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)

Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions docs/topics/forms/modelforms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ For example:
>>> article = Article.objects.get(pk=1)
>>> form = ArticleForm(instance=article)

.. _model-form-field-types:

Field types
-----------

Expand Down Expand Up @@ -683,6 +685,8 @@ the field declaratively and setting its ``validators`` parameter::
See the :doc:`form field documentation </ref/forms/fields>` for more
information on fields and their arguments.

.. _modelforms-enabling-localization-of-fields:

Enabling localization of fields
-------------------------------

Expand Down
15 changes: 15 additions & 0 deletions tests/admin_widgets/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading