Posty oznaczone etykietą omijać

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.

Django Page CMS

Jest taki projekt, jak w tytule. Linkował nie będę, radzę tylko omijać.

  • niedeterministyczna identyfikacja stron wg slug ("item1/item2" jest tożsamy z "item1/costam/gdziestam/item2")
  • brak szablonów w BD (jedynie konfigurowane w settings; ich zmiana nie powoduje błędu tylko problemy wyświetlania)
  • nieoptymalne generowanie menu
  • publikacja strony odbywa się przez ustawienie wartości Published na "in navigation" lub "hidden" (sic!)
  • problemy z cache (źle użyty mechanizm powoduje problemy przy backendzie innym niż memcached)
  • parszywa implementacja wielojęzykowości
  • ograniczony panel admina przez przesłonięte szablony dla changelist_view (sztywne kolumny, brak features, etc)
  • wyświetlanie stron cms zawsze gdy 404 domyślna trasa do wyświetlenia strony z regexp .* (w połączeniu z błędem rozpoznawania po slug powoduje niesamowite wrażenia)

Lepiej od razu iść w kierunku https://www.django-cms.org/ albo własnej implementacji pod konkretny cel.