django-searchkit 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_searchkit-0.1.dist-info/METADATA +82 -0
- django_searchkit-0.1.dist-info/RECORD +36 -0
- django_searchkit-0.1.dist-info/WHEEL +5 -0
- django_searchkit-0.1.dist-info/licenses/LICENCE +27 -0
- django_searchkit-0.1.dist-info/top_level.txt +3 -0
- django_searchkit-0.1.dist-info/zip-safe +1 -0
- example/example/__init__.py +0 -0
- example/example/admin.py +16 -0
- example/example/asgi.py +16 -0
- example/example/management/__init__.py +0 -0
- example/example/management/commands/__init__.py +0 -0
- example/example/management/commands/createtestdata.py +62 -0
- example/example/migrations/0001_initial.py +48 -0
- example/example/migrations/__init__.py +0 -0
- example/example/models.py +38 -0
- example/example/settings.py +125 -0
- example/example/urls.py +23 -0
- example/example/wsgi.py +16 -0
- searchkit/__init__.py +0 -0
- searchkit/__version__.py +16 -0
- searchkit/admin.py +30 -0
- searchkit/apps.py +6 -0
- searchkit/filters.py +31 -0
- searchkit/forms/__init__.py +2 -0
- searchkit/forms/fields.py +55 -0
- searchkit/forms/search.py +45 -0
- searchkit/forms/searchkit.py +192 -0
- searchkit/forms/utils.py +149 -0
- searchkit/migrations/0001_initial.py +30 -0
- searchkit/migrations/__init__.py +0 -0
- searchkit/models.py +23 -0
- searchkit/templatetags/__init__.py +0 -0
- searchkit/templatetags/searchkit.py +47 -0
- searchkit/tests.py +197 -0
- searchkit/urls.py +8 -0
- searchkit/views.py +30 -0
searchkit/tests.py
ADDED
@@ -0,0 +1,197 @@
|
|
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())
|
searchkit/urls.py
ADDED
@@ -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
|
+
]
|
searchkit/views.py
ADDED
@@ -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())
|