I've just discovered that Django doesn't automatically strip out extra whitespace from form field inputs, and I think I understand the rationale ('frameworks shouldn't be altering user input').
I think I know how to remove the excess whitespace using python's re:
#data = re.sub('\A\s+|\s+\Z', '', data)
data = data.strip()
data = re.sub('\s+', ' ', data)
The question is where should I do this? Presumably this should happen in one of the form's clean stages, but which one? Ideally, I would like to clean all my fields of extra whitespace. If it should be done in the clean_field() method, that would mean I would have to have a lot of clean_field() methods that basically do the same thing, which seems like a lot of repetition.
If not the form's cleaning stages, then perhaps in the model that the form is based on?
My approach is borrowed from here. But instead of subclassing django.forms.Form, I use a mixin. That way I can use it with both Form and ModelForm. The method defined here overrides BaseForm's _clean_fields method.
class StripWhitespaceMixin(object):
def _clean_fields(self):
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields.
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, FileField):
initial = self.initial.get(name, field.initial)
value = field.clean(value, initial)
else:
if isinstance(value, basestring):
value = field.clean(value.strip())
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self._errors[name] = self.error_class(e.messages)
if name in self.cleaned_data:
del self.cleaned_data[name]
To use, simply add the mixin to your form
class MyForm(StripeWhitespaceMixin, ModelForm):
...
Also, if you want to trim whitespace when saving models that do not have a form you can use the following mixin. Models without forms aren't validated by default. I use this when I create objects based off of json data returned from external rest api call.
class ValidateModelMixin(object):
def clean(self):
for field in self._meta.fields:
value = getattr(self, field.name)
if value:
# ducktyping attempt to strip whitespace
try:
setattr(self, field.name, value.strip())
except Exception:
pass
def save(self, *args, **kwargs):
self.full_clean()
super(ValidateModelMixin, self).save(*args, **kwargs)
Then in your models.py
class MyModel(ValidateModelMixin, Model):
....
Create a custom model field so that your custom form field will be used automatically.
class TrimmedCharFormField(forms.CharField):
def clean(self, value):
if value:
value = value.strip()
return super(TrimmedCharFormField, self).clean(value)
# (If you use South) add_introspection_rules([], ["^common\.fields\.TrimmedCharField"])
class TrimmedCharField(models.CharField):
__metaclass__ = models.SubfieldBase
def formfield(self, **kwargs):
return super(TrimmedCharField, self).formfield(form_class=TrimmedCharFormField, **kwargs)
Then in your models just replace django.db.models.CharField with TrimmedCharField
How about adding that to the def clean(self): in the form?
For further documentation see:
https://docs.djangoproject.com/en/dev/ref/forms/validation/#cleaning-and-validating-fields-that-depend-on-each-other
Your method could look something like this:
def clean(self):
cleaned_data = self.cleaned_data
for k in self.cleaned_data:
data = re.sub('\A\s+', '', self.cleaned_data[k])
data = re.sub('\s+\Z', '', data)
data = re.sub('\s+', ' ', data)
cleaned_data[k]=data
return cleaned_data
Use the following mixin:
class StripWhitespaceMixin(object):
def full_clean(self):
# self.data can be dict (usually empty) or QueryDict here.
self.data = self.data.copy()
is_querydict = hasattr(self.data, 'setlist')
strip = lambda val: val.strip()
for k in list(self.data.keys()):
if is_querydict:
self.data.setlist(k, map(strip, self.data.getlist(k)))
else:
self.data[k] = strip(self.data[k])
super(StripWhitespaceMixin, self).full_clean()
Add this as a mixin to your form e.g.:
class MyForm(StripWhitespaceMixin, Form):
pass
This is similar to pymarco's answer, but doesn't involve copy-pasting and then modifying Django code (the contents of the _clean_fields method).
Instead, it overrides full_clean but calls the original full_clean method after making some adjustments to the input data. This makes it less dependent on implementation details of Django's Form class that might change (and in fact have changed since that answer).
Since Django 1.9 you can use the strip keyword argument in the field of your form definition :
stripĀ¶
New in Django 1.9.
If True (default), the value will be stripped of leading and trailing whitespace.
Which should give something like :
class MyForm(forms.Form):
myfield = forms.CharField(min_length=42, strip=True)
And since its default value is True this should be automatic with django>=1.9.
It's also relevant with RegexField.
In this case, it could be useful to create your own form field (it's not that hard as it sounds). In the clean() method you would remove that extra whitespaces.
Quoting the documentation:
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 (required, label, initial, widget,
help_text).
More about it: https://docs.djangoproject.com/en/1.3/ref/forms/fields/#creating-custom-fields
One way to do this is to specify custom form widget that strips whitespace:
>>> from django import forms
>>> class StripTextField(forms.CharField):
... def clean(self,value):
... return value.strip()
...
>>> f = StripTextField()
>>> f.clean(' hello ')
'hello'
Then to use this in your ModelForm:
class MyForm(ModelForm):
strip_field = StripTextField()
class Meta:
model = MyModel
However, the best place to do this is in your view after the form has been validated; before you do any inserts into the db or other manipulation of data if you are using ModelForms.
You can always create your own non-ModelForm forms and control every aspect of the field and validation that way.
ModelForm's validation adds checks for values that would violate the db constraints; so if the field can accept ' hello ' as a valid input, ModelForm's is_valid() would have no reason to strip the whitespaces (as it wouldn't make for arbitrary clean logic, in addition to what you mentioned "frameworks shouldn't alter user's input").
If you want to strip() every CharField in your project; it may be simplest to monkeypatch CharField's default cleaning method.
within: monkey_patch/__init__.py
from django.forms.fields import CharField
def new_clean(self, value):
""" Strip leading and trailing whitespace on all CharField's """
if value:
# We try/catch here, because other fields subclass CharField. So I'm not totally certain that value will always be stripable.
try:
value = value.strip()
except:
pass
return super(CharField, self).clean(value)
CharField.clean = new_clean
Related
With this method, I can make a field save as lowercase, but this does not
change the field in the existing model (that is in memory).
def get_prep_value(self, value):
value = super(LowercaseField, self).get_prep_value(value)
if value is not None:
value = value.lower()
return value
I'm having a hard time figuring out how to force this field to lowercase without overriding save and doing the change there. But that splits the logic for this lowercase field. I'd like all of it in the field. What do I override so that setting this value forces lowercase in memory AND on in the DB?
I don't want to change a form, I want all the lowercase logic contained inside the field class.
I've found a partial work around like so:
def pre_save(self, model_instance, add):
""" Returns field's value just before saving. """
attr = getattr(model_instance, self.attname)
if attr is not None:
attr = attr.lower()
setattr(model_instance, self.attname, attr)
return attr
def get_prep_value(self, value):
value = super(LowercaseField, self).get_prep_value(value)
if value is not None:
value = value.lower()
return value
It has a bit of a code smell, and does not handle checking the value before a save, but I don't see how to do that without overriding setattr on the actual model class and catching dealing with that inside the model class itself.
You can override the "save" method in Django by adding the following code in your models.py file
def save(self, *args, **kwargs):
self.yourfiled = self.yourfield.lower()
return super(ModelsName, self).save(*args, **kwargs)
Of course is possible to handle all params with a loop.
For all existing record you can create a Management command that can convert all strings to lowercase
here the docs:
Writing custom django-admin commands
If you don't want to change the Save method, just add to the form the "|lower" tag that will be convert all string to lowercase in UI
{{ value|lower }}
To workaround issues with Taggit, I'm trying to add quotes around values in the tag field before they're transferred into a model. This is what I have so far but it's not working. What am I doing wrong?
class TagField(models.CharField):
description = "Simplifies entering tags w/ taggit"
def __init__(self, *args, **kwargs):
super(TagField, self).__init__(self, *args, **kwargs)
# Adds quotes to the value if there are no commas
def to_python(self, value):
if ',' in value:
return value
else:
return '"' + value + '"'
class CaseForm(forms.ModelForm):
class Meta:
model = Case
fields = ['title', 'file', 'tags']
labels = {
'file': 'Link to File',
'tags': 'Categories'
}
widgets = {
'tags': TagField()
}
You are subclassing models.CharField, instead you should subclass forms.CharField, you're specifying for widget attribute in the form but you're trying to create a form field subclass.
The reason this is not working is you are defining a custom model field and then trying to specify it as a widget in the form. If you indeed want a custom widget, you need to actually provide a widget instance, not a model field instance.
But to get the behavior you want, instead you need to declare the field at the Model level as an instance of your custom field class.
Try something like -
from django.db import models
class TagField(models.CharField):
description = "Simplifies entering tags w/ taggit"
def __init__(self, *args, **kwargs):
super(TagField, self).__init__(*args, **kwargs)
# Adds quotes to the value if there are no commas
def to_python(self, value):
if any( x in value for x in (',', '"') ):
return value
else:
return "\"%s\"" % value
class ModelWithTag(models.Model):
tag = TagField(max_length = 100)
The to_python method is also called by Model.clean(), which is called during form validation, so I think this will provide the behavior you need.
Note, I also check for the presence of a double-quote in your condition in the to_python method, otherwise the quotes will continue to "stack up" every time save() is called.
In a form I am using a MultiValueField (MVF) with a MultiWidget that has several fields. If there is a validation error in one of the fields of the MVF, this gets handled (displayed) at the MVF level, rather than at the individual sub-fields, which can lead to:
* Ensure this value is greater than or equal to 1.
* Ensure this value is greater than or equal to -100.0.
Number of days: -1
...
...
Threshold: -200
Where the first error refers to the first field of the MVF and the second error the the last field of the MVF.
Is it possible to put those error messages "inside" the MVF at the field where they belong? (maybe in the format_output method of the MultiWidget?)
The following solution doesn't use a MultiValueField but instead:
dynamically replaces the original field with several ones on form's __init__
reconstruct valid data for the original field during the form validation on _post_clean
Here is some test code that needs to be adapted to each case:
class MyMultiField(CharField):
def split(self, form):
name = 'test'
form.fields_backup[name] = form.fields[name]
del form.fields[name]
# here is where you define your individual fields:
for i in range(3):
form.fields[name + '_' + str(i)] = CharField()
# you need to extract the initial data for these fields
form.initial[name + '_' + str(i)] = somefunction(form.initial[name])
form.fields['test_1'] = DecimalField() # because I only want numbers in the 2nd field
def restore(self, form):
# here is where you describe how to joins the individual fields:
value = ''.join([unicode(v) for k, v in form.cleaned_data.items() if 'test_' in k])
# extra step to validate the combined value against the original field:
try:
restored_data = form.cleaned_data.copy()
restored_data["test"] = form.fields_backup["test"].clean(value)
for k in form.cleaned_data:
if k.startswith("test_"):
del restored_data[k]
form.cleaned_data = restored_data
except Exception, e:
form._errors[NON_FIELD_ERRORS] = form.error_class(e)
class MyForm(Form):
test = MyMultiField()
def __init__(self, *args, **kwargs):
super(ItemForm, self).__init__(*args, **kwargs)
self.fields_backup = {}
self.fields['data'].split(self)
def _post_clean(self):
self.fields_backup['data'].restore(self)
return super(MyForm, self)._post_clean()
Before:
After (validating some input):
I'm not sure if it's possible to decouple this Field/Form code further using this approach. I'm also not quite satisfied with this code as the new field class needs to inherit from the original one.
Nonetheless, the basic idea is there and I successfully used it to individually validate form fields built from a dictionary stored in a single model field with PostgreSQL hstore.
We have one application containing models.py which contains n no. of classes that inherits base class.We want to create form which dynamically takes value from user n saves in db but problem is that we want to use django form fields instead of django model forms.
As we know there are some fields missing in django forms such as PositiveIntegerField, CommaSeparetedIntegerFields etc. How can we achieve this using django form fields?
If we write follwing code in shell.
from djnago.db import models
mvar = models.PositiveIntegerFields()
from django import forms
fvar = forms.PositiveIntegerFields()
AttributeError: 'module' object has no attribute 'PositiveIntegerField'
forms.py
from django import forms
class ContextForm(forms.Form):
def __init__(self, rdict, *args, **kwargs):
super(ContextForm, self).__init__(*args, **kwargs)
for key in rdict.keys():
self.fields['%s' % str(key)] = getattr(forms,rdict.get(key))()
rdict = {'address': 'CharField','phone': 'CharField', 'Salary': 'PositiveIntegerField','first name': 'CharField','last name':'CharField'}
Looking at the source, all the field does is call the default form field with a keyword argument: min_value.
class PositiveIntegerField(IntegerField):
description = _("Positive integer")
def get_internal_type(self):
return "PositiveIntegerField"
def formfield(self, **kwargs):
defaults = {'min_value': 0}
defaults.update(kwargs)
return super(PositiveIntegerField, self).formfield(**defaults)
Therefore what you are looking for is merely
from django import forms
fvar = forms.IntegerField(min_value=0)
fvar.clean(-1)
# ValidationError: [u'Ensure this value is greater than or equal to 0.']
As for CommaSeparatedIntegerField, it looks like a CharField with some django.core.validators.validate_comma_separated_integer_list passed in.
f = forms.CharField(validators=[django.core.validators.validate_comma_separated_integer_list])
f.clean('1,2,3')
All this does is make sure the passed in string is '^[\d,]+$'. The field doesn't even do any python conversions... it doesn't really seem to save much time if just validates form input. Indeed, there's a comment that says "maybe move to contrib". Agreed..
Decided to look into this for fun. Here's a ModelForm generator that overrides model fields with new fields... It doesn't yet handle kwargs. It was just the first method I could think of to do this.. without looking into modelform generation itself. It constructs a regular ModelForm that modifies the form /after/ initialization.
MODEL_FIELD_MAP = {
models.IntegerField: forms.CharField,
# change all IntegerField to forms.CharField
}
def modelform_generator(mymodel):
class MyModelForm(forms.ModelForm):
class Meta:
model = mymodel
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
for name, form_field in self.fields.items():
try:
model_field = self._meta.model._meta.get_field_by_name(name)[0]
# is this a model field?
field_override = MODEL_FIELD_MAP.get(model_field.__class__)
# do we have this model field mapped to a form field?
if field_override:
self.fields[name] = field_override()
# set the form field to the target field class
except models.FieldDoesNotExist:
pass
return MyModelForm
I want to display a field as read only in a ModelAdmin form, so I added it to the readonly_fields attribute.
However, since the field contains a currency, stored as an integer, I want to apply some nice formatting it. I've created a custom ModelForm for my ModelAdmin, trying to apply the formatting in the overridden __init__ method.
The problem is, I cannot find the value. The field is not present in the self.fields attribute.
Does anyone know where the values for the readonly_fields are kept, or is there a better/different approach?
Just do something like:
class MyAdmin(admin.ModelAdmin):
readonly_fields = ('foo',)
def foo(self, obj):
return '${0}'.format(obj.amount)
An alternate approach, which works for all types of forms is to create a widget to represent a read only field. Here is one that I wrote for my own use. You can change the <span %s>%s</span> to suit your own requirements.
from django import forms
from django.utils.safestring import mark_safe
from django.utils.encoding import force_unicode
class ReadOnlyWidget(forms.TextInput):
def render(self, name, value, attrs=None):
if value is None:
value = ''
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
if value != '':
# Only add the 'value' attribute if a value is non-empty.
final_attrs['value'] = force_unicode(self._format_value(value))
return mark_safe(u'<span%s />%s</span>' % (flatatt(final_attrs),value))
Once you have that added, simply do this:
class MyAdmin(admin.ModelAdmin):
foo = models.TextField(widget=ReadOnlyWidget(attrs={'class':'read-only'}
initial="$50")
Then in your CSS, do some styling for a read-only class, or you can adjust the attributes accordingly.
Another, more appropriate solution, works in Django 2.1.2:
ModelAdmin renders read-only fields via special wrapper AdminReadonlyField (django/contrib/admin/helpers.py) if we look at contents method, we can see
the code
if getattr(widget, 'read_only', False):
return widget.render(field, value)
It means that if a widget has read_only attribute with True value
then the read-only field will invoke widget's render method.
Hence, you can use render method to format your value.
For example:
class CustomDateInput(widgets.DateInput):
read_only = True
def _render(self, template_name, context, renderer=None):
return 'you value'
class CustomForm(forms.ModelForm):
some_field = forms.DateTimeField(widget=CustomDateInput())
#admin.register(SomeModel)
class SomeModelAdmin(admin.ModelAdmin):
form = CustomForm
readonly_fields = ['some_field']