django-searchkit 0.1__py3-none-any.whl → 1.0__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 (42) hide show
  1. build/lib/example/example/__init__.py +0 -0
  2. build/lib/example/example/admin.py +16 -0
  3. build/lib/example/example/asgi.py +16 -0
  4. build/lib/example/example/management/__init__.py +0 -0
  5. build/lib/example/example/management/commands/__init__.py +0 -0
  6. build/lib/example/example/management/commands/createtestdata.py +62 -0
  7. build/lib/example/example/migrations/0001_initial.py +48 -0
  8. build/lib/example/example/migrations/__init__.py +0 -0
  9. build/lib/example/example/models.py +38 -0
  10. build/lib/example/example/settings.py +125 -0
  11. build/lib/example/example/urls.py +23 -0
  12. build/lib/example/example/wsgi.py +16 -0
  13. build/lib/searchkit/__init__.py +0 -0
  14. build/lib/searchkit/__version__.py +16 -0
  15. build/lib/searchkit/admin.py +30 -0
  16. build/lib/searchkit/apps.py +6 -0
  17. build/lib/searchkit/filters.py +31 -0
  18. build/lib/searchkit/forms/__init__.py +3 -0
  19. build/lib/searchkit/forms/fields.py +55 -0
  20. build/lib/searchkit/forms/search.py +42 -0
  21. build/lib/searchkit/forms/searchkit.py +189 -0
  22. build/lib/searchkit/forms/utils.py +149 -0
  23. build/lib/searchkit/migrations/0001_initial.py +30 -0
  24. build/lib/searchkit/migrations/__init__.py +0 -0
  25. build/lib/searchkit/models.py +23 -0
  26. build/lib/searchkit/templatetags/__init__.py +0 -0
  27. build/lib/searchkit/templatetags/searchkit.py +47 -0
  28. build/lib/searchkit/tests.py +250 -0
  29. build/lib/searchkit/urls.py +8 -0
  30. build/lib/searchkit/views.py +30 -0
  31. {django_searchkit-0.1.dist-info → django_searchkit-1.0.dist-info}/METADATA +44 -8
  32. django_searchkit-1.0.dist-info/RECORD +66 -0
  33. {django_searchkit-0.1.dist-info → django_searchkit-1.0.dist-info}/WHEEL +1 -1
  34. {django_searchkit-0.1.dist-info → django_searchkit-1.0.dist-info}/top_level.txt +1 -0
  35. searchkit/__version__.py +1 -1
  36. searchkit/forms/__init__.py +2 -1
  37. searchkit/forms/search.py +0 -3
  38. searchkit/forms/searchkit.py +11 -14
  39. searchkit/tests.py +121 -68
  40. django_searchkit-0.1.dist-info/RECORD +0 -36
  41. {django_searchkit-0.1.dist-info → django_searchkit-1.0.dist-info}/licenses/LICENCE +0 -0
  42. {django_searchkit-0.1.dist-info → django_searchkit-1.0.dist-info}/zip-safe +0 -0
@@ -0,0 +1,8 @@
1
+ from django.urls import path
2
+ from .views import SearchkitAjaxView
3
+
4
+
5
+ urlpatterns = [
6
+ path("searchkit/form/", SearchkitAjaxView.as_view(), name="searchkit_form"),
7
+ path("searchkit/form/<slug:app_label>/<slug:model_name>/", SearchkitAjaxView.as_view(), name="searchkit_form_model"),
8
+ ]
@@ -0,0 +1,30 @@
1
+ from django.utils.translation import gettext_lazy as _
2
+ from django.http import HttpResponse
3
+ from django.http import Http404
4
+ from django.apps import apps
5
+ from django.views.generic import View
6
+ from .forms import SearchkitFormSet
7
+
8
+
9
+ # FIXME: Check permissions and authentication.
10
+ class SearchkitAjaxView(View):
11
+ """
12
+ Reload the formset via ajax.
13
+ """
14
+ def get_model(self, **kwargs):
15
+ """
16
+ Get the model from the URL parameters.
17
+ """
18
+ if all(k in kwargs for k in ('app_label', 'model_name')):
19
+ app_label, model_name = kwargs['app_label'], kwargs['model_name']
20
+ try:
21
+ return apps.get_model(app_label=app_label, model_name=model_name)
22
+ except LookupError:
23
+ raise Http404(_('Model %s.%s not found') % (app_label, model_name))
24
+ else:
25
+ return None
26
+
27
+ def get(self, request, **kwargs):
28
+ model = self.get_model(**kwargs)
29
+ formset = SearchkitFormSet(data=request.GET, model=model)
30
+ return HttpResponse(formset.render())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-searchkit
3
- Version: 0.1
3
+ Version: 1.0
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,7 +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.0
13
12
  Classifier: Framework :: Django :: 3.1
14
13
  Classifier: Framework :: Django :: 3.2
15
14
  Classifier: Framework :: Django :: 4.0
@@ -33,7 +32,8 @@ Classifier: Topic :: Software Development
33
32
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
34
33
  Description-Content-Type: text/markdown
35
34
  License-File: LICENCE
36
- Requires-Dist: Django>=3.0
35
+ Requires-Dist: Django>=3.1
36
+ Requires-Dist: django-picklefield>=2.0
37
37
  Dynamic: author
38
38
  Dynamic: author-email
39
39
  Dynamic: classifier
@@ -51,12 +51,18 @@ Dynamic: summary
51
51
  [<img src="https://github.com/thomst/django-searchkit/actions/workflows/ci.yml/badge.svg">](https://github.com/thomst/django-searchkit/)
52
52
  [<img src="https://coveralls.io/repos/github/thomst/django-searchkit/badge.svg?branch=main">](https://coveralls.io/github/thomst/django-searchkit?branch=main)
53
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-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-orange)
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)
55
55
 
56
56
 
57
57
  ## Description
58
58
 
59
- TODO
59
+ Finally there is a real searchkit application for django that integrates best
60
+ with the django admin backend.
61
+
62
+ Build and apply complex searches on model instances right in the backend without
63
+ any coding. Save and reuse your searches by a handy django admin filter with a
64
+ single click.
65
+
60
66
 
61
67
  ## Setup
62
68
 
@@ -73,10 +79,40 @@ INSTALLED_APPS = [
73
79
  ]
74
80
  ```
75
81
 
76
- ## Getting started
82
+ Add the `SearkitFilter` to your `ModelAdmin`:
83
+ ```
84
+ from django.contrib import admin
85
+ from searchkit.filters import SearchkitFilter
86
+ from .models import MyModel
77
87
 
78
- TODO
88
+
89
+ @admin.register(MyModel)
90
+ class MyModelAdmin(admin.ModelAdmin):
91
+ ...
92
+ list_filter = [
93
+ SearchkitFilter,
94
+ ...
95
+ ]
96
+ ...
97
+ ```
79
98
 
80
99
  ## Usage
81
100
 
82
- TODO
101
+ 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.
104
+ 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.
@@ -0,0 +1,66 @@
1
+ build/lib/example/example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ build/lib/example/example/admin.py,sha256=eTZ5mCA1ToGouqYgIf-6xDl2nvYAz7T9lU1Bnu_gP9c,462
3
+ build/lib/example/example/asgi.py,sha256=mqLnzcCH9X-8UhDo5ZhcM8tF3jtALEGSjsBmphbkLcs,391
4
+ build/lib/example/example/models.py,sha256=fB9X782cX01Yg2uIiPCVgrS8C7DkgG9U-9mxGtX3P7Q,1097
5
+ build/lib/example/example/settings.py,sha256=du9bWbAceSvDkYeNYOc_3KxKL1p1se93TdAueT2RQ_E,3256
6
+ build/lib/example/example/urls.py,sha256=D8LxjiTdJSA87AVh3BzpVo5E2aC30hlp20HshPDrfvU,800
7
+ build/lib/example/example/wsgi.py,sha256=OAXHHeNQoj7FBDc5NMET39i_kIGpFrcdNDdY1thrh58,391
8
+ build/lib/example/example/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ build/lib/example/example/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ build/lib/example/example/management/commands/createtestdata.py,sha256=IVtyUqak133h9Ua-Ag_GP8mw3dUdiDYAa2tGmLkKluU,2737
11
+ build/lib/example/example/migrations/0001_initial.py,sha256=62tOi17oHGuqqLmSAYWLep6AMkxCJF6Tp13odghS7gI,2014
12
+ build/lib/example/example/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ build/lib/searchkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ build/lib/searchkit/__version__.py,sha256=gc1Zs90La5le9aifTQBFNfNHJn3SMI23TH8V2w-DQb4,661
15
+ build/lib/searchkit/admin.py,sha256=75E1jff9LDmzRWNAJ-mpW4yChKtuE1L1WFXeCmCiibI,1218
16
+ build/lib/searchkit/apps.py,sha256=17m6wPu5N59Dq4MLoWaQd0aNHEzMT4z4emQD632GIMA,150
17
+ build/lib/searchkit/filters.py,sha256=TSsK8K0Oj7-gTfHLq7zLacZyXeGIM5MtLAHNMbjxYIo,1343
18
+ build/lib/searchkit/models.py,sha256=pRCWOnwbxroUzyS8SFBSLyAUn0I7udiXP1wIxGB307A,894
19
+ build/lib/searchkit/tests.py,sha256=kV4p6yoThhUH1Y_e8uH3bOrjG4Et9xcuyT9uwAl0Cqg,9237
20
+ build/lib/searchkit/urls.py,sha256=bqF5UAcUFPfBCCi1CljHSLbjEqHqzj-ripmJ6VbtC7s,289
21
+ build/lib/searchkit/views.py,sha256=EAA2yyRMsD8wva2F5lKo-X9-mxvFHOqn7bGazS-XNFk,1051
22
+ build/lib/searchkit/forms/__init__.py,sha256=BEogTSn1yPliVfgQ5ywXG9lTo1wtGABlYNYPStBRP9w,116
23
+ build/lib/searchkit/forms/fields.py,sha256=TpFZdAyWKaMhOoZFTuy5ODNrW6RLy289Lk1fcMSGJ20,1621
24
+ build/lib/searchkit/forms/search.py,sha256=GaGW_srrYD2qth6smThjwW0CwO5bNBxh5kLME-atFZY,1316
25
+ build/lib/searchkit/forms/searchkit.py,sha256=asiS2hEqCTagEWmZ_1NZk-DCVOACxbpbYDsHNh-iBNQ,7237
26
+ build/lib/searchkit/forms/utils.py,sha256=QtCsCveq20eaHc-nWP36-ICPq_CgItr5ckNp2DQ6LAc,4381
27
+ build/lib/searchkit/migrations/0001_initial.py,sha256=cP886X1UMrDTf-KxfHfuoQ4b1T1lhdNJyVNOGMNyWNw,1093
28
+ build/lib/searchkit/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ build/lib/searchkit/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ build/lib/searchkit/templatetags/searchkit.py,sha256=QA-hqxsHJ7vRoRLqFmGeRqRErkywv1pmepVFKqZuKcg,1238
31
+ django_searchkit-1.0.dist-info/licenses/LICENCE,sha256=oqmlRYPA5GHG2T1yp8fPFGQdOO-NnJPdGITeFiJYWLg,1521
32
+ example/example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ example/example/admin.py,sha256=eTZ5mCA1ToGouqYgIf-6xDl2nvYAz7T9lU1Bnu_gP9c,462
34
+ example/example/asgi.py,sha256=mqLnzcCH9X-8UhDo5ZhcM8tF3jtALEGSjsBmphbkLcs,391
35
+ example/example/models.py,sha256=fB9X782cX01Yg2uIiPCVgrS8C7DkgG9U-9mxGtX3P7Q,1097
36
+ example/example/settings.py,sha256=du9bWbAceSvDkYeNYOc_3KxKL1p1se93TdAueT2RQ_E,3256
37
+ example/example/urls.py,sha256=D8LxjiTdJSA87AVh3BzpVo5E2aC30hlp20HshPDrfvU,800
38
+ example/example/wsgi.py,sha256=OAXHHeNQoj7FBDc5NMET39i_kIGpFrcdNDdY1thrh58,391
39
+ example/example/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
+ example/example/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ example/example/management/commands/createtestdata.py,sha256=IVtyUqak133h9Ua-Ag_GP8mw3dUdiDYAa2tGmLkKluU,2737
42
+ example/example/migrations/0001_initial.py,sha256=62tOi17oHGuqqLmSAYWLep6AMkxCJF6Tp13odghS7gI,2014
43
+ example/example/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
+ searchkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ searchkit/__version__.py,sha256=gc1Zs90La5le9aifTQBFNfNHJn3SMI23TH8V2w-DQb4,661
46
+ searchkit/admin.py,sha256=75E1jff9LDmzRWNAJ-mpW4yChKtuE1L1WFXeCmCiibI,1218
47
+ searchkit/apps.py,sha256=17m6wPu5N59Dq4MLoWaQd0aNHEzMT4z4emQD632GIMA,150
48
+ searchkit/filters.py,sha256=TSsK8K0Oj7-gTfHLq7zLacZyXeGIM5MtLAHNMbjxYIo,1343
49
+ searchkit/models.py,sha256=pRCWOnwbxroUzyS8SFBSLyAUn0I7udiXP1wIxGB307A,894
50
+ searchkit/tests.py,sha256=kV4p6yoThhUH1Y_e8uH3bOrjG4Et9xcuyT9uwAl0Cqg,9237
51
+ searchkit/urls.py,sha256=bqF5UAcUFPfBCCi1CljHSLbjEqHqzj-ripmJ6VbtC7s,289
52
+ searchkit/views.py,sha256=EAA2yyRMsD8wva2F5lKo-X9-mxvFHOqn7bGazS-XNFk,1051
53
+ searchkit/forms/__init__.py,sha256=BEogTSn1yPliVfgQ5ywXG9lTo1wtGABlYNYPStBRP9w,116
54
+ searchkit/forms/fields.py,sha256=TpFZdAyWKaMhOoZFTuy5ODNrW6RLy289Lk1fcMSGJ20,1621
55
+ searchkit/forms/search.py,sha256=GaGW_srrYD2qth6smThjwW0CwO5bNBxh5kLME-atFZY,1316
56
+ searchkit/forms/searchkit.py,sha256=asiS2hEqCTagEWmZ_1NZk-DCVOACxbpbYDsHNh-iBNQ,7237
57
+ searchkit/forms/utils.py,sha256=QtCsCveq20eaHc-nWP36-ICPq_CgItr5ckNp2DQ6LAc,4381
58
+ searchkit/migrations/0001_initial.py,sha256=cP886X1UMrDTf-KxfHfuoQ4b1T1lhdNJyVNOGMNyWNw,1093
59
+ searchkit/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
+ searchkit/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
+ searchkit/templatetags/searchkit.py,sha256=QA-hqxsHJ7vRoRLqFmGeRqRErkywv1pmepVFKqZuKcg,1238
62
+ django_searchkit-1.0.dist-info/METADATA,sha256=L0LZJphBHkr9H_5emLV_bs01GcvqN6miju_RT0vXjv4,4015
63
+ django_searchkit-1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
64
+ django_searchkit-1.0.dist-info/top_level.txt,sha256=-4gF42VIaG-ckn2rb2wFa7LhxIZymHBsYVedNOr_NIY,29
65
+ django_searchkit-1.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
66
+ django_searchkit-1.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,3 +1,4 @@
1
+ build
1
2
  dist
2
3
  example
3
4
  searchkit
searchkit/__version__.py CHANGED
@@ -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__ = "0.1"
16
+ __version__ = "1.0"
@@ -1,2 +1,3 @@
1
1
  from .search import SearchkitSearchForm
2
- from .searchkit import SearchkitFormSet
2
+ from .searchkit import SearchkitFormSet
3
+ from .searchkit import SearchkitForm
searchkit/forms/search.py CHANGED
@@ -13,9 +13,6 @@ class SearchkitSearchForm(forms.ModelForm):
13
13
  model = SearchkitSearch
14
14
  fields = ['name']
15
15
 
16
- def __init__(self, *args, **kwargs):
17
- super().__init__(*args, **kwargs)
18
-
19
16
  @property
20
17
  def media(self):
21
18
  # TODO: Check if child classes inherit those media files.
@@ -56,7 +56,7 @@ class SearchkitForm(CSS_CLASSES, forms.Form):
56
56
  # Do we have a valid value?
57
57
  return self.fields[field_name].clean(self.unprefixed_data[field_name])
58
58
  except forms.ValidationError:
59
- pass
59
+ return self.fields[field_name].choices[0][0]
60
60
  else:
61
61
  # At last simply return the first option which will be the selected
62
62
  # one.
@@ -115,7 +115,7 @@ class ContentTypeForm(CSS_CLASSES, forms.Form):
115
115
  Form to select a content type.
116
116
  """
117
117
  contenttype = forms.ModelChoiceField(
118
- queryset=ContentType.objects.all(),
118
+ queryset=ContentType.objects.all(), # FIXME: Limit choices to models that can be filtered.
119
119
  label=_('Model'),
120
120
  empty_label=_('Select a Model'),
121
121
  widget=forms.Select(attrs={"class": CSS_CLASSES.reload_on_change_css_class}),
@@ -140,26 +140,23 @@ class BaseSearchkitFormset(CSS_CLASSES, forms.BaseFormSet):
140
140
  contenttype_form_class = ContentTypeForm
141
141
 
142
142
  def __init__(self, *args, **kwargs):
143
- self.contenttype_form = self.get_conttenttype_form(kwargs)
144
- self.model = self.get_model(kwargs)
143
+ self.model = kwargs.pop('model', None)
145
144
  super().__init__(*args, **kwargs)
145
+ self.contenttype_form = self.get_conttenttype_form(kwargs)
146
+ if not self.model and self.contenttype_form.is_valid():
147
+ self.model = self.contenttype_form.cleaned_data.get('contenttype').model_class()
146
148
  if self.initial:
147
149
  self.extra = 0
148
150
 
149
151
  def get_conttenttype_form(self, kwargs):
150
152
  ct_kwargs = dict()
151
- ct_kwargs['data'] = kwargs.get('data')
152
- ct_kwargs['prefix'] = kwargs.get('prefix')
153
- if model := kwargs.pop('model', None):
154
- ct_kwargs['initial'] = dict(contenttype=ContentType.objects.get_for_model(model))
153
+ ct_kwargs['data'] = self.data or None
154
+ ct_kwargs['prefix'] = self.prefix
155
+ if self.model:
156
+ contenttype = ContentType.objects.get_for_model(self.model)
157
+ ct_kwargs['initial'] = dict(contenttype=contenttype)
155
158
  return self.contenttype_form_class(**ct_kwargs)
156
159
 
157
- def get_model(self, kwargs):
158
- if self.contenttype_form.initial:
159
- return self.contenttype_form.initial['contenttype'].model_class()
160
- elif self.contenttype_form.is_valid():
161
- return self.contenttype_form.cleaned_data['contenttype'].model_class()
162
-
163
160
  def get_form_kwargs(self, index):
164
161
  kwargs = self.form_kwargs.copy()
165
162
  kwargs['model'] = self.model
searchkit/tests.py CHANGED
@@ -1,22 +1,25 @@
1
1
  from django.test import TestCase
2
+ from django.contrib.contenttypes.models import ContentType
2
3
  from django import forms
3
4
  from example.models import ModelA
4
- from .searchkit import FIELD_PLAN
5
- from .searchkit import SearchkitForm
6
- from .searchkit import SearchkitFormSet
5
+ from searchkit.forms.utils import FIELD_PLAN
6
+ from searchkit.forms.utils import SUPPORTED_FIELDS
7
+ from searchkit.forms.utils import SUPPORTED_RELATIONS
8
+ from searchkit.forms import SearchkitSearchForm
9
+ from searchkit.forms import SearchkitForm
10
+ from searchkit.forms import SearchkitFormSet
7
11
 
8
12
 
9
13
  INITIAL_DATA = [
10
14
  dict(
11
- field='chars',
15
+ field='model_b__chars',
12
16
  operator='exact',
13
17
  value='anytext',
14
18
  ),
15
19
  dict(
16
20
  field='integer',
17
21
  operator='range',
18
- value_0=1,
19
- value_1=123,
22
+ value=[1, 123],
20
23
  ),
21
24
  dict(
22
25
  field='float',
@@ -40,18 +43,26 @@ INITIAL_DATA = [
40
43
  )
41
44
  ]
42
45
 
46
+ add_prefix = lambda i: SearchkitFormSet(model=ModelA).add_prefix(i)
43
47
  DEFAULT_PREFIX = SearchkitFormSet.get_default_prefix()
44
48
  FORM_DATA = {
45
- f'{DEFAULT_PREFIX}-TOTAL_FORMS': '6',
46
- f'{DEFAULT_PREFIX}-INITIAL_FORMS': '1'
49
+ 'name': 'test search', # The name of the search.
50
+ f'{DEFAULT_PREFIX}-TOTAL_FORMS': '6', # Data for the managment form.
51
+ f'{DEFAULT_PREFIX}-INITIAL_FORMS': '1', # Data for the managment form.
52
+ f'{DEFAULT_PREFIX}-contenttype': f'{ContentType.objects.get_for_model(ModelA).pk}',
53
+ f'{add_prefix(1)}-value_0': '1', # Data for the range operator.
54
+ f'{add_prefix(1)}-value_1': '123', # Data for the range operator.
47
55
  }
48
- for i, data in enumerate(INITIAL_DATA):
56
+ for i, data in enumerate(INITIAL_DATA, 0):
57
+ prefix = SearchkitFormSet(model=ModelA).add_prefix(i)
49
58
  for key, value in data.items():
50
- FORM_DATA.update({f'{DEFAULT_PREFIX}-{i}-{key}': value})
59
+ FORM_DATA.update({f'{prefix}-{key}': value})
51
60
 
52
61
 
53
- class SearchkitFormTestCase(TestCase):
54
-
62
+ class CheckFormMixin:
63
+ """
64
+ Mixin to check the form fields and their choices.
65
+ """
55
66
  def check_form(self, form):
56
67
  # Three fields should be generated on instantiation.
57
68
  self.assertIn('field', form.fields)
@@ -62,9 +73,19 @@ class SearchkitFormTestCase(TestCase):
62
73
  # Check choices of the model_field.
63
74
  form_model_field = form.fields['field']
64
75
  self.assertTrue(form_model_field.choices)
65
- self.assertEqual(len(form_model_field.choices), len(ModelA._meta.fields))
76
+ options = [c[0] for c in form_model_field.choices]
77
+ for model_field in ModelA._meta.fields:
78
+ if isinstance(model_field, tuple(SUPPORTED_FIELDS)):
79
+ self.assertIn(model_field.name, options)
80
+
81
+ # Check choices for relational lookups.
66
82
  for model_field in ModelA._meta.fields:
67
- self.assertIn(model_field.name, [c[0] for c in form_model_field.choices])
83
+ if isinstance(model_field, tuple(SUPPORTED_RELATIONS)):
84
+ remote_fields = model_field.remote_field.model._meta.fields
85
+ for remote_field in remote_fields:
86
+ if isinstance(model_field, tuple(SUPPORTED_FIELDS)):
87
+ lookup_path = f'{model_field.name}__{remote_field.name}'
88
+ self.assertIn(lookup_path, options)
68
89
 
69
90
  # Check the field_plan choosen based on the model_field.
70
91
  field_plan = next(iter([p for t, p in FIELD_PLAN.items() if t(form.model_field)]))
@@ -73,27 +94,26 @@ class SearchkitFormTestCase(TestCase):
73
94
  # Check choices of the operator field based on the field_plan.
74
95
  operator_field = form.fields['operator']
75
96
  self.assertTrue(operator_field.choices)
76
- self.assertEqual(len(form_model_field.choices), len(form.field_plan))
97
+ self.assertEqual(len(operator_field.choices), len(form.field_plan))
77
98
  for operator in form.field_plan.keys():
78
99
  self.assertIn(operator, [c[0] for c in operator_field.choices])
79
100
 
80
101
 
102
+ class SearchkitFormTestCase(CheckFormMixin, TestCase):
103
+
81
104
  def test_blank_searchkitform(self):
82
- for index in range(3):
83
- prefix = SearchkitFormSet(ModelA).add_prefix(index)
84
- form = SearchkitForm(ModelA, prefix=prefix)
85
- self.check_form(form)
105
+ form = SearchkitForm(ModelA, prefix=add_prefix(0))
106
+ self.check_form(form)
86
107
 
87
- # Form should not be bound or valid.
88
- self.assertFalse(form.is_bound)
89
- self.assertFalse(form.is_valid())
108
+ # Form should not be bound or valid.
109
+ self.assertFalse(form.is_bound)
110
+ self.assertFalse(form.is_valid())
90
111
 
91
112
  def test_searchkitform_with_invalid_model_field_data(self):
92
- prefix = SearchkitFormSet(ModelA).add_prefix(0)
93
113
  data = {
94
- f'{prefix}-field': 'foobar',
114
+ f'{add_prefix(0)}-field': 'foobar',
95
115
  }
96
- form = SearchkitForm(ModelA, data, prefix=prefix)
116
+ form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
97
117
  self.check_form(form)
98
118
 
99
119
  # Form should be invalid.
@@ -101,26 +121,24 @@ class SearchkitFormTestCase(TestCase):
101
121
 
102
122
  # Check error message in html.
103
123
  errors = ['Select a valid choice. foobar is not one of the available choices.']
104
- self.assertFormError(form, 'field', errors)
124
+ self.assertIn(errors, form.errors.values())
105
125
 
106
126
  def test_searchkitform_with_valid_model_field_data(self):
107
- prefix = SearchkitFormSet(ModelA).add_prefix(0)
108
127
  data = {
109
- f'{prefix}-field': 'integer',
128
+ f'{add_prefix(0)}-field': 'integer',
110
129
  }
111
- form = SearchkitForm(ModelA, data, prefix=prefix)
130
+ form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
112
131
  self.check_form(form)
113
132
 
114
- # Form should be invalid.
133
+ # Form should be invalid since no value data is provieded.
115
134
  self.assertFalse(form.is_valid())
116
135
 
117
136
  def test_searchkitform_with_invalid_operator_data(self):
118
- prefix = SearchkitFormSet(ModelA).add_prefix(0)
119
137
  data = {
120
- f'{prefix}-field': 'integer',
121
- f'{prefix}-operator': 'foobar',
138
+ f'{add_prefix(0)}-field': 'integer',
139
+ f'{add_prefix(0)}-operator': 'foobar',
122
140
  }
123
- form = SearchkitForm(ModelA, data, prefix=prefix)
141
+ form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
124
142
  self.check_form(form)
125
143
 
126
144
  # Form should be invalid.
@@ -128,45 +146,38 @@ class SearchkitFormTestCase(TestCase):
128
146
 
129
147
  # Check error message in html.
130
148
  errors = ['Select a valid choice. foobar is not one of the available choices.']
131
- self.assertFormError(form, 'operator', errors)
149
+ self.assertIn(errors, form.errors.values())
132
150
 
133
151
  def test_searchkitform_with_valid_operator_data(self):
134
- prefix = SearchkitFormSet(ModelA).add_prefix(0)
135
152
  data = {
136
- f'{prefix}-field': 'integer',
137
- f'{prefix}-operator': 'exact',
153
+ f'{add_prefix(0)}-field': 'integer',
154
+ f'{add_prefix(0)}-operator': 'exact',
138
155
  }
139
- form = SearchkitForm(ModelA, data, prefix=prefix)
156
+ form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
140
157
  self.check_form(form)
141
158
 
142
- # Form should be invalid.
159
+ # Form should be invalid since no value data is provieded.
143
160
  self.assertFalse(form.is_valid())
144
161
 
145
162
  def test_searchkitform_with_valid_data(self):
146
- prefix = SearchkitFormSet(ModelA).add_prefix(0)
147
163
  data = {
148
- f'{prefix}-field': 'integer',
149
- f'{prefix}-operator': 'exact',
150
- f'{prefix}-value': '123',
164
+ f'{add_prefix(0)}-field': 'integer',
165
+ f'{add_prefix(0)}-operator': 'exact',
166
+ f'{add_prefix(0)}-value': '123',
151
167
  }
152
- form = SearchkitForm(ModelA, data, prefix=prefix)
168
+ form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
153
169
  self.check_form(form)
154
170
 
155
- # Form should be valid, bound and complete
171
+ # Form should be valid.
156
172
  self.assertTrue(form.is_valid())
157
173
 
158
- # Get filter rule and check if a lookup does not raises any error.
159
- rule = form.get_filter_rule()
160
- self.assertFalse(ModelA.objects.filter(**dict((rule,))))
161
-
162
174
  def test_searchkitform_with_invalid_data(self):
163
- prefix = SearchkitFormSet(ModelA).add_prefix(0)
164
175
  data = {
165
- f'{prefix}-field': 'integer',
166
- f'{prefix}-operator': 'exact',
167
- f'{prefix}-value': 'foobar',
176
+ f'{add_prefix(0)}-field': 'integer',
177
+ f'{add_prefix(0)}-operator': 'exact',
178
+ f'{add_prefix(0)}-value': 'foobar',
168
179
  }
169
- form = SearchkitForm(ModelA, data, prefix=prefix)
180
+ form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
170
181
  self.check_form(form)
171
182
 
172
183
  # Form should be invalid.
@@ -174,24 +185,66 @@ class SearchkitFormTestCase(TestCase):
174
185
 
175
186
  # Check error message in html.
176
187
  errors = ['Enter a whole number.']
177
- self.assertFormError(form, 'value', errors)
178
-
179
- # get_filter_rule should raise an error.
180
- with self.assertRaises(forms.ValidationError):
181
- form.get_filter_rule()
188
+ self.assertIn(errors, form.errors.values())
182
189
 
183
190
 
184
- class SearchkitFormSetTestCase(TestCase):
191
+ class SearchkitFormSetTestCase(CheckFormMixin, TestCase):
192
+ def test_blank_searchkitform(self):
193
+ # Instantiating the formset neither with a model instance nor with model
194
+ # related data or initial data should result in a formset without forms,
195
+ # that is invalid and unbound.
196
+ formset = SearchkitFormSet()
197
+ self.assertFalse(formset.is_bound)
198
+ self.assertFalse(formset.is_valid())
185
199
 
186
200
  def test_searchkit_formset_with_valid_data(self):
187
- formset = SearchkitFormSet(ModelA, FORM_DATA)
201
+ formset = SearchkitFormSet(FORM_DATA)
188
202
  self.assertTrue(formset.is_valid())
189
203
 
190
- # Just check if the filter rules are applicable. Result should be empty.
191
- self.assertFalse(ModelA.objects.filter(**formset.get_filter_rules()))
192
-
193
- def test_searchkit_formset_with_incomplete_data(self):
204
+ def test_searchkit_formset_with_invalid_data(self):
194
205
  data = FORM_DATA.copy()
195
- del data[f'{DEFAULT_PREFIX}-0-value']
196
- formset = SearchkitFormSet(ModelA, data)
206
+ del data[f'{add_prefix(0)}-value']
207
+ formset = SearchkitFormSet(data, model=ModelA)
208
+ self.assertFalse(formset.is_valid())
209
+
210
+ # Check error message in html.
211
+ errors = ['This field is required.']
212
+ self.assertIn(errors, formset.forms[0].errors.values())
213
+
214
+ def test_searchkit_formset_with_initial_data(self):
215
+ formset = SearchkitFormSet(initial=INITIAL_DATA, model=ModelA)
216
+ self.assertFalse(formset.is_bound)
197
217
  self.assertFalse(formset.is_valid())
218
+ self.assertEqual(len(formset.forms), len(INITIAL_DATA))
219
+ for i, form in enumerate(formset.forms):
220
+ self.assertEqual(form.initial, INITIAL_DATA[i])
221
+ self.check_form(form)
222
+
223
+
224
+ class SearchkitSearchFormTestCase(TestCase):
225
+ def test_searchkit_search_form_without_data(self):
226
+ form = SearchkitSearchForm()
227
+ self.assertFalse(form.is_bound)
228
+ self.assertFalse(form.is_valid())
229
+ self.assertIsInstance(form.formset, SearchkitFormSet)
230
+ self.assertEqual(form.formset.model, None)
231
+
232
+ def test_searchkit_search_form_with_data(self):
233
+ form = SearchkitSearchForm(FORM_DATA)
234
+ self.assertTrue(form.is_bound)
235
+ self.assertTrue(form.is_valid())
236
+ self.assertIsInstance(form.formset, SearchkitFormSet)
237
+ self.assertEqual(form.formset.model, ModelA)
238
+ self.assertEqual(form.instance.data, form.formset.cleaned_data)
239
+
240
+ # Saving the instance works.
241
+ form.instance.save()
242
+ self.assertTrue(form.instance.pk)
243
+
244
+ # Using the instance data as filter rules works.
245
+ filter_rules = form.instance.get_filter_rules()
246
+ self.assertEqual(len(filter_rules), len(INITIAL_DATA))
247
+ for data in INITIAL_DATA:
248
+ self.assertIn(f"{data['field']}__{data['operator']}", filter_rules)
249
+ queryset = form.formset.model.objects.filter(**filter_rules)
250
+ self.assertTrue(queryset.model == ModelA)
@@ -1,36 +0,0 @@
1
- django_searchkit-0.1.dist-info/licenses/LICENCE,sha256=oqmlRYPA5GHG2T1yp8fPFGQdOO-NnJPdGITeFiJYWLg,1521
2
- example/example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- example/example/admin.py,sha256=eTZ5mCA1ToGouqYgIf-6xDl2nvYAz7T9lU1Bnu_gP9c,462
4
- example/example/asgi.py,sha256=mqLnzcCH9X-8UhDo5ZhcM8tF3jtALEGSjsBmphbkLcs,391
5
- example/example/models.py,sha256=fB9X782cX01Yg2uIiPCVgrS8C7DkgG9U-9mxGtX3P7Q,1097
6
- example/example/settings.py,sha256=du9bWbAceSvDkYeNYOc_3KxKL1p1se93TdAueT2RQ_E,3256
7
- example/example/urls.py,sha256=D8LxjiTdJSA87AVh3BzpVo5E2aC30hlp20HshPDrfvU,800
8
- example/example/wsgi.py,sha256=OAXHHeNQoj7FBDc5NMET39i_kIGpFrcdNDdY1thrh58,391
9
- example/example/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- example/example/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- example/example/management/commands/createtestdata.py,sha256=IVtyUqak133h9Ua-Ag_GP8mw3dUdiDYAa2tGmLkKluU,2737
12
- example/example/migrations/0001_initial.py,sha256=62tOi17oHGuqqLmSAYWLep6AMkxCJF6Tp13odghS7gI,2014
13
- example/example/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- searchkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- searchkit/__version__.py,sha256=UrnVsWmQRmq1HwADBGwjljHcaaKf8RXFuEzjigqiQxk,661
16
- searchkit/admin.py,sha256=75E1jff9LDmzRWNAJ-mpW4yChKtuE1L1WFXeCmCiibI,1218
17
- searchkit/apps.py,sha256=17m6wPu5N59Dq4MLoWaQd0aNHEzMT4z4emQD632GIMA,150
18
- searchkit/filters.py,sha256=TSsK8K0Oj7-gTfHLq7zLacZyXeGIM5MtLAHNMbjxYIo,1343
19
- searchkit/models.py,sha256=pRCWOnwbxroUzyS8SFBSLyAUn0I7udiXP1wIxGB307A,894
20
- searchkit/tests.py,sha256=IEF8i3hW17PZivk4pK9MNM7gn81C-mTfkwaNLl8HDB0,6484
21
- searchkit/urls.py,sha256=bqF5UAcUFPfBCCi1CljHSLbjEqHqzj-ripmJ6VbtC7s,289
22
- searchkit/views.py,sha256=EAA2yyRMsD8wva2F5lKo-X9-mxvFHOqn7bGazS-XNFk,1051
23
- searchkit/forms/__init__.py,sha256=lhHtkL2orjV7kN2GPx_IlpbME4_i_guigSpHMLemul8,79
24
- searchkit/forms/fields.py,sha256=TpFZdAyWKaMhOoZFTuy5ODNrW6RLy289Lk1fcMSGJ20,1621
25
- searchkit/forms/search.py,sha256=b8XzTWFWh1f3kjnEBp7h17xWpl2kHDFYYANSZPBceI4,1400
26
- searchkit/forms/searchkit.py,sha256=7y-m0jOd69Zni-97x-NMD81wg8WwkTBUNsm13bWiulk,7256
27
- searchkit/forms/utils.py,sha256=QtCsCveq20eaHc-nWP36-ICPq_CgItr5ckNp2DQ6LAc,4381
28
- searchkit/migrations/0001_initial.py,sha256=cP886X1UMrDTf-KxfHfuoQ4b1T1lhdNJyVNOGMNyWNw,1093
29
- searchkit/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- searchkit/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- searchkit/templatetags/searchkit.py,sha256=QA-hqxsHJ7vRoRLqFmGeRqRErkywv1pmepVFKqZuKcg,1238
32
- django_searchkit-0.1.dist-info/METADATA,sha256=hbfgBWIjY4sp2kKMwT2WxdIjSrYH6cOt54eYxDD3gfc,2689
33
- django_searchkit-0.1.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
34
- django_searchkit-0.1.dist-info/top_level.txt,sha256=u_pfgvHiVe7yUSQ6f3ESnHMO5MCliX7lQFaUT9idnpk,23
35
- django_searchkit-0.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
36
- django_searchkit-0.1.dist-info/RECORD,,