This commit is contained in:
d42 2018-04-27 22:53:38 +02:00
parent 10330601b8
commit fe073182f1
7 changed files with 407 additions and 33 deletions

View File

@ -9,6 +9,7 @@ set -e
psql --dbname template1 -U postgres <<EOSQL psql --dbname template1 -U postgres <<EOSQL
CREATE EXTENSION hstore; CREATE EXTENSION hstore;
CREATE EXTENSION ltree; CREATE EXTENSION ltree;
CREATE EXTENSION pg_trgm;
DROP DATABASE $POSTGRES_USER; DROP DATABASE $POSTGRES_USER;
CREATE DATABASE $POSTGRES_USER TEMPLATE template1; CREATE DATABASE $POSTGRES_USER TEMPLATE template1;
EOSQL EOSQL

View File

@ -0,0 +1,212 @@
var xD = $;
var initDjangoHStoreWidget = function(hstore_field_name, inline_prefix) {
// ignore inline templates
// if hstore_field_name contains "__prefix__"
if(hstore_field_name.indexOf('__prefix__') > -1){
return;
}
var $ = django.jQuery;
// processing inlines
if(hstore_field_name.indexOf('inline') > -1){
var inlineClass = $('#id_'+hstore_field_name).parents('.inline-related, .grp-group').attr('class');
// if using TabularInlines stop here
// TabularInlines not supported
if (inlineClass.indexOf('tabular') > -1) {
return;
}
}
// reusable function that retrieves a template even if ID is not correct
// (written to support inlines)
var retrieveTemplate = function(template_name, field_name){
var specific_template = $('#'+template_name+'-'+field_name);
// if found specific template return that
if(specific_template.length){
return specific_template.html();
}
else{
// get fallback template
var html = $('.'+template_name+'-inline').html();
// replace all occurrences of __prefix__ with field_name
// and return
html = html.replace(/__prefix__/g, inline_prefix);
return html;
}
}
// reusable function that compiles the UI
var compileUI = function(params){
var hstore_field_id = 'id_'+hstore_field_name,
original_textarea = $('#'+hstore_field_id),
original_value = original_textarea.val(),
original_container = original_textarea.parents('.form-row, .grp-row').eq(0),
errorHtml = original_container.find('.errorlist').html(),
json_data = {};
if(original_value !== ''){
// manage case in which textarea is blank
try{
json_data = JSON.parse(original_value);
}
catch(e){
alert('invalid JSON:\n'+e);
return false;
}
}
var hstore_field_data = {
"id": hstore_field_id,
"label": original_container.find('label').text(),
"name": hstore_field_name,
"value": original_textarea.val(),
"help": original_container.find('.grp-help, .help').text(),
"errors": errorHtml,
"data": json_data
},
// compile template
ui_html = retrieveTemplate('hstore-ui-template', hstore_field_name),
compiled_ui_html = _.template(ui_html, hstore_field_data);
// this is just to DRY up a bit
if(params && params.replace_original === true){
// remove original textarea to avoid having two textareas with same ID
original_textarea.remove();
// inject compiled template and hide original
original_container.after(compiled_ui_html).hide();
}
return compiled_ui_html;
};
// generate UI
compileUI({ replace_original: true });
// cache other objects that we'll reuse
var row_html = retrieveTemplate('hstore-row-template', hstore_field_name),
empty_row = _.template(row_html, { 'key': '', 'value': '' }),
$hstore = $('#id_'+hstore_field_name).parents('.hstore');
// reusable function that updates the textarea value
var updateTextarea = function(container) {
// init empty json object
var new_value = {},
raw_textarea = container.find('textarea'),
rows = container.find('.form-row, .grp-row');
// loop over each object and populate json
rows.each(function() {
var inputs = $(this).find('input'),
key = $(this).find('.hs-key').val(),
value = $(this).find('.hs-val').val();
new_value[key] = value;
});
// update textarea value
$(raw_textarea).val(JSON.stringify(new_value, null, 4));
};
// remove row link
$hstore.delegate('a.remove-row', 'click', function(e) {
e.preventDefault();
// cache container jquery object before $(this) gets removed
$(this).parents('.form-row, .grp-row').eq(0).remove();
updateTextarea($hstore);
});
// add row link
$hstore.delegate('a.hs-add-row, .hs-add-row a', 'click', function(e) {
e.preventDefault();
$hstore.find('.hstore-rows').append(empty_row);
xD('.django-select2').djangoSelect2()
xD('select').on( 'select2:close', function () {
$(this).focus();
});
});
// toggle textarea link
$hstore.delegate('.hstore-toggle-txtarea', 'click', function(e) {
e.preventDefault();
var raw_textarea = $hstore.find('.hstore-textarea'),
hstore_rows = $hstore.find('.hstore-rows'),
add_row = $hstore.find('.hs-add-row');
if(raw_textarea.is(':visible')) {
var compiled_ui = compileUI();
// in case of JSON error
if(compiled_ui === false){
return;
}
// jquery < 1.8
try{
var $ui = $(compiled_ui);
}
// jquery >= 1.8
catch(e){
var $ui = $($.parseHTML(compiled_ui));
}
// update rows with only relevant content
hstore_rows.html($ui.find('.hstore-rows').html());
raw_textarea.hide();
hstore_rows.show();
add_row.show();
xD('.django-select2').djangoSelect2()
}
else{
raw_textarea.show();
hstore_rows.hide();
add_row.hide();
}
});
// update textarea whenever a field changes
$hstore.delegate('.hs-val', 'keyup', function() {
updateTextarea($hstore);
});
$hstore.delegate('.hs-key', 'change', function() {
updateTextarea($hstore);
});
};
django.jQuery(window).load(function() {
// support inlines
// bind only once
if(django.hstoreWidgetBoundInlines === undefined){
var $ = django.jQuery;
$('.grp-group .grp-add-handler, .inline-group .hs-add-row a, .inline-group .add-row').click(function(e){
var hstore_original_textareas = $(this).parents('.grp-group, .inline-group').eq(0).find('.hstore-original-textarea');
// if module contains .hstore-original-textarea
if(hstore_original_textareas.length > 0){
// loop over each inline
$(this).parents('.grp-group, .inline-group').find('.grp-items div.grp-dynamic-form, .inline-related').each(function(e, i){
var prefix = i;
// loop each textarea
$(this).find('.hstore-original-textarea').each(function(e, i){
// cache field name
var field_name = $(this).attr('name');
// ignore templates
// if name attribute contains __prefix__
if(field_name.indexOf('prefix') > -1){
// skip to next
return;
}
initDjangoHStoreWidget(field_name, prefix);
});
});
}
});
django.hstoreWidgetBoundInlines = true;
}
});

View File

@ -1,21 +1,11 @@
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from .models import Item, ItemImage, Category, Label
from django_select2.forms import ModelSelect2Widget, Select2MultipleWidget from django_select2.forms import ModelSelect2Widget, Select2MultipleWidget
from .models import Item, ItemImage, Category, Label
from .widgets import ItemSelectWidget, PropsSelectWidget
class ItemSelectWidget(ModelSelect2Widget):
search_fields = [
'name__icontains',
'description__icontains'
]
def __init__(self, *args, **kwargs):
kwargs['data_view'] = 'item-complete'
super(ItemSelectWidget, self).__init__(*args, **kwargs)
def label_from_instance(self, obj):
return obj.name
class ItemForm(forms.ModelForm): class ItemForm(forms.ModelForm):
name = forms.CharField(widget=forms.TextInput()) name = forms.CharField(widget=forms.TextInput())
@ -25,16 +15,20 @@ class ItemForm(forms.ModelForm):
exclude = [] exclude = []
widgets = { widgets = {
'parent': ItemSelectWidget, 'parent': ItemSelectWidget,
'categories': Select2MultipleWidget 'categories': Select2MultipleWidget,
'props': PropsSelectWidget
} }
class ItemImageInline(admin.TabularInline): class ItemImageInline(admin.TabularInline):
model = ItemImage model = ItemImage
extra = 1 extra = 1
class LabelInline(admin.TabularInline): class LabelInline(admin.TabularInline):
model = Label model = Label
class ItemAdmin(admin.ModelAdmin): class ItemAdmin(admin.ModelAdmin):
list_display = ('_name',) list_display = ('_name',)
list_filter = ('categories',) list_filter = ('categories',)
@ -75,5 +69,6 @@ class ItemAdmin(admin.ModelAdmin):
with Item.disabled_tree_trigger(): with Item.disabled_tree_trigger():
return super(ItemAdmin, self).response_action(request, queryset) return super(ItemAdmin, self).response_action(request, queryset)
admin.site.register(Item, ItemAdmin) admin.site.register(Item, ItemAdmin)
admin.site.register(Category) admin.site.register(Category)

View File

@ -1,10 +1,13 @@
from django.conf.urls import include, url from django.conf.urls import include, url
from storage.views import index, search, item_display, label_lookup, ItemSelectView from storage.views import (
index, search, item_display, label_lookup, ItemSelectView, PropSelectView
)
urlpatterns = [ urlpatterns = [
url(r'^$', index), url(r'^$', index),
url(r'^search$', search), url(r'^search$', search),
url(r'^item/(?P<pk>.*)$', item_display, name='item-display'), url(r'^item/(?P<pk>.*)$', item_display, name='item-display'),
url(r'^autocomplete.json$', ItemSelectView.as_view(), name='item-complete'), url(r'^autocomplete.json$', ItemSelectView.as_view(), name='item-complete'),
url(r'^autocomplete_prop.json$', PropSelectView.as_view(), name='prop-complete'),
url(r'^(?P<pk>[^/]*)$', label_lookup, name='label-lookup'), url(r'^(?P<pk>[^/]*)$', label_lookup, name='label-lookup'),
] ]

View File

@ -1,10 +1,12 @@
import shlex import shlex
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.postgres.search import SearchVector from django.contrib.postgres.search import SearchVector, TrigramSimilarity
from django.http import Http404, JsonResponse from django.http import Http404, JsonResponse
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
from django_select2.views import AutoResponseView from django_select2.views import AutoResponseView
from django.db.models import Q
from django.db import connection
from storage.models import Item, Label from storage.models import Item, Label
@ -19,30 +21,37 @@ def apply_smart_search(query, objects):
general_term.append(prop) general_term.append(prop)
else: else:
key, value = prop.split(':', 1) key, value = prop.split(':', 1)
if hasattr(Item, key): if key in ['owner', 'taken_by']:
filters[key + '__username'] = value
elif hasattr(Item, key):
filters[key + '__search'] = value filters[key + '__search'] = value
elif key == 'ancestor': elif key == 'ancestor':
objects = Item.objects.get(pk=value).get_children() objects = Item.objects.get(pk=value).get_children()
elif key == 'prop' or value: elif key == 'prop' or value:
if key == 'prop': if key == 'prop':
key, value = value.split(':', 1) key, _, value = value.partition(':')
if not value:
if 'props__contains' not in filters: filters['props__isnull'] = {key: False}
filters['props__contains'] = {} else:
filters['props__contains'] = {key: value} filters['props__contains'] = {key: value}
else: else:
# "Whatever:" # "Whatever:"
general_term.append(prop) general_term.append(prop)
if general_term: objects = objects.filter(**filters)
if not general_term:
return objects
objects = objects.annotate( objects = objects.annotate(
search=SearchVector('name', 'description', 'props', config='simple'), search=SearchVector('name', 'description', 'props', config='simple'),
) )
filters['search'] = ' '.join(general_term) general_term = ' '.join(general_term)
objects = objects.filter(**filters)
objects = objects.annotate(
similarity=TrigramSimilarity('name', general_term)
).filter(
similarity__gte=0.15
).order_by('-similarity')
return objects return objects
@ -53,14 +62,14 @@ def index(request):
def search(request): def search(request):
query = request.GET.get('q', '') query = request.GET.get('q', '')
results = apply_smart_search(query, Item.objects) results = apply_smart_search(query, Item.objects).all()
if results.count() == 1: if results and len(results) == 1 or getattr(results[0], 'similarity', 0) == 1:
return redirect(results.all()[0]) return redirect(results[0])
return render(request, 'results.html', { return render(request, 'results.html', {
'query': query, 'query': query,
'results': results.all(), 'results': results,
}) })
@ -105,3 +114,23 @@ class ItemSelectView(AutoResponseView):
], ],
'more': context['page_obj'].has_next() 'more': context['page_obj'].has_next()
}) })
class PropSelectView(AutoResponseView):
def get(self, request, *args, **kwargs):
# self.widget = self.get_widget_or_404()
self.term = kwargs.get('term', request.GET.get('term', ''))
# context = self.get_context_data()
with connection.cursor() as c:
c.execute("select e from (select skeys(props) as e, count(skeys(props)) as e_count from storage_item group by e order by e_count desc) as xD where e like %s limit 10;", ['%' + self.term + '%'])
props = c.fetchall()
return JsonResponse({
'results': [
{
'text': p,
'id': p,
}
for p in props
],
})

59
storage/widgets.py Normal file
View File

@ -0,0 +1,59 @@
from pkg_resources import parse_version
from django_select2.forms import ModelSelect2Widget, HeavySelect2Widget
from django_hstore.forms import DictionaryFieldWidget
from django import get_version
from django.urls import reverse
from django.conf import settings
from django.utils.safestring import mark_safe
from django.template import Context
from django.template.loader import get_template
from django.contrib.admin.widgets import AdminTextareaWidget
class ItemSelectWidget(ModelSelect2Widget):
search_fields = [
'name__icontains',
'description__icontains'
]
def __init__(self, *args, **kwargs):
kwargs['data_view'] = 'item-complete'
super(ItemSelectWidget, self).__init__(*args, **kwargs)
def label_from_instance(self, obj):
return obj.name
class PropsSelectWidget(DictionaryFieldWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def render(self, name, value, attrs=None):
if attrs is None:
attrs = {}
# it's called "original" because it will be replaced by a copy
attrs['class'] = 'hstore-original-textarea'
w = HeavySelect2Widget(data_view='prop-complete', attrs={'data-tags': 'true', 'class': 'hs-key'})
# get default HTML from AdminTextareaWidget
html = AdminTextareaWidget.render(self, name, value, attrs)
# prepare template context
template_context = Context({
'field_name': name,
'STATIC_URL': settings.STATIC_URL,
'use_svg': parse_version(get_version()) >= parse_version('1.9'), # use svg icons if django >= 1.9
'ajax_url': reverse('prop-complete'),
'w': w.build_attrs()
})
# get template object
template = get_template('hstore_%s_widget.html' % self.admin_style)
# render additional html
additional_html = template.render(template_context)
# append additional HTML and mark as safe
html = html + additional_html
html = mark_safe(html)
return html

View File

@ -0,0 +1,75 @@
{% load i18n %}
<script type="text/html" id="hstore-row-template-{{ field_name }}" class="hstore-row-template-inline">
<div class="form-row field-data">
<div>
<select {% for k, v in w.items %} {{k}}="{{v}}" {% endfor %}>
<option value="<%= _.escape(key) %>"><%= _.escape(key) %></option>
</select>
&nbsp; <strong>:</strong> &nbsp;&nbsp;
<input class='hs-val'
style="min-width:300px;"
value="<%= _.escape(value) %>"
type="text"
placeholder="{% trans 'value' %}">
&nbsp;&nbsp;
<a href="#" class="remove-row" title="{% trans 'remove row' %}">
<img src="{{ STATIC_URL }}admin/img/{% if use_svg %}icon-deletelink.svg{% else %}icon_deletelink.gif{% endif %}">
</a>
</div>
</div>
</script>
<script type="text/html" id="hstore-ui-template-{{ field_name }}"{% if '__prefix__' in field_name %} class="hstore-ui-template-inline"{% endif %}>
<div class="hstore" id="hstore-{{ field_name }}">
<h2><%= label %></h2>
<% if(help && help != '') { %>
<div class="form-row">
<p class="help" style="margin:0;padding:0"><%= help %></p>
</div>
<% } %>
<div class="hstore-rows">
<% if(errors){ %>
<div class="form-row field-data">
<div>
<ul class="errorlist">
<%= errors %>
</ul>
</div>
</div>
<% } %>
<% for(key in data){ %>
<%= _.template(django.jQuery('.hstore-row-template-inline').eq(0).html(), { 'key': key, 'value': data[key] }) %>
<% } %>
</div>
<div class="form-row field-data hstore-textarea" style="display:none">
<div>
<label for="<%= id %>" class="required">{% trans 'Raw textarea' %}:</label>
<textarea class="vLargeTextField" cols="40" id="<%= id %>" name="<%= name %>" rows="10"><%= value %></textarea>
</div>
</div>
<div class="form-row">
<a href="#" class="hs-add-row" title="{% trans 'Add another row' %}">
<img src="{{ STATIC_URL }}admin/img/{% if use_svg %}icon-addlink.svg{% else %}icon_addlink.gif{% endif %}" alt="{% trans 'Add Another' %}">
{% trans "Add row" %}
</a>
<a href="#" class="hstore-toggle-txtarea" title="{% trans 'toggle textarea' %}" style="float:right">
<img src="{{ STATIC_URL }}admin/img/{% if use_svg %}icon-changelink.svg{% else %}icon_changelink.gif{% endif %}" alt="{% trans 'toggle textarea' %}">
{% trans 'toggle textarea' %}
</a>
</div>
</div>
</script>
<script>
django.jQuery('a.hs-add-row').on('click', console.log);
django.jQuery(function() { initDjangoHStoreWidget('{{ field_name }}') });
// function(e) {
// $('.django-select2').select2();
// });
</script>