I have a simple model form, to which I've added a simple checkbox:
class OrderForm(forms.ModelForm):
more_info = models.BooleanField(widget=forms.CheckboxInput())
def clean(self):
if 'more_info' not in self.cleaned_data:
self.instance.details = ""
class Meta:
model = Order
fields = ('details', 'address', ) # more fields
But this does not work and the 'details' fields is still updated by the user value even if the checkbox is not selected (and the if block is executed, debugged). I've also tried changing self.cleaned_data['details'] instead of self.instance.details but it does not work either.
This is not so important, by in the client side I have a simple javascript code which hide/show the details field if the checkbox is selected.
class OrderForm(forms.ModelForm):
more_info = models.BooleanField(required=False)
def clean(self):
cleaned_data = super().clean()
if not cleaned_data['more_info']:
cleaned_data['details'] = ''
return cleaned_data
From Customizing validation:
This method [clean()] can return a completely different dictionary if it wishes, which will be used as the cleaned_data.
Also:
CheckboxInput is default widget for BooleanField.
BooleanField note:
If you want to include a boolean in your form that can be either True or False (e.g. a checked or unchecked checkbox), you must remember to pass in required=False when creating the BooleanField.
Instead of updating cleaned_data, try overriding the save method instead
def save(self, force_insert=False, force_update=False, commit=True, *args, **kwargs):
order = super(OrderForm, self).save(commit=False)
if not self.cleaned_data.get('more_info', False):
order.details = ""
if commit:
order.save()
return order
Additionally, if you want to use the clean method you need to call super's clean first.
def clean(self):
cleaned_data = super(BailiffAddForm, self).clean()
if not cleaned_data.get('more_info', False):
...
return cleaned_data
Related
I want to implement the "Save as new" feature in Django's admin for a model such as this one:
class Plasmid (models.Model):
name = models.CharField("Name", max_length = 255, blank=False)
other_name = models.CharField("Other Name", max_length = 255, blank=True)
selection = models.CharField("Selection", max_length = 50, blank=False)
created_by = models.ForeignKey(User)
In the admin, if the user who requests a Plasmid object is NOT the same as the one who created it, some of the above-shown fields are set as read-only. If the user is the same, they are all editable. For example:
class PlasmidPage(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
if obj:
if not request.user == obj.created_by:
return ['name', 'created_by',]
else:
return ['created_by',]
else:
return []
def change_view(self,request,object_id,extra_context=None):
self.fields = ('name', 'other_name', 'selection', 'created_by',)
return super(PlasmidPage,self).change_view(request,object_id)
The issue I have is that when a field is read-only and a user hits the "Save as new" button, the value of that field is not 'transferred' to the new object. On the other hand, the values of fields that are not read-only are transferred.
Does anybody why, or how I could solve this problem? I want to transfer the values of both read-only and non-read-only fields to the new object.
Did you try Field.disabled attribute?
The disabled boolean argument, when set to True, disables a form field using the disabled HTML attribute so that it won’t be editable by users. Even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data.
I did a quick test in my project. When I added a new entry the disabled fields were sent to the server.
So something like this should work for you:
class PlasmidPage(admin.ModelAdmin):
def get_form(self, request, *args, **kwargs):
form = super(PlasmidPage, self).get_form(request, *args, **kwargs)
if not request.user == self.cleaned_data['created_by'].:
form.base_fields['created_by'].disabled = True
form.base_fields['name'].disabled = True
def change_view(self,request,object_id,extra_context=None):
self.fields = ('name', 'other_name', 'selection', 'created_by',)
return super(PlasmidPage,self).change_view(request,object_id)
It happens because Django uses request.POST data to build a new object, but readonly fields are not sent with the request body. You can overcome this by making widget readonly, not the field itself, like this:
form.fields['name'].widget.attrs = {'readonly': True}
This has a drawback: it's still possible to change field values by tampering the form (e.g if you remove this readonly attribute from the widget using devtools console). You could protect from that by checking that values haven't actually changed in clean() method.
So full solution will be:
class PlasmidForm(models.ModelForm):
class Meta:
model = Plasmid
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and not self.instance.created_by == request.user:
self.fields['name'].widget.attrs = {'readonly': True}
def clean(self):
cleaned_data = super().clean()
if self.instance and not self.instance.created_by == request.user:
self.cleaned_data['name'] = instance.name # just in case user tampered with the form
return cleaned_data
class PlasmidAdmin(admin.ModelAdmin):
form = PlasmidForm
readonly_fields = ('created_by',)
def save_model(self, request, obj, form, change):
if obj.created_by is None:
obj.created_by = request.user
super().save_model(request, obj, form, change)
Notice I left created_by to be readonly, and instead populate it with the current user whenever object is saved. I don't think you really want to transfer this property from another object.
Django form the cleaned_data field is None.
This field has not passed the validation.
I want to change the value of this field.
Is there another way to get the non-valid fields?
def clean(self):
cleaned_data = super(Form, self).clean()
print(cleaned_data.get('info')) <---- It is None
return cleaned_data
If cleaned_data is None, it should be because your existing form fields have not been validated or there is no data in them.
You can try something like this:
class Form1(forms.Form):
# Your fields here
def clean(self):
if self.is_valid():
return super(forms.Form, self).clean() # Returns cleaned_data
else:
raise ValidationError(...)
EDIT: Taking note of what #Alasdair said - the following approach is better:
You could consider changing the value of 'info' beforehand, i.e. in the view, like so, instead of overriding the form's clean() method:
# In the view
data = request.POST.dict()
data['info'] = # your modifications here
form = Form1(data)
if form.is_valid():
...
I have model
#with_author
class Lease(CommonInfo):
version = IntegerVersionField( )
is_renewed = models.BooleanField(default=False)
unit = models.ForeignKey(Unit)
is_terminated = models.BooleanField(default=False)
def __unicode__(self):
return u'%s %i %s ' % ("lease#", self.id, self.unit)
def clean(self):
model = self.__class__
if self.unit and (self.is_active == True) and model.objects.filter(unit=self.unit, is_terminated = False , is_active = True).count() == 1:
raise ValidationError('Unit has active lease already, Terminate existing one prior to creation of new one or create a not active lease '.format(self.unit))
and I have a form
class LeaseForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(LeaseForm, self).__init__(*args, **kwargs)
self.fields['unit'].required = True
class Meta:
model = Lease
fields = [ 'unit',
'is_active','is_renewed', 'description']
and each time a save this form without selecting value for unit I am getting
error RelatedObjectDoesNotExist
from my clean function in model since there is no self.unit
but I am explicitly validating the unit field.(at least i believe so)
what am I doing wrong?
Note that full_clean() will not be called automatically when you call
your model’s save() method. You’ll need to call it manually when you
want to run one-step model validation for your own manually created
models. [docs]
This is apparently done for backwards compatibility reasons, check this ticket out.
Model's full_clean() method is responsible for calling Model.clean(), but because it's never been called, your clean method inside model is basically omitted.
You can do couple of things for this. You can call model's clean manually. Or, you can move your validation logic into ModelForm using its clean methods. If you are mainly creating instance via form, I think it's the best place to perform validation (and more common practice).
Try this:
class LeaseForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(LeaseForm, self).__init__(*args, **kwargs)
self.fields['unit'].required = True
# IF your validation logic includes multiple fields override this
def clean(self):
cleaned_data = super(LeaseForm, self).clean()
# .. your logic
return cleaned_data
# IF just your field's value is enough for validation override this
def clean__unit(self):
data = self.cleaned_data.get('unit', None)
# .. your logic
return data
class Meta:
model = Lease
fields = [ 'unit',
'is_active','is_renewed', 'description']
I have a form:
class CourseStudentForm(forms.ModelForm):
class Meta:
model = CourseStudent
exclude = ['user']
for a model with some complicated requirements:
class CourseStudent(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
semester = models.ForeignKey(Semester)
block = models.ForeignKey(Block)
course = models.ForeignKey(Course)
grade = models.PositiveIntegerField()
class Meta:
unique_together = (
('semester', 'block', 'user'),
('user','course','grade'),
)
I want the new object to use the current logged in user for CourseStudent.user:
class CourseStudentCreate(CreateView):
model = CourseStudent
form_class = CourseStudentForm
success_url = reverse_lazy('quests:quests')
def form_valid(self, form):
form.instance.user = self.request.user
return super(CourseStudentCreate, self).form_valid(form)
This works, however, because the user is not part of the form, it misses the validation that Django would otherwise do with the unique_together constraints.
How can I get my form and view to use Django's validation on these constraints rather than having to write my own?
I though of passing the user in a hidden field in the form (rather than exclude it), but that appears to be unsafe (i.e. the user value could be changed)?
Setting form.instance.user in form_valid is too late, because the form has already been validated by then. Since that's the only custom thing your form_valid method does, you should remove it.
You could override get_form_kwargs, and pass in a CourseStudent instance with the user already set:
class CourseStudentCreate(CreateView):
model = CourseStudent
form_class = CourseStudentForm
success_url = reverse_lazy('quests:quests')
def get_form_kwargs(self):
kwargs = super(CreateView, self).get_form_kwargs()
kwargs['instance'] = CourseStudent(user=self.request.user)
return kwargs
That isn't enough to make it work, because the form validation skips the unique together constraints that refer to the user field. The solution is to override the model form's full_clean() method, and explicitly call validate_unique() on the model. Overriding the clean method (as you would normally do) doesn't work, because the instance hasn't been populated with values from the form at that point.
class CourseStudentForm(forms.ModelForm):
class Meta:
model = CourseStudent
exclude = ['user']
def full_clean(self):
super(CourseStudentForm, self).full_clean()
try:
self.instance.validate_unique()
except forms.ValidationError as e:
self._update_errors(e)
This worked for me, please check. Requesting feedback/suggestions.
(Based on this SO post.)
1) Modify POST request to send the excluded_field.
def post(self, request, *args, **kwargs):
obj = get_object_or_404(Model, id=id)
request.POST = request.POST.copy()
request.POST['excluded_field'] = obj
return super(Model, self).post(request, *args, **kwargs)
2) Update form's clean method with the required validation
def clean(self):
cleaned_data = self.cleaned_data
product = cleaned_data.get('included_field')
component = self.data['excluded_field']
if Model.objects.filter(included_field=included_field, excluded_field=excluded_field).count() > 0:
del cleaned_data['included_field']
self.add_error('included_field', 'Combination already exists.')
return cleaned_data
In the django admin, I have an inline that I want to have the viewing user filled in automatically. During the clean function, it fills in the created_by field with request.user. The problem is that since the created_by field is excluded by the form, the value that gets inserted into cleaned_fields gets ignored apparently. How can I do this? I want the widget t not be displayed at all.
class NoteInline(admin.TabularInline):
model = Note
extra = 1
can_delete = False
def get_formset(self, request, obj=None, **kwargs):
"""
Generate a form with the viewing CSA filled in automatically
"""
class NoteForm(forms.ModelForm):
def clean(self):
self.cleaned_data['created_by'] = request.user
return self.cleaned_data
class Meta:
exclude = ('created_by', )
model = Note
widgets = {'note': forms.TextInput(attrs={'style': "width:80%"})}
return forms.models.inlineformset_factory(UserProfile, Note,
extra=self.extra,
form=NoteForm,
can_delete=self.can_delete)
ORIGINAL SUGGESTION:
Why not just leave the field in place, rather than excluding it and then make it a hiddeninput?
class NoteForm(forms.ModelForm):
def __init__(*args, **kwargs):
super(NoteForm, self).__init__(*args, **kwargs)
self.fields['created_by'].widget = forms.widgets.HiddenInput()
#rest of your form code follows, except you don't exclude 'created_by' any more
SUGGESTION #2 (because the hidden field still appears in the column header in the inline):
Don't set self.cleaned_data['created_by'] in the clean() method at all. Instead, override NoteForm.save() and set it there.
(Either pass in the request to save(), if you can, or cache it in the init by adding it to self, or use it as a class-level variable as you appear to do already.)
My solution was to edit the formfield_for_foreignkey function for the Inline, which restricted the dropdown to just the logged in user.
class NoteInline(admin.TabularInline):
model = Note
extra = 1
can_delete = False
def queryset(self, request):
return Note.objects.get_empty_query_set()
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'created_by':
# limit the 'created_by' dropdown box to just the CSR user who is
# logged in and viewing the page.
kwargs['queryset'] = User.objects.filter(pk=request.user.pk)
return super(NoteInline, self).formfield_for_foreignkey(db_field, request, **kwargs)