django-smartbase-admin 1.0.32__py3-none-any.whl → 1.0.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_smartbase_admin/admin/admin_base.py +45 -8
- django_smartbase_admin/admin/widgets.py +211 -16
- django_smartbase_admin/engine/configuration.py +1 -1
- django_smartbase_admin/engine/filter_widgets.py +39 -1
- django_smartbase_admin/engine/request.py +2 -0
- django_smartbase_admin/services/views.py +3 -2
- django_smartbase_admin/static/sb_admin/dist/main.js +1 -1
- django_smartbase_admin/static/sb_admin/dist/main_style.css +1 -1
- django_smartbase_admin/static/sb_admin/src/js/autocomplete.js +9 -1
- django_smartbase_admin/static/sb_admin/src/js/choices.js +14 -12
- django_smartbase_admin/static/sb_admin/src/js/datepicker.js +1 -1
- django_smartbase_admin/templates/sb_admin/actions/change_form.html +2 -3
- django_smartbase_admin/templates/sb_admin/widgets/autocomplete.html +1 -1
- django_smartbase_admin/templates/sb_admin/widgets/includes/related_item_buttons.html +2 -2
- {django_smartbase_admin-1.0.32.dist-info → django_smartbase_admin-1.0.34.dist-info}/METADATA +1 -1
- {django_smartbase_admin-1.0.32.dist-info → django_smartbase_admin-1.0.34.dist-info}/RECORD +18 -19
- django_smartbase_admin/templates/sb_admin/integrations/sorting/change_list.html +0 -401
- {django_smartbase_admin-1.0.32.dist-info → django_smartbase_admin-1.0.34.dist-info}/LICENSE.md +0 -0
- {django_smartbase_admin-1.0.32.dist-info → django_smartbase_admin-1.0.34.dist-info}/WHEEL +0 -0
|
@@ -4,6 +4,7 @@ import urllib.parse
|
|
|
4
4
|
from collections.abc import Iterable
|
|
5
5
|
from functools import partial
|
|
6
6
|
from typing import Any
|
|
7
|
+
from urllib.parse import urlparse
|
|
7
8
|
|
|
8
9
|
from ckeditor.fields import RichTextFormField
|
|
9
10
|
from ckeditor_uploader.fields import RichTextUploadingFormField
|
|
@@ -30,7 +31,7 @@ from django.forms.models import (
|
|
|
30
31
|
from django.http import HttpResponse
|
|
31
32
|
from django.template.loader import render_to_string
|
|
32
33
|
from django.template.response import TemplateResponse
|
|
33
|
-
from django.urls import reverse, NoReverseMatch
|
|
34
|
+
from django.urls import reverse, NoReverseMatch, resolve
|
|
34
35
|
from django.utils.safestring import mark_safe, SafeString
|
|
35
36
|
from django.utils.text import capfirst
|
|
36
37
|
from django.utils.translation import gettext_lazy as _
|
|
@@ -126,6 +127,7 @@ from django_smartbase_admin.engine.admin_base_view import (
|
|
|
126
127
|
SBADMIN_PARENT_INSTANCE_PK_VAR,
|
|
127
128
|
SBADMIN_PARENT_INSTANCE_LABEL_VAR,
|
|
128
129
|
SBADMIN_PARENT_INSTANCE_FIELD_NAME_VAR,
|
|
130
|
+
SBADMIN_RELOAD_ON_SAVE_VAR,
|
|
129
131
|
)
|
|
130
132
|
from django_smartbase_admin.engine.const import (
|
|
131
133
|
OBJECT_ID_PLACEHOLDER,
|
|
@@ -316,6 +318,13 @@ class SBAdminBaseFormInit(SBAdminFormFieldWidgetsMixin, FormFieldsetMixin):
|
|
|
316
318
|
"request", SBAdminThreadLocalService.get_request()
|
|
317
319
|
)
|
|
318
320
|
super().__init__(*args, **kwargs)
|
|
321
|
+
self.init_widgets_dynamic(threadsafe_request)
|
|
322
|
+
for field in self.declared_fields:
|
|
323
|
+
form_field = self.fields.get(field)
|
|
324
|
+
if form_field:
|
|
325
|
+
self.assign_widget_to_form_field(form_field, request=threadsafe_request)
|
|
326
|
+
|
|
327
|
+
def init_widgets_dynamic(self, request):
|
|
319
328
|
for field in self.fields:
|
|
320
329
|
if not hasattr(self.fields[field].widget, "init_widget_dynamic"):
|
|
321
330
|
continue
|
|
@@ -324,12 +333,8 @@ class SBAdminBaseFormInit(SBAdminFormFieldWidgetsMixin, FormFieldsetMixin):
|
|
|
324
333
|
self.fields[field],
|
|
325
334
|
field,
|
|
326
335
|
self.view,
|
|
327
|
-
|
|
336
|
+
request,
|
|
328
337
|
)
|
|
329
|
-
for field in self.declared_fields:
|
|
330
|
-
form_field = self.fields.get(field)
|
|
331
|
-
if form_field:
|
|
332
|
-
self.assign_widget_to_form_field(form_field, request=threadsafe_request)
|
|
333
338
|
|
|
334
339
|
|
|
335
340
|
class SBAdminBaseForm(SBAdminBaseFormInit, forms.ModelForm):
|
|
@@ -822,7 +827,14 @@ class SBAdmin(
|
|
|
822
827
|
return Q()
|
|
823
828
|
|
|
824
829
|
def get_change_view_context(self, request, object_id) -> dict | dict[str, Any]:
|
|
825
|
-
return {
|
|
830
|
+
return {
|
|
831
|
+
"show_back_button": True,
|
|
832
|
+
"back_url": reverse(
|
|
833
|
+
"sb_admin:{}_{}_changelist".format(
|
|
834
|
+
self.opts.app_label, self.opts.model_name
|
|
835
|
+
)
|
|
836
|
+
),
|
|
837
|
+
}
|
|
826
838
|
|
|
827
839
|
def get_previous_next_context(self, request, object_id) -> dict | dict[str, Any]:
|
|
828
840
|
if not self.sbadmin_previous_next_buttons_enabled or not object_id:
|
|
@@ -958,7 +970,7 @@ class SBAdmin(
|
|
|
958
970
|
"field": request.POST.get("sb_admin_source_field"),
|
|
959
971
|
"id": obj.pk,
|
|
960
972
|
"label": str(obj),
|
|
961
|
-
"reload": request.POST.get(
|
|
973
|
+
"reload": request.POST.get(SBADMIN_RELOAD_ON_SAVE_VAR) == "1",
|
|
962
974
|
},
|
|
963
975
|
)
|
|
964
976
|
trigger_client_event(response, "hideModal", {"elt": "sb-admin-modal"})
|
|
@@ -1051,6 +1063,31 @@ class SBAdminInline(
|
|
|
1051
1063
|
self.initialize_form_class(form_class, request)
|
|
1052
1064
|
form_class()
|
|
1053
1065
|
|
|
1066
|
+
def get_parent_instance_from_request(self):
|
|
1067
|
+
# Try to get parent instance from request referrer
|
|
1068
|
+
request = (
|
|
1069
|
+
getattr(self, "threadsafe_request", None)
|
|
1070
|
+
or SBAdminThreadLocalService.get_request()
|
|
1071
|
+
)
|
|
1072
|
+
allowed = SBAdminViewService.has_permission(
|
|
1073
|
+
request=request, model=self.parent_model, permission="view"
|
|
1074
|
+
)
|
|
1075
|
+
if not allowed:
|
|
1076
|
+
return None
|
|
1077
|
+
|
|
1078
|
+
referer = request.META.get("HTTP_REFERER")
|
|
1079
|
+
if not referer:
|
|
1080
|
+
return None
|
|
1081
|
+
resolved = resolve(urlparse(referer).path)
|
|
1082
|
+
# Try common kwargs for object ID
|
|
1083
|
+
object_id = resolved.kwargs.get("object_id")
|
|
1084
|
+
if not object_id:
|
|
1085
|
+
return None
|
|
1086
|
+
base_qs = SBAdminViewService.get_restricted_queryset(
|
|
1087
|
+
self.parent_model, request, request.request_data
|
|
1088
|
+
)
|
|
1089
|
+
return base_qs.get(pk=object_id)
|
|
1090
|
+
|
|
1054
1091
|
def get_context_data(self, request) -> dict[str, Any]:
|
|
1055
1092
|
is_sortable_active: bool = self.sortable_field_name and (
|
|
1056
1093
|
self.has_add_permission(request) or self.has_change_permission(request)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
import sys
|
|
3
4
|
|
|
4
5
|
from ckeditor.widgets import CKEditorWidget
|
|
5
6
|
from ckeditor_uploader.widgets import CKEditorUploadingWidget
|
|
@@ -10,7 +11,7 @@ from django.contrib.admin.widgets import (
|
|
|
10
11
|
ForeignKeyRawIdWidget,
|
|
11
12
|
)
|
|
12
13
|
from django.contrib.auth.forms import ReadOnlyPasswordHashWidget
|
|
13
|
-
from django.core.exceptions import ValidationError
|
|
14
|
+
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
|
14
15
|
from django.template.loader import render_to_string
|
|
15
16
|
from django.urls import reverse
|
|
16
17
|
from django.utils.formats import get_format
|
|
@@ -358,11 +359,19 @@ class SBAdminAutocompleteWidget(
|
|
|
358
359
|
form = None
|
|
359
360
|
field_name = None
|
|
360
361
|
initialised = None
|
|
362
|
+
default_create_data = None
|
|
363
|
+
reload_on_save = None
|
|
364
|
+
REQUEST_CREATED_DATA_KEY = "autocomplete_created_data"
|
|
361
365
|
|
|
362
366
|
def __init__(self, form_field=None, *args, **kwargs):
|
|
363
367
|
attrs = kwargs.pop("attrs", None)
|
|
368
|
+
self.reload_on_save = kwargs.pop("reload_on_save", False)
|
|
364
369
|
super().__init__(form_field, *args, **kwargs)
|
|
365
370
|
self.attrs = {} if attrs is None else attrs.copy()
|
|
371
|
+
if self.multiselect and self.allow_add:
|
|
372
|
+
raise ImproperlyConfigured(
|
|
373
|
+
"Multiselect with creation is currently not supported."
|
|
374
|
+
)
|
|
366
375
|
|
|
367
376
|
def get_id(self):
|
|
368
377
|
base_id = super().get_id()
|
|
@@ -370,7 +379,9 @@ class SBAdminAutocompleteWidget(
|
|
|
370
379
|
base_id += f"_{self.form.__class__.__name__}"
|
|
371
380
|
return base_id
|
|
372
381
|
|
|
373
|
-
def init_widget_dynamic(
|
|
382
|
+
def init_widget_dynamic(
|
|
383
|
+
self, form, form_field, field_name, view, request, default_create_data=None
|
|
384
|
+
):
|
|
374
385
|
super().init_widget_dynamic(form, form_field, field_name, view, request)
|
|
375
386
|
if self.initialised:
|
|
376
387
|
return
|
|
@@ -378,6 +389,7 @@ class SBAdminAutocompleteWidget(
|
|
|
378
389
|
self.field_name = field_name
|
|
379
390
|
self.view = view
|
|
380
391
|
self.form = form
|
|
392
|
+
self.default_create_data = default_create_data or {}
|
|
381
393
|
self.init_autocomplete_widget_static(
|
|
382
394
|
self.field_name,
|
|
383
395
|
self.model,
|
|
@@ -416,20 +428,50 @@ class SBAdminAutocompleteWidget(
|
|
|
416
428
|
parsed_value = None
|
|
417
429
|
if value:
|
|
418
430
|
parsed_value = self.parse_value_from_input(threadsafe_request, value)
|
|
431
|
+
is_create = self.parse_is_create_from_input(
|
|
432
|
+
threadsafe_request,
|
|
433
|
+
threadsafe_request.request_data.request_post.get(name),
|
|
434
|
+
)
|
|
435
|
+
selected_options = []
|
|
436
|
+
if is_create:
|
|
437
|
+
errors = getattr(self.form, "errors", {})
|
|
438
|
+
if errors.get(self.field_name):
|
|
439
|
+
parsed_value = None
|
|
419
440
|
if parsed_value:
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
{
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
441
|
+
if self.is_multiselect() and not isinstance(parsed_value, list):
|
|
442
|
+
parsed_value = [parsed_value]
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
for item in self.get_queryset(threadsafe_request).filter(
|
|
446
|
+
**{f"{self.get_value_field()}{query_suffix}": parsed_value}
|
|
447
|
+
):
|
|
448
|
+
selected_options.append(
|
|
449
|
+
{
|
|
450
|
+
"value": self.get_value(threadsafe_request, item),
|
|
451
|
+
"label": self.get_label(threadsafe_request, item),
|
|
452
|
+
}
|
|
453
|
+
)
|
|
454
|
+
except ValueError as e:
|
|
455
|
+
new_object_id = threadsafe_request.request_data.additional_data.get(
|
|
456
|
+
self.REQUEST_CREATED_DATA_KEY, {}
|
|
457
|
+
).get(self.field_name)
|
|
458
|
+
if new_object_id:
|
|
459
|
+
selected_options.append(
|
|
460
|
+
{
|
|
461
|
+
"value": new_object_id,
|
|
462
|
+
"label": value,
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
elif hasattr(self.form, "add_error"):
|
|
466
|
+
self.form.add_error(
|
|
467
|
+
self.field_name,
|
|
468
|
+
_(
|
|
469
|
+
"The new value was created but became unselected due to another validation error. Please select it again."
|
|
470
|
+
),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
context["widget"]["value"] = json.dumps(selected_options)
|
|
474
|
+
context["widget"]["value_list"] = selected_options
|
|
433
475
|
|
|
434
476
|
if (
|
|
435
477
|
threadsafe_request.request_data.configuration.autocomplete_show_related_buttons(
|
|
@@ -441,6 +483,7 @@ class SBAdminAutocompleteWidget(
|
|
|
441
483
|
and not self.is_multiselect()
|
|
442
484
|
):
|
|
443
485
|
self.add_related_buttons_urls(parsed_value, threadsafe_request, context)
|
|
486
|
+
context["reload_on_save"] = self.reload_on_save
|
|
444
487
|
|
|
445
488
|
return context
|
|
446
489
|
|
|
@@ -472,13 +515,165 @@ class SBAdminAutocompleteWidget(
|
|
|
472
515
|
model_field = getattr(self.field, "model_field", None)
|
|
473
516
|
return not (model_field and (model_field.one_to_one or model_field.many_to_one))
|
|
474
517
|
|
|
518
|
+
def _is_in_validation_context(self):
|
|
519
|
+
"""
|
|
520
|
+
Check if value_from_datadict is being called during form validation
|
|
521
|
+
(full_clean, _clean_fields, etc.) vs. during change detection by formsets.
|
|
522
|
+
|
|
523
|
+
Returns True if called during actual validation, False if called during
|
|
524
|
+
change detection or other non-validation contexts.
|
|
525
|
+
|
|
526
|
+
Uses sys._getframe() instead of inspect.currentframe() for better performance,
|
|
527
|
+
as this method is called frequently during form processing.
|
|
528
|
+
"""
|
|
529
|
+
# Get the call stack - using sys._getframe() for better performance
|
|
530
|
+
# sys._getframe(1) gets the caller's frame (skipping this method)
|
|
531
|
+
try:
|
|
532
|
+
current_frame = sys._getframe(1)
|
|
533
|
+
except ValueError:
|
|
534
|
+
# Fallback if _getframe is not available (unlikely in CPython)
|
|
535
|
+
return False
|
|
536
|
+
|
|
537
|
+
# Look for validation-related methods in the call stack
|
|
538
|
+
validation_methods = {
|
|
539
|
+
"_clean_bound_field",
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
# Walk up the call stack
|
|
543
|
+
depth = 0
|
|
544
|
+
while current_frame and depth < 5: # Limit depth to avoid infinite loops
|
|
545
|
+
method_name = current_frame.f_code.co_name
|
|
546
|
+
if method_name in validation_methods:
|
|
547
|
+
return True
|
|
548
|
+
current_frame = current_frame.f_back
|
|
549
|
+
depth += 1
|
|
550
|
+
|
|
551
|
+
return False
|
|
552
|
+
|
|
553
|
+
def get_forward_data(self, request, name):
|
|
554
|
+
"""
|
|
555
|
+
Parse forward data from request.request_data.request_post.
|
|
556
|
+
|
|
557
|
+
For each field in self.forward, use name as base field name and replace
|
|
558
|
+
in it current field name with forward field name, return dict.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
request: The request object
|
|
562
|
+
name: The base field name (e.g., "product__category")
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
dict: Forward data with keys being forward field names and values
|
|
566
|
+
from request data
|
|
567
|
+
"""
|
|
568
|
+
forward_data = {}
|
|
569
|
+
if not getattr(self, "forward", None):
|
|
570
|
+
return forward_data
|
|
571
|
+
|
|
572
|
+
post_data = getattr(request.request_data, "request_post", {})
|
|
573
|
+
if not post_data:
|
|
574
|
+
return forward_data
|
|
575
|
+
|
|
576
|
+
# For each field in self.forward list
|
|
577
|
+
for forward_field in self.forward:
|
|
578
|
+
# Replace only from end of name, separated by last -
|
|
579
|
+
# Example: if name="prefix-field_name", self.field_name="field_name",
|
|
580
|
+
# forward_field="parent" -> result="prefix-parent"
|
|
581
|
+
name_parts = name.split("-")
|
|
582
|
+
|
|
583
|
+
# Replace only if the last part matches self.field_name
|
|
584
|
+
if name_parts and name_parts[-1] == self.field_name:
|
|
585
|
+
# Replace the last part with forward_field and join back
|
|
586
|
+
name_parts[-1] = forward_field
|
|
587
|
+
forward_field_name = "-".join(name_parts)
|
|
588
|
+
else:
|
|
589
|
+
# If last part doesn't match, don't create forward field name
|
|
590
|
+
continue
|
|
591
|
+
|
|
592
|
+
# Get value from post_data if it exists
|
|
593
|
+
if forward_field_name in post_data:
|
|
594
|
+
forward_data[forward_field] = post_data.get(forward_field_name)
|
|
595
|
+
|
|
596
|
+
return forward_data
|
|
597
|
+
|
|
475
598
|
def value_from_datadict(self, data, files, name):
|
|
476
599
|
input_value = super().value_from_datadict(data, files, name)
|
|
477
600
|
threadsafe_request = SBAdminThreadLocalService.get_request()
|
|
478
601
|
parsed_value = self.parse_value_from_input(threadsafe_request, input_value)
|
|
479
602
|
if parsed_value is None:
|
|
480
603
|
return parsed_value
|
|
481
|
-
|
|
604
|
+
|
|
605
|
+
if not self.is_multiselect():
|
|
606
|
+
parsed_value = next(iter(parsed_value), None)
|
|
607
|
+
|
|
608
|
+
# Only perform validation during actual form cleaning, not during change detection
|
|
609
|
+
# by inline formsets or during HTML rendering
|
|
610
|
+
is_in_validation = self._is_in_validation_context()
|
|
611
|
+
if is_in_validation:
|
|
612
|
+
try:
|
|
613
|
+
has_changed = self.form_field.has_changed(
|
|
614
|
+
self.form.initial.get(self.field_name, None), parsed_value
|
|
615
|
+
)
|
|
616
|
+
except AttributeError:
|
|
617
|
+
has_changed = False
|
|
618
|
+
if has_changed:
|
|
619
|
+
parsed_is_create = self.parse_is_create_from_input(
|
|
620
|
+
threadsafe_request, input_value
|
|
621
|
+
)
|
|
622
|
+
if not self.is_multiselect():
|
|
623
|
+
parsed_is_create = next(iter(parsed_is_create), None)
|
|
624
|
+
base_qs = self.get_queryset(threadsafe_request)
|
|
625
|
+
forward_data = self.get_forward_data(threadsafe_request, name)
|
|
626
|
+
qs = self.filter_search_queryset(
|
|
627
|
+
threadsafe_request,
|
|
628
|
+
base_qs,
|
|
629
|
+
forward_data=forward_data,
|
|
630
|
+
)
|
|
631
|
+
self.form_field.queryset = qs
|
|
632
|
+
parsed_value = self.validate(
|
|
633
|
+
parsed_value, qs, threadsafe_request, parsed_is_create
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
return parsed_value
|
|
637
|
+
|
|
638
|
+
def should_create_new_obj(self):
|
|
639
|
+
return self.allow_add and self.create_value_field
|
|
640
|
+
|
|
641
|
+
def create_new_obj(self, value, queryset, is_create):
|
|
642
|
+
if isinstance(value, list):
|
|
643
|
+
# TODO: multiselect creation
|
|
644
|
+
return self.form_field.to_python(value)
|
|
645
|
+
else:
|
|
646
|
+
data_to_create = {
|
|
647
|
+
self.create_value_field: value,
|
|
648
|
+
**self.default_create_data,
|
|
649
|
+
}
|
|
650
|
+
new_obj = queryset.model.objects.create(**data_to_create)
|
|
651
|
+
try:
|
|
652
|
+
return self.form_field.to_python(new_obj.id)
|
|
653
|
+
except ValidationError:
|
|
654
|
+
new_obj.delete()
|
|
655
|
+
raise ValidationError(
|
|
656
|
+
self.form_field.error_messages["invalid_choice"],
|
|
657
|
+
code="invalid_choice",
|
|
658
|
+
params={"value": value},
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
def validate(self, value, queryset, request, is_create=False):
|
|
662
|
+
is_create_value = (
|
|
663
|
+
True in is_create if isinstance(is_create, list) else is_create
|
|
664
|
+
)
|
|
665
|
+
if is_create_value and self.should_create_new_obj():
|
|
666
|
+
new_object = self.create_new_obj(value, queryset, is_create)
|
|
667
|
+
request.request_data.additional_data[self.REQUEST_CREATED_DATA_KEY] = (
|
|
668
|
+
request.request_data.additional_data.get(
|
|
669
|
+
self.REQUEST_CREATED_DATA_KEY, {}
|
|
670
|
+
)
|
|
671
|
+
)
|
|
672
|
+
request.request_data.additional_data[self.REQUEST_CREATED_DATA_KEY][
|
|
673
|
+
self.field_name
|
|
674
|
+
] = new_object.pk
|
|
675
|
+
return new_object
|
|
676
|
+
return self.form_field.to_python(value)
|
|
482
677
|
|
|
483
678
|
@classmethod
|
|
484
679
|
def apply_to_model_field(cls, model_field):
|
|
@@ -99,7 +99,7 @@ class SBAdminRoleConfiguration(metaclass=Singleton):
|
|
|
99
99
|
|
|
100
100
|
for name, view in plugin_pool.plugins.items():
|
|
101
101
|
if hasattr(view, "get_id"):
|
|
102
|
-
view_instance = view()
|
|
102
|
+
view_instance = view(view.model, sb_admin_site)
|
|
103
103
|
self.view_map[view_instance.get_id()] = view_instance
|
|
104
104
|
view_instance.init_view_static(
|
|
105
105
|
self, view_instance.model, sb_admin_site
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
from datetime import datetime, timedelta
|
|
3
3
|
|
|
4
4
|
from django.core.exceptions import ImproperlyConfigured
|
|
5
|
+
from django.contrib.postgres.fields import ArrayField
|
|
5
6
|
from django.db.models import Q, fields, FilteredRelation, Count
|
|
6
7
|
from django.http import JsonResponse
|
|
7
8
|
from django.utils import timezone
|
|
@@ -45,6 +46,22 @@ class AutocompleteParseMixin:
|
|
|
45
46
|
value = input_value
|
|
46
47
|
return value
|
|
47
48
|
|
|
49
|
+
def parse_is_create_from_input(self, request, input_value):
|
|
50
|
+
try:
|
|
51
|
+
input_value = json.loads(input_value)
|
|
52
|
+
except:
|
|
53
|
+
pass
|
|
54
|
+
if isinstance(input_value, list):
|
|
55
|
+
value = []
|
|
56
|
+
for data in input_value:
|
|
57
|
+
if type(data) is dict:
|
|
58
|
+
value.append(data.get("create", False))
|
|
59
|
+
else:
|
|
60
|
+
value.append(False)
|
|
61
|
+
else:
|
|
62
|
+
value = False
|
|
63
|
+
return value
|
|
64
|
+
|
|
48
65
|
|
|
49
66
|
class SBAdminFilterWidget(JSONSerializableMixin):
|
|
50
67
|
template_name = None
|
|
@@ -212,6 +229,8 @@ class ChoiceFilterWidget(SBAdminFilterWidget):
|
|
|
212
229
|
return found_label[0] if found_label else default_value
|
|
213
230
|
|
|
214
231
|
def get_base_filter_query_for_parsed_value(self, request, filter_value):
|
|
232
|
+
if isinstance(self.model_field, ArrayField):
|
|
233
|
+
return Q(**{f"{self.field.filter_field}__contains": [filter_value]})
|
|
215
234
|
return Q(**{self.field.filter_field: filter_value})
|
|
216
235
|
|
|
217
236
|
|
|
@@ -248,6 +267,11 @@ class MultipleChoiceFilterWidget(AutocompleteParseMixin, ChoiceFilterWidget):
|
|
|
248
267
|
self.select_all_label = select_all_label
|
|
249
268
|
|
|
250
269
|
def get_base_filter_query_for_parsed_value(self, request, filter_value):
|
|
270
|
+
if isinstance(self.model_field, ArrayField):
|
|
271
|
+
q_objects = Q()
|
|
272
|
+
for value in filter_value:
|
|
273
|
+
q_objects |= Q(**{f"{self.field.filter_field}__contains": [value]})
|
|
274
|
+
return q_objects
|
|
251
275
|
return Q(**{f"{self.field.filter_field}__in": filter_value})
|
|
252
276
|
|
|
253
277
|
def get_advanced_filter_operators(self):
|
|
@@ -471,6 +495,7 @@ class AutocompleteFilterWidget(
|
|
|
471
495
|
allow_add = False
|
|
472
496
|
hide_clear_button = False
|
|
473
497
|
search_query_lambda = None
|
|
498
|
+
create_value_field = None
|
|
474
499
|
|
|
475
500
|
def get_field_name(self):
|
|
476
501
|
return self.field.name
|
|
@@ -493,13 +518,16 @@ class AutocompleteFilterWidget(
|
|
|
493
518
|
allow_add=None,
|
|
494
519
|
hide_clear_button=None,
|
|
495
520
|
search_query_lambda=None,
|
|
521
|
+
create_value_field=None,
|
|
496
522
|
**kwargs,
|
|
497
523
|
) -> None:
|
|
498
524
|
super().__init__(template_name, default_value, **kwargs)
|
|
499
525
|
self.model = model or self.model
|
|
500
526
|
self.value_field = value_field or self.value_field
|
|
501
527
|
self.filter_query_lambda = filter_query_lambda or self.filter_query_lambda
|
|
528
|
+
# filters queryset to search in
|
|
502
529
|
self.filter_search_lambda = filter_search_lambda or self.filter_search_lambda
|
|
530
|
+
# defines fields to search on
|
|
503
531
|
self.search_query_lambda = search_query_lambda or self.search_query_lambda
|
|
504
532
|
self.label_lambda = label_lambda or self.label_lambda
|
|
505
533
|
self.value_lambda = value_lambda or self.value_lambda
|
|
@@ -507,6 +535,7 @@ class AutocompleteFilterWidget(
|
|
|
507
535
|
self.multiselect = self.multiselect if self.multiselect is not None else True
|
|
508
536
|
self.forward = forward or self.forward
|
|
509
537
|
self.allow_add = allow_add or self.allow_add
|
|
538
|
+
self.create_value_field = create_value_field or self.create_value_field
|
|
510
539
|
self.hide_clear_button = (
|
|
511
540
|
hide_clear_button
|
|
512
541
|
if hide_clear_button is not None
|
|
@@ -615,8 +644,9 @@ class AutocompleteFilterWidget(
|
|
|
615
644
|
def get_value_field(self):
|
|
616
645
|
return self.value_field or self.model._meta.pk.name
|
|
617
646
|
|
|
618
|
-
def filter_search_queryset(self, request, qs, search_term, forward_data):
|
|
647
|
+
def filter_search_queryset(self, request, qs, search_term="", forward_data=None):
|
|
619
648
|
if self.filter_search_lambda:
|
|
649
|
+
forward_data = forward_data or {}
|
|
620
650
|
qs = qs.filter(
|
|
621
651
|
self.filter_search_lambda(request, search_term, forward_data)
|
|
622
652
|
)
|
|
@@ -628,9 +658,16 @@ class AutocompleteFilterWidget(
|
|
|
628
658
|
page_num = int(post_data.get(AUTOCOMPLETE_PAGE_NUM, 1))
|
|
629
659
|
from_item = (page_num - 1) * AUTOCOMPLETE_PAGE_SIZE
|
|
630
660
|
to_item = (page_num) * AUTOCOMPLETE_PAGE_SIZE
|
|
661
|
+
|
|
662
|
+
# filter queryset
|
|
663
|
+
# base restricted queryset
|
|
631
664
|
qs = self.get_queryset(request)
|
|
665
|
+
# filters queryset to search in, uses filter_search_lambda
|
|
632
666
|
qs = self.filter_search_queryset(request, qs, search_term, forward_data)
|
|
667
|
+
|
|
668
|
+
# search in queryset
|
|
633
669
|
if self.search_query_lambda:
|
|
670
|
+
# defines fields to search on
|
|
634
671
|
qs = self.search_query_lambda(
|
|
635
672
|
request,
|
|
636
673
|
qs,
|
|
@@ -639,6 +676,7 @@ class AutocompleteFilterWidget(
|
|
|
639
676
|
SBAdminTranslationsService.get_main_lang_code(),
|
|
640
677
|
)
|
|
641
678
|
else:
|
|
679
|
+
# defines default fields to search on - all char fields
|
|
642
680
|
qs = self.get_default_search_query(
|
|
643
681
|
request,
|
|
644
682
|
qs,
|
|
@@ -20,6 +20,7 @@ class SBAdminViewRequestData(object):
|
|
|
20
20
|
configuration = None
|
|
21
21
|
selected_view = None
|
|
22
22
|
session = None
|
|
23
|
+
additional_data = None
|
|
23
24
|
|
|
24
25
|
def __init__(
|
|
25
26
|
self,
|
|
@@ -47,6 +48,7 @@ class SBAdminViewRequestData(object):
|
|
|
47
48
|
self.request_method = request_method
|
|
48
49
|
self.global_filter = global_filter or {}
|
|
49
50
|
self.session = session or {}
|
|
51
|
+
self.additional_data = {}
|
|
50
52
|
|
|
51
53
|
def refresh_selected_view(self, request):
|
|
52
54
|
self.configuration = SBAdminConfigurationService.get_configuration(self)
|
|
@@ -141,7 +141,7 @@ class SBAdminViewService(object):
|
|
|
141
141
|
)
|
|
142
142
|
|
|
143
143
|
@classmethod
|
|
144
|
-
def has_permission(cls, request, view, model=None, obj=None, permission=None):
|
|
144
|
+
def has_permission(cls, request, view=None, model=None, obj=None, permission=None):
|
|
145
145
|
return request.request_data.configuration.has_permission(
|
|
146
146
|
request, request.request_data, view, model, obj, permission
|
|
147
147
|
)
|
|
@@ -154,8 +154,9 @@ class SBAdminViewService(object):
|
|
|
154
154
|
request_data,
|
|
155
155
|
global_filter=True,
|
|
156
156
|
global_filter_data_map=None,
|
|
157
|
+
qs=None,
|
|
157
158
|
):
|
|
158
|
-
qs = model.objects.all()
|
|
159
|
+
qs = qs or model.objects.all()
|
|
159
160
|
if global_filter:
|
|
160
161
|
qs = cls.apply_global_filter_to_queryset(
|
|
161
162
|
qs, request, request_data, global_filter_data_map
|