django-searchkit 0.1__tar.gz → 1.0__tar.gz
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.
- {django_searchkit-0.1/django_searchkit.egg-info → django_searchkit-1.0}/PKG-INFO +44 -8
- django_searchkit-1.0/README.md +70 -0
- {django_searchkit-0.1 → django_searchkit-1.0/django_searchkit.egg-info}/PKG-INFO +44 -8
- django_searchkit-1.0/django_searchkit.egg-info/requires.txt +2 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/django_searchkit.egg-info/top_level.txt +1 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/__version__.py +1 -1
- django_searchkit-1.0/searchkit/forms/__init__.py +3 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/forms/search.py +0 -3
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/forms/searchkit.py +11 -14
- django_searchkit-1.0/searchkit/tests.py +250 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/setup.py +2 -2
- django_searchkit-0.1/README.md +0 -34
- django_searchkit-0.1/django_searchkit.egg-info/requires.txt +0 -1
- django_searchkit-0.1/searchkit/forms/__init__.py +0 -2
- django_searchkit-0.1/searchkit/tests.py +0 -197
- {django_searchkit-0.1 → django_searchkit-1.0}/LICENCE +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/MANIFEST.in +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/django_searchkit.egg-info/SOURCES.txt +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/django_searchkit.egg-info/dependency_links.txt +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/django_searchkit.egg-info/zip-safe +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/__init__.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/admin.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/asgi.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/management/__init__.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/management/commands/__init__.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/management/commands/createtestdata.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/migrations/0001_initial.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/migrations/__init__.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/models.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/settings.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/urls.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/example/example/wsgi.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/__init__.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/admin.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/apps.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/filters.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/forms/fields.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/forms/utils.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/migrations/0001_initial.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/migrations/__init__.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/models.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/templatetags/__init__.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/templatetags/searchkit.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/urls.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/views.py +0 -0
- {django_searchkit-0.1 → django_searchkit-1.0}/setup.cfg +0 -0
@@ -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,70 @@
|
|
1
|
+
# Welcome to django-searchkit
|
2
|
+
|
3
|
+
[<img src="https://github.com/thomst/django-searchkit/actions/workflows/ci.yml/badge.svg">](https://github.com/thomst/django-searchkit/)
|
4
|
+
[<img src="https://coveralls.io/repos/github/thomst/django-searchkit/badge.svg?branch=main">](https://coveralls.io/github/thomst/django-searchkit?branch=main)
|
5
|
+
[<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)
|
6
|
+
[<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)
|
7
|
+
|
8
|
+
|
9
|
+
## Description
|
10
|
+
|
11
|
+
Finally there is a real searchkit application for django that integrates best
|
12
|
+
with the django admin backend.
|
13
|
+
|
14
|
+
Build and apply complex searches on model instances right in the backend without
|
15
|
+
any coding. Save and reuse your searches by a handy django admin filter with a
|
16
|
+
single click.
|
17
|
+
|
18
|
+
|
19
|
+
## Setup
|
20
|
+
|
21
|
+
Install via pip:
|
22
|
+
```
|
23
|
+
pip install django-searchkit
|
24
|
+
```
|
25
|
+
|
26
|
+
Add `searchkit` to your `INSTALLED_APPS`:
|
27
|
+
```
|
28
|
+
INSTALLED_APPS = [
|
29
|
+
'searchkit',
|
30
|
+
...
|
31
|
+
]
|
32
|
+
```
|
33
|
+
|
34
|
+
Add the `SearkitFilter` to your `ModelAdmin`:
|
35
|
+
```
|
36
|
+
from django.contrib import admin
|
37
|
+
from searchkit.filters import SearchkitFilter
|
38
|
+
from .models import MyModel
|
39
|
+
|
40
|
+
|
41
|
+
@admin.register(MyModel)
|
42
|
+
class MyModelAdmin(admin.ModelAdmin):
|
43
|
+
...
|
44
|
+
list_filter = [
|
45
|
+
SearchkitFilter,
|
46
|
+
...
|
47
|
+
]
|
48
|
+
...
|
49
|
+
```
|
50
|
+
|
51
|
+
## Usage
|
52
|
+
|
53
|
+
1. Open the admin changelist of your Model.
|
54
|
+
2. Click "Add filter" on the Searchkit filter.
|
55
|
+
3. Choose the Model you want to filter.
|
56
|
+
4. Configure as many filter rules as you want.
|
57
|
+
5. Click "Save and apply"
|
58
|
+
|
59
|
+
|
60
|
+
## TODO
|
61
|
+
|
62
|
+
- Limit the choices of the model field by models that should be searchable.
|
63
|
+
- Add an apply button to the search edit page to be able to use a search without
|
64
|
+
saving it.
|
65
|
+
- Coming from the search edit page the filtering should be done by an id__in url
|
66
|
+
parameter, not by an search parameter as it is used by the searchkit filter.
|
67
|
+
- Preselect the right model in the model field when coming from a models
|
68
|
+
changelist by the "Add filter" button.
|
69
|
+
- Add a public field for searches and only offer public searches in the
|
70
|
+
searchkit filter.
|
@@ -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.
|
@@ -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
|
-
|
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
|
@@ -0,0 +1,250 @@
|
|
1
|
+
from django.test import TestCase
|
2
|
+
from django.contrib.contenttypes.models import ContentType
|
3
|
+
from django import forms
|
4
|
+
from example.models import ModelA
|
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
|
11
|
+
|
12
|
+
|
13
|
+
INITIAL_DATA = [
|
14
|
+
dict(
|
15
|
+
field='model_b__chars',
|
16
|
+
operator='exact',
|
17
|
+
value='anytext',
|
18
|
+
),
|
19
|
+
dict(
|
20
|
+
field='integer',
|
21
|
+
operator='range',
|
22
|
+
value=[1, 123],
|
23
|
+
),
|
24
|
+
dict(
|
25
|
+
field='float',
|
26
|
+
operator='exact',
|
27
|
+
value='0.3',
|
28
|
+
),
|
29
|
+
dict(
|
30
|
+
field='decimal',
|
31
|
+
operator='exact',
|
32
|
+
value='1.23',
|
33
|
+
),
|
34
|
+
dict(
|
35
|
+
field='date',
|
36
|
+
operator='exact',
|
37
|
+
value='2025-05-14',
|
38
|
+
),
|
39
|
+
dict(
|
40
|
+
field='datetime',
|
41
|
+
operator='exact',
|
42
|
+
value='2025-05-14 08:45',
|
43
|
+
)
|
44
|
+
]
|
45
|
+
|
46
|
+
add_prefix = lambda i: SearchkitFormSet(model=ModelA).add_prefix(i)
|
47
|
+
DEFAULT_PREFIX = SearchkitFormSet.get_default_prefix()
|
48
|
+
FORM_DATA = {
|
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.
|
55
|
+
}
|
56
|
+
for i, data in enumerate(INITIAL_DATA, 0):
|
57
|
+
prefix = SearchkitFormSet(model=ModelA).add_prefix(i)
|
58
|
+
for key, value in data.items():
|
59
|
+
FORM_DATA.update({f'{prefix}-{key}': value})
|
60
|
+
|
61
|
+
|
62
|
+
class CheckFormMixin:
|
63
|
+
"""
|
64
|
+
Mixin to check the form fields and their choices.
|
65
|
+
"""
|
66
|
+
def check_form(self, form):
|
67
|
+
# Three fields should be generated on instantiation.
|
68
|
+
self.assertIn('field', form.fields)
|
69
|
+
self.assertIn('operator', form.fields)
|
70
|
+
self.assertIn('value', form.fields)
|
71
|
+
self.assertEqual(len(form.fields), 3)
|
72
|
+
|
73
|
+
# Check choices of the model_field.
|
74
|
+
form_model_field = form.fields['field']
|
75
|
+
self.assertTrue(form_model_field.choices)
|
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.
|
82
|
+
for model_field in ModelA._meta.fields:
|
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)
|
89
|
+
|
90
|
+
# Check the field_plan choosen based on the model_field.
|
91
|
+
field_plan = next(iter([p for t, p in FIELD_PLAN.items() if t(form.model_field)]))
|
92
|
+
self.assertEqual(form.field_plan, field_plan)
|
93
|
+
|
94
|
+
# Check choices of the operator field based on the field_plan.
|
95
|
+
operator_field = form.fields['operator']
|
96
|
+
self.assertTrue(operator_field.choices)
|
97
|
+
self.assertEqual(len(operator_field.choices), len(form.field_plan))
|
98
|
+
for operator in form.field_plan.keys():
|
99
|
+
self.assertIn(operator, [c[0] for c in operator_field.choices])
|
100
|
+
|
101
|
+
|
102
|
+
class SearchkitFormTestCase(CheckFormMixin, TestCase):
|
103
|
+
|
104
|
+
def test_blank_searchkitform(self):
|
105
|
+
form = SearchkitForm(ModelA, prefix=add_prefix(0))
|
106
|
+
self.check_form(form)
|
107
|
+
|
108
|
+
# Form should not be bound or valid.
|
109
|
+
self.assertFalse(form.is_bound)
|
110
|
+
self.assertFalse(form.is_valid())
|
111
|
+
|
112
|
+
def test_searchkitform_with_invalid_model_field_data(self):
|
113
|
+
data = {
|
114
|
+
f'{add_prefix(0)}-field': 'foobar',
|
115
|
+
}
|
116
|
+
form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
|
117
|
+
self.check_form(form)
|
118
|
+
|
119
|
+
# Form should be invalid.
|
120
|
+
self.assertFalse(form.is_valid())
|
121
|
+
|
122
|
+
# Check error message in html.
|
123
|
+
errors = ['Select a valid choice. foobar is not one of the available choices.']
|
124
|
+
self.assertIn(errors, form.errors.values())
|
125
|
+
|
126
|
+
def test_searchkitform_with_valid_model_field_data(self):
|
127
|
+
data = {
|
128
|
+
f'{add_prefix(0)}-field': 'integer',
|
129
|
+
}
|
130
|
+
form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
|
131
|
+
self.check_form(form)
|
132
|
+
|
133
|
+
# Form should be invalid since no value data is provieded.
|
134
|
+
self.assertFalse(form.is_valid())
|
135
|
+
|
136
|
+
def test_searchkitform_with_invalid_operator_data(self):
|
137
|
+
data = {
|
138
|
+
f'{add_prefix(0)}-field': 'integer',
|
139
|
+
f'{add_prefix(0)}-operator': 'foobar',
|
140
|
+
}
|
141
|
+
form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
|
142
|
+
self.check_form(form)
|
143
|
+
|
144
|
+
# Form should be invalid.
|
145
|
+
self.assertFalse(form.is_valid())
|
146
|
+
|
147
|
+
# Check error message in html.
|
148
|
+
errors = ['Select a valid choice. foobar is not one of the available choices.']
|
149
|
+
self.assertIn(errors, form.errors.values())
|
150
|
+
|
151
|
+
def test_searchkitform_with_valid_operator_data(self):
|
152
|
+
data = {
|
153
|
+
f'{add_prefix(0)}-field': 'integer',
|
154
|
+
f'{add_prefix(0)}-operator': 'exact',
|
155
|
+
}
|
156
|
+
form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
|
157
|
+
self.check_form(form)
|
158
|
+
|
159
|
+
# Form should be invalid since no value data is provieded.
|
160
|
+
self.assertFalse(form.is_valid())
|
161
|
+
|
162
|
+
def test_searchkitform_with_valid_data(self):
|
163
|
+
data = {
|
164
|
+
f'{add_prefix(0)}-field': 'integer',
|
165
|
+
f'{add_prefix(0)}-operator': 'exact',
|
166
|
+
f'{add_prefix(0)}-value': '123',
|
167
|
+
}
|
168
|
+
form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
|
169
|
+
self.check_form(form)
|
170
|
+
|
171
|
+
# Form should be valid.
|
172
|
+
self.assertTrue(form.is_valid())
|
173
|
+
|
174
|
+
def test_searchkitform_with_invalid_data(self):
|
175
|
+
data = {
|
176
|
+
f'{add_prefix(0)}-field': 'integer',
|
177
|
+
f'{add_prefix(0)}-operator': 'exact',
|
178
|
+
f'{add_prefix(0)}-value': 'foobar',
|
179
|
+
}
|
180
|
+
form = SearchkitForm(ModelA, data, prefix=add_prefix(0))
|
181
|
+
self.check_form(form)
|
182
|
+
|
183
|
+
# Form should be invalid.
|
184
|
+
self.assertFalse(form.is_valid())
|
185
|
+
|
186
|
+
# Check error message in html.
|
187
|
+
errors = ['Enter a whole number.']
|
188
|
+
self.assertIn(errors, form.errors.values())
|
189
|
+
|
190
|
+
|
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())
|
199
|
+
|
200
|
+
def test_searchkit_formset_with_valid_data(self):
|
201
|
+
formset = SearchkitFormSet(FORM_DATA)
|
202
|
+
self.assertTrue(formset.is_valid())
|
203
|
+
|
204
|
+
def test_searchkit_formset_with_invalid_data(self):
|
205
|
+
data = FORM_DATA.copy()
|
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)
|
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)
|
@@ -43,12 +43,12 @@ setup(
|
|
43
43
|
packages=find_namespace_packages(exclude=["example"]),
|
44
44
|
include_package_data=True,
|
45
45
|
install_requires=[
|
46
|
-
"Django>=3.
|
46
|
+
"Django>=3.1",
|
47
|
+
"django-picklefield>=2.0",
|
47
48
|
],
|
48
49
|
classifiers=[
|
49
50
|
dev_status,
|
50
51
|
"Framework :: Django",
|
51
|
-
"Framework :: Django :: 3.0",
|
52
52
|
"Framework :: Django :: 3.1",
|
53
53
|
"Framework :: Django :: 3.2",
|
54
54
|
"Framework :: Django :: 4.0",
|
django_searchkit-0.1/README.md
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
# Welcome to django-searchkit
|
2
|
-
|
3
|
-
[<img src="https://github.com/thomst/django-searchkit/actions/workflows/ci.yml/badge.svg">](https://github.com/thomst/django-searchkit/)
|
4
|
-
[<img src="https://coveralls.io/repos/github/thomst/django-searchkit/badge.svg?branch=main">](https://coveralls.io/github/thomst/django-searchkit?branch=main)
|
5
|
-
[<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)
|
6
|
-
[<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)
|
7
|
-
|
8
|
-
|
9
|
-
## Description
|
10
|
-
|
11
|
-
TODO
|
12
|
-
|
13
|
-
## Setup
|
14
|
-
|
15
|
-
Install via pip:
|
16
|
-
```
|
17
|
-
pip install django-searchkit
|
18
|
-
```
|
19
|
-
|
20
|
-
Add `searchkit` to your `INSTALLED_APPS`:
|
21
|
-
```
|
22
|
-
INSTALLED_APPS = [
|
23
|
-
'searchkit',
|
24
|
-
...
|
25
|
-
]
|
26
|
-
```
|
27
|
-
|
28
|
-
## Getting started
|
29
|
-
|
30
|
-
TODO
|
31
|
-
|
32
|
-
## Usage
|
33
|
-
|
34
|
-
TODO
|
@@ -1 +0,0 @@
|
|
1
|
-
Django>=3.0
|
@@ -1,197 +0,0 @@
|
|
1
|
-
from django.test import TestCase
|
2
|
-
from django import forms
|
3
|
-
from example.models import ModelA
|
4
|
-
from .searchkit import FIELD_PLAN
|
5
|
-
from .searchkit import SearchkitForm
|
6
|
-
from .searchkit import SearchkitFormSet
|
7
|
-
|
8
|
-
|
9
|
-
INITIAL_DATA = [
|
10
|
-
dict(
|
11
|
-
field='chars',
|
12
|
-
operator='exact',
|
13
|
-
value='anytext',
|
14
|
-
),
|
15
|
-
dict(
|
16
|
-
field='integer',
|
17
|
-
operator='range',
|
18
|
-
value_0=1,
|
19
|
-
value_1=123,
|
20
|
-
),
|
21
|
-
dict(
|
22
|
-
field='float',
|
23
|
-
operator='exact',
|
24
|
-
value='0.3',
|
25
|
-
),
|
26
|
-
dict(
|
27
|
-
field='decimal',
|
28
|
-
operator='exact',
|
29
|
-
value='1.23',
|
30
|
-
),
|
31
|
-
dict(
|
32
|
-
field='date',
|
33
|
-
operator='exact',
|
34
|
-
value='2025-05-14',
|
35
|
-
),
|
36
|
-
dict(
|
37
|
-
field='datetime',
|
38
|
-
operator='exact',
|
39
|
-
value='2025-05-14 08:45',
|
40
|
-
)
|
41
|
-
]
|
42
|
-
|
43
|
-
DEFAULT_PREFIX = SearchkitFormSet.get_default_prefix()
|
44
|
-
FORM_DATA = {
|
45
|
-
f'{DEFAULT_PREFIX}-TOTAL_FORMS': '6',
|
46
|
-
f'{DEFAULT_PREFIX}-INITIAL_FORMS': '1'
|
47
|
-
}
|
48
|
-
for i, data in enumerate(INITIAL_DATA):
|
49
|
-
for key, value in data.items():
|
50
|
-
FORM_DATA.update({f'{DEFAULT_PREFIX}-{i}-{key}': value})
|
51
|
-
|
52
|
-
|
53
|
-
class SearchkitFormTestCase(TestCase):
|
54
|
-
|
55
|
-
def check_form(self, form):
|
56
|
-
# Three fields should be generated on instantiation.
|
57
|
-
self.assertIn('field', form.fields)
|
58
|
-
self.assertIn('operator', form.fields)
|
59
|
-
self.assertIn('value', form.fields)
|
60
|
-
self.assertEqual(len(form.fields), 3)
|
61
|
-
|
62
|
-
# Check choices of the model_field.
|
63
|
-
form_model_field = form.fields['field']
|
64
|
-
self.assertTrue(form_model_field.choices)
|
65
|
-
self.assertEqual(len(form_model_field.choices), len(ModelA._meta.fields))
|
66
|
-
for model_field in ModelA._meta.fields:
|
67
|
-
self.assertIn(model_field.name, [c[0] for c in form_model_field.choices])
|
68
|
-
|
69
|
-
# Check the field_plan choosen based on the model_field.
|
70
|
-
field_plan = next(iter([p for t, p in FIELD_PLAN.items() if t(form.model_field)]))
|
71
|
-
self.assertEqual(form.field_plan, field_plan)
|
72
|
-
|
73
|
-
# Check choices of the operator field based on the field_plan.
|
74
|
-
operator_field = form.fields['operator']
|
75
|
-
self.assertTrue(operator_field.choices)
|
76
|
-
self.assertEqual(len(form_model_field.choices), len(form.field_plan))
|
77
|
-
for operator in form.field_plan.keys():
|
78
|
-
self.assertIn(operator, [c[0] for c in operator_field.choices])
|
79
|
-
|
80
|
-
|
81
|
-
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)
|
86
|
-
|
87
|
-
# Form should not be bound or valid.
|
88
|
-
self.assertFalse(form.is_bound)
|
89
|
-
self.assertFalse(form.is_valid())
|
90
|
-
|
91
|
-
def test_searchkitform_with_invalid_model_field_data(self):
|
92
|
-
prefix = SearchkitFormSet(ModelA).add_prefix(0)
|
93
|
-
data = {
|
94
|
-
f'{prefix}-field': 'foobar',
|
95
|
-
}
|
96
|
-
form = SearchkitForm(ModelA, data, prefix=prefix)
|
97
|
-
self.check_form(form)
|
98
|
-
|
99
|
-
# Form should be invalid.
|
100
|
-
self.assertFalse(form.is_valid())
|
101
|
-
|
102
|
-
# Check error message in html.
|
103
|
-
errors = ['Select a valid choice. foobar is not one of the available choices.']
|
104
|
-
self.assertFormError(form, 'field', errors)
|
105
|
-
|
106
|
-
def test_searchkitform_with_valid_model_field_data(self):
|
107
|
-
prefix = SearchkitFormSet(ModelA).add_prefix(0)
|
108
|
-
data = {
|
109
|
-
f'{prefix}-field': 'integer',
|
110
|
-
}
|
111
|
-
form = SearchkitForm(ModelA, data, prefix=prefix)
|
112
|
-
self.check_form(form)
|
113
|
-
|
114
|
-
# Form should be invalid.
|
115
|
-
self.assertFalse(form.is_valid())
|
116
|
-
|
117
|
-
def test_searchkitform_with_invalid_operator_data(self):
|
118
|
-
prefix = SearchkitFormSet(ModelA).add_prefix(0)
|
119
|
-
data = {
|
120
|
-
f'{prefix}-field': 'integer',
|
121
|
-
f'{prefix}-operator': 'foobar',
|
122
|
-
}
|
123
|
-
form = SearchkitForm(ModelA, data, prefix=prefix)
|
124
|
-
self.check_form(form)
|
125
|
-
|
126
|
-
# Form should be invalid.
|
127
|
-
self.assertFalse(form.is_valid())
|
128
|
-
|
129
|
-
# Check error message in html.
|
130
|
-
errors = ['Select a valid choice. foobar is not one of the available choices.']
|
131
|
-
self.assertFormError(form, 'operator', errors)
|
132
|
-
|
133
|
-
def test_searchkitform_with_valid_operator_data(self):
|
134
|
-
prefix = SearchkitFormSet(ModelA).add_prefix(0)
|
135
|
-
data = {
|
136
|
-
f'{prefix}-field': 'integer',
|
137
|
-
f'{prefix}-operator': 'exact',
|
138
|
-
}
|
139
|
-
form = SearchkitForm(ModelA, data, prefix=prefix)
|
140
|
-
self.check_form(form)
|
141
|
-
|
142
|
-
# Form should be invalid.
|
143
|
-
self.assertFalse(form.is_valid())
|
144
|
-
|
145
|
-
def test_searchkitform_with_valid_data(self):
|
146
|
-
prefix = SearchkitFormSet(ModelA).add_prefix(0)
|
147
|
-
data = {
|
148
|
-
f'{prefix}-field': 'integer',
|
149
|
-
f'{prefix}-operator': 'exact',
|
150
|
-
f'{prefix}-value': '123',
|
151
|
-
}
|
152
|
-
form = SearchkitForm(ModelA, data, prefix=prefix)
|
153
|
-
self.check_form(form)
|
154
|
-
|
155
|
-
# Form should be valid, bound and complete
|
156
|
-
self.assertTrue(form.is_valid())
|
157
|
-
|
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
|
-
def test_searchkitform_with_invalid_data(self):
|
163
|
-
prefix = SearchkitFormSet(ModelA).add_prefix(0)
|
164
|
-
data = {
|
165
|
-
f'{prefix}-field': 'integer',
|
166
|
-
f'{prefix}-operator': 'exact',
|
167
|
-
f'{prefix}-value': 'foobar',
|
168
|
-
}
|
169
|
-
form = SearchkitForm(ModelA, data, prefix=prefix)
|
170
|
-
self.check_form(form)
|
171
|
-
|
172
|
-
# Form should be invalid.
|
173
|
-
self.assertFalse(form.is_valid())
|
174
|
-
|
175
|
-
# Check error message in html.
|
176
|
-
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()
|
182
|
-
|
183
|
-
|
184
|
-
class SearchkitFormSetTestCase(TestCase):
|
185
|
-
|
186
|
-
def test_searchkit_formset_with_valid_data(self):
|
187
|
-
formset = SearchkitFormSet(ModelA, FORM_DATA)
|
188
|
-
self.assertTrue(formset.is_valid())
|
189
|
-
|
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):
|
194
|
-
data = FORM_DATA.copy()
|
195
|
-
del data[f'{DEFAULT_PREFIX}-0-value']
|
196
|
-
formset = SearchkitFormSet(ModelA, data)
|
197
|
-
self.assertFalse(formset.is_valid())
|
File without changes
|
File without changes
|
File without changes
|
{django_searchkit-0.1 → django_searchkit-1.0}/django_searchkit.egg-info/dependency_links.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_searchkit-0.1 → django_searchkit-1.0}/example/example/management/commands/__init__.py
RENAMED
File without changes
|
{django_searchkit-0.1 → django_searchkit-1.0}/example/example/management/commands/createtestdata.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|