Pull to refresh

Django — обработка ошибок в ajax-формах

Django *
Hello everyone!

Все мы знаем что Django — очень мощный и динамично развивающийся фреймворк для создания веб-приложений. Однако, несмотря на наступление эпохи Веб 2.0, в нём всё ещё нет встроенных механизмов для работы с AJAX, в частности отправки и проверки форм. Возможно django просто не хочет навязывать пользователю какой-то js-фреймворк и хочет оставаться гибкой в этом вопросе, но так или иначе при разработке часто требуются формы, работающие через ajax, без перезагрузок страниц.
О создании таких форм и работе с ними и пойдёт речь в данной статье.

Сразу оговорюсь, что идея не нова, и существует несколько библиотек, реализующих требуемую функциональность, например одна из них — http://www.dajaxproject.com/.
Для тех же, кто предпочитает сам управлять взаимодействием клиента с сервером или тех, кто не хочет втягивать в проект дополнительную библиотеку и иметь дело с её багами, я расскажу как изобрести велосипед реализовать механизм самостоятельно и опишу различные способы решения проблемы.

Форма


Для примера возьмём простую форму регистрации пользователя на сайте:
class RegisterForm(forms.Form):
    email = forms.EmailField()
    password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
    password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput)

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

Вывод формы


Для отображения этой формы на странице у нас есть два варианта:
  • Сделать контейнер с display:none внутри всех страниц, с которых можно вызывать форму (или внутри родительского шаблона), затем с помощью JS создавать диалог из этого контейнера.
  • Подгружать форму через ajax с отдельного URL, и затем также создавать диалог.

Преимущество первого варианта — в его скорости работы (не надо делать дополнительного запроса к серверу), зато во втором варианте можно использовать один и тот же view для обработки GET и POST запросов к форме (POST нам потребуется в обоих случаях), плюс мы делаем отдельный файл шаблона для вывода формы, что делает структуру кода более упорядоченной, хотя это конечно дело вкуса.
Лично я внедряю что-то в шаблон страницы только если это простой диалог типа да/нет, а для форм всегда использую отдельные view.
Поэтому остановимся здесь на отдельном представлении для формы, тогда код будет выглядеть следующим образом:
  • view:
    def register(request):
        form = RegisterForm()
        return direct_to_template(request, "register.html", extra_context={'form': form })
    

  • template:
    {% load i18n %}
    <form id="register_form" method="post" action="{% url register %}">
        {% csrf_token %}
        <ul>
            {{ form.as_ul }}
            <li><span id="register">{% trans "Register" %}</span></li>
        </ul>
    </form>
    

Обработка формы


Переходим к обработке формы. Здесь необходимо учесть, что ajax-обработчик должен как-то понимать, была ли форма успешно проверена или в ней присутствуют ошибки. Логичным решением здесь, на мой взгляд, будет использование JSON. В ответе сервера будет содержаться 2 параметра, первый из которых — булевый, будет сообщать об успешности или неудаче проверки формы. Со вторым параметром опять же есть различные варианты:
  • В случае успешной проверки формы этот параметр может быть пустым, так как форма логина скорее всего перенаправляет пользователя на необходимый url после входа и нам неважно что там, либо это может быть строка, которую необходимо отобразить в диалоге подтверждения.
  • В случае когда в форме присутствуют ошибки снова возможно 2 способа отображения:
    1. Первый способ состоит в том, чтобы заново отрендерить форму с ошибками через шаблон и весь html-ответ поместить в json-переменную, которая затем заменяет содержимое всей формы.
    2. Второй способ — создать массив ошибок для всех полей формы и разместить его в json-переменной, затем вывести ошибки для каждого поля в цикле.
Второй вариант идейно мне нравится больше, так как отображает только то, что нужно, без замены всей формы. Здесь я покажу как реализовать оба способа. Код обработки формы на стороне клиента использует популярный JS-фреймворк jQuery и плагин к нему под названием jQuery Form Plugin.
  1. Первый способ:

    Финальная версия view:
    def register(request):
        if request.method == 'POST':
            form = RegisterForm(request.POST)
            if form.is_valid():
                # Обработка
                # ...
                return HttpResponse(simplejson.dumps({'response': _("Email with a confirmation link has been sent"), 'result': 'success'}))
            else:
                t = loader.get_template('register.html')
                ctx = RequestContext(request, {'form': form})
                response = t.render(ctx)
                return HttpResponse(simplejson.dumps({'response': unicode(response), 'result': 'error'}))
        form = RegisterForm()
        return direct_to_template(request, "register.html", extra_context={'form': form })
    


    Обработка на стороне клиента, javascript:
    $(document).ready(function() {
        $('#register').live('click', function() {
            $('#register_form').ajaxSubmit({
                success: function(data, statusText, xhr, $form) {
                    // Удаляем ошибки если были
                    $form.find('.error').remove();
                    if (data['result'] == 'success') {
                        // Делаем что-то полезное
                    }
                    else if (data['result'] == 'error') {
                        // Показываем ошибки
                        $form.replaceWith(data['response']);
                    }
                },
                dataType: 'json'
            });
        });
    }
    
  2. Второй способ:

    Финальная версия view:
    def register(request):
        if request.method == 'POST':
            form = RegisterForm(request.POST)
            if form.is_valid():
                # Обработка
                # ...
                return HttpResponse(simplejson.dumps({'response': _("Email with a confirmation link has been sent"), 'result': 'success'}))
            else:
                # Заполняем словарь response ошибками формы, ключ - название поля
                response = {}
                for k in form.errors:
                    # Теоретически у поля может быть несколько ошибок...
                    response[k] = form.errors[k][0]
                return HttpResponse(simplejson.dumps({'response': response, 'result': 'error'}))
        form = RegisterForm()
        return direct_to_template(request, "register.html", extra_context={'form': form })
    


    Обработка на стороне клиента, javascript:
    function display_form_errors(errors, $form) {
        for (var k in errors) {
            $form.find('input[name=' + k + ']').after('<div class="error">' + errors[k] + '</div>');
        }
    }
    
    $(document).ready(function() {
        $('#register').live('click', function() {
            $('#register_form').ajaxSubmit({
                success: function(data, statusText, xhr, $form) {
                    // Удаляем ошибки если были
                    $form.find('.error').remove();
                    if (data['result'] == 'success') {
                        // Делаем что-то полезное
                    }
                    else if (data['result'] == 'error') {
                        // Показываем ошибки
                        display_form_errors(data['response'], $form);
                    }
                },
                dataType: 'json'
            });
        });
    }
    
Стоит также заметить, что при использовании второго способа и описанного ранее шаблона формы, ошибки будут отображаться с использованием элемента списка <ul>, для другого отображения необходимо сделать другой шаблон либо переопределить класс ошибок для формы.

Вот и всё. Прилагаю скриншоты получившейся формы в различных состояниях:

image

Буду рад услышать комментарии и узнать другие способы работы с ajax-формами.
Tags:
Hubs:
Total votes 39: ↑34 and ↓5 +29
Views 15K
Comments Comments 38