Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Comparison of three ways of assigning excluded fields to a model when creating via a form
# Question - how best to handle model fields that are excluded from a form's
# fields.
# ------------------------------------------------------------
# Option 1: assign fields to instance before passing into form
# ------------------------------------------------------------
# views.py
instance = Vote(review=review, user=request.user)
form = VoteForm(data=request.POST, instance=instance)
# --------------------------------------------------------
# Option 2: pass excluded field values to form constructor
# --------------------------------------------------------
# views.py
form = VoteForm(data=request.POST, review=review, user=request.user,
instance=instance)
# forms.py
class VoteForm(forms.ModelForm):
def __init__(self, review, user, *args, **kwargs):
super(VoteForm, self).__init__(*args, **kwargs)
self.instance.review = review
self.instance.user = user
# -----------------------------------------------------------------------------
# Option 3: pass excluded field values to form constructor but defer assignment
# -----------------------------------------------------------------------------
# views.py
form = VoteForm(data=request.POST, review=review, user=request.user,
instance=instance)
# forms.py
class VoteForm(forms.ModelForm):
def __init__(self, review, user, *args, **kwargs):
self.review = review
self.user = user
super(VoteForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
vote = super(VoteForm, self).save(commit=False)
vote.review = self.review
vote.user = self.user
if commit:
vote.save()
return vote
# (This method is bad as the model's clean method doesn't have access to the
# review and user instances).
# Option 1 is best right? (Confession - I've been using option 2 for ages)
@howieweiner
Copy link

howieweiner commented Aug 7, 2013

Are you using CBVs? If so, why not simply use the Meta.fields attribute to define the required fields for the form and use an UpdateView. Job done.

@AndrewIngram
Copy link

AndrewIngram commented Aug 7, 2013

I'm not sure I understand what you're trying to do. If you're creating an object, you don't normally have an instance to pass to the modelform's constructor. you only have an instance to play with after you call save.

@codeinthehole
Copy link
Author

codeinthehole commented Aug 8, 2013

@howieweiner Yes, I normally use CBVs but I omitted the CBV methods here for brevity. However, using Meta.fields doesn't solve this problem as I am interested in the required fields that arent' in Meta.fields - for instance, lots of models require a user field which you would take from the request, not from the submitted form data.

@AndrewIngram You're right that when creating an object you don't have a saved instance to pass to the constructor, but it's valid to construct your own unsaved instance and pass that. As in option 1, you can also assign the fields that aren't included in the form fields too. Here's an example from Oscar where this technique is used: https://github.com/tangentlabs/django-oscar/blob/master/oscar/apps/catalogue/reviews/views.py#L59-62

@howieweiner
Copy link

howieweiner commented Aug 8, 2013

Right. Bit clearer on what you're trying to achieve. Just took a look at how django-braces implement their UserFormKwargsMixin/UserKwargModelFormMixin. They update kwargs in the view, and pop off the args in the form. I've seen this approach for User on Stackoverflow too. Slightly more succint version of option 2.

# views.py (mixin)
def get_form_kwargs(self):
    kwargs = super(UserFormKwargsMixin, self).get_form_kwargs()
    # Update the existing form kwargs dict with the request's user.
    kwargs.update({"user": self.request.user})
    return kwargs
# forms.py (mixin)
def __init__(self, *args, **kwargs):
    self.user = kwargs.pop("user", None)  # Pop the user off the
                                          # passed in kwargs.
    super(UserKwargModelFormMixin, self).__init__(*args, **kwargs)

@AndrewIngram
Copy link

AndrewIngram commented Aug 8, 2013

I think a lot of the confusion is because modelforms confused the concept of what a form actually is. They change the domain from validation and sanitisation to also include constructing and saving instances of models. Assuming you don't need the excluded fields in order to construct or validate the form, you should really be calling .save(commit=False) and then assigning the missing fields in the view. Of course Django's class-based view make this approach a little bit yucky.

Howie's approach is essentially Option 3, and that's the one I normally use. With Options 1 and 2 you're breaking the contract of a modelform in that they can be passed instance=None and be instantiated as an unbound form. And with Option 2 the form will explode if instance is None, because you're trying to assign to it in the constructor.

The other thing I like about Option 3 is it keeps all the form code other than save blissfully unaware of what a model instance is, which helps mitigate the yuck that is modelforms.

@codeinthehole
Copy link
Author

codeinthehole commented Aug 8, 2013

Assuming you don't need the excluded fields in order to construct or validate the form, ...

I think that's quite a big assumption. Often you do need the excluded fields for validation and they will only be available to the model's clean method if they are on the form's instance property.

That's the main problem with option 3 or assigning them in the view and why I'm leaning towards using option 1 (or 2) henceforth. As a rule of thumb, it seems more robust.

With Options 1 and 2 you're breaking the contract of a modelform in that they can be passed instance=None and be instantiated as an unbound form.

Whether a form is bound or not only depends on whether data is None - the passed instance makes no difference. When instance=None is passed, then the model form just creates an empty model instance:
https://github.com/django/django/blob/stable/1.5.x/django/forms/models.py#L236-241

Similarly, I don't think option 2 would explode as the instance property is always assigned in the model form constructor.

Does that make sense?

@AndrewIngram
Copy link

AndrewIngram commented Aug 8, 2013

I think that's quite a big assumption. Often you do need the excluded fields for validation and they will only be available to the model's clean method if they are on the form's instance property.

Is it really that often? I can't recall it ever happening. Do people even use model validation in the real world? If option 3 is fundamentally unworkable for your use case, why is it an option?

Anyway, if you have to choose between Option 1 and 2, I'd go with 2. With option 1 you can't just look at the form's init method and see that we require a review and a user just to make the form work. If you forget to partially build the model before instantiating the form it'll break, and in a non-obvious way. Especially if it only breaks in the model's clean method, because there is potentially zero indication in the form code itself that those fields are essential.

@codeinthehole
Copy link
Author

codeinthehole commented Aug 8, 2013

Do people even use model validation in the real world?

True - but it's something I'm going to do more.

Your points about option 1 are excellent - I was just attracted by the brevity of it as a solution (ie you don't need to customise the form). If you forget to partially construct the instance then it doesn't fail silently, but you're right that the failure is non-obvious. Option 2 is more explicit and gives a more meaningful error if you don't pass the required fields.

Thanks for the feedback.

@patrys
Copy link

patrys commented Aug 8, 2013

I vote for #1. It keeps the form limited to what it really does. And unlike .save(commit=False) it allows you to validate the values in their destination context (e.g. limit some choices based on values of uneditable fields).

@skyjur
Copy link

skyjur commented Aug 8, 2013

I like #1.

Also on the same topic, in a response to @howieweiner post I don't like things like this:

def __init__(self, *args, **kwargs):
    kwargs.pop('user')

Makes it really hard to understand how I need to use this class. I want to see more explicit definition, at least like this:

def __init__(self, user, *args, **kwargs):

Yes django forms has many kwargs but all are well documented and django form is generic. Form that you write probably won't be well documented and is for a very specific use case.

For example in most forms you won't need files kwarg, and the same thing applies for most of other.

I think that in some cases it is a good idea to redefine __init__ with only the attributes that you are guaranteed that your form will support:

def __init__(self, user, data=None, initial=None, instance=None):

@skyjur
Copy link

skyjur commented Aug 8, 2013

Also I vote for comment on #1 by @AndrewIngram:

Anyway, if you have to choose between Option 1 and 2, I'd go with 2. With option 1 you can't just look at the form's init method and see that we require a review and a user just to make the form work.

Very well noted. But there are also many cases where form actually doesn't need those excluded fields to work in this case #1 should be fine.

@codeinthehole
Copy link
Author

codeinthehole commented Sep 11, 2013

For the record, I think option 2 is better for Oscar as the API is simpler (as @AndrewIngram pointed out). It's more cryptic when you have to assign things to the instance argument.

However, option 1 is slightly easier to unit test as you don't have to create instances of a review and user to pass to the constructor.

@AndrewIngram
Copy link

AndrewIngram commented Sep 11, 2013

Assuming you're using factories, I'm not sure it's much of a problem to generate a review and a user.

@AndrewIngram
Copy link

AndrewIngram commented Sep 11, 2013

Alternatively, just pass in None if your test doesn't hit the part of the code where review and user are actually required to be anything in particular. And if it does hit those parts, you're gonna need to construct proper instances anyway.

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