initial commit

django-5.0
Thomas Leichtfuß 2020-09-04 18:40:00 +02:00
commit 94e57d4e8c
9 changed files with 585 additions and 0 deletions

10
.gitignore vendored 100644
View File

@ -0,0 +1,10 @@
__pycache__/
.python-version
.coverage
.coveralls.yml
db.sqlite3*
/django_admin_more_filters.egg-info/
/build/
/dist/
/.tox/
/.vscode/

27
LICENCE 100644
View File

@ -0,0 +1,27 @@
Copyright (c) 2020, Thomas Leichtfuß.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the author nor the names of contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3
MANIFEST.in 100644
View File

@ -0,0 +1,3 @@
include LICENSE
include README.rst
recursive-include more_filters/templates *

View File

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class DjangoAdminSelectfilterConfig(AppConfig):
name = 'django_admin_more_filters'

View File

@ -0,0 +1,423 @@
# -*- coding: utf-8 -*-
from django.contrib.admin.utils import prepare_lookup_value
from django.contrib import admin
from django.db.models import Q
from django.utils.encoding import force_text
from django.utils.translation import gettext_lazy as _
from django.contrib.admin.utils import reverse_field_path
from django.contrib.admin.utils import get_model_from_relation
from django.core.exceptions import ValidationError
from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.admin.filters import AllValuesFieldListFilter
from django.contrib.admin.filters import ChoicesFieldListFilter
from django.contrib.admin.filters import RelatedFieldListFilter
from django.contrib.admin.filters import RelatedOnlyFieldListFilter
class SelectFilter(admin.SimpleListFilter):
title = _('Selection')
parameter_name = 'selected'
parameter_inverse = 'inverse'
template = 'selectfilter.html'
def __init__(self, request, params, model, model_admin):
super(SelectFilter, self).__init__(request, params, model, model_admin)
self.inverse = eval(params.pop(self.parameter_inverse, 'False'))
def has_output(self):
return True
def lookups(self, request, model_admin):
return ()
def queryset(self, request, queryset):
if not self.value(): return
if self.inverse:
return queryset.exclude(id__in=self.value().split(','))
else:
return queryset.filter(id__in=self.value().split(','))
def choices(self, changelist):
exclude = [self.parameter_name, self.parameter_inverse]
yield {
'selected': self.value() is None,
'query_string': changelist.get_query_string({}, exclude),
'display': _('All'),
}
yield {
'selected': bool(self.value()),
'query_string': changelist.get_query_string({}, exclude),
'display': _('Select'),
'id': 'selectfilter',
}
if self.value() and self.inverse:
yield {
'selected': False,
'query_string': changelist.get_query_string({}, []),
'display': _('* Remove'),
'id': 'selectfilter_add'
}
exclude = [self.parameter_inverse]
yield {
'selected': False,
'query_string': changelist.get_query_string({}, exclude),
'display': _('* Undo inversion'),
}
elif self.value() and not self.inverse:
yield {
'selected': False,
'query_string': changelist.get_query_string({}, []),
'display': _('* Remove'),
'id': 'selectfilter_remove'
}
include = {self.parameter_inverse: True}
yield {
'selected': False,
'query_string': changelist.get_query_string(include, []),
'display': _('* Invert'),
}
class MultiSelectMixin(object):
def queryset(self, request, queryset):
params = Q()
for lookup_arg, value in self.used_parameters.items():
params |= Q(**{lookup_arg:value})
try:
return queryset.filter(params)
except (ValueError, ValidationError) as e:
# Fields may raise a ValueError or ValidationError when converting
# the parameters to the correct type.
raise IncorrectLookupParameters(e)
def querystring_for_choices(self, val, changelist):
lookup_vals = self.lookup_vals[:]
if val in self.lookup_vals:
lookup_vals.remove(val)
else:
lookup_vals.append(val)
if lookup_vals:
query_string = changelist.get_query_string({
self.lookup_kwarg: ','.join(lookup_vals),
}, [])
else:
query_string = changelist.get_query_string({},
[self.lookup_kwarg])
return query_string
def querystring_for_isnull(self, changelist):
if self.lookup_val_isnull:
query_string = changelist.get_query_string({},
[self.lookup_kwarg_isnull])
else:
query_string = changelist.get_query_string({
self.lookup_kwarg_isnull: 'True',
}, [])
return query_string
def has_output(self):
return len(self.lookup_choices) > 1
class MultiSelectFilter(MultiSelectMixin, admin.AllValuesFieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
self.lookup_kwarg = '%s__in' % field_path
self.lookup_kwarg_isnull = '%s__isnull' % field_path
lookup_vals = request.GET.get(self.lookup_kwarg)
self.lookup_vals = lookup_vals.split(',') if lookup_vals else list()
self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull)
self.empty_value_display = model_admin.get_empty_value_display()
parent_model, reverse_path = reverse_field_path(model, field_path)
# Obey parent ModelAdmin queryset when deciding which options to show
if model == parent_model:
queryset = model_admin.get_queryset(request)
else:
queryset = parent_model._default_manager.all()
self.lookup_choices = (queryset
.distinct()
.order_by(field.name)
.values_list(field.name, flat=True))
super(admin.AllValuesFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path)
self.used_parameters = self.prepare_used_parameters(self.used_parameters)
def prepare_querystring_value(self, value):
# mask all commas or these values will be used
# in a comma-seperated-list as get-parameter
return str(value).replace(',', '%~')
def prepare_used_parameters(self, used_parameters):
# remove comma-mask from list-values for __in-lookups
for key, value in used_parameters.items():
if not key.endswith('__in'): continue
used_parameters[key] = [v.replace('%~', ',') for v in value]
return used_parameters
def choices(self, changelist):
yield {
'selected': not self.lookup_vals and self.lookup_val_isnull is None,
'query_string': changelist.get_query_string({}, [self.lookup_kwarg, self.lookup_kwarg_isnull]),
'display': _('All'),
}
include_none = False
for val in self.lookup_choices:
if val is None:
include_none = True
continue
val = str(val)
qval = self.prepare_querystring_value(val)
yield {
'selected': qval in self.lookup_vals,
'query_string': self.querystring_for_choices(qval, changelist),
'display': val,
}
if include_none:
yield {
'selected': bool(self.lookup_val_isnull),
'query_string': self.querystring_for_isnull(changelist),
'display': self.empty_value_display,
}
class MultiSelectRelatedFilter(MultiSelectMixin, admin.RelatedFieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
other_model = get_model_from_relation(field)
self.lookup_kwarg = '%s__%s__in' % (field_path, field.target_field.name)
self.lookup_kwarg_isnull = '%s__isnull' % field_path
lookup_vals = request.GET.get(self.lookup_kwarg)
self.lookup_vals = lookup_vals.split(',') if lookup_vals else list()
self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull)
super(admin.RelatedFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path)
self.lookup_choices = self.field_choices(field, request, model_admin)
if hasattr(field, 'verbose_name'):
self.lookup_title = field.verbose_name
else:
self.lookup_title = other_model._meta.verbose_name
self.title = self.lookup_title
self.empty_value_display = model_admin.get_empty_value_display()
def choices(self, changelist):
yield {
'selected': not self.lookup_vals and not self.lookup_val_isnull,
'query_string': changelist.get_query_string(
{},
[self.lookup_kwarg, self.lookup_kwarg_isnull]
),
'display': _('All'),
}
for pk_val, val in self.lookup_choices:
pk_val = str(pk_val)
yield {
'selected': pk_val in self.lookup_vals,
'query_string': self.querystring_for_choices(pk_val, changelist),
'display': val,
}
if self.include_empty_choice:
yield {
'selected': bool(self.lookup_val_isnull),
'query_string': self.querystring_for_isnull(changelist),
'display': self.empty_value_display,
}
class MultiSelectDropdownFilter(MultiSelectFilter):
template = 'dropdownmultiselectfilter.html'
def choices(self, changelist):
query_string = changelist.get_query_string({}, [self.lookup_kwarg, self.lookup_kwarg_isnull])
yield {
'selected': not self.lookup_vals and self.lookup_val_isnull is None,
'query_string': query_string,
'display': _('All'),
}
include_none = False
for val in self.lookup_choices:
if val is None:
include_none = True
continue
val = str(val)
qval = self.prepare_querystring_value(val)
yield {
'selected': qval in self.lookup_vals,
'query_string': query_string,
'display': val,
'value': val,
'key': self.lookup_kwarg,
}
if include_none:
yield {
'selected': bool(self.lookup_val_isnull),
'query_string': query_string,
'display': self.empty_value_display,
'value': 'True',
'key': self.lookup_kwarg_isnull,
}
class MultiSelectRelatedDropdownFilter(MultiSelectRelatedFilter):
template = 'dropdownmultiselectfilter.html'
def choices(self, changelist):
query_string = changelist.get_query_string({}, [self.lookup_kwarg, self.lookup_kwarg_isnull])
yield {
'selected': not self.lookup_vals and not self.lookup_val_isnull,
'query_string': query_string,
'display': _('All'),
}
for pk_val, val in self.lookup_choices:
pk_val = str(pk_val)
yield {
'selected': pk_val in self.lookup_vals,
'query_string': query_string,
'display': val,
'value': pk_val,
'key': self.lookup_kwarg,
}
if self.include_empty_choice:
yield {
'selected': bool(self.lookup_val_isnull),
'query_string': query_string,
'display': self.empty_value_display,
'value': 'True',
'key': self.lookup_kwarg_isnull,
}
class DropdownFilter(AllValuesFieldListFilter):
template = 'dropdownfilter.html'
class ChoicesDropdownFilter(ChoicesFieldListFilter):
template = 'dropdownfilter.html'
class RelatedDropdownFilter(RelatedFieldListFilter):
template = 'dropdownfilter.html'
class RelatedOnlyDropdownFilter(RelatedOnlyFieldListFilter):
template = 'dropdownfilter.html'
class PlusMinusFilter(admin.AllValuesFieldListFilter):
"""
PlusMinusFilter
"""
template = 'plusminusfilter.html'
def queryset(self, request, queryset):
if self.lookup_val is None: return queryset
sign = self.lookup_val[0]
value = self.lookup_val[1:]
if sign == '-':
return queryset.exclude(**{self.field_path:value})
elif sign == '+':
return queryset.filter(**{self.field_path:value})
def choices(self, changelist):
yield {
'selected': self.lookup_val is None,
'query_string': changelist.get_query_string({}, [self.lookup_kwarg]),
'display': _('All'),
}
for lookup in self.lookup_choices:
plus_lookup = '+' + lookup
minus_lookup = '-' + lookup
yield {
'display': lookup,
'selected': self.lookup_val and self.lookup_val[1:] == force_text(lookup),
'plus': {
'selected': self.lookup_val == force_text(plus_lookup),
'query_string': changelist.get_query_string({self.lookup_kwarg: plus_lookup}, []),
},
'minus': {
'selected': self.lookup_val == force_text(minus_lookup),
'query_string': changelist.get_query_string({self.lookup_kwarg: minus_lookup}, []),
}
}
class AnnotationListFilter(admin.ListFilter):
"""
Baseclass for annotation-list-filters.
This is more or less a rewrite of admin.FieldListFilter. But we must not
subclass it to not confuse django's filter-setup-routine.
"""
attribute_name = None
nullable_attribute = None
@classmethod
def init(cls, attribute_name, nullable=True):
"""
Since filters are listed as classes in ModelAdmin.list_filter we are
not able to initialize the filter within the ModelAdmin.
We use this classmethod to setup a filter-class for a specific annotated
attribute::
MyModelAdmin(admin.ModelAdmin):
list_filter = [
MyAnnotationListFilter.init('my_attribute'),
]
"""
attrs = dict(attribute_name=attribute_name, nullable=nullable)
cls = type('cls.__name__' + attribute_name, (cls,), attrs)
return cls
def __init__(self, attribute_name, request, params, model, model_admin):
self.title = attribute_name
super().__init__(request, params, model, model_admin)
for p in self.expected_parameters():
if p in params:
value = params.pop(p)
self.used_parameters[p] = prepare_lookup_value(p, value)
def has_output(self):
return True
def queryset(self, request, queryset):
try:
return queryset.filter(**self.used_parameters)
except (ValueError, ValidationError) as e:
# Fields may raise a ValueError or ValidationError when converting
# the parameters to the correct type.
raise IncorrectLookupParameters(e)
class BooleanAnnotationListFilter(AnnotationListFilter):
"""
Filter for annotated boolean-attributes.
This is more or less the same than admin.BooleanFieldListFilter but for
annotated attributes.
"""
def __init__(self, request, params, model, model_admin):
self.lookup_kwarg = '%s__exact' % self.attribute_name
self.lookup_kwarg2 = '%s__isnull' % self.attribute_name
self.lookup_val = params.get(self.lookup_kwarg)
self.lookup_val2 = params.get(self.lookup_kwarg2)
super().__init__(self.attribute_name, request, params, model, model_admin)
if (self.used_parameters and self.lookup_kwarg in self.used_parameters and
self.used_parameters[self.lookup_kwarg] in ('1', '0')):
self.used_parameters[self.lookup_kwarg] = bool(int(self.used_parameters[self.lookup_kwarg]))
def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg2]
def choices(self, changelist):
for lookup, title in (
(None, _('All')),
('1', _('Yes')),
('0', _('No'))):
yield {
'selected': self.lookup_val == lookup and not self.lookup_val2,
'query_string': changelist.get_query_string({self.lookup_kwarg: lookup}, [self.lookup_kwarg2]),
'display': title,
}
if self.nullable_attribute:
yield {
'selected': self.lookup_val2 == 'True',
'query_string': changelist.get_query_string({self.lookup_kwarg2: 'True'}, [self.lookup_kwarg]),
'display': _('Unknown'),
}

View File

@ -0,0 +1,26 @@
{% load i18n admin_urls %}
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<ul>
{% if choices|slice:"4:" %}
<li><select id="{{ title|slugify }}_filter_select" style="width:100%;color:#666">
{% for choice in choices %}
<option{% if choice.selected %} selected="selected"{% endif %}
value="{{ choice.query_string|iriencode }}">{{ choice.display }}</option>
{% endfor %}
</select></li>
{% else %}
{% for choice in choices %}
<li{% if choice.selected %} class="selected"{% endif %}>
<a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a></li>
{% endfor %}
{% endif %}
</ul>
<script>
(function($) {
$('select#{{ title|slugify }}_filter_select').change(function(evt) {
window.location.href = $(this).val();
});
})(django.jQuery);
</script>

View File

@ -0,0 +1,55 @@
{% load i18n admin_urls %}
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<ul>
{% for choice in choices|slice:":1" %}
<li{% if choice.selected %} class="selected"{% endif %}>
<a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a></li>
</li>
{% endfor %}
<li>
<select id="{{ title|slugify }}_select" multiple style="color:#999;width:100%">
{% for choice in choices|slice:"1:" %}
<option{% if choice.selected %} selected{% endif %}
value="{{ choice.value }}"
data-key="{{ choice.key }}"
data-query="{{ choice.query_string|iriencode }}">
{{ choice.display }}
</option>
{% endfor %}
</select>
</li>
<li>
<a id="{{ title|slugify }}_submit" href="" title="filter" class="button"
style="background-color:buttonface;color:#666">filter</a>
</li>
</ul>
<script>
(function($) {
$('a#{{ title|slugify }}_submit').click(function(evt) {
// fetch data from selected options
var params = {};
var query = null;
$('select#{{ title|slugify }}_select > option:selected').each(function() {
var key = $(this).attr('data-key');
if (! params.hasOwnProperty(key)) params[key] = [];
params[key].push($(this).val());
if (!query) query = $(this).attr('data-query');
});
// do we had selected options at all?
if (!query) return false;
// build up the href
var params_query = '';
for (var key in params) {
if (params.hasOwnProperty(key)) {
params_query += '&' + key + '=' + params[key].join()
}
}
this.href = query + params_query;
});
})(django.jQuery);
</script>

View File

@ -0,0 +1,36 @@
{% load i18n %}
<h3>{% blocktrans with filter_title=title %}{{ filter_title }} {% endblocktrans %}</h3>
<ul>
{% for choice in choices %}
<li{% if choice.selected %} class="selected"{% endif %}>
<a href="{{ choice.query_string|iriencode }}"{% if choice.id %} id="{{ choice.id }}"{% endif %} title="{{ choice.display }}">{{ choice.display }}</a></li>
{% endfor %}
</ul>
<script>
(function($) {
$('a#selectfilter').click(function(evt) {
var l = [];
$('input.action-select:checked').each(function(){l.push(this.value)});
if (l.length == 0) return false;
var conj = (this.href.slice(-1) == '?') ? '' : '&';
this.href += conj + 'selected=' + l.join()
});
$('a#selectfilter_remove').click(function(evt) {
var l = [];
var a = this;
$('input.action-select:checked').each(function(){l.push(this.value)});
if (l.length == 0) return false;
$.each(l, function() {
var regex = new RegExp('(%2C'+this+'$|'+this+'%2C)');
a.href = a.href.replace(regex, '');
})
});
$('a#selectfilter_add').click(function(evt) {
var l = [];
var a = this;
$('input.action-select:checked').each(function(){l.push(this.value)});
if (l.length == 0) return false;
this.href = this.href.replace('selected=', 'selected=' + l.join() + ',');
});
})(django.jQuery);
</script>