Posty oznaczone etykietą pyramid

Colander i Django: ModelChoice

Budując REST API mogą przydać się walidatory danych wejściowych bardziej elastyczne niż formularze Django. Do realizacji tego celu polecam i używam Colander.

Colander jest świetnym pakietem do (de)serializacji danych. Jest to odpowiednik formularzy Django dla Pyramida, lecz zdecydowanie bardziej elastyczny oraz bez związku HTML. Formularze Django są źle zaprojektowane - zbyt ściśle związane z HTML (widgety), których przecież nie używamy budując REST API, oraz są ograniczone do płaskich struktur. Jedynie jest są dyspozycji formsety, co pozwala zwalidować i zdeserializować listę obiektów jednego typu.

Colander wolny jest od tych wad, a ponieważ nie ma zależności od Pyramid (jest pakietem samodzielnym) to można go użyć z Django bez żadnego problemu. Jednak w niektórych sytuacjach brakuje mi pola typu ModelChoice, co utrudnia fabrykowanie instancji modeli. Z tego powodu zrobiłem poniższy snippet definiujący walidator ModelOneOf oraz klasę węzła ModelChoice. Jest na tyle przydatny, że postanowiłem się z nim podzielić jak najszybciej:

import colander
import types


class ModelOneOf(object):
    def __init__(self, qs):
        self._qs = qs

    def __call__(self, node, value):
        if not self._qs.filter(pk=value).exists():
            raise colander.Invalid(node, '%r is not valid choice' % value)


class ModelChoice(colander.SchemaType):
    def __init__(self, qs, *args, **kw):
        self._qs = qs
        self._model = qs.model
        self._validate = ModelOneOf(self._qs)

        super(ModelChoice, self).__init__(*args, **kw)

    def serialize(self, node, appstruct):
        if appstruct is colander.null:
            return colander.null
        if not isinstance(appstruct, self._model):
            raise colander.Invalid(
                    node, '%r is not a %s' % (appstruct, self._model))
        return appstruct.pk

    def deserialize(self, node, cstruct):
        if cstruct is colander.null:
            return colander.null
        if not isinstance(cstruct, (types.StringType, int)):
            raise colander.Invalid(
                    node, '%r is not a string nor int' % cstruct)

        self._validate(node, cstruct)
        return self._qs.get(pk=cstruct)

Przykładowe użycie (forms.py):

import colander
from .models import MyModel


class MySchema(colander.MappingSchema):
    model = colander.SchemaNode(ModelChoice(MyModel.objects.all()))
In [1]: s = MySchema()
In [2]: s.deserialize({'model': '1'})  # gdzie '1' to wartość PK
Out[2]: {'model': <MyModel pk=1>} 

Teraz zdeserializowane dane wejściowe da się wprost przekazać do fabryk lub konstruktora modeli, np:

instance = MyModel(**s.deserialize(...))

W ten sposób można całkowicie pominąć formularze django.