вторник, 1 сентября 2009 г.

Django Form + JQuery

Решений по асинхронному сохранению Form в Django в интернете описано множество. Но каждая реализация имеет свои особенности, одни почему то показалась трудоемкими по внедрению(много кода ручками ваять, возможно было лень провести рефакторинг, ну это уже другая история), другие наоборот были лишь указанием для реализации и предлагали данные собирать ручками. В общем от каждого этого решение скребло на душе.

Недавно и до меня дошло озарения(лучше поздно, чем никогда), и я понял, что УЖЕ пользуюсь асинхронной валидацией форм, меня оно устраивало - его было легко внедрять, и кода было минимум. Основой был django-ajax-validation проект, на его основе и построен следующий пример.
Для реализации определил следующие критерии:
- Универсальный механизм сбора данных на клиенте.
- Минимум кода при внедрении.

1. Серверная часть

В реализации немного дорабатываем и расширяем серверную часть обработки запроса. Для работы понадобиться simplejson, его можно скачать тут.

Поправив странный момент с перетиранием дополнительных входящих параметров в функцию обрабатывающую запрос получился следующий код(submit_form.py):
from django.utils.functional import Promise
from django.utils.encoding import force_unicode
from simplejson import JSONEncoder
from django import forms
from django.http import HttpResponse
from django.views.decorators.http import require_POST

class LazyEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Promise):
            return force_unicode(obj)
        return obj

def submit_form(request, *args, **kwargs):
    form_class = kwargs.pop('form_class')
    complete_func = kwargs.pop('complete_func', None)
    extra_args_func = kwargs.pop('callback', lambda request, *args, **kwargs: {})
    kwargs_form_data = extra_args_func(request, *args, **kwargs)
    kwargs_form_data['data'] = request.POST
    form = form_class(**kwargs_form_data)
    if form.is_valid():
        result_data = {
            'saved': True,
        }
        if complete_func:
            complete_func(request,result_data,kwargs_form_data,*args,**kwargs)
    else:
        if request.POST.getlist('fields'):
            fields = request.POST.getlist('fields') + ['__all__']
            errors = dict([(key, val) for key, val in form.errors.iteritems() if key in fields])
        else:
            errors = form.errors
        final_errors = {}
        for key, val in errors.iteritems():
            if not isinstance(form.fields[key], forms.FileField):
                html_id = form.fields[key].widget.attrs.get('id') or form[key].auto_id
                html_id = form.fields[key].widget.id_for_label(html_id)
                final_errors[html_id] = val
        result_data = {
            'saved': False,
            'errors': final_errors,
        }
    json_serializer = LazyEncoder()
    return HttpResponse(json_serializer.encode(result_data), mimetype='application/json')
submit_form = require_POST(submit_form)
В метод получает на вход объект запроса и дополнительные аргументы, данные проверяются на валидность и в случае успешного завершения вызывают функцию сохранения данных, в случае ошибки собирается текст ошибок и передается в качестве ответа. Останавливаться на описании алгоритма не буду, тут вроде бы и так все понятно, все тонкости будут понятны на примере (см. 3. Внедрение)

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

Дорабатываем клиентскую часть с использованием jquery 1.3.2(jquery-ajax-submit.js):
(function($)    {
    function form_data(form)   {
        return form.find("input[checked], input[type='text'], input[type='hidden'], input[type='password'], input[type='submit'], option[selected], textarea").filter(':enabled');
    }
    function inputs(form)   {
        return form.find("input, select, textarea")
    }
 
 $.fn.last_submit_data = null;
    
    $.fn.submit_form = function(url, settings) {
        settings = $.extend({
            type: 'table',
            callback: false,
            fields: false,
        }, settings);
        var form = $(this);
 
  var params = {};
        form_data(form).each(function() {
            params[ this.name || this.id || this.parentNode.name || this.parentNode.id ] = this.value; 
        });
        
        var status = false;
        if (settings.fields) {
            params.fields = settings.fields;
        }
        $.ajax({
            async: false,
            data: params,
            dataType: 'json',
            error: function(XHR, textStatus, errorThrown)   {
                status = false;
            },
            success: function(data, textStatus) {
                status = data.saved;
                if (!status)    {

                if (settings.callback)  {
                    settings.callback(data, form);
                }
                else{
                    if (settings.type == 'p')    {
                        inputs(form).parent().prev('ul').remove();
                        inputs(form).parent().prev('ul').remove();
                        $.each(data.errors, function(key, val){
                            if (key == '__all__'){
                            var error = inputs(form).filter(':first').parent();
                            if (error.prev().is('ul.errorlist')) {
                            error.prev().before('
  • ' + val + '
'); } else{ error.before('
  • ' + val +'
'); } } else{ $('#' + key).parent().before('
  • ' + val + '
'); } }); } if (settings.type == 'table') { inputs(form).prev('ul').remove(); inputs(form).filter(':first').parent().parent().prev('tr').remove(); $.each(data.errors, function(key, val) { if (key == '__all__') { inputs(form).filter(':first').parent().parent().before('
  • ' + val + '.
'); } else{ $('#' + key).before('
  • ' + val + '
'); } }); } if (settings.type == 'ul') { inputs(form).prev().prev('ul').remove(); inputs(form).filter(':first').parent().prev('li').remove(); $.each(data.errors, function(key, val) { if (key == '__all__') { inputs(form).filter(':first').parent().before('
    • ' + val + '
  • '); } else { $('#' + key).prev().before('
    • ' + val + '
    '); } }); } } } $.fn.last_submit_data = data }, type: 'POST', url: url }); return status; }; })(jQuery);
    Вызвать отправку данных можно следующей строчкой кода $(form).submit_form(url, {type: 'table'}), где form - jquery строка поиска элемента(в данном примере результатом поиска должен быть один элемент!), url - урл на который нужно слать запросы сохранения. При вызове будет отослан синхронный http запрос. Можно конечно сделать его асинхронным, но тогда необходимо предусмотреть деактивирование кнопки "Отправить данные" для того, чтобы нервные пользователи зря не гоняли запросы на сервер.
    Если пролистать код по диагонали, то можно заметить следующее объявление "$.fn.last_submit_data = null", в данной переменной будет содержаться результат последнего выполнения функции.

    3. Внедрение

    Перейдем к непосредственному использованию. Пол дела сделано, теперь внедрим в приложение и посмотрим на сколько стало удобнее :) или не стало, такое тоже бывает. Для начала опишем стандартный разделы приложения.
    models.py:
    from django.db import models
    
    class MyModel(models.Model):
        name = models.CharField(max_length=150)
        description = models.TextField
    
    forms.py
    from django import forms
    from myapp.models import MyModel
    
    class MyForm(forms.ModelForm):
        class Meta:
            model = MyModel
            fields = ['name', 'description']
    
    Для сохранения результатов запроса давайте в файл views.py добавим функцию сохранения значений.
    views.py
    from django.shortcuts import render_to_response
    from django.template import RequestContext
    from myapp.forms import MyForm
    from myapp.models import MyModel
    
    def mymodel_form(request):
        # какой то код логики
        return render_to_response("mymodel_form.html", {'form' : MyForm}, RequestContext(request))
    
    def item_save(request,result_data,kwargs_form_data,*args,**kwargs):
        item = MyModel();
        form = MyForm(instance = item, **kwargs_form_data)
        form.save(commit=False)
        try:
            # Код по дополнительной обработке данных
            item.save()
        except Exception:
            result_data['valid'] = False
            result_data['error'] = 'Вознилка ошибка при сохранении'
    
    Как вы уже заметили в функции сохранения нет нечего особенного, она лишь реализует самый простой механизм обработки и сохранения данных, где
    request - непосредственно объект запроса;
    result_data - словарь данных которые будут возврашены на клиент, его можно дополнить своими значениями, или установить флаг о ошибке сохранения. При возвращении сериализуеться в JSON;
    kwargs_form_data - данные с формы;
    *args - дополнительные аргументы;
    **kwargs - дополнительные именованные аргументы.
    Зарегистрируем эти функции в файле url.py.
    url.py
    from django.conf.urls.defaults import *
    from myapp.forms import MyForm
    from myapp.views import *
    from myapp.submit_form import submit_form
    
    urlpatterns = patterns('',
        url(r'^form/$', mymodel_form, name='from'),
        url(r'^submit/$', submit_form, {'form_class': MyForm, 
            'callback': lambda request, *args, **kwargs: { },
            'complete_func': item_save,
            }, 'submit'),
    )
    
    Немного разберем что за параметры необходимо передать для функции submit_form:
    form_class - класс формы;
    callback - функция для обработки дополнительных параметров переда валидацией, результатом выполнения ожидается словарь содержащий данные которые будут переданы в форму;
    complete_func - функция по обработки результата при успешной валидации;

    Пример не будет закончен без описание шаблона.
    mymodel_form.html
    ...
    <script>
    function submit_item(form, url){
     if ($(form).submit_form(url, {type: 'table'})) {
      alert('Ваше сообщение сохранено! Можете смело юзать данное решение :).');
     }
    }
    </script>
    <div id="main"><table id="my_form" width="150px;">{{ form }}
    <tr><td></td><td><br />
    <input type="button" value="Сохранить" onclick="javascript:submit_item('#my_form','{% url submit %}');"/><br />
    <input type="button" value="Отмена"/><br />
    </td></tr>
    </table></div>...
    
    Внимание: не забудте подключить на страницу jquery скрипты!

    Ну вот и все, решение готово, запускаем и убеждаемся что работает как часы. Подсумировав можно сказать, что для внедрения данного метода вам понадобиться определить функцию по генерированию основного html контента(view.py), функцию по обработке результата синхронного запроса(view.py), подключить их в url конфиге, и дописать javascript функцию обработки результата ответа на стороне клиента. Считаю что поставленная задача выполнена. На последок можно сказать что есть тонкости и недоработки в данной реализации :), без этого жить было бы скучно. А как вы решаете данную задачу?!

    Комментариев нет:

    Отправить комментарий