Last active
November 17, 2021 06:39
-
-
Save powderflask/de0d3d6c6bef6c1b3890bdc1e83ff05c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Patch for https://code.djangoproject.com/ticket/32244 | |
Concept: | |
- define a ModelPkField intended specifically to define a read-only pk field. | |
- validates that the pk value in the post data matches the form's instance, (or that the pk value is None if form has no instance) | |
i.e., verify that no one tampered with the form's pk data on the round trip | |
- ModelFormSet uses this field type to define these hidden pk fields, and returns the form's original instance if it validates | |
""" | |
from django.core.exceptions import ValidationError | |
from django.forms.fields import Field | |
from django.forms.formsets import BaseFormSet | |
from django.forms.models import BaseModelFormSet | |
from django.forms.widgets import HiddenInput | |
from django.utils.translation import gettext_lazy as _ | |
class ModelPkField(Field): | |
""" | |
A basic integer field that deals with validating that a pk value matches the ModelForm instance. | |
Validation simply ensures the pk field was not tampered with on round-trip. | |
Useful in a formset where a pk field is needed to uniquely id each form in the set. | |
Validation error is treated as an error on the whole form rather than this specific field, which is usually hidden. | |
""" | |
widget = HiddenInput | |
default_error_messages = { | |
'corrupt_form': _('Corrupted form detected. Reload page before saving.'), | |
} | |
def __init__(self, form, *args, **kwargs): | |
self.form = form | |
self.new_instance = form.instance._state.adding | |
kwargs["initial"] = None if self.new_instance else self.form.instance.pk | |
kwargs["required"] = True | |
super().__init__(*args, **kwargs) | |
def clean(self, value): | |
try: | |
value = super().clean(value) | |
except ValidationError as e: | |
self.form.add_error(None, e.message) | |
value = None | |
is_valid = ( | |
(value in self.empty_values and self.new_instance) # new instance, empty value or... | |
or | |
str(value) == str(self.form.instance.pk) # form value matches form instance | |
) | |
if not is_valid: | |
self.form.add_error(None, self.error_messages['corrupt_form']) | |
return self.form.instance # the instance is always the cleaned value | |
def has_changed(self, initial, data): | |
return False | |
class EfficientModelFormSet(BaseModelFormSet): | |
""" | |
Uses a ModelPkField instead of a ModelChoiceField to inject hidden pk field into form | |
This prevents a spurious DB hit for every form instance being validated. | |
Code for add_fields lifted from django2.2.22 - no clean way to inject the form field type. | |
""" | |
def add_fields(self, form, index): | |
"""Add a hidden field for the object's primary key.""" | |
from django.db.models import AutoField, OneToOneField, ForeignKey | |
self._pk_field = pk = self.model._meta.pk | |
# If a pk isn't editable, then it won't be on the form, so we need to | |
# add it here so we can tell which object is which when we get the | |
# data back. Generally, pk.editable should be false, but for some | |
# reason, auto_created pk fields and AutoField's editable attribute is | |
# True, so check for that as well. | |
def pk_is_not_editable(pk): | |
return ( | |
(not pk.editable) or (pk.auto_created or isinstance(pk, AutoField)) or ( | |
pk.remote_field and pk.remote_field.parent_link and | |
pk_is_not_editable(pk.remote_field.model._meta.pk) | |
) | |
) | |
if pk_is_not_editable(pk) or pk.name not in form.fields: | |
# if form.is_bound: | |
# # If we're adding the related instance, ignore its primary key | |
# # as it could be an auto-generated default which isn't actually | |
# # in the database. | |
# pk_value = None if form.instance._state.adding else form.instance.pk | |
# else: | |
# try: | |
# if index is not None: | |
# pk_value = self.get_queryset()[index].pk | |
# else: | |
# pk_value = None | |
# except IndexError: | |
# pk_value = None | |
# if isinstance(pk, (ForeignKey, OneToOneField)): | |
# qs = pk.remote_field.model._default_manager.get_queryset() | |
# else: | |
# qs = self.model._default_manager.get_queryset() | |
# qs = qs.using(form.instance._state.db) | |
if form._meta.widgets: | |
widget = form._meta.widgets.get(self._pk_field.name, HiddenInput) | |
else: | |
widget = HiddenInput | |
form.fields[self._pk_field.name] = ModelPkField(form=form, required=False, widget=widget) | |
super(BaseModelFormSet, self).add_fields(form, index) # skip over BaseModelFormset add_fields |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment