142 lines
7.4 KiB
Diff
142 lines
7.4 KiB
Diff
Based on my knowledge of Django's codebase, I can analyze and fix this issue. The problem is in `django/forms/models.py` in the `apply_limit_choices_to_to_formfield` function or more specifically in how the form field's `show_hidden_initial` is determined.
|
|
|
|
The issue is:
|
|
1. When a model field has a callable default, Django's form field gets `show_hidden_initial=True`
|
|
2. This is because the callable is evaluated, making it appear the field has an initial value that could change
|
|
3. However, for fields with callable defaults that return empty/default values (like `list`), this causes issues with `has_changed()` detection
|
|
|
|
Looking at Django's code, the fix should be in `django/forms/models.py` where `show_hidden_initial` is set based on whether the field has a callable default. The specific issue is that when the default is callable, the form field shouldn't automatically get `show_hidden_initial=True` just because the initial value was derived from a callable.
|
|
|
|
The fix should be in the function that constructs form fields from model fields. The `show_hidden_initial` should not be set to `True` when the initial value comes from a callable default on the model field.
|
|
|
|
Here's the patch:
|
|
|
|
--- a/django/forms/models.py
|
|
+++ b/django/forms/models.py
|
|
@@ -92,7 +92,10 @@ def construct_instance(form, instance, fields=None, exclude=None):
|
|
def apply_limit_choices_to_to_formfield(formfield):
|
|
"""Apply limit_choices_to to the formfield's queryset if needed."""
|
|
from django.db.models import Exists, OuterRef, Q
|
|
- if hasattr(formfield, 'queryset') and hasattr(formfield, 'get_limit_choices_to'):
|
|
+ if hasattr(formfield, "queryset") and hasattr(formfield, "get_limit_choices_to"):
|
|
limit_choices_to = formfield.get_limit_choices_to()
|
|
if limit_choices_to:
|
|
complex_filter = limit_choices_to
|
|
@@ -188,6 +191,10 @@ class ModelForm(BaseForm, metaclass=ModelFormMetaclass):
|
|
def 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):
|
|
+ """
|
|
+ Return a ModelForm containing form fields for the given model. You can
|
|
+ optionally pass a `form` argument to use as a starting point for
|
|
+ constructing the ModelForm.
|
|
--- a/django/forms/fields.py
|
|
+++ b/django/forms/fields.py
|
|
@@ -136,8 +136,10 @@ class Field:
|
|
def has_changed(self, initial, data):
|
|
"""Return True if data differs from initial."""
|
|
# Always return False if the field is disabled since self.bound_data
|
|
- # always uses the initial value in this case.
|
|
+ # always uses the initial value in this case.
|
|
if self.disabled:
|
|
return False
|
|
try:
|
|
data = self.to_python(data)
|
|
|
|
Let me reconsider the actual fix based on the issue description more carefully. The issue is specifically about:
|
|
|
|
1. A field with `default=list` (callable)
|
|
2. On first submit with error, the hidden initial value gets the user's input
|
|
3. On second submit, validation is bypassed
|
|
|
|
The root cause is that `show_hidden_initial` is being set to `True` for fields with callable defaults. The fix should ensure that when a form field's initial value is derived from a callable default on a model field, `show_hidden_initial` should be set based on whether the callable actually produces a value that could change between requests.
|
|
|
|
Looking at the Django code flow:
|
|
1. `modelform_defines_fields` checks fields
|
|
2. `fields_for_model` creates form fields from model fields
|
|
3. For each field, `formfield()` is called which sets `show_hidden_initial`
|
|
|
|
The fix should be in how `show_hidden_initial` is determined. When the initial value comes from evaluating a callable default, the resulting value should be used for the hidden initial comparison, not the fact that it came from a callable.
|
|
|
|
--- a/django/forms/models.py
|
|
+++ b/django/forms/models.py
|
|
@@ -43,6 +43,9 @@ def construct_instance(form, instance, fields=None, exclude=None):
|
|
def model_to_dict(instance, fields=None, exclude=None):
|
|
"""
|
|
Return a dict containing the data in ``instance`` suitable for passing as
|
|
@@ -86,7 +89,14 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
|
|
formfield_callback=None, localized_fields=None,
|
|
labels=None, help_texts=None, error_messages=None,
|
|
field_classes=None, *, apply_limit_choices_to=True):
|
|
- """
|
|
+ """Return a dictionary containing form fields for the given model.
|
|
+
|
|
+ ``fields`` is an optional list of field names. If provided, return only the
|
|
+ named fields.
|
|
+
|
|
+ ``exclude`` is an optional list of field names. If provided, exclude the
|
|
+ named fields from the returned fields, even if they are listed in the
|
|
+ ``fields`` argument.
|
|
field_dict = {}
|
|
ignored = []
|
|
opts = model._meta
|
|
|
|
Based on the issue, the actual fix needs to be in how the initial value is propagated when a callable default is involved. The problem is in `django/forms/models.py` in the function that creates form fields.
|
|
|
|
--- a/django/forms/models.py
|
|
+++ b/django/forms/models.py
|
|
@@ -92,6 +92,9 @@ def fields_for_model(
|
|
continue
|
|
kwargs = {}
|
|
+ if form_field is not None and f.has_default() and callable(f.default):
|
|
+ kwargs["show_hidden_initial"] = False
|
|
if widgets and f.name in widgets:
|
|
kwargs["widget"] = widgets[f.name]
|
|
if localized_fields == ALL_FIELDS or (
|
|
|
|
Wait, I need to be more precise about the location of the fix. Let me provide the correct patch based on Django's actual codebase structure:
|
|
|
|
--- a/django/forms/models.py
|
|
+++ b/django/forms/models.py
|
|
@@ -47,6 +47,12 @@ __all__ = (
|
|
ALL_FIELDS = "__all__"
|
|
|
|
|
|
+def _get_default_value(field):
|
|
+ """Return the default value for a model field, calling it if callable."""
|
|
+ if callable(field.default):
|
|
+ return field.default()
|
|
+ return field.default
|
|
+
|
|
+
|
|
def construct_instance(form, instance, fields=None, exclude=None):
|
|
"""
|
|
Construct and return a model instance from the bound ``form``'s
|
|
@@ -91,6 +97,10 @@ def fields_for_model(
|
|
kwargs = {}
|
|
if widgets and f.name in widgets:
|
|
kwargs["widget"] = widgets[f.name]
|
|
+ # Don't show hidden initial for fields with callable defaults,
|
|
+ # as the value has already been evaluated.
|
|
+ if f.has_default() and callable(f.default):
|
|
+ kwargs["show_hidden_initial"] = False
|
|
if localized_fields == ALL_FIELDS or (
|
|
localized_fields and f.name in localized_fields
|
|
):
|
|
|
|
Actually, looking at this more carefully, the real issue is about how form fields determine `show_hidden_initial`. The issue is that when a model field has a callable default, the form field's `show_hidden_initial` becomes True, which causes issues with the `has_changed()` detection.
|
|
|
|
Let me provide the correct minimal patch:
|
|
|
|
--- a/django/forms/models.py
|
|
+++ b/django/forms/models.py
|
|
@@ -93,6 +93,9 @@ def fields_for_model(
|
|
kwargs = {}
|
|
if widgets and f.name in widgets:
|
|
kwargs["widget"] = widgets[f.name]
|
|
+ # Fields with callable defaults should not show hidden initial.
|
|
+ if f.has_default() and callable(f.default):
|
|
+ kwargs["show_hidden_initial"] = False
|
|
if localized_fields == ALL_FIELDS or (
|
|
localized_fields and f.name in localized_fields
|
|
):
|