Change django form value - django

I have changed a ForeignKey in a model form, to use a TextBox instead.
Then I override clean method to return the object based on the name field (instead id field)
class SongForm(forms.ModelForm):
artist = forms.CharField(widget=forms.TextInput())
def clean_artist(self):
data = self.cleaned_data['artist']
artist = Artist.objects.get(name=data)
self.cleaned_data['artist_id'] = artist.id
return artist
class Meta:
model = Song
It saves the form correctly, how ever when it renders again appears the id value instead the name value. How may I change the display values of a django form? I think overriding init will do it, but can't find where is the value property

I just wrote Field and Widget subclasses, that solve this particular problem and could be used with JS autocompletion, for example - and is reusable. Still, it required more work than your solution and I'm not sure whether you'll want to use mine or not. Either way - I hope I'll get few upvotes - I spent quite some time and effort writing this...
Instead of defining your ModelForm like you did and messing with clean_ I suggest something like that:
class SongForm(forms.ModelForm):
artist = CustomModelChoiceField( queryset = Artist.objects.all(), query_field = "name" )
class Meta:
model = Song
Now, CustomModelChoiceField (I can't think of better name for the class) is ModelChoiceField subclass, which is good, because we can use queryset argument to narrow acceptable choices. If widget argument is not present, like above, the default one for this field is used (more about it later). query_field is optional and defaults to "pk". So, here is the field code:
class CustomModelChoiceField( forms.ModelChoiceField ):
def __init__( self, queryset, query_field = "pk", **kwargs ):
if "widget" not in kwargs:
kwargs["widget"] = ModelTextInput( model_class = queryset.model, query_field = query_field )
super( CustomModelChoiceField, self ).__init__( queryset, **kwargs )
def to_python( self, value ):
try:
int(value)
except:
from django.core.exceptions import ValidationError
raise ValidationError(self.error_messages['invalid_choice'])
return super( CustomModelChoiceField, self ).to_python( value )
What body of __init__ means is that setting widget = None during creation of CustomModelChoiceField gives us plain ModelChoiceField (which was very helpful while debugging...). Now, actual work is done in ModelTextInput widget:
class ModelTextInput( forms.TextInput ):
def __init__( self, model_class, query_field, attrs = None ):
self.model_class = model_class
self.query_field = query_field
super( ModelTextInput, self ).__init__( attrs )
def render(self, name, value, attrs = None ):
try:
obj = self.model_class.objects.get( pk = value )
value = getattr( obj, self.query_field )
except:
pass
return super(ModelTextInput, self).render( name, value, attrs )
def value_from_datadict( self, data, files, name ):
try:
return self.model_class.objects.get( **{ self.query_field : data[name] } ).id
except:
return data[name]
It's essentially TextInput, that is aware of two additional things - which attribute of which model it represents. ( model_class should be replaced with queryset for narrowing of possible choices to actually work, I'll fix it later). Looking at implementation of value_from_datadict it's easy to spot why to_python in the field had to be overridden - it expects int value, but does not check if it's true - and just passes the value to associated model, which fails with ugly exception.
I tested this for a while and it works - you can specify different model fields by which form field will try to find your artist, form error handling is done automatically by base classes and you don't need to write custom clean_ method every time you want to use similar functionality.
I'm too tired right now, but I'll try to edit this post (and code) tomorrow.

I have just got it, the initial hash was what I was missing:
if self.instance.id:
val = self.initial['artist']
self.initial['artist'] = Artist.objects.get(id=val).name

Related

Django, guidelines for writing a clean method for a ModelChoiceField

I have read the docs and I can only find guidelines on how to write a clean method for a field within a form: https://docs.djangoproject.com/en/3.1/ref/forms/validation/#cleaning-a-specific-field-attribute
However I have created a field which inherits from ModelChoiceField. I wish to add some custom validation and cleaning, attached to the field and not the form, because the field is used in multiple forms, hence keeping it DRY.
I can take a stab at creating a clean method, but eactly what args are passed in, and what should be returned seems to be lacking in the documentation, or I can't find it.
Here my field that I wish to add custom cleaning and validation to:
class FooChoiceField(forms.ModelChoiceField):
def __init__(self, required=True):
queryset = Foo.objects.filter(enabled=True).order_by('name')
super().__init__(
widget=forms.RadioSelect,
queryset=queryset,
to_field_name='id', # The radio button value field
required=required,
empty_label=None,
)
self.error_messages = {
'required': "Please select a Foo.",
'invalid_choice': "Invalid Foo selected, please try again.",
}
# Pass the whole DB object into the template so one can access all fields
def label_from_instance(self, obj):
return obj
Heres a guess at it, although it is called, the cleaned field always ends up as None, even when its valid:
class FooChoiceField(forms.ModelChoiceField):
...
def clean(self, value):
if value != 'correct':
raise ValidationError("Value is challenged in it's correctness")
return value
def validate(self, obj):
if obj.foo != 'foo':
raise ValidationError("Validation Error on foo")
If this is a model field, and the validation is re-used, you should move the validation on the model itself
def validate_correct(value):
if value != 'correct':
raise ValidationError("!", code='incorrect')
class MyModel(models.Model):
my_field = models.CharField(
max_length=31,
validators=[validate_correct],
)
If you want to keep your new form field, you should add some validators too
class MyModelChoiceFields(forms.ModelChoiceField):
default_validators = [validate_correct]
default_error_messages = {'incorrect': "This is not correct"}

Django: Modifying field values before they're submitted

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.

ModelChoiceField in forms.Form won't validate if queryset is overridden

I have a django ModelChoiceField that won't validate if I override the queryset.
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
form = PersonalNote()
form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
The form.is_valid() error is: "Select a valid choice. That choice is not one of the available choices".
If Tile.objects.none() is replaced with Tile.objects.all() it validates, but loads far too much data from the database. I've also tried:
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
def __init__(self, *args, **kwargs):
yyy = kwargs.pop('yyy', None)
super(PersonalNote, self).__init__(*args, **kwargs)
if yyy:
self.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
What might be wrong here? Note the real application also overrides the label, but that does not seem to be a factor here:
class ModelChoiceField2(forms.ModelChoiceField):
def label_from_instance(self, obj):
assert isinstance(obj,Tile)
return obj.child_title()
After 2 hours I found the solution. Because you specified a queryset of none in the class definition, when you instantiate that PersonalNote(request.POST) to be validated it is referenceing a null query set
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
To fix this, when you create your form based on a POST request be sure to overwrite your queryset AGAIN before you check is_valid()
def some_view_def(request):
form = PersonalNote(request.POST)
**form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)**
if form.is_valid():
#Do whatever it is
When you pass an empty queryset to ModelChoiceField you're saying that nothing will be valid for that field. Perhaps you could filter the queryset so there aren't too many options.
I also had this problem. The idea is to dynamically change the queryset of a ModelChoiceField based on a condition (in my case it was a filter made by another ModelChoiceField).
So, having the next model as example:
class FilterModel(models.Model):
name = models.CharField()
class FooModel(models.Model):
filter_field = models.ForeignKey(FilterModel)
name = models.CharField()
class MyModel(models.Model):
foo_field = models.ForeignKey(FooModel)
As you can see, MyModel has a foreign key with FooModel, but not with FilterModel. So, in order to filter the FooModel options, I added a new ModelChoiceField on my form:
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
# your code here
self.fields['my_filter_field'] = forms.ModelChoiceField(FilterModel, initial=my_filter_field_selected)
self.fields['my_filter_field'].queryset = FilterModel.objects.all()
Then, on your Front-End you can use Ajax to load the options of foo_field, based on the selected value of my_filter_field. At this point everyting should be working. But, when the form is loaded, it will bring all the posible options from FooModel. To avoid this, you need to dynamically change the queryset of foo_field.
On my form view, I passed a new argument to MyForm:
id_filter_field = request.POST.get('my_filter_field', None)
form = MyForm(data=request.POST, id_filter_field=id_filter_field)
Now, you can use that argument on MyForm to change the queryset:
class MyForm(forms.ModelForm):
# your code here
def __init__(self, *args, **kwargs):
self.id_filter_field = kwargs.pop('id_filter_field', None)
# your code here
if self.id_filter_field:
self.fields['foo_field'].queryset = FooModel.objects.filter(filter_field_id=self.id_filter_field)
else:
self.fields['foo_field'].queryset = FooModel.objects.none()

Read-only form field formatting

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']

Where to clean extra whitespace from form field inputs?

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