-
-
Save codeinthehole/6178034 to your computer and use it in GitHub Desktop.
# 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) |
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.
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.
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).
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):
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.
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.
Assuming you're using factories, I'm not sure it's much of a problem to generate a review and a user.
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.
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'sinstance
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.
Whether a form is bound or not only depends on whether
data
is None - the passed instance makes no difference. Wheninstance=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?