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.
Files changed (46) hide show
  1. {django_searchkit-0.1/django_searchkit.egg-info → django_searchkit-1.0}/PKG-INFO +44 -8
  2. django_searchkit-1.0/README.md +70 -0
  3. {django_searchkit-0.1 → django_searchkit-1.0/django_searchkit.egg-info}/PKG-INFO +44 -8
  4. django_searchkit-1.0/django_searchkit.egg-info/requires.txt +2 -0
  5. {django_searchkit-0.1 → django_searchkit-1.0}/django_searchkit.egg-info/top_level.txt +1 -0
  6. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/__version__.py +1 -1
  7. django_searchkit-1.0/searchkit/forms/__init__.py +3 -0
  8. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/forms/search.py +0 -3
  9. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/forms/searchkit.py +11 -14
  10. django_searchkit-1.0/searchkit/tests.py +250 -0
  11. {django_searchkit-0.1 → django_searchkit-1.0}/setup.py +2 -2
  12. django_searchkit-0.1/README.md +0 -34
  13. django_searchkit-0.1/django_searchkit.egg-info/requires.txt +0 -1
  14. django_searchkit-0.1/searchkit/forms/__init__.py +0 -2
  15. django_searchkit-0.1/searchkit/tests.py +0 -197
  16. {django_searchkit-0.1 → django_searchkit-1.0}/LICENCE +0 -0
  17. {django_searchkit-0.1 → django_searchkit-1.0}/MANIFEST.in +0 -0
  18. {django_searchkit-0.1 → django_searchkit-1.0}/django_searchkit.egg-info/SOURCES.txt +0 -0
  19. {django_searchkit-0.1 → django_searchkit-1.0}/django_searchkit.egg-info/dependency_links.txt +0 -0
  20. {django_searchkit-0.1 → django_searchkit-1.0}/django_searchkit.egg-info/zip-safe +0 -0
  21. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/__init__.py +0 -0
  22. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/admin.py +0 -0
  23. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/asgi.py +0 -0
  24. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/management/__init__.py +0 -0
  25. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/management/commands/__init__.py +0 -0
  26. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/management/commands/createtestdata.py +0 -0
  27. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/migrations/0001_initial.py +0 -0
  28. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/migrations/__init__.py +0 -0
  29. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/models.py +0 -0
  30. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/settings.py +0 -0
  31. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/urls.py +0 -0
  32. {django_searchkit-0.1 → django_searchkit-1.0}/example/example/wsgi.py +0 -0
  33. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/__init__.py +0 -0
  34. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/admin.py +0 -0
  35. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/apps.py +0 -0
  36. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/filters.py +0 -0
  37. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/forms/fields.py +0 -0
  38. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/forms/utils.py +0 -0
  39. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/migrations/0001_initial.py +0 -0
  40. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/migrations/__init__.py +0 -0
  41. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/models.py +0 -0
  42. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/templatetags/__init__.py +0 -0
  43. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/templatetags/searchkit.py +0 -0
  44. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/urls.py +0 -0
  45. {django_searchkit-0.1 → django_searchkit-1.0}/searchkit/views.py +0 -0
  46. {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.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,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.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,2 @@
1
+ Django>=3.1
2
+ django-picklefield>=2.0
@@ -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"
@@ -0,0 +1,3 @@
1
+ from .search import SearchkitSearchForm
2
+ from .searchkit import SearchkitFormSet
3
+ from .searchkit import SearchkitForm
@@ -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
@@ -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.0",
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",
@@ -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,2 +0,0 @@
1
- from .search import SearchkitSearchForm
2
- from .searchkit import SearchkitFormSet
@@ -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