From 73180ef06c9a776f640ddef146d66f413d8311cd Mon Sep 17 00:00:00 2001 From: Andrew Yager Date: Sun, 8 Mar 2026 20:12:28 +1100 Subject: [PATCH 1/3] fix: use F() reference for SearchRank to avoid tsvector re-parse SearchRank("search_vector", ...) was generating ts_rank(to_tsvector(COALESCE(search_vector::text, '')), ...) which defeats the GIN index. F("search_vector") generates the correct ts_rank("assets_asset"."search_vector", ...). Co-Authored-By: Claude Opus 4.6 --- src/assets/services/search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/services/search.py b/src/assets/services/search.py index c4def0b..af34631 100644 --- a/src/assets/services/search.py +++ b/src/assets/services/search.py @@ -9,7 +9,7 @@ """ from django.db import connection -from django.db.models import Case, Q, Value, When +from django.db.models import Case, F, Q, Value, When from django.db.models.fields import FloatField MAX_SEARCH_WORDS = 20 @@ -48,7 +48,7 @@ def _build_fts_search(queryset, words, search_text, icontains_q): return ( queryset.annotate( - fts_rank=SearchRank("search_vector", search_query), + fts_rank=SearchRank(F("search_vector"), search_query), barcode_boost=barcode_exact, ) .filter(combined_filter) From 1696c144387c8cde4da28bbdfd85d7a5324780ce Mon Sep 17 00:00:00 2001 From: Andrew Yager Date: Sun, 8 Mar 2026 20:22:40 +1100 Subject: [PATCH 2/3] fix: include category name in asset list and export text search Asset list, export, and print-all-filtered views were calling build_asset_search with include_category=False (default), so searching for a category name like "religious" only matched tags, not assets belonging to that category. Co-Authored-By: Claude Opus 4.6 --- src/assets/views.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/assets/views.py b/src/assets/views.py index 32d2331..bfaf0c3 100644 --- a/src/assets/views.py +++ b/src/assets/views.py @@ -319,7 +319,9 @@ def asset_list(request): q = request.GET.get("q", "").strip()[:200] if q: - queryset = build_asset_search(queryset, q, include_nfc=True) + queryset = build_asset_search( + queryset, q, include_nfc=True, include_category=True + ) # Filters department = request.GET.get("department") @@ -4188,7 +4190,9 @@ def export_assets(request): q = request.GET.get("q", "").strip()[:200] if q: - queryset = build_asset_search(queryset, q, include_nfc=False) + queryset = build_asset_search( + queryset, q, include_nfc=False, include_category=True + ) buffer = export_assets_xlsx(queryset) @@ -4546,7 +4550,9 @@ def print_all_filtered_labels(request): q = request.GET.get("q", "") if q: - queryset = build_asset_search(queryset, q, include_nfc=True) + queryset = build_asset_search( + queryset, q, include_nfc=True, include_category=True + ) department = request.GET.get("department") if department: From 9024614627eb403d597b4bbd85fafafd47d01c24 Mon Sep 17 00:00:00 2001 From: Andrew Yager Date: Sun, 8 Mar 2026 20:29:32 +1100 Subject: [PATCH 3/3] fix: sanitise search input in print labels view, add category search tests Address Copilot review: apply .strip()[:200] to q parameter in print_all_filtered_labels for consistency with other views. Add view-level tests for text search by category name in asset list and export views. Co-Authored-By: Claude Opus 4.6 --- src/assets/tests/test_views.py | 56 ++++++++++++++++++++++++++++++++++ src/assets/views.py | 2 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/assets/tests/test_views.py b/src/assets/tests/test_views.py index 0839e15..30af9c6 100644 --- a/src/assets/tests/test_views.py +++ b/src/assets/tests/test_views.py @@ -5821,6 +5821,34 @@ def test_asset_list_filters_by_category( assert asset.name in content assert other_asset.name not in content + def test_text_search_by_category_name( + self, client_logged_in, category, location, user, department + ): + """Searching for a category name returns assets in that category.""" + from assets.models import Asset, Category + + cat = Category.objects.create(name="Religious", department=department) + matching = Asset.objects.create( + name="Plain Chalice", + category=cat, + current_location=location, + status="active", + created_by=user, + ) + unmatched = Asset.objects.create( + name="Wooden Table", + category=category, + current_location=location, + status="active", + created_by=user, + ) + url = reverse("assets:asset_list") + response = client_logged_in.get(url, {"q": "Religious"}) + assert response.status_code == 200 + content = response.content.decode() + assert matching.name in content + assert unmatched.name not in content + # ============================================================ # V213 (S2.6.1-04): Search by location @@ -7743,6 +7771,34 @@ def test_export_multi_word_search_excludes_partial_match( ] assert "Red Bonnet" not in names + def test_export_search_by_category_name( + self, admin_client, location, user, department + ): + """Export with category name query includes matching assets.""" + from assets.models import Category + + cat = Category.objects.create(name="Religious", department=department) + matching = AssetFactory( + name="Plain Chalice", + category=cat, + current_location=location, + status="active", + created_by=user, + ) + url = reverse("assets:export_assets") + resp = admin_client.get(url, {"q": "Religious"}) + assert resp.status_code == 200 + from io import BytesIO + + import openpyxl + + wb = openpyxl.load_workbook(BytesIO(resp.content)) + ws = wb["Assets"] + names = [ + row[0].value for row in ws.iter_rows(min_row=2) if row[0].value + ] + assert matching.name in names + # ============================================================ # PR #61 REGRESSION: Query truncation to 200 characters diff --git a/src/assets/views.py b/src/assets/views.py index bfaf0c3..929ed1b 100644 --- a/src/assets/views.py +++ b/src/assets/views.py @@ -4548,7 +4548,7 @@ def print_all_filtered_labels(request): if status: queryset = queryset.filter(status=status) - q = request.GET.get("q", "") + q = request.GET.get("q", "").strip()[:200] if q: queryset = build_asset_search( queryset, q, include_nfc=True, include_category=True