Skip to content

Commit

Permalink
Merge pull request #62 from farhan0581/pre_release
Browse files Browse the repository at this point in the history
New Version Changes
  • Loading branch information
farhan0581 authored Jun 29, 2021
2 parents 9ee0f81 + e6ebfe5 commit 8a0ea8c
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 58 deletions.
82 changes: 47 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,49 @@
[![PyPI version](https://badge.fury.io/py/django-admin-autocomplete-filter.svg?kill_cache=1)](https://badge.fury.io/py/django-admin-autocomplete-filter)


Django Admin Autocomplete Filter
================================
A simple Django app to render list filters in django admin using autocomplete widget. This app is heavily inspired from [dal-admin-filters.](https://github.com/shamanu4/dal_admin_filters)
A simple Django app to render list filters in django admin using an autocomplete widget. This app is heavily inspired by [dal-admin-filters.](https://github.com/shamanu4/dal_admin_filters)


Overview:
---------

Django comes preshipped with an admin panel which is a great utility to create quick CRUD's.
The django 2.0 came with much needed [autocomplete_fields](https://docs.djangoproject.com/en/2.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields "autocomplete_fields") which uses select2 widget that comes with a search feature that loads the options asynchronously.
We can use this in django admin list filter.
Version 2.0 came with a much needed [`autocomplete_fields`](https://docs.djangoproject.com/en/2.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields "autocomplete_fields") property which uses a select2 widget to load the options asynchronously. We leverage this in `django-admin-list-filter`.



Requirements:
-------------

Requires Django version >= 2.0


Features:
-------------

* Custom search view/endpoint ([more details](#functionality-to-provide-custom-view-for-search))
* `list_filter` Filter Factory support ([more details](#shortcut-for-creating-filters))
* Custom widget text ([more details](#customizing-widget-text))
* Support for [Grappelli](https://grappelliproject.com/)


Installation:
-------------
You can install it via pip or to get the latest version clone this repo.

You can install it via pip. To get the latest version clone this repo.

```shell script
pip install django-admin-autocomplete-filter
```

Add `admin_auto_filters` to your `INSTALLED_APPS` inside settings.py of your project.


Usage:
------

Let's say we have following models:
```python
from django.db import models
Expand All @@ -41,7 +56,8 @@ class Album(models.Model):
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
cover = models.CharField(max_length=256, null=True, default=None)
```
And you would like to filter results in Album Admin on the basis of artist, then you can define `search fields` in Artist and then define filter as:

And you would like to filter results in `AlbumAdmin` on the basis of `artist`. You need to define `search fields` in `Artist` and then define filter like this:

```python
from django.contrib import admin
Expand All @@ -60,14 +76,6 @@ class ArtistAdmin(admin.ModelAdmin):

class AlbumAdmin(admin.ModelAdmin):
list_filter = [ArtistFilter]

"""
defining this class is required for AutocompleteFilter
it's a bug and I am working on it.
"""
class Media:
pass

# ...
```

Expand All @@ -77,12 +85,13 @@ After following these steps you may see the filter as:

![](https://raw.githubusercontent.com/farhan0581/django-admin-autocomplete-filter/master/admin_auto_filters/media/screenshot2.png)

Functionality to provide custom view for search:
------------------------------------------------

Now you can also register your custom view instead of using django admin's search_results to control the results in the autocomplete. For this you will need to create your custom view and register the url in your admin class as shown below:
Functionality to provide a custom view for search:
--------------------------------------------------

You can also register your custom view instead of using Django admin's `search_results` to control the results in the autocomplete. For this you will need to create your custom view and register the URL in your admin class as shown below:

In your views.py:
In your `views.py`:

```python
from admin_auto_filters.views import AutocompleteJsonView
Expand All @@ -97,7 +106,7 @@ class CustomSearchView(AutocompleteJsonView):
return queryset
```

After this, register this view in your admin class as:
After this, register this view in your admin class:

```python
from django.contrib import admin
Expand All @@ -107,9 +116,6 @@ from django.urls import path
class AlbumAdmin(admin.ModelAdmin):
list_filter = [ArtistFilter]

class Media:
pass

def get_urls(self):
urls = super().get_urls()
custom_urls = [
Expand All @@ -119,7 +125,7 @@ class AlbumAdmin(admin.ModelAdmin):
return custom_urls + urls
```

Finally just tell the filter class to use this new view as:
Finally, just tell the filter class to use this new view:

```python
from django.shortcuts import reverse
Expand All @@ -134,11 +140,12 @@ class ArtistFilter(AutocompleteFilter):
return reverse('admin:custom_search')
```


Shortcut for creating filters:
------------------------------

It is now possible to use the `AutocompleteFilterFactory` shortcut to create filters
on the fly, as shown below. Nested relations are supported, with
It's also possible to use the `AutocompleteFilterFactory` shortcut to create
filters on the fly, as shown below. Nested relations are supported too, with
no need to specify the model.

```python
Expand All @@ -155,14 +162,15 @@ class AlbumAdmin(admin.ModelAdmin):
"""As above..."""
```

Customizing Widget Text

Customizing widget text
-----------------------

It is also possible to customize the text displayed in the filter
widget, to use something other than `str(obj)`. This needs to be
configured for both the dropdown endpoint and the widget itself.
You can customize the text displayed in the filter widget, to use something
other than `str(obj)`. This needs to be configured for both the dropdown
endpoint and the widget itself.

In your views.py, override display_text:
In your `views.py`, override `display_text`:

```python
from admin_auto_filters.views import AutocompleteJsonView
Expand All @@ -178,8 +186,10 @@ class CustomSearchView(AutocompleteJsonView):
"""As above..."""
```

Then use either of two options to customize the text. Option one is to
specify the form_field in an AutocompleteFilter in your admin.py:
Then use either of two options to customize the text.

Option one is to specify the form_field in an AutocompleteFilter in your
`admin.py`:

```python
from django import forms
Expand Down Expand Up @@ -209,8 +219,8 @@ class AlbumAdmin(admin.ModelAdmin):
"""As above..."""
```

Or use option two. In an AutocompleteFilterFactory in your admin.py,
add a *label_by* argument:
Option two is to use an AutocompleteFilterFactory in your `admin.py`
add a `label_by` argument:

```python
from django.contrib import admin
Expand All @@ -226,14 +236,16 @@ class AlbumAdmin(admin.ModelAdmin):
"""As above..."""
```


Contributing:
------------
This project is a combined effort of a lot of selfless developers who try to make things easier. Your contribution is most welcome.

Please make a PR to the branch `pre_release` and make sure your branch does not have any conflicts and clearly mention the problems or improvements your PR is addressing.
This project is a combined effort of a lot of selfless developers who try to make things easier. Your contribution is most welcome.

Please make a pull-request to the branch `pre_release`, make sure your branch does not have any conflicts, and clearly mention the problems or improvements your PR is addressing.


License:
--------

Django Admin Autocomplete Filter is an Open Source project licensed under the terms of the GNU GENERAL PUBLIC LICENSE.
15 changes: 11 additions & 4 deletions admin_auto_filters/filters.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from django.contrib.admin.widgets import AutocompleteSelect as Base
from django import forms
from django.contrib import admin
from django.db.models import ManyToOneRel
from django.db.models import ForeignObjectRel
from django.db.models.constants import LOOKUP_SEP # this is '__'
from django.db.models.fields.related_descriptors import ReverseManyToOneDescriptor, ManyToManyDescriptor
from django.forms.widgets import Media, MEDIA_TYPES, media_property
from django.shortcuts import reverse

from django import VERSION as DJANGO_VERSION

class AutocompleteSelect(Base):
def __init__(self, rel, admin_site, attrs=None, choices=(), using=None, custom_url=None):
Expand Down Expand Up @@ -50,7 +50,10 @@ def __init__(self, request, params, model, model_admin):
if self.rel_model:
model = self.rel_model

remote_field = model._meta.get_field(self.field_name).remote_field
if DJANGO_VERSION >= (3, 2):
remote_field = model._meta.get_field(self.field_name)
else:
remote_field = model._meta.get_field(self.field_name).remote_field

widget = AutocompleteSelect(remote_field,
model_admin.admin_site,
Expand Down Expand Up @@ -85,9 +88,13 @@ def get_queryset_for_field(model, name):
related_model = field_desc.rel.related_model if field_desc.reverse else field_desc.rel.model
elif isinstance(field_desc, ReverseManyToOneDescriptor):
related_model = field_desc.rel.related_model # look at field_desc.related_manager_cls()?
elif isinstance(field_desc, ManyToOneRel):
elif isinstance(field_desc, ForeignObjectRel):
# includes ManyToOneRel, ManyToManyRel
# also includes OneToOneRel - not sure how this would be used
related_model = field_desc.related_model
else:
# primarily for ForeignKey/ForeignKeyDeferredAttribute
# also includes ForwardManyToOneDescriptor, ForwardOneToOneDescriptor, ReverseOneToOneDescriptor
return field_desc.get_queryset()
return related_model.objects.get_queryset()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function search_to_hash() {
function hash_to_search(h) {
var search = String("?");
for (var k in h) {
if (k === '') { continue; } // ignore invalid inputs, e.g. '?&=value'
for (var i = 0; i < h[k].length; i++) {
search += search == "?" ? "" : "&";
search += encodeURIComponent(k) + "=" + encodeURIComponent(h[k][i]);
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

setup(
name='django-admin-autocomplete-filter',
version='0.6.1',
version='0.5',
packages=find_packages(),
include_package_data=True,
description='A simple Django app to render list filters in django admin using autocomplete widget',
Expand Down
46 changes: 43 additions & 3 deletions tests/testapp/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ class PersonFoodFilter(AutocompleteFilter):
parameter_name = 'person'


class PersonLeastFavFoodFilter(AutocompleteFilter):
title = 'least favorite food of person (manual)'
field_name = 'people_with_this_least_fav_food'
rel_model = Food
parameter_name = 'people_with_this_least_fav_food'


class CuratorsFilter(AutocompleteFilter):
title = 'curators (manual)'
field_name = 'curators'
Expand All @@ -42,6 +49,20 @@ class FriendFilter(AutocompleteFilter):
parameter_name = 'best_friend'


class TwinFilter(AutocompleteFilter):
title = 'twin (manual)'
field_name = 'twin'
rel_model = Person
parameter_name = 'twin'


class RevTwinFilter(AutocompleteFilter):
title = 'reverse twin (manual)'
field_name = 'rev_twin'
rel_model = Person
parameter_name = 'rev_twin'


class FriendFriendFilter(AutocompleteFilter):
title = 'best friend\'s best friend (manual)'
field_name = 'best_friend'
Expand Down Expand Up @@ -100,6 +121,13 @@ class RevPersonFoodFilter(AutocompleteFilter):
parameter_name = 'person__favorite_food'


class RevCollectionFilter(AutocompleteFilter):
title = 'collections as curator (manual)'
field_name = 'collection'
rel_model = Person
parameter_name = 'collection'


class AuthorFilter(AutocompleteFilter):
title = 'author (manual)'
field_name = 'author'
Expand Down Expand Up @@ -139,6 +167,10 @@ class PersonInline(admin.TabularInline):
model = Person


class PersonFavoriteFoodInline(PersonInline):
fk_name = 'favorite_food'


class BookInline(admin.TabularInline):
extra = 0
fields = ['isbn', 'title']
Expand All @@ -163,14 +195,16 @@ def get_list_filter(self, request):
@admin.register(Food)
class FoodAdmin(CustomAdmin):
fields = ['id', 'name']
inlines = [PersonInline]
inlines = [PersonFavoriteFoodInline]
list_display = ['id', 'name']
list_display_links = ['name']
list_filter = [
PersonFoodFilter,
PersonLeastFavFoodFilter,
]
list_filter_auto = [
AutocompleteFilterFactory('favorite food of person (auto)', 'person'),
AutocompleteFilterFactory('least favorite food of person (auto)', 'people_with_this_least_fav_food'),
]
ordering = ['id']
readonly_fields = ['id']
Expand Down Expand Up @@ -200,30 +234,36 @@ class CollectionAdmin(CustomAdmin):

@admin.register(Person)
class PersonAdmin(CustomAdmin):
autocomplete_fields = ['best_friend', 'siblings', 'favorite_food', 'curated_collections']
fields = ['id', 'name', 'best_friend', 'siblings', 'favorite_food', 'curated_collections']
autocomplete_fields = ['best_friend', 'twin', 'siblings', 'favorite_food', 'curated_collections']
fields = ['id', 'name', 'best_friend', 'twin', 'siblings', 'favorite_food', 'curated_collections']
inlines = [BookInline]
list_display = ['id', 'name']
list_display_links = ['name']
list_filter = [
FriendFilter,
TwinFilter,
RevTwinFilter,
FriendFriendFilter,
FriendFoodFilter,
SiblingsFilter,
FoodFilter,
BestFriendOfFilter,
AuthoredFilter,
RevPersonFoodFilter,
RevCollectionFilter,
]
list_filter_auto = [
AutocompleteFilterFactory('best friend (auto)', 'best_friend'),
AutocompleteFilterFactory('twin (auto)', 'twin'),
AutocompleteFilterFactory('reverse twin (auto)', 'rev_twin'),
AutocompleteFilterFactory('best friend\'s best friend (auto)', 'best_friend__best_friend'),
AutocompleteFilterFactory('best friend\'s favorite food (auto)', 'best_friend__favorite_food'),
AutocompleteFilterFactory('siblings (auto)', 'siblings'),
AutocompleteFilterFactory('food (auto)', 'favorite_food', viewname='admin:foods_that_are_favorites', label_by='alternate_name'),
AutocompleteFilterFactory('best friend of (auto)', 'person'),
AutocompleteFilterFactory('authored (auto)', 'book'),
AutocompleteFilterFactory('best friend of person with fav food (auto)', 'person__favorite_food'),
AutocompleteFilterFactory('collections as curator (auto)', 'collection'),
# AutocompleteFilterFactory('curated_collections (auto)', 'curated_collections'), # does not work...
]
ordering = ['id']
Expand Down
3 changes: 3 additions & 0 deletions tests/testapp/fixtures/fixture.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
"fields": {
"best_friend": 1,
"favorite_food": 1,
"least_favorite_food": 3,
"name": "Bob",
"siblings": [
1,
Expand All @@ -179,6 +180,8 @@
"fields": {
"best_friend": 1,
"favorite_food": 3,
"least_favorite_food": 2,
"twin": 1,
"name": "Carol"
},
"model": "testapp.person",
Expand Down
Loading

0 comments on commit 8a0ea8c

Please sign in to comment.