From b67ac5a7d7479097454c71a232c0c70fe67751e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20GOUZI?= Date: Mon, 3 Aug 2026 15:33:48 +0200 Subject: [PATCH] Fix: crash filtering document link custom fields with an unset or unrelated field present (#13518) --- src/documents/filters.py | 6 ++- .../tests/test_api_filter_by_custom_fields.py | 51 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/documents/filters.py b/src/documents/filters.py index d5dac705c..b511d56de 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -725,9 +725,13 @@ class CustomFieldQueryParser: ) # First we look up reverse links from the requested documents. + # Scoped to this specific field (not just any document link field) and + # excluding unset instances, which have a null value_document_ids and + # are equivalent to having no reverse link at all. links = CustomFieldInstance.objects.filter( document_id__in=value, - field__data_type=CustomField.FieldDataType.DOCUMENTLINK, + field=custom_field, + value_document_ids__isnull=False, ) # Check if any of the requested IDs are missing. diff --git a/src/documents/tests/test_api_filter_by_custom_fields.py b/src/documents/tests/test_api_filter_by_custom_fields.py index 396b07b72..0c4f9f37c 100644 --- a/src/documents/tests/test_api_filter_by_custom_fields.py +++ b/src/documents/tests/test_api_filter_by_custom_fields.py @@ -8,6 +8,7 @@ from django.contrib.auth.models import User from rest_framework.test import APITestCase from documents.models import CustomField +from documents.models import CustomFieldInstance from documents.models import Document from documents.models import SavedView from documents.models import SavedViewFilterRule @@ -606,6 +607,56 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase): match_nothing_ok=True, ) + def test_document_link_contains_unset_reverse_link(self) -> None: + # Another edge case: the document in the value list has the same + # document link field attached, but it was never given a value + # (value_document_ids is None). This must be treated the same as + # having no reverse link at all, not raise a TypeError. + unset_document = self.documents[6] + CustomFieldInstance.objects.create( + document=unset_document, + field=self.custom_fields["documentlink_field"], + value_document_ids=None, + ) + self._assert_query_match_predicate( + ["documentlink_field", "contains", [unset_document.id]], + lambda document: ( + "documentlink_field" in document + and document["documentlink_field"] is not None + and set(document["documentlink_field"]) >= {unset_document.id} + ), + match_nothing_ok=True, + ) + + def test_document_link_contains_ignores_unrelated_document_link_field( + self, + ) -> None: + # A document referenced in the value list may have a *different* + # Document Link custom field attached with no value set. This must + # not be pulled into the reverse-link lookup for the field being + # queried, and must not crash. + unrelated_field = CustomField.objects.create( + name="unrelated_documentlink_field", + data_type=CustomField.FieldDataType.DOCUMENTLINK, + ) + # self.documents[0] is reciprocally linked from self.documents[35] + # (documentlink_field=[documents[0].id, documents[1].id, documents[2].id]). + target_document = self.documents[0] + CustomFieldInstance.objects.create( + document=target_document, + field=unrelated_field, + value_document_ids=None, + ) + + self._assert_query_match_predicate( + ["documentlink_field", "contains", [target_document.id]], + lambda document: ( + "documentlink_field" in document + and document["documentlink_field"] is not None + and set(document["documentlink_field"]) >= {target_document.id} + ), + ) + # ==========================================================# # Logical expressions # # ==========================================================#