diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2e2dc3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,137 @@ + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python + +config/settings/development.py +config/settings/test.py +/media/ +.tm_properties +!.gitkeep \ No newline at end of file diff --git a/djaa_list_filter/admin.py b/djaa_list_filter/admin.py new file mode 100644 index 0000000..1b69654 --- /dev/null +++ b/djaa_list_filter/admin.py @@ -0,0 +1,82 @@ +# pylint: disable=R0903,R0913 + +from django import forms +from django.contrib import admin +from django.contrib.admin.widgets import AutocompleteSelect +from django.utils.translation import ugettext_lazy as _ + +# from django.core.exceptions import ImproperlyConfigured + + +class AjaxAutocompleteSelectWidget(AutocompleteSelect): + def __init__(self, *args, **kwargs): + self.qs_target_value = kwargs.pop('qs_target_value') + self.model_admin = kwargs.pop('model_admin') + self.model = kwargs.pop('model') + self.field_name = kwargs.pop('field_name') + kwargs['admin_site'] = self.model_admin.admin_site + kwargs['rel'] = getattr(self.model, self.field_name).field.remote_field + super().__init__(*args, **kwargs) + + def render(self, name, value, attrs=None, renderer=None): + rendered = super().render(name, value, attrs, renderer) + return ( + f'
' + ) + + +class AjaxAutocompleteListFilter(admin.RelatedFieldListFilter): + title = _('list filter') + parameter_name = '%s__%s__exact' + template = 'djaa_list_filter/admin/filter/autocomplete_list_filter.html' + + def __init__(self, field, request, params, model, model_admin, field_path): + super().__init__(field, request, params, model, model_admin, field_path) + + qs_target_value = self.parameter_name % (field.name, model._meta.pk.name) + queryset = getattr(model, field.name).get_queryset() + widget = AjaxAutocompleteSelectWidget( + model_admin=model_admin, model=model, field_name=field.name, qs_target_value=qs_target_value + ) + + class AutocompleteForm(forms.Form): + autocomplete_field = forms.ModelChoiceField(queryset=queryset, widget=widget, required=False) + querystring_value = forms.CharField(widget=forms.HiddenInput()) + + autocomplete_field_initial_value = request.GET.get(qs_target_value, None) + initial_values = dict(querystring_value=request.GET.urlencode()) + if autocomplete_field_initial_value: + initial_values.update(autocomplete_field=autocomplete_field_initial_value) + self.autocomplete_form = AutocompleteForm(initial=initial_values) + + +class AjaxAutocompleteListFilterModelAdmin(admin.ModelAdmin): + def get_list_filter(self, request): + list_filter = list(super().get_list_filter(request)) + autocomplete_list_filter = self.get_autocomplete_list_filter() + if autocomplete_list_filter: + for field in autocomplete_list_filter: + list_filter.append((field, AjaxAutocompleteListFilter)) + return list_filter + + def get_autocomplete_list_filter(self): + return list(getattr(self, 'autocomplete_list_filter', [])) + + class Media: + js = [ + 'admin/js/vendor/jquery/jquery.js', + 'admin/js/vendor/select2/select2.full.js', + 'admin/js/vendor/select2/i18n/tr.js', + 'admin/js/jquery.init.js', + 'admin/js/autocomplete.js', + 'djaa_list_filter/admin/js/autocomplete_list_filter.js', + ] + css = { + 'screen': [ + 'admin/css/vendor/select2/select2.css', + 'admin/css/autocomplete.css', + 'djaa_list_filter/admin/css/autocomplete_list_filter.css', + ] + } diff --git a/djaa_list_filter/apps.py b/djaa_list_filter/apps.py new file mode 100644 index 0000000..4bdc9c9 --- /dev/null +++ b/djaa_list_filter/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class DjaaListFilterConfig(AppConfig): + name = 'djaa_list_filter' diff --git a/djaa_list_filter/static/djaa_list_filter/admin/css/autocomplete_list_filter.css b/djaa_list_filter/static/djaa_list_filter/admin/css/autocomplete_list_filter.css new file mode 100644 index 0000000..4d24846 --- /dev/null +++ b/djaa_list_filter/static/djaa_list_filter/admin/css/autocomplete_list_filter.css @@ -0,0 +1,3 @@ +.select2-container--admin-autocomplete { + width: 100% !important; +} \ No newline at end of file diff --git a/djaa_list_filter/static/djaa_list_filter/admin/js/autocomplete_list_filter.js b/djaa_list_filter/static/djaa_list_filter/admin/js/autocomplete_list_filter.js new file mode 100644 index 0000000..deab094 --- /dev/null +++ b/djaa_list_filter/static/djaa_list_filter/admin/js/autocomplete_list_filter.js @@ -0,0 +1,30 @@ +function handle_querystring_and_redirect(querystring_value, qs_target_value, selection) { + var required_queryset = []; + for(const field_eq_value of querystring_value.split("&")){ + var [field, value] = field_eq_value.split("="); + if (field != qs_target_value){ + required_queryset.push(field_eq_value) + } + } + if (selection.length > 0) { + required_queryset.push(qs_target_value + "=" + selection); + } + window.location.href = "?" + required_queryset.join("&"); +} + +django.jQuery(document).ready(function(){ + django.jQuery(".ajax-autocomplete-select-widget-wrapper select").on('select2:unselect', function(e){ + var qs_target_value = django.jQuery(this).parent().data("qs-target-value"); + var querystring_value = django.jQuery(this).closest("form").find('input[name="querystring_value"]').val(); + handle_querystring_and_redirect(querystring_value, qs_target_value, ""); + }); + + django.jQuery(".ajax-autocomplete-select-widget-wrapper select").on('change', function(e, choice){ + var selection = django.jQuery(e.target).val() || ""; + var qs_target_value = django.jQuery(this).parent().data("qs-target-value"); + var querystring_value = django.jQuery(this).closest("form").find('input[name="querystring_value"]').val(); + if(selection.length > 0){ + handle_querystring_and_redirect(querystring_value, qs_target_value, selection); + } + }); +}); diff --git a/djaa_list_filter/templates/djaa_list_filter/admin/filter/autocomplete_list_filter.html b/djaa_list_filter/templates/djaa_list_filter/admin/filter/autocomplete_list_filter.html new file mode 100644 index 0000000..9b6c340 --- /dev/null +++ b/djaa_list_filter/templates/djaa_list_filter/admin/filter/autocomplete_list_filter.html @@ -0,0 +1,9 @@ +{% load i18n staticfiles %} +