├── .github └── workflows │ └── release.yaml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── autocompletefilter ├── __init__.py ├── admin.py ├── filters.py ├── static │ └── admin │ │ └── js │ │ └── autocomplete_filter.js └── templates │ └── admin │ └── filter_autocomplete.html ├── setup.cfg └── setup.py /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | pypi-publish: 10 | runs-on: ubuntu-latest 11 | if: startsWith(github.ref, 'refs/tags/') && github.repository == 'julianwachholz/django-autocompletefilter' 12 | environment: 13 | name: pypi 14 | url: https://pypi.org/p/django-autocompletefilter 15 | permissions: 16 | contents: write 17 | id-token: write 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v5 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install -U pip 29 | python -m pip install -U build setuptools twine wheel 30 | 31 | - name: Build package 32 | run: | 33 | python -m build 34 | twine check dist/* 35 | 36 | - name: Upload packages 37 | uses: pypa/gh-action-pypi-publish@v1.10.2 38 | 39 | - name: Create release 40 | uses: softprops/action-gh-release@v2 41 | with: 42 | generate_release_notes: true 43 | files: dist/* 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.egg-info 3 | 4 | build/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Julian Wachholz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include autocompletefilter/static * 4 | recursive-include autocompletefilter/templates * 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-autocompletefilter 2 | ========================= 3 | 4 | A django application that lets you use the built in autocomplete function of the 5 | django admin to filter in admin list views by foreign key relations. 6 | 7 | .. image:: https://pbs.twimg.com/media/DgmzYLbW4AA9oL3.jpg:large 8 | 9 | Usage 10 | ----- 11 | 12 | #. Install the package, for example from PyPi:: 13 | 14 | pip install django-autocompletefilter 15 | 16 | #. Add ``autocompletefilter`` to your ``INSTALLED_APPS`` setting. 17 | 18 | #. Create and register a model admin for the model you want to filter by. 19 | Ensure it has ``search_fields`` specified for autocomplete to work. 20 | 21 | #. In your second model admin, use the ``AutocompleteFilterMixin`` on your class and 22 | add the desired foreign key attribute to filter by to the ``list_filter`` 23 | items by using the AutocompleteListFilter class:: 24 | 25 | from autocompletefilter.admin import AutocompleteFilterMixin 26 | from autocompletefilter.filters import AutocompleteListFilter 27 | 28 | class FooAdmin(AutocompleteFilterMixin, admin.ModelAdmin): 29 | list_filter = ( 30 | ('bar', AutocompleteListFilter), 31 | ) 32 | 33 | 34 | Status of this project 35 | ---------------------- 36 | 37 | This project is currently using a rather hacky way to implement this. 38 | Caution is advised when using it. 39 | 40 | Using multiple autocomplete filters on the same page does work. 41 | 42 | Currently only tested on Python 3.6 43 | 44 | 45 | Contributing 46 | ------------ 47 | 48 | All suggestions are welcome. Especially about ways to make this cleaner. 49 | 50 | 51 | Common issues 52 | ------------- 53 | 54 | - **Reverse for '__autocomplete' not found.** 55 | 56 | You must register a model admin with ``search_fields`` for the model you want to look up. 57 | 58 | - **The results could not be loaded.** 59 | 60 | You likely forgot to specify ``search_fields`` on your model admin for the model you want to look up. 61 | -------------------------------------------------------------------------------- /autocompletefilter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/julianwachholz/django-autocompletefilter/727914c12036bc5172aec51658f5d53890aaba2c/autocompletefilter/__init__.py -------------------------------------------------------------------------------- /autocompletefilter/admin.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.contrib.admin.widgets import SELECT2_TRANSLATIONS 3 | from django.utils.datastructures import OrderedSet 4 | from django.utils.translation import get_language 5 | 6 | 7 | class AutocompleteFilterMixin: 8 | @property 9 | def media(self): 10 | media = super().media 11 | 12 | i18n_file = None 13 | i18n_name = SELECT2_TRANSLATIONS.get(get_language(), None) 14 | if i18n_name: 15 | i18n_file = "admin/js/vendor/select2/i18n/%s.js" % i18n_name 16 | 17 | extra_js = [ 18 | "admin/js/vendor/jquery/jquery.js", 19 | "admin/js/vendor/select2/select2.full.js", 20 | ] 21 | if i18n_file: 22 | extra_js.append(i18n_file) 23 | extra_js.extend( 24 | [ 25 | "admin/js/jquery.init.js", 26 | "admin/js/autocomplete.js", 27 | "admin/js/autocomplete_filter.js", 28 | ] 29 | ) 30 | extra_css = [ 31 | "admin/css/vendor/select2/select2.css", 32 | "admin/css/autocomplete.css", 33 | ] 34 | if django.VERSION >= (2, 2, 0, "final", 0): 35 | media._js_lists.append(extra_js) 36 | media._css_lists.append({"screen": extra_css}) 37 | else: 38 | media._js = OrderedSet(extra_js + media._js) 39 | media._css.setdefault("screen", []) 40 | media._css["screen"].extend(extra_css) 41 | return media 42 | -------------------------------------------------------------------------------- /autocompletefilter/filters.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.contrib.admin.filters import RelatedFieldListFilter 3 | from django.urls import NoReverseMatch, reverse 4 | 5 | 6 | def get_request(): 7 | """Walk the stack up to find a request in a context variable.""" 8 | import inspect 9 | 10 | frame = None 11 | try: 12 | for f in inspect.stack()[1:]: 13 | frame = f[0] 14 | code = frame.f_code 15 | if code.co_varnames and "context" in code.co_varnames: 16 | return frame.f_locals["context"]["request"] 17 | finally: 18 | del frame 19 | 20 | 21 | class AutocompleteListFilter(RelatedFieldListFilter): 22 | """Admin list_filter using autocomplete select 2 widget.""" 23 | 24 | template = "admin/filter_autocomplete.html" 25 | 26 | def has_output(self): 27 | """Show the autocomplete filter at all times.""" 28 | return True 29 | 30 | @staticmethod 31 | def get_admin_namespace(): 32 | request = get_request() 33 | return request.resolver_match.namespace 34 | 35 | def get_url(self): 36 | if django.VERSION > (3, 2): 37 | return self.get_generic_url() 38 | 39 | remote_model = self.field.related_model 40 | args = ( 41 | self.get_admin_namespace(), 42 | remote_model._meta.app_label, 43 | remote_model._meta.model_name, 44 | ) 45 | return reverse("%s:%s_%s_autocomplete" % args) 46 | 47 | def get_generic_url(self): 48 | try: 49 | return reverse("admin:autocomplete") 50 | except NoReverseMatch: 51 | pass 52 | 53 | namespace = self.get_admin_namespace() 54 | return reverse("%s:autocomplete" % namespace) 55 | 56 | def field_choices(self, field, request, model_admin): 57 | # Do not populate the field choices with a huge queryset 58 | return [] 59 | 60 | def choices(self, changelist): 61 | """ 62 | Get choices for the widget. 63 | 64 | Yields a single choice populated with template context variables. 65 | 66 | """ 67 | url = self.get_url() 68 | 69 | placeholder = "PKVAL" 70 | query_string = changelist.get_query_string( 71 | {self.lookup_kwarg: placeholder}, [self.lookup_kwarg_isnull] 72 | ) 73 | 74 | lookup_display = None 75 | if self.lookup_val: 76 | if django.VERSION >= (5, 0): 77 | instance = self.field.related_model.objects.get(pk=self.lookup_val[-1]) 78 | else: 79 | instance = self.field.related_model.objects.get(pk=self.lookup_val) 80 | lookup_display = str(instance) 81 | 82 | model = self.field.model 83 | 84 | yield { 85 | "url": url, 86 | "selected": self.lookup_val, 87 | "selected_display": lookup_display, 88 | "query_string": query_string, 89 | "query_string_placeholder": placeholder, 90 | "query_string_all": changelist.get_query_string( 91 | {}, [self.lookup_kwarg, self.lookup_kwarg_isnull] 92 | ), 93 | # Data attrs required for Django 3.2+ 94 | "app_label": model._meta.app_label, 95 | "model_name": model._meta.model_name, 96 | "field_name": self.field.name, 97 | } 98 | -------------------------------------------------------------------------------- /autocompletefilter/static/admin/js/autocomplete_filter.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 'use strict'; 3 | 4 | $.fn.djangoAdminAutocompleteFilter = function(options) { 5 | $.each(this, function(i, element) { 6 | var template = element.dataset.querystring; 7 | var placeholder = element.dataset.querystringPlaceholder; 8 | $(element).on('change', function(event) { 9 | var url; 10 | if (event.target.value) { 11 | url = template.replace(placeholder, event.target.value); 12 | } else { 13 | url = element.dataset.querystringAll; 14 | } 15 | window.location = url; 16 | }); 17 | }); 18 | }; 19 | 20 | $(function() { 21 | $('.admin-autocomplete-filter').djangoAdminAutocompleteFilter(); 22 | }); 23 | }(django.jQuery)); 24 | -------------------------------------------------------------------------------- /autocompletefilter/templates/admin/filter_autocomplete.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |

{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}

3 | 4 | 30 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-autocompletefilter 3 | version = 0.0.9 4 | description = Django ModelAdmin list_filter with autocomplete widget. 5 | long_description = file: README.rst 6 | 7 | author = Julian Wachholz 8 | author_email = julian@wachholz.ch 9 | url = https://github.com/julianwachholz/django-autocompletefilter 10 | license = MIT 11 | license_files = LICENSE 12 | keywords = django django-admin autocomplete auto-complete filter select2 13 | classifiers = 14 | Development Status :: 4 - Beta 15 | Environment :: Web Environment 16 | Framework :: Django 17 | Framework :: Django :: 2.0 18 | Framework :: Django :: 2.1 19 | Framework :: Django :: 2.2 20 | Framework :: Django :: 3.0 21 | Framework :: Django :: 3.1 22 | Framework :: Django :: 3.2 23 | Framework :: Django :: 4.0 24 | Framework :: Django :: 4.1 25 | Framework :: Django :: 4.2 26 | Framework :: Django :: 5.0 27 | Framework :: Django :: 5.1 28 | Intended Audience :: Developers 29 | License :: OSI Approved :: MIT License 30 | Programming Language :: Python 31 | Programming Language :: Python :: 3 32 | Programming Language :: Python :: 3 :: Only 33 | 34 | [options] 35 | python_requires = >=3.4 36 | install_requires = 37 | Django >= 2.0 38 | packages = find: 39 | include_package_data = true 40 | 41 | [flake8] 42 | exclude = .git,__pycache__,.venv,build,dist 43 | max-line-length = 119 44 | # don't force docstrings 45 | ignore = D100,D101,D102,D103,D104,D105,D106,D107 46 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup 4 | 5 | setup() 6 | --------------------------------------------------------------------------------