Django i None/null w BooleanField (forms)

Dostałem kolejnym issue związany z "nietypowym" zastosowaniem Django, a typowym w prawdziwym świecie.

Powszechnie wiadomo, że formularze w Django służą do walidacji danych. Niestety błąd projektowy Django polega na tym, że formularze są ściśle związane z danymi wejściowymi pochodzącymi z HTML, a konkretnie związane są widgetami HTML.

Formularze można wykorzystać do walidacji jakichkolwiek wejściowych danych (np. przy imporcie z CSV), przełykając niepotrzebne widgety HTML oraz godząc się na komunikaty o błędach walidacji formatowane do HTML (co jest także swoistym nonsensem, ale od v1.7 będzie już możliwość zwrócenia jako dict lub JSON). Jednak kiedy spróbujecie zrobić API i formularz walidujący dane wejściowe z requestu z użyciem NullBooleanField, to się niemiło zaskoczycie.

Praktyka, czyli rozbicie się o mur

Potrzebujemy filtrowania zasobu restful API i parametry przkazywane przez GET mają charakter opcjonalny. W przypadku parametrów typu bool potrzebujemy rozróżnienia trzech stanów:

  • nie użyto filtra
  • filtr dla wartości True - cleaning z [1,'1','true','True'], jak w to_python()
  • filtr dla wartości False

Dobrym zwyczajem walidację filtra przepuszczamy przez formularz. Podczas definicji formularza mamy dwie możliwości:

  • forms.BooleanField, który mimo required=False defaultowo zwraca zawsze False, zatem odpada
  • forms.NullBooleanField, który zezwala na None

Niestety żadne z powyższych nie działa dla wartości wejściowych ze zbioru [1,'1','True','true']. Pierwsze wiadomo dlaczego: z definicji nie przyjmuje None, więc mamy False by default. Ale dlaczego nie działa NullBooleanField? Bo NullBoolenField w cleaningu odwołuje się do parsowania danych za pomocą... widgetu NullBooleanSelect, którego implementacja wygląda tak:

def value_from_datadict(self, data, files, name):
    value = data.get(name, None)
    return {u'2': True,
            True: True,
            'True': True,
            u'3': False,
            'False': False,
            False: False}.get(value, None)

Niech was gęś kopnie, cudaczni developerzy Django! Nie wszędzie używa się wejścia z waszych wbudowanych widgetów HTML! Dlaczego widget HTML jest domyślny i używany nawet przy wejściu nie pochodzącym z tego widgetu? Nie wiedzieć... Może to Django ethos.

Obejście problemu

class MyNullBooleanSelectWidget(forms.NullBooleanSelect):
    def __init__(self, *args, **kw):
        super(MyNullBooleanSelectWidget, self).__init__(*args, **kw)
        self.choices = ((u'', _('Unknown')),
                   (u'1', _('Yes')),
                   (u'0', _('No')))

    def render(self, *args, **kw):
        """
        tu mozna sobie zaimplementowac rendering w razie potrzeby,
        bo Django developers zlamali DRY i nie uzywaja choices 
        w renderingu; (kolejny klops na kolejny wpis)
        """
        raise NotImplementedError("Not used")

    def value_from_datadict(self, data, files, name):
        value = data.get(name, None)
        if value is None or value=='':
            return None
        if value in ('True', 'true', '1', 1):
            return True
        return False


class MyNullBooleanField(forms.NullBooleanField):
    widget = MyNullBooleanSelectWidget

W formsach API używamy MyNullBooleanField zamiast forms.NullBooleanField. A dla profesjonalnych zastosowań w kontekście budowania API polecam Colander i oczywiście Pyramid.

Konkluzja

Nie pchajcie się ludzie w Django, jeśli nie robicie kolejnego bloga albo frontendu dla jakiegoś portalu.

Opisane zachowanie oparłem o Django v1.4, ale w źródłach master widzę ten sam gnój.

Krótko o mnie

Jestem programistą i architektem systemów IT. Specjalizuję się w aplikacjach webowych, szczególnie w Python oraz Django, PostgreSQL oraz systemach wyszukiwana ElasticSearch.

Zajmuję się wsparciem istniejących systemów oraz projektowaniem i produkcją. W branży działam od 2001 roku. Oferuję doświadczenie, profesjonalizm oraz indywidualne podejście do zleceń.

Zainteresowanych moimi usługami zapraszam do wysłania zapytania.

Javascript logo PostgreSQL logo Cassandra logo Redis logo ElasticSearch logo Ansible logo HTML5 logo CSS3 logo NGINX logo Docker logo

Komentarze

Brak komentarzy