From this question I want to convert my form from regular Form to ModelForm so I can take advantage of instance parameter in ModelForm.
Here is my current form code:
class OrderDetailForm(forms.Form):
def __init__(
self,
user,
can_edit_work_type=None,
can_edit_vendor=None,
can_edit_note=None,
*args,
**kwargs
):
super(OrderDetailForm, self).__init__(*args, **kwargs)
if can_edit_work_type:
self.fields['work_type'] = forms.ChoiceField(choices=Order.WORK_TYPE_CHOICES)
if can_edit_vendor:
self.fields['vendor'] = forms.ModelChoiceField(
queryset=Vendor.objects.all(),
empty_label="Choose a vendor",
)
if can_edit_note:
self.fields['note'] = forms.CharField(widget=forms.Textarea)
def clean(self):
super(OrderDetailForm, self).clean()
if 'note' in self.cleaned_data:
if len(self.cleaned_data['note']) < 50:
self._errors['note'] = self.error_class([u"Please enter a longer note."])
del self.cleaned_data['note']
return self.cleaned_data
As you can see, I have some if statements that determine whether the fields even show you in the forms (logically it means certain users can only edit certain parts of the fields).
How would I do that in ModelForm? I understand fields to be a tuple, so it can't be appended like I did in Form. So I want to do something like
class OrderDetailForm(forms.ModelForm):
class Meta:
model = Order
# fields = ('work_type', 'vendor', 'note') I can't do that since I need to be able to control it. See below.
# Can I control widgets even if that field doesn't exist?
widgets = {
'note': forms.Textarea(),
}
def __init__(
self,
user,
can_edit_work_type=None,
can_edit_vendor=None,
can_edit_note=None,
*args,
**kwargs
):
super(OrderDetailForm, self).__init__(*args, **kwargs)
fields = []
if can_edit_work_type:
fields.append('work_type')
if can_edit_vendor:
fields.append('vendor')
if can_edit_note:
fields.append('note')
self.Meta.fields = tuple(fields) # Does this work?
def clean(self):
super(OrderDetailForm, self).clean()
if 'note' in self.cleaned_data:
if len(self.cleaned_data['note']) < 50:
self._errors['note'] = self.error_class([u"Please enter a longer note."])
del self.cleaned_data['note']
return self.cleaned_data
Is that possible? How do you control the fields in ModelForm?
Another possible way is generate a inline form class in the view to exclude fields based on the request, for example, define a normal model form for Order model, called OrderDetailForm:
class OrderDetailForm(forms.ModelForm):
class Meta:
model = Order
fields = ('work_type', 'vendor', 'note')
widgets = {
'note': forms.Textarea(),
}
In the view, for example, edit order, create a customized form based on the OrderDetailForm:
def edit(request, order_id):
order = Order.objects.get(pk=order_id)
can_edit_work_type = bool(request.REQUEST.get('can_edit_work_type', False))
can_edit_vender = bool(request.REQUEST.get('can_edit_vender', False))
can_edit_note = bool(request.REQUEST.get('can_edit_note', False))
exclude_fields = []
if not can_edit_work_type:
exclude_fields.append('work_type')
if not can_edit_vender:
exclude_fields.append('vender')
if not can_edit_note:
exclude_fields.append('note')
class CustomizedOrderForm(OrderDetailForm):
class Meta:
model = Order
exclude = tuple(exclude_fields)
if request.method == 'POST':
form = CustomizedOrderForm(instance=order, data=request.POST)
if form.is_valid():
form.save()
else:
form = CustomizedOrderForm(instance=order)
return render(request, 'order_form.html', {'form': form})
The ModelForm api is very similar to that of the regular Form. The advantage is that you now get model validation in addition to conveniences like default widgets, the instance kwarg, and the save method.
fields attr is still dict-like. You can see fields getting built by the metaclass here. Then, going through the inheritance and calling super() in the BaseModelForm.__init__, we arrive at a deepcopy of the declared fields, originally a SortedDict. This is common to Form and ModelForm, both subclasses of BaseForm.
Put the fields in the exclude and add them the way you are doing in the original __init__.
Clean them the same way.
Then, you can override the save method: you can call super() to get the object back and deal with the data in cleaned_data however you want.
class OrderDetailForm(forms.ModelForm):
# regular fields, not based on bools
# ...
class Meta:
model = Order
exclude = ('work_type', 'vendor', 'note')
# or fields = (...other fields )
def __init__(
self,
user,
can_edit_work_type=None,
can_edit_vendor=None,
can_edit_note=None,
*args,
**kwargs,
):
super(OrderDetailForm, self).__init__(*args, **kwargs)
if can_edit_work_type:
self.fields['work_type'] = forms.ChoiceField(
choices=Order.WORK_TYPE_CHOICES)
if can_edit_vendor:
self.fields['vendor'] = forms.ModelChoiceField(
queryset=Vendor.objects.all(),
empty_label="Choose a vendor",
)
if can_edit_note:
self.fields['note'] = forms.CharField(widget=forms.Textarea)
def clean(self):
# I never call super() in clean .. do I? .. hmmm
# maybe I should or is sth magic going on?
# alternately,
# data = self.cleaned_data
# let's call super though
data = super(OrderDetailForm, self).clean()
if 'note' in data:
if len(data['note']) < 50:
# I raise a validation error so .is_valid() comes back False
# form.errors happens magically ...
raise forms.ValidationError("Not long enough ...")
return data
def save(self, *args, **kwargs):
data = self.cleaned_data
# maybe do some stuff here
# ...
# commit=True or commit=False could be important
order = super(OrderDetailForm, self).save(*args, **kwargs)
if 'note' in data:
order.note = data['note']
# ... do other stuff
# probably ...
order.save()
# respect how model forms work.
return order
Related
class MyCustomInline(admin.TabularInline):
min_num = 1
extra = 0
fields = ['matcher', 'param0', 'param1']
model = MyModel
form = MyCustomInlineForm
def get_formset(self, request, obj=None, **kwargs):
extra_fields = {
'param0': forms.CharField(label='First Param', required=False),
'param1': forms.CharField(label='Second Param', required=False)
}
kwargs['form'] = type('MyCustomInline', (MyCustomInlineForm,), extra_fields)
return super(MyCustomInline, self).get_formset(request, obj, **kwargs)
This is basically how I define my inline form so that it has two extra fields - matcher is a standard field in the related table and the inline form handles it automatically. And I save the extra param values in a different storage via overriding the save() in MyCustomInlineForm.
But if I edit an existing record - matcher value appears correctly but I obviously also want to preload the param0 and param1 with the corresponding values. Where can I hook up to do that?
I managed to do it on my own. I also managed to simplify the way I define my custom extra fields, without overriding get_formset method:
class MyCustomInlineForm(forms.ModelForm):
matcher = forms.ChoiceField(choices=[(v['name'], v['name']) for v in matchers], label='Matcher')
param0 = forms.CharField(label='First Param', required=False)
param1 = forms.CharField(label='Second Param', required=False)
def __init__(self, *args, **kwargs):
super(MyCustomInlineForm, self).__init__(*args, **kwargs)
if self.instance.pk:
""" self.instance is the model for the current row.
If there is a pk property that is not None, it means it's not
a new, empty inline model but we are working with existing one."""
self.initial['param0'], self.initial['param1'] = custom_way_to_load_params(self.instance)
def save(self, commit=True):
model = super(MyCustomInlineForm, self).save(True)
param0 = self.cleaned_data['param0']
param1 = self.cleaned_data['param1']
custom_way_to_save_params(model, param0, param1)
return model
class MyCustomInline(admin.TabularInline):
min_num = 1
extra = 0
fields = ['matcher', 'param0', 'param1']
model = MyModel
form = MyCustomInlineForm
If needed - validation of custom params could be done by overriding is_valid() method of forms.ModelForm class and adding errors via self.add_error(). I hope it helps someone.
I have a form that I need to show my project outside the area of administration, some fields can not be edited but can see them.
To do this would be great "AdminReadonlyField" found in "django.contrib.admin.helpers" The problem is that you can not do.
I have tried to create some widget that can replace this complex class, but I can not get it to work properly with DateTiemField fields.
class UserUpdateForm(forms.ModelForm):
"""
We need field "date_joined" can not be edited but can see them
"""
class Meta:
model = User
fields = ('first_name', 'last_name',
'email', 'date_joined', 'slug')
def __init__(self, user, *args, **kwargs):
kwargs['instance'] = user
super(UserUpdateForm, self).__init__(*args, **kwargs)
self.fields['date_joined'].widget = widgets.CMDateTimeText()
def clean_date_joined(self):
instance = getattr(self, 'instance', None)
if instance and instance.pk:
return instance.date_joined
else:
return self.cleaned_data['date_joined']
My code, something is not right.
class CMDateTimeText(DateTimeBaseInput):
"""
A SplitDateTime Widget that has some admin-specific styling.
Hereda Field and Widget.
"""
format_key = 'DATETIME_FORMAT'
def __init__(self, attrs=None, format=None):
# Use slightly better defaults than HTML's 20x2 box
default_attrs = {'class': 'date_id'}
if attrs:
default_attrs.update(attrs)
super(CMDateTimeText, self).__init__(attrs=default_attrs, format=format)
def render(self, name, value, attrs=None):
if value is None:
value = ''
value = self._format_value(value)
final_attrs = self.build_attrs(attrs, name=name)
return format_html('<p{}>{}</p>', flatatt(final_attrs), conditional_escape(value))
Result image:
any idea how to do "AdminReadonlyField"" any view or form?
So after hours of looking for various solutions, I found out how to do it the Django way.
Simply add the attribute disabled to the field in the form (not the widget!):
# in __init__() with crispy-forms for instance
self.fields['field'].disabled = True
# as form field
field = forms.CharField(disabled=True)
And it works... Django is taking care of not saving the field, if some hacker tampered with it although it's disabled. Only works with Django 1.9+.
I use modelformset_factory, and I use full_clean() to validate the form with unique_together=True. I wonder what is the best way to handle error in case the unique_together do not validate in order to return the error message in the template.
Please take a look to my view, and tell me if im correct the way I do it, or if there is a better approach.
model:
class Attribute(models.Model):
shapefile = models.ForeignKey(Shapefile)
name = models.CharField(max_length=255, db_index=True)
type = models.IntegerField()
width = models.IntegerField()
precision = models.IntegerField()
def __unicode__(self):
return self.name
def delete(self):
shapefile = self.shapefile
feature_selected = Feature.objectshstore.filter(shapefile=shapefile)
feature_selected.hremove('attribute_value', self.name)
super(Attribute, self).delete()
class Meta:
unique_together = (('name', 'shapefile'),)
form:
class AttributeForm(ModelForm):
def __init__(self, *args, **kwargs):
super(AttributeForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.pk:
self.fields['type'].widget.attrs['disabled'] = True
self.fields['type'].required = False
self.fields['width'].widget.attrs['readonly'] = True
self.fields['precision'].widget.attrs['readonly'] = True
def clean_type(self):
if self.instance and self.instance.pk:
return self.instance.type
else:
return self.cleaned_data['type']
type = forms.ChoiceField(choices=FIELD_TYPE)
class Meta:
model = Attribute
exclude = 'shapefile'
view:
def editFields(request, shapefile_id):
layer_selected = Shapefile.objects.get(pk=shapefile_id)
attributes_selected= Attribute.objects.filter(shapefile__pk=shapefile_id)
attributesFormset = modelformset_factory(Attribute, form=AttributeForm, extra=1, can_delete=True)
if request.POST:
formset = attributesFormset(request.POST, queryset=attributes_selected)
if formset.is_valid():
instances = formset.save(commit=False)
for instance in instances:
instance.shapefile = layer_selected
try:
instance.full_clean()
except ValidationError as e:
non_field_errors = e.message_dict[NON_FIELD_ERRORS]
print non_field_errors
formset = attributesFormset(queryset=attributes_selected)
return render_to_response("basqui/manage_layer_editFields.html", {'shapefile': layer_selected, 'formset':formset}, context_instance=RequestContext(request))
instance.save()
formset = attributesFormset(queryset=attributes_selected)
return render_to_response("basqui/manage_layer_editFields.html", {'shapefile': layer_selected, 'formset':formset}, context_instance=RequestContext(request))
The disadvantage of your approach is that you have moved the validation from the form to the view.
I had the same problem recently of validating a unique together constraint where one field is excluded from the model form. My solution was to override the model form's clean method, and query the database to check the unique together constraint. This duplicates the code that is called by full_clean, but I like it because it's explicit.
I briefly thought about overriding _get_validation_exclusions which would have been more DRY, but I decided not to rely on a private api.
models.py:
class Tag(models.Model):
name = models.CharField(max_length=100)
description = models.CharField(max_length=500, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now_add=True)
class Post(models.Model):
user = models.ForeignKey(User)
tag = models.ManyToManyField(Tag)
title = models.CharField(max_length=100)
content = models.TextField()
created = models.DateTimeField(default=datetime.datetime.now)
modified = models.DateTimeField(default=datetime.datetime.now)
def __unicode__(self):
return '%s,%s' % (self.title,self.content)
class PostModelForm(forms.ModelForm):
class Meta:
model = Post
class PostModelFormNormalUser(forms.ModelForm):
class Meta:
model = Post
widgets = { 'tag' : TextInput() }
exclude = ('user', 'created', 'modified')
def __init__(self, *args, **kwargs):
super(PostModelFormNormalUser, self).__init__(*args, **kwargs)
self.fields['tag'].help_text = None
views.py:
if request.method == 'POST':
form = PostModelFormNormalUser(request.POST)
print form
print form.errors
tagstring = form.data['tag']
splitedtag = tagstring.split()
if form.is_valid():
temp = form.save(commit=False)
temp.user_id = user.id
temp.save()
l = len(splitedtag)
for i in range(l):
obj = Tag(name=splitedtag[i])
obj.save()
post.tag_set.add(obj)
post = Post.objects.get(id=temp.id)
return HttpResponseRedirect('/viewpost/' + str(post.id))
else:
form = PostModelFormNormalUser()
context = {'form':form}
return render_to_response('addpost.html', context, context_instance=RequestContext(request))
Here form.is_valid() is always false because it gets the tag as string from form. But it expects list as form.data['tag'] input. Can anyone tell me how can i fix it?
How can i write a custom widget to solve this?
I don't think you need a custom widget (you still want a TextInput), you want a custom Field. To do this, you should subclass django.forms.Field. Unfortunately the documentation is scant on this topic:
If the built-in Field classes don’t meet your needs, you can easily create custom Field classes. To do this, just create a subclass of django.forms.Field. Its only requirements are that it implement a clean() method and that its init() method accept the core arguments mentioned above (required, label, initial, widget, help_text).
I found this blog post that covers both custom widgets and fields in more depth. The author disagrees with the documentation I quoted above - it's worth reading over.
For your specific situation, you would do something like this (untested):
class MyTagField(forms.Field):
default_error_messages = {
'some_error': _(u'This is a message re: the somr_error!'),
}
def to_python(self, value):
# put code here to coerce 'value' (raw data from your TextInput)
# into the form your code will want (a list of Tag objects, perhaps)
def validate(self, value):
if <not valid for some reason>:
raise ValidationError(self.error_messages['some_error'])
Then in your ModelForm:
class PostModelFormNormalUser(forms.ModelForm):
tag = MyTagField()
class Meta:
model = Post
exclude = ('user', 'created', 'modified')
def __init__(self, *args, **kwargs):
super(PostModelFormNormalUser, self).__init__(*args, **kwargs)
self.fields['tag'].help_text = None
I know this seems to be an over-asked question in the Django circles but I'm afraid to say that i've not yet found the solution to this.
My Model -
from djago.... import User
class InfoPersonal(models.Model):
...
person = models.OneToOneField(User)
I've tried overriding the save_model() in the admin definition and also overriding the save() in the Form but nothing seems to work.
If you were to auto add data into a ForeignKey or OneToOneField column to a Model how would you do it?
def profile_creation_personal(request):
if request.method == 'POST': # If the form has been submitted...
form = PersonalForm(request.POST) # A form bound to the POST data
# form.person = request.user
if form.is_valid(): # All validation rules pass
# Process the data in form.cleaned_data
# ...
form.save()
return HttpResponseRedirect('/done') # Redirect after POST
else:
form = PersonalForm() # An unbound form
return render_to_response('info/personal/profile_create.html', { 'form': form,})
class PersonalForm(ModelForm):
#hometown_id = ModelChoiceField(queryset=InfoReferenceCities.objects.all(),empty_label=None)
def save(self, *args, **kwargs):
self.person = request.user
super(PersonalForm, self).save(*args, **kwargs)
class Meta:
model = InfoPersonal
exclude = ('person',)
widgets = {'dateofbirth' : SelectDateWidget()}
I got the answer!!! I feel good!
personal = form.save(commit = False)
personal.person = request.user
personal.save()
This goes into the view much like Ignacio said, only commit = False being a critical statement for it to save an instance without throwing an exception. Thanks all who helped!!
Cheers
In your PersonalForm, you can subclass your save() function, so the appropriate data is added, something like this:
class PersonalForm(ModelForm):
def save(self, *args, **kwargs):
self.person = request.user
super(PersonalForm, self).save(*args, **kwargs)
class Meta:
model = Personal
see this
parent = models.ForeignKey('self', blank=True, null=True, verbose_name=_("parent"))
this ok but have problem with sqlite , change it to postgresql its ok. ( its for my code , change it to your status )