-
-
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'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.
@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
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)
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.
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?
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.
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.