mirror of
https://github.com/jazzband/django-admin2.git
synced 2026-05-04 05:24:45 +00:00
Merge branch 'list_filter' of git://github.com/beezz/django-admin2 into beezz-list_filter
This commit is contained in:
commit
115c7f9a09
9 changed files with 223 additions and 0 deletions
108
djadmin2/filters.py
Normal file
108
djadmin2/filters.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vim:fenc=utf-8
|
||||
|
||||
import collections
|
||||
import django_filters
|
||||
|
||||
from itertools import chain
|
||||
from django.forms.util import flatatt
|
||||
from django.utils.html import format_html
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.forms import widgets as django_widgets
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
LINK_TEMPLATE = '<a href=?{0}={1} {2}>{3}</a>'
|
||||
|
||||
|
||||
class ChoicesAsLinksWidget(django_widgets.Select):
|
||||
"""Select form widget taht renders links for choices
|
||||
instead of select element with options.
|
||||
"""
|
||||
def render(self, name, value, attrs=None, choices=()):
|
||||
links = []
|
||||
for choice_value, choice_label in chain(self.choices, choices):
|
||||
links.append(format_html(
|
||||
LINK_TEMPLATE,
|
||||
name, choice_value, flatatt(attrs), force_text(choice_label),
|
||||
))
|
||||
return mark_safe(u"<br />".join(links))
|
||||
|
||||
|
||||
class NullBooleanLinksWidget(
|
||||
ChoicesAsLinksWidget,
|
||||
django_widgets.NullBooleanSelect
|
||||
):
|
||||
def __init__(self, attrs=None, choices=()):
|
||||
super(ChoicesAsLinksWidget, self).__init__(attrs)
|
||||
self.choices = [
|
||||
('1', ugettext_lazy('Unknown')),
|
||||
('2', ugettext_lazy('Yes')),
|
||||
('3', ugettext_lazy('No')),
|
||||
]
|
||||
|
||||
#: Maps `django_filter`'s field filters types to our
|
||||
#: custom form widget.
|
||||
FILTER_TYPE_TO_WIDGET = {
|
||||
django_filters.BooleanFilter: NullBooleanLinksWidget,
|
||||
django_filters.ChoiceFilter: ChoicesAsLinksWidget,
|
||||
django_filters.ModelChoiceFilter: ChoicesAsLinksWidget,
|
||||
}
|
||||
|
||||
|
||||
def build_list_filter(request, model_admin, queryset):
|
||||
"""Builds :class:`~django_filters.FilterSet` instance
|
||||
for :attr:`djadmin2.ModelAdmin2.Meta.list_filter` option.
|
||||
|
||||
If :attr:`djadmin2.ModelAdmin2.Meta.list_filter` is not
|
||||
sequence, it's considered to be class with interface like
|
||||
:class:`django_filters.FilterSet` and its instantiate wit
|
||||
`request.GET` and `queryset`.
|
||||
"""
|
||||
# if ``list_filter`` is not iterable return it right away
|
||||
if not isinstance(model_admin.list_filter, collections.Iterable):
|
||||
return model_admin.list_filter(
|
||||
request.GET,
|
||||
queryset=queryset,
|
||||
)
|
||||
# otherwise build :mod:`django_filters.FilterSet`
|
||||
filters = []
|
||||
for field_filter in model_admin.list_filter:
|
||||
if isinstance(field_filter, basestring):
|
||||
filters.append(get_filter_for_field_name(
|
||||
queryset.model,
|
||||
field_filter,
|
||||
))
|
||||
else:
|
||||
filters.append(field_filter)
|
||||
filterset_dict = {}
|
||||
for field_filter in filters:
|
||||
filterset_dict[field_filter.name] = field_filter
|
||||
fields = filterset_dict.keys()
|
||||
filterset_dict['Meta'] = type(
|
||||
"Meta",
|
||||
(),
|
||||
{
|
||||
'model': queryset.model,
|
||||
'fields': fields,
|
||||
},
|
||||
)
|
||||
return type(
|
||||
"%sFilterSet" % queryset.model.__name__,
|
||||
(django_filters.FilterSet, ),
|
||||
filterset_dict,
|
||||
)(request.GET, queryset=queryset)
|
||||
|
||||
|
||||
def get_filter_for_field_name(model, field_name):
|
||||
"""Returns filter for model field by field name.
|
||||
"""
|
||||
filter_ = django_filters.FilterSet.filter_for_field(
|
||||
django_filters.filterset.get_model_field(model, field_name,),
|
||||
field_name,
|
||||
)
|
||||
filter_.widget = FILTER_TYPE_TO_WIDGET.get(
|
||||
filter_.__class__,
|
||||
filter_.widget,
|
||||
)
|
||||
return filter_
|
||||
|
|
@ -102,6 +102,17 @@
|
|||
{{ object_list|length }} {{ model_name_pluralized }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
{% block filters %}
|
||||
{% if list_filter %}
|
||||
<div id="list_filter_container" class="well span4">
|
||||
<h4>{% trans "Filter" %}</h4>
|
||||
{{ list_filter.form.as_p }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock filters %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,16 @@ from django.views import generic
|
|||
|
||||
import extra_views
|
||||
|
||||
import collections
|
||||
import django_filters
|
||||
|
||||
import operator
|
||||
|
||||
from . import permissions, utils
|
||||
from .forms import AdminAuthenticationForm
|
||||
from .viewmixins import Admin2Mixin, AdminModel2Mixin, Admin2ModelFormMixin
|
||||
from .filters import build_list_filter
|
||||
|
||||
|
||||
|
||||
class IndexView(Admin2Mixin, generic.TemplateView):
|
||||
|
|
@ -126,17 +131,32 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
|
|||
if self.model_admin.search_fields and search_term:
|
||||
queryset, search_use_distinct = self.get_search_results(queryset, search_term)
|
||||
|
||||
if self.model_admin.list_filter:
|
||||
queryset = self.build_list_filter(queryset).qs
|
||||
|
||||
if search_use_distinct:
|
||||
return queryset.distinct()
|
||||
else:
|
||||
return queryset
|
||||
|
||||
def build_list_filter(self, queryset=None):
|
||||
if not hasattr(self, '_list_filter'):
|
||||
if queryset is None:
|
||||
queryset = self.get_queryset()
|
||||
self._list_filter = build_list_filter(
|
||||
self.request,
|
||||
self.model_admin,
|
||||
queryset,
|
||||
)
|
||||
return self._list_filter
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ModelListView, self).get_context_data(**kwargs)
|
||||
context['model'] = self.get_model()
|
||||
context['actions'] = self.get_actions().values()
|
||||
context['search_fields'] = self.get_search_fields()
|
||||
context['search_term'] = self.request.GET.get('q', '')
|
||||
context['list_filter'] = self.build_list_filter()
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class CommentInline(admin.TabularInline):
|
|||
class PostAdmin(admin.ModelAdmin):
|
||||
inlines = [CommentInline, ]
|
||||
search_fields = ('title', 'body')
|
||||
list_filter = ['published', 'title' ]
|
||||
|
||||
admin.site.register(Post, PostAdmin)
|
||||
admin.site.register(Comment)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from djadmin2.actions import DeleteSelectedAction
|
|||
from djadmin2.forms import UserCreationForm, UserChangeForm
|
||||
from djadmin2.apiviews import Admin2APISerializer
|
||||
|
||||
|
||||
from .actions import CustomPublishAction
|
||||
from .models import Post, Comment
|
||||
|
||||
|
|
@ -47,10 +48,12 @@ class PostAdmin(djadmin2.ModelAdmin2):
|
|||
list_actions = [DeleteSelectedAction, CustomPublishAction, unpublish_items]
|
||||
inlines = [CommentInline]
|
||||
search_fields = ('title', '^body')
|
||||
list_filter = ['published', ]
|
||||
|
||||
|
||||
class CommentAdmin(djadmin2.ModelAdmin2):
|
||||
search_fields = ('body', '=post__title')
|
||||
list_filter = ['post', ]
|
||||
|
||||
|
||||
class UserAdmin2(djadmin2.ModelAdmin2):
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ from test_permissions import *
|
|||
from test_modelforms import *
|
||||
from test_views import *
|
||||
from test_nestedobjects import *
|
||||
from test_filters import *
|
||||
|
|
|
|||
72
example/blog/tests/test_filters.py
Normal file
72
example/blog/tests/test_filters.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vim:fenc=utf-8
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test.client import RequestFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from ..models import Post
|
||||
|
||||
import djadmin2
|
||||
import djadmin2.filters as djadmin2_filters
|
||||
|
||||
import django_filters
|
||||
|
||||
|
||||
class PostAdminSimple(djadmin2.ModelAdmin2):
|
||||
list_filter = ['published', ]
|
||||
|
||||
|
||||
class PostAdminWithFilterInstances(djadmin2.ModelAdmin2):
|
||||
list_filter = [
|
||||
django_filters.BooleanFilter(name='published'),
|
||||
]
|
||||
|
||||
|
||||
class FS(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = ['published']
|
||||
|
||||
|
||||
class PostAdminWithFilterSetInst(djadmin2.ModelAdmin2):
|
||||
list_filter = FS
|
||||
|
||||
|
||||
class ListFilterBuilderTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.rf = RequestFactory()
|
||||
|
||||
def test_filter_building(self):
|
||||
Post.objects.create(title="post_1_title", body="body")
|
||||
Post.objects.create(title="post_2_title", body="another body")
|
||||
request = self.rf.get(reverse("admin2:dashboard"))
|
||||
list_filter_inst = djadmin2_filters.build_list_filter(
|
||||
request,
|
||||
PostAdminSimple,
|
||||
Post.objects.all(),
|
||||
)
|
||||
self.assertTrue(
|
||||
issubclass(list_filter_inst.__class__, django_filters.FilterSet)
|
||||
)
|
||||
self.assertEqual(
|
||||
list_filter_inst.filters['published'].widget,
|
||||
djadmin2_filters.NullBooleanLinksWidget,
|
||||
)
|
||||
list_filter_inst = djadmin2_filters.build_list_filter(
|
||||
request,
|
||||
PostAdminWithFilterInstances,
|
||||
Post.objects.all(),
|
||||
)
|
||||
self.assertNotEqual(
|
||||
list_filter_inst.filters['published'].widget,
|
||||
djadmin2_filters.NullBooleanLinksWidget,
|
||||
)
|
||||
list_filter_inst = djadmin2_filters.build_list_filter(
|
||||
request,
|
||||
PostAdminWithFilterSetInst,
|
||||
Post.objects.all(),
|
||||
)
|
||||
self.assertTrue(isinstance(list_filter_inst, FS))
|
||||
|
|
@ -56,6 +56,12 @@ class PostListTest(BaseIntegrationTest):
|
|||
response = self.client.get(reverse("admin2:blog_post_index"))
|
||||
self.assertContains(response, post.title)
|
||||
|
||||
def test_list_filter_presence(self):
|
||||
Post.objects.create(title="post_1_title", body="body")
|
||||
Post.objects.create(title="post_2_title", body="another body")
|
||||
response = self.client.get(reverse("admin2:blog_post_index"))
|
||||
self.assertContains(response, 'id="list_filter_container"')
|
||||
|
||||
def test_actions_displayed(self):
|
||||
response = self.client.get(reverse("admin2:blog_post_index"))
|
||||
self.assertInHTML('<a tabindex="-1" href="#" data-name="action" data-value="DeleteSelectedAction">Delete selected items</a>', response.content)
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ django-coverage==1.2.2
|
|||
django-extra-views==0.6.2
|
||||
django-floppyforms==1.1
|
||||
Sphinx==1.2b1
|
||||
django-filter==0.6
|
||||
|
|
|
|||
Loading…
Reference in a new issue