Используем Python для обработки HTML форм

    Когда я только начинал пользоваться django, самым приятным моментом после ORM, для меня, был пакет django.forms. Теперь django в прошлом — я использую стэк Werkzeug + SqlAlchemy + Jinja2, а иногда даже пытаюсь вместо SqlAlchemy экспериментировать с нереляционными хранилищами данных. Но вот замену django.forms я так и не нашёл. Поэтому решил набросать по-быстренькому что-нибудь своё.

    В итоге, я пришёл примерно к следующему описанию. На входе мы имеем данные представленные типом dict, причём ключами этого словарика являются строки, а значениями — строки или другие словарики, той же структуры. Например:

    data = {
        "key1": "value1"
        "key2": {
            "key3": "value3"
        }
    }
    


    Далее у нас есть некоторые предположения относительно этих данных — какой-то набор правил, который мы будем называть схемой. Теперь нам нужен способ пройтись по всем полям словарика с данными и проверить их значение на правильность, а также привести к нужным типам. Всё просто!

    Отсюда вытекают вполне понятные требования к реализации:

    * Простой способ описания схем — хочется, чтобы это было наглядно и удобно, то есть декларативно.
    * Переиспользование кода — довольно нудно по 10 раз описывать одни и те же схемы.
    * Определение схем для вложенных структур данных — и это может понадобиться.

    Основные принципы реализации
    Базовые принципы

    Предполагается, что ошибка валидации данных будет описываться следующим исключением:

    class SchemaValidationError(TypeError):
       def __init__(self, error):
           self.error = error
    


    Валидация данных, это практически анализ типов данных, поэтому я считаю уместным наследоваться от стандартного исключения TypeError.

    Схема будет задаваться ввиде класса, аттрибутами которого будут объекты, описывающие поля. Так как мы хотим описывать вложенные конструкции, то аттрибутами у нас могут быть как объекты строковых полей, так и другие схемы. Вот что получается на первом этапе:

    class SchemaElement(object):
        u"""
        Абстрактный класс для элемента схемы.
        """
        def validate(self, data):
            raise NotImplementedError()
    
    class Field(SchemaElement):
       u"""
       Класс Field описывает строковое поле.
       """
       def validate(self, data):
           raise SchemaValidationError("not valid value")
    
    class Schema(SchemaElement):
       u"""
       Класс Schema описывает схему валидации.
       """
       def validate(self, data):
           # Код валидации данных data
           return data
    


    Так как элементом схемы может быть как поле, так и другая схема, я наследовал Field и Schema от общего класса SchemaElement. Это шаблон проектирования composite, он отлично подходит для описания иерархических типов данных.

    Также SchemaElement определяет абстрактный интерфейс для валидации — метод validate. Дело в том, что теперь следуя этому интерфейсу, мы можем не различать объекты Field и Schema с точки зрения валидации, для нас это одно и тоже.

    Наследники класса Field будут использоваться для описания полей схемы, то есть для обработки строковых значений. Для того, чтобы реализовать алгоритм валидации данных для конкретного поля, нужно просто переопределить метод validate, который будет возвращать правильные и приведённые данные data или выкидывать исключение SchemaValidationError в случае ошибки. Реализация по-умолчания всегда будет выкидывать исключение.

    Класс Schema будет использоваться для описания структуры состоящей из полей и другим схем. Код метода validate будет представлен чуть позднее.
    Декларативное описание схем

    Как я уже говорил, наиболее удачным мне кажется задание схем ввиде класса, аттрибутами которого являются другие объекты Field и Schema. Это называется декларативное описание. Чтобы это реализовать нам понадобиться метакласс для класса-контейнера Schema:

    class SchemaMeta(type):
       def __new__(mcs, name, bases, attrs):
           if not name == "Schema":
               fields = {}
               for base in reversed(bases):
                   if issubclass(base, Schema) and not base is Schema:
                       fields.update(base.__fields__)
               for field_name, field in attrs.items():
                   if isinstance(field, SchemaElement):
                       fields[field_name] = attrs[field_name]
               attrs["__fields__"] = fields
           cls = type.__new__(mcs, name, bases, attrs)
           return cls
    
       def __contains__(cls, value):
           return value in cls.__fields__
    
       def __iter__(cls):
           return cls.__fields__.items().__iter__()
    


    Основная причина почему я использую этот метакласс — это желание сгруппировать все поля схемы вместе и поместить в аттрибут __fields__. Это будет удобно при обработке полей или интроспекции структуры, так как __fields__ не содержит лишнего мусора, как если бы мы каждый раз обходили __dict__.

    Если мы создаём класс с именем Schema, то метакласс никак не будет обрабатывать его, если же это другой класс, наследующийся от Schema, то сначала он соберёт в __fields__ все поля суперклассов в порядке справа-налево и потом добавит туда поля текущего класса.

    Также я добавил методы __contains__, который будет проверять содержится ли поле с данным именем внутри схемы, и метод __iter__, что делает класс со схемой итерируемым. Напомню, что так как мы опредили эти методы у метакласса, то мы получаем методы класса, что эквивалентно применению декоратора classmethod на методы объекта.

    Теперь осталоcь добавить аттрибут __metaclass__ в класс Schema:

    class Schema(SchemaElement):
        ...
        __metaclass__ = SchemaMeta
        ...
    


    Мы уже можем определять схемы следующим образом:

    >>> class MySchema(Schema):
    ...     my_field = Field()
    
    >>> class AnotherSchema(MySchema):
    ...     another_field = Field()
    
    >>> "my_field" in MySchema
    True
    >>> "another_field" in AnotherSchema
    True
    >>> "my_field" in AnotherSchema
    True
    


    Наследование схем работает — аттрибут my_field появился и у схемы AnotherSchema. Чтобы создать схему для валидации иерархических структур данных, нужно просто добавить аттрибутом схемы другую схему:

    >>> class CompositeSchema(Schema):
            sub_schema = MySchema()
            my_field = Field()
    
    >>> "my_field" in CompositeSchema
    True
    >>> "sub_schema" in CompositeSchema
    True
    >>> "my_field" in CompositeSchema.sub_schema
    True
    


    Валидация данных

    Валидация выполняется методом validate, объекты класса Field должны сами переопредилить его, реализацию же метода validate у класса Schema я привожу тут:

    class Schema(SchemaElement):
       ...
       def validate(self, data):
           errors = {}
           for field_name, field in self.__fields__.items():
               try:
                   data[field_name] = field.validate(data.get(field_name, None))
               except SchemaValidationError, error:
                   errors[field_name] = error.error
           if errors:
               raise SchemaValidationError(errors)
           return data
       ...
    


    Сначала у каждого поля схемы вызывается метод validate c нужным параметром из словаря data. Если есть ошибка, она ловится и сохраняется в словаре errors. После того, как мы обошли все поля, проверяется словарик errors, и если он не пуст, то выкидывается исключение SchemaValidationError с этим словариком ввиде параметра. Это позволяет нам собрать все ошибки, начиная с самого нижнего уровня в иерархии.

    Теперь можно попробовать определить несколько базовых полей и схем и попробовать валидацию данных в действии:

    class NotEmptyField(Field):
        u"""
        Класс описывающий поле, которое не может быть пустым.
        """
        def validate(self, data):
            print "Валидация поля"
            if not data:
                raise SchemaValidationError("empty field")
    
    class CustomSchema(Schema):
        not_empty_field = NotEmptyField()
    
        def validate(self, data):
            print "Валидацию полей схемы"
            data = super(CustomSchema, self).validate(data)
            print "Код валидации на уровне схемы"
            return data
    


    Внутри метода validate мы должны обязательно вызвать метод validate суперкласса. Также обязательно необходимо вернуть data или выкинуть исключение SchemaValidationError. Проверим нашу форму в деле:

    >>> schema = CustomSchema()
    >>> try:
    ...     schema.validate({ "not_empty_field": "some value" })
    ... except SchemaValidationError, e:
    ...     errors = e.error
    Валидацию полей схемы
    Валидация поля
    Код валидации на уровне схемы
    >>> schema.errors
    {}
    


    Теперь попробуем предоставить на валидацию неверные данные:

    >>> try:
    ...     schema.validate({ "not_empty_field": "" })
    ... except SchemaValidationError, e:
    ...     errors = e.error
    Сначала сделаем валидацию полей схемы
    Валидация поля
    >>> errors
    { "not_empty_field": "empty field" }
    


    Как и предполагалось, валидация данных завершилась ошибкой.
    Заключение

    И так, мы имеем маленькую, но уже достаточно мощную библиотечку для валидации данных. Конечно необходимо пополнить её необходимыми полями (классами-наследниками Field). Кстати получилось довольно компактно — не более 130 строк. Если есть желание получить исходный код, вы можете написать мне.
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 4

      –1
      хм… так вроде ж форму можно валидировать средствами джанго?
      Типа:

      def contact(request):
          if request.method == 'POST': # If the form has been submitted...
              form = ContactForm(request.POST) # A form bound to the POST data
              if form.is_valid(): # All validation rules pass
                  # Process the data in form.cleaned_data
                  # ...
                  return HttpResponseRedirect('/thanks/') # Redirect after POST
          else:
              form = ContactForm() # An unbound form
      
          return render_to_response('contact.html', {
              'form': form,
          })
      


      Почитайте в общем оффициальную документанцию Джанго, там все написано.

      Или я вас не понял?
        0
        не по теме: вы зря делаете контекст без использования сначала context = RequestContext(request). Вы отдаете темплейту контекст который не был пропущен через context_processors.
          +1
          Автор пишет не про django.forms а про свою реализацию. Смотрите вступление
        • UFO just landed and posted this here

          Only users with full accounts can post comments. Log in, please.