Ajax валидация форм в Django

в 14:45, , рубрики: django, метки:

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js