diff --git a/advanced_filters/admin.py b/advanced_filters/admin.py index 6789368..927e25f 100644 --- a/advanced_filters/admin.py +++ b/advanced_filters/admin.py @@ -7,8 +7,9 @@ from django.shortcuts import resolve_url from django.utils.translation import ugettext_lazy as _ -from .forms import AdvancedFilterForm +from .forms import AdvancedFilterForm, AdvancedFilterQueryForm from .models import AdvancedFilter +from .q_serializer import QSerializer logger = logging.getLogger('advanced_filters.admin') @@ -43,6 +44,31 @@ def queryset(self, request, queryset): return queryset +class AdvancedQueryFilters(admin.SimpleListFilter): + """Allow filtering by advanced filters query""" + title = ' ' + template = 'admin/query_filter.html' + + parameter_name = '_aquery' + + def lookups(self, request, model_admin): + return None + + def queryset(self, request, queryset): + if self.value(): + query_serializer = QSerializer(base64=True) + query = query_serializer.loads(self.value()) + return queryset.filter(query).distinct() + + return queryset + + def choices(self, changelist): + return [] + + def has_output(self): + return True + + class AdminAdvancedFiltersMixin(object): """ Generic AdvancedFilters mixin """ advanced_change_list_template = "admin/advanced_filters.html" @@ -56,15 +82,24 @@ def __init__(self, *args, **kwargs): self.original_change_list_template = "admin/change_list.html" self.change_list_template = self.advanced_change_list_template # add list filters to filters - self.list_filter = (AdvancedListFilters,) + tuple(self.list_filter) - - def save_advanced_filter(self, request, form): + self.list_filter = (AdvancedListFilters, AdvancedQueryFilters,) + tuple(self.list_filter) + + def save_advanced_filter(self, request, form, save_instance): + if '_just_filter' in request.POST: + search_query = form.generate_query() + query_serializer = QSerializer(base64=True) + b64_query = query_serializer.dumps(search_query) + url = "{path}?_aquery={query}"\ + .format(path=request.path, query=b64_query) + return HttpResponseRedirect(url) if form.is_valid(): afilter = form.save(commit=False) afilter.created_by = request.user afilter.query = form.generate_query() - afilter.save() - afilter.users.add(request.user) + + if save_instance: + afilter.save() + afilter.users.add(request.user) messages.add_message( request, messages.SUCCESS, _('Advanced filter added successfully.') @@ -77,9 +112,36 @@ def save_advanced_filter(self, request, form): elif request.method == "POST": logger.info('Failed saving advanced filter, params: %s', form.data) + def get_advanced_filter_data(self, request): + if request.POST.get('action') == 'advanced_filters': + return request.POST, True + + search_query = request.GET.get('_aquery') + if search_query: + query_serializer = QSerializer(base64=True) + raw_query = query_serializer.loads(search_query, raw=True) + query_list = query_serializer.get_field_values_list(raw_query) + + data = { + 'form-TOTAL_FORMS': 0, + 'form-INITIAL_FORMS': 0, + 'title': 'unsaved filter' + } + + for idx, query in enumerate(query_list): + query_dict = AdvancedFilterQueryForm._parse_query_dict(query, self.model) + + for key, value in query_dict.items(): + data['form-{idx}-{key}'.format(idx=idx, key=key)] = value + + data['form-TOTAL_FORMS'] += 1 + + return data, False + + return None, False + def adv_filters_handle(self, request, extra_context={}): - data = request.POST if request.POST.get( - 'action') == 'advanced_filters' else None + data, save_instance = self.get_advanced_filter_data(request) adv_filters_form = self.advanced_filter_form( data=data, model_admin=self, extra_form=True) extra_context.update({ @@ -88,7 +150,7 @@ def adv_filters_handle(self, request, extra_context={}): 'current_afilter': request.GET.get('_afilter'), 'app_label': self.opts.app_label, }) - return self.save_advanced_filter(request, adv_filters_form) + return self.save_advanced_filter(request, adv_filters_form, save_instance) def changelist_view(self, request, extra_context=None): """Add advanced_filters form to changelist context""" diff --git a/advanced_filters/forms.py b/advanced_filters/forms.py index 32eec22..a67ce8b 100644 --- a/advanced_filters/forms.py +++ b/advanced_filters/forms.py @@ -67,7 +67,7 @@ class AdvancedFilterQueryForm(CleanWhiteSpacesMixin, forms.Form): label=_('Operator'), required=True, choices=OPERATORS, initial="iexact", widget=forms.Select(attrs={'class': 'query-operator'})) - value = VaryingTypeCharField(required=True, widget=forms.TextInput( + value = VaryingTypeCharField(required=False, widget=forms.TextInput( attrs={'class': 'query-value'}), label=_('Value')) value_from = forms.DateTimeField(widget=forms.HiddenInput( attrs={'class': 'query-dt-from'}), required=False) @@ -135,12 +135,8 @@ def _parse_query_dict(query_data, model): elif query_data['value'] is False: query_data['operator'] = "isfalse" else: - if isinstance(mfield, DateField): - # this is a date/datetime field - query_data['operator'] = "range" # default - else: + if not query_data.get('operator') == 'range': query_data['operator'] = operator # default - if isinstance(query_data.get('value'), list) and query_data['operator'] == 'range': date_from = date_to_string(query_data.get('value_from')) @@ -169,6 +165,15 @@ def clean(self): self.set_range_value(cleaned_data) return cleaned_data + def clean_value(self): + value = self.cleaned_data['value'] + op = self.cleaned_data.get('operator', '') + list = ['istrue', 'isfalse', 'isnull'] + if op not in list: + self.fields['value'].required = True + return self.fields['value'].clean(value) + return value + def make_query(self, *args, **kwargs): """ Returns a Q object from the submitted form """ query = Q() # initial is an empty query diff --git a/advanced_filters/static/advanced-filters/advanced-filters.js b/advanced_filters/static/advanced-filters/advanced-filters.js index 3e3865a..3c1edfb 100644 --- a/advanced_filters/static/advanced-filters/advanced-filters.js +++ b/advanced_filters/static/advanced-filters/advanced-filters.js @@ -34,10 +34,10 @@ var OperatorHandlers = function($) { } self.val_input.css({display: 'none'}); - $(".hasDatepicker").datepicker("destroy"); + try {$(".hasDatepicker").datepicker("destroy");} catch(e) {} $from.addClass('vDateField'); $to.addClass('vDateField'); - grappelli.initDateAndTimePicker(); + try {grappelli.initDateAndTimePicker();} catch(e) {} }; self.remove_datepickers = function() { @@ -45,7 +45,7 @@ var OperatorHandlers = function($) { if (self.val_input.parent().find('input.vDateField').length > 0) { var datefields = self.val_input.parent().find('input.vDateField'); datefields.each(function() { - $(this).datepicker("destroy"); + try {$(this).datepicker("destroy");} catch(e) {} }); datefields.remove(); } @@ -53,28 +53,50 @@ var OperatorHandlers = function($) { self.modify_widget = function(elm) { // pick a widget for the value field according to operator + list = ['istrue', 'isfalse', 'isnull']; self.value = $(elm).val(); self.val_input = $(elm).parents('tr').find('.query-value'); console.log("selected operator: " + self.value); + var field = $(elm).parents('tr').find('.query-field'); + self.initialize_select2(field); + if (self.value == "range") { self.add_datepickers(); } else { self.remove_datepickers(); } + + var input = $(elm).parents('tr').find('input.query-value'); + if (list.includes(self.value)) { + input.prop('readonly', true); + } else { + input.prop('readonly', false); + } }; self.initialize_select2 = function(elm) { // initialize select2 widget and populate field choices var field = $(elm).val(); - var choices_url = ADVANCED_FILTER_CHOICES_LOOKUP_URL + (FORM_MODEL || - MODEL_LABEL) + '/' + field; - var input = $(elm).parents('tr').find('input.query-value'); - input.select2("destroy"); - $.get(choices_url, function(data) { - input.select2({'data': data, 'createSearchChoice': function(term) { - return { 'id': term, 'text': term }; - }}); - }); + var op = $(elm).parents('tr').find('.query-operator'); + if (field.includes('__') && op.val() == 'iexact') { + var choices_url = ADVANCED_FILTER_CHOICES_LOOKUP_URL + (FORM_MODEL || + MODEL_LABEL) + '/' + field; + var input = $(elm).parents('tr').find('input.query-value'); + var value = input.val(); + input.select2("destroy"); + $.get(choices_url, function(data) { + if (value) { + data.results.push({'id': value, 'text': value}) + } + input.select2({'data': data, 'createSearchChoice': function(term) { + return { 'id': term, 'text': term }; + }}); + }); + } + else { + var input = $(elm).parents('tr').find('input.query-value'); + input.select2("destroy"); + } }; self.field_selected = function(elm) { @@ -108,15 +130,16 @@ var OperatorHandlers = function($) { // if only 1 form and it's empty, add first extra formset $('[data-rules-formset] .add-row a').click(); } + $('.form-row select.query-operator').each(function() { $(this).off("change"); - $(this).data('pre_change', $(this).val()); + // $(this).data('pre_change', $(this).val()); $(this).on("change", function() { var before_change = $(this).data('pre_change'); if ($(this).val() != before_change) self.modify_widget(this); $(this).data('pre_change', $(this).val()); }).change(); - self.modify_widget(this); + // self.modify_widget(this); }); $('.form-row select.query-field').each(function() { $(this).off("change"); @@ -126,19 +149,19 @@ var OperatorHandlers = function($) { if ($(this).val() != before_change) self.field_selected(this); $(this).data('pre_change', $(this).val()); }).change(); + self.initialize_select2(this) }); - self.field_selected($('.form-row select.query-field').first()); - + // self.field_selected($('.form-row select.query-field').first()); }; self.destroy = function() { - $('.form-row select.query-operator').each(function() { + $('.form-row select.query-operator:last').each(function() { $(this).off("change"); }); - $('.form-row select.query-field').each(function() { + $('.form-row select.query-field:last').each(function() { $(this).off("change"); }); - $('.form-row input.query-value').each(function() { + $('.form-row input.query-value:last').each(function() { $(this).select2("destroy"); }); }; diff --git a/advanced_filters/templates/admin/advanced_filters.html b/advanced_filters/templates/admin/advanced_filters.html index f1f5487..d7780dd 100644 --- a/advanced_filters/templates/admin/advanced_filters.html +++ b/advanced_filters/templates/admin/advanced_filters.html @@ -63,6 +63,7 @@

{% trans "Create advanced filter" %}:


+ {% trans "Cancel" %} diff --git a/advanced_filters/templates/admin/advanced_filters/change_form.html b/advanced_filters/templates/admin/advanced_filters/change_form.html index 5504945..63a6eab 100644 --- a/advanced_filters/templates/admin/advanced_filters/change_form.html +++ b/advanced_filters/templates/admin/advanced_filters/change_form.html @@ -11,7 +11,6 @@
{% csrf_token %}{% block form_top %}{% endblock %} {% with adminform.form.fields_formset as formset %}
-

{% trans "Change advanced filter" %}:

{% csrf_token %} {{ formset.management_form }} diff --git a/advanced_filters/templates/admin/common_js_init.html b/advanced_filters/templates/admin/common_js_init.html index 509cd5e..049d140 100644 --- a/advanced_filters/templates/admin/common_js_init.html +++ b/advanced_filters/templates/admin/common_js_init.html @@ -24,4 +24,4 @@ } }); })(django.jQuery); - \ No newline at end of file + diff --git a/advanced_filters/templates/admin/query_filter.html b/advanced_filters/templates/admin/query_filter.html new file mode 100644 index 0000000..9299a53 --- /dev/null +++ b/advanced_filters/templates/admin/query_filter.html @@ -0,0 +1 @@ +{# for now - make filter hidden in sidebar #} diff --git a/advanced_filters/tests/test_creation.py b/advanced_filters/tests/test_creation.py index de3cab0..098591b 100644 --- a/advanced_filters/tests/test_creation.py +++ b/advanced_filters/tests/test_creation.py @@ -86,3 +86,13 @@ def test_create_form_valid(user, client, good_data, query): assert url.endswith("%s?_afilter=%s" % (URL_CLIENT_CHANGELIST, created_filter.pk)) assert list(created_filter.query.children[0]) == query + + # just filter + form_data.pop('_save_goto') + form_data['_just_filter'] = 1 + res = client.post(URL_CLIENT_CHANGELIST, data=form_data) + + assert res.status_code == 302 + assert AdvancedFilter.objects.count() == 2 + url = res['location'] + assert '?_aquery=' in url