django-searchkit 0.1__py3-none-any.whl → 1.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.
Files changed (83) hide show
  1. build/lib/build/lib/example/example/__init__.py +0 -0
  2. build/lib/build/lib/example/example/admin.py +16 -0
  3. build/lib/build/lib/example/example/asgi.py +16 -0
  4. build/lib/build/lib/example/example/management/__init__.py +0 -0
  5. build/lib/build/lib/example/example/management/commands/__init__.py +0 -0
  6. build/lib/build/lib/example/example/management/commands/createtestdata.py +62 -0
  7. build/lib/build/lib/example/example/migrations/0001_initial.py +48 -0
  8. build/lib/build/lib/example/example/migrations/__init__.py +0 -0
  9. build/lib/build/lib/example/example/models.py +38 -0
  10. build/lib/build/lib/example/example/settings.py +125 -0
  11. build/lib/build/lib/example/example/urls.py +23 -0
  12. build/lib/build/lib/example/example/wsgi.py +16 -0
  13. build/lib/build/lib/searchkit/__init__.py +0 -0
  14. build/lib/build/lib/searchkit/__version__.py +16 -0
  15. build/lib/build/lib/searchkit/admin.py +30 -0
  16. build/lib/build/lib/searchkit/apps.py +6 -0
  17. build/lib/build/lib/searchkit/filters.py +27 -0
  18. build/lib/build/lib/searchkit/forms/__init__.py +5 -0
  19. build/lib/build/lib/searchkit/forms/fields.py +55 -0
  20. build/lib/build/lib/searchkit/forms/search.py +62 -0
  21. build/lib/build/lib/searchkit/forms/searchkit.py +154 -0
  22. build/lib/build/lib/searchkit/forms/utils.py +178 -0
  23. build/lib/build/lib/searchkit/migrations/0001_initial.py +30 -0
  24. build/lib/build/lib/searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
  25. build/lib/build/lib/searchkit/migrations/__init__.py +0 -0
  26. build/lib/build/lib/searchkit/models.py +27 -0
  27. build/lib/build/lib/searchkit/templatetags/__init__.py +0 -0
  28. build/lib/build/lib/searchkit/templatetags/searchkit.py +20 -0
  29. build/lib/build/lib/searchkit/tests.py +402 -0
  30. build/lib/build/lib/searchkit/urls.py +7 -0
  31. build/lib/build/lib/searchkit/views.py +23 -0
  32. build/lib/example/example/__init__.py +0 -0
  33. build/lib/example/example/admin.py +16 -0
  34. build/lib/example/example/asgi.py +16 -0
  35. build/lib/example/example/management/__init__.py +0 -0
  36. build/lib/example/example/management/commands/__init__.py +0 -0
  37. build/lib/example/example/management/commands/createtestdata.py +62 -0
  38. build/lib/example/example/migrations/0001_initial.py +48 -0
  39. build/lib/example/example/migrations/__init__.py +0 -0
  40. build/lib/example/example/models.py +38 -0
  41. build/lib/example/example/settings.py +125 -0
  42. build/lib/example/example/urls.py +23 -0
  43. build/lib/example/example/wsgi.py +16 -0
  44. build/lib/searchkit/__init__.py +0 -0
  45. build/lib/searchkit/__version__.py +16 -0
  46. build/lib/searchkit/admin.py +30 -0
  47. build/lib/searchkit/apps.py +6 -0
  48. build/lib/searchkit/filters.py +27 -0
  49. build/lib/searchkit/forms/__init__.py +5 -0
  50. build/lib/searchkit/forms/fields.py +55 -0
  51. build/lib/searchkit/forms/search.py +62 -0
  52. build/lib/searchkit/forms/searchkit.py +154 -0
  53. build/lib/searchkit/forms/utils.py +178 -0
  54. build/lib/searchkit/migrations/0001_initial.py +30 -0
  55. build/lib/searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
  56. build/lib/searchkit/migrations/__init__.py +0 -0
  57. build/lib/searchkit/models.py +27 -0
  58. build/lib/searchkit/templatetags/__init__.py +0 -0
  59. build/lib/searchkit/templatetags/searchkit.py +20 -0
  60. build/lib/searchkit/tests.py +402 -0
  61. build/lib/searchkit/urls.py +7 -0
  62. build/lib/searchkit/views.py +23 -0
  63. {django_searchkit-0.1.dist-info → django_searchkit-1.1.dist-info}/METADATA +34 -13
  64. django_searchkit-1.1.dist-info/RECORD +99 -0
  65. {django_searchkit-0.1.dist-info → django_searchkit-1.1.dist-info}/WHEEL +1 -1
  66. {django_searchkit-0.1.dist-info → django_searchkit-1.1.dist-info}/top_level.txt +1 -0
  67. example/example/admin.py +1 -1
  68. searchkit/__version__.py +1 -1
  69. searchkit/admin.py +4 -4
  70. searchkit/filters.py +7 -11
  71. searchkit/forms/__init__.py +5 -2
  72. searchkit/forms/search.py +36 -19
  73. searchkit/forms/searchkit.py +61 -99
  74. searchkit/forms/utils.py +44 -15
  75. searchkit/migrations/0002_rename_searchkitsearch_search.py +18 -0
  76. searchkit/models.py +8 -4
  77. searchkit/templatetags/searchkit.py +0 -27
  78. searchkit/tests.py +283 -78
  79. searchkit/urls.py +1 -2
  80. searchkit/views.py +11 -18
  81. django_searchkit-0.1.dist-info/RECORD +0 -36
  82. {django_searchkit-0.1.dist-info → django_searchkit-1.1.dist-info}/licenses/LICENCE +0 -0
  83. {django_searchkit-0.1.dist-info → django_searchkit-1.1.dist-info}/zip-safe +0 -0
File without changes
@@ -0,0 +1,16 @@
1
+ from django.contrib import admin
2
+ from searchkit.filters import SearchkitFilter
3
+ from .models import ModelA
4
+ from .models import ModelB
5
+
6
+
7
+ @admin.register(ModelA)
8
+ class ModelAAdmin(admin.ModelAdmin):
9
+ list_display = [f.name for f in ModelA._meta.fields]
10
+ list_filter = [SearchkitFilter, 'chars_choices']
11
+
12
+
13
+ @admin.register(ModelB)
14
+ class ModelBAdmin(admin.ModelAdmin):
15
+ list_display = [f.name for f in ModelB._meta.fields]
16
+ list_filter = [SearchkitFilter]
@@ -0,0 +1,16 @@
1
+ """
2
+ ASGI config for example project.
3
+
4
+ It exposes the ASGI 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/asgi/
8
+ """
9
+
10
+ import os
11
+
12
+ from django.core.asgi import get_asgi_application
13
+
14
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings')
15
+
16
+ application = get_asgi_application()
@@ -0,0 +1,62 @@
1
+ import random
2
+ from datetime import timedelta
3
+ from decimal import Decimal
4
+ from django.utils import timezone
5
+ from django.core.management.base import BaseCommand
6
+ from django.contrib.auth.models import User
7
+ from example.models import ModelA, ModelB, CHARS_CHOICES, INTEGER_CHOICES
8
+
9
+
10
+ class Command(BaseCommand):
11
+ help = 'Generate test data for ModelA and ModelB'
12
+
13
+ def handle(self, *args, **kwargs):
14
+ self.stdout.write("Creating test data...")
15
+
16
+ # Create a superuser
17
+ if not User.objects.filter(username='admin').exists():
18
+ User.objects.create_superuser(
19
+ username='admin',
20
+ email='admin@example.com',
21
+ password='adminpassword'
22
+ )
23
+ self.stdout.write(self.style.SUCCESS("Superuser 'admin' created successfully!"))
24
+ else:
25
+ self.stdout.write(self.style.WARNING("Superuser 'admin' already exists."))
26
+
27
+ # Generate test data for ModelA
28
+ model_a_instances = []
29
+ for i in range(10):
30
+ model_a = ModelA.objects.create(
31
+ boolean=random.choice([None, True, False]),
32
+ chars=f"ModelA_Chars_{i}",
33
+ chars_choices=random.choice([c[0] for c in CHARS_CHOICES]),
34
+ integer=random.randint(1, 100),
35
+ integer_choices=random.choice([c[0] for c in INTEGER_CHOICES]),
36
+ float=random.uniform(1.0, 100.0),
37
+ decimal=Decimal(f"{random.randint(1, 100)}.{random.randint(0, 99)}"),
38
+ date=timezone.now().date() - timedelta(days=random.randint(0, 365)), # Use timezone.now()
39
+ datetime=timezone.now() - timedelta(days=random.randint(0, 365)) # Use timezone.now()
40
+ )
41
+ model_a_instances.append(model_a)
42
+
43
+ # Generate test data for ModelB
44
+ model_b_instances = []
45
+ for i in range(10):
46
+ model_b = ModelB.objects.create(
47
+ chars=f"ModelB_Chars_{i}",
48
+ integer=random.randint(1, 100),
49
+ float=random.uniform(1.0, 100.0),
50
+ decimal=Decimal(f"{random.randint(1, 100)}.{random.randint(0, 99)}"),
51
+ date=timezone.now().date() - timedelta(days=random.randint(0, 365)), # Use timezone.now()
52
+ datetime=timezone.now() - timedelta(days=random.randint(0, 365)), # Use timezone.now()
53
+ model_a=random.choice(model_a_instances)
54
+ )
55
+ model_b_instances.append(model_b)
56
+
57
+ # Add modela -> modelb relation.
58
+ for index, model_a in enumerate(model_a_instances):
59
+ model_a.model_b = model_b_instances[index]
60
+ model_a.save()
61
+
62
+ self.stdout.write(self.style.SUCCESS("Test data created successfully!"))
@@ -0,0 +1,48 @@
1
+ # Generated by Django 5.1.3 on 2025-05-25 05:46
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name='ModelA',
17
+ fields=[
18
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19
+ ('boolean', models.BooleanField(null=True)),
20
+ ('chars', models.CharField(max_length=255)),
21
+ ('chars_choices', models.CharField(choices=[('one', 'One'), ('two', 'Two'), ('three', 'Three'), ('four', 'Four')], max_length=255)),
22
+ ('integer', models.IntegerField()),
23
+ ('integer_choices', models.IntegerField(choices=[(1, 1), (2, 2), (3, 3), (4, 4)])),
24
+ ('float', models.FloatField()),
25
+ ('decimal', models.DecimalField(decimal_places=2, max_digits=5)),
26
+ ('date', models.DateField()),
27
+ ('datetime', models.DateTimeField()),
28
+ ],
29
+ ),
30
+ migrations.CreateModel(
31
+ name='ModelB',
32
+ fields=[
33
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34
+ ('chars', models.CharField(max_length=255)),
35
+ ('integer', models.IntegerField()),
36
+ ('float', models.FloatField()),
37
+ ('decimal', models.DecimalField(decimal_places=2, max_digits=5)),
38
+ ('date', models.DateField()),
39
+ ('datetime', models.DateTimeField()),
40
+ ('model_a', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='example.modela')),
41
+ ],
42
+ ),
43
+ migrations.AddField(
44
+ model_name='modela',
45
+ name='model_b',
46
+ field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='example.modelb'),
47
+ ),
48
+ ]
@@ -0,0 +1,38 @@
1
+ from django.db import models
2
+
3
+
4
+ CHARS_CHOICES = (
5
+ ('one', 'One'),
6
+ ('two', 'Two'),
7
+ ('three', 'Three'),
8
+ ('four', 'Four'),
9
+ )
10
+ INTEGER_CHOICES = (
11
+ (1, 1),
12
+ (2, 2),
13
+ (3, 3),
14
+ (4, 4),
15
+ )
16
+
17
+
18
+ class ModelA(models.Model):
19
+ boolean = models.BooleanField(null=True)
20
+ chars = models.CharField(max_length=255)
21
+ chars_choices = models.CharField(max_length=255, choices=CHARS_CHOICES)
22
+ integer = models.IntegerField()
23
+ integer_choices = models.IntegerField(choices=INTEGER_CHOICES)
24
+ float = models.FloatField()
25
+ decimal = models.DecimalField(max_digits=5, decimal_places=2)
26
+ date = models.DateField()
27
+ datetime = models.DateTimeField()
28
+ model_b = models.OneToOneField('ModelB', on_delete=models.CASCADE, null=True)
29
+
30
+
31
+ class ModelB(models.Model):
32
+ chars = models.CharField(max_length=255)
33
+ integer = models.IntegerField()
34
+ float = models.FloatField()
35
+ decimal = models.DecimalField(max_digits=5, decimal_places=2)
36
+ date = models.DateField()
37
+ datetime = models.DateTimeField()
38
+ model_a = models.ForeignKey(ModelA, on_delete=models.CASCADE)
@@ -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.1"
@@ -0,0 +1,30 @@
1
+ from django.contrib import admin
2
+ from django.http import HttpResponseRedirect
3
+ from django.urls import reverse
4
+ from .models import Search
5
+ from .forms import SearchForm
6
+ from .filters import SearchkitFilter
7
+
8
+
9
+ @admin.register(Search)
10
+ class SearchkitSearchAdmin(admin.ModelAdmin):
11
+ form = SearchForm
12
+ list_display = ('name', 'contenttype', 'created_date')
13
+
14
+ def get_url_for_applied_search(self, obj):
15
+ app_label = obj.contenttype.app_label
16
+ model_name = obj.contenttype.model
17
+ base_url = reverse(f'admin:{app_label}_{model_name}_changelist')
18
+ return f'{base_url}?{SearchkitFilter.parameter_name}={obj.pk}'
19
+
20
+ def response_add(self, request, obj, *args, **kwargs):
21
+ if '_save_and_apply' in request.POST:
22
+ return HttpResponseRedirect(self.get_url_for_applied_search(obj))
23
+ else:
24
+ return super().response_add(request, obj, *args, **kwargs)
25
+
26
+ def response_change(self, request, obj, *args, **kwargs):
27
+ if '_save_and_apply' in request.POST:
28
+ return HttpResponseRedirect(self.get_url_for_applied_search(obj))
29
+ else:
30
+ return super().response_change(request, obj, *args, **kwargs)
@@ -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,27 @@
1
+ from django.contrib.admin import SimpleListFilter
2
+ from django.contrib.contenttypes.models import ContentType
3
+ from .models import Search
4
+
5
+
6
+ class SearchkitFilter(SimpleListFilter):
7
+ title = 'Searchkit Filter'
8
+ parameter_name = 'search'
9
+ template = 'searchkit/searchkit_filter.html'
10
+
11
+ def __init__(self, request, params, model, model_admin):
12
+ # We need the app_label and model as get parameter for the new search
13
+ # link.
14
+ self.searchkit_model = ContentType.objects.get_for_model(model)
15
+ super().__init__(request, params, model, model_admin)
16
+
17
+ def lookups(self, request, model_admin):
18
+ # Fetch the last three objects from SearchkitSearch and return them as
19
+ # choices.
20
+ searches = Search.objects.filter(contenttype=self.searchkit_model).order_by('-created_date')[:3]
21
+ return [(str(obj.id), obj.name) for obj in searches]
22
+
23
+ def queryset(self, request, queryset):
24
+ # Filter the queryset based on the selected SearchkitSearch object
25
+ if self.value():
26
+ search = Search.objects.get(id=int(self.value()))
27
+ return search.as_queryset()
@@ -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,55 @@
1
+ from django import forms
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class RangeWidget(forms.MultiWidget):
6
+ def decompress(self, value):
7
+ """The value should be already a list."""
8
+ if value:
9
+ return value
10
+ else:
11
+ return [None, None]
12
+
13
+
14
+ class BaseRangeField(forms.MultiValueField):
15
+ incomplete_message = None
16
+ field_type = None
17
+ widget_type = None
18
+ field_kwargs = dict()
19
+
20
+ def __init__(self, **kwargs):
21
+ error_messages = dict(incomplete=self.incomplete_message)
22
+ widget = RangeWidget(widgets=[self.widget_type, self.widget_type])
23
+ fields = (
24
+ self.field_type(label=_('From'), **self.field_kwargs),
25
+ self.field_type(label=_('To'), **self.field_kwargs),
26
+ )
27
+ super().__init__(
28
+ fields=fields,
29
+ widget=widget,
30
+ error_messages=error_messages,
31
+ require_all_fields=True,
32
+ **kwargs
33
+ )
34
+
35
+ def compress(self, data_list):
36
+ """We want the data list as data list."""
37
+ return data_list
38
+
39
+
40
+ class IntegerRangeField(BaseRangeField):
41
+ incomplete_message = _("Enter the first and the last number.")
42
+ field_type = forms.IntegerField
43
+ widget_type = forms.NumberInput
44
+
45
+
46
+ class DateRangeField(BaseRangeField):
47
+ incomplete_message = _("Enter the first and the last date.")
48
+ field_type = forms.DateField
49
+ widget_type = forms.DateInput
50
+
51
+
52
+ class DateTimeRangeField(BaseRangeField):
53
+ incomplete_message = _("Enter the first and the last datetime.")
54
+ field_type = forms.DateTimeField
55
+ widget_type = forms.DateTimeInput
@@ -0,0 +1,62 @@
1
+ from django import forms
2
+ from django.utils.functional import cached_property
3
+ from django.contrib.contenttypes.models import ContentType
4
+ from ..models import Search
5
+ from .searchkit import SearchkitModelForm
6
+ from .searchkit import searchkit_formset_factory
7
+ from .utils import MediaMixin
8
+
9
+
10
+ class SearchForm(MediaMixin, forms.ModelForm):
11
+ """
12
+ Represents a SearchkitSearch model. Using a SearchkitFormSet for the data
13
+ json field.
14
+ """
15
+ class Meta:
16
+ model = Search
17
+ fields = ['name']
18
+
19
+ @cached_property
20
+ def searchkit_model(self):
21
+ if self.instance.pk:
22
+ return self.instance.contenttype.model_class()
23
+ elif self.searchkit_model_form.is_valid():
24
+ return self.searchkit_model_form.cleaned_data['searchkit_model'].model_class()
25
+ elif 'searchkit_model' in self.searchkit_model_form.initial:
26
+ value = self.searchkit_model_form.initial['searchkit_model']
27
+ try:
28
+ return self.searchkit_model_form.fields['searchkit_model'].clean(value).model_class()
29
+ except forms.ValidationError:
30
+ return None
31
+
32
+ @cached_property
33
+ def searchkit_model_form(self):
34
+ kwargs = dict(data=self.data or None, initial=self.initial or None)
35
+ if self.instance.pk:
36
+ kwargs['initial'] = dict(searchkit_model=self.instance.contenttype)
37
+ return SearchkitModelForm(**kwargs)
38
+
39
+ @cached_property
40
+ def formset(self):
41
+ """
42
+ A searchkit formset for the model.
43
+ """
44
+ kwargs = dict()
45
+ if self.searchkit_model and self.data:
46
+ kwargs = dict(data=self.data)
47
+ elif self.searchkit_model and self.instance.pk:
48
+ kwargs = dict(initial=self.instance.data)
49
+
50
+ extra = 0 if self.instance.pk else 1
51
+ formset = searchkit_formset_factory(model=self.searchkit_model, extra=extra)
52
+ return formset(**kwargs)
53
+
54
+ def is_valid(self):
55
+ return self.formset.is_valid() and self.searchkit_model_form.is_valid and super().is_valid()
56
+
57
+ def clean(self):
58
+ if self.searchkit_model_form.is_valid():
59
+ self.instance.contenttype = self.searchkit_model_form.cleaned_data['searchkit_model']
60
+ if self.formset.is_valid():
61
+ self.instance.data = self.formset.cleaned_data
62
+ return super().clean()