django-searchkit 1.0__tar.gz → 1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {django_searchkit-1.0/django_searchkit.egg-info → django_searchkit-1.2}/PKG-INFO +9 -24
  2. {django_searchkit-1.0 → django_searchkit-1.2}/README.md +6 -18
  3. {django_searchkit-1.0 → django_searchkit-1.2/django_searchkit.egg-info}/PKG-INFO +9 -24
  4. {django_searchkit-1.0 → django_searchkit-1.2}/django_searchkit.egg-info/SOURCES.txt +1 -0
  5. django_searchkit-1.2/django_searchkit.egg-info/requires.txt +3 -0
  6. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/admin.py +1 -1
  7. {django_searchkit-1.0 → django_searchkit-1.2}/searchkit/__version__.py +1 -1
  8. {django_searchkit-1.0 → django_searchkit-1.2}/searchkit/admin.py +4 -4
  9. django_searchkit-1.2/searchkit/filters.py +28 -0
  10. django_searchkit-1.2/searchkit/forms/__init__.py +5 -0
  11. django_searchkit-1.2/searchkit/forms/search.py +62 -0
  12. django_searchkit-1.2/searchkit/forms/searchkit.py +154 -0
  13. {django_searchkit-1.0 → django_searchkit-1.2}/searchkit/forms/utils.py +44 -15
  14. django_searchkit-1.2/searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
  15. {django_searchkit-1.0 → django_searchkit-1.2}/searchkit/models.py +2 -4
  16. django_searchkit-1.2/searchkit/templatetags/searchkit.py +20 -0
  17. django_searchkit-1.2/searchkit/tests.py +400 -0
  18. django_searchkit-1.2/searchkit/urls.py +7 -0
  19. django_searchkit-1.2/searchkit/views.py +23 -0
  20. {django_searchkit-1.0 → django_searchkit-1.2}/setup.py +2 -5
  21. django_searchkit-1.0/django_searchkit.egg-info/requires.txt +0 -2
  22. django_searchkit-1.0/searchkit/filters.py +0 -31
  23. django_searchkit-1.0/searchkit/forms/__init__.py +0 -3
  24. django_searchkit-1.0/searchkit/forms/search.py +0 -42
  25. django_searchkit-1.0/searchkit/forms/searchkit.py +0 -189
  26. django_searchkit-1.0/searchkit/templatetags/searchkit.py +0 -47
  27. django_searchkit-1.0/searchkit/tests.py +0 -250
  28. django_searchkit-1.0/searchkit/urls.py +0 -8
  29. django_searchkit-1.0/searchkit/views.py +0 -30
  30. {django_searchkit-1.0 → django_searchkit-1.2}/LICENCE +0 -0
  31. {django_searchkit-1.0 → django_searchkit-1.2}/MANIFEST.in +0 -0
  32. {django_searchkit-1.0 → django_searchkit-1.2}/django_searchkit.egg-info/dependency_links.txt +0 -0
  33. {django_searchkit-1.0 → django_searchkit-1.2}/django_searchkit.egg-info/top_level.txt +0 -0
  34. {django_searchkit-1.0 → django_searchkit-1.2}/django_searchkit.egg-info/zip-safe +0 -0
  35. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/__init__.py +0 -0
  36. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/asgi.py +0 -0
  37. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/management/__init__.py +0 -0
  38. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/management/commands/__init__.py +0 -0
  39. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/management/commands/createtestdata.py +0 -0
  40. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/migrations/0001_initial.py +0 -0
  41. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/migrations/__init__.py +0 -0
  42. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/models.py +0 -0
  43. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/settings.py +0 -0
  44. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/urls.py +0 -0
  45. {django_searchkit-1.0 → django_searchkit-1.2}/example/example/wsgi.py +0 -0
  46. {django_searchkit-1.0 → django_searchkit-1.2}/searchkit/__init__.py +0 -0
  47. {django_searchkit-1.0 → django_searchkit-1.2}/searchkit/apps.py +0 -0
  48. {django_searchkit-1.0 → django_searchkit-1.2}/searchkit/forms/fields.py +0 -0
  49. {django_searchkit-1.0 → django_searchkit-1.2}/searchkit/migrations/0001_initial.py +0 -0
  50. {django_searchkit-1.0 → django_searchkit-1.2}/searchkit/migrations/__init__.py +0 -0
  51. {django_searchkit-1.0 → django_searchkit-1.2}/searchkit/templatetags/__init__.py +0 -0
  52. {django_searchkit-1.0 → django_searchkit-1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-searchkit
3
- Version: 1.0
3
+ Version: 1.2
4
4
  Summary: Finally a real searchkit for django!
5
5
  Home-page: https://github.com/thomst/django-searchkit
6
6
  Author: Thomas Leichtfuß
@@ -9,8 +9,6 @@ License: BSD License
9
9
  Platform: OS Independent
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Framework :: Django
12
- Classifier: Framework :: Django :: 3.1
13
- Classifier: Framework :: Django :: 3.2
14
12
  Classifier: Framework :: Django :: 4.0
15
13
  Classifier: Framework :: Django :: 4.1
16
14
  Classifier: Framework :: Django :: 4.2
@@ -22,8 +20,6 @@ Classifier: License :: OSI Approved :: BSD License
22
20
  Classifier: Operating System :: OS Independent
23
21
  Classifier: Programming Language :: Python
24
22
  Classifier: Programming Language :: Python :: 3
25
- Classifier: Programming Language :: Python :: 3.7
26
- Classifier: Programming Language :: Python :: 3.8
27
23
  Classifier: Programming Language :: Python :: 3.9
28
24
  Classifier: Programming Language :: Python :: 3.10
29
25
  Classifier: Programming Language :: Python :: 3.11
@@ -32,8 +28,9 @@ Classifier: Topic :: Software Development
32
28
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
33
29
  Description-Content-Type: text/markdown
34
30
  License-File: LICENCE
35
- Requires-Dist: Django>=3.1
31
+ Requires-Dist: Django>=4.0
36
32
  Requires-Dist: django-picklefield>=2.0
33
+ Requires-Dist: django-modeltree
37
34
  Dynamic: author
38
35
  Dynamic: author-email
39
36
  Dynamic: classifier
@@ -50,8 +47,8 @@ Dynamic: summary
50
47
 
51
48
  [<img src="https://github.com/thomst/django-searchkit/actions/workflows/ci.yml/badge.svg">](https://github.com/thomst/django-searchkit/)
52
49
  [<img src="https://coveralls.io/repos/github/thomst/django-searchkit/badge.svg?branch=main">](https://coveralls.io/github/thomst/django-searchkit?branch=main)
53
- [<img src="https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue">](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue)
54
- [<img src="https://img.shields.io/badge/django-3.1%20%7C%203.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange">](https://img.shields.io/badge/django-3.1%20%7C%203.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange)
50
+ [<img src="https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-blue">](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-blue)
51
+ [<img src="https://img.shields.io/badge/django-4.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange">](https://img.shields.io/badge/django-4.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange)
55
52
 
56
53
 
57
54
  ## Description
@@ -99,20 +96,8 @@ class MyModelAdmin(admin.ModelAdmin):
99
96
  ## Usage
100
97
 
101
98
  1. Open the admin changelist of your Model.
102
- 2. Click "Add filter" on the Searchkit filter.
103
- 3. Choose the Model you want to filter.
99
+ 2. Click the "Add filter" button of the Searchkit filter.
100
+ 3. Give your Filter a name.
104
101
  4. Configure as many filter rules as you want.
105
- 5. Click "Save and apply"
106
-
107
-
108
- ## TODO
109
-
110
- - Limit the choices of the model field by models that should be searchable.
111
- - Add an apply button to the search edit page to be able to use a search without
112
- saving it.
113
- - Coming from the search edit page the filtering should be done by an id__in url
114
- parameter, not by an search parameter as it is used by the searchkit filter.
115
- - Preselect the right model in the model field when coming from a models
116
- changelist by the "Add filter" button.
117
- - Add a public field for searches and only offer public searches in the
118
- searchkit filter.
102
+ 5. Click "Save and apply".
103
+ 6. Reuse your filter whenever you want using the Searchkit filter section.
@@ -2,8 +2,8 @@
2
2
 
3
3
  [<img src="https://github.com/thomst/django-searchkit/actions/workflows/ci.yml/badge.svg">](https://github.com/thomst/django-searchkit/)
4
4
  [<img src="https://coveralls.io/repos/github/thomst/django-searchkit/badge.svg?branch=main">](https://coveralls.io/github/thomst/django-searchkit?branch=main)
5
- [<img src="https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue">](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue)
6
- [<img src="https://img.shields.io/badge/django-3.1%20%7C%203.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange">](https://img.shields.io/badge/django-3.1%20%7C%203.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange)
5
+ [<img src="https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-blue">](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-blue)
6
+ [<img src="https://img.shields.io/badge/django-4.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange">](https://img.shields.io/badge/django-4.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange)
7
7
 
8
8
 
9
9
  ## Description
@@ -51,20 +51,8 @@ class MyModelAdmin(admin.ModelAdmin):
51
51
  ## Usage
52
52
 
53
53
  1. Open the admin changelist of your Model.
54
- 2. Click "Add filter" on the Searchkit filter.
55
- 3. Choose the Model you want to filter.
54
+ 2. Click the "Add filter" button of the Searchkit filter.
55
+ 3. Give your Filter a name.
56
56
  4. Configure as many filter rules as you want.
57
- 5. Click "Save and apply"
58
-
59
-
60
- ## TODO
61
-
62
- - Limit the choices of the model field by models that should be searchable.
63
- - Add an apply button to the search edit page to be able to use a search without
64
- saving it.
65
- - Coming from the search edit page the filtering should be done by an id__in url
66
- parameter, not by an search parameter as it is used by the searchkit filter.
67
- - Preselect the right model in the model field when coming from a models
68
- changelist by the "Add filter" button.
69
- - Add a public field for searches and only offer public searches in the
70
- searchkit filter.
57
+ 5. Click "Save and apply".
58
+ 6. Reuse your filter whenever you want using the Searchkit filter section.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-searchkit
3
- Version: 1.0
3
+ Version: 1.2
4
4
  Summary: Finally a real searchkit for django!
5
5
  Home-page: https://github.com/thomst/django-searchkit
6
6
  Author: Thomas Leichtfuß
@@ -9,8 +9,6 @@ License: BSD License
9
9
  Platform: OS Independent
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Framework :: Django
12
- Classifier: Framework :: Django :: 3.1
13
- Classifier: Framework :: Django :: 3.2
14
12
  Classifier: Framework :: Django :: 4.0
15
13
  Classifier: Framework :: Django :: 4.1
16
14
  Classifier: Framework :: Django :: 4.2
@@ -22,8 +20,6 @@ Classifier: License :: OSI Approved :: BSD License
22
20
  Classifier: Operating System :: OS Independent
23
21
  Classifier: Programming Language :: Python
24
22
  Classifier: Programming Language :: Python :: 3
25
- Classifier: Programming Language :: Python :: 3.7
26
- Classifier: Programming Language :: Python :: 3.8
27
23
  Classifier: Programming Language :: Python :: 3.9
28
24
  Classifier: Programming Language :: Python :: 3.10
29
25
  Classifier: Programming Language :: Python :: 3.11
@@ -32,8 +28,9 @@ Classifier: Topic :: Software Development
32
28
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
33
29
  Description-Content-Type: text/markdown
34
30
  License-File: LICENCE
35
- Requires-Dist: Django>=3.1
31
+ Requires-Dist: Django>=4.0
36
32
  Requires-Dist: django-picklefield>=2.0
33
+ Requires-Dist: django-modeltree
37
34
  Dynamic: author
38
35
  Dynamic: author-email
39
36
  Dynamic: classifier
@@ -50,8 +47,8 @@ Dynamic: summary
50
47
 
51
48
  [<img src="https://github.com/thomst/django-searchkit/actions/workflows/ci.yml/badge.svg">](https://github.com/thomst/django-searchkit/)
52
49
  [<img src="https://coveralls.io/repos/github/thomst/django-searchkit/badge.svg?branch=main">](https://coveralls.io/github/thomst/django-searchkit?branch=main)
53
- [<img src="https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue">](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue)
54
- [<img src="https://img.shields.io/badge/django-3.1%20%7C%203.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange">](https://img.shields.io/badge/django-3.1%20%7C%203.2%20%7C%204.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange)
50
+ [<img src="https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-blue">](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11-blue)
51
+ [<img src="https://img.shields.io/badge/django-4.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange">](https://img.shields.io/badge/django-4.0%20%7C%204.1%20%7C%204.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange)
55
52
 
56
53
 
57
54
  ## Description
@@ -99,20 +96,8 @@ class MyModelAdmin(admin.ModelAdmin):
99
96
  ## Usage
100
97
 
101
98
  1. Open the admin changelist of your Model.
102
- 2. Click "Add filter" on the Searchkit filter.
103
- 3. Choose the Model you want to filter.
99
+ 2. Click the "Add filter" button of the Searchkit filter.
100
+ 3. Give your Filter a name.
104
101
  4. Configure as many filter rules as you want.
105
- 5. Click "Save and apply"
106
-
107
-
108
- ## TODO
109
-
110
- - Limit the choices of the model field by models that should be searchable.
111
- - Add an apply button to the search edit page to be able to use a search without
112
- saving it.
113
- - Coming from the search edit page the filtering should be done by an id__in url
114
- parameter, not by an search parameter as it is used by the searchkit filter.
115
- - Preselect the right model in the model field when coming from a models
116
- changelist by the "Add filter" button.
117
- - Add a public field for searches and only offer public searches in the
118
- searchkit filter.
102
+ 5. Click "Save and apply".
103
+ 6. Reuse your filter whenever you want using the Searchkit filter section.
@@ -35,6 +35,7 @@ searchkit/forms/search.py
35
35
  searchkit/forms/searchkit.py
36
36
  searchkit/forms/utils.py
37
37
  searchkit/migrations/0001_initial.py
38
+ searchkit/migrations/0002_rename_searchkitsearch_search.py
38
39
  searchkit/migrations/__init__.py
39
40
  searchkit/templatetags/__init__.py
40
41
  searchkit/templatetags/searchkit.py
@@ -0,0 +1,3 @@
1
+ Django>=4.0
2
+ django-picklefield>=2.0
3
+ django-modeltree
@@ -7,7 +7,7 @@ from .models import ModelB
7
7
  @admin.register(ModelA)
8
8
  class ModelAAdmin(admin.ModelAdmin):
9
9
  list_display = [f.name for f in ModelA._meta.fields]
10
- list_filter = [SearchkitFilter, 'chars_choices']
10
+ list_filter = [SearchkitFilter]
11
11
 
12
12
 
13
13
  @admin.register(ModelB)
@@ -13,4 +13,4 @@ Version 0.x should be considered a development version with an unstable API,
13
13
  and backwards compatibility is not guaranteed for minor versions.
14
14
  """
15
15
 
16
- __version__ = "1.0"
16
+ __version__ = "1.2"
@@ -1,14 +1,14 @@
1
1
  from django.contrib import admin
2
2
  from django.http import HttpResponseRedirect
3
3
  from django.urls import reverse
4
- from .models import SearchkitSearch
5
- from .forms import SearchkitSearchForm
4
+ from .models import Search
5
+ from .forms import SearchForm
6
6
  from .filters import SearchkitFilter
7
7
 
8
8
 
9
- @admin.register(SearchkitSearch)
9
+ @admin.register(Search)
10
10
  class SearchkitSearchAdmin(admin.ModelAdmin):
11
- form = SearchkitSearchForm
11
+ form = SearchForm
12
12
  list_display = ('name', 'contenttype', 'created_date')
13
13
 
14
14
  def get_url_for_applied_search(self, obj):
@@ -0,0 +1,28 @@
1
+ from django.contrib.admin import SimpleListFilter
2
+ from django.contrib.contenttypes.models import ContentType
3
+ from .models import Search
4
+
5
+
6
+ class SearchkitFilter(SimpleListFilter):
7
+ title = 'Searchkit Filter'
8
+ parameter_name = 'search'
9
+ template = 'searchkit/searchkit_filter.html'
10
+
11
+ def __init__(self, request, params, model, model_admin):
12
+ # We need the app_label and model as get parameter for the new search
13
+ # link.
14
+ self.searchkit_model = ContentType.objects.get_for_model(model)
15
+ super().__init__(request, params, model, model_admin)
16
+
17
+ def has_output(self):
18
+ return True
19
+
20
+ def lookups(self, request, model_admin):
21
+ searches = Search.objects.filter(contenttype=self.searchkit_model).order_by('-created_date')
22
+ return [(str(obj.id), obj.name) for obj in searches]
23
+
24
+ def queryset(self, request, queryset):
25
+ # Filter the queryset based on the selected SearchkitSearch object
26
+ if self.value():
27
+ search = Search.objects.get(id=int(self.value()))
28
+ return queryset.filter(**search.as_lookups())
@@ -0,0 +1,5 @@
1
+ from .search import SearchForm
2
+ from .searchkit import SearchkitModelForm
3
+ from .searchkit import BaseSearchkitFormSet
4
+ from .searchkit import BaseSearchkitForm
5
+ from .searchkit import searchkit_formset_factory
@@ -0,0 +1,62 @@
1
+ from django import forms
2
+ from django.utils.functional import cached_property
3
+ from django.contrib.contenttypes.models import ContentType
4
+ from ..models import Search
5
+ from .searchkit import SearchkitModelForm
6
+ from .searchkit import searchkit_formset_factory
7
+ from .utils import MediaMixin
8
+
9
+
10
+ class SearchForm(MediaMixin, forms.ModelForm):
11
+ """
12
+ Represents a SearchkitSearch model. Using a SearchkitFormSet for the data
13
+ json field.
14
+ """
15
+ class Meta:
16
+ model = Search
17
+ fields = ['name']
18
+
19
+ @cached_property
20
+ def searchkit_model(self):
21
+ if self.instance.pk:
22
+ return self.instance.contenttype.model_class()
23
+ elif self.searchkit_model_form.is_valid():
24
+ return self.searchkit_model_form.cleaned_data['searchkit_model'].model_class()
25
+ elif 'searchkit_model' in self.searchkit_model_form.initial:
26
+ value = self.searchkit_model_form.initial['searchkit_model']
27
+ try:
28
+ return self.searchkit_model_form.fields['searchkit_model'].clean(value).model_class()
29
+ except forms.ValidationError:
30
+ return None
31
+
32
+ @cached_property
33
+ def searchkit_model_form(self):
34
+ kwargs = dict(data=self.data or None, initial=self.initial or None)
35
+ if self.instance.pk:
36
+ kwargs['initial'] = dict(searchkit_model=self.instance.contenttype)
37
+ return SearchkitModelForm(**kwargs)
38
+
39
+ @cached_property
40
+ def formset(self):
41
+ """
42
+ A searchkit formset for the model.
43
+ """
44
+ kwargs = dict()
45
+ if self.searchkit_model and self.data:
46
+ kwargs = dict(data=self.data)
47
+ elif self.searchkit_model and self.instance.pk:
48
+ kwargs = dict(initial=self.instance.data)
49
+
50
+ extra = 0 if self.instance.pk else 1
51
+ formset = searchkit_formset_factory(model=self.searchkit_model, extra=extra)
52
+ return formset(**kwargs)
53
+
54
+ def is_valid(self):
55
+ return self.formset.is_valid() and self.searchkit_model_form.is_valid and super().is_valid()
56
+
57
+ def clean(self):
58
+ if self.searchkit_model_form.is_valid():
59
+ self.instance.contenttype = self.searchkit_model_form.cleaned_data['searchkit_model']
60
+ if self.formset.is_valid():
61
+ self.instance.data = self.formset.cleaned_data
62
+ return super().clean()
@@ -0,0 +1,154 @@
1
+ from django import forms
2
+ from django.utils.translation import gettext_lazy as _
3
+ from django.utils.functional import cached_property
4
+ from .utils import CssClassMixin, FIELD_PLAN, OPERATOR_DESCRIPTION
5
+ from .utils import SUPPORTED_FIELDS
6
+ from .utils import ModelTree
7
+ from .utils import MediaMixin
8
+ from .utils import get_searchable_models
9
+
10
+
11
+ class SearchkitModelForm(forms.Form):
12
+ """
13
+ Form to select a content type.
14
+ """
15
+ searchkit_model = forms.ModelChoiceField(
16
+ queryset=get_searchable_models(),
17
+ label=_('Model'),
18
+ empty_label=_('Select a Model'),
19
+ widget=forms.Select(attrs={
20
+ "class": CssClassMixin.reload_on_change_css_class,
21
+ "data-total-forms": 1,
22
+ }),
23
+ )
24
+
25
+
26
+ class BaseSearchkitForm(MediaMixin, CssClassMixin, forms.Form):
27
+ """
28
+ Searchkit form representing a model field lookup based on the field name,
29
+ the operator and one or two values.
30
+
31
+ The unbound form is composed of an index field (the count of the searchkit
32
+ form) and a choice field offering the names of the model fields.
33
+
34
+ The bound form is dynamically extended by the operator field or the operator and
35
+ the value field depending on the provided data
36
+
37
+ See the FIELD_PLAN variable for the logic of building the form.
38
+ """
39
+ model = None # Set by the formset factory.
40
+
41
+ def __init__(self, *args, **kwargs):
42
+ super().__init__(*args, **kwargs)
43
+ self.model_tree = ModelTree(self.model)
44
+ self.model_field = None
45
+ self.field_plan = None
46
+ self.operator = None
47
+ self._add_field_name_field()
48
+ lookup = self._preload_clean_data('field')
49
+ self.model_field = self._get_model_field(lookup)
50
+ self.field_plan = next(iter([p for t, p in FIELD_PLAN.items() if t(self.model_field)]))
51
+ self._add_operator_field()
52
+ self.operator = self._preload_clean_data('operator')
53
+ self._add_value_field()
54
+
55
+ @cached_property
56
+ def unprefixed_data(self):
57
+ data = dict()
58
+ for key, value in self.data.items():
59
+ if key.startswith(self.prefix):
60
+ data[key[len(self.prefix) + 1:]] = value
61
+ return data
62
+
63
+ def _preload_clean_data(self, field_name):
64
+ # Try the initial value first since it is already cleaned.
65
+ if self.initial and field_name in self.initial:
66
+ return self.initial[field_name]
67
+ # Otherwise look up the data dict.
68
+ elif field_name in self.unprefixed_data:
69
+ try:
70
+ # Do we have a valid value?
71
+ return self.fields[field_name].clean(self.unprefixed_data[field_name])
72
+ except forms.ValidationError:
73
+ return self.fields[field_name].choices[0][0]
74
+ else:
75
+ # At last simply return the first option which will be the selected
76
+ # one.
77
+ return self.fields[field_name].choices[0][0]
78
+
79
+ def _get_model_field(self, lookup):
80
+ path = lookup.split('__')
81
+ field_name = path[-1]
82
+ if path[:-1]:
83
+ model = self.model_tree.get('__'.join(path[:-1])).model
84
+ else:
85
+ model = self.model
86
+ return model._meta.get_field(field_name)
87
+
88
+ def _get_model_field_choices(self):
89
+ choices = list()
90
+ label_path = list()
91
+ for node in self.model_tree.iterate():
92
+ label_path.append
93
+ for model_field in node.model._meta.fields:
94
+ if not any(isinstance(model_field, f) for f in SUPPORTED_FIELDS):
95
+ continue
96
+ if node.is_root:
97
+ lookup = model_field.name
98
+ label = f'`{model_field.verbose_name}`'
99
+ else:
100
+ lookup = f'{node.field_path}__{model_field.name}'
101
+ get_field_name = lambda f: getattr(f, 'verbose_name', f.name)
102
+ label_path = [f'`{get_field_name(n.field)}` => <{n.model._meta.verbose_name}>' for n in node.path[1:]]
103
+ label = ".".join(label_path + [f'`{model_field.verbose_name}`'])
104
+ choices.append((lookup, label))
105
+ return choices
106
+
107
+ def _add_field_name_field(self):
108
+ initial = self.initial.get('field')
109
+ choices = self._get_model_field_choices()
110
+ field = forms.ChoiceField(label=_('Model field'), choices=choices, initial=initial)
111
+ field.widget.attrs.update({"class": self.reload_on_change_css_class})
112
+ self.fields['field'] = field
113
+
114
+ def _add_operator_field(self):
115
+ initial = self.initial.get('operator')
116
+ choices = [(o, OPERATOR_DESCRIPTION[o]) for o in self.field_plan.keys()]
117
+ field = forms.ChoiceField(label=_('Operator'), choices=choices, initial=initial)
118
+ field.widget.attrs.update({"class": self.reload_on_change_css_class})
119
+ self.fields['operator'] = field
120
+
121
+ def _add_value_field(self):
122
+ initial = self.initial.get('value')
123
+ field_class = self.field_plan[self.operator][0]
124
+ if getattr(field_class, 'choices', None) and getattr(self.model_field, 'choices', None):
125
+ field = field_class(choices=self.model_field.choices, initial=initial)
126
+ else:
127
+ field = field_class()
128
+ self.fields['value'] = field
129
+
130
+
131
+ class BaseSearchkitFormSet(CssClassMixin, forms.BaseFormSet):
132
+ """
133
+ Formset holding all searchkit forms.
134
+ """
135
+ template_name = "searchkit/searchkit.html"
136
+ template_name_div = "searchkit/searchkit.html"
137
+ model = None # Set by the formset factory.
138
+
139
+ def add_prefix(self, index):
140
+ return "%s-%s-%s-%s" % (self.prefix, self.model._meta.app_label, self.model._meta.model_name, index)
141
+
142
+ @classmethod
143
+ def get_default_prefix(self):
144
+ return "searchkit"
145
+
146
+
147
+ def searchkit_formset_factory(model, **kwargs):
148
+ form = type('SearchkitForm', (BaseSearchkitForm,), dict(model=model))
149
+ formset = type('SearchkitFormSet', (BaseSearchkitFormSet,), dict(model=model))
150
+ return forms.formset_factory(
151
+ form=form,
152
+ formset=formset,
153
+ **kwargs
154
+ )
@@ -1,8 +1,23 @@
1
+ from modeltree import ModelTree as BaseModelTree
2
+ from collections import OrderedDict
3
+ from django.apps import apps
4
+ from django.contrib import admin
5
+ from django.contrib.contenttypes.models import ContentType
1
6
  from django.db import models
7
+ from django.db.utils import OperationalError
2
8
  from django import forms
3
9
  from django.utils.translation import gettext_lazy as _
4
- from collections import OrderedDict
5
- from searchkit.forms import fields as searchkit_fields
10
+ from ..filters import SearchkitFilter
11
+ from . import fields as searchkit_fields
12
+
13
+
14
+ class ModelTree(BaseModelTree):
15
+ MAX_DEPTH = 3
16
+ FOLLOW_ACROSS_APPS = True
17
+ RELATION_TYPES = [
18
+ 'one_to_one',
19
+ 'many_to_one',
20
+ ]
6
21
 
7
22
 
8
23
  OPERATOR_DESCRIPTION = {
@@ -29,10 +44,6 @@ SUPPORTED_FIELDS = [
29
44
  models.DateField,
30
45
  models.DateTimeField,
31
46
  ]
32
- SUPPORTED_RELATIONS = [
33
- models.ForeignKey,
34
- models.OneToOneField,
35
- ]
36
47
 
37
48
 
38
49
  FIELD_PLAN = OrderedDict((
@@ -132,18 +143,36 @@ FIELD_PLAN = OrderedDict((
132
143
  ))
133
144
 
134
145
 
135
- class CSS_CLASSES:
146
+ class CssClassMixin:
136
147
  reload_on_change_css_class = "searchkit-reload-on-change"
137
148
  reload_on_click_css_class = "searchkit-reload-on-click"
138
149
 
139
150
 
140
- def get_filter_rules(formset):
151
+ class MediaMixin:
152
+ class Media:
153
+ js = [
154
+ 'admin/js/vendor/jquery/jquery.min.js',
155
+ 'admin/js/jquery.init.js',
156
+ "searchkit/searchkit.js"
157
+ ]
158
+
159
+
160
+ def is_searchable_model(model):
161
+ """
162
+ Check if the model is searchable by Searchkit.
163
+ """
164
+ return admin.site.is_registered(model) and SearchkitFilter in admin.site._registry[model].list_filter
165
+
166
+
167
+ def get_searchable_models():
141
168
  """
142
- Build filter rules out of the cleaned data of the formset.
143
- :param formset: Formset to extract filter rules from.
144
- :return: OrderedDict with filter rule pairs: field__operator: value
169
+ Return a queryset of searchable models.
145
170
  """
146
- lookups = OrderedDict()
147
- for data in formset.cleaned_data:
148
- lookups[f'{data["field"]}__{data["operator"]}'] = data['value']
149
- return lookups
171
+ # Before mirating the database we get an OperationalError when trying to
172
+ # access ContentType database table.
173
+ try:
174
+ models = [m for m in apps.get_models() if is_searchable_model(m)]
175
+ ids = [ContentType.objects.get_for_model(m).id for m in models]
176
+ return ContentType.objects.filter(pk__in=ids).order_by('app_label', 'model')
177
+ except OperationalError:
178
+ return ContentType.objects.all()
@@ -0,0 +1,18 @@
1
+ # Generated by Django 5.1.3 on 2025-06-07 05:46
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('contenttypes', '0002_remove_content_type_name'),
10
+ ('searchkit', '0001_initial'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.RenameModel(
15
+ old_name='SearchkitSearch',
16
+ new_name='Search',
17
+ ),
18
+ ]
@@ -5,9 +5,7 @@ from django.contrib.contenttypes.models import ContentType
5
5
  from django.utils.translation import gettext_lazy as _
6
6
 
7
7
 
8
- # Create your models here.
9
- # TODO: Use pickled cleaned data with a char field.
10
- class SearchkitSearch(models.Model):
8
+ class Search(models.Model):
11
9
  name = models.CharField(_('Search name'), max_length=255)
12
10
  contenttype = models.ForeignKey(ContentType, on_delete=models.CASCADE, verbose_name=_('Model'))
13
11
  data = PickledObjectField(_('Serialized filter rule data'))
@@ -16,7 +14,7 @@ class SearchkitSearch(models.Model):
16
14
  class Meta:
17
15
  unique_together = ('name', 'contenttype')
18
16
 
19
- def get_filter_rules(self):
17
+ def as_lookups(self):
20
18
  lookups = OrderedDict()
21
19
  for data in self.data:
22
20
  lookups[f'{data["field"]}__{data["operator"]}'] = data['value']
@@ -0,0 +1,20 @@
1
+ from django.template import Library
2
+ from django.contrib.admin.helpers import Fieldset
3
+
4
+
5
+ register = Library()
6
+
7
+ @register.inclusion_tag("admin/includes/fieldset.html")
8
+ def as_fieldset(form, heading_level=2, prefix='', id_prefix=0, id_suffix='', **fieldset_kwargs):
9
+ """
10
+ Create and render a fieldset for form.
11
+ """
12
+ fieldset = Fieldset(form, fields=form.fields, **fieldset_kwargs)
13
+ context = dict(
14
+ fieldset=fieldset,
15
+ heading_level=heading_level,
16
+ prefix=prefix,
17
+ id_prefix=id_prefix,
18
+ id_suffix=id_suffix,
19
+ )
20
+ return context