Skip to content

Commit c741403

Browse files
authored
triv: added caching and parsing enhancements for autocomplete widgets… (#164)
* triv: added caching and parsing enhancements for autocomplete widgets and formset integration adjustments * triv: added `keep_label` support for widgets and inline table rendering adjustments * triv: updated cache key generation to include formset prefix and removed unused parameters * triv: added `hide_label` support for readonly fields and widgets, updated inline table and boolean field rendering
1 parent 658f99e commit c741403

13 files changed

Lines changed: 119 additions & 24 deletions

File tree

src/django_smartbase_admin/admin/admin_base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,11 @@ def full_clean(self):
881881
form.has_changed = lambda: True
882882
return super().full_clean()
883883

884+
def _construct_form(self, i, **kwargs):
885+
form = super()._construct_form(i, **kwargs)
886+
form._sbadmin_formset = self
887+
return form
888+
884889

885890
class SBAdminGenericInlineFormSet(SBAdminInlineFormSetMixin, BaseGenericInlineFormSet):
886891
pass

src/django_smartbase_admin/admin/widgets.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,7 @@ class SBAdminAutocompleteWidget(
726726
reload_on_save = None
727727
full_width = False
728728
REQUEST_CREATED_DATA_KEY = "autocomplete_created_data"
729+
SELECTED_OPTION_CACHE_KEY = "_sbadmin_autocomplete_selected_options"
729730

730731
def __init__(self, form_field=None, *args, **kwargs):
731732
attrs = kwargs.pop("attrs", None)
@@ -764,6 +765,7 @@ def init_widget_dynamic(
764765
self, form, form_field, field_name, view, request, default_create_data=None
765766
):
766767
super().init_widget_dynamic(form, form_field, field_name, view, request)
768+
self.bound_form = form
767769
if self.initialised:
768770
return
769771
self.initialised = True
@@ -781,6 +783,66 @@ def init_widget_dynamic(
781783
def get_field_name(self):
782784
return self.field_name
783785

786+
def get_selected_option_cache_key(self, request):
787+
formset = getattr(getattr(self, "bound_form", None), "_sbadmin_formset", None)
788+
return (
789+
self.get_id(),
790+
getattr(formset, "prefix", None),
791+
# Defensive: value_field changes how submitted values map back to rows.
792+
self.get_value_field(),
793+
)
794+
795+
def get_selected_option_cache(self, request):
796+
cache = getattr(request, self.SELECTED_OPTION_CACHE_KEY, None)
797+
if cache is None:
798+
cache = {}
799+
setattr(request, self.SELECTED_OPTION_CACHE_KEY, cache)
800+
return cache
801+
802+
def get_selected_option_items_from_cache(self, request, parsed_value):
803+
formset = getattr(getattr(self, "bound_form", None), "_sbadmin_formset", None)
804+
if formset is None:
805+
return None
806+
807+
cache = self.get_selected_option_cache(request)
808+
cache_key = self.get_selected_option_cache_key(request)
809+
if cache_key not in cache:
810+
values = []
811+
value_set = set()
812+
for form in formset.forms:
813+
field = form.fields.get(self.field_name)
814+
if field is None or not isinstance(
815+
field.widget, SBAdminAutocompleteWidget
816+
):
817+
continue
818+
raw_value = form[self.field_name].value()
819+
for value in field.widget.parse_value_list_from_input(
820+
request, raw_value
821+
):
822+
value_key = str(value)
823+
if value_key in value_set:
824+
continue
825+
value_set.add(value_key)
826+
values.append(value)
827+
828+
items_by_value = {}
829+
if values:
830+
for item in self.get_queryset(request).filter(
831+
**{f"{self.get_value_field()}__in": values}
832+
):
833+
items_by_value[str(self.get_value(request, item))] = item
834+
cache[cache_key] = items_by_value
835+
836+
items_by_value = cache[cache_key]
837+
parsed_values = (
838+
parsed_value if isinstance(parsed_value, list) else [parsed_value]
839+
)
840+
return [
841+
items_by_value[str(value)]
842+
for value in parsed_values
843+
if str(value) in items_by_value
844+
]
845+
784846
def get_context(self, name, value, attrs):
785847
context = super().get_context(name, value, attrs)
786848
self.input_id = (
@@ -824,9 +886,14 @@ def get_context(self, name, value, attrs):
824886
parsed_value = [parsed_value]
825887

826888
try:
827-
for item in self.get_queryset(threadsafe_request).filter(
828-
**{f"{self.get_value_field()}{query_suffix}": parsed_value}
829-
):
889+
items = self.get_selected_option_items_from_cache(
890+
threadsafe_request, parsed_value
891+
)
892+
if items is None:
893+
items = self.get_queryset(threadsafe_request).filter(
894+
**{f"{self.get_value_field()}{query_suffix}": parsed_value}
895+
)
896+
for item in items:
830897
selected_options.append(
831898
{
832899
"value": self.get_value(threadsafe_request, item),

src/django_smartbase_admin/engine/fake_inline.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ def save_new(self, form, commit=True):
4848
form, self.inline_instance.parent_instance, commit
4949
)
5050

51+
def _construct_form(self, i, **kwargs):
52+
form = super()._construct_form(i, **kwargs)
53+
form._sbadmin_formset = self
54+
return form
55+
5156

5257
class SBAdminFakeInlineMixin:
5358
fk_name = "inline_fake_relationship"

src/django_smartbase_admin/engine/filter_widgets.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
from datetime import datetime, timedelta
33

4+
from django import forms
45
from django.core.exceptions import ImproperlyConfigured
56
from django.contrib.postgres.fields import ArrayField
67
from django.db.models import Q, fields, FilteredRelation, Count
@@ -47,6 +48,16 @@ def parse_value_from_input(self, request, input_value):
4748
value = input_value
4849
return value
4950

51+
def parse_value_list_from_input(self, request, input_value):
52+
parsed_value = self.parse_value_from_input(request, input_value)
53+
if parsed_value in forms.Field.empty_values:
54+
return []
55+
if not isinstance(parsed_value, list):
56+
parsed_value = [parsed_value]
57+
return [
58+
value for value in parsed_value if value not in forms.Field.empty_values
59+
]
60+
5061
def parse_is_create_from_input(self, request, input_value):
5162
try:
5263
input_value = json.loads(input_value)

src/django_smartbase_admin/monkeypatch/admin_readonly_field_monkeypatch.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ class SBAdminReadonlyField(django.contrib.admin.helpers.AdminReadonlyField):
1515
readonly_template = "sb_admin/includes/readonly_field.html"
1616
readonly_boolean_template = "sb_admin/includes/readonly_boolean_field.html"
1717

18-
def _boolean_field_content(self, value):
18+
def _boolean_field_content(self, value, hide_label=False):
1919
return mark_safe(
2020
render_to_string(
2121
template_name=self.readonly_boolean_template,
2222
context={
23-
"field_label": self.field.get("label"),
23+
"field_label": None if hide_label else self.field.get("label"),
2424
"field_name": self.field.get("name"),
2525
"value": value,
2626
},
2727
),
2828
)
2929

30-
def contents(self, request=None):
30+
def contents(self, request=None, hide_label=False):
3131
admin_site = getattr(self.model_admin, "admin_site", None)
3232
if getattr(admin_site, "name", None) != sb_admin_site.name:
3333
return super().contents()
@@ -47,10 +47,11 @@ def contents(self, request=None):
4747
# This isn't elegant but suffices for contrib.auth's
4848
# ReadOnlyPasswordHashWidget.
4949
if getattr(widget, "read_only", False):
50-
return widget.render(field, value)
50+
attrs = {"sbadmin_hide_label": True} if hide_label else None
51+
return widget.render(field, value, attrs=attrs)
5152
if f is None:
5253
if getattr(attr, "boolean", False):
53-
return self._boolean_field_content(value)
54+
return self._boolean_field_content(value, hide_label=hide_label)
5455
else:
5556
if hasattr(value, "__html__"):
5657
result_repr = value
@@ -81,14 +82,14 @@ def contents(self, request=None):
8182
result_repr = self.get_admin_url(f.remote_field, value)
8283
else:
8384
if isinstance(f, models.BooleanField):
84-
return self._boolean_field_content(value)
85+
return self._boolean_field_content(value, hide_label=hide_label)
8586
result_repr = display_for_field(value, f, self.empty_value_display)
8687
result_repr = linebreaksbr(result_repr)
8788
return render_to_string(
8889
template_name=self.readonly_template,
8990
context={
9091
"readonly_content": conditional_escape(result_repr),
91-
"field_label": self.field.get("label"),
92+
"field_label": None if hide_label else self.field.get("label"),
9293
"field_label_suffix": self.form.label_suffix,
9394
},
9495
)

src/django_smartbase_admin/static/sb_admin/src/css/_inlines.css

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@
3535
@apply relative;
3636
@apply min-h-56;
3737
@apply border-b border-dark-200;
38-
label:first-child:not(.input-file-edit-btn) {
39-
@apply hidden;
40-
}
4138
&:not(:first-child) {
4239
@apply border-l;
4340
}

src/django_smartbase_admin/templates/sb_admin/includes/readonly_boolean_field.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
<div class="relative">
44
<input type="checkbox" name="{{ field_name }}" id="id_{{ field_name }}" form="#" class="toggle"{% if value %} checked{% endif %} readonly disabled>
55
<label for="id_{{ field_name }}"></label>
6-
<label for="id_{{ field_name }}">{{ field_label }}</label>
6+
{% if field_label %}
7+
<label for="id_{{ field_name }}">{{ field_label }}</label>
8+
{% endif %}
79
</div>
810
</div>
9-
</div>
11+
</div>

src/django_smartbase_admin/templates/sb_admin/inlines/table_inline.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,10 @@
200200
<td class="djn-td field-{{ field.field.name }}">
201201
{% if field.is_readonly %}
202202
<div class="px-10 py-8">
203-
{% call_method field "contents" request %}
203+
{% call_method field "contents" request True %}
204204
</div>
205205
{% else %}
206-
{{ field.field }}
206+
{% sb_admin_render_inline_table_field field.field %}
207207
{% if field.field.errors %}
208208
{{ field.field.errors.as_ul }}
209209
{% endif %}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %}
1+
{% for name, value in widget.attrs.items %}{% if name != "sbadmin_hide_label" and value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="relative">
22
{% include "sb_admin/widgets/input.html" %}
3-
{% include 'sb_admin/widgets/includes/simple_field_label.html' %}
3+
{% include 'sb_admin/widgets/includes/simple_field_label.html' with keep_label=True %}
44
{% include 'sb_admin/widgets/includes/help_text.html' %}
55
{{ field.errors }}
66
</div>

0 commit comments

Comments
 (0)