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 intra- oraz internetowyh. Zajmuję się wsparciem istniejących systemów oraz projektowaniem i produkcją.

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