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 wto_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 mimorequired=False
defaultowo zwraca zawszeFalse
, zatem odpadaforms.NullBooleanField
, który zezwala naNone
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.