Skip to content

Commit

Permalink
Added quickfilter plugin. Only made for in default menu style
Browse files Browse the repository at this point in the history
  • Loading branch information
Jairus Martin committed Apr 10, 2014
1 parent 6c0f854 commit 5270606
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 9 deletions.
5 changes: 3 additions & 2 deletions demo_app/app/adminx.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class GlobalSetting(object):
global_models_icon = {
Host: 'fa fa-laptop', IDC: 'fa fa-cloud'
}
menu_style = 'accordion'
menu_style = 'default'#'accordion'
xadmin.site.register(views.CommAdminView, GlobalSetting)


Expand Down Expand Up @@ -76,7 +76,8 @@ def open_web(self, instance):
search_fields = ['name', 'ip', 'description']
list_filter = ['idc', 'guarantee_date', 'status', 'brand', 'model',
'cpu', 'core_num', 'hard_disk', 'memory', ('service_type',xadmin.filters.MultiSelectFieldListFilter)]


list_quick_filter = ['service_type',{'field':'idc__name','limit':10}]
list_bookmarks = [{'title': "Need Guarantee", 'query': {'status__exact': 2}, 'order': ('-guarantee_date',), 'cols': ('brand', 'guarantee_date', 'service_type')}]

show_detail_fields = ('idc',)
Expand Down
54 changes: 48 additions & 6 deletions xadmin/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.utils.safestring import mark_safe
from django.utils.html import escape,format_html
from django.utils.text import Truncator
from django.core.cache import cache, get_cache

from xadmin.views.list import EMPTY_CHANGELIST_VALUE
import datetime
Expand Down Expand Up @@ -199,7 +200,7 @@ def choices(self):
@manager.register
class TextFieldListFilter(FieldFilter):
template = 'xadmin/filters/char.html'
lookup_formats = {'search': '%s__contains'}
lookup_formats = {'in': '%s__in','search': '%s__contains'}

@classmethod
def test(cls, field, request, params, model, admin_view, field_path):
Expand Down Expand Up @@ -322,7 +323,7 @@ def __init__(self, field, request, params, model, model_admin, field_path):
else:
rel_name = other_model._meta.pk.name

self.lookup_formats = {'exact': '%%s__%s__exact' % rel_name}
self.lookup_formats = {'in': '%%s__%s__in' % rel_name,'exact': '%%s__%s__exact' % rel_name}
super(RelatedFieldSearchFilter, self).__init__(
field, request, params, model, model_admin, field_path)

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

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

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

def __init__(self, field, request, params, model, model_admin, field_path):
def get_cached_choices(self):
if not self.cache_config['enabled']:
return None
c = get_cache(self.cache_config['cache'])
return c.get(self.cache_config['key']%self.field_path)

def set_cached_choices(self,choices):
if not self.cache_config['enabled']:
return
c = get_cache(self.cache_config['cache'])
return c.set(self.cache_config['key']%self.field_path,choices)

def __init__(self, field, request, params, model, model_admin, field_path,field_order_by=None,field_limit=None,sort_key=None,cache_config=None):
super(MultiSelectFieldListFilter,self).__init__(field, request, params, model, model_admin, field_path)
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)

# Check for it in the cachce
if cache_config is not None and type(cache_config)==dict:
self.cache_config.update(cache_config)

if self.cache_config['enabled']:
self.field_path = field_path
choices = self.get_cached_choices()
if choices:
self.lookup_choices = choices
return

# Else rebuild it
queryset = self.admin_view.queryset().exclude(**{"%s__isnull"%field_path:True}).values_list(field_path, flat=True).distinct()
#queryset = self.admin_view.queryset().distinct(field_path).exclude(**{"%s__isnull"%field_path:True})

if field_order_by is not None:
# Do a subquery to order the distinct set
queryset = self.admin_view.queryset().filter(id__in=queryset).order_by(field_order_by)

if field_limit is not None and type(field_limit)==int and queryset.count()>field_limit:
queryset = queryset[:field_limit]

self.lookup_choices = [str(it) for it in queryset.values_list(field_path,flat=True) if str(it).strip()!=""]
if sort_key is not None:
self.lookup_choices = sorted(self.lookup_choices,key=sort_key)

if self.cache_config['enabled']:
self.set_cached_choices(self.lookup_choices)

def choices(self):
self.lookup_in_val = (type(self.lookup_in_val) in (tuple,list)) and self.lookup_in_val or list(self.lookup_in_val)
Expand Down Expand Up @@ -501,4 +543,4 @@ def choices(self):
'query_string': self.query_string({self.lookup_isnull_name: 'True'},
[self.lookup_exact_name]),
'display': EMPTY_CHANGELIST_VALUE,
}
}
2 changes: 1 addition & 1 deletion xadmin/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
PLUGINS = ('actions', 'filters', 'bookmark', 'export', 'layout', 'refresh', 'sortable', 'details',
'editable', 'relate', 'chart', 'ajax', 'relfield', 'inline', 'topnav', 'portal', 'quickform',
'wizard', 'images', 'auth', 'multiselect', 'themes', 'aggregation', 'mobile', 'passwords',
'sitemenu', 'language', 'comments')
'sitemenu', 'language', 'comments','quickfilter')


def register_builtin_plugins(site):
Expand Down
158 changes: 158 additions & 0 deletions xadmin/plugins/quickfilter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
'''
Created on Mar 26, 2014
@author: LAB_ADM
'''
from django.utils.translation import ugettext_lazy as _
from xadmin.filters import manager,MultiSelectFieldListFilter
from xadmin.plugins.filters import *

@manager.register
class QuickFilterMultiSelectFieldListFilter(MultiSelectFieldListFilter):
""" Delegates the filter to the default filter and ors the results of each
Lists the distinct values of each field as a checkbox
Uses the default spec for each
"""
template = 'xadmin/filters/quickfilter.html'

class QuickFilterPlugin(BaseAdminPlugin):
""" Add a filter menu to the left column of the page """
list_quick_filter = () # these must be a subset of list_filter to work
quickfilter = {}
search_fields = ()
free_query_filter = True

def init_request(self, *args, **kwargs):
menu_style_accordian = hasattr(self.admin_view,'menu_style') and self.admin_view.menu_style == 'accordion'
return bool(self.list_quick_filter) and not menu_style_accordian

# Media
def get_media(self, media):
return media + self.vendor('xadmin.plugin.quickfilter.js','xadmin.plugin.quickfilter.css')

def lookup_allowed(self, lookup, value):
model = self.model
# Check FKey lookups that are allowed, so that popups produced by
# ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
# are allowed to work.
for l in model._meta.related_fkey_lookups:
for k, v in widgets.url_params_from_lookup_dict(l).items():
if k == lookup and v == value:
return True

parts = lookup.split(LOOKUP_SEP)

# Last term in lookup is a query term (__exact, __startswith etc)
# This term can be ignored.
if len(parts) > 1 and parts[-1] in QUERY_TERMS:
parts.pop()

# Special case -- foo__id__exact and foo__id queries are implied
# if foo has been specificially included in the lookup list; so
# drop __id if it is the last part. However, first we need to find
# the pk attribute name.
rel_name = None
for part in parts[:-1]:
try:
field, _, _, _ = model._meta.get_field_by_name(part)
except FieldDoesNotExist:
# Lookups on non-existants fields are ok, since they're ignored
# later.
return True
if hasattr(field, 'rel'):
model = field.rel.to
rel_name = field.rel.get_related_field().name
elif isinstance(field, RelatedObject):
model = field.model
rel_name = model._meta.pk.name
else:
rel_name = None
if rel_name and len(parts) > 1 and parts[-1] == rel_name:
parts.pop()

if len(parts) == 1:
return True
clean_lookup = LOOKUP_SEP.join(parts)
return clean_lookup in self.list_quick_filter

def get_list_queryset(self, queryset):
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 != ''])
for p_key, p_val in lookup_params.iteritems():
if p_val == "False":
lookup_params[p_key] = False
use_distinct = False

if not hasattr(self.admin_view,'quickfilter'):
self.admin_view.quickfilter = {}

# for clean filters
self.admin_view.quickfilter['has_query_param'] = bool(lookup_params)
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)])

# Normalize the types of keys
if not self.free_query_filter:
for key, value in lookup_params.items():
if not self.lookup_allowed(key, value):
raise SuspiciousOperation("Filtering by %s not allowed" % key)

self.filter_specs = []
if self.list_quick_filter:
for list_quick_filter in self.list_quick_filter:
field_path = None
field_order_by = None
field_limit = None
field_parts = []
sort_key = None
cache_config = None

if type(list_quick_filter)==dict and 'field' in list_quick_filter:
field = list_quick_filter['field']
if 'order_by' in list_quick_filter:
field_order_by = list_quick_filter['order_by']
if 'limit' in list_quick_filter:
field_limit = list_quick_filter['limit']
if 'sort' in list_quick_filter and callable(list_quick_filter['sort']):
sort_key = list_quick_filter['sort']
if 'cache' in list_quick_filter and type(list_quick_filter)==dict:
cache_config = list_quick_filter['cache']

else:
field = list_quick_filter # This plugin only uses MultiselectFieldListFilter

if not isinstance(field, models.Field):
field_path = field
field_parts = get_fields_from_path(self.model, field_path)
field = field_parts[-1]
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)

if len(field_parts)>1:
spec.title = "%s %s"%(field_parts[-2].name,spec.title)

# Check if we need to use distinct()
use_distinct = True#(use_distinct orlookup_needs_distinct(self.opts, field_path))
if spec and spec.has_output():
try:
new_qs = spec.do_filte(queryset)
except ValidationError, e:
new_qs = None
self.admin_view.message_user(_("<b>Filtering error:</b> %s") % e.messages[0], 'error')
if new_qs is not None:
queryset = new_qs

self.filter_specs.append(spec)

self.has_filters = bool(self.filter_specs)
self.admin_view.quickfilter['filter_specs'] = self.filter_specs
self.admin_view.quickfilter['used_filter_num'] = len(filter(lambda f: f.is_used, self.filter_specs))

if use_distinct:
return queryset.distinct()
else:
return queryset

def block_left_navbar(self, context, nodes):
nodes.append(loader.render_to_string('xadmin/blocks/modal_list.left_navbar.quickfilter.html',context))

site.register_plugin(QuickFilterPlugin, ListAdminView)
17 changes: 17 additions & 0 deletions xadmin/static/xadmin/css/xadmin.plugin.quickfilter.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.nav-quickfilter .filter-item {
white-space: nowrap;
overflow: hidden;
padding: 5px;
}

.nav-quickfilter .filter-col-1 {
margin: 3px 2px 0 -2px;
float: left;
}

.nav-quickfilter .filter-col-2 {
}

.nav-quickfilter .nav-expand {
z-index:100;
}
49 changes: 49 additions & 0 deletions xadmin/static/xadmin/js/xadmin.plugin.quickfilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
;(function($){
$('[data-toggle=tooltip]').tooltip();
var max=10;

function addShowMore($,v){
$(v).nextUntil('li.nav-header').last().after(
$('<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){
e.preventDefault();
e.stopPropagation();
$(v).nextUntil('li.nav-header').show();
$(v).nextUntil('li.nav-header').last().remove();
addShowLess($,v);
})
);
$(v).nextUntil('li.nav-header').last().show();
}

function addShowLess($,v){
$(v).nextUntil('li.nav-header').last().after(
$('<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){
e.preventDefault();
e.stopPropagation();
$(v).nextUntil('li.nav-header').filter(function(i){return !$(this).find('input').is(':checked');}).slice(max).hide();
$(v).nextUntil('li.nav-header').last().remove();
$(v).scrollMinimal(3000);
addShowMore($,v);
})
);
$(v).nextUntil('li.nav-header').last().show();
}

$.each($('.nav-quickfilter li.nav-header'),function(i,v){
if ($(v).nextUntil('li.nav-header').size()>max) {
$(v).nextUntil('li.nav-header').filter(function(i){return !$(this).find('input').is(':checked');}).slice(max).hide();
addShowMore($,v);
}
});

$('.nav-quickfilter li.nav-header').on('click',function(e) {
e.preventDefault();
e.stopPropagation();
$('.nav-quickfilter li.nav-header i').toggleClass('icon-chevron-right');
$('.nav-quickfilter li.nav-header i').toggleClass('icon-chevron-left');
$('#left-side').toggleClass('col-md-2');
$('#left-side').toggleClass('col-md-4');
$('#content-block').toggleClass('col-md-10');
$('#content-block').toggleClass('col-md-8');
});
})(jQuery)
1 change: 1 addition & 0 deletions xadmin/templates/xadmin/base_site.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<p>{% trans "You don't have permission to edit anything." %}</p>
{% endif %}
{% endblock %}
{% view_block 'left_navbar' %}
</div>

<div id="content-block" class="col-sm-11 col-md-10">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<ul class="well nav nav-pills nav-stacked nav-quickfilter hide-sm">
{% for spec in cl.quickfilter.filter_specs %}{{ spec|safe }}{% endfor %}
</ul>
10 changes: 10 additions & 0 deletions xadmin/templates/xadmin/filters/quickfilter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% load i18n %}
<li class="nav-header ">{{title}} <i class="icon-chevron-right pull-right"></i></li>
{% for choice in choices %}
<li class="filter-multiselect">
<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 }}">
<input class="filter-col-1" type="checkbox" {% if choice.selected %} checked="checked"{% endif %}>
<span class="filter-col-2">{{ choice.display }}</span>
</a>
</li>
{% endfor %}

0 comments on commit 5270606

Please sign in to comment.