Skip to content

Commit 5270606

Browse files
author
Jairus Martin
committed
Added quickfilter plugin. Only made for in default menu style
1 parent 6c0f854 commit 5270606

File tree

9 files changed

+290
-9
lines changed

9 files changed

+290
-9
lines changed

demo_app/app/adminx.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class GlobalSetting(object):
3232
global_models_icon = {
3333
Host: 'fa fa-laptop', IDC: 'fa fa-cloud'
3434
}
35-
menu_style = 'accordion'
35+
menu_style = 'default'#'accordion'
3636
xadmin.site.register(views.CommAdminView, GlobalSetting)
3737

3838

@@ -76,7 +76,8 @@ def open_web(self, instance):
7676
search_fields = ['name', 'ip', 'description']
7777
list_filter = ['idc', 'guarantee_date', 'status', 'brand', 'model',
7878
'cpu', 'core_num', 'hard_disk', 'memory', ('service_type',xadmin.filters.MultiSelectFieldListFilter)]
79-
79+
80+
list_quick_filter = ['service_type',{'field':'idc__name','limit':10}]
8081
list_bookmarks = [{'title': "Need Guarantee", 'query': {'status__exact': 2}, 'order': ('-guarantee_date',), 'cols': ('brand', 'guarantee_date', 'service_type')}]
8182

8283
show_detail_fields = ('idc',)

xadmin/filters.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.utils.safestring import mark_safe
99
from django.utils.html import escape,format_html
1010
from django.utils.text import Truncator
11+
from django.core.cache import cache, get_cache
1112

1213
from xadmin.views.list import EMPTY_CHANGELIST_VALUE
1314
import datetime
@@ -199,7 +200,7 @@ def choices(self):
199200
@manager.register
200201
class TextFieldListFilter(FieldFilter):
201202
template = 'xadmin/filters/char.html'
202-
lookup_formats = {'search': '%s__contains'}
203+
lookup_formats = {'in': '%s__in','search': '%s__contains'}
203204

204205
@classmethod
205206
def test(cls, field, request, params, model, admin_view, field_path):
@@ -322,7 +323,7 @@ def __init__(self, field, request, params, model, model_admin, field_path):
322323
else:
323324
rel_name = other_model._meta.pk.name
324325

325-
self.lookup_formats = {'exact': '%%s__%s__exact' % rel_name}
326+
self.lookup_formats = {'in': '%%s__%s__in' % rel_name,'exact': '%%s__%s__exact' % rel_name}
326327
super(RelatedFieldSearchFilter, self).__init__(
327328
field, request, params, model, model_admin, field_path)
328329

@@ -369,7 +370,7 @@ def __init__(self, field, request, params, model, model_admin, field_path):
369370
else:
370371
rel_name = other_model._meta.pk.name
371372

372-
self.lookup_formats = {'exact': '%%s__%s__exact' %
373+
self.lookup_formats = {'in': '%%s__%s__in' % rel_name,'exact': '%%s__%s__exact' %
373374
rel_name, 'isnull': '%s__isnull'}
374375
self.lookup_choices = field.get_choices(include_blank=False)
375376
super(RelatedFieldListFilter, self).__init__(
@@ -429,14 +430,55 @@ class MultiSelectFieldListFilter(ListFieldFilter):
429430
"""
430431
template = 'xadmin/filters/checklist.html'
431432
lookup_formats = {'in': '%s__in'}
433+
cache_config = {'enabled':False,'key':'quickfilter_%s','timeout':3600,'cache':'default'}
432434

433435
@classmethod
434436
def test(cls, field, request, params, model, admin_view, field_path):
435437
return True
436438

437-
def __init__(self, field, request, params, model, model_admin, field_path):
439+
def get_cached_choices(self):
440+
if not self.cache_config['enabled']:
441+
return None
442+
c = get_cache(self.cache_config['cache'])
443+
return c.get(self.cache_config['key']%self.field_path)
444+
445+
def set_cached_choices(self,choices):
446+
if not self.cache_config['enabled']:
447+
return
448+
c = get_cache(self.cache_config['cache'])
449+
return c.set(self.cache_config['key']%self.field_path,choices)
450+
451+
def __init__(self, field, request, params, model, model_admin, field_path,field_order_by=None,field_limit=None,sort_key=None,cache_config=None):
438452
super(MultiSelectFieldListFilter,self).__init__(field, request, params, model, model_admin, field_path)
439-
self.lookup_choices = [x[0] for x in self.admin_view.queryset().order_by(field_path).values_list(field_path).distinct().exclude(**{"%s__isnull"%field_path:True}) if str(x[0]).strip()!=""]#field.get_choices(include_blank=False)
453+
454+
# Check for it in the cachce
455+
if cache_config is not None and type(cache_config)==dict:
456+
self.cache_config.update(cache_config)
457+
458+
if self.cache_config['enabled']:
459+
self.field_path = field_path
460+
choices = self.get_cached_choices()
461+
if choices:
462+
self.lookup_choices = choices
463+
return
464+
465+
# Else rebuild it
466+
queryset = self.admin_view.queryset().exclude(**{"%s__isnull"%field_path:True}).values_list(field_path, flat=True).distinct()
467+
#queryset = self.admin_view.queryset().distinct(field_path).exclude(**{"%s__isnull"%field_path:True})
468+
469+
if field_order_by is not None:
470+
# Do a subquery to order the distinct set
471+
queryset = self.admin_view.queryset().filter(id__in=queryset).order_by(field_order_by)
472+
473+
if field_limit is not None and type(field_limit)==int and queryset.count()>field_limit:
474+
queryset = queryset[:field_limit]
475+
476+
self.lookup_choices = [str(it) for it in queryset.values_list(field_path,flat=True) if str(it).strip()!=""]
477+
if sort_key is not None:
478+
self.lookup_choices = sorted(self.lookup_choices,key=sort_key)
479+
480+
if self.cache_config['enabled']:
481+
self.set_cached_choices(self.lookup_choices)
440482

441483
def choices(self):
442484
self.lookup_in_val = (type(self.lookup_in_val) in (tuple,list)) and self.lookup_in_val or list(self.lookup_in_val)
@@ -501,4 +543,4 @@ def choices(self):
501543
'query_string': self.query_string({self.lookup_isnull_name: 'True'},
502544
[self.lookup_exact_name]),
503545
'display': EMPTY_CHANGELIST_VALUE,
504-
}
546+
}

xadmin/plugins/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
PLUGINS = ('actions', 'filters', 'bookmark', 'export', 'layout', 'refresh', 'sortable', 'details',
33
'editable', 'relate', 'chart', 'ajax', 'relfield', 'inline', 'topnav', 'portal', 'quickform',
44
'wizard', 'images', 'auth', 'multiselect', 'themes', 'aggregation', 'mobile', 'passwords',
5-
'sitemenu', 'language', 'comments')
5+
'sitemenu', 'language', 'comments','quickfilter')
66

77

88
def register_builtin_plugins(site):

xadmin/plugins/quickfilter.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
'''
2+
Created on Mar 26, 2014
3+
4+
@author: LAB_ADM
5+
'''
6+
from django.utils.translation import ugettext_lazy as _
7+
from xadmin.filters import manager,MultiSelectFieldListFilter
8+
from xadmin.plugins.filters import *
9+
10+
@manager.register
11+
class QuickFilterMultiSelectFieldListFilter(MultiSelectFieldListFilter):
12+
""" Delegates the filter to the default filter and ors the results of each
13+
14+
Lists the distinct values of each field as a checkbox
15+
Uses the default spec for each
16+
17+
"""
18+
template = 'xadmin/filters/quickfilter.html'
19+
20+
class QuickFilterPlugin(BaseAdminPlugin):
21+
""" Add a filter menu to the left column of the page """
22+
list_quick_filter = () # these must be a subset of list_filter to work
23+
quickfilter = {}
24+
search_fields = ()
25+
free_query_filter = True
26+
27+
def init_request(self, *args, **kwargs):
28+
menu_style_accordian = hasattr(self.admin_view,'menu_style') and self.admin_view.menu_style == 'accordion'
29+
return bool(self.list_quick_filter) and not menu_style_accordian
30+
31+
# Media
32+
def get_media(self, media):
33+
return media + self.vendor('xadmin.plugin.quickfilter.js','xadmin.plugin.quickfilter.css')
34+
35+
def lookup_allowed(self, lookup, value):
36+
model = self.model
37+
# Check FKey lookups that are allowed, so that popups produced by
38+
# ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
39+
# are allowed to work.
40+
for l in model._meta.related_fkey_lookups:
41+
for k, v in widgets.url_params_from_lookup_dict(l).items():
42+
if k == lookup and v == value:
43+
return True
44+
45+
parts = lookup.split(LOOKUP_SEP)
46+
47+
# Last term in lookup is a query term (__exact, __startswith etc)
48+
# This term can be ignored.
49+
if len(parts) > 1 and parts[-1] in QUERY_TERMS:
50+
parts.pop()
51+
52+
# Special case -- foo__id__exact and foo__id queries are implied
53+
# if foo has been specificially included in the lookup list; so
54+
# drop __id if it is the last part. However, first we need to find
55+
# the pk attribute name.
56+
rel_name = None
57+
for part in parts[:-1]:
58+
try:
59+
field, _, _, _ = model._meta.get_field_by_name(part)
60+
except FieldDoesNotExist:
61+
# Lookups on non-existants fields are ok, since they're ignored
62+
# later.
63+
return True
64+
if hasattr(field, 'rel'):
65+
model = field.rel.to
66+
rel_name = field.rel.get_related_field().name
67+
elif isinstance(field, RelatedObject):
68+
model = field.model
69+
rel_name = model._meta.pk.name
70+
else:
71+
rel_name = None
72+
if rel_name and len(parts) > 1 and parts[-1] == rel_name:
73+
parts.pop()
74+
75+
if len(parts) == 1:
76+
return True
77+
clean_lookup = LOOKUP_SEP.join(parts)
78+
return clean_lookup in self.list_quick_filter
79+
80+
def get_list_queryset(self, queryset):
81+
lookup_params = dict([(smart_str(k)[len(FILTER_PREFIX):], v) for k, v in self.admin_view.params.items() if smart_str(k).startswith(FILTER_PREFIX) and v != ''])
82+
for p_key, p_val in lookup_params.iteritems():
83+
if p_val == "False":
84+
lookup_params[p_key] = False
85+
use_distinct = False
86+
87+
if not hasattr(self.admin_view,'quickfilter'):
88+
self.admin_view.quickfilter = {}
89+
90+
# for clean filters
91+
self.admin_view.quickfilter['has_query_param'] = bool(lookup_params)
92+
self.admin_view.quickfilter['clean_query_url'] = self.admin_view.get_query_string(remove=[k for k in self.request.GET.keys() if k.startswith(FILTER_PREFIX)])
93+
94+
# Normalize the types of keys
95+
if not self.free_query_filter:
96+
for key, value in lookup_params.items():
97+
if not self.lookup_allowed(key, value):
98+
raise SuspiciousOperation("Filtering by %s not allowed" % key)
99+
100+
self.filter_specs = []
101+
if self.list_quick_filter:
102+
for list_quick_filter in self.list_quick_filter:
103+
field_path = None
104+
field_order_by = None
105+
field_limit = None
106+
field_parts = []
107+
sort_key = None
108+
cache_config = None
109+
110+
if type(list_quick_filter)==dict and 'field' in list_quick_filter:
111+
field = list_quick_filter['field']
112+
if 'order_by' in list_quick_filter:
113+
field_order_by = list_quick_filter['order_by']
114+
if 'limit' in list_quick_filter:
115+
field_limit = list_quick_filter['limit']
116+
if 'sort' in list_quick_filter and callable(list_quick_filter['sort']):
117+
sort_key = list_quick_filter['sort']
118+
if 'cache' in list_quick_filter and type(list_quick_filter)==dict:
119+
cache_config = list_quick_filter['cache']
120+
121+
else:
122+
field = list_quick_filter # This plugin only uses MultiselectFieldListFilter
123+
124+
if not isinstance(field, models.Field):
125+
field_path = field
126+
field_parts = get_fields_from_path(self.model, field_path)
127+
field = field_parts[-1]
128+
spec = QuickFilterMultiSelectFieldListFilter(field, self.request, lookup_params,self.model, self.admin_view, field_path=field_path,field_order_by=field_order_by,field_limit=field_limit,sort_key=sort_key,cache_config=cache_config)
129+
130+
if len(field_parts)>1:
131+
spec.title = "%s %s"%(field_parts[-2].name,spec.title)
132+
133+
# Check if we need to use distinct()
134+
use_distinct = True#(use_distinct orlookup_needs_distinct(self.opts, field_path))
135+
if spec and spec.has_output():
136+
try:
137+
new_qs = spec.do_filte(queryset)
138+
except ValidationError, e:
139+
new_qs = None
140+
self.admin_view.message_user(_("<b>Filtering error:</b> %s") % e.messages[0], 'error')
141+
if new_qs is not None:
142+
queryset = new_qs
143+
144+
self.filter_specs.append(spec)
145+
146+
self.has_filters = bool(self.filter_specs)
147+
self.admin_view.quickfilter['filter_specs'] = self.filter_specs
148+
self.admin_view.quickfilter['used_filter_num'] = len(filter(lambda f: f.is_used, self.filter_specs))
149+
150+
if use_distinct:
151+
return queryset.distinct()
152+
else:
153+
return queryset
154+
155+
def block_left_navbar(self, context, nodes):
156+
nodes.append(loader.render_to_string('xadmin/blocks/modal_list.left_navbar.quickfilter.html',context))
157+
158+
site.register_plugin(QuickFilterPlugin, ListAdminView)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.nav-quickfilter .filter-item {
2+
white-space: nowrap;
3+
overflow: hidden;
4+
padding: 5px;
5+
}
6+
7+
.nav-quickfilter .filter-col-1 {
8+
margin: 3px 2px 0 -2px;
9+
float: left;
10+
}
11+
12+
.nav-quickfilter .filter-col-2 {
13+
}
14+
15+
.nav-quickfilter .nav-expand {
16+
z-index:100;
17+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
;(function($){
2+
$('[data-toggle=tooltip]').tooltip();
3+
var max=10;
4+
5+
function addShowMore($,v){
6+
$(v).nextUntil('li.nav-header').last().after(
7+
$('<li class="filter-multiselect"><a class="small filter-item" href="#"><input class="filter-col-1" type="checkbox"><span class="filter-col-2">Show more</span></a></li>').click(function(e){
8+
e.preventDefault();
9+
e.stopPropagation();
10+
$(v).nextUntil('li.nav-header').show();
11+
$(v).nextUntil('li.nav-header').last().remove();
12+
addShowLess($,v);
13+
})
14+
);
15+
$(v).nextUntil('li.nav-header').last().show();
16+
}
17+
18+
function addShowLess($,v){
19+
$(v).nextUntil('li.nav-header').last().after(
20+
$('<li class="filter-multiselect"><a class="small filter-item" href="#"><input class="filter-col-1" type="checkbox"><span class="filter-col-2">Show less</span></a></li>').click(function(e){
21+
e.preventDefault();
22+
e.stopPropagation();
23+
$(v).nextUntil('li.nav-header').filter(function(i){return !$(this).find('input').is(':checked');}).slice(max).hide();
24+
$(v).nextUntil('li.nav-header').last().remove();
25+
$(v).scrollMinimal(3000);
26+
addShowMore($,v);
27+
})
28+
);
29+
$(v).nextUntil('li.nav-header').last().show();
30+
}
31+
32+
$.each($('.nav-quickfilter li.nav-header'),function(i,v){
33+
if ($(v).nextUntil('li.nav-header').size()>max) {
34+
$(v).nextUntil('li.nav-header').filter(function(i){return !$(this).find('input').is(':checked');}).slice(max).hide();
35+
addShowMore($,v);
36+
}
37+
});
38+
39+
$('.nav-quickfilter li.nav-header').on('click',function(e) {
40+
e.preventDefault();
41+
e.stopPropagation();
42+
$('.nav-quickfilter li.nav-header i').toggleClass('icon-chevron-right');
43+
$('.nav-quickfilter li.nav-header i').toggleClass('icon-chevron-left');
44+
$('#left-side').toggleClass('col-md-2');
45+
$('#left-side').toggleClass('col-md-4');
46+
$('#content-block').toggleClass('col-md-10');
47+
$('#content-block').toggleClass('col-md-8');
48+
});
49+
})(jQuery)

xadmin/templates/xadmin/base_site.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<p>{% trans "You don't have permission to edit anything." %}</p>
3838
{% endif %}
3939
{% endblock %}
40+
{% view_block 'left_navbar' %}
4041
</div>
4142

4243
<div id="content-block" class="col-sm-11 col-md-10">
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<ul class="well nav nav-pills nav-stacked nav-quickfilter hide-sm">
2+
{% for spec in cl.quickfilter.filter_specs %}{{ spec|safe }}{% endfor %}
3+
</ul>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{% load i18n %}
2+
<li class="nav-header ">{{title}} <i class="icon-chevron-right pull-right"></i></li>
3+
{% for choice in choices %}
4+
<li class="filter-multiselect">
5+
<a class="small filter-item" {% if choice.selected %} href="{{ choice.remove_query_string|iriencode }}" {% else %} href="{{ choice.query_string|iriencode }}" {% endif %} data-toggle="tooltip" data-placement="right" title="{{ choice.display }}">
6+
<input class="filter-col-1" type="checkbox" {% if choice.selected %} checked="checked"{% endif %}>
7+
<span class="filter-col-2">{{ choice.display }}</span>
8+
</a>
9+
</li>
10+
{% endfor %}

0 commit comments

Comments
 (0)