diff mbox

[23/49] filters: Rewrite the submitter autocompletion code

Message ID 1443708774-26996-24-git-send-email-damien.lespiau@intel.com
State Accepted
Headers show

Commit Message

Damien Lespiau Oct. 1, 2015, 2:12 p.m. UTC
We now have a nice(r) autocompletion widget that will popup the list of
options directly underneath the input fild. A few changes are done in
this commit that couldn't be split any further:

- Use jQuery's $.ajax() for the completion query
- Use the application/json content type on the completion answer to
  allow jQuery to directly create an object from it instead of giving
  back a string (because of the text/plain content type)
- Crafted more logical objects in the json answer ie all properties are
  at the same level instead of the default queryset serializer that put
  the object fields in a 'field' sub-object.
- Use selectize.js for the autocompletion widget logic

A slight change in behaviour is that we now don't allow "free form"
submitter search, ie we need a valid completion to search with that
specific person (through its primary key). I didn't remove the backend
logic that allows the "free form" mechanism, but maybe we should.

v2: Squash the unit tests fixes into this patch (Jeremy Kerr)

Signed-off-by: Damien Lespiau <damien.lespiau@intel.com>
---
 patchwork/filters.py                       |  16 ++-
 patchwork/templates/patchwork/filters.html | 152 ++++++-----------------------
 patchwork/tests/test_person.py             |   4 +-
 patchwork/views/base.py                    |  18 ++--
 4 files changed, 53 insertions(+), 137 deletions(-)
diff mbox

Patch

diff --git a/patchwork/filters.py b/patchwork/filters.py
index 3f8bd3e..db671ff 100644
--- a/patchwork/filters.py
+++ b/patchwork/filters.py
@@ -87,6 +87,11 @@  class SubmitterFilter(Filter):
         self.person = None
         self.person_match = None
         submitter_id = None
+
+        str = str.strip()
+        if str == '':
+            return
+
         try:
             submitter_id = int(str)
         except ValueError:
@@ -128,15 +133,8 @@  class SubmitterFilter(Filter):
         return ''
 
     def _form(self):
-        name = ''
-        if self.person:
-            name = self.person.name
-        return mark_safe(('<input onKeyUp="submitter_field_change(this)" ' +
-                'name="submitter" id="submitter_input" ' +
-                'class="form-control"' +
-                        'value="%s">' % escape(name)) +
-                '<select id="submitter_select" ' +
-                'disabled="true"></select>')
+        return mark_safe(('<input type="text" name="submitter" ' + \
+                          'id="submitter_input" class="form-control">'))
 
     def key(self):
         if self.person:
diff --git a/patchwork/templates/patchwork/filters.html b/patchwork/templates/patchwork/filters.html
index 842e643..6ab8108 100644
--- a/patchwork/templates/patchwork/filters.html
+++ b/patchwork/templates/patchwork/filters.html
@@ -19,127 +19,40 @@  function filter_click()
 
 
 }
-function enable_selected_submitter(select, input)
-{
-    select.name = 'submitter';
-    input.name = '';
-}
-function filter_form_submit(form)
-{
-    var i;
-
-    var submitter_select = document.getElementById("submitter_select");
-    var submitter_input = document.getElementById("submitter_input");
-    if (!submitter_select || !submitter_input) {
-        req = null;
-        return;
-    }
-
-    /* submitter handling. if possible, use the select box, otherwise leave
-     * as-is (and so the text box is used). */
-
-    if (submitter_select.options.length == 0) {
-        /* if there's no match, just use the input */
-
-    } else if (submitter_select.options.length == 1) {
-        /* if there's only one match, request by id */
-        submitter_select.selectedIndex = 0;
-        enable_selected_submitter(submitter_select, submitter_input);
-
-    } else if (submitter_select.selectedIndex != -1) {
-        /* if the user has explicitly selected, request by id */
-        enable_selected_submitter(submitter_select, submitter_input);
-
-    }
 
-    for (i = 0; i < form.elements.length; i++) {
-        var e = form.elements[i];
-        if (e.type == 'submit') {
-            continue;
-        }
-
-        /* handle submitter data */
-        if (e.type == 'select-one') {
-            if (e.name == '') {
-                e.disabled = true;
-            }
-            if (e.selectedIndex != -1
-                    && e.options[e.selectedIndex].value == '') {
-                e.disabled = true;
+$(document).ready(function() {
+    $('#submitter_input').selectize({
+        valueField: 'pk',
+        labelField: 'name',
+        searchField: ['name', 'email'],
+        maxItems: 1,
+        persist: false,
+        render: {
+            option: function(item, escape) {
+                return '<div>' + escape(item.name) + ' &lt;' +
+                                 escape(item.email) + '&gt;' + '</div>';
+            },
+            item: function(item, escape) {
+                return '<div>' + escape(item.name) + '</div>';
             }
-
-            continue;
-        }
-
-        if (e.value == '') {
-            e.disabled = true;
-        }
-    }
-}
-
-var req = null;
-
-function submitter_complete_response()
-{
-    if (req.readyState != 4) {
-        return
-    }
-
-    var completions;
-    eval("completions = " + req.responseText);
-
-    if (completions.length == 0) {
-        req = null;
-        return;
-    }
-
-    var submitter_select = document.getElementById("submitter_select");
-    var submitter_input = document.getElementById("submitter_input");
-    if (!submitter_select || !submitter_input) {
-        req = null;
-        return;
-    }
-
-    for (i = 0; i < completions.length; i++) {
-        name = completions[i]['fields']['name'];
-        if (name) {
-            name = completions[i]['fields']['name'] +
-                ' <' + completions[i]['fields']['email'] + '>';
-        } else {
-            name = completions[i]['fields']['email'];
+        },
+        load: function(query, callback) {
+            if (query.length < 4)
+                return callback();
+
+            req = $.ajax({
+                url: '{% url 'patchwork.views.submitter_complete' %}?q=' +
+                      encodeURIComponent(query) + '&l=10',
+                error: function() {
+                    callback();
+                },
+                success: function(res) {
+                    callback(res);
+                }
+            });
         }
-        o = new Option(name, completions[i]['pk']);
-        submitter_select.options[i] = o;
-    }
-
-    /* remove remaining options */
-    for (; i < submitter_select.length; i++) {
-        submitter_select.options[i] = null;
-    }
-
-    submitter_select.disabled = false;
-    req = null;
-}
-
-function submitter_field_change(field)
-{
-    var limit = 20;
-    var value = field.value;
-    if (value.length < 4) {
-        return;
-    }
-
-    if (req) {
-         return;
-    }
-
-    var url = '{% url 'patchwork.views.submitter_complete' %}?q=' + value +
-                        '&l=' + limit;
-    req = new XMLHttpRequest();
-    req.onreadystatechange = submitter_complete_response;
-    req.open("GET", url, true);
-    req.send('');
-}
+    });
+});
 </script>
 
 <div class="filters">
@@ -162,8 +75,7 @@  function submitter_field_change(field)
  {% endif %}
  </div>
  <div id="filterform" style="padding-top: 1em; display: none">
-  <form class="form-horizontal" role="form" method="get"
-   onSubmit="return filter_form_submit(this)">
+  <form class="form-horizontal" role="form" method="get">
    {% for filter in filters.available_filters %}
    <div class="form-group">
     <label class="col-sm-2 control-label">{{ filter.name }}</label>
diff --git a/patchwork/tests/test_person.py b/patchwork/tests/test_person.py
index d948096..5ce8713 100644
--- a/patchwork/tests/test_person.py
+++ b/patchwork/tests/test_person.py
@@ -36,14 +36,14 @@  class SubmitterCompletionTest(TestCase):
         self.assertEquals(response.status_code, 200)
         data = json.loads(response.content)
         self.assertEquals(len(data), 1)
-        self.assertEquals(data[0]['fields']['name'], 'Test Name')
+        self.assertEquals(data[0]['name'], 'Test Name')
 
     def testEmailComplete(self):
         response = self.client.get('/submitter/', {'q': 'test2'})
         self.assertEquals(response.status_code, 200)
         data = json.loads(response.content)
         self.assertEquals(len(data), 1)
-        self.assertEquals(data[0]['fields']['email'], 'test2@example.com')
+        self.assertEquals(data[0]['email'], 'test2@example.com')
 
     def testCompleteLimit(self):
         for i in range(3,10):
diff --git a/patchwork/views/base.py b/patchwork/views/base.py
index 6d7dd13..9d90f56 100644
--- a/patchwork/views/base.py
+++ b/patchwork/views/base.py
@@ -17,12 +17,13 @@ 
 # along with Patchwork; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+import json
 
 from patchwork.models import Patch, Project, Person, EmailConfirmation
 from django.shortcuts import render_to_response, get_object_or_404
 from django.http import HttpResponse, HttpResponseRedirect, Http404
 from patchwork.requestcontext import PatchworkRequestContext
-from django.core import serializers, urlresolvers
+from django.core import urlresolvers
 from django.template.loader import render_to_string
 from django.conf import settings
 from django.db.models import Q
@@ -87,10 +88,9 @@  def confirm(request, key):
 def submitter_complete(request):
     search = request.GET.get('q', '')
     limit = request.GET.get('l', None)
-    response = HttpResponse(content_type = "text/plain")
 
     if len(search) <= 3:
-        return response
+        return HttpResponse(content_type="application/json")
 
     queryset = Person.objects.filter(Q(name__icontains = search) |
                                      Q(email__icontains = search))
@@ -103,9 +103,15 @@  def submitter_complete(request):
     if limit is not None and limit > 0:
             queryset = queryset[:limit]
 
-    json_serializer = serializers.get_serializer("json")()
-    json_serializer.serialize(queryset, ensure_ascii=False, stream=response)
-    return response
+    data = []
+    for submitter in queryset:
+        item = {}
+        item['pk'] = submitter.id
+        item['name'] = submitter.name
+        item['email'] = submitter.email
+        data.append(item)
+
+    return HttpResponse(json.dumps(data), content_type="application/json")
 
 help_pages = {'':           'index.html',
               'about/':     'about.html',