Skip to content

Instantly share code, notes, and snippets.

@powderflask
Last active November 17, 2021 06:39
Show Gist options
  • Save powderflask/de0d3d6c6bef6c1b3890bdc1e83ff05c to your computer and use it in GitHub Desktop.
Save powderflask/de0d3d6c6bef6c1b3890bdc1e83ff05c to your computer and use it in GitHub Desktop.
"""
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