django-searchkit 1.1__py3-none-any.whl → 1.3__py3-none-any.whl
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.
- build/lib/build/lib/build/lib/build/lib/example/example/__init__.py +0 -0
- build/lib/build/lib/build/lib/build/lib/example/example/admin.py +16 -0
- build/lib/build/lib/build/lib/build/lib/example/example/asgi.py +16 -0
- build/lib/build/lib/build/lib/build/lib/example/example/management/__init__.py +0 -0
- build/lib/build/lib/build/lib/build/lib/example/example/management/commands/__init__.py +0 -0
- build/lib/build/lib/build/lib/build/lib/example/example/management/commands/createtestdata.py +62 -0
- build/lib/build/lib/build/lib/build/lib/example/example/migrations/0001_initial.py +48 -0
- build/lib/build/lib/build/lib/build/lib/example/example/migrations/__init__.py +0 -0
- build/lib/build/lib/build/lib/build/lib/example/example/models.py +38 -0
- build/lib/build/lib/build/lib/build/lib/example/example/settings.py +125 -0
- build/lib/build/lib/build/lib/build/lib/example/example/urls.py +23 -0
- build/lib/build/lib/build/lib/build/lib/example/example/wsgi.py +16 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/__init__.py +0 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/__version__.py +16 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/admin.py +44 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/apps.py +6 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/filters.py +36 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/forms/__init__.py +5 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/forms/fields.py +56 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/forms/search.py +61 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/forms/searchkit.py +177 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/forms/utils.py +154 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/migrations/0001_initial.py +30 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/migrations/__init__.py +0 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/models.py +21 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/templatetags/__init__.py +0 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/templatetags/searchkit.py +20 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/tests.py +400 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/urls.py +7 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/utils.py +13 -0
- build/lib/build/lib/build/lib/build/lib/searchkit/views.py +23 -0
- build/lib/build/lib/build/lib/example/example/__init__.py +0 -0
- build/lib/build/lib/build/lib/example/example/admin.py +16 -0
- build/lib/build/lib/build/lib/example/example/asgi.py +16 -0
- build/lib/build/lib/build/lib/example/example/management/__init__.py +0 -0
- build/lib/build/lib/build/lib/example/example/management/commands/__init__.py +0 -0
- build/lib/build/lib/build/lib/example/example/management/commands/createtestdata.py +62 -0
- build/lib/build/lib/build/lib/example/example/migrations/0001_initial.py +48 -0
- build/lib/build/lib/build/lib/example/example/migrations/__init__.py +0 -0
- build/lib/build/lib/build/lib/example/example/models.py +38 -0
- build/lib/build/lib/build/lib/example/example/settings.py +125 -0
- build/lib/build/lib/build/lib/example/example/urls.py +23 -0
- build/lib/build/lib/build/lib/example/example/wsgi.py +16 -0
- build/lib/build/lib/build/lib/searchkit/__init__.py +0 -0
- build/lib/build/lib/build/lib/searchkit/__version__.py +16 -0
- build/lib/build/lib/build/lib/searchkit/admin.py +44 -0
- build/lib/build/lib/build/lib/searchkit/apps.py +6 -0
- build/lib/build/lib/build/lib/searchkit/filters.py +36 -0
- build/lib/build/lib/build/lib/searchkit/forms/__init__.py +5 -0
- build/lib/build/lib/build/lib/searchkit/forms/fields.py +56 -0
- build/lib/build/lib/build/lib/searchkit/forms/search.py +61 -0
- build/lib/build/lib/build/lib/searchkit/forms/searchkit.py +177 -0
- build/lib/build/lib/build/lib/searchkit/forms/utils.py +154 -0
- build/lib/build/lib/build/lib/searchkit/migrations/0001_initial.py +30 -0
- build/lib/build/lib/build/lib/searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
- build/lib/build/lib/build/lib/searchkit/migrations/__init__.py +0 -0
- build/lib/build/lib/build/lib/searchkit/models.py +21 -0
- build/lib/build/lib/build/lib/searchkit/templatetags/__init__.py +0 -0
- build/lib/build/lib/build/lib/searchkit/templatetags/searchkit.py +20 -0
- build/lib/build/lib/build/lib/searchkit/tests.py +400 -0
- build/lib/build/lib/build/lib/searchkit/urls.py +7 -0
- build/lib/build/lib/build/lib/searchkit/utils.py +13 -0
- build/lib/build/lib/build/lib/searchkit/views.py +23 -0
- build/lib/build/lib/example/example/admin.py +1 -1
- build/lib/build/lib/searchkit/__version__.py +1 -1
- build/lib/build/lib/searchkit/admin.py +15 -1
- build/lib/build/lib/searchkit/filters.py +15 -6
- build/lib/build/lib/searchkit/forms/fields.py +5 -4
- build/lib/build/lib/searchkit/forms/search.py +0 -1
- build/lib/build/lib/searchkit/forms/searchkit.py +36 -13
- build/lib/build/lib/searchkit/forms/utils.py +50 -74
- build/lib/build/lib/searchkit/models.py +0 -6
- build/lib/build/lib/searchkit/tests.py +1 -3
- build/lib/build/lib/searchkit/utils.py +13 -0
- build/lib/build/lib/searchkit/views.py +3 -3
- build/lib/example/example/admin.py +1 -1
- build/lib/searchkit/__version__.py +1 -1
- build/lib/searchkit/admin.py +15 -1
- build/lib/searchkit/filters.py +15 -6
- build/lib/searchkit/forms/fields.py +5 -4
- build/lib/searchkit/forms/search.py +0 -1
- build/lib/searchkit/forms/searchkit.py +36 -13
- build/lib/searchkit/forms/utils.py +50 -74
- build/lib/searchkit/models.py +0 -6
- build/lib/searchkit/tests.py +1 -3
- build/lib/searchkit/utils.py +13 -0
- build/lib/searchkit/views.py +3 -3
- {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/METADATA +1 -1
- django_searchkit-1.3.dist-info/RECORD +166 -0
- searchkit/__version__.py +1 -1
- searchkit/admin.py +15 -1
- searchkit/filters.py +15 -6
- searchkit/forms/fields.py +5 -4
- searchkit/forms/search.py +0 -1
- searchkit/forms/searchkit.py +36 -13
- searchkit/forms/utils.py +50 -74
- searchkit/models.py +0 -6
- searchkit/tests.py +1 -3
- searchkit/utils.py +13 -0
- searchkit/views.py +3 -3
- django_searchkit-1.1.dist-info/RECORD +0 -99
- {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/WHEEL +0 -0
- {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/licenses/LICENCE +0 -0
- {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/top_level.txt +0 -0
- {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/zip-safe +0 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
from django.utils.translation import gettext_lazy as _
|
2
|
+
from django.http import HttpResponse
|
3
|
+
from django.http import Http404, HttpResponseBadRequest
|
4
|
+
from django.apps import apps
|
5
|
+
from django.contrib.contenttypes.models import ContentType
|
6
|
+
from django.views.generic import View
|
7
|
+
from .forms import SearchkitModelForm
|
8
|
+
from .forms import searchkit_formset_factory
|
9
|
+
|
10
|
+
|
11
|
+
# FIXME: Check permissions and authentication.
|
12
|
+
class SearchkitAjaxView(View):
|
13
|
+
"""
|
14
|
+
Reload the formset via ajax.
|
15
|
+
"""
|
16
|
+
def get(self, request, **kwargs):
|
17
|
+
model_form = SearchkitModelForm(data=self.request.GET)
|
18
|
+
if model_form.is_valid():
|
19
|
+
model = model_form.cleaned_data['searchkit_model'].model_class()
|
20
|
+
formset = searchkit_formset_factory(model=model)(data=request.GET)
|
21
|
+
return HttpResponse(formset.render())
|
22
|
+
else:
|
23
|
+
return HttpResponseBadRequest(_('Invalid searchkit-model-form.'))
|
@@ -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
|
10
|
+
list_filter = [SearchkitFilter]
|
11
11
|
|
12
12
|
|
13
13
|
@admin.register(ModelB)
|
@@ -1,15 +1,18 @@
|
|
1
1
|
from django.contrib import admin
|
2
2
|
from django.http import HttpResponseRedirect
|
3
3
|
from django.urls import reverse
|
4
|
+
from django.utils.html import format_html
|
4
5
|
from .models import Search
|
5
6
|
from .forms import SearchForm
|
6
7
|
from .filters import SearchkitFilter
|
8
|
+
from .filters import SearchableModelFilter
|
7
9
|
|
8
10
|
|
9
11
|
@admin.register(Search)
|
10
12
|
class SearchkitSearchAdmin(admin.ModelAdmin):
|
11
13
|
form = SearchForm
|
12
|
-
list_display = ('name', 'contenttype', 'created_date')
|
14
|
+
list_display = ('name', 'contenttype', 'created_date', 'apply_search_view')
|
15
|
+
list_filter = (('contenttype', SearchableModelFilter),)
|
13
16
|
|
14
17
|
def get_url_for_applied_search(self, obj):
|
15
18
|
app_label = obj.contenttype.app_label
|
@@ -28,3 +31,14 @@ class SearchkitSearchAdmin(admin.ModelAdmin):
|
|
28
31
|
return HttpResponseRedirect(self.get_url_for_applied_search(obj))
|
29
32
|
else:
|
30
33
|
return super().response_change(request, obj, *args, **kwargs)
|
34
|
+
|
35
|
+
def apply_search_view(self, obj):
|
36
|
+
"""
|
37
|
+
Returns a link to apply the search.
|
38
|
+
"""
|
39
|
+
return format_html(
|
40
|
+
'<a href="{}" >Apply search "{}"</a>',
|
41
|
+
self.get_url_for_applied_search(obj),
|
42
|
+
obj.name
|
43
|
+
)
|
44
|
+
apply_search_view.short_description = 'Apply Search'
|
@@ -1,9 +1,10 @@
|
|
1
|
-
from django.contrib
|
1
|
+
from django.contrib import admin
|
2
2
|
from django.contrib.contenttypes.models import ContentType
|
3
3
|
from .models import Search
|
4
|
+
from .utils import is_searchable_model
|
4
5
|
|
5
6
|
|
6
|
-
class SearchkitFilter(SimpleListFilter):
|
7
|
+
class SearchkitFilter(admin.SimpleListFilter):
|
7
8
|
title = 'Searchkit Filter'
|
8
9
|
parameter_name = 'search'
|
9
10
|
template = 'searchkit/searchkit_filter.html'
|
@@ -14,14 +15,22 @@ class SearchkitFilter(SimpleListFilter):
|
|
14
15
|
self.searchkit_model = ContentType.objects.get_for_model(model)
|
15
16
|
super().__init__(request, params, model, model_admin)
|
16
17
|
|
18
|
+
def has_output(self):
|
19
|
+
return True
|
20
|
+
|
17
21
|
def lookups(self, request, model_admin):
|
18
|
-
|
19
|
-
# choices.
|
20
|
-
searches = Search.objects.filter(contenttype=self.searchkit_model).order_by('-created_date')[:3]
|
22
|
+
searches = Search.objects.filter(contenttype=self.searchkit_model).order_by('-created_date')
|
21
23
|
return [(str(obj.id), obj.name) for obj in searches]
|
22
24
|
|
23
25
|
def queryset(self, request, queryset):
|
24
26
|
# Filter the queryset based on the selected SearchkitSearch object
|
25
27
|
if self.value():
|
26
28
|
search = Search.objects.get(id=int(self.value()))
|
27
|
-
return search.
|
29
|
+
return queryset.filter(**search.as_lookups())
|
30
|
+
|
31
|
+
|
32
|
+
class SearchableModelFilter(admin.filters.RelatedFieldListFilter):
|
33
|
+
def __init__(self, *args, **kwargs):
|
34
|
+
super().__init__(*args, **kwargs)
|
35
|
+
contenttypes = ContentType.objects.order_by('app_label', 'model')
|
36
|
+
self.lookup_choices = [(m.id, m) for m in contenttypes if is_searchable_model(m.model_class())]
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from django import forms
|
2
|
+
from django.contrib.admin import widgets
|
2
3
|
from django.utils.translation import gettext_lazy as _
|
3
4
|
|
4
5
|
|
@@ -46,10 +47,10 @@ class IntegerRangeField(BaseRangeField):
|
|
46
47
|
class DateRangeField(BaseRangeField):
|
47
48
|
incomplete_message = _("Enter the first and the last date.")
|
48
49
|
field_type = forms.DateField
|
49
|
-
widget_type =
|
50
|
+
widget_type = widgets.AdminDateWidget
|
50
51
|
|
51
52
|
|
52
|
-
class DateTimeRangeField(
|
53
|
+
class DateTimeRangeField(DateRangeField):
|
53
54
|
incomplete_message = _("Enter the first and the last datetime.")
|
54
|
-
field_type = forms.
|
55
|
-
widget_type =
|
55
|
+
field_type = forms.SplitDateTimeField
|
56
|
+
widget_type = widgets.AdminSplitDateTime
|
@@ -1,19 +1,30 @@
|
|
1
1
|
from django import forms
|
2
|
+
from django.apps import apps
|
3
|
+
from django.contrib.admin import widgets
|
4
|
+
from django.contrib.contenttypes.models import ContentType
|
2
5
|
from django.utils.translation import gettext_lazy as _
|
3
6
|
from django.utils.functional import cached_property
|
4
7
|
from .utils import CssClassMixin, FIELD_PLAN, OPERATOR_DESCRIPTION
|
5
8
|
from .utils import SUPPORTED_FIELDS
|
6
9
|
from .utils import ModelTree
|
7
10
|
from .utils import MediaMixin
|
8
|
-
from .
|
11
|
+
from .fields import DateRangeField
|
12
|
+
from ..utils import is_searchable_model
|
9
13
|
|
10
14
|
|
11
15
|
class SearchkitModelForm(forms.Form):
|
12
16
|
"""
|
13
17
|
Form to select a content type.
|
14
18
|
"""
|
19
|
+
def __init__(self, *args, **kwargs):
|
20
|
+
super().__init__(*args, **kwargs)
|
21
|
+
models = [m for m in apps.get_models() if is_searchable_model(m)]
|
22
|
+
ids = [ContentType.objects.get_for_model(m).id for m in models]
|
23
|
+
queryset = self.fields['searchkit_model'].queryset.filter(pk__in=ids)
|
24
|
+
self.fields['searchkit_model'].queryset = queryset
|
25
|
+
|
15
26
|
searchkit_model = forms.ModelChoiceField(
|
16
|
-
queryset=
|
27
|
+
queryset=ContentType.objects.all().order_by('app_label', 'model'),
|
17
28
|
label=_('Model'),
|
18
29
|
empty_label=_('Select a Model'),
|
19
30
|
widget=forms.Select(attrs={
|
@@ -105,27 +116,19 @@ class BaseSearchkitForm(MediaMixin, CssClassMixin, forms.Form):
|
|
105
116
|
return choices
|
106
117
|
|
107
118
|
def _add_field_name_field(self):
|
108
|
-
initial = self.initial.get('field')
|
109
119
|
choices = self._get_model_field_choices()
|
110
|
-
field = forms.ChoiceField(label=_('Model field'), choices=choices
|
120
|
+
field = forms.ChoiceField(label=_('Model field'), choices=choices)
|
111
121
|
field.widget.attrs.update({"class": self.reload_on_change_css_class})
|
112
122
|
self.fields['field'] = field
|
113
123
|
|
114
124
|
def _add_operator_field(self):
|
115
|
-
initial = self.initial.get('operator')
|
116
125
|
choices = [(o, OPERATOR_DESCRIPTION[o]) for o in self.field_plan.keys()]
|
117
|
-
field = forms.ChoiceField(label=_('Operator'), choices=choices
|
126
|
+
field = forms.ChoiceField(label=_('Operator'), choices=choices)
|
118
127
|
field.widget.attrs.update({"class": self.reload_on_change_css_class})
|
119
128
|
self.fields['operator'] = field
|
120
129
|
|
121
130
|
def _add_value_field(self):
|
122
|
-
|
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
|
131
|
+
self.fields['value'] = self.field_plan[self.operator](self.model_field)
|
129
132
|
|
130
133
|
|
131
134
|
class BaseSearchkitFormSet(CssClassMixin, forms.BaseFormSet):
|
@@ -143,6 +146,26 @@ class BaseSearchkitFormSet(CssClassMixin, forms.BaseFormSet):
|
|
143
146
|
def get_default_prefix(self):
|
144
147
|
return "searchkit"
|
145
148
|
|
149
|
+
@cached_property
|
150
|
+
def uses_date_widget(self):
|
151
|
+
"""
|
152
|
+
Check if the form uses a date widget.
|
153
|
+
"""
|
154
|
+
for form in self.forms:
|
155
|
+
for field in form.fields.values():
|
156
|
+
if isinstance(field.widget, (widgets.AdminDateWidget, widgets.AdminSplitDateTime)):
|
157
|
+
return True
|
158
|
+
elif isinstance(field, DateRangeField):
|
159
|
+
return True
|
160
|
+
return False
|
161
|
+
|
162
|
+
@cached_property
|
163
|
+
def forms(self):
|
164
|
+
if self.model:
|
165
|
+
return super().forms
|
166
|
+
else:
|
167
|
+
return []
|
168
|
+
|
146
169
|
|
147
170
|
def searchkit_formset_factory(model, **kwargs):
|
148
171
|
form = type('SearchkitForm', (BaseSearchkitForm,), dict(model=model))
|
@@ -1,13 +1,9 @@
|
|
1
1
|
from modeltree import ModelTree as BaseModelTree
|
2
2
|
from collections import OrderedDict
|
3
|
-
from django.
|
4
|
-
from django.contrib import admin
|
5
|
-
from django.contrib.contenttypes.models import ContentType
|
3
|
+
from django.contrib.admin.widgets import AdminDateWidget, AdminSplitDateTime
|
6
4
|
from django.db import models
|
7
|
-
from django.db.utils import OperationalError
|
8
5
|
from django import forms
|
9
6
|
from django.utils.translation import gettext_lazy as _
|
10
|
-
from ..filters import SearchkitFilter
|
11
7
|
from . import fields as searchkit_fields
|
12
8
|
|
13
9
|
|
@@ -50,94 +46,95 @@ FIELD_PLAN = OrderedDict((
|
|
50
46
|
(
|
51
47
|
lambda f: isinstance(f, models.BooleanField),
|
52
48
|
{
|
53
|
-
'exact':
|
49
|
+
'exact': lambda f: forms.NullBooleanField() if f.null else forms.BooleanField(),
|
54
50
|
}
|
55
51
|
),
|
56
52
|
(
|
57
53
|
lambda f: isinstance(f, models.CharField) and f.choices,
|
58
54
|
{
|
59
|
-
'exact':
|
60
|
-
'contains':
|
61
|
-
'startswith':
|
62
|
-
'endswith':
|
63
|
-
'regex':
|
64
|
-
'in':
|
55
|
+
'exact': lambda f: forms.ChoiceField(choices=f.choices),
|
56
|
+
'contains': lambda f: forms.CharField(),
|
57
|
+
'startswith': lambda f: forms.CharField(),
|
58
|
+
'endswith': lambda f: forms.CharField(),
|
59
|
+
'regex': lambda f: forms.CharField(),
|
60
|
+
'in': lambda f: forms.MultipleChoiceField(choices=f.choices),
|
65
61
|
}
|
66
62
|
),
|
67
63
|
(
|
68
64
|
lambda f: isinstance(f, models.CharField),
|
69
65
|
{
|
70
|
-
'exact':
|
71
|
-
'contains':
|
72
|
-
'startswith':
|
73
|
-
'endswith':
|
74
|
-
'regex':
|
66
|
+
'exact': lambda f: forms.CharField(),
|
67
|
+
'contains': lambda f: forms.CharField(),
|
68
|
+
'startswith': lambda f: forms.CharField(),
|
69
|
+
'endswith': lambda f: forms.CharField(),
|
70
|
+
'regex': lambda f: forms.CharField(),
|
75
71
|
}
|
76
72
|
),
|
77
73
|
(
|
78
74
|
lambda f: isinstance(f, models.IntegerField) and f.choices,
|
79
75
|
{
|
80
|
-
'exact':
|
81
|
-
'
|
82
|
-
'
|
83
|
-
'
|
84
|
-
'
|
85
|
-
'
|
76
|
+
'exact': lambda f: forms.ChoiceField(choices=f.choices),
|
77
|
+
'gt': lambda f: forms.IntegerField(),
|
78
|
+
'gte': lambda f: forms.IntegerField(),
|
79
|
+
'lt': lambda f: forms.IntegerField(),
|
80
|
+
'lte': lambda f: forms.IntegerField(),
|
81
|
+
'range': lambda f: searchkit_fields.IntegerRangeField(),
|
82
|
+
'in': lambda f: forms.MultipleChoiceField(choices=f.choices),
|
86
83
|
}
|
87
84
|
),
|
88
85
|
(
|
89
86
|
lambda f: isinstance(f, models.IntegerField),
|
90
87
|
{
|
91
|
-
'exact':
|
92
|
-
'gt':
|
93
|
-
'gte':
|
94
|
-
'lt':
|
95
|
-
'lte':
|
96
|
-
'range':
|
88
|
+
'exact': lambda f: forms.IntegerField(),
|
89
|
+
'gt': lambda f: forms.IntegerField(),
|
90
|
+
'gte': lambda f: forms.IntegerField(),
|
91
|
+
'lt': lambda f: forms.IntegerField(),
|
92
|
+
'lte': lambda f: forms.IntegerField(),
|
93
|
+
'range': lambda f: searchkit_fields.IntegerRangeField(),
|
97
94
|
}
|
98
95
|
),
|
99
96
|
(
|
100
97
|
lambda f: isinstance(f, models.FloatField),
|
101
98
|
{
|
102
|
-
'exact':
|
103
|
-
'gt':
|
104
|
-
'gte':
|
105
|
-
'lt':
|
106
|
-
'lte':
|
107
|
-
'range':
|
99
|
+
'exact': lambda f: forms.FloatField(),
|
100
|
+
'gt': lambda f: forms.FloatField(),
|
101
|
+
'gte': lambda f: forms.FloatField(),
|
102
|
+
'lt': lambda f: forms.FloatField(),
|
103
|
+
'lte': lambda f: forms.FloatField(),
|
104
|
+
'range': lambda f: searchkit_fields.IntegerRangeField(),
|
108
105
|
}
|
109
106
|
),
|
110
107
|
(
|
111
108
|
lambda f: isinstance(f, models.DecimalField),
|
112
109
|
{
|
113
|
-
'exact':
|
114
|
-
'gt':
|
115
|
-
'gte':
|
116
|
-
'lt':
|
117
|
-
'lte':
|
118
|
-
'range':
|
110
|
+
'exact': lambda f: forms.DecimalField(),
|
111
|
+
'gt': lambda f: forms.DecimalField(),
|
112
|
+
'gte': lambda f: forms.DecimalField(),
|
113
|
+
'lt': lambda f: forms.DecimalField(),
|
114
|
+
'lte': lambda f: forms.DecimalField(),
|
115
|
+
'range': lambda f: searchkit_fields.IntegerRangeField(),
|
119
116
|
}
|
120
117
|
),
|
121
118
|
(
|
122
119
|
lambda f: isinstance(f, models.DateTimeField),
|
123
120
|
{
|
124
|
-
'exact':
|
125
|
-
'gt':
|
126
|
-
'gte':
|
127
|
-
'lt':
|
128
|
-
'lte':
|
129
|
-
'range':
|
121
|
+
'exact': lambda f: forms.SplitDateTimeField(widget=AdminSplitDateTime()),
|
122
|
+
'gt': lambda f: forms.SplitDateTimeField(widget=AdminSplitDateTime()),
|
123
|
+
'gte': lambda f: forms.SplitDateTimeField(widget=AdminSplitDateTime()),
|
124
|
+
'lt': lambda f: forms.SplitDateTimeField(widget=AdminSplitDateTime()),
|
125
|
+
'lte': lambda f: forms.SplitDateTimeField(widget=AdminSplitDateTime()),
|
126
|
+
'range': lambda f: searchkit_fields.DateTimeRangeField(),
|
130
127
|
}
|
131
128
|
),
|
132
129
|
(
|
133
130
|
lambda f: isinstance(f, models.DateField),
|
134
131
|
{
|
135
|
-
'exact':
|
136
|
-
'gt':
|
137
|
-
'gte':
|
138
|
-
'lt':
|
139
|
-
'lte':
|
140
|
-
'range':
|
132
|
+
'exact': lambda f: forms.DateField(widget=AdminDateWidget()),
|
133
|
+
'gt': lambda f: forms.DateField(widget=AdminDateWidget()),
|
134
|
+
'gte': lambda f: forms.DateField(widget=AdminDateWidget()),
|
135
|
+
'lt': lambda f: forms.DateField(widget=AdminDateWidget()),
|
136
|
+
'lte': lambda f: forms.DateField(widget=AdminDateWidget()),
|
137
|
+
'range': lambda f: searchkit_fields.DateRangeField(),
|
141
138
|
}
|
142
139
|
),
|
143
140
|
))
|
@@ -155,24 +152,3 @@ class MediaMixin:
|
|
155
152
|
'admin/js/jquery.init.js',
|
156
153
|
"searchkit/searchkit.js"
|
157
154
|
]
|
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():
|
168
|
-
"""
|
169
|
-
Return a queryset of searchable models.
|
170
|
-
"""
|
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()
|
@@ -19,9 +19,3 @@ class Search(models.Model):
|
|
19
19
|
for data in self.data:
|
20
20
|
lookups[f'{data["field"]}__{data["operator"]}'] = data['value']
|
21
21
|
return lookups
|
22
|
-
|
23
|
-
def as_queryset(self):
|
24
|
-
"""
|
25
|
-
Returns a filtered queryset for the model.
|
26
|
-
"""
|
27
|
-
return self.contenttype.model_class().objects.filter(**self.as_lookups())
|
@@ -49,7 +49,7 @@ INITIAL_DATA = [
|
|
49
49
|
dict(
|
50
50
|
field='datetime',
|
51
51
|
operator='exact',
|
52
|
-
value='2025-05-14 08:45',
|
52
|
+
value=['2025-05-14', '08:45'],
|
53
53
|
)
|
54
54
|
]
|
55
55
|
|
@@ -263,8 +263,6 @@ class SearchkitSearchFormTestCase(TestCase):
|
|
263
263
|
self.assertEqual(len(filter_rules), len(INITIAL_DATA))
|
264
264
|
for data in INITIAL_DATA:
|
265
265
|
self.assertIn(f"{data['field']}__{data['operator']}", filter_rules)
|
266
|
-
queryset = form.instance.as_queryset()
|
267
|
-
self.assertTrue(queryset.model == ModelA)
|
268
266
|
|
269
267
|
|
270
268
|
class SearchkitModelFormTestCase(TestCase):
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from django.contrib import admin
|
2
|
+
|
3
|
+
|
4
|
+
def is_searchable_model(model):
|
5
|
+
"""
|
6
|
+
Check if the model is searchable by Searchkit.
|
7
|
+
"""
|
8
|
+
# We do not import SearchkitFilter to avoid circular imports. So we check
|
9
|
+
# the filter by its name.
|
10
|
+
return (
|
11
|
+
admin.site.is_registered(model)
|
12
|
+
and 'SearchkitFilter' in [getattr(f, '__name__', '') for f in admin.site._registry[model].list_filter]
|
13
|
+
)
|
@@ -14,9 +14,9 @@ class SearchkitAjaxView(View):
|
|
14
14
|
Reload the formset via ajax.
|
15
15
|
"""
|
16
16
|
def get(self, request, **kwargs):
|
17
|
-
|
18
|
-
if
|
19
|
-
model =
|
17
|
+
model_form = SearchkitModelForm(data=self.request.GET)
|
18
|
+
if model_form.is_valid():
|
19
|
+
model = model_form.cleaned_data['searchkit_model'].model_class()
|
20
20
|
formset = searchkit_formset_factory(model=model)(data=request.GET)
|
21
21
|
return HttpResponse(formset.render())
|
22
22
|
else:
|
@@ -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
|
10
|
+
list_filter = [SearchkitFilter]
|
11
11
|
|
12
12
|
|
13
13
|
@admin.register(ModelB)
|
build/lib/searchkit/admin.py
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
from django.contrib import admin
|
2
2
|
from django.http import HttpResponseRedirect
|
3
3
|
from django.urls import reverse
|
4
|
+
from django.utils.html import format_html
|
4
5
|
from .models import Search
|
5
6
|
from .forms import SearchForm
|
6
7
|
from .filters import SearchkitFilter
|
8
|
+
from .filters import SearchableModelFilter
|
7
9
|
|
8
10
|
|
9
11
|
@admin.register(Search)
|
10
12
|
class SearchkitSearchAdmin(admin.ModelAdmin):
|
11
13
|
form = SearchForm
|
12
|
-
list_display = ('name', 'contenttype', 'created_date')
|
14
|
+
list_display = ('name', 'contenttype', 'created_date', 'apply_search_view')
|
15
|
+
list_filter = (('contenttype', SearchableModelFilter),)
|
13
16
|
|
14
17
|
def get_url_for_applied_search(self, obj):
|
15
18
|
app_label = obj.contenttype.app_label
|
@@ -28,3 +31,14 @@ class SearchkitSearchAdmin(admin.ModelAdmin):
|
|
28
31
|
return HttpResponseRedirect(self.get_url_for_applied_search(obj))
|
29
32
|
else:
|
30
33
|
return super().response_change(request, obj, *args, **kwargs)
|
34
|
+
|
35
|
+
def apply_search_view(self, obj):
|
36
|
+
"""
|
37
|
+
Returns a link to apply the search.
|
38
|
+
"""
|
39
|
+
return format_html(
|
40
|
+
'<a href="{}" >Apply search "{}"</a>',
|
41
|
+
self.get_url_for_applied_search(obj),
|
42
|
+
obj.name
|
43
|
+
)
|
44
|
+
apply_search_view.short_description = 'Apply Search'
|
build/lib/searchkit/filters.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
-
from django.contrib
|
1
|
+
from django.contrib import admin
|
2
2
|
from django.contrib.contenttypes.models import ContentType
|
3
3
|
from .models import Search
|
4
|
+
from .utils import is_searchable_model
|
4
5
|
|
5
6
|
|
6
|
-
class SearchkitFilter(SimpleListFilter):
|
7
|
+
class SearchkitFilter(admin.SimpleListFilter):
|
7
8
|
title = 'Searchkit Filter'
|
8
9
|
parameter_name = 'search'
|
9
10
|
template = 'searchkit/searchkit_filter.html'
|
@@ -14,14 +15,22 @@ class SearchkitFilter(SimpleListFilter):
|
|
14
15
|
self.searchkit_model = ContentType.objects.get_for_model(model)
|
15
16
|
super().__init__(request, params, model, model_admin)
|
16
17
|
|
18
|
+
def has_output(self):
|
19
|
+
return True
|
20
|
+
|
17
21
|
def lookups(self, request, model_admin):
|
18
|
-
|
19
|
-
# choices.
|
20
|
-
searches = Search.objects.filter(contenttype=self.searchkit_model).order_by('-created_date')[:3]
|
22
|
+
searches = Search.objects.filter(contenttype=self.searchkit_model).order_by('-created_date')
|
21
23
|
return [(str(obj.id), obj.name) for obj in searches]
|
22
24
|
|
23
25
|
def queryset(self, request, queryset):
|
24
26
|
# Filter the queryset based on the selected SearchkitSearch object
|
25
27
|
if self.value():
|
26
28
|
search = Search.objects.get(id=int(self.value()))
|
27
|
-
return search.
|
29
|
+
return queryset.filter(**search.as_lookups())
|
30
|
+
|
31
|
+
|
32
|
+
class SearchableModelFilter(admin.filters.RelatedFieldListFilter):
|
33
|
+
def __init__(self, *args, **kwargs):
|
34
|
+
super().__init__(*args, **kwargs)
|
35
|
+
contenttypes = ContentType.objects.order_by('app_label', 'model')
|
36
|
+
self.lookup_choices = [(m.id, m) for m in contenttypes if is_searchable_model(m.model_class())]
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from django import forms
|
2
|
+
from django.contrib.admin import widgets
|
2
3
|
from django.utils.translation import gettext_lazy as _
|
3
4
|
|
4
5
|
|
@@ -46,10 +47,10 @@ class IntegerRangeField(BaseRangeField):
|
|
46
47
|
class DateRangeField(BaseRangeField):
|
47
48
|
incomplete_message = _("Enter the first and the last date.")
|
48
49
|
field_type = forms.DateField
|
49
|
-
widget_type =
|
50
|
+
widget_type = widgets.AdminDateWidget
|
50
51
|
|
51
52
|
|
52
|
-
class DateTimeRangeField(
|
53
|
+
class DateTimeRangeField(DateRangeField):
|
53
54
|
incomplete_message = _("Enter the first and the last datetime.")
|
54
|
-
field_type = forms.
|
55
|
-
widget_type =
|
55
|
+
field_type = forms.SplitDateTimeField
|
56
|
+
widget_type = widgets.AdminSplitDateTime
|