From 8811bebb0f1985dfef599f65bcab923ba92ac9cc Mon Sep 17 00:00:00 2001 From: Rune Kaagaard Date: Thu, 30 Sep 2021 13:47:27 +0200 Subject: [PATCH] Fixes. --- README.markdown | 118 ++++++++++++++++++++++++++++++++++++++++++-- filtrate/filters.py | 113 +++++++++++++++++++++++------------------- 2 files changed, 175 insertions(+), 56 deletions(-) diff --git a/README.markdown b/README.markdown index b0178ff..d398436 100644 --- a/README.markdown +++ b/README.markdown @@ -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: @@ -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/. \ No newline at end of file diff --git a/filtrate/filters.py b/filtrate/filters.py index 357504b..2e84c97 100644 --- a/filtrate/filters.py +++ b/filtrate/filters.py @@ -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 = '' - _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): """ @@ -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. @@ -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) @@ -151,47 +157,52 @@ def get_content(self, request): %(get_params)s """ % ({ - '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()): @@ -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(""" @@ -231,14 +242,14 @@ def get_content(self, request): """ % (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, @@ -248,7 +259,7 @@ def get_tree(self): obj4, ), ), - ) + ) """ raise NotImplementedError