Skip to content

Instantly share code, notes, and snippets.

@dracos
Last active December 18, 2015 11:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dracos/5778389 to your computer and use it in GitHub Desktop.
Save dracos/5778389 to your computer and use it in GitHub Desktop.
Upgrading Django 1.1 to 1.5 validation issue

I'm upgrading an old Django 1.1 project to a clean Django 1.5 installation. The Django upgrade itself has gone very well; {% url %} changes, settings, some class-based generic views, some core patches replaced with a custom User model, others done with nicer middleware, and so on, hooray.

What I didn't expect to stop working, but has, is a model creation form that lets someone add a related object at the same time. In the code, this is done using a MultiValueField subclass:

class AutoCompleteMultiValueField(forms.MultiValueField):
    def __init__(self, model, column, *args, **kwargs):
        self.model = model
        self.column = column
        super(AutoCompleteMultiValueField, self).__init__(*args, **kwargs)

    def compress(self, data_list):
        if not data_list:
            return None
        if data_list[0] and not data_list[1]:
            data_list[1] = self.model(**{self.column: data_list[0]})
        return data_list[1]

Which is used in the following form:

class ProductionForm(forms.ModelForm):
    ...
    play = AutoCompleteMultiValueField(
        Play, 'title',
        required = False, # It is required, but will be spotted in the clean function
        fields = (StripCharField(), forms.ModelChoiceField(Play.objects.all())),
        widget = ForeignKeySearchInput(Production.play.field.rel, ('title',))
    )
    ...
    def clean_play(self):
        if not self.cleaned_data['play']:
            raise forms.ValidationError('You must specify a play.')
        ...
    def save(self, **kwargs):
        if not self.cleaned_data['play'].id:
            self.cleaned_data['play'].save()
        return super(ProductionForm, self).save(**kwargs)

Where ForeignKeySearchInput is just a widget that displays the two fields as a text input and a hidden input, attached to some auto-complete JavaScript. So you can type in a play title and pick from a drop-down that appears (and then the StripCharField gets the text of the play title, and the ModelChoiceField is its ID), but it still lets you type a title that doesn't match anything already in the database (then the text input is your new play title, and the ModelChoiceField is empty).

Currently, running with Django 1.1, this works fine - the form validates, play is either an existing Play object or a new Play object created by the AutoCompleteMultiValueField, and the form's save() function saves the Play before saving the new Production.

Django 1.5, on the other hand, before the form save happens, calls construct_instance() in the ModelForm's _post_clean() and then tries to validate the model. As the model, correctly, says that a Production must have a Play, this raises a "play cannot be null" error. And I can't work out the correct way to get this working again. Any help would be appreciated, do let me know if more details would be helpful.

@dracos
Copy link
Author

dracos commented Jun 14, 2013

I guess I can stop it being a ModelForm and just a Form and do the save()ing in the right order manually. But it feels like something I should be able to do, especially as I could before.

@dracos
Copy link
Author

dracos commented Jun 15, 2013

With thanks to Marco Fucci on Twitter for the pointer, I've subclassed _get_validation_exclusions (though it's an internal method, at least it's in my code not Django like this project originally might have done!):

def _get_validation_exclusions(self):
    """Add 'play' to the list of columns we don't want model validation
       for, as it might correctly be null at this point."""
    exclusions = super(ProductionForm, self)._get_validation_exclusions()
    exclusions.append('play')
    return exclusions

I then also need to alter the save() because self.instance has already been created by this point and saving a related model doesn't update the ID on the parent. See Django #8892 for details - though the proposal there to error if you even try and assign scares me, as that's precisely what I'm doing here, and I don't particularly want this to break again in future:

def save(self, **kwargs):
    if not self.cleaned_data['play'].id:
        self.cleaned_data['play'].save()
        # Must reattach the now-saved play to the instance to pass in the I
        self.instance.play = self.cleaned_data['play']
    return super(ProductionForm, self).save(**kwargs)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment