Opened 12 years ago
Closed 11 years ago
#21753 closed Cleanup/optimization (fixed)
Generic editing views should raise ImproperlyConfigured if both `form_class` and `fields` are specified
| Reported by: | Gabe Jackson | Owned by: | Berker Peksag |
|---|---|---|---|
| Component: | Generic views | Version: | dev |
| Severity: | Normal | Keywords: | generic edit views form_class fields ModelForm |
| Cc: | Triage Stage: | Accepted | |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
The addition of the fields property to ModelFormMixin for generic form views allows to set fields for the given model as documented here: https://docs.djangoproject.com/en/1.6/ref/class-based-views/mixins-editing/#django.views.generic.edit.ModelFormMixin.fields
This allows the following:
class Author(models.Model):
name = models.CharField(max_length=60)
url = models.CharField(max_length=60)
class AuthorUpdate(UpdateView):
model = Author
fields = ['name']
Which will result in a form with only the field name.
Unfortunately, it is currently impossible to use the fields in combination with the form_class attribute. This means you cannot override fields with custom ModelForms:
class Author(models.Model):
name = models.CharField(max_length=60)
url = models.CharField(max_length=60)
class AuthorForm(ModelForm):
# Customization ...
class Meta:
model = Author
class AuthorUpdate(UpdateView):
model = Author
form_class = AuthorForm
fields = ['name']
This will result in a form with all fields of Author.
This is due to the implementation of get_form_class doing this:
def get_form_class(self):
"""
Returns the form class to use in this view.
"""
if self.form_class:
return self.form_class
else:
if self.model is not None:
# If a model has been explicitly provided, use it
model = self.model
elif hasattr(self, 'object') and self.object is not None:
# If this view is operating on a single object, use
# the class of that object
model = self.object.__class__
else:
# Try to get a queryset and extract the model class
# from that
model = self.get_queryset().model
if self.fields is None:
warnings.warn("Using ModelFormMixin (base class of %s) without "
"the 'fields' attribute is deprecated." % self.__class__.__name__,
PendingDeprecationWarning)
return model_forms.modelform_factory(model, fields=self.fields)
It is apparent that fields gets ignored if form_class is set.
I attached a patch including a test case and fix for this issue/feature.
This could be classified as a bug, since no warning is raised when using form_class combined with fields.
Attachments (2)
Change History (10)
by , 12 years ago
| Attachment: | 0001-Allow-generic-editing-views-to-use-form_class-and-fi.patch added |
|---|
by , 12 years ago
| Attachment: | 0002-Allow-generic-editing-views-to-use-form_class-and-fi.patch added |
|---|
comment:1 by , 12 years ago
To be specific about customization in the ModelForm: Customization means either: defining fields in Meta, adding further base classes, overriding ModelForm methods (for instance init for to add a django-crispy-form helper and layout).
There is a discussion point here which was discussed with timograham in #django-dev:
Should the behavior be:
a) Respect the fields in form_class's Meta even if fields is defined in the View
b) Respect fields of the View even if fields of the form_class's Meta is set
timograham's opinion on this is a) "I would expect fields to be used on the view only if they aren't specified on the form"
My opinion is that fields of the View should always be respected since they allow more specific functionality of the form within the view, specifically removing fields for custom views where data is added to the create Model, say in form_valid. A simple use case for this is the following model:
class Voucher(models.Model):
user = models.ForeignKey(get_user_model())
amount = models.DecimalField(...)
class VoucherForm(ModelForm):
user = autocomplete_light.GenericModelChoiceField(
label='User',
widget=autocomplete_light.ChoiceWidget(
autocomplete='AutocompleteUser',
autocomplete_js_attributes={
'minimum_characters': 1,
'placeholder': u'Start typing to find a user'
},
)
)
class Meta:
model = Voucher
def clean_amount(self):
amount = self.cleaned_data['amount']
if amount < 20:
raise forms.ValidationError("amount must be greater than 20")
return amount
def __init__(self, *args, **kwargs):
super(VoucherForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(.....)
class VoucherCreateView(CreateView):
model = Voucher
form_class = VoucherForm
class VoucherCreateForUserView(CreateView):
model = Voucher
form_class = VoucherForm
fields = ('amount',)
def form_valid(self, form):
self.object = form.save(commit=False)
# Get user from somewhere...
self.object.user = get_user_model().objects.get(pk=self.kwargs['user_id'])
self.object.save()
return HttpResponseRedirect(self.get_success_url())
With the current approach (as in Django 1.6) or according to opinion a) you must create a ModelForm for each of these use cases redefining Meta.fields.
timo and I agree that there at least but be some kind of warning when using form_class and fields together.
comment:2 by , 12 years ago
I would suggest this should only give a warning, possibly even ImproperlyConfigured, not change the form. From options a) and b) above it is not clear what the "correct" approach is - and either has the potential to confuse. I do not want class based views to fall into a trap of allowing myriad hooks to customise the form class, like form.Meta allows with fields. The reason why fields was introduced on these views is to allow you to ensure security when using an auto generated form_class. If you're setting the form_class, you can obviously set up the form_class correctly.
Personally, I'd rather get rid of automatic form_class generation all together, but that's another story.
comment:3 by , 12 years ago
| Component: | Forms → Generic views |
|---|---|
| Triage Stage: | Unreviewed → Accepted |
comment:4 by , 12 years ago
Main argument from discussion with mjtamyln: "one of the common criticisms of our CBVs is that they have too many customisation points and do too much magic (or implicit behaviour). Let's not add any more"
Generally fatter forms and thinner views are recommended. The above use-case should be solved by subclassing ModelForm and adapting the fields, then use the new ModelForm in the View.
Dynamic form creation dependent of request data can be done by passing data to Form.__init__() and adapting the fields there. Alternatively one can change the form_class in get_form_class by doing something like: return SuperForm if self.request.user.is_superuser else NormalForm.
It should be noted in the docs that fields was added to View from a security point of view, not for customization of forms.
The question remains if using form_class and fields together should raise an ImproperlyConfigured or just fail silently. Compare this to the approach of using queryset vs model in a View: This will fail silently preferring queryset over model. This seems logical because queryset allows more fine-grained control. Does the same hold true for fields and form_class? One might expect that fields will just work with form_class, or one might make the reverse assumption that fields defined in form_class will override what is in fields of the View.
to be discussed.
comment:5 by , 12 years ago
| Patch needs improvement: | set |
|---|---|
| Summary: | allow generic editing views to use `form_class` and `fields` together → Generic editing views should raise ImproperlyConfigured if both `form_class` and `fields` are specified |
| Type: | New feature → Cleanup/optimization |
I would like to see ImproperlyConfigured as I don't see any benefit to failing silently.
comment:6 by , 11 years ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
comment:7 by , 11 years ago
| Patch needs improvement: | unset |
|---|
comment:8 by , 11 years ago
| Resolution: | → fixed |
|---|---|
| Status: | assigned → closed |
PEP8 remove whitespace