├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── dist └── django-grappelli-filters-0.2.tar.gz ├── docs_img └── screenshot.png ├── grappelli_filters ├── __init__.py ├── admin.py ├── filters.py ├── models.py ├── static │ └── grappelli_filters │ │ ├── filter.css │ │ └── filter.js ├── templates │ └── grappelli_filters │ │ ├── related_autocomplete.html │ │ └── search.html ├── tests.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pot 3 | *.pyc 4 | local_settings.py 5 | django_grappelli_filters.egg-info 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include grappelli_filters/static * 4 | recursive-include grappelli_filters/templates * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | django-grappelli-filters 2 | ==================================== 3 | 4 | Autocomplete filter for Grappelli 5 | 6 | v 0.2.1 (usable, but only if you know why) 7 | 8 | Filter for Foreign key and ManyToMany relations with AJAX autocomplete. Reuses features from Grappelli, works nicely along other filters and with both standard and sidebar filter template... 9 | 10 | ![Screenshot](docs_img/screenshot.png) 11 | 12 | ## Installation 13 | 14 | Put `grappelli-filters` in your `PYTHONPATH`. 15 | 16 | Add `'grappelli_filters'` to `INSTALLED_APPS` 17 | 18 | ## Filters 19 | 20 | ### Related Autocomplete Filter 21 | 22 | This filter is similar to a regular foreign key field filter, with two differences: 23 | 24 | * it has a nice AJAX-powered autocomplete UI (straight from Grappelli) 25 | * does not load all possible filter values in HTML - good for situations with many of related objects 26 | 27 | ##### Usage 28 | 29 | Configure Grappelli autocomplete feature as described [here](https://django-grappelli.readthedocs.org/en/latest/customization.html#autocomplete-lookups). Both Model method and `SETTINGS` value will work fine. For the inpatient, here is the `SETTINGS` value: 30 | 31 | GRAPPELLI_AUTOCOMPLETE_SEARCH_FIELDS = { 32 | "myapp": { 33 | "mycategory": ("id__iexact", "name__icontains",), 34 | } 35 | } 36 | 37 | 38 | In `admin.py` add: 39 | 40 | from grappelli_filters import RelatedAutocompleteFilter, FiltersMixin 41 | 42 | class MyModelAdmin(FiltersMixin, admin.ModelAdmin): 43 | list_filter = ( ... ('field_name', RelatedAutocompleteFilter), ... ) 44 | 45 | 46 | ### Search Filter 47 | 48 | This filter allows string searches over a single field. Several filters combined provide better control over the resulting list then does the built-in django-admin `search_fields` feature. 49 | 50 | ##### Usage 51 | 52 | In admin.py add: 53 | 54 | from grappelli_filters SearchFilter 55 | 56 | class MyModelAdmin(admin.ModelAdmin): 57 | list_filter = ( ... ('field_name', SearchFilter), ... ) 58 | 59 | 60 | ### Case-sensitive Search Filter 61 | 62 | Similar to Search Filter, but searches are case-sensitive. 63 | 64 | 65 | ##### Usage 66 | 67 | Use `SearchFilterC` instead of `SearchFilter` 68 | 69 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-grappelli-filters 2 | ======================== 3 | 4 | Autocomplete filter for Grappelli 5 | 6 | v 0.2.1 (usable, but only if you know why) 7 | 8 | Filter for Foreign key and ManyToMany relations with AJAX autocomplete. 9 | Reuses features from Grappelli, works nicely along other filters and 10 | with both standard and sidebar filter template. 11 | 12 | .. figure:: docs_img/screenshot.png 13 | :alt: Screenshot 14 | 15 | Installation 16 | ------------ 17 | 18 | Put ``grappelli-filters`` in your ``PYTHONPATH``. 19 | 20 | Add ``'grappelli_filters'`` to ``INSTALLED_APPS`` 21 | 22 | Filters 23 | ------- 24 | 25 | Related Autocomplete Filter 26 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 27 | 28 | This filter is similar to a regular foreign key field filter, with two 29 | differences: 30 | 31 | - it has a nice AJAX-powered autocomplete UI (straight from Grappelli) 32 | - does not load all possible filter values in HTML - good for 33 | situations with many of related objects 34 | 35 | Usage 36 | ''''' 37 | 38 | Configure Grappelli autocomplete feature as described `here`_. Both 39 | Model method and ``SETTINGS`` value will work fine. For the inpatient, 40 | here is the ``SETTINGS`` value: 41 | 42 | :: 43 | 44 | GRAPPELLI_AUTOCOMPLETE_SEARCH_FIELDS = { 45 | "myapp": { 46 | "mycategory": ("id__iexact", "name__icontains",), 47 | } 48 | } 49 | 50 | In ``admin.py`` add: 51 | 52 | :: 53 | 54 | from grappelli_filters import RelatedAutocompleteFilter, FiltersMixin 55 | 56 | class MyModelAdmin(FiltersMixin, admin.ModelAdmin): 57 | list_filter = ( ... ('field_name', RelatedAutocompleteFilter), ... ) 58 | 59 | 60 | Search Filter 61 | ~~~~~~~~~~~~~ 62 | 63 | This filter allows string searches over a single field. Several filters 64 | combined provide better control over the resulting list then does the 65 | built-in django-admin ``search_fields`` feature. 66 | 67 | Usage 68 | ''''' 69 | 70 | In admin.py add: 71 | 72 | :: 73 | 74 | from grappelli_filters SearchFilter 75 | 76 | class MyModelAdmin(admin.ModelAdmin): 77 | list_filter = ( ... ('field_name', SearchFilter), ... ) 78 | 79 | 80 | Case-sensitive Search Filter 81 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 82 | 83 | Similar to Search Filter, but searches are case-sensitive. 84 | 85 | Usage 86 | ''''' 87 | 88 | Use ``SearchFilterC`` instead of ``SearchFilter`` 89 | 90 | .. _here: https://django-grappelli.readthedocs.org/en/latest/customization.html#autocomplete-lookups 91 | -------------------------------------------------------------------------------- /dist/django-grappelli-filters-0.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frnhr/django-grappelli-filters/ff6ba22eb1073ec9353b2c025475c797db03c39c/dist/django-grappelli-filters-0.2.tar.gz -------------------------------------------------------------------------------- /docs_img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frnhr/django-grappelli-filters/ff6ba22eb1073ec9353b2c025475c797db03c39c/docs_img/screenshot.png -------------------------------------------------------------------------------- /grappelli_filters/__init__.py: -------------------------------------------------------------------------------- 1 | from .filters import RelatedAutocompleteFilter, SearchFilter, SearchFilterC 2 | from .admin import FiltersMixin 3 | -------------------------------------------------------------------------------- /grappelli_filters/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.templatetags.static import static 3 | 4 | 5 | class FiltersMixin( admin.ModelAdmin ): 6 | 7 | class Media: 8 | js = (static('grappelli_filters/filter.js'),) 9 | css = { 10 | 'all': (static('grappelli_filters/filter.css'),), 11 | } 12 | -------------------------------------------------------------------------------- /grappelli_filters/filters.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.contenttypes.models import ContentType 3 | 4 | 5 | class AbstractFieldListFilter(admin.FieldListFilter): 6 | tempalte = '' 7 | filter_parameter = None 8 | url_parameter = None 9 | 10 | def get_parameter_name(self, field_path): 11 | """ Query parameter name for the URL """ 12 | if self.url_parameter: 13 | return self.url_parameter 14 | raise NotImplementedError 15 | 16 | def __init__(self, field, request, params, model, model_admin, field_path): 17 | self.parameter_name = self.get_parameter_name(field_path) 18 | super(AbstractFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path) 19 | 20 | def has_output(self): 21 | """ Whether to show filter """ 22 | return True 23 | 24 | def lookups(self, request, model_admin): 25 | """ Not using lookups """ 26 | return () 27 | 28 | def choices(self, cl): 29 | """ Not used, but required by admin_list_filter template tag """ 30 | return () 31 | 32 | def queryset(self, request, queryset): 33 | """ Does the actual filtering """ 34 | if self.used_param(): 35 | filter_parameter = self.filter_parameter if self.filter_parameter else self.parameter_name 36 | return queryset.filter(**{filter_parameter: self.used_param()}) 37 | 38 | def expected_parameters(self): 39 | """ 40 | Returns the list of parameter names that are expected from the 41 | request's query string and that will be used by this filter. 42 | """ 43 | return [self.parameter_name] 44 | 45 | def used_param(self): 46 | """ Value from the query string""" 47 | return self.used_parameters.get(self.parameter_name, '') 48 | 49 | 50 | class RelatedAutocompleteFilter(AbstractFieldListFilter): 51 | template = 'grappelli_filters/related_autocomplete.html' 52 | model = None 53 | 54 | def get_parameter_name(self, field_path): 55 | if self.url_parameter: 56 | field_path = self.url_parameter 57 | return u'{0}__id__exact'.format(field_path) 58 | 59 | def __init__(self, field, request, params, model, model_admin, field_path): 60 | super(RelatedAutocompleteFilter, self).__init__(field, request, params, model, model_admin, field_path) 61 | if self.model: 62 | content_type = ContentType.objects.get_for_model(self.model) 63 | else: 64 | content_type = ContentType.objects.get_for_model(field.rel.to) 65 | self.grappelli_trick = u'/{app_label}/{model_name}/'.format( 66 | app_label=content_type.app_label, 67 | model_name=content_type.model 68 | ) 69 | 70 | 71 | class SearchFilter(AbstractFieldListFilter): 72 | template = 'grappelli_filters/search.html' 73 | 74 | def get_parameter_name(self, field_path): 75 | return u'{0}__icontains'.format(field_path) 76 | 77 | 78 | class SearchFilterC(SearchFilter): 79 | """ Case-sensitive serach filter """ 80 | 81 | def get_parameter_name(self, field_path): 82 | return u'{0}__contains'.format(field_path) 83 | 84 | -------------------------------------------------------------------------------- /grappelli_filters/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /grappelli_filters/static/grappelli_filters/filter.css: -------------------------------------------------------------------------------- 1 | #grp-filters .grp-autocomplete-wrapper-fk { 2 | display: block; 3 | width: 100%; 4 | } 5 | 6 | #grp-filters .grp-autocomplete-wrapper-fk .ui-autocomplete-input { 7 | padding-right: 25px; 8 | } 9 | 10 | #grp-filters .grp-autocomplete-wrapper-fk .grp-related-remove { 11 | padding: 0; 12 | margin: 0 -24px 0 0; 13 | 14 | -moz-border-radius-topright: 3px; 15 | -webkit-border-top-right-radius: 3px; 16 | border-top-right-radius: 3px; 17 | -moz-border-radius-bottomright: 3px; 18 | -webkit-border-bottom-right-radius: 3px; 19 | border-bottom-right-radius: 3px; 20 | } 21 | 22 | #grp-filters .grp-autocomplete-wrapper-fk .grp-loader { 23 | right: 0; 24 | 25 | -moz-border-radius-topright: 3px; 26 | -webkit-border-top-right-radius: 3px; 27 | border-top-right-radius: 3px; 28 | -moz-border-radius-bottomright: 3px; 29 | -webkit-border-bottom-right-radius: 3px; 30 | border-bottom-right-radius: 3px; 31 | } 32 | 33 | .ui-menu li.ui-menu-item a.ui-state-focus { 34 | color: #fff; 35 | border: 1px solid #333; 36 | background: #444; 37 | } 38 | -------------------------------------------------------------------------------- /grappelli_filters/static/grappelli_filters/filter.js: -------------------------------------------------------------------------------- 1 | 2 | (function($) { 3 | 4 | $(document).ready(function(){ 5 | 6 | 7 | //////////////////////////////////////////////////////////////////////// 8 | // prepare functions 9 | //////////////////////////////////////////////////////////////////////// 10 | 11 | 12 | // handle value change 13 | var urlParams = {}; 14 | (function () { 15 | var match, 16 | pl = /\+/g, // Regex for replacing addition symbol with a space 17 | search = /([^&=]+)=?([^&]*)/g, 18 | decode = function (s) { 19 | return decodeURIComponent(s.replace(pl, " ")); 20 | }, 21 | query = window.location.search.substring(1); 22 | 23 | while (match = search.exec(query)) 24 | urlParams[decode(match[1])] = decode(match[2]); 25 | })(); 26 | 27 | 28 | //@TODO make array of filter_queries instead of setting up multiple timers 29 | var detect_change = function(filter_query) { 30 | setTimeout(function(){detect_change(filter_query);}, 100); 31 | var new_val = $('#id_'+filter_query).val(); 32 | if (detect_change_last_val[filter_query] == -1) { 33 | detect_change_last_val[filter_query] = new_val; 34 | }else { 35 | if (detect_change_last_val[filter_query] !== new_val) { 36 | detect_change_last_val[filter_query] = new_val; 37 | urlParams[filter_query] = new_val; 38 | 39 | if (!urlParams[filter_query]) 40 | delete urlParams[filter_query]; 41 | 42 | url = buildUrl(window.location.href.split('?')[0], urlParams); 43 | window.location = url; 44 | } 45 | } 46 | }; 47 | var detect_change_last_val = []; 48 | 49 | 50 | // autocomplete drop-down varies in width, make is fixed - don't seem to be a smarter way :/ 51 | 52 | var fix_dropdown_width = function() { 53 | setTimeout(fix_dropdown_width, 100); 54 | $('ul.ui-autocomplete').width(200); 55 | }; 56 | fix_dropdown_width(); 57 | 58 | 59 | 60 | // url helper 61 | 62 | function buildUrl(url, parameters) { 63 | var qs = ""; 64 | for (var key in parameters) { 65 | if (!parameters.hasOwnProperty(key)) continue; 66 | var value = parameters[key]; 67 | qs += encodeURIComponent(key) + "=" + encodeURIComponent(value) + "&"; 68 | } 69 | if (qs.length > 0) { 70 | qs = qs.substring(0, qs.length - 1); //chop-off trailing "&" 71 | url = url + "?" + qs; 72 | } 73 | return url; 74 | } 75 | 76 | 77 | 78 | //////////////////////////////////////////////////////////////////////// 79 | // find all autocomplete filters and apply stuff 80 | //////////////////////////////////////////////////////////////////////// 81 | 82 | $('.grp-filter input.autocomplete').each(function() { 83 | var $this = $(this); 84 | var filter_query = $this.attr('name'); 85 | $("#id_"+filter_query).grp_autocomplete_fk({ 86 | lookup_url:"/grappelli/lookup/related/", 87 | autocomplete_lookup_url:"/grappelli/lookup/autocomplete/" 88 | }); 89 | $("#id_"+filter_query+'-autocomplete').prop('placeholder', 'All'); 90 | detect_change_last_val[filter_query] = -1; 91 | detect_change(filter_query); 92 | }); 93 | }); 94 | 95 | })(django.jQuery); 96 | 97 | -------------------------------------------------------------------------------- /grappelli_filters/templates/grappelli_filters/related_autocomplete.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |
4 |
5 |
6 | 7 |
8 | 9 | grappelli_trick 10 |
11 |
12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /grappelli_filters/templates/grappelli_filters/search.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |
4 |
5 |
6 | 7 |
8 | 9 |
10 | 11 |
12 |
13 | 14 | 66 |
67 | -------------------------------------------------------------------------------- /grappelli_filters/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /grappelli_filters/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | README = open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding='utf-8').read() 5 | 6 | # allow setup.py to be run from any path 7 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 8 | 9 | setup( 10 | name='django-grappelli-filters', 11 | version='0.2.1', 12 | packages=['grappelli_filters'], 13 | include_package_data=True, 14 | install_requires=['django', 'django-grappelli', ], 15 | license='Unlicense', 16 | description='Additional filters for Djagno Grappelli admin', 17 | long_description=README, 18 | url='https://github.com/frnhr/django-grappelli-filters/', 19 | author='Fran Hrzenjak', 20 | author_email='fran@changeset.hr', 21 | classifiers=[ 22 | 'Environment :: Web Environment', 23 | 'Framework :: Django', 24 | 'Intended Audience :: Developers', 25 | 'License :: Freeware', 26 | 'Operating System :: OS Independent', 27 | 'Programming Language :: Python', 28 | # Replace these appropriately if you are stuck on Python 2. 29 | 'Programming Language :: Python :: 2', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Topic :: Internet :: WWW/HTTP', 32 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 33 | ], 34 | ) 35 | --------------------------------------------------------------------------------