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.
Files changed (106) hide show
  1. build/lib/build/lib/build/lib/build/lib/example/example/__init__.py +0 -0
  2. build/lib/build/lib/build/lib/build/lib/example/example/admin.py +16 -0
  3. build/lib/build/lib/build/lib/build/lib/example/example/asgi.py +16 -0
  4. build/lib/build/lib/build/lib/build/lib/example/example/management/__init__.py +0 -0
  5. build/lib/build/lib/build/lib/build/lib/example/example/management/commands/__init__.py +0 -0
  6. build/lib/build/lib/build/lib/build/lib/example/example/management/commands/createtestdata.py +62 -0
  7. build/lib/build/lib/build/lib/build/lib/example/example/migrations/0001_initial.py +48 -0
  8. build/lib/build/lib/build/lib/build/lib/example/example/migrations/__init__.py +0 -0
  9. build/lib/build/lib/build/lib/build/lib/example/example/models.py +38 -0
  10. build/lib/build/lib/build/lib/build/lib/example/example/settings.py +125 -0
  11. build/lib/build/lib/build/lib/build/lib/example/example/urls.py +23 -0
  12. build/lib/build/lib/build/lib/build/lib/example/example/wsgi.py +16 -0
  13. build/lib/build/lib/build/lib/build/lib/searchkit/__init__.py +0 -0
  14. build/lib/build/lib/build/lib/build/lib/searchkit/__version__.py +16 -0
  15. build/lib/build/lib/build/lib/build/lib/searchkit/admin.py +44 -0
  16. build/lib/build/lib/build/lib/build/lib/searchkit/apps.py +6 -0
  17. build/lib/build/lib/build/lib/build/lib/searchkit/filters.py +36 -0
  18. build/lib/build/lib/build/lib/build/lib/searchkit/forms/__init__.py +5 -0
  19. build/lib/build/lib/build/lib/build/lib/searchkit/forms/fields.py +56 -0
  20. build/lib/build/lib/build/lib/build/lib/searchkit/forms/search.py +61 -0
  21. build/lib/build/lib/build/lib/build/lib/searchkit/forms/searchkit.py +177 -0
  22. build/lib/build/lib/build/lib/build/lib/searchkit/forms/utils.py +154 -0
  23. build/lib/build/lib/build/lib/build/lib/searchkit/migrations/0001_initial.py +30 -0
  24. build/lib/build/lib/build/lib/build/lib/searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
  25. build/lib/build/lib/build/lib/build/lib/searchkit/migrations/__init__.py +0 -0
  26. build/lib/build/lib/build/lib/build/lib/searchkit/models.py +21 -0
  27. build/lib/build/lib/build/lib/build/lib/searchkit/templatetags/__init__.py +0 -0
  28. build/lib/build/lib/build/lib/build/lib/searchkit/templatetags/searchkit.py +20 -0
  29. build/lib/build/lib/build/lib/build/lib/searchkit/tests.py +400 -0
  30. build/lib/build/lib/build/lib/build/lib/searchkit/urls.py +7 -0
  31. build/lib/build/lib/build/lib/build/lib/searchkit/utils.py +13 -0
  32. build/lib/build/lib/build/lib/build/lib/searchkit/views.py +23 -0
  33. build/lib/build/lib/build/lib/example/example/__init__.py +0 -0
  34. build/lib/build/lib/build/lib/example/example/admin.py +16 -0
  35. build/lib/build/lib/build/lib/example/example/asgi.py +16 -0
  36. build/lib/build/lib/build/lib/example/example/management/__init__.py +0 -0
  37. build/lib/build/lib/build/lib/example/example/management/commands/__init__.py +0 -0
  38. build/lib/build/lib/build/lib/example/example/management/commands/createtestdata.py +62 -0
  39. build/lib/build/lib/build/lib/example/example/migrations/0001_initial.py +48 -0
  40. build/lib/build/lib/build/lib/example/example/migrations/__init__.py +0 -0
  41. build/lib/build/lib/build/lib/example/example/models.py +38 -0
  42. build/lib/build/lib/build/lib/example/example/settings.py +125 -0
  43. build/lib/build/lib/build/lib/example/example/urls.py +23 -0
  44. build/lib/build/lib/build/lib/example/example/wsgi.py +16 -0
  45. build/lib/build/lib/build/lib/searchkit/__init__.py +0 -0
  46. build/lib/build/lib/build/lib/searchkit/__version__.py +16 -0
  47. build/lib/build/lib/build/lib/searchkit/admin.py +44 -0
  48. build/lib/build/lib/build/lib/searchkit/apps.py +6 -0
  49. build/lib/build/lib/build/lib/searchkit/filters.py +36 -0
  50. build/lib/build/lib/build/lib/searchkit/forms/__init__.py +5 -0
  51. build/lib/build/lib/build/lib/searchkit/forms/fields.py +56 -0
  52. build/lib/build/lib/build/lib/searchkit/forms/search.py +61 -0
  53. build/lib/build/lib/build/lib/searchkit/forms/searchkit.py +177 -0
  54. build/lib/build/lib/build/lib/searchkit/forms/utils.py +154 -0
  55. build/lib/build/lib/build/lib/searchkit/migrations/0001_initial.py +30 -0
  56. build/lib/build/lib/build/lib/searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
  57. build/lib/build/lib/build/lib/searchkit/migrations/__init__.py +0 -0
  58. build/lib/build/lib/build/lib/searchkit/models.py +21 -0
  59. build/lib/build/lib/build/lib/searchkit/templatetags/__init__.py +0 -0
  60. build/lib/build/lib/build/lib/searchkit/templatetags/searchkit.py +20 -0
  61. build/lib/build/lib/build/lib/searchkit/tests.py +400 -0
  62. build/lib/build/lib/build/lib/searchkit/urls.py +7 -0
  63. build/lib/build/lib/build/lib/searchkit/utils.py +13 -0
  64. build/lib/build/lib/build/lib/searchkit/views.py +23 -0
  65. build/lib/build/lib/example/example/admin.py +1 -1
  66. build/lib/build/lib/searchkit/__version__.py +1 -1
  67. build/lib/build/lib/searchkit/admin.py +15 -1
  68. build/lib/build/lib/searchkit/filters.py +15 -6
  69. build/lib/build/lib/searchkit/forms/fields.py +5 -4
  70. build/lib/build/lib/searchkit/forms/search.py +0 -1
  71. build/lib/build/lib/searchkit/forms/searchkit.py +36 -13
  72. build/lib/build/lib/searchkit/forms/utils.py +50 -74
  73. build/lib/build/lib/searchkit/models.py +0 -6
  74. build/lib/build/lib/searchkit/tests.py +1 -3
  75. build/lib/build/lib/searchkit/utils.py +13 -0
  76. build/lib/build/lib/searchkit/views.py +3 -3
  77. build/lib/example/example/admin.py +1 -1
  78. build/lib/searchkit/__version__.py +1 -1
  79. build/lib/searchkit/admin.py +15 -1
  80. build/lib/searchkit/filters.py +15 -6
  81. build/lib/searchkit/forms/fields.py +5 -4
  82. build/lib/searchkit/forms/search.py +0 -1
  83. build/lib/searchkit/forms/searchkit.py +36 -13
  84. build/lib/searchkit/forms/utils.py +50 -74
  85. build/lib/searchkit/models.py +0 -6
  86. build/lib/searchkit/tests.py +1 -3
  87. build/lib/searchkit/utils.py +13 -0
  88. build/lib/searchkit/views.py +3 -3
  89. {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/METADATA +1 -1
  90. django_searchkit-1.3.dist-info/RECORD +166 -0
  91. searchkit/__version__.py +1 -1
  92. searchkit/admin.py +15 -1
  93. searchkit/filters.py +15 -6
  94. searchkit/forms/fields.py +5 -4
  95. searchkit/forms/search.py +0 -1
  96. searchkit/forms/searchkit.py +36 -13
  97. searchkit/forms/utils.py +50 -74
  98. searchkit/models.py +0 -6
  99. searchkit/tests.py +1 -3
  100. searchkit/utils.py +13 -0
  101. searchkit/views.py +3 -3
  102. django_searchkit-1.1.dist-info/RECORD +0 -99
  103. {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/WHEEL +0 -0
  104. {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/licenses/LICENCE +0 -0
  105. {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/top_level.txt +0 -0
  106. {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, '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.1"
16
+ __version__ = "1.3"
@@ -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.admin import SimpleListFilter
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
- # Fetch the last three objects from SearchkitSearch and return them as
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.as_queryset()
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 = forms.DateInput
50
+ widget_type = widgets.AdminDateWidget
50
51
 
51
52
 
52
- class DateTimeRangeField(BaseRangeField):
53
+ class DateTimeRangeField(DateRangeField):
53
54
  incomplete_message = _("Enter the first and the last datetime.")
54
- field_type = forms.DateTimeField
55
- widget_type = forms.DateTimeInput
55
+ field_type = forms.SplitDateTimeField
56
+ widget_type = widgets.AdminSplitDateTime
@@ -1,6 +1,5 @@
1
1
  from django import forms
2
2
  from django.utils.functional import cached_property
3
- from django.contrib.contenttypes.models import ContentType
4
3
  from ..models import Search
5
4
  from .searchkit import SearchkitModelForm
6
5
  from .searchkit import searchkit_formset_factory
@@ -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 .utils import get_searchable_models
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=get_searchable_models(),
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, initial=initial)
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, initial=initial)
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
- 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
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.apps import apps
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': (forms.NullBooleanField,),
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': (forms.ChoiceField,),
60
- 'contains': (forms.CharField,),
61
- 'startswith': (forms.CharField,),
62
- 'endswith': (forms.CharField,),
63
- 'regex': (forms.CharField,),
64
- 'in': (forms.MultipleChoiceField,),
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': (forms.CharField,),
71
- 'contains': (forms.CharField,),
72
- 'startswith': (forms.CharField,),
73
- 'endswith': (forms.CharField,),
74
- 'regex': (forms.CharField,),
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': (forms.ChoiceField,),
81
- 'contains': (forms.IntegerField,),
82
- 'startswith': (forms.IntegerField,),
83
- 'endswith': (forms.IntegerField,),
84
- 'regex': (forms.IntegerField,),
85
- 'in': (forms.MultipleChoiceField,),
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': (forms.IntegerField,),
92
- 'gt': (forms.IntegerField,),
93
- 'gte': (forms.IntegerField,),
94
- 'lt': (forms.IntegerField,),
95
- 'lte': (forms.IntegerField,),
96
- 'range': (searchkit_fields.IntegerRangeField,),
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': (forms.FloatField,),
103
- 'gt': (forms.FloatField,),
104
- 'gte': (forms.FloatField,),
105
- 'lt': (forms.FloatField,),
106
- 'lte': (forms.FloatField,),
107
- 'range': (searchkit_fields.IntegerRangeField,),
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': (forms.DecimalField,),
114
- 'gt': (forms.DecimalField,),
115
- 'gte': (forms.DecimalField,),
116
- 'lt': (forms.DecimalField,),
117
- 'lte': (forms.DecimalField,),
118
- 'range': (searchkit_fields.IntegerRangeField,),
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': (forms.DateTimeField,),
125
- 'gt': (forms.DateTimeField,),
126
- 'gte': (forms.DateTimeField,),
127
- 'lt': (forms.DateTimeField,),
128
- 'lte': (forms.DateTimeField,),
129
- 'range': (searchkit_fields.DateTimeRangeField,),
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': (forms.DateField,),
136
- 'gt': (forms.DateField,),
137
- 'gte': (forms.DateField,),
138
- 'lt': (forms.DateField,),
139
- 'lte': (forms.DateField,),
140
- 'range': (searchkit_fields.DateRangeField,),
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
- contenttype_form = SearchkitModelForm(data=self.request.GET)
18
- if contenttype_form.is_valid():
19
- model = contenttype_form.cleaned_data['searchkit_model'].model_class()
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, '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.1"
16
+ __version__ = "1.3"
@@ -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.admin import SimpleListFilter
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
- # Fetch the last three objects from SearchkitSearch and return them as
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.as_queryset()
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 = forms.DateInput
50
+ widget_type = widgets.AdminDateWidget
50
51
 
51
52
 
52
- class DateTimeRangeField(BaseRangeField):
53
+ class DateTimeRangeField(DateRangeField):
53
54
  incomplete_message = _("Enter the first and the last datetime.")
54
- field_type = forms.DateTimeField
55
- widget_type = forms.DateTimeInput
55
+ field_type = forms.SplitDateTimeField
56
+ widget_type = widgets.AdminSplitDateTime
@@ -1,6 +1,5 @@
1
1
  from django import forms
2
2
  from django.utils.functional import cached_property
3
- from django.contrib.contenttypes.models import ContentType
4
3
  from ..models import Search
5
4
  from .searchkit import SearchkitModelForm
6
5
  from .searchkit import searchkit_formset_factory