Merge branch 'list_filter' of git://github.com/beezz/django-admin2 into beezz-list_filter

This commit is contained in:
Daniel Greenfeld 2013-07-06 17:59:21 +02:00
commit 115c7f9a09
9 changed files with 223 additions and 0 deletions

108
djadmin2/filters.py Normal file
View 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_

View file

@ -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 %}

View file

@ -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):

View file

@ -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)

View file

@ -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):

View file

@ -5,3 +5,4 @@ from test_permissions import *
from test_modelforms import *
from test_views import *
from test_nestedobjects import *
from test_filters import *

View 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))

View file

@ -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)

View file

@ -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