Hot Dot Production
Компания
29,44
рейтинг
30 июля 2013 в 18:45

Разработка → Ajax валидация форм в Django tutorial recovery mode

imageСегодня я расскажу о том, как мы валидируем формы с использованием технологии Ajax. На стороне сервера будет использоваться Django.
Ничего нового в этом способе нет, в интернете есть несколько статей на эту тему, главное же отличие от этих статей заключается в том, что всю логику валидации формы через Ajax мы вынесем в отдельное представление (view). Это позволит нам писать все остальные представления без какой-либо дополнительной логики. При написании функционала будет использоваться Class Based Views подход.

При валидации форм с использованием Ajax мы получаем следующие преимущества:
  • страница пользователя не перезагружается пока данные не валидны;
  • форма не отрисовывается заново;
  • логика валидации формы описывается в одном месте.

Если заинтересовались, добро пожаловать под кат.

Коротко о том, как все работает:
  1. javascript обработчик перехватывает событие «submit» для всех форм на странице с определенным классом, например, validForm;
  2. когда происходит событие «submit», обработчик передает данные формы и ее идентификатор на определенный адрес;
  3. Представление, обрабатывающее запросы с данного адреса:
    1. инициализирует форму;
    2. валидирует данные;
    3. возвращает обратно ответ об успехе валидации или список с ошибками, если таковые обнаружены;
  4. При успешной валидации обработчик разрешает отправку данных формы обычным образом, иначе — отображает текст ошибок.

В статье более подробно будет описана реализация серверной части валидации. При интересе со стороны хабрасообщества можно будет более подробно описать клиентскую часть.
Рассмотрим, как все работает, на примере формы авторизации.

Сторона сервера


Представления
Создадим представление, которое будет работать с формой авторизации:
class SignIn(FormView):
    form_class = SignInForm
    template_name = 'valid.html'

    def form_valid(self, form):
        user = authenticate(**form.cleaned_data)
        if user:
            login(self.request, user)
        return HttpResponseRedirect('/')


Как можно заметить, никакой дополнительной логики связанной с нашей Ajax
обработкой здесь нет.

Теперь напишем представление, которое будет обрабатывать Аjax запросы:
class AjaxValidation(FormView):
    form_dict = {
        'signin': SignInForm,
    }

    def get(self, request, *args, **kwargs):
        return HttpResponseRedirect('/')

    def form_invalid(self, form):
        data = []
        for k, v in form._errors.iteritems():
            text = {
                'desc': ', '.join(v),
            }
            if k == '__all__':
                text['key'] = '#%s' % self.request.POST.get('form')
            else:
                text['key'] = '#id_%s' % k
            data.append(text)
        return HttpResponse(json.dumps(data))

    def form_valid(self, form):
        return HttpResponse("ok")

    def get_form_class(self):
        return self.form_dict[self.request.POST.get('form')]

Здесь мы создаем представление AjaxValidation, которое в случае не валидности формы будет передавать ошибки клиентской стороне в виде списка объектов в формате Json следующего вида:
[{“key”: ”#id_email”, “desc”: ”Введен некоректный email адрес”}, {“key”: ”...”, “desc”: ”...”}] 

В нем мы указываем идентификатор поля, в котором возникла ошибка, в соответствии со стандартным, генерируемым фреймворком форматом — “id_<название поля>”, или название формы, если эта ошибка не относится к определенным полям формы. Так же передаем текст ошибки, полученный на основании валидации формы.
В словаре form_dict мы указываем идентификатор формы и соответствующий класс формы для валидации.

Иногда, для правильной валидации, форме требуется передать дополнительные аргументы. Их можно передать переопределив метод get_form_kwargs, например, так:
    def get_form_kwargs(self):
        kwargs = super(AjaxValidation, self).get_form_kwargs()
        cls = self.get_form_class()
        if hasattr(cls, 'get_arguments'):
            kwargs.update(cls.get_arguments(self))       
    return kwargs

Здесь используется статический метод. Статический метод позволяет хранить логику, относящуюся к форме, в классе формы без необходимости инициализации экземпляра для исполнения этого метода. Мы проверяем класс на наличие метода и, в случае, если метод в классе определен, обновляем словарь аргументов, передаваемый в форму.

Сам метод может выглядеть следующим образом:
    @staticmethod
    def get_arguments(arg):
        user = arg.request.user
        return {'instance': user}

В данном случае мы передаем в наш метод экземпляр AjaxValidation, из которого мы получаем объект пользователя. Затем передаем его в качестве аргумента в нашу форму.

Формы
Далее рассмотрим класс формы:
class SignInForm(forms.Form):
    email = forms.EmailField(widget=forms.TextInput(attrs={'humanReadable': 'E-mail'}), label='E-mail')
    password = forms.CharField(min_length=6, max_length=32, widget=forms.PasswordInput(attrs={'humanReadable': 'Пароль'}), label='Пароль')

    def clean(self):
        if not self._errors:
            cleaned_data = super(SignInForm, self).clean()
            email = cleaned_data.get('email')
            password = cleaned_data.get('password')
            try:
                user = User.objects.get(email=email)
                if not user.check_password(password):
                    raise forms.ValidationError(u'Неверное сочетание e-mail \ Пароль.')
                elif not user.is_active:
                    raise forms.ValidationError(u'Пользователь с таким e-mail заблокирован.')
            except User.DoesNotExist:
                raise forms.ValidationError(u'Пользователь с таким e-mail не существует.')
            return cleaned_data

В качестве дополнительных аргументов передаем полям формы значения humanReadable, которые будем использовать при выводе ошибок в данной реализации.

В валидации все достаточно просто: проверяем есть ли email пользователя в базе, правильно ли указан пароль и не заблокирован ли пользователь. Так как мы работаем со значениями сразу нескольких полей то валидация осуществляется в методе clean.

Шаблон
Способ вывода информации в каждом проекте может варьироваться и ниже приведен самый простой шаблон для вывода формы:
<!DOCTYPE html>
<html>
    <head>
        <title>Авторизация</title>
    </head>
    <body>
        <form class='validForm' id='signin' humanReadable='Форма логина' method='post' action=''>
            <div class='validation_info'></div>
            {{ form.as_ul }}
            {% csrf_token %}
            <input type='submit'>
        </form>
    </body>
</html>

В блоке с классом validation_info будут выводиться название поля, которое мы получаем из атрибута humanReadable и текст ошибки, сгенерированный фреймворком.

Клиентская часть


Работа с Ajax валидацией в нашем случае требует, чтобы каждой форме был присвоен
идентификатор. С помощью этого идентификатора мы связываем форму с соответствующим классом в переменной form_dict. Каждая валидируемая форма должна иметь класс validForm, тогда javascript обработчик начнет с ней работать.

Сам javascript код выглядит следующим образом:
(function(){
    var _p = project;
    _p.setupValidator = function($f){
        var targetInputs = $f.find('input,textarea,select'),
            infoElement = $f.find('.validation_info');
        targetInputs.add($f).each(function(){
            var $i = $(this),
                hR = $i.attr('humanReadable');
            $i.data('showErrorMessage',function(msg){
                infoElement.append($('<p/>').html(hR+': '+msg));
            });
        });
        $f.on('submit', function(e, machineGenerated) {
            if(machineGenerated) return;
            infoElement.html('');
            e.preventDefault();
            var ser = targetInputs.serialize();
            ser += '&form=' + $f.attr('id');
            $.post('/ajaxValidation/', ser, function(info) {
                if (info != 'ok') {
                    var errors = $.parseJSON(info);
                    for (var i = 0; i < errors.length; i++) {
                        var input = $f.find(errors[i].key);
                        if($f.is(errors[i].key)){
                            input = $f;
                        }
                        if(input.data('showErrorMessage')){
                            input.data('showErrorMessage')(errors[i].desc);
                        } else {
                            console.log(input,errors[i].desc);
                        }
                    }
                } else {
                    $f.trigger('submit', [true]);
                }
            });
        });
    }

    $(function (){
        _p.setupValidator($('.'+_p.validateeFormClass));
    });
})();

Обработчик перехватывает событие submit у формы, выполняет $.serialize(),
собирая в строку идентификаторы и значения полей ввода, а также присоединяет идентификатор самой формы.
В случае, если в ответ пришло сообщение об ошибке, получаем идентификатор объекта с ошибкой и текст ошибки. Далее выводим удобочитаемое описание элемента (формы) и сам текст ошибки в элемент с классом validation_info.

Итог


У нас есть представление AjaxValidation, в которое мы вынесли все
взаимодействие с клиентской частью. В представлении мы регистрируем формы, которые необходимо валидировать. Дополнительные параметры можно передать в статическом методе формы.
Данное решение позволяет использовать все преимущества Ajax валидации и писать только необходимую нам функциональность, ни на что не отвлекаясь.
В соавторстве с flip89
Автор: @erdmko
Hot Dot Production
рейтинг 29,44
Компания прекратила активность на сайте

Похожие публикации

Комментарии (6)

  • +1
    А если время поджимает, и трафика не жалко — можно использовать тот же html-шаблон, а в коллбэке аякса выдирать из ответа форму и подставлять на место старой целиком, со всеми сообщениями об ошибках и прочим.
    • +1
      Вообще, не уверен, что складывать валидацию всех форм из проекта в одну view — хорошая идея.
    • +2
      Или отдавать *почти* тот же шаблон, если запрос аяксовый.

      Шаблон 1: myform.inc.html — собственно форма;
      Шаблон 2: myform.html — страничка с формой; подключает myform.inc.html через include.

      Если запрос обычный, отдаем myform.html; если аяксовый — myform.inc.html. Нет дублирования кода, клиент предельно простой, не нужно из ответа выдирать форму (ответ и есть форма, если запрос аяксовый), легко добавлять аяксовость имеющимся вьюхам.

      Если завести в проекте какое-нибудь соглашение об именовании «обычных» шаблонов и аяксовых, то можно хелпер написать, который будет правильный шаблон отдавать, и аякс почти-что сам заработает: используем везде хелпер (втч где нет аякса); пишем немного js-кода на клиенте; потом в html помечаем нужные формы и они начинают отправляться аяксом.

      Из загашников, таким когда-то хелпером пользовались:

      from django.template.response import TemplateResponse
       
      class show(TemplateResponse):
          """
          TemplateResponse subclass with extra goodies. It is intended to be used
          as a better replacement of `django.shortcuts.render`.
          """
       
          def resolve_template(self, template):
              """
              Accepts only path-to-template (unlike TemplateResponse method).
       
              For ajax requests it tries a template in 'ajax' subfolder first.
              E.g. if request is ajax then
       
                  show(request, 'myapp/index.html')
       
              will try 'myapp/ajax/index.html' first and fallback to 'myapp/index.html'
              if ajax template is not found.
              """
              assert isinstance(template, basestring)
              if self._request.is_ajax():
                  if '/' in template:
                      path, name = template.rsplit('/', 1)
                      ajax_template = "/".join((path, 'ajax', name))
                  else:
                      ajax_template = "ajax/"+template
                  template = [ajax_template, template]
              return super(show, self).resolve_template(template)
      
  • 0
    Позволю себе пару замечаний:
    1. jQuery.post() имеет параметр dataType, который может иметь значение 'json' — тогда вы избавляетесь от необходимости парсить вывод «вручную».
    2. Не стоит просто так выплевывать поля формы при ошибке — логично отдельно выдавать статус(что-то вроде {'success': True, 'field_errors': {'field1': 'atata'}}). Тогда у вас будет единый подход к обработке любого ответа формы.

    Вот пример из моего кода(правда, у меня не CBV, но в данном случае разница непринципиальна): github.com/side2k/edmin_test/blob/master/edmin_test/views.py
  • +2
    А я когда то пользовался вот такой штукой. В шаблоне сверху прописывал:

    {% extends request.is_ajax|yesno:'base-ajax.html,base.html' %}

    Собсно base.html — layout, а base-ajax.html — просто прописано:
    {% block content %}{% endblock %}

    И тогда при аяксе просто отправляется шаблон именно этой вьюхи с формой без основного лейаута, которую выдергиваю через JS.
    • 0
      Приверно так работает django-easy-pjax.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка