Django: Form with list of integers

6.6k Views Asked by At

I have a javascript application (in angular) that calls my django application. It uses lists of integers to filter the response. In Django I'm using a form to clean the data.

Javascript:

app.factory('SearchData', 
      function(){
            return {
                shop:[],
                sort:'',
                xhr:'',
                brand:[],
             };
       });
app.factory('SearchQuery',
        ['$http', '$location', '$route', 'SearchData', 
        function($http, $location, $route, SearchData){
            return {
                getItems: function(){
                    return $http.get('/search/',{
                        params: SearchData,
                        responseType: 'json',
                    });
                }
            };
        }
    ]);

Python form:

class SearchForm(forms.Form):
    shop = forms.IntegerField(widget=forms.SelectMultiple(),required=False)
    sort = forms.CharField(max_length=1, min_length=1, required=False)
    brand = forms.IntegerField(widget=forms.SelectMultiple(),required=False)

I get a list of integers in shop and brand but I do not how to handle it on the django side. I don't want to use MultipleChoiceField as I need to supply choices in form (which creates an unnecessary query). All I want to do is have a list of integers.

The form above throws "Enter a whole number.". I could just ditch the form and use request.GET.getlist('shop') (which works). But I'd rather use a form if possible...

Update, for now I'm using a MultipleChoiceField and pass the choices before validation in the view. Like:

shops = request.GET.getlist('shop', None)
sf = SearchForm(request.GET)
sf.fields['shop'].choices = shops

It works, but it isn't pretty.

3

There are 3 best solutions below

3
On BEST ANSWER

Use a custom widget/field:

from django import forms
from django.core.exceptions import ValidationError


class MultipleValueWidget(forms.TextInput):
    def value_from_datadict(self, data, files, name):
        return data.getlist(name)


class MultipleValueField(forms.Field):
    widget = MultipleValueWidget


def clean_int(x):
    try:
        return int(x)
    except ValueError:
        raise ValidationError("Cannot convert to integer: {}".format(repr(x)))


class MultipleIntField(MultipleValueField):
    def clean(self, value):
        return [clean_int(x) for x in value]


class SearchForm(forms.Form):
    shop = MultipleIntField()
0
On

Udi's code is good, but there is a problem (under Django 1.11.7) if you want to use this as (say) a hidden field of a completely general user-input form. The problem is that if the user input fails to validate and is re-POSTed with corrections, the multi-valued POST data comes back the second time around as a repr of itself, i.e ['a','b'] comes back as ["['a', 'b']"] and further mangled with each re-POST

So I wrote the following function which can be used to repair the damage every time the view processes POST data. It's a hack, because it involves making request.POST temporarily mutable using a private variable. Also it doesn't properly handle lists of strings containing commas, escaped quotes etc.

def sanitize_keys( request, only=None):
    """ Restore multi-valued keys that have been re-posted. there's a repr
    in the round trip, somewhere. 
    only = list of keys to sanitize. Default is all of them."""

    mutt = request.POST._mutable
    request.POST._mutable = True
    keylist = only or request.POST.keys()
    for key in keylist:
        v = request.POST.get(key)
        if v.startswith("[") and v.endswith("]"):
            #print( "Debug: sanitizing " + v )
            sanitized=[]
            for s in v[1:-1].split(','):
                s = s.strip()
                if s.startswith("'") and s.endswith("'"):
                    s=s[1:-1].replace("\\'","'")
                sanitized.append(s)
            #print( "Debug: sanitized= ", sanitized )
            request.POST.setlist( key, sanitized)
    request.POST._mutable = mutt
    return

Usage (fragments):

class TestForm( forms.Form):
   name = forms.CharField()
   ...
   customer_iid  = MultipleValueField( required=False) 

...

# POST
sanitize_keys( request, only=('customer_iid',) )
#print( 'Debug: customer_iid', request.POST.getlist('customer_iid', []) )
form = TestForm( request.POST)
0
On

You can use TypedMultipleChoiceField from Django forms with coerce=int and to avoid validation against predefined list of choices override the def valid_value(self, value): method:

class MultipleIntegersField(forms.TypedMultipleChoiceField):
    def __init__(self, *args, **kwargs):
        super(MultipleIntegersField, self).__init__(*args, **kwargs)
        self.coerce = int

    def valid_value(self, value):
        return True

class SearchForm(forms.Form):
    shop = MultipleIntegersField()