django-searchkit 1.0__py3-none-any.whl → 1.1__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 (65) hide show
  1. build/lib/build/lib/example/example/__init__.py +0 -0
  2. build/lib/build/lib/example/example/admin.py +16 -0
  3. build/lib/build/lib/example/example/asgi.py +16 -0
  4. build/lib/build/lib/example/example/management/__init__.py +0 -0
  5. build/lib/build/lib/example/example/management/commands/__init__.py +0 -0
  6. build/lib/build/lib/example/example/management/commands/createtestdata.py +62 -0
  7. build/lib/build/lib/example/example/migrations/0001_initial.py +48 -0
  8. build/lib/build/lib/example/example/migrations/__init__.py +0 -0
  9. build/lib/build/lib/example/example/models.py +38 -0
  10. build/lib/build/lib/example/example/settings.py +125 -0
  11. build/lib/build/lib/example/example/urls.py +23 -0
  12. build/lib/build/lib/example/example/wsgi.py +16 -0
  13. build/lib/build/lib/searchkit/__init__.py +0 -0
  14. build/lib/build/lib/searchkit/__version__.py +16 -0
  15. build/lib/build/lib/searchkit/admin.py +30 -0
  16. build/lib/build/lib/searchkit/apps.py +6 -0
  17. build/lib/build/lib/searchkit/filters.py +27 -0
  18. build/lib/build/lib/searchkit/forms/__init__.py +5 -0
  19. build/lib/build/lib/searchkit/forms/fields.py +55 -0
  20. build/lib/build/lib/searchkit/forms/search.py +62 -0
  21. build/lib/build/lib/searchkit/forms/searchkit.py +154 -0
  22. build/lib/build/lib/searchkit/forms/utils.py +178 -0
  23. build/lib/build/lib/searchkit/migrations/0001_initial.py +30 -0
  24. build/lib/build/lib/searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
  25. build/lib/build/lib/searchkit/migrations/__init__.py +0 -0
  26. build/lib/build/lib/searchkit/models.py +27 -0
  27. build/lib/build/lib/searchkit/templatetags/__init__.py +0 -0
  28. build/lib/build/lib/searchkit/templatetags/searchkit.py +20 -0
  29. build/lib/build/lib/searchkit/tests.py +402 -0
  30. build/lib/build/lib/searchkit/urls.py +7 -0
  31. build/lib/build/lib/searchkit/views.py +23 -0
  32. build/lib/searchkit/__version__.py +1 -1
  33. build/lib/searchkit/admin.py +4 -4
  34. build/lib/searchkit/filters.py +7 -11
  35. build/lib/searchkit/forms/__init__.py +5 -3
  36. build/lib/searchkit/forms/search.py +37 -17
  37. build/lib/searchkit/forms/searchkit.py +60 -95
  38. build/lib/searchkit/forms/utils.py +44 -15
  39. build/lib/searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
  40. build/lib/searchkit/models.py +8 -4
  41. build/lib/searchkit/templatetags/searchkit.py +0 -27
  42. build/lib/searchkit/tests.py +201 -49
  43. build/lib/searchkit/urls.py +1 -2
  44. build/lib/searchkit/views.py +11 -18
  45. {django_searchkit-1.0.dist-info → django_searchkit-1.1.dist-info}/METADATA +9 -24
  46. django_searchkit-1.1.dist-info/RECORD +99 -0
  47. example/example/admin.py +1 -1
  48. searchkit/__version__.py +1 -1
  49. searchkit/admin.py +4 -4
  50. searchkit/filters.py +7 -11
  51. searchkit/forms/__init__.py +5 -3
  52. searchkit/forms/search.py +37 -17
  53. searchkit/forms/searchkit.py +60 -95
  54. searchkit/forms/utils.py +44 -15
  55. searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
  56. searchkit/models.py +8 -4
  57. searchkit/templatetags/searchkit.py +0 -27
  58. searchkit/tests.py +201 -49
  59. searchkit/urls.py +1 -2
  60. searchkit/views.py +11 -18
  61. django_searchkit-1.0.dist-info/RECORD +0 -66
  62. {django_searchkit-1.0.dist-info → django_searchkit-1.1.dist-info}/WHEEL +0 -0
  63. {django_searchkit-1.0.dist-info → django_searchkit-1.1.dist-info}/licenses/LICENCE +0 -0
  64. {django_searchkit-1.0.dist-info → django_searchkit-1.1.dist-info}/top_level.txt +0 -0
  65. {django_searchkit-1.0.dist-info → django_searchkit-1.1.dist-info}/zip-safe +0 -0
@@ -0,0 +1,402 @@
1
+ from urllib.parse import urlencode
2
+ from django.test import TestCase
3
+ from django.contrib.contenttypes.models import ContentType
4
+ from django.contrib.auth.models import User
5
+ from django.urls import reverse
6
+ from example.models import ModelA
7
+ from example.management.commands.createtestdata import Command as CreateTestData
8
+ from searchkit.forms.utils import FIELD_PLAN
9
+ from searchkit.forms.utils import SUPPORTED_FIELDS
10
+ from searchkit.forms.utils import ModelTree
11
+ from searchkit.forms import SearchForm
12
+ from searchkit.forms import SearchkitModelForm
13
+ from searchkit.forms import BaseSearchkitFormSet
14
+ from searchkit.forms import searchkit_formset_factory
15
+ from searchkit.models import Search
16
+ from searchkit import __version__
17
+
18
+
19
+ SearchkitFormSet = searchkit_formset_factory(model=ModelA)
20
+ SearchkitForm = SearchkitFormSet.form
21
+
22
+
23
+ INITIAL_DATA = [
24
+ dict(
25
+ field='model_b__chars',
26
+ operator='contains',
27
+ value='anytext',
28
+ ),
29
+ dict(
30
+ field='integer',
31
+ operator='range',
32
+ value=[1, 123],
33
+ ),
34
+ dict(
35
+ field='float',
36
+ operator='gt',
37
+ value='0.3',
38
+ ),
39
+ dict(
40
+ field='decimal',
41
+ operator='lte',
42
+ value='1.23',
43
+ ),
44
+ dict(
45
+ field='date',
46
+ operator='exact',
47
+ value='2025-05-14',
48
+ ),
49
+ dict(
50
+ field='datetime',
51
+ operator='exact',
52
+ value='2025-05-14 08:45',
53
+ )
54
+ ]
55
+
56
+ add_prefix = lambda i: SearchkitFormSet().add_prefix(i)
57
+ contenttype = ContentType.objects.get_for_model(ModelA)
58
+ DEFAULT_PREFIX = SearchkitFormSet.get_default_prefix()
59
+
60
+ def get_form_data(initial_data=INITIAL_DATA):
61
+ count = len(initial_data)
62
+ data = {
63
+ 'name': 'test search', # The name of the search.
64
+ 'searchkit_model': f'{contenttype.pk}', # Data for the searchkit-model form.
65
+ f'{DEFAULT_PREFIX}-TOTAL_FORMS': f'{count}', # Data for the managment form.
66
+ f'{DEFAULT_PREFIX}-INITIAL_FORMS': f'{count}', # Data for the managment form.
67
+ }
68
+ for i, d in enumerate(initial_data):
69
+ prefix = SearchkitFormSet().add_prefix(i)
70
+ for key, value in d.items():
71
+ if isinstance(value, list):
72
+ for i, v in enumerate(value):
73
+ data.update({f'{prefix}-{key}_{i}': v})
74
+ else:
75
+ data.update({f'{prefix}-{key}': value})
76
+ return data
77
+
78
+ FORM_DATA = get_form_data()
79
+
80
+
81
+ class CheckFormMixin:
82
+ """
83
+ Mixin to check the form fields and their choices.
84
+ """
85
+ def check_form(self, form):
86
+ # Three fields should be generated on instantiation.
87
+ self.assertIn('field', form.fields)
88
+ self.assertIn('operator', form.fields)
89
+ self.assertIn('value', form.fields)
90
+ self.assertEqual(len(form.fields), 3)
91
+
92
+ # Check field choices for the model.
93
+ form_model_field = form.fields['field']
94
+ self.assertTrue(form_model_field.choices)
95
+ options = [c[0] for c in form_model_field.choices]
96
+ tree = ModelTree(ModelA)
97
+ for node in tree.iterate():
98
+ for model_field in node.model._meta.fields:
99
+ if not any(isinstance(model_field, f) for f in SUPPORTED_FIELDS):
100
+ continue
101
+ if node.is_root:
102
+ self.assertIn(model_field.name, options)
103
+ else:
104
+ self.assertIn(f'{node.field_path}__{model_field.name}', options)
105
+
106
+ # Check the field_plan choosen based on the model_field.
107
+ field_plan = next(iter([p for t, p in FIELD_PLAN.items() if t(form.model_field)]))
108
+ self.assertEqual(form.field_plan, field_plan)
109
+
110
+ # Check choices of the operator field based on the field_plan.
111
+ operator_field = form.fields['operator']
112
+ self.assertTrue(operator_field.choices)
113
+ self.assertEqual(len(operator_field.choices), len(form.field_plan))
114
+ for operator in form.field_plan.keys():
115
+ self.assertIn(operator, [c[0] for c in operator_field.choices])
116
+
117
+
118
+ class SearchkitFormTestCase(CheckFormMixin, TestCase):
119
+
120
+ def test_blank_searchkitform(self):
121
+ form = SearchkitForm(prefix=add_prefix(0))
122
+ self.check_form(form)
123
+
124
+ # Form should not be bound or valid.
125
+ self.assertFalse(form.is_bound)
126
+ self.assertFalse(form.is_valid())
127
+
128
+ def test_searchkitform_with_invalid_model_field_data(self):
129
+ data = {
130
+ f'{add_prefix(0)}-field': 'foobar',
131
+ }
132
+ form = SearchkitForm(data, prefix=add_prefix(0))
133
+ self.check_form(form)
134
+
135
+ # Form should be invalid.
136
+ self.assertFalse(form.is_valid())
137
+
138
+ # Check error message in html.
139
+ errors = ['Select a valid choice. foobar is not one of the available choices.']
140
+ self.assertIn(errors, form.errors.values())
141
+
142
+ def test_searchkitform_with_valid_model_field_data(self):
143
+ data = {
144
+ f'{add_prefix(0)}-field': 'integer',
145
+ }
146
+ form = SearchkitForm(data, prefix=add_prefix(0))
147
+ self.check_form(form)
148
+
149
+ # Form should be invalid since no value data is provieded.
150
+ self.assertFalse(form.is_valid())
151
+
152
+ def test_searchkitform_with_invalid_operator_data(self):
153
+ data = {
154
+ f'{add_prefix(0)}-field': 'integer',
155
+ f'{add_prefix(0)}-operator': 'foobar',
156
+ }
157
+ form = SearchkitForm(data, prefix=add_prefix(0))
158
+ self.check_form(form)
159
+
160
+ # Form should be invalid.
161
+ self.assertFalse(form.is_valid())
162
+
163
+ # Check error message in html.
164
+ errors = ['Select a valid choice. foobar is not one of the available choices.']
165
+ self.assertIn(errors, form.errors.values())
166
+
167
+ def test_searchkitform_with_valid_operator_data(self):
168
+ data = {
169
+ f'{add_prefix(0)}-field': 'integer',
170
+ f'{add_prefix(0)}-operator': 'exact',
171
+ }
172
+ form = SearchkitForm(data, prefix=add_prefix(0))
173
+ self.check_form(form)
174
+
175
+ # Form should be invalid since no value data is provieded.
176
+ self.assertFalse(form.is_valid())
177
+
178
+ def test_searchkitform_with_valid_data(self):
179
+ data = {
180
+ f'{add_prefix(0)}-field': 'integer',
181
+ f'{add_prefix(0)}-operator': 'exact',
182
+ f'{add_prefix(0)}-value': '123',
183
+ }
184
+ form = SearchkitForm(data, prefix=add_prefix(0))
185
+ self.check_form(form)
186
+
187
+ # Form should be valid.
188
+ self.assertTrue(form.is_valid())
189
+
190
+ def test_searchkitform_with_invalid_data(self):
191
+ data = {
192
+ f'{add_prefix(0)}-field': 'integer',
193
+ f'{add_prefix(0)}-operator': 'exact',
194
+ f'{add_prefix(0)}-value': 'foobar',
195
+ }
196
+ form = SearchkitForm(data, prefix=add_prefix(0))
197
+ self.check_form(form)
198
+
199
+ # Form should be invalid.
200
+ self.assertFalse(form.is_valid())
201
+
202
+ # Check error message in html.
203
+ errors = ['Enter a whole number.']
204
+ self.assertIn(errors, form.errors.values())
205
+
206
+
207
+ class SearchkitFormSetTestCase(CheckFormMixin, TestCase):
208
+ def test_blank_searchkitform(self):
209
+ # Instantiating the formset neither with a model instance nor with model
210
+ # related data or initial data should result in a formset without forms,
211
+ # that is invalid and unbound.
212
+ formset = SearchkitFormSet()
213
+ self.assertFalse(formset.is_bound)
214
+ self.assertFalse(formset.is_valid())
215
+
216
+ def test_searchkit_formset_with_valid_data(self):
217
+ formset = SearchkitFormSet(FORM_DATA)
218
+ self.assertTrue(formset.is_valid())
219
+
220
+ def test_searchkit_formset_with_invalid_data(self):
221
+ data = FORM_DATA.copy()
222
+ del data[f'{add_prefix(0)}-value']
223
+ formset = SearchkitFormSet(data)
224
+ self.assertFalse(formset.is_valid())
225
+
226
+ # Check error message in html.
227
+ errors = ['This field is required.']
228
+ self.assertIn(errors, formset.forms[0].errors.values())
229
+
230
+ def test_searchkit_formset_with_initial_data(self):
231
+ formset_class = searchkit_formset_factory(model=ModelA, extra=0)
232
+ formset = formset_class(initial=INITIAL_DATA)
233
+ self.assertFalse(formset.is_bound)
234
+ self.assertFalse(formset.is_valid())
235
+ self.assertEqual(len(formset.forms), len(INITIAL_DATA))
236
+ for i, form in enumerate(formset.forms):
237
+ self.assertEqual(form.initial, INITIAL_DATA[i])
238
+ self.check_form(form)
239
+
240
+
241
+ class SearchkitSearchFormTestCase(TestCase):
242
+ def test_searchkit_search_form_without_data(self):
243
+ form = SearchForm()
244
+ self.assertFalse(form.is_bound)
245
+ self.assertFalse(form.is_valid())
246
+ self.assertIsInstance(form.formset, BaseSearchkitFormSet)
247
+ self.assertEqual(form.formset.model, None)
248
+
249
+ def test_searchkit_search_form_with_data(self):
250
+ form = SearchForm(FORM_DATA)
251
+ self.assertTrue(form.is_bound)
252
+ self.assertTrue(form.is_valid())
253
+ self.assertIsInstance(form.formset, BaseSearchkitFormSet)
254
+ self.assertEqual(form.formset.model, ModelA)
255
+ self.assertEqual(form.instance.data, form.formset.cleaned_data)
256
+
257
+ # Saving the instance works.
258
+ form.instance.save()
259
+ self.assertTrue(form.instance.pk)
260
+
261
+ # Using the instance data as filter rules works.
262
+ filter_rules = form.instance.as_lookups()
263
+ self.assertEqual(len(filter_rules), len(INITIAL_DATA))
264
+ for data in INITIAL_DATA:
265
+ self.assertIn(f"{data['field']}__{data['operator']}", filter_rules)
266
+ queryset = form.instance.as_queryset()
267
+ self.assertTrue(queryset.model == ModelA)
268
+
269
+
270
+ class SearchkitModelFormTestCase(TestCase):
271
+ def test_searchkit_model_form_choices(self):
272
+ form = SearchkitModelForm()
273
+ labels = [c[1] for c in form.fields['searchkit_model'].choices]
274
+ self.assertEqual(len(labels), 3)
275
+ self.assertEqual('select a model', labels[0].lower())
276
+ self.assertEqual('example | model a', labels[1].lower())
277
+ self.assertEqual('example | model b', labels[2].lower())
278
+
279
+
280
+ class AdminBackendTest(TestCase):
281
+ @classmethod
282
+ def setUpTestData(cls):
283
+ CreateTestData().handle()
284
+
285
+ def setUp(self):
286
+ admin = User.objects.get(username='admin')
287
+ self.client.force_login(admin)
288
+
289
+ def test_search_form(self):
290
+ url = reverse('admin:searchkit_search_add')
291
+ resp = self.client.get(url)
292
+ self.assertEqual(resp.status_code, 200)
293
+ select = b'<select name="searchkit_model" class="searchkit-reload-on-change" data-total-forms="1" required id="id_searchkit_model">'
294
+ for snippet in select.split(b' '):
295
+ self.assertIn(snippet, resp.content)
296
+
297
+ def test_search_form_with_initial(self):
298
+ url = reverse('admin:searchkit_search_add') + '?searchkit_model=1'
299
+ resp = self.client.get(url)
300
+ self.assertEqual(resp.status_code, 200)
301
+ select = '<select name="searchkit_model" class="searchkit-reload-on-change" data-total-forms="1" required id="id_searchkit_model">'
302
+ for snippet in select.split(' '):
303
+ self.assertIn(snippet, str(resp.content))
304
+ self.assertIn('<option value="1" selected>', str(resp.content))
305
+ self.assertIn('name="searchkit-example-modela-0-field"', str(resp.content))
306
+
307
+ def test_add_search(self):
308
+ # Create a search object via the admin backend.
309
+ url = reverse('admin:searchkit_search_add')
310
+ data = FORM_DATA.copy()
311
+ data['_save_and_apply'] = True
312
+ resp = self.client.post(url, data, follow=True)
313
+ self.assertEqual(resp.status_code, 200)
314
+ self.assertEqual(len(Search.objects.all()), 1)
315
+
316
+ # Change it via backend.
317
+ url = reverse('admin:searchkit_search_change', args=(1,))
318
+ data['name'] = 'Changed name'
319
+ data['searchkit-example-modela-0-field'] = 'boolean'
320
+ data['searchkit-example-modela-0-operator'] = 'exact'
321
+ data['searchkit-example-modela-0-value'] = 'true'
322
+ resp = self.client.post(url, data, follow=True)
323
+ self.assertEqual(resp.status_code, 200)
324
+ self.assertEqual(Search.objects.get(pk=1).name, data['name'])
325
+
326
+ # Will the search be listed in the admin filter?
327
+ url = reverse('admin:example_modela_changelist')
328
+ resp = self.client.get(url)
329
+ self.assertEqual(resp.status_code, 200)
330
+ self.assertIn('href="?search=1"', str(resp.content))
331
+ self.assertIn(data['name'], str(resp.content))
332
+
333
+
334
+ class SearchViewTest(TestCase):
335
+
336
+ def setUp(self):
337
+ self.initial = [
338
+ dict(
339
+ field='integer',
340
+ operator='exact',
341
+ value=1,
342
+ )
343
+ ]
344
+ self.initial_range = [
345
+ dict(
346
+ field='integer',
347
+ operator='range',
348
+ value=[1,3],
349
+ )
350
+ ]
351
+
352
+ def test_search_view_invalid_data(self):
353
+ initial = self.initial.copy()
354
+ initial[0]['value'] = 'no integer'
355
+ data = get_form_data(initial)
356
+ url_params = urlencode(data)
357
+ base_url = reverse('searchkit_form')
358
+ url = f'{base_url}?{url_params}'
359
+ resp = self.client.get(url)
360
+ self.assertEqual(resp.status_code, 200)
361
+ html_error = '<li>Enter a whole number.</li>'
362
+ self.assertInHTML(html_error, str(resp.content))
363
+
364
+ def test_search_view_missing_data(self):
365
+ initial = self.initial.copy()
366
+ del(initial[0]['value'])
367
+ data = get_form_data(initial)
368
+ url_params = urlencode(data)
369
+ base_url = reverse('searchkit_form')
370
+ url = f'{base_url}?{url_params}'
371
+ resp = self.client.get(url)
372
+ self.assertEqual(resp.status_code, 200)
373
+ html_error = '<li>This field is required.</li>'
374
+ self.assertInHTML(html_error, str(resp.content))
375
+
376
+ def test_search_view_with_range_operator(self):
377
+ data = get_form_data(self.initial_range)
378
+ url_params = urlencode(data)
379
+ base_url = reverse('searchkit_form')
380
+ url = f'{base_url}?{url_params}'
381
+ resp = self.client.get(url)
382
+ self.assertEqual(resp.status_code, 200)
383
+ html = '<input type="number" name="searchkit-example-modela-0-value_1" value="3" id="id_searchkit-example-modela-0-value_1">'
384
+ self.assertInHTML(html, str(resp.content))
385
+
386
+ def test_search_view_with_model(self):
387
+ data = get_form_data(self.initial)
388
+ data['searchkit_model'] = ContentType.objects.get_for_model(ModelA).pk
389
+ url_params = urlencode(data)
390
+ base_url = reverse('searchkit_form')
391
+ url = f'{base_url}?{url_params}'
392
+ resp = self.client.get(url)
393
+ self.assertEqual(resp.status_code, 200)
394
+
395
+ def test_search_view_with_invalid_model(self):
396
+ data = get_form_data(self.initial)
397
+ data['searchkit_model'] = 9999 # Non-existing content type.
398
+ url_params = urlencode(data)
399
+ base_url = reverse('searchkit_form')
400
+ url = f'{base_url}?{url_params}'
401
+ resp = self.client.get(url)
402
+ self.assertEqual(resp.status_code, 400)
@@ -0,0 +1,7 @@
1
+ from django.urls import path
2
+ from .views import SearchkitAjaxView
3
+
4
+
5
+ urlpatterns = [
6
+ path("searchkit/", SearchkitAjaxView.as_view(), name="searchkit_form"),
7
+ ]
@@ -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
+ contenttype_form = SearchkitModelForm(data=self.request.GET)
18
+ if contenttype_form.is_valid():
19
+ model = contenttype_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.'))
@@ -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.1"
@@ -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):
@@ -1,8 +1,6 @@
1
1
  from django.contrib.admin import SimpleListFilter
2
2
  from django.contrib.contenttypes.models import ContentType
3
- from .models import SearchkitSearch
4
- from .forms import SearchkitFormSet
5
- from .forms.utils import get_filter_rules
3
+ from .models import Search
6
4
 
7
5
 
8
6
  class SearchkitFilter(SimpleListFilter):
@@ -11,21 +9,19 @@ class SearchkitFilter(SimpleListFilter):
11
9
  template = 'searchkit/searchkit_filter.html'
12
10
 
13
11
  def __init__(self, request, params, model, model_admin):
14
- # We need the app_label and model_name for the reverse url lookup in the
15
- # template.
16
- self.app_label = model._meta.app_label
17
- self.model_name = model._meta.model_name
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)
18
15
  super().__init__(request, params, model, model_admin)
19
16
 
20
17
  def lookups(self, request, model_admin):
21
18
  # Fetch the last three objects from SearchkitSearch and return them as
22
19
  # choices.
23
- ct = ContentType.objects.get_for_model(model_admin.model)
24
- searches = SearchkitSearch.objects.filter(contenttype=ct).order_by('-created_date')[:3]
20
+ searches = Search.objects.filter(contenttype=self.searchkit_model).order_by('-created_date')[:3]
25
21
  return [(str(obj.id), obj.name) for obj in searches]
26
22
 
27
23
  def queryset(self, request, queryset):
28
24
  # Filter the queryset based on the selected SearchkitSearch object
29
25
  if self.value():
30
- search = SearchkitSearch.objects.get(id=int(self.value()))
31
- return queryset.filter(**search.get_filter_rules())
26
+ search = Search.objects.get(id=int(self.value()))
27
+ return search.as_queryset()
@@ -1,3 +1,5 @@
1
- from .search import SearchkitSearchForm
2
- from .searchkit import SearchkitFormSet
3
- from .searchkit import SearchkitForm
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
@@ -1,22 +1,40 @@
1
1
  from django import forms
2
2
  from django.utils.functional import cached_property
3
- from ..models import SearchkitSearch
4
- from .searchkit import SearchkitFormSet
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
5
8
 
6
9
 
7
- class SearchkitSearchForm(forms.ModelForm):
10
+ class SearchForm(MediaMixin, forms.ModelForm):
8
11
  """
9
12
  Represents a SearchkitSearch model. Using a SearchkitFormSet for the data
10
13
  json field.
11
14
  """
12
15
  class Meta:
13
- model = SearchkitSearch
16
+ model = Search
14
17
  fields = ['name']
15
18
 
16
- @property
17
- def media(self):
18
- # TODO: Check if child classes inherit those media files.
19
- return self.formset.media
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)
20
38
 
21
39
  @cached_property
22
40
  def formset(self):
@@ -24,19 +42,21 @@ class SearchkitSearchForm(forms.ModelForm):
24
42
  A searchkit formset for the model.
25
43
  """
26
44
  kwargs = dict()
27
- kwargs['data'] = self.data or None
28
- kwargs['prefix'] = self.prefix
29
- if self.instance.pk:
30
- kwargs['model'] = self.instance.contenttype.model_class()
31
- kwargs['initial'] = self.instance.data
32
- return SearchkitFormSet(**kwargs)
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)
33
53
 
34
54
  def is_valid(self):
35
- return self.formset.is_valid() and super().is_valid()
55
+ return self.formset.is_valid() and self.searchkit_model_form.is_valid and super().is_valid()
36
56
 
37
57
  def clean(self):
38
- if self.formset.contenttype_form.is_valid():
39
- self.instance.contenttype = self.formset.contenttype_form.cleaned_data['contenttype']
58
+ if self.searchkit_model_form.is_valid():
59
+ self.instance.contenttype = self.searchkit_model_form.cleaned_data['searchkit_model']
40
60
  if self.formset.is_valid():
41
61
  self.instance.data = self.formset.cleaned_data
42
62
  return super().clean()