Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asynchronous suggestions for field values with a limit #52

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,23 @@ In this example we've defined a custom GroupNameField that sorts suggestions
for group names by popularity (no. of users in a group) instead of default
alphabetical sorting.

**Set value suggestions limit**

By default, field value suggestions number is limited to 50 entries.
If you want to change this, override ``suggestions_limit`` field in your custom schema.

.. code:: python

from djangoql.schema import DjangoQLSchema, StrField


class UserQLSchema(DjangoQLSchema):
exclude = (Book,)
suggest_options = {
Group: ['name'],
}
suggestions_limit = 30 # Set a desired limit here

**Custom search lookup**

DjangoQL base fields provide two basic methods that you can override to
Expand Down
14 changes: 14 additions & 0 deletions djangoql/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ def get_urls(self):
)),
name='djangoql_syntax_help',
),
url(
r'^suggestions/$',
self.admin_site.admin_view(self.field_value_suggestions),
name='djangoql_field_value_suggestions',
),
]
return custom_urls + super(DjangoQLSearchMixin, self).get_urls()

Expand All @@ -132,3 +137,12 @@ def introspect(self, request):
content=json.dumps(response, indent=2),
content_type='application/json; charset=utf-8',
)

def field_value_suggestions(self, request):
suggestions = self.djangoql_schema(self.model)\
.get_field_instance(self.model, request.GET['field_name'])\
.get_sugestions(request.GET['text'])
return HttpResponse(
content=json.dumps(list(suggestions), indent=2),
content_type='application/json; charset=utf-8',
)
33 changes: 26 additions & 7 deletions djangoql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class DjangoQLField(object):
value_types_description = ''

def __init__(self, model=None, name=None, nullable=None,
suggest_options=None):
suggest_options=None, suggestions_limit=None):
if model is not None:
self.model = model
if name is not None:
Expand All @@ -36,12 +36,16 @@ def __init__(self, model=None, name=None, nullable=None,
self.nullable = nullable
if suggest_options is not None:
self.suggest_options = suggest_options
if suggestions_limit is not None:
self.suggestions_limit = suggestions_limit

def _get_options_queryset(self):
return self.model.objects.order_by(self.name)

def as_dict(self):
return {
'type': self.type,
'nullable': self.nullable,
'options': list(self.get_options()) if self.suggest_options else [],
}

def _field_choices(self):
Expand All @@ -54,15 +58,27 @@ def _field_choices(self):

def get_options(self):
"""
Override this method to provide custom suggestion options
DEPRECATED: field value suggestions are now using get_sugestions() method
"""
choices = self._field_choices()
if choices:
return [c[1] for c in choices]
else:
return self.model.objects.\
order_by(self.name).\
values_list(self.name, flat=True)
return self._get_options_queryset().values_list(self.name, flat=True)

def get_sugestions(self, text):
"""
Override this method to provide custom suggestion options
"""
choices = self._field_choices()
if choices:
return [c[1] for c in choices]

kwargs = {'{}__icontains'.format(self.name): text}

return self._get_options_queryset()\
.filter(**kwargs)[:self.suggestions_limit]\
.values_list(self.name, flat=True)

def get_lookup_name(self):
"""
Expand Down Expand Up @@ -267,12 +283,13 @@ class RelationField(DjangoQLField):
type = 'relation'

def __init__(self, model, name, related_model, nullable=False,
suggest_options=False):
suggest_options=False, suggestions_limit=None):
super(RelationField, self).__init__(
model=model,
name=name,
nullable=nullable,
suggest_options=suggest_options,
suggestions_limit=suggestions_limit,
)
self.related_model = related_model

Expand All @@ -290,6 +307,7 @@ class DjangoQLSchema(object):
include = () # models to include into introspection
exclude = () # models to exclude from introspection
suggest_options = None
suggestions_limit = 50

def __init__(self, model):
if not inspect.isclass(model) or not issubclass(model, models.Model):
Expand Down Expand Up @@ -397,6 +415,7 @@ def get_field_instance(self, model, field_name):
field_kwargs['suggest_options'] = (
field.name in self.suggest_options.get(model, [])
)
field_kwargs['suggestions_limit'] = self.suggestions_limit
field_instance = field_cls(**field_kwargs)
# Check if suggested options conflict with field type
if field_cls != StrField and field_instance.suggest_options:
Expand Down
27 changes: 24 additions & 3 deletions djangoql/static/djangoql/js/completion.js
Original file line number Diff line number Diff line change
Expand Up @@ -807,9 +807,12 @@
}.bind(this);
}
this.highlightCaseSensitive = this.valuesCaseSensitive;
this.suggestions = field.options.map(function (f) {
return suggestion(f, snippetBefore, snippetAfter);
});
this.loadSuggestions(
context.prefix, context.field, function (responseData) {
this.suggestions = responseData.map(function (f) {
return suggestion(f, snippetBefore, snippetAfter);
});
}.bind(this));
} else if (field.type === 'bool') {
this.suggestions = [
suggestion('True', '', ' '),
Expand Down Expand Up @@ -842,6 +845,24 @@
} else {
this.selected = null;
}
},
// Load suggestions from backend
loadSuggestions: function (text, fieldName, callback) {
var xhr = new XMLHttpRequest();
var params = new URLSearchParams();
params.set('text', text);
params.set('field_name', fieldName);
xhr.open(
'GET',
this.options.suggestionsUrl + params.toString(),
true
);
xhr.onload = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
callback(JSON.parse(xhr.responseText));
}
};
xhr.send();
}

};
Expand Down
1 change: 1 addition & 0 deletions djangoql/static/djangoql/js/completion_admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
djangoQL = new DjangoQL({
completionEnabled: QLEnabled,
introspections: 'introspect/',
suggestionsUrl: 'suggestions/?',
syntaxHelp: 'djangoql-syntax/',
selector: 'textarea[name=q]',
autoResize: true
Expand Down
5 changes: 3 additions & 2 deletions test_project/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.utils.timezone import now

from djangoql.admin import DjangoQLSearchMixin
from djangoql.schema import DjangoQLSchema, IntField
from djangoql.schema import DjangoQLSchema, IntField, StrField

from .models import Book

Expand Down Expand Up @@ -89,8 +89,9 @@ def years_ago(self, n):
class UserQLSchema(DjangoQLSchema):
exclude = (Book,)
suggest_options = {
Group: ['name'],
Group: ['name'], User: ['username']
}
suggestions_limit = 30

def get_fields(self, model):
fields = super(UserQLSchema, self).get_fields(model)
Expand Down
5 changes: 4 additions & 1 deletion test_project/core/templates/completion_demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
// doesn't fit, and shrink back when text is removed. The purpose
// of this is to see full search query without scrolling, could be
// helpful for really long queries.
autoResize: true
autoResize: true,

// An URL to load field value suggestions
suggestionsUrl: 'suggestions/?'
});
});
</script>
Expand Down
34 changes: 34 additions & 0 deletions test_project/core/tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,37 @@ def test_validation_fail(self):
self.fail('This query should\'t pass validation: %s' % query)
except DjangoQLSchemaError as e:
pass


class UserQLSchema(DjangoQLSchema):
include = (User,)
suggestions_limit = 30


class BookQLSchema(DjangoQLSchema):
include = (Book,)
suggestions_limit = 2


class DjangoQLFieldTest(TestCase):
def setUp(self):
for i in range(2, 70):
User.objects.create(username='a' * i)

def test_get_suggestions_limit_doesnt_affect_choices(self):
result = BookQLSchema(Book).get_field_instance(Book, 'genre').get_sugestions('blah')
self.assertEqual(len(result), len(Book.GENRES))

def test_get_suggestions_with_string_field_should_be_limited(self):
suggestions = IncludeUserGroupSchema(User).get_field_instance(User, 'username').get_sugestions('aaa')
self.assertEqual(len(suggestions), 50)

def test_get_suggestions_with_custom_limit(self):
suggestions = UserQLSchema(User).get_field_instance(User, 'username').get_sugestions('aaa')
self.assertEqual(len(suggestions), UserQLSchema.suggestions_limit)

def test_suggestions_should_contain_query_string(self):
query_string = 'aaa'
suggestions = UserQLSchema(User).get_field_instance(User, 'username').get_sugestions(query_string)
for suggestion in suggestions:
self.assertIn(query_string, suggestion)
12 changes: 12 additions & 0 deletions test_project/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.contrib.auth.models import Group, User
from django.shortcuts import render
from django.views.decorators.http import require_GET
from django.http.response import HttpResponse

from djangoql.exceptions import DjangoQLError
from djangoql.queryset import apply_search
Expand Down Expand Up @@ -30,3 +31,14 @@ def completion_demo(request):
'search_results': query,
'introspections': json.dumps(UserQLSchema(query.model).as_dict()),
})


@require_GET
def suggestions(request):
payload = UserQLSchema(User) \
.get_field_instance(User, request.GET['field_name']) \
.get_sugestions(request.GET['text'])
return HttpResponse(
content=json.dumps(list(payload), indent=2),
content_type='application/json; charset=utf-8',
)
3 changes: 2 additions & 1 deletion test_project/test_project/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
from django.conf.urls import url, include
from django.contrib import admin

from core.views import completion_demo
from core.views import completion_demo, suggestions


urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$', completion_demo),
url(r'^suggestions/', suggestions),
]

if settings.DEBUG and settings.DJDT:
Expand Down