django-searchkit 1.1__py3-none-any.whl → 1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. build/lib/build/lib/build/lib/build/lib/example/example/__init__.py +0 -0
  2. build/lib/build/lib/build/lib/build/lib/example/example/admin.py +16 -0
  3. build/lib/build/lib/build/lib/build/lib/example/example/asgi.py +16 -0
  4. build/lib/build/lib/build/lib/build/lib/example/example/management/__init__.py +0 -0
  5. build/lib/build/lib/build/lib/build/lib/example/example/management/commands/__init__.py +0 -0
  6. build/lib/build/lib/build/lib/build/lib/example/example/management/commands/createtestdata.py +62 -0
  7. build/lib/build/lib/build/lib/build/lib/example/example/migrations/0001_initial.py +48 -0
  8. build/lib/build/lib/build/lib/build/lib/example/example/migrations/__init__.py +0 -0
  9. build/lib/build/lib/build/lib/build/lib/example/example/models.py +38 -0
  10. build/lib/build/lib/build/lib/build/lib/example/example/settings.py +125 -0
  11. build/lib/build/lib/build/lib/build/lib/example/example/urls.py +23 -0
  12. build/lib/build/lib/build/lib/build/lib/example/example/wsgi.py +16 -0
  13. build/lib/build/lib/build/lib/build/lib/searchkit/__init__.py +0 -0
  14. build/lib/build/lib/build/lib/build/lib/searchkit/__version__.py +16 -0
  15. build/lib/build/lib/build/lib/build/lib/searchkit/admin.py +44 -0
  16. build/lib/build/lib/build/lib/build/lib/searchkit/apps.py +6 -0
  17. build/lib/build/lib/build/lib/build/lib/searchkit/filters.py +36 -0
  18. build/lib/build/lib/build/lib/build/lib/searchkit/forms/__init__.py +5 -0
  19. build/lib/build/lib/build/lib/build/lib/searchkit/forms/fields.py +56 -0
  20. build/lib/build/lib/build/lib/build/lib/searchkit/forms/search.py +61 -0
  21. build/lib/build/lib/build/lib/build/lib/searchkit/forms/searchkit.py +177 -0
  22. build/lib/build/lib/build/lib/build/lib/searchkit/forms/utils.py +154 -0
  23. build/lib/build/lib/build/lib/build/lib/searchkit/migrations/0001_initial.py +30 -0
  24. build/lib/build/lib/build/lib/build/lib/searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
  25. build/lib/build/lib/build/lib/build/lib/searchkit/migrations/__init__.py +0 -0
  26. build/lib/build/lib/build/lib/build/lib/searchkit/models.py +21 -0
  27. build/lib/build/lib/build/lib/build/lib/searchkit/templatetags/__init__.py +0 -0
  28. build/lib/build/lib/build/lib/build/lib/searchkit/templatetags/searchkit.py +20 -0
  29. build/lib/build/lib/build/lib/build/lib/searchkit/tests.py +400 -0
  30. build/lib/build/lib/build/lib/build/lib/searchkit/urls.py +7 -0
  31. build/lib/build/lib/build/lib/build/lib/searchkit/utils.py +13 -0
  32. build/lib/build/lib/build/lib/build/lib/searchkit/views.py +23 -0
  33. build/lib/build/lib/build/lib/example/example/__init__.py +0 -0
  34. build/lib/build/lib/build/lib/example/example/admin.py +16 -0
  35. build/lib/build/lib/build/lib/example/example/asgi.py +16 -0
  36. build/lib/build/lib/build/lib/example/example/management/__init__.py +0 -0
  37. build/lib/build/lib/build/lib/example/example/management/commands/__init__.py +0 -0
  38. build/lib/build/lib/build/lib/example/example/management/commands/createtestdata.py +62 -0
  39. build/lib/build/lib/build/lib/example/example/migrations/0001_initial.py +48 -0
  40. build/lib/build/lib/build/lib/example/example/migrations/__init__.py +0 -0
  41. build/lib/build/lib/build/lib/example/example/models.py +38 -0
  42. build/lib/build/lib/build/lib/example/example/settings.py +125 -0
  43. build/lib/build/lib/build/lib/example/example/urls.py +23 -0
  44. build/lib/build/lib/build/lib/example/example/wsgi.py +16 -0
  45. build/lib/build/lib/build/lib/searchkit/__init__.py +0 -0
  46. build/lib/build/lib/build/lib/searchkit/__version__.py +16 -0
  47. build/lib/build/lib/build/lib/searchkit/admin.py +44 -0
  48. build/lib/build/lib/build/lib/searchkit/apps.py +6 -0
  49. build/lib/build/lib/build/lib/searchkit/filters.py +36 -0
  50. build/lib/build/lib/build/lib/searchkit/forms/__init__.py +5 -0
  51. build/lib/build/lib/build/lib/searchkit/forms/fields.py +56 -0
  52. build/lib/build/lib/build/lib/searchkit/forms/search.py +61 -0
  53. build/lib/build/lib/build/lib/searchkit/forms/searchkit.py +177 -0
  54. build/lib/build/lib/build/lib/searchkit/forms/utils.py +154 -0
  55. build/lib/build/lib/build/lib/searchkit/migrations/0001_initial.py +30 -0
  56. build/lib/build/lib/build/lib/searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
  57. build/lib/build/lib/build/lib/searchkit/migrations/__init__.py +0 -0
  58. build/lib/build/lib/build/lib/searchkit/models.py +21 -0
  59. build/lib/build/lib/build/lib/searchkit/templatetags/__init__.py +0 -0
  60. build/lib/build/lib/build/lib/searchkit/templatetags/searchkit.py +20 -0
  61. build/lib/build/lib/build/lib/searchkit/tests.py +400 -0
  62. build/lib/build/lib/build/lib/searchkit/urls.py +7 -0
  63. build/lib/build/lib/build/lib/searchkit/utils.py +13 -0
  64. build/lib/build/lib/build/lib/searchkit/views.py +23 -0
  65. build/lib/build/lib/example/example/admin.py +1 -1
  66. build/lib/build/lib/searchkit/__version__.py +1 -1
  67. build/lib/build/lib/searchkit/admin.py +15 -1
  68. build/lib/build/lib/searchkit/filters.py +15 -6
  69. build/lib/build/lib/searchkit/forms/fields.py +5 -4
  70. build/lib/build/lib/searchkit/forms/search.py +0 -1
  71. build/lib/build/lib/searchkit/forms/searchkit.py +36 -13
  72. build/lib/build/lib/searchkit/forms/utils.py +50 -74
  73. build/lib/build/lib/searchkit/models.py +0 -6
  74. build/lib/build/lib/searchkit/tests.py +1 -3
  75. build/lib/build/lib/searchkit/utils.py +13 -0
  76. build/lib/build/lib/searchkit/views.py +3 -3
  77. build/lib/example/example/admin.py +1 -1
  78. build/lib/searchkit/__version__.py +1 -1
  79. build/lib/searchkit/admin.py +15 -1
  80. build/lib/searchkit/filters.py +15 -6
  81. build/lib/searchkit/forms/fields.py +5 -4
  82. build/lib/searchkit/forms/search.py +0 -1
  83. build/lib/searchkit/forms/searchkit.py +36 -13
  84. build/lib/searchkit/forms/utils.py +50 -74
  85. build/lib/searchkit/models.py +0 -6
  86. build/lib/searchkit/tests.py +1 -3
  87. build/lib/searchkit/utils.py +13 -0
  88. build/lib/searchkit/views.py +3 -3
  89. {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/METADATA +1 -1
  90. django_searchkit-1.3.dist-info/RECORD +166 -0
  91. searchkit/__version__.py +1 -1
  92. searchkit/admin.py +15 -1
  93. searchkit/filters.py +15 -6
  94. searchkit/forms/fields.py +5 -4
  95. searchkit/forms/search.py +0 -1
  96. searchkit/forms/searchkit.py +36 -13
  97. searchkit/forms/utils.py +50 -74
  98. searchkit/models.py +0 -6
  99. searchkit/tests.py +1 -3
  100. searchkit/utils.py +13 -0
  101. searchkit/views.py +3 -3
  102. django_searchkit-1.1.dist-info/RECORD +0 -99
  103. {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/WHEEL +0 -0
  104. {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/licenses/LICENCE +0 -0
  105. {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/top_level.txt +0 -0
  106. {django_searchkit-1.1.dist-info → django_searchkit-1.3.dist-info}/zip-safe +0 -0
@@ -0,0 +1,177 @@
1
+ from django import forms
2
+ from django.apps import apps
3
+ from django.contrib.admin import widgets
4
+ from django.contrib.contenttypes.models import ContentType
5
+ from django.utils.translation import gettext_lazy as _
6
+ from django.utils.functional import cached_property
7
+ from .utils import CssClassMixin, FIELD_PLAN, OPERATOR_DESCRIPTION
8
+ from .utils import SUPPORTED_FIELDS
9
+ from .utils import ModelTree
10
+ from .utils import MediaMixin
11
+ from .fields import DateRangeField
12
+ from ..utils import is_searchable_model
13
+
14
+
15
+ class SearchkitModelForm(forms.Form):
16
+ """
17
+ Form to select a content type.
18
+ """
19
+ def __init__(self, *args, **kwargs):
20
+ super().__init__(*args, **kwargs)
21
+ models = [m for m in apps.get_models() if is_searchable_model(m)]
22
+ ids = [ContentType.objects.get_for_model(m).id for m in models]
23
+ queryset = self.fields['searchkit_model'].queryset.filter(pk__in=ids)
24
+ self.fields['searchkit_model'].queryset = queryset
25
+
26
+ searchkit_model = forms.ModelChoiceField(
27
+ queryset=ContentType.objects.all().order_by('app_label', 'model'),
28
+ label=_('Model'),
29
+ empty_label=_('Select a Model'),
30
+ widget=forms.Select(attrs={
31
+ "class": CssClassMixin.reload_on_change_css_class,
32
+ "data-total-forms": 1,
33
+ }),
34
+ )
35
+
36
+
37
+ class BaseSearchkitForm(MediaMixin, CssClassMixin, forms.Form):
38
+ """
39
+ Searchkit form representing a model field lookup based on the field name,
40
+ the operator and one or two values.
41
+
42
+ The unbound form is composed of an index field (the count of the searchkit
43
+ form) and a choice field offering the names of the model fields.
44
+
45
+ The bound form is dynamically extended by the operator field or the operator and
46
+ the value field depending on the provided data
47
+
48
+ See the FIELD_PLAN variable for the logic of building the form.
49
+ """
50
+ model = None # Set by the formset factory.
51
+
52
+ def __init__(self, *args, **kwargs):
53
+ super().__init__(*args, **kwargs)
54
+ self.model_tree = ModelTree(self.model)
55
+ self.model_field = None
56
+ self.field_plan = None
57
+ self.operator = None
58
+ self._add_field_name_field()
59
+ lookup = self._preload_clean_data('field')
60
+ self.model_field = self._get_model_field(lookup)
61
+ self.field_plan = next(iter([p for t, p in FIELD_PLAN.items() if t(self.model_field)]))
62
+ self._add_operator_field()
63
+ self.operator = self._preload_clean_data('operator')
64
+ self._add_value_field()
65
+
66
+ @cached_property
67
+ def unprefixed_data(self):
68
+ data = dict()
69
+ for key, value in self.data.items():
70
+ if key.startswith(self.prefix):
71
+ data[key[len(self.prefix) + 1:]] = value
72
+ return data
73
+
74
+ def _preload_clean_data(self, field_name):
75
+ # Try the initial value first since it is already cleaned.
76
+ if self.initial and field_name in self.initial:
77
+ return self.initial[field_name]
78
+ # Otherwise look up the data dict.
79
+ elif field_name in self.unprefixed_data:
80
+ try:
81
+ # Do we have a valid value?
82
+ return self.fields[field_name].clean(self.unprefixed_data[field_name])
83
+ except forms.ValidationError:
84
+ return self.fields[field_name].choices[0][0]
85
+ else:
86
+ # At last simply return the first option which will be the selected
87
+ # one.
88
+ return self.fields[field_name].choices[0][0]
89
+
90
+ def _get_model_field(self, lookup):
91
+ path = lookup.split('__')
92
+ field_name = path[-1]
93
+ if path[:-1]:
94
+ model = self.model_tree.get('__'.join(path[:-1])).model
95
+ else:
96
+ model = self.model
97
+ return model._meta.get_field(field_name)
98
+
99
+ def _get_model_field_choices(self):
100
+ choices = list()
101
+ label_path = list()
102
+ for node in self.model_tree.iterate():
103
+ label_path.append
104
+ for model_field in node.model._meta.fields:
105
+ if not any(isinstance(model_field, f) for f in SUPPORTED_FIELDS):
106
+ continue
107
+ if node.is_root:
108
+ lookup = model_field.name
109
+ label = f'`{model_field.verbose_name}`'
110
+ else:
111
+ lookup = f'{node.field_path}__{model_field.name}'
112
+ get_field_name = lambda f: getattr(f, 'verbose_name', f.name)
113
+ label_path = [f'`{get_field_name(n.field)}` => <{n.model._meta.verbose_name}>' for n in node.path[1:]]
114
+ label = ".".join(label_path + [f'`{model_field.verbose_name}`'])
115
+ choices.append((lookup, label))
116
+ return choices
117
+
118
+ def _add_field_name_field(self):
119
+ choices = self._get_model_field_choices()
120
+ field = forms.ChoiceField(label=_('Model field'), choices=choices)
121
+ field.widget.attrs.update({"class": self.reload_on_change_css_class})
122
+ self.fields['field'] = field
123
+
124
+ def _add_operator_field(self):
125
+ choices = [(o, OPERATOR_DESCRIPTION[o]) for o in self.field_plan.keys()]
126
+ field = forms.ChoiceField(label=_('Operator'), choices=choices)
127
+ field.widget.attrs.update({"class": self.reload_on_change_css_class})
128
+ self.fields['operator'] = field
129
+
130
+ def _add_value_field(self):
131
+ self.fields['value'] = self.field_plan[self.operator](self.model_field)
132
+
133
+
134
+ class BaseSearchkitFormSet(CssClassMixin, forms.BaseFormSet):
135
+ """
136
+ Formset holding all searchkit forms.
137
+ """
138
+ template_name = "searchkit/searchkit.html"
139
+ template_name_div = "searchkit/searchkit.html"
140
+ model = None # Set by the formset factory.
141
+
142
+ def add_prefix(self, index):
143
+ return "%s-%s-%s-%s" % (self.prefix, self.model._meta.app_label, self.model._meta.model_name, index)
144
+
145
+ @classmethod
146
+ def get_default_prefix(self):
147
+ return "searchkit"
148
+
149
+ @cached_property
150
+ def uses_date_widget(self):
151
+ """
152
+ Check if the form uses a date widget.
153
+ """
154
+ for form in self.forms:
155
+ for field in form.fields.values():
156
+ if isinstance(field.widget, (widgets.AdminDateWidget, widgets.AdminSplitDateTime)):
157
+ return True
158
+ elif isinstance(field, DateRangeField):
159
+ return True
160
+ return False
161
+
162
+ @cached_property
163
+ def forms(self):
164
+ if self.model:
165
+ return super().forms
166
+ else:
167
+ return []
168
+
169
+
170
+ def searchkit_formset_factory(model, **kwargs):
171
+ form = type('SearchkitForm', (BaseSearchkitForm,), dict(model=model))
172
+ formset = type('SearchkitFormSet', (BaseSearchkitFormSet,), dict(model=model))
173
+ return forms.formset_factory(
174
+ form=form,
175
+ formset=formset,
176
+ **kwargs
177
+ )
@@ -0,0 +1,154 @@
1
+ from modeltree import ModelTree as BaseModelTree
2
+ from collections import OrderedDict
3
+ from django.contrib.admin.widgets import AdminDateWidget, AdminSplitDateTime
4
+ from django.db import models
5
+ from django import forms
6
+ from django.utils.translation import gettext_lazy as _
7
+ from . import fields as searchkit_fields
8
+
9
+
10
+ class ModelTree(BaseModelTree):
11
+ MAX_DEPTH = 3
12
+ FOLLOW_ACROSS_APPS = True
13
+ RELATION_TYPES = [
14
+ 'one_to_one',
15
+ 'many_to_one',
16
+ ]
17
+
18
+
19
+ OPERATOR_DESCRIPTION = {
20
+ 'exact': _('is exact'),
21
+ 'contains': _('contains'),
22
+ 'startswith': _('starts with'),
23
+ 'endswith': _('ends with'),
24
+ 'regex': _('matches regular expression'),
25
+ 'gt': _('is greater than'),
26
+ 'gte': _('is greater than or equal'),
27
+ 'lt': _('is lower than'),
28
+ 'lte': _('is lower than or equal'),
29
+ 'range': _('is between'),
30
+ 'in': _('is one of'),
31
+ }
32
+
33
+
34
+ SUPPORTED_FIELDS = [
35
+ models.BooleanField,
36
+ models.CharField,
37
+ models.IntegerField,
38
+ models.FloatField,
39
+ models.DecimalField,
40
+ models.DateField,
41
+ models.DateTimeField,
42
+ ]
43
+
44
+
45
+ FIELD_PLAN = OrderedDict((
46
+ (
47
+ lambda f: isinstance(f, models.BooleanField),
48
+ {
49
+ 'exact': lambda f: forms.NullBooleanField() if f.null else forms.BooleanField(),
50
+ }
51
+ ),
52
+ (
53
+ lambda f: isinstance(f, models.CharField) and f.choices,
54
+ {
55
+ 'exact': lambda f: forms.ChoiceField(choices=f.choices),
56
+ 'contains': lambda f: forms.CharField(),
57
+ 'startswith': lambda f: forms.CharField(),
58
+ 'endswith': lambda f: forms.CharField(),
59
+ 'regex': lambda f: forms.CharField(),
60
+ 'in': lambda f: forms.MultipleChoiceField(choices=f.choices),
61
+ }
62
+ ),
63
+ (
64
+ lambda f: isinstance(f, models.CharField),
65
+ {
66
+ 'exact': lambda f: forms.CharField(),
67
+ 'contains': lambda f: forms.CharField(),
68
+ 'startswith': lambda f: forms.CharField(),
69
+ 'endswith': lambda f: forms.CharField(),
70
+ 'regex': lambda f: forms.CharField(),
71
+ }
72
+ ),
73
+ (
74
+ lambda f: isinstance(f, models.IntegerField) and f.choices,
75
+ {
76
+ 'exact': lambda f: forms.ChoiceField(choices=f.choices),
77
+ 'gt': lambda f: forms.IntegerField(),
78
+ 'gte': lambda f: forms.IntegerField(),
79
+ 'lt': lambda f: forms.IntegerField(),
80
+ 'lte': lambda f: forms.IntegerField(),
81
+ 'range': lambda f: searchkit_fields.IntegerRangeField(),
82
+ 'in': lambda f: forms.MultipleChoiceField(choices=f.choices),
83
+ }
84
+ ),
85
+ (
86
+ lambda f: isinstance(f, models.IntegerField),
87
+ {
88
+ 'exact': lambda f: forms.IntegerField(),
89
+ 'gt': lambda f: forms.IntegerField(),
90
+ 'gte': lambda f: forms.IntegerField(),
91
+ 'lt': lambda f: forms.IntegerField(),
92
+ 'lte': lambda f: forms.IntegerField(),
93
+ 'range': lambda f: searchkit_fields.IntegerRangeField(),
94
+ }
95
+ ),
96
+ (
97
+ lambda f: isinstance(f, models.FloatField),
98
+ {
99
+ 'exact': lambda f: forms.FloatField(),
100
+ 'gt': lambda f: forms.FloatField(),
101
+ 'gte': lambda f: forms.FloatField(),
102
+ 'lt': lambda f: forms.FloatField(),
103
+ 'lte': lambda f: forms.FloatField(),
104
+ 'range': lambda f: searchkit_fields.IntegerRangeField(),
105
+ }
106
+ ),
107
+ (
108
+ lambda f: isinstance(f, models.DecimalField),
109
+ {
110
+ 'exact': lambda f: forms.DecimalField(),
111
+ 'gt': lambda f: forms.DecimalField(),
112
+ 'gte': lambda f: forms.DecimalField(),
113
+ 'lt': lambda f: forms.DecimalField(),
114
+ 'lte': lambda f: forms.DecimalField(),
115
+ 'range': lambda f: searchkit_fields.IntegerRangeField(),
116
+ }
117
+ ),
118
+ (
119
+ lambda f: isinstance(f, models.DateTimeField),
120
+ {
121
+ 'exact': lambda f: forms.SplitDateTimeField(widget=AdminSplitDateTime()),
122
+ 'gt': lambda f: forms.SplitDateTimeField(widget=AdminSplitDateTime()),
123
+ 'gte': lambda f: forms.SplitDateTimeField(widget=AdminSplitDateTime()),
124
+ 'lt': lambda f: forms.SplitDateTimeField(widget=AdminSplitDateTime()),
125
+ 'lte': lambda f: forms.SplitDateTimeField(widget=AdminSplitDateTime()),
126
+ 'range': lambda f: searchkit_fields.DateTimeRangeField(),
127
+ }
128
+ ),
129
+ (
130
+ lambda f: isinstance(f, models.DateField),
131
+ {
132
+ 'exact': lambda f: forms.DateField(widget=AdminDateWidget()),
133
+ 'gt': lambda f: forms.DateField(widget=AdminDateWidget()),
134
+ 'gte': lambda f: forms.DateField(widget=AdminDateWidget()),
135
+ 'lt': lambda f: forms.DateField(widget=AdminDateWidget()),
136
+ 'lte': lambda f: forms.DateField(widget=AdminDateWidget()),
137
+ 'range': lambda f: searchkit_fields.DateRangeField(),
138
+ }
139
+ ),
140
+ ))
141
+
142
+
143
+ class CssClassMixin:
144
+ reload_on_change_css_class = "searchkit-reload-on-change"
145
+ reload_on_click_css_class = "searchkit-reload-on-click"
146
+
147
+
148
+ class MediaMixin:
149
+ class Media:
150
+ js = [
151
+ 'admin/js/vendor/jquery/jquery.min.js',
152
+ 'admin/js/jquery.init.js',
153
+ "searchkit/searchkit.js"
154
+ ]
@@ -0,0 +1,30 @@
1
+ # Generated by Django 5.1.3 on 2025-05-22 19:04
2
+
3
+ import django.db.models.deletion
4
+ import picklefield.fields
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ initial = True
11
+
12
+ dependencies = [
13
+ ('contenttypes', '0002_remove_content_type_name'),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name='SearchkitSearch',
19
+ fields=[
20
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+ ('name', models.CharField(max_length=255, verbose_name='Search name')),
22
+ ('data', picklefield.fields.PickledObjectField(editable=False, verbose_name='Serialized filter rule data')),
23
+ ('created_date', models.DateTimeField(auto_now_add=True)),
24
+ ('contenttype', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Model')),
25
+ ],
26
+ options={
27
+ 'unique_together': {('name', 'contenttype')},
28
+ },
29
+ ),
30
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 5.1.3 on 2025-06-07 05:46
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('contenttypes', '0002_remove_content_type_name'),
10
+ ('searchkit', '0001_initial'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.RenameModel(
15
+ old_name='SearchkitSearch',
16
+ new_name='Search',
17
+ ),
18
+ ]
@@ -0,0 +1,21 @@
1
+ from collections import OrderedDict
2
+ from picklefield.fields import PickledObjectField
3
+ from django.db import models
4
+ from django.contrib.contenttypes.models import ContentType
5
+ from django.utils.translation import gettext_lazy as _
6
+
7
+
8
+ class Search(models.Model):
9
+ name = models.CharField(_('Search name'), max_length=255)
10
+ contenttype = models.ForeignKey(ContentType, on_delete=models.CASCADE, verbose_name=_('Model'))
11
+ data = PickledObjectField(_('Serialized filter rule data'))
12
+ created_date = models.DateTimeField(auto_now_add=True)
13
+
14
+ class Meta:
15
+ unique_together = ('name', 'contenttype')
16
+
17
+ def as_lookups(self):
18
+ lookups = OrderedDict()
19
+ for data in self.data:
20
+ lookups[f'{data["field"]}__{data["operator"]}'] = data['value']
21
+ return lookups
@@ -0,0 +1,20 @@
1
+ from django.template import Library
2
+ from django.contrib.admin.helpers import Fieldset
3
+
4
+
5
+ register = Library()
6
+
7
+ @register.inclusion_tag("admin/includes/fieldset.html")
8
+ def as_fieldset(form, heading_level=2, prefix='', id_prefix=0, id_suffix='', **fieldset_kwargs):
9
+ """
10
+ Create and render a fieldset for form.
11
+ """
12
+ fieldset = Fieldset(form, fields=form.fields, **fieldset_kwargs)
13
+ context = dict(
14
+ fieldset=fieldset,
15
+ heading_level=heading_level,
16
+ prefix=prefix,
17
+ id_prefix=id_prefix,
18
+ id_suffix=id_suffix,
19
+ )
20
+ return context