Skip to content

Commit

Permalink
- Minimal multiselect field filter
Browse files Browse the repository at this point in the history
  • Loading branch information
elipe17 committed Aug 21, 2024
1 parent 8bf260b commit 4bdc0ee
Show file tree
Hide file tree
Showing 2 changed files with 12 additions and 187 deletions.
7 changes: 4 additions & 3 deletions django508/tests/admin/dummies.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.contrib import admin
from .multi_select_dropdown import MultiSelectDropdownFilter
from .multi_select_dropdown import FieldListMultiSelectFilter


class ReadOnlyDummyMixin(admin.ModelAdmin):
Expand Down Expand Up @@ -36,7 +36,8 @@ class Dummy_Admin(ReadOnlyDummyMixin):
]

list_filter = [
('name', MultiSelectDropdownFilter),
('name', FieldListMultiSelectFilter),
('description', FieldListMultiSelectFilter),
"created"
]

Expand All @@ -52,7 +53,7 @@ class Dummy2_Admin(ReadOnlyDummyMixin):
]

list_filter = [
('name', MultiSelectDropdownFilter),
('name', FieldListMultiSelectFilter),
"created"
]

192 changes: 8 additions & 184 deletions django508/tests/admin/multi_select_dropdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,6 @@
from django.core.exceptions import ValidationError
from django.contrib.admin.options import IncorrectLookupParameters


def flatten_used_parameters(used_parameters: dict, keep_list: bool = True):
"""Flatten length 1 lists in dictionary."""
# FieldListFilter.__init__ calls prepare_lookup_value,
# which returns a list if lookup_kwarg ends with "__in"
for k, v in used_parameters.items():
if len(v) == 1 and (isinstance(v[0], list) or not keep_list):
used_parameters[k] = v[0]

class MultiSelectMixin(object):
"""Mixin for multi-select filters."""

Expand All @@ -31,50 +22,17 @@ def queryset(self, request, queryset):
# the parameters to the correct type.
raise IncorrectLookupParameters(e)

def querystring_for_choices(self, val, changelist):
"""Build query string based on new val."""
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):
"""Build query string based on a null val."""
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 if there is output."""
return len(self.lookup_choices) > 1

def get_facet_counts(self, pk_attname, filtered_qs):
"""Return count of __in facets."""
if not self.lookup_kwarg.endswith("__in"):
raise NotImplementedError("Facets are only supported for default lookup_kwarg values, ending with '__in' "
"(got '%s')" % self.lookup_kwarg)
def prepare_querystring_value(self, value):
"""Preparse the query string value."""
# mask all commas or these values will be used
# in a comma-seperated-list as get-parameter
return str(value).replace(',', '%~')

orig_lookup_kwarg = self.lookup_kwarg
self.lookup_kwarg = self.lookup_kwarg.removesuffix("in") + "exact"
counts = super().get_facet_counts(pk_attname, filtered_qs)
self.lookup_kwarg = orig_lookup_kwarg
return counts

class FieldListMultiSelectFilter(MultiSelectMixin, admin.AllValuesFieldListFilter):
"""Multi select dropdown filter for all kind of fields."""

class MultiSelectFilter(MultiSelectMixin, admin.AllValuesFieldListFilter):
"""Multi select filter for all kind of fields."""
template = 'multiselectdropdownfilter.html'

def __init__(self, field, request, params, model, model_admin, field_path):
self.lookup_kwarg = '%s__in' % field_path
Expand All @@ -94,62 +52,6 @@ def __init__(self, field, request, params, model, model_admin, field_path):
.order_by(field.name)
.values_list(field.name, flat=True))
super(admin.AllValuesFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path)
flatten_used_parameters(self.used_parameters)
self.used_parameters = self.prepare_used_parameters(self.used_parameters)

def prepare_querystring_value(self, value):
"""Preparse the query string 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):
"""Prepare 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):
"""Generate choices."""
add_facets = getattr(changelist, "add_facets", False)
facet_counts = self.get_facet_queryset(changelist) if add_facets else None
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
count = None
empty_title = self.empty_value_display
for i, val in enumerate(self.lookup_choices):
if add_facets:
count = facet_counts[f"{i}__c"]
if val is None:
include_none = True
empty_title = f"{empty_title} ({count})" if add_facets else empty_title
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": f"{val} ({count})" if add_facets else val,
}
if include_none:
yield {
'selected': bool(self.lookup_val_isnull),
'query_string': self.querystring_for_isnull(changelist),
'display': empty_title,
}


class MultiSelectDropdownFilter(MultiSelectFilter):
"""Multi select dropdown filter for all kind of fields."""

template = 'multiselectdropdownfilter.html'

def choices(self, changelist):
"""Generate choices."""
Expand Down Expand Up @@ -189,81 +91,3 @@ def choices(self, changelist):
'value': 'True',
'key': self.lookup_kwarg_isnull,
}







class NewMultiSelectFilter(admin.AllValuesFieldListFilter):
"""Multi select filter for all kind of fields."""

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)
flatten_used_parameters(self.used_parameters)
self.used_parameters = self.prepare_used_parameters(self.used_parameters)

def prepare_querystring_value(self, value):
"""Preparse the query string 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):
"""Prepare 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):
"""Generate choices."""
add_facets = getattr(changelist, "add_facets", False)
facet_counts = self.get_facet_queryset(changelist) if add_facets else None
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
count = None
empty_title = self.empty_value_display
for i, val in enumerate(self.lookup_choices):
if add_facets:
count = facet_counts[f"{i}__c"]
if val is None:
include_none = True
empty_title = f"{empty_title} ({count})" if add_facets else empty_title
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": f"{val} ({count})" if add_facets else val,
}
if include_none:
yield {
'selected': bool(self.lookup_val_isnull),
'query_string': self.querystring_for_isnull(changelist),
'display': empty_title,
}

0 comments on commit 4bdc0ee

Please sign in to comment.