Skip to content

Commit

Permalink
Fixes.
Browse files Browse the repository at this point in the history
  • Loading branch information
runekaagaard committed Sep 30, 2021
1 parent b3ca10d commit 8811beb
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 56 deletions.
118 changes: 113 additions & 5 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
This Django app makes it easier to create custom filters in the change list of
Django Admin and supplies a `TreeFilter` and a `DateRangeFilter` too. Se below.

## About this branch ##
## Updating to python3.9 and django3.2.7

Install with:

pip install git+git://github.com/runekaagaard/django-admin-filtrate@b3ca10ddaa4eb3912e32ff4561fb3e6be298b68b

## Updating to 1.4-1.7 support ##

There are no release yet, but a version tested with 1.4 and 1.7 can be installed with pip like:

Expand All @@ -15,10 +21,112 @@ Major differences in this version is:
- Uses the new builtin Django Filter classes.
- No need to register with `filtrate.register_filter`.

## Installation
## Updating to 1.3 support ##
I will write proper docs, things are getting messy here, but here is the
lowdown.

## Docs for 1.2 version ##

### New settings required ###
As I found out, you can't reliably convert the django date formats to
Datepicker formats. So this commit introduces these two new settings:

FILTRATE = {
# See http://jqueryui.com/demos/datepicker/#localization.
'datepicker_region': 'en-GB',
# See http://docs.jquery.com/UI/Datepicker/formatDate.
'datepicker_date_format': 'yy-mm-dd',
}

So if the above defaults does not suit you, you have to change them your self.
Check out the Datepicker documentation to see how to use them.

### lookup_allowed() ###
Django 1.2.4 introduces restrictions on which lookups that can be queried
in the url, so at the moment the end user are responsible for
checking for those, as in this example:

class CaseAdmin(admin.ModelAdmin):
list_filter = ['client']

def lookup_allowed(self, key, *args, **kwargs):
if 'client__start_date' in key:
return True
else:
return super(CaseAdmin, self).lookup_allowed(key, *args, **kwargs)

### Undefined Media() class bug ###
Time and my Python meta-fu is running out, and I couldn't fix it
so its not neccessary to define an empty Media() class as in:

class CaseAdmin(admin.ModelAdmin):
class Media():
pass
## The FiltrateFilter ##
The base class that adds support for custom html in the content of the filter
and for using `Media()` classes.

## TreeFilter ##
A recursive tree filter using the excellent library http://www.jstree.com/.

### Example ###
```python
# The Filter.
from filtrate.filters import TreeFilter
from itertools import groupby
class CompanyDepartmentFilter(TreeFilter):
field_name = "client__department__id__in"

def get_title(self):
return 'By Department'

def get_tree(self):
from company.models import Department
qs = Department.objects.all().order_by('company_order', 'company')
return groupby(qs, lambda obj: getattr(obj, 'company'))

# The model.
from filtrate import register_filter
class Case(Model):
...
client = models.ForeignKey(Client)
register_filter(client, CompanyDepartmentFilter)
...
```

## DateRangeFilter ##
Filters results in a given date range using the jQueryUI datepicker plugin.

### Example ###
```python
# The Filter.
from filtrate.filters import DateRangeFilter

class CaseLicenseStartDateFilter(DateRangeFilter):
field_name = 'caselicense__start_date'

def get_title(self):
return "By license start date"

# The model.
from filtrate import register_filter
class Case(Model):
...
caselicense = models.ForeignKey(Licence)
register_filter(caselicense, CaseLicenseStartDateFilter)
...
```

## Installation ##

Stub.
* Clone the repo and symlink or copy the "filtrate" folder to your apps folder.
* Add `filtrate` to your installed apps, before `django.contrib.admin`.
* Add the "filtrate/templates" folder to your template folders.

## Usage
### Static files ###

Stub.
FlexSelect requires "django.contrib.staticfiles" installed to work out of the
box. If it is not then the js and css files must be installed manually.
Read more about "django.contrib.staticfiles" at
https://docs.djangoproject.com/en/1.3/ref/contrib/staticfiles/.
113 changes: 62 additions & 51 deletions filtrate/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,30 @@ def __init__(self, request, params, model, model_admin):
self.model_admin = model_admin
super(FiltrateFilter, self).__init__(request, params, model,
model_admin)

class Media:
js = ('filtrate/js/filtrate.js',)
css = {'all': ('filtrate/css/filtrate.css',)}
js = ('filtrate/js/filtrate.js', )
css = {'all': ('filtrate/css/filtrate.css', )}

def _add_media(self, model_admin):
def _get_media(obj):
return Media(media=getattr(obj, 'Media', None))
media = (_get_media(model_admin) + _get_media(FiltrateFilter)
+ _get_media(self))

media = (_get_media(model_admin) + _get_media(FiltrateFilter) +
_get_media(self))

for name in MEDIA_TYPES:
setattr(model_admin.Media, name, getattr(media, "_" + name))

def _form_duplicate_getparams(self, omitted_fields):
"""Replicates the get parameters as hidden form fields."""
s = '<input type="hidden" name="%s" value="%s"/>'
_omitted_fields = tuple(omitted_fields) + ('e',)
_omitted_fields = tuple(omitted_fields) + ('e', )
_keys = list(self.request.GET.keys())
return "".join([s % (k, v) for k, v in _keys
if k not in _omitted_fields])
return "".join([
s % (k, self.request.GET[k]) for k in _keys
if k not in _omitted_fields
])

def lookups(self, request, model_admin):
"""
Expand All @@ -67,13 +69,13 @@ def choices(self, cl):
'title': self.get_title(self.request),
'content': self.get_content(self.request),
}]

def get_title(self, request):
"""
Change the title dynamically.
"""
return self.title

def get_content(self, request):
"""
The content part of the filter in html.
Expand All @@ -93,45 +95,49 @@ def queryset(self, request, queryset):

class DateRangeFilter(FiltrateFilter):
class Media:
js = (
'filtrate/js/daterangefilter.js',
)
js = ('filtrate/js/daterangefilter.js', )

if settings.FILTRATE['include_jquery']:
js = (
'//ajax.googleapis.com/ajax/libs/jqueryui/1.8.14/jquery-ui.min.js',
'//ajax.googleapis.com/ajax/libs/jqueryui/1.8.14/i18n/jquery-ui-i18n.min.js',
) + js
css = {'all': ('//ajax.googleapis.com/ajax/libs/jqueryui/1.8.14/themes/flick/jquery-ui.css',)}

) + js
css = {
'all':
('//ajax.googleapis.com/ajax/libs/jqueryui/1.8.14/themes/flick/jquery-ui.css',
)
}

def _get_form(self, field_name):
"""
Returns form with from and to fields. The '__alt' fields are alternative
fields with the correct non localized dateform needed for Django,
fields with the correct non localized dateform needed for Django,
handled by jsTree.
"""
from_name = self.parameter_name + '__gte'
to_name = self.parameter_name + '__lte'

display_widget = Input(attrs={'class': 'filtrate_date'})
hidden_widget = HiddenInput(attrs={'class': 'filtrate_date_hidden'})

def add_fields(fields, name, label):
fields[name + '__alt'] = f.CharField(label=label,
widget=display_widget, required=False)
fields[name + '__alt'] = f.CharField(label=label,
widget=display_widget,
required=False)
fields[name] = f.CharField(widget=hidden_widget, required=False)

def add_data(data, name, request):
date = request.GET.get(name)

if date:
data[name + '__alt'] = date

class DateRangeForm(f.Form):
def __init__(self, *args, **kwargs):
super(DateRangeForm, self).__init__(*args, **kwargs)
add_fields(self.fields, from_name, _('From'))
add_fields(self.fields, to_name, _('To'))

data = {}
add_data(data, from_name, self.request)
add_data(data, to_name, self.request)
Expand All @@ -151,47 +157,52 @@ def get_content(self, request):
%(get_params)s
</form>
""" % ({
'form': form.as_p(),
'submit': _('Apply filter'),
'datepicker_region': settings.FILTRATE['datepicker_region'],
'datepicker_date_format': settings.FILTRATE['datepicker_date_format'],
'get_params': self._form_duplicate_getparams(list(form.fields.keys())),
'form':
form.as_p(),
'submit':
_('Apply filter'),
'datepicker_region':
settings.FILTRATE['datepicker_region'],
'datepicker_date_format':
settings.FILTRATE['datepicker_date_format'],
'get_params':
self._form_duplicate_getparams(list(form.fields.keys())),
}))


class TreeFilter(FiltrateFilter):
"""
A tree filter for models. Uses the jsTree jQuery plugin found at
A tree filter for models. Uses the jsTree jQuery plugin found at
http://www.jstree.com/ in the frontend.
Overiding classes needs to implement `parameter_name`, `title`, and
`get_tree()`.
"""

INCLUDE = 1 # The selected nodes are included in the query with ".filter()".
EXCLUDE = 2 # The selected nodes are excluded in the query with ".exclude()".
INCLUDE = 1 # The selected nodes are included in the query with ".filter()".
EXCLUDE = 2 # The selected nodes are excluded in the query with ".exclude()".
query_mode = INCLUDE

# The keyword argument used in the Django ORM query. If None it defaults to
# the parameter_name.
query_name = None
query_name = None

def __init__(self, request, params, model, model_admin):
super(TreeFilter, self).__init__(request, params, model, model_admin)
if self.value() is not None:
self.selected_nodes = list(map(int, self.value().split(',')))
else:
self.selected_nodes = []

class Media:
js = (
'filtrate/js/jstree/jquery.jstree.js',
'filtrate/js/filtertree.js',
)

def _tree_to_json(self, tree):
"""Recusively walks through the tree and generate json in a format
suitable for jsTree."""
suitable for jsTree."""
def parse_tree(tree, cur_tree):
for node in tree:
if type(node) == type(tuple()):
Expand All @@ -208,16 +219,16 @@ def parse_tree(tree, cur_tree):
title = force_text(node)
cur_tree.append({
"attr": {
"obj_id": node.pk,
"obj_id": node.pk,
"is_selected": node.pk in self.selected_nodes,
},
},
'data': force_text(node),
})

json_tree = []
parse_tree(tree, json_tree)
return json.dumps(json_tree)

def get_content(self, request):
"""Return html for entire filter."""
return mark_safe("""
Expand All @@ -231,14 +242,14 @@ def get_content(self, request):
</form>
</div>
""" % (self._tree_to_json(self.get_tree()), self.parameter_name,
self._form_duplicate_getparams((self.parameter_name,))))
self._form_duplicate_getparams((self.parameter_name, ))))

def get_tree(self):
"""
Must return a tree of model instances as a tuple of objects or tuples
Must return a tree of model instances as a tuple of objects or tuples
as:
( # Root level.
obj1,
obj1,
obj2,
( # Nested level.
obj3,
Expand All @@ -248,7 +259,7 @@ def get_tree(self):
obj4,
),
),
)
)
"""
raise NotImplementedError

Expand Down

0 comments on commit 8811beb

Please sign in to comment.