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.
- build/lib/example/example/__init__.py +0 -0
- build/lib/example/example/admin.py +16 -0
- build/lib/example/example/asgi.py +16 -0
- build/lib/example/example/management/__init__.py +0 -0
- build/lib/example/example/management/commands/__init__.py +0 -0
- build/lib/example/example/management/commands/createtestdata.py +62 -0
- build/lib/example/example/migrations/0001_initial.py +48 -0
- build/lib/example/example/migrations/__init__.py +0 -0
- build/lib/example/example/models.py +38 -0
- build/lib/example/example/settings.py +125 -0
- build/lib/example/example/urls.py +23 -0
- build/lib/example/example/wsgi.py +16 -0
- build/lib/searchkit/__init__.py +0 -0
- build/lib/searchkit/__version__.py +16 -0
- build/lib/searchkit/admin.py +30 -0
- build/lib/searchkit/apps.py +6 -0
- build/lib/searchkit/filters.py +31 -0
- build/lib/searchkit/forms/__init__.py +3 -0
- build/lib/searchkit/forms/fields.py +55 -0
- build/lib/searchkit/forms/search.py +42 -0
- build/lib/searchkit/forms/searchkit.py +189 -0
- build/lib/searchkit/forms/utils.py +149 -0
- build/lib/searchkit/migrations/0001_initial.py +30 -0
- build/lib/searchkit/migrations/__init__.py +0 -0
- build/lib/searchkit/models.py +23 -0
- build/lib/searchkit/templatetags/__init__.py +0 -0
- build/lib/searchkit/templatetags/searchkit.py +47 -0
- build/lib/searchkit/tests.py +250 -0
- build/lib/searchkit/urls.py +8 -0
- build/lib/searchkit/views.py +30 -0
- {django_searchkit-0.1.dist-info → django_searchkit-1.0.dist-info}/METADATA +44 -8
- django_searchkit-1.0.dist-info/RECORD +66 -0
- {django_searchkit-0.1.dist-info → django_searchkit-1.0.dist-info}/WHEEL +1 -1
- {django_searchkit-0.1.dist-info → django_searchkit-1.0.dist-info}/top_level.txt +1 -0
- searchkit/__version__.py +1 -1
- searchkit/forms/__init__.py +2 -1
- searchkit/forms/search.py +0 -3
- searchkit/forms/searchkit.py +11 -14
- searchkit/tests.py +121 -68
- django_searchkit-0.1.dist-info/RECORD +0 -36
- {django_searchkit-0.1.dist-info → django_searchkit-1.0.dist-info}/licenses/LICENCE +0 -0
- {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
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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,,
|
searchkit/__version__.py
CHANGED
searchkit/forms/__init__.py
CHANGED
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.
|
searchkit/forms/searchkit.py
CHANGED
@@ -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
|
-
|
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.
|
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'] =
|
152
|
-
ct_kwargs['prefix'] =
|
153
|
-
if
|
154
|
-
|
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 .
|
5
|
-
from .
|
6
|
-
from .
|
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='
|
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
|
-
|
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
|
-
|
46
|
-
f'{DEFAULT_PREFIX}-
|
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'{
|
59
|
+
FORM_DATA.update({f'{prefix}-{key}': value})
|
51
60
|
|
52
61
|
|
53
|
-
class
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
83
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
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'{
|
114
|
+
f'{add_prefix(0)}-field': 'foobar',
|
95
115
|
}
|
96
|
-
form = SearchkitForm(ModelA, data, 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.
|
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'{
|
128
|
+
f'{add_prefix(0)}-field': 'integer',
|
110
129
|
}
|
111
|
-
form = SearchkitForm(ModelA, data, 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'{
|
121
|
-
f'{
|
138
|
+
f'{add_prefix(0)}-field': 'integer',
|
139
|
+
f'{add_prefix(0)}-operator': 'foobar',
|
122
140
|
}
|
123
|
-
form = SearchkitForm(ModelA, data, 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.
|
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'{
|
137
|
-
f'{
|
153
|
+
f'{add_prefix(0)}-field': 'integer',
|
154
|
+
f'{add_prefix(0)}-operator': 'exact',
|
138
155
|
}
|
139
|
-
form = SearchkitForm(ModelA, data, 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'{
|
149
|
-
f'{
|
150
|
-
f'{
|
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=
|
168
|
+
form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
|
153
169
|
self.check_form(form)
|
154
170
|
|
155
|
-
# Form should be valid
|
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'{
|
166
|
-
f'{
|
167
|
-
f'{
|
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=
|
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.
|
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(
|
201
|
+
formset = SearchkitFormSet(FORM_DATA)
|
188
202
|
self.assertTrue(formset.is_valid())
|
189
203
|
|
190
|
-
|
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'{
|
196
|
-
formset = SearchkitFormSet(
|
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,,
|
File without changes
|
File without changes
|