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,125 @@
1
+ """
2
+ Django settings for example project.
3
+
4
+ Generated by 'django-admin startproject' using Django 4.1.6.
5
+
6
+ For more information on this file, see
7
+ https://docs.djangoproject.com/en/4.1/topics/settings/
8
+
9
+ For the full list of settings and their values, see
10
+ https://docs.djangoproject.com/en/4.1/ref/settings/
11
+ """
12
+
13
+ from pathlib import Path
14
+
15
+ # Build paths inside the project like this: BASE_DIR / 'subdir'.
16
+ BASE_DIR = Path(__file__).resolve().parent.parent
17
+
18
+
19
+ # Quick-start development settings - unsuitable for production
20
+ # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
21
+
22
+ # SECURITY WARNING: keep the secret key used in production secret!
23
+ SECRET_KEY = 'django-insecure-^@et7v8l)mu@#@tooytj77v&jtkqy&i^wgp9+ro&v!rb6f1y__'
24
+
25
+ # SECURITY WARNING: don't run with debug turned on in production!
26
+ DEBUG = True
27
+
28
+ ALLOWED_HOSTS = []
29
+
30
+
31
+ # Application definition
32
+
33
+ INSTALLED_APPS = [
34
+ 'example',
35
+ 'searchkit',
36
+ 'django.contrib.admin',
37
+ 'django.contrib.auth',
38
+ 'django.contrib.contenttypes',
39
+ 'django.contrib.sessions',
40
+ 'django.contrib.messages',
41
+ 'django.contrib.staticfiles',
42
+ ]
43
+
44
+ MIDDLEWARE = [
45
+ 'django.middleware.security.SecurityMiddleware',
46
+ 'django.contrib.sessions.middleware.SessionMiddleware',
47
+ 'django.middleware.common.CommonMiddleware',
48
+ 'django.middleware.csrf.CsrfViewMiddleware',
49
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
50
+ 'django.contrib.messages.middleware.MessageMiddleware',
51
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
52
+ ]
53
+
54
+ ROOT_URLCONF = 'example.urls'
55
+
56
+ TEMPLATES = [
57
+ {
58
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
59
+ 'DIRS': [],
60
+ 'APP_DIRS': True,
61
+ 'OPTIONS': {
62
+ 'context_processors': [
63
+ 'django.template.context_processors.debug',
64
+ 'django.template.context_processors.request',
65
+ 'django.contrib.auth.context_processors.auth',
66
+ 'django.contrib.messages.context_processors.messages',
67
+ ],
68
+ },
69
+ },
70
+ ]
71
+
72
+ WSGI_APPLICATION = 'example.wsgi.application'
73
+
74
+
75
+ # Database
76
+ # https://docs.djangoproject.com/en/4.1/ref/settings/#databases
77
+
78
+ DATABASES = {
79
+ 'default': {
80
+ 'ENGINE': 'django.db.backends.sqlite3',
81
+ 'NAME': BASE_DIR / 'db.sqlite3',
82
+ }
83
+ }
84
+
85
+
86
+ # Password validation
87
+ # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
88
+
89
+ AUTH_PASSWORD_VALIDATORS = [
90
+ {
91
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
92
+ },
93
+ {
94
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
95
+ },
96
+ {
97
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
98
+ },
99
+ {
100
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
101
+ },
102
+ ]
103
+
104
+
105
+ # Internationalization
106
+ # https://docs.djangoproject.com/en/4.1/topics/i18n/
107
+
108
+ LANGUAGE_CODE = 'en-us'
109
+
110
+ TIME_ZONE = 'UTC'
111
+
112
+ USE_I18N = True
113
+
114
+ USE_TZ = True
115
+
116
+
117
+ # Static files (CSS, JavaScript, Images)
118
+ # https://docs.djangoproject.com/en/4.1/howto/static-files/
119
+
120
+ STATIC_URL = 'static/'
121
+
122
+ # Default primary key field type
123
+ # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
124
+
125
+ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
@@ -0,0 +1,23 @@
1
+ """example URL Configuration
2
+
3
+ The `urlpatterns` list routes URLs to views. For more information please see:
4
+ https://docs.djangoproject.com/en/4.1/topics/http/urls/
5
+ Examples:
6
+ Function views
7
+ 1. Add an import: from my_app import views
8
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
9
+ Class-based views
10
+ 1. Add an import: from other_app.views import Home
11
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12
+ Including another URLconf
13
+ 1. Import the include() function: from django.urls import include, path
14
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15
+ """
16
+ from django.contrib import admin
17
+ from django.urls import path, include
18
+
19
+
20
+ urlpatterns = [
21
+ path('', include('searchkit.urls')),
22
+ path('admin/', admin.site.urls),
23
+ ]
@@ -0,0 +1,16 @@
1
+ """
2
+ WSGI config for example project.
3
+
4
+ It exposes the WSGI callable as a module-level variable named ``application``.
5
+
6
+ For more information on this file, see
7
+ https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
8
+ """
9
+
10
+ import os
11
+
12
+ from django.core.wsgi import get_wsgi_application
13
+
14
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings')
15
+
16
+ application = get_wsgi_application()
File without changes
@@ -0,0 +1,16 @@
1
+ """
2
+ This project uses the Semantic Versioning scheme in conjunction with PEP 0440:
3
+ <http://semver.org/>
4
+ <https://www.python.org/dev/peps/pep-0440>
5
+
6
+ Major versions introduce significant changes to the API, and backwards
7
+ compatibility is not guaranteed. Minor versions are for new features and other
8
+ backwards-compatible changes to the API. Patch versions are for bug fixes and
9
+ internal code changes that do not affect the API. Development versions are
10
+ incomplete states of a release .
11
+
12
+ Version 0.x should be considered a development version with an unstable API,
13
+ and backwards compatibility is not guaranteed for minor versions.
14
+ """
15
+
16
+ __version__ = "1.3"
@@ -0,0 +1,44 @@
1
+ from django.contrib import admin
2
+ from django.http import HttpResponseRedirect
3
+ from django.urls import reverse
4
+ from django.utils.html import format_html
5
+ from .models import Search
6
+ from .forms import SearchForm
7
+ from .filters import SearchkitFilter
8
+ from .filters import SearchableModelFilter
9
+
10
+
11
+ @admin.register(Search)
12
+ class SearchkitSearchAdmin(admin.ModelAdmin):
13
+ form = SearchForm
14
+ list_display = ('name', 'contenttype', 'created_date', 'apply_search_view')
15
+ list_filter = (('contenttype', SearchableModelFilter),)
16
+
17
+ def get_url_for_applied_search(self, obj):
18
+ app_label = obj.contenttype.app_label
19
+ model_name = obj.contenttype.model
20
+ base_url = reverse(f'admin:{app_label}_{model_name}_changelist')
21
+ return f'{base_url}?{SearchkitFilter.parameter_name}={obj.pk}'
22
+
23
+ def response_add(self, request, obj, *args, **kwargs):
24
+ if '_save_and_apply' in request.POST:
25
+ return HttpResponseRedirect(self.get_url_for_applied_search(obj))
26
+ else:
27
+ return super().response_add(request, obj, *args, **kwargs)
28
+
29
+ def response_change(self, request, obj, *args, **kwargs):
30
+ if '_save_and_apply' in request.POST:
31
+ return HttpResponseRedirect(self.get_url_for_applied_search(obj))
32
+ else:
33
+ return super().response_change(request, obj, *args, **kwargs)
34
+
35
+ def apply_search_view(self, obj):
36
+ """
37
+ Returns a link to apply the search.
38
+ """
39
+ return format_html(
40
+ '<a href="{}" >Apply search "{}"</a>',
41
+ self.get_url_for_applied_search(obj),
42
+ obj.name
43
+ )
44
+ apply_search_view.short_description = 'Apply Search'
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class SearchkitConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'searchkit'
@@ -0,0 +1,36 @@
1
+ from django.contrib import admin
2
+ from django.contrib.contenttypes.models import ContentType
3
+ from .models import Search
4
+ from .utils import is_searchable_model
5
+
6
+
7
+ class SearchkitFilter(admin.SimpleListFilter):
8
+ title = 'Searchkit Filter'
9
+ parameter_name = 'search'
10
+ template = 'searchkit/searchkit_filter.html'
11
+
12
+ def __init__(self, request, params, model, model_admin):
13
+ # We need the app_label and model as get parameter for the new search
14
+ # link.
15
+ self.searchkit_model = ContentType.objects.get_for_model(model)
16
+ super().__init__(request, params, model, model_admin)
17
+
18
+ def has_output(self):
19
+ return True
20
+
21
+ def lookups(self, request, model_admin):
22
+ searches = Search.objects.filter(contenttype=self.searchkit_model).order_by('-created_date')
23
+ return [(str(obj.id), obj.name) for obj in searches]
24
+
25
+ def queryset(self, request, queryset):
26
+ # Filter the queryset based on the selected SearchkitSearch object
27
+ if self.value():
28
+ search = Search.objects.get(id=int(self.value()))
29
+ return queryset.filter(**search.as_lookups())
30
+
31
+
32
+ class SearchableModelFilter(admin.filters.RelatedFieldListFilter):
33
+ def __init__(self, *args, **kwargs):
34
+ super().__init__(*args, **kwargs)
35
+ contenttypes = ContentType.objects.order_by('app_label', 'model')
36
+ self.lookup_choices = [(m.id, m) for m in contenttypes if is_searchable_model(m.model_class())]
@@ -0,0 +1,5 @@
1
+ from .search import SearchForm
2
+ from .searchkit import SearchkitModelForm
3
+ from .searchkit import BaseSearchkitFormSet
4
+ from .searchkit import BaseSearchkitForm
5
+ from .searchkit import searchkit_formset_factory
@@ -0,0 +1,56 @@
1
+ from django import forms
2
+ from django.contrib.admin import widgets
3
+ from django.utils.translation import gettext_lazy as _
4
+
5
+
6
+ class RangeWidget(forms.MultiWidget):
7
+ def decompress(self, value):
8
+ """The value should be already a list."""
9
+ if value:
10
+ return value
11
+ else:
12
+ return [None, None]
13
+
14
+
15
+ class BaseRangeField(forms.MultiValueField):
16
+ incomplete_message = None
17
+ field_type = None
18
+ widget_type = None
19
+ field_kwargs = dict()
20
+
21
+ def __init__(self, **kwargs):
22
+ error_messages = dict(incomplete=self.incomplete_message)
23
+ widget = RangeWidget(widgets=[self.widget_type, self.widget_type])
24
+ fields = (
25
+ self.field_type(label=_('From'), **self.field_kwargs),
26
+ self.field_type(label=_('To'), **self.field_kwargs),
27
+ )
28
+ super().__init__(
29
+ fields=fields,
30
+ widget=widget,
31
+ error_messages=error_messages,
32
+ require_all_fields=True,
33
+ **kwargs
34
+ )
35
+
36
+ def compress(self, data_list):
37
+ """We want the data list as data list."""
38
+ return data_list
39
+
40
+
41
+ class IntegerRangeField(BaseRangeField):
42
+ incomplete_message = _("Enter the first and the last number.")
43
+ field_type = forms.IntegerField
44
+ widget_type = forms.NumberInput
45
+
46
+
47
+ class DateRangeField(BaseRangeField):
48
+ incomplete_message = _("Enter the first and the last date.")
49
+ field_type = forms.DateField
50
+ widget_type = widgets.AdminDateWidget
51
+
52
+
53
+ class DateTimeRangeField(DateRangeField):
54
+ incomplete_message = _("Enter the first and the last datetime.")
55
+ field_type = forms.SplitDateTimeField
56
+ widget_type = widgets.AdminSplitDateTime
@@ -0,0 +1,61 @@
1
+ from django import forms
2
+ from django.utils.functional import cached_property
3
+ from ..models import Search
4
+ from .searchkit import SearchkitModelForm
5
+ from .searchkit import searchkit_formset_factory
6
+ from .utils import MediaMixin
7
+
8
+
9
+ class SearchForm(MediaMixin, forms.ModelForm):
10
+ """
11
+ Represents a SearchkitSearch model. Using a SearchkitFormSet for the data
12
+ json field.
13
+ """
14
+ class Meta:
15
+ model = Search
16
+ fields = ['name']
17
+
18
+ @cached_property
19
+ def searchkit_model(self):
20
+ if self.instance.pk:
21
+ return self.instance.contenttype.model_class()
22
+ elif self.searchkit_model_form.is_valid():
23
+ return self.searchkit_model_form.cleaned_data['searchkit_model'].model_class()
24
+ elif 'searchkit_model' in self.searchkit_model_form.initial:
25
+ value = self.searchkit_model_form.initial['searchkit_model']
26
+ try:
27
+ return self.searchkit_model_form.fields['searchkit_model'].clean(value).model_class()
28
+ except forms.ValidationError:
29
+ return None
30
+
31
+ @cached_property
32
+ def searchkit_model_form(self):
33
+ kwargs = dict(data=self.data or None, initial=self.initial or None)
34
+ if self.instance.pk:
35
+ kwargs['initial'] = dict(searchkit_model=self.instance.contenttype)
36
+ return SearchkitModelForm(**kwargs)
37
+
38
+ @cached_property
39
+ def formset(self):
40
+ """
41
+ A searchkit formset for the model.
42
+ """
43
+ kwargs = dict()
44
+ if self.searchkit_model and self.data:
45
+ kwargs = dict(data=self.data)
46
+ elif self.searchkit_model and self.instance.pk:
47
+ kwargs = dict(initial=self.instance.data)
48
+
49
+ extra = 0 if self.instance.pk else 1
50
+ formset = searchkit_formset_factory(model=self.searchkit_model, extra=extra)
51
+ return formset(**kwargs)
52
+
53
+ def is_valid(self):
54
+ return self.formset.is_valid() and self.searchkit_model_form.is_valid and super().is_valid()
55
+
56
+ def clean(self):
57
+ if self.searchkit_model_form.is_valid():
58
+ self.instance.contenttype = self.searchkit_model_form.cleaned_data['searchkit_model']
59
+ if self.formset.is_valid():
60
+ self.instance.data = self.formset.cleaned_data
61
+ return super().clean()
@@ -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
+ )