diff --git a/djangoql/admin.py b/djangoql/admin.py
index f54b98a..5abd165 100644
--- a/djangoql/admin.py
+++ b/djangoql/admin.py
@@ -4,6 +4,7 @@
from django.contrib import messages
from django.contrib.admin.views.main import ChangeList
from django.core.exceptions import FieldError, ValidationError
+from django.http.response import JsonResponse
from django.forms import Media
from django.http import HttpResponse
from django.views.generic import TemplateView
@@ -12,6 +13,9 @@
from .exceptions import DjangoQLError
from .queryset import apply_search
from .schema import DjangoQLSchema
+from .models import history
+
+import sys
DJANGOQL_SEARCH_MARKER = 'q-l'
@@ -57,10 +61,9 @@ def get_search_results(self, request, queryset, search_term):
if not search_term:
return queryset, use_distinct
try:
- return (
- apply_search(queryset, search_term, self.djangoql_schema),
- use_distinct,
- )
+ qs = apply_search(queryset, search_term, self.djangoql_schema)
+ self.new_history_item(request.user, search_term)
+ return ( qs, use_distinct, )
except (DjangoQLError, ValueError, FieldError) as e:
msg = text_type(e)
except ValidationError as e:
@@ -101,6 +104,11 @@ def get_urls(self):
self.model._meta.model_name,
),
),
+ url(
+ r'^djangoql-history/$',
+ self.admin_site.admin_view(self.get_history),
+ name='history',
+ ),
url(
r'^djangoql-syntax/$',
TemplateView.as_view(
@@ -117,3 +125,20 @@ def introspect(self, request):
content=json.dumps(response, indent=2),
content_type='application/json; charset=utf-8',
)
+
+ def new_history_item(self,user,search_term):
+ history_item = history()
+ history_item.user = user
+ history_item.query = search_term
+ history_item.save()
+
+ def get_history(self, request):
+ reverse_history_qs = history.objects.filter(user=request.user)
+ history_qs = reverse_history_qs.order_by('-pk')
+ lastQueries = []
+ for i,q in enumerate(history_qs):
+ if i < 200: #limit history to 200 items per user
+ lastQueries.append(q.query)
+ else:
+ q.delete()
+ return JsonResponse({'history':lastQueries})
diff --git a/djangoql/migrations/0001_initial.py b/djangoql/migrations/0001_initial.py
new file mode 100644
index 0000000..3e1f962
--- /dev/null
+++ b/djangoql/migrations/0001_initial.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.0.6 on 2018-06-15 20:22
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='history',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('query', models.CharField(max_length=2000)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
diff --git a/djangoql/migrations/__init__.py b/djangoql/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/djangoql/models.py b/djangoql/models.py
new file mode 100644
index 0000000..58bc428
--- /dev/null
+++ b/djangoql/models.py
@@ -0,0 +1,6 @@
+from django.db import models
+from django.contrib.auth.models import User
+
+class history(models.Model):
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
+ query = models.CharField(max_length=2000)
diff --git a/djangoql/static/djangoql/js/completion.js b/djangoql/static/djangoql/js/completion.js
index 3b6c1b8..dd7fe35 100644
--- a/djangoql/static/djangoql/js/completion.js
+++ b/djangoql/static/djangoql/js/completion.js
@@ -114,6 +114,8 @@
return {
currentModel: null,
models: {},
+ currentHistory: null,
+ historyEnabled: false,
token: token,
lexer: lexer,
@@ -130,13 +132,18 @@
init: function (options) {
var syntaxHelp;
+ var history;
+ var history_enabled = false;
// Initialization
if (!this.isObject(options)) {
this.logError('Please pass an object with initialization parameters');
return;
}
+
this.loadIntrospections(options.introspections);
+ this.loadHistory(options.history)
+
this.textarea = document.querySelector(options.selector);
if (!this.textarea) {
this.logError('Element not found by selector: ' + options.selector);
@@ -199,6 +206,17 @@
document.querySelector('body').appendChild(this.completion);
this.completionUL = document.createElement('ul');
this.completion.appendChild(this.completionUL);
+
+ history = document.createElement('p');
+ history.className = 'syntax-help';
+ history.innerHTML = 'Search History';
+ history.addEventListener('mousedown', function (e) {
+ this.historyEnabled = true;
+ this.loadHistory(options.history);
+ this.renderHistory();
+ }.bind(this));
+ this.completion.appendChild(history);
+
if (typeof options.syntaxHelp === 'string') {
syntaxHelp = document.createElement('p');
syntaxHelp.className = 'syntax-help';
@@ -223,6 +241,35 @@
this.hideCompletion();
},
+ loadHistory: function (history) {
+ var onLoadError;
+ var request;
+
+ onLoadError = function (history) {
+ this.logError('failed to load history from' + history);
+ }.bind(this);
+ request = new XMLHttpRequest();
+ request.open('GET', history, true);
+ request.onload = function () {
+ var data;
+ if (request.status === 200) {
+ data = JSON.parse(request.responseText);
+ this.currentHistory = data.history;
+ } else {
+ onLoadError();
+ }
+ }.bind(this);
+ request.ontimeout = onLoadError;
+ request.onerror = onLoadError;
+ /* eslint-disable max-len */
+ // Workaround for IE9, see
+ // https://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/
+ /* eslint-enable max-len */
+ request.onprogress = function () {};
+ window.setTimeout(request.send.bind(request));
+
+ },
+
loadIntrospections: function (introspections) {
var onLoadError;
var request;
@@ -318,7 +365,12 @@
},
onCompletionMouseClick: function (e) {
- this.selectCompletion(parseInt(e.target.getAttribute('data-index'), 10));
+ if (!this.historyEnabled) {
+ this.selectCompletion(parseInt(e.target.getAttribute('data-index'), 10));
+ } else {
+ this.textarea.value = e.target.textContent;
+ document.getElementById('changelist-search').submit();
+ }
},
onCompletionMouseDown: function (e) {
@@ -327,16 +379,21 @@
},
onCompletionMouseOut: function () {
- this.selected = null;
- this.debouncedRenderCompletion();
+ if (!this.historyEnabled) {
+ this.selected = null;
+ this.debouncedRenderCompletion();
+ }
},
onCompletionMouseOver: function (e) {
- this.selected = parseInt(e.target.getAttribute('data-index'), 10);
- this.debouncedRenderCompletion();
+ if (!this.historyEnabled) {
+ this.selected = parseInt(e.target.getAttribute('data-index'), 10);
+ this.debouncedRenderCompletion();
+ }
},
onKeydown: function (e) {
+
switch (e.keyCode) {
case 38: // up arrow
if (this.suggestions.length) {
@@ -419,8 +476,10 @@
},
popupCompletion: function () {
- this.generateSuggestions();
- this.renderCompletion();
+ if (!this.historyEnabled) {
+ this.generateSuggestions();
+ this.renderCompletion();
+ }
},
selectCompletion: function (index) {
@@ -445,13 +504,17 @@
this.textareaResize();
}
this.generateSuggestions(this.textarea);
- this.renderCompletion();
+ if (!this.historyEnabled) {
+ this.renderCompletion();
+ }
},
hideCompletion: function () {
- this.selected = null;
- if (this.completion) {
- this.completion.style.display = 'none';
+ if (!this.historyEnabled) {
+ this.selected = null;
+ if (this.completion) {
+ this.completion.style.display = 'none';
+ }
}
},
@@ -472,6 +535,80 @@
'$1');
},
+ renderHistory: function (dontForceDisplay) {
+ var currentLi;
+ var i;
+ var completionRect;
+ var currentLiRect;
+ var inputRect;
+ var li;
+ var liLen;
+ var historyLen;
+
+ if (!this.completionEnabled) {
+ this.hideCompletion();
+ return;
+ }
+
+ if (dontForceDisplay && this.completion.style.display === 'none') {
+ return;
+ }
+ if (!this.currentHistory.length) {
+ this.hideCompletion();
+ return;
+ }
+
+ historyLen = this.currentHistory.length;
+ li = [].slice.call(this.completionUL.querySelectorAll('li'));
+ liLen = li.length;
+
+ // Update or create necessary elements
+ for (i = 0; i < historyLen; i++) {
+ if (i < liLen) {
+ currentLi = li[i];
+ } else {
+ currentLi = document.createElement('li');
+ currentLi.setAttribute('data-index', i);
+ this.completionUL.appendChild(currentLi);
+ currentLi.addEventListener('click', this.onCompletionMouseClick);
+ currentLi.addEventListener('mousedown', this.onCompletionMouseDown);
+ currentLi.addEventListener('mouseout', this.onCompletionMouseOut);
+ currentLi.addEventListener('mouseover', this.onCompletionMouseOver);
+ }
+ currentLi.textContent = this.currentHistory[i];
+
+ if (this.currentHistory[i] == this.selected) {
+ currentLi.className = 'active';
+ currentLiRect = currentLi.getBoundingClientRect();
+ completionRect = this.completionUL.getBoundingClientRect();
+ if (currentLiRect.bottom > completionRect.bottom) {
+ this.completionUL.scrollTop = this.completionUL.scrollTop + 2 +
+ (currentLiRect.bottom - completionRect.bottom);
+ } else if (currentLiRect.top < completionRect.top) {
+ this.completionUL.scrollTop = this.completionUL.scrollTop - 2 -
+ (completionRect.top - currentLiRect.top);
+ }
+ } else {
+ currentLi.className = '';
+ }
+ }
+ // Remove redundant elements
+ while (liLen > historyLen) {
+ liLen--;
+ li[liLen].removeEventListener('click', this.onCompletionMouseClick);
+ li[liLen].removeEventListener('mousedown', this.onCompletionMouseDown);
+ li[liLen].removeEventListener('mouseout', this.onCompletionMouseOut);
+ li[liLen].removeEventListener('mouseover', this.onCompletionMouseOver);
+ this.completionUL.removeChild(li[liLen]);
+ }
+
+ inputRect = this.textarea.getBoundingClientRect();
+ this.completion.style.top = window.pageYOffset + inputRect.top +
+ inputRect.height + 'px';
+ this.completion.style.left = inputRect.left + 'px';
+ this.completion.style.display = 'block';
+ },
+
renderCompletion: function (dontForceDisplay) {
var currentLi;
var i;
@@ -482,6 +619,11 @@
var liLen;
var suggestionsLen;
+ if (this.historyEnabled) {
+ this.historyEnabled = false;
+ return
+ }
+
if (!this.completionEnabled) {
this.hideCompletion();
return;
@@ -495,6 +637,10 @@
return;
}
+ if (!this.currentHistory) {
+ return;
+ }
+
suggestionsLen = this.suggestions.length;
li = [].slice.call(this.completionUL.querySelectorAll('li'));
liLen = li.length;
@@ -669,6 +815,7 @@
return { prefix: prefix, scope: scope, model: model, field: field };
},
+
generateSuggestions: function () {
var input = this.textarea;
var context;
@@ -681,6 +828,12 @@
var textBefore;
var textAfter;
+
+ if (this.historyEnabled) {
+ this.historyEnabled = false;
+ return
+ }
+
if (!this.completionEnabled) {
this.prefix = '';
this.suggestions = [];
diff --git a/djangoql/static/djangoql/js/completion_admin.js b/djangoql/static/djangoql/js/completion_admin.js
index 0136dff..a81eb68 100644
--- a/djangoql/static/djangoql/js/completion_admin.js
+++ b/djangoql/static/djangoql/js/completion_admin.js
@@ -26,6 +26,31 @@
return result;
}
+ function parseQueryString() {
+ var qs = window.location.search.substring(1);
+ var result = {};
+ var vars = qs.split('&');
+ var i;
+ var l = vars.length;
+ var pair;
+ var key;
+ for (i = 0; i < l; i++) {
+ pair = vars[i].split('=');
+ key = decodeURIComponent(pair[0]);
+ if (key) {
+ if (typeof result[key] !== 'undefined') {
+ if (({}).toString.call(result[key]) !== '[object Array]') {
+ result[key] = [result[key]];
+ }
+ result[key].push(decodeURIComponent(pair[1]));
+ } else {
+ result[key] = decodeURIComponent(pair[1]);
+ }
+ }
+ }
+ return result;
+ }
+
// Replace standard search input with textarea and add completion toggle
DjangoQL.DOMReady(function () {
// use '-' in the param name to prevent conflicts with any model field name
@@ -36,6 +61,7 @@
var QLPlaceholder = 'Advanced search with Query Language';
var originalPlaceholder;
var textarea;
+ var datalist;
var input = document.querySelector('input[name=q]');
if (!input) {
@@ -86,6 +112,7 @@
textarea.rows = 1;
textarea.placeholder = QLEnabled ? QLPlaceholder : originalPlaceholder;
textarea.setAttribute('maxlength', 2000);
+
input.parentNode.insertBefore(textarea, input);
input.parentNode.removeChild(input);
@@ -95,6 +122,7 @@
completionEnabled: QLEnabled,
introspections: 'introspect/',
syntaxHelp: 'djangoql-syntax/',
+ history: 'djangoql-history/',
selector: 'textarea[name=q]',
autoResize: true
});