Skip to content

Commit

Permalink
feat: filters
Browse files Browse the repository at this point in the history
  • Loading branch information
Lukas Vinclav committed Oct 21, 2022
1 parent fd58f9b commit 3e7257b
Show file tree
Hide file tree
Showing 19 changed files with 161 additions and 108 deletions.
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Unfold is a new theme for Django Admin incorporating some most common practises
- [Decorators](#decorators)
- [@display](#display)
- [Actions](#actions)
- [Filters](#filters)
- [User Admin Form](#user-admin-form)
- [Adding Custom Styles and Scripts](#adding-custom-styles-and-scripts)
- [Project Level Tailwind Stylesheet](#project-level-tailwind-stylesheet)
Expand All @@ -40,7 +41,7 @@ The installation process is minimal. Everything what is needed after installatio

INSTALLED_APPS = [
"unfold", # before django.contrib.admin
"unfold.contrib.numeric_filters", # optional
"unfold.contrib.filters", # optional
"django.contrib.admin", # required
]
```
Expand Down Expand Up @@ -291,9 +292,45 @@ class UserAdmin(ModelAdmin):
pass
```

## Filters

By default, Django admin handles all filters as regular HTML links pointing at the same URL with different query parameters. This approach is for basic filtering more than enough. In the case of more advanced filtering by incorporating input fields, it is not going to work.

Currently, Unfold implements numeric filters inside `unfold.contrib.filters` application. In order to use these filters, it is required to add this application into `INSTALLED_APPS` in `settings.py` right after `unfold` application.

```python
# admin.py

from django.contrib import admin
from unfold.contrib.admin import (
NumericFilterModelAdmin,
RangeNumericFilter,
SingleNumericFilter,
SliderNumericFilter,
)

from .models import YourModel


class CustomSliderNumericFilter(SliderNumericFilter):
MAX_DECIMALS = 2
STEP = 10


@admin.register(YourModel)
class YourModelAdmin(NumericFilterModelAdmin):
list_filter = (
("field_A", SingleNumericFilter), # Single field search, __gte lookup
("field_B", RangeNumericFilter), # Range search, __gte and __lte lookup
("field_C", SliderNumericFilter), # Same as range above but with slider
("field_D", CustomSliderNumericFilter), # Filter with custom attributes
)
list_filter_submit = True # Display submit button
```

## User Admin Form

User's admin in Django is little bit specific as it contains several forms which are requiring custom styling. All of these forms has been inherited and accordingly adjusted. In user admin class it is needed to use these inherited form classes to enable custom styling matching rest of the website.
User's admin in Django is specific as it contains several forms which are requiring custom styling. All of these forms has been inherited and accordingly adjusted. In user admin class it is needed to use these inherited form classes to enable custom styling matching rest of the website.

```python
# models.py
Expand Down
1 change: 1 addition & 0 deletions src/unfold/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
actions_submit_line = ()
custom_urls = ()
add_fieldsets = ()
list_filter_submit = False

def __init__(self, model, admin_site):
super().__init__(model, admin_site)
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,34 @@

class NumericFilterModelAdmin(admin.ModelAdmin):
class Media:
css = {"all": ("js/nouislider.min.css",)}
css = {"all": ("css/nouislider.min.css",)}
js = (
"js/wNumb.min.js",
"js/nouislider.min.js",
"js/admin-numeric-filter.js",
)


class SingleNumericMixin:
class SingleNumericFilter(admin.FieldListFilter):
request = None
parameter_name = None
template = "admin/filter_numeric_single.html"
template = "unfold/filters/filters_numeric_single.html"

def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(field, request, params, model, model_admin, field_path)

if not isinstance(field, (DecimalField, IntegerField, FloatField, AutoField)):
raise TypeError(
"Class {} is not supported for {}.".format(
type(self.field), self.__class__.__name__
)
)

self.request = request

if self.parameter_name is None:
self.parameter_name = self.field_path

def prefill_used_parameters(self, params):
if self.parameter_name in params:
value = params.pop(self.parameter_name)
self.used_parameters[self.parameter_name] = value
Expand All @@ -47,48 +61,31 @@ def choices(self, changelist):
)


class SingleNumericFilter(SingleNumericMixin, admin.SimpleListFilter):
def __init__(self, request, params, model, model_admin):
super().__init__(request, params, model, model_admin)

self.request = request
if not self.parameter_name:
raise ValueError("Parameter name cannot be None")

self.prefill_used_parameters(params)

def lookups(self, request, model_admin):
return (("dummy", "dummy"),)

class RangeNumericFilter(admin.FieldListFilter):
request = None
parameter_name = None
template = "unfold/filters/filters_numeric_range.html"

class SingleFieldNumericFilter(SingleNumericMixin, admin.FieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(field, request, params, model, model_admin, field_path)
if not isinstance(field, (DecimalField, IntegerField, FloatField, AutoField)):
raise TypeError(
f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
"Class {} is not supported for {}.".format(
type(self.field), self.__class__.__name__
)
)

self.request = request
if self.parameter_name is None:
self.parameter_name = self.field_path

self.prefill_used_parameters(params)


class RangeNumericMixin:
request = None
parameter_name = None
template = "admin/filter_numeric_range.html"

def init_used_parameters(self, params):
if self.parameter_name + "_from" in params:
value = params.pop(self.parameter_name + "_from")
self.used_parameters[self.parameter_name + "_from"] = value
value = params.pop(self.field_path + "_from")
self.used_parameters[self.field_path + "_from"] = value

if self.parameter_name + "_to" in params:
value = params.pop(self.parameter_name + "_to")
self.used_parameters[self.parameter_name + "_to"] = value
value = params.pop(self.field_path + "_to")
self.used_parameters[self.field_path + "_to"] = value

def queryset(self, request, queryset):
filters = {}
Expand Down Expand Up @@ -145,39 +142,11 @@ def choices(self, changelist):
)


class RangeNumericFilter(RangeNumericMixin, admin.SimpleListFilter):
def __init__(self, request, params, model, model_admin):
super().__init__(request, params, model, model_admin)
if not self.parameter_name:
raise ValueError("Parameter name cannot be None")

self.request = request
self.init_used_parameters(params)

def lookups(self, request, model_admin):
return (("dummy", "dummy"),)


class RangeFieldNumericFilter(RangeNumericMixin, admin.FieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(field, request, params, model, model_admin, field_path)
if not isinstance(field, (DecimalField, IntegerField, FloatField, AutoField)):
raise TypeError(
f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
)

self.request = request
if self.parameter_name is None:
self.parameter_name = self.field_path

self.init_used_parameters(params)


class SliderNumericFilter(RangeNumericFilter):
MAX_DECIMALS = 7
STEP = None

template = "admin/filter_numeric_slider.html"
template = "unfold/filters/filters_numeric_slider.html"
field = None

def __init__(self, field, request, params, model, model_admin, field_path):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@


class DefaultAppConfig(AppConfig):
name = "unfold.contrib.numeric_filters"
name = "unfold.contrib.filters"
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django import forms
from django.utils.translation import gettext_lazy as _

from ...widgets import INPUT_CLASSES


class SingleNumericForm(forms.Form):
def __init__(self, *args, **kwargs):
Expand All @@ -11,11 +13,7 @@ def __init__(self, *args, **kwargs):
label="",
required=False,
widget=forms.NumberInput(
attrs={
"placeholder": _("Value"),
"class": "border bg-white font-medium mb-2 px-3 py-2 rounded-md shadow-sm text-gray-500 text-sm \
w-full focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none",
}
attrs={"placeholder": _("Value"), "class": " ".join(INPUT_CLASSES)}
),
)

Expand All @@ -31,22 +29,14 @@ def __init__(self, *args, **kwargs):
label="",
required=False,
widget=forms.NumberInput(
attrs={
"placeholder": _("From"),
"class": "border bg-white font-medium px-3 py-2 rounded-md shadow-sm text-gray-500 text-sm w-full \
focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none",
}
attrs={"placeholder": _("From"), "class": " ".join(INPUT_CLASSES)}
),
)
self.fields[self.name + "_to"] = forms.FloatField(
label="",
required=False,
widget=forms.NumberInput(
attrs={
"placeholder": _("To"),
"class": "border bg-white font-medium my-2 px-3 py-2 rounded-md shadow-sm text-gray-500 text-sm \
w-full focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none",
}
attrs={"placeholder": _("To"), "class": " ".join(INPUT_CLASSES)}
),
)

Expand Down
1 change: 1 addition & 0 deletions src/unfold/contrib/filters/static/css/nouislider.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/unfold/contrib/filters/static/js/nouislider.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/unfold/contrib/filters/static/js/wNumb.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% load i18n %}

{% with choices.0 as choice %}
<form class="flex flex-col" method="get">
<div class="flex flex-col mb-6">
{% for k, v in choice.request.GET.items %}
{% if not k == choice.parameter_name|add:'_from' and not k == choice.parameter_name|add:'_to' %}
<input type="hidden" name="{{ k }}" value="{{ v }}">
Expand All @@ -10,7 +10,8 @@

<h3 class="font-medium mb-4 text-sm">{% blocktrans with filter_title=title %}By {{ filter_title }}{% endblocktrans %}</h3>

{{ choice.form.as_p }}
<button type="submit" class="self-end bg-primary-600 border border-transparent font-medium px-3 py-2 rounded-md text-sm text-white">{% trans 'Apply' %}</button>
</form>
<div class="flex flex-row space-x-4">
{{ choice.form.as_p }}
</div>
</div>
{% endwith %}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% load i18n %}

{% with choices.0 as choice %}
<form class="flex flex-col" method="get">
<div class="flex flex-col mb-6">
{% for k, v in choice.request.GET.items %}
{% if not k == choice.parameter_name %}
<input type="hidden" name="{{ k }}" value="{{ v }}">
Expand All @@ -11,6 +11,5 @@
<h3 class="font-medium mb-4 text-sm">{% blocktrans with filter_title=title %}By {{ filter_title }}{% endblocktrans %}</h3>

{{ choice.form.as_p }}
<button type="submit" class="self-end bg-primary-600 border border-transparent font-medium px-3 py-2 rounded-md text-sm text-white">{% trans 'Apply' %}</button>
</form>
</div>
{% endwith %}
Loading

0 comments on commit 3e7257b

Please sign in to comment.