Продвинутые формы — мультиселект с автокомплитом

    Наши пользователи следят за спецпредложениями авиакомпаний и дешевыми перелетами, каждому интересно своё направление, пунктов вылета и прилета — тысячи, пользователи хотят одновременно следить за несколькоми городами, странами или регионами. Появилась задача — как предоставить удобный механизм подписки? Без долгого тыканья в мульти селект, без кнопок «Добавить пункт вылета». Ответ выглядит так —

    Решением стал контрол из Фейсбука и Контакта — пользователи знакомы с ним, а значит не нужно объяснять как он работает. Осталось скрестить его с django.

    Этот контрол подойдет для многих случаев когда нужна выборка из большого списка — сотнями или тысячами элементов.
    Живой пример — на форме выбора городов для отcлеживания акций авиакомпаний.

    Со стророны юзера используем готовый jquery plug-in, доступный на www.emposha.com/javascript/fcbkcomplete.html, скачиваем его и кладем css и js в папку media ( в моём случае — media/css и media/js )
    Добавим код формы в foms.py-
    class MultiOriginSelect(forms.SelectMultiple):
        class Media:
            css = {
                'all': ('/media/css/fcbkinput.css',)
            }
            js = ('/media/js/jquery.fcbkcomplete.js')
     
    class SubscriptionFilterForm(forms.Form):

        CHOICES =[]
        ........
        orgs = forms.MultipleChoiceField(widget=MultiOriginSelect, choices=CHOICES, required=False, initial=[])
        dsts = forms.MultipleChoiceField(widget=MultiOriginSelect, choices=CHOICES,  required=False, initial=[])
        ........

        def __init__(self*args, **kwargs):
            super(SubscriptionFilterForm, self).__init__(*args, **kwargs)

            fcbkcomplete_fields = [u'orgs'u'dsts']
            for field in fcbkcomplete_fields:
                # check whether we have init parameters
                if args:
                    loc_list = args[0].getlist(field)
                    .....
                    # generate dynamic choices for fcbk fields from args, like [id, name]
                    self.fields[field].choices = ([(int(o), name(o)) for o in loc_list] )

    Последняя строчка — основная, если у формы есть инициализационные значения — заполним choices.

    В html файл с формой добавим инициалиазацию контролов:
    <head>
    <script>
       $(document).ready(function(){
         $("#id_orgs, #id_dsts").fcbkcomplete({
            json_url:'/subscribe_autocomplete',
            first_selected:false,
            filter_hide : true,
            filter_case:false,
            complete_text:"Enter country, city or airport.",
            maxitems: 100
            });
       });
    </script>
    </head>

    <body>
    ....
        <form>
        ......
        {{form.orgs}}
        {{form.dsts}} 
        <input type = "submit">
        </form>
       ......
    </body>


    Для того чтобы работал автопокомплит — добавим вьюху, которая будет генерировать необходимый json В нашем случае он расположен по адресу — /subscribe_autocomplete. Итак добавляем — в urls.py:
    .....
    url(r'^subscribe_autocomplete', subscribe_autocomplete, name='subscribe_autocomplete'),
    .....


    во views.py:

    def subscribe_autocomplete(request):
        q = request.GET.get('tag','')
        # skip too short requests 
        if len(q)<3return HttpResponse('')

        # filter any instances according to tag
        qr = Objects.objects.filter(Q(....))
        
        #generate json
        #message format - [{"caption":"London", "value":4}]
        s =[...];

        return HttpResponse(s)


    теперь у нас работает форма на странице и успешно генериться заполненная форма из пост заспроса (form = SubscriptionFilterForm(request.POST) )

    Для генерации формы из модели — использую отдельную функцию ( в моем случае ModelsForm не подошел )

    def subs_form_from_model(s):
        src_d = {}
        src_d['subscriptionemail'= s.email
        ,,,,

        qd = django.http.QueryDict('')
        qd = qd.copy() # to make muttable
        qd.update(src_d)

        # fill form fields
        qd.setlist('orgs', [unicode(o.id) for o in s.orgs.all()])
        qd.setlist('dsts', [unicode(d.id) for d in s.dsts.all()])

       # create form
        form = SubscriptionFilterForm(qd)
        .......... 

        return form</code>


    Большинство пользователей видит поле уже заполненным ( по гео айпи ) и понимает что к чему.
    Такую же форму можно ( и нужно ) использовать в админке, для больших мени-ту-мени полей.

    PS И чтобы два раза не вставать — если вам понравились буруки (или вы уже пользуетесь нашем поиском и рассылкой) — с радостью пообщаемся с умелым верстальщиком и акробатом JSа.
    Поделиться публикацией

    Комментарии 41

      +3
      Похожую реализацию мы сделали у себя — cinemate.cc/movie/filter/ (см. вкладки актеры и режиссеры)
      Как говорится, все в конечном итоге приходят к одному наиболее удобному и приятно выглядящему варианту.
        0
        с таким контролом особых альтернатив нет, есть еще ajax_selects, поддерживающий мультиселект, но у него немного другая идея — выбранные элементы «откладываются» отдельно от окна ввода
          0
          ajax_select мы тоже используем, но в админке :) По некоторым причинам мы пока не можем отказаться от него в пользу описываемого варианта.
          +4
            +13
            Я вижу вы очень любите Джорджа Клуни :)
              0
              Первый, кто выпал в autocomplete на первую нажавшуюся букву. Ну и на вторую тоже. Наверное, я очень люблю букву G)
                0
                GGG?
                • НЛО прилетело и опубликовало эту надпись здесь
              0
              А что за параноидальная мода в последнее время коммерчески белые проекты в домене на кокосовых островах (.cc) делать? Вы же даже не трекер, а база типа кинопоиск.ру — удобная, классная спору нет — оценил… но для ру-аудитории в зоне .cc поди нашлось бы поудобнее для запоминания чем окончаени открытого слога… не ну я понимаю в свое время carderplanet в .cc перехал… но каталог фильмов… это уже диагноз…
                0
                Вы не поверите, но пользователям западает именно эта отличительная особенность сайта, немало человек приходят с поисковиков, забивая в строке www.google.ru/search?q=поиск+по+торрентам+.cc (реальный запрос)

                А если серьезно, то не смотря на то, что вы понимаете, мы понимаем и пользователи понимают, что сайт по сути аггрегатор ссылок на торрент-трекеры, сам ничего не хранит, являясь в некотором роде поисковиком, выдающим ссылки на иные сайты, тем не менее даже его можно закрыть при должном желании. Достаточно вспомнить недавние публикации на тему удаления из Яндекса и/или других поисковиков ссылок на трекеры.
                  0
                  cinemate.cc — ого!
                  Как вы собрали столько фильмов и ссылок — вручную?! Кажется это просто титанический по объему труд.
                    0
                    Ссылки и информацию о фильмах собирает робот с трекеров, а мы лишь вручную проверяем данные фильмов перед их публикацией на сайте. Вручную ссылки никто не добавляет, нам бы понадобились тысячи китайцев :)
                      0
                      робот так же на питоне написан?
                        0
                        Да. Уж больно язык приятный :)
                0
                ого, а рутрекер мониторите?
                  0
                  Да. Полный список сайтов можно посмотреть здесь — cinemate.cc/site/location/
                  Список периодически пополняется. В ближайшее время добавим kinozal.tv.
                0
                Первая что приходит на ум глядя на скриншот — Facebook.
                  +3
                  Может потому, что «решением стал контрол из Фейсбука и Контакта — пользователи знакомы с ним, а значит не нужно объяснять как он работает»? :)
                    0
                    и мы только за! контрол называется — fcbkcomplete, стилизацией займемся под общий редизайн
                    +11
                    Чёрт, смешной код.

                    def __init__(self, *args, **kwargs):
                        ...
                        args[0]
                    


                    Если кто-то сделает не Form(initial_data), а Form(initial=initial_data), то будет вам эксепшн.

                    А еще повеселил момент, когда собираете json. Про simplejson (который в >py2.6 import json) не слышали?

                    Ой, да вы потом еще не application/json (или хотя бы text/javascript) отдаёте клиенту, а text/html.

                    Кхм, ошибок хоть жопой жуй. Плохая статья, негодная.
                      0
                      Ух, я ошибся. Там «if args» — ну вы тут, определенно, решили проблему например.
                        +1
                        Это повод для обсуждения, спасибо
                          0
                          Я думаю, это повод даже не для обсуждения, а для прохождения туториала.
                            –1
                            про аргсы и типы, спасибо
                            про джон — проще собрать руками, убрал чтобы не смущать
                              +1
                              Проще? Ну ок.

                              А, ну да, ну да. Explicit is better than implicit.
                        +2
                        На каком языке заголовок новости?
                          +1
                          Абсолютно согласен. Мне тоже не нравится, когда так коверкают язык :(
                          Можно же сказать по-русски — что-нибудь вроде «выбор вариантов с автодополнением» или около того.
                          0
                          на нашем
                            +3
                            self.fields[field].choices = ([(int(o), Location.objects.get(id = int(o)).complete_name) for o in loc_list] )


                            Но это же ужасно, для loc_list в 1000 элементов будет сделано 1000 запросов в базу данных. Как минимум, замените на
                            Location.objects.filter(id__in=loc_list)


                            Про точки с запятой в конце строк я вообще молчу.
                              –1
                              скорее всего вы правы, в этом конкрентном месте тысяче кратного выигрыша не будет, элементов столько не бывает. точка с запятой осталась от правки раскраски кода — кстати, какая есть нормальная( чтобы можно было редактировать раскрашенный код) подсветка питона, работающая на хабре?
                                +2
                                Даже если столько локаций у вас не будет, пример кода вы все равно подаете плохой.

                                Ну и заметку: django.http.QueryDict можно сделать «мутабельным», передав ему параметр mutable=True при создании экземпляра.
                                  0
                                  насчет раскраски — хабр поддерживает тег source с параметром lang='python', гляньте в его справку по html-тегам
                                0
                                у меня в опере нихрена не работает!
                                  0
                                  Скажем нет многословию в формах!

                                  Так должно выглядеть определение тру-декларативного филда для m2m:

                                  field = AutoSuggestSelectMultiple(attrs={url:'whatever'})

                                  :)
                                    0
                                    доведем до такого состояния, на очередном рефакторинге, сейчас главное чтобы работало и приносило пользу, остальное никого не волнует, с той стороны экрана
                                    0
                                    При использовании FCBKcomplete столкнулся с двумя багами (первый — критичный):

                                    1. Если заполнить поле каким-либо значением, затем удалить его и снова выбрать это значение, то при отправке формы это значение уже не отправится (проверить можно с помощью демки: www.emposha.com/demo/fcbkcomplete_2/). Этот баг уже известен 4 месяца: github.com/emposha/FCBKcomplete/issues#issue/17, тем не менее, автор не спешит его исправить, как и остальные баги (видимо, проект более не развивается).

                                    2. Глушится TAB, из-за чего мы не можем перейти к следующему полю формы. TAB используется при выборе значения из списка, однако пока мы не начали ничего вводить в поле — глушить TAB незачем.
                                      0
                                      «Последняя строчка — основная, если у формы есть инициализационные значения — заполним choices.»
                                      Где-то вы не договариваете. Если просто заполнить choices, элементы SelectMultiple выведутся, но не будут selected. Из-за этого скрипт их не отрисовывает. У вас на странице предзаполненные города имеют параметр selected=«selected», а это значит, что вы заполняете еще и поле initial. Но код этого заполнения не показываете. Почему?
                                        0
                                        А как вы сделали, что автокомплит для городов находит соответствия и для кириллицы и для латыницы?
                                          0
                                          с этим уже сервер разбирается, можно и ошибки типа «vjcrdf» == «москва» исправлять
                                          0
                                          а может кто то подсказать как использую FCBKcomplete вводить не только то что есть в базе (autocomplete) но и новые пункты. Что то подобное есть в Wordpress с добавлением тегов cl.ly/2B01090h2A413n2w1T21
                                            0
                                            [{«caption»:«London», «value»:4}]

                                            В версии 2.7.5 fcbkcomplete формат json поменялся:

                                            [{«key»:«London», «value»:4}]

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

                                            Самое читаемое