Skip to content

Commit 23499ce

Browse files
committed
fixup! ✨(backend) handle indexer not configured
I fall back on title_search when the indexer is not configured Signed-off-by: charles <charles.englebert@protonmail.com> Signed-off-by: charles <charles.englebert@protonmail.com> Signed-off-by: charles <charles.englebert@protonmail.com> Signed-off-by: charles <charles.englebert@protonmail.com>
1 parent ee7ffcf commit 23499ce

File tree

5 files changed

+64
-29
lines changed

5 files changed

+64
-29
lines changed

src/backend/core/api/filters.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,13 @@ class DocumentFilter(django_filters.FilterSet):
4747
title = AccentInsensitiveCharFilter(
4848
field_name="title", lookup_expr="unaccent__icontains", label=_("Title")
4949
)
50+
q = AccentInsensitiveCharFilter(
51+
field_name="title", lookup_expr="unaccent__icontains", label=_("Search")
52+
)
5053

5154
class Meta:
5255
model = models.Document
53-
fields = ["title"]
56+
fields = ["title", "q"]
5457

5558

5659
class ListDocumentFilter(DocumentFilter):
@@ -70,7 +73,7 @@ class ListDocumentFilter(DocumentFilter):
7073

7174
class Meta:
7275
model = models.Document
73-
fields = ["is_creator_me", "is_favorite", "title"]
76+
fields = ["is_creator_me", "is_favorite", "title", "q"]
7477

7578
# pylint: disable=unused-argument
7679
def filter_is_creator_me(self, queryset, name, value):

src/backend/core/api/viewsets.py

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ def list(self, request, *args, **kwargs):
600600
filter_data = filterset.form.cleaned_data
601601

602602
# Filter as early as possible on fields that are available on the model
603-
for field in ["is_creator_me", "title"]:
603+
for field in ["is_creator_me", "title", "q"]:
604604
queryset = filterset.filters[field].filter(queryset, filter_data[field])
605605

606606
queryset = queryset.annotate_user_roles(user)
@@ -1067,7 +1067,7 @@ def all(self, request, *args, **kwargs):
10671067
filter_data = filterset.form.cleaned_data
10681068

10691069
# Filter as early as possible on fields that are available on the model
1070-
for field in ["is_creator_me", "title"]:
1070+
for field in ["is_creator_me", "title", "q"]:
10711071
queryset = filterset.filters[field].filter(queryset, filter_data[field])
10721072

10731073
queryset = queryset.annotate_user_roles(user)
@@ -1084,6 +1084,30 @@ def all(self, request, *args, **kwargs):
10841084

10851085
return self.get_response_for_queryset(queryset)
10861086

1087+
@drf.decorators.action(
1088+
detail=True,
1089+
methods=["get"],
1090+
ordering=["path"],
1091+
)
1092+
def descendants(self, request, *args, **kwargs):
1093+
"""Deprecated endpoint to list descendants of a document."""
1094+
logger.warning(
1095+
"The 'descendants' endpoint is deprecated and will be removed in a future release. "
1096+
"The search endpoint should be used for all document retrieval use cases."
1097+
)
1098+
document = self.get_object()
1099+
1100+
queryset = document.get_descendants().filter(ancestors_deleted_at__isnull=True)
1101+
queryset = self.filter_queryset(queryset)
1102+
1103+
filterset = DocumentFilter(request.GET, queryset=queryset)
1104+
if not filterset.is_valid():
1105+
raise drf.exceptions.ValidationError(filterset.errors)
1106+
1107+
queryset = filterset.qs
1108+
1109+
return self.get_response_for_queryset(queryset)
1110+
10871111
@drf.decorators.action(
10881112
detail=True,
10891113
methods=["get"],
@@ -1366,8 +1390,8 @@ def search(self, request, *args, **kwargs):
13661390
"""
13671391
Returns an ordered list of documents best matching the search query parameter 'q'.
13681392
1369-
It depends on a search configurable Search Indexer. If no Search Indexer is configured or if it
1370-
is not reachable, the function falls back to a basic title search.
1393+
It depends on a search configurable Search Indexer. If no Search Indexer is configured
1394+
or if it is not reachable, the function falls back to a basic title search.
13711395
"""
13721396
params = serializers.SearchDocumentSerializer(data=request.query_params)
13731397
params.is_valid(raise_exception=True)
@@ -1382,10 +1406,7 @@ def search(self, request, *args, **kwargs):
13821406
except requests.exceptions.RequestException as e:
13831407
logger.error("Error while searching documents with indexer: %s", e)
13841408
# fallback on title search if the indexer is not reached
1385-
return self._title_search(
1386-
request, params.validated_data, *args, **kwargs
1387-
)
1388-
1409+
return self._title_search(request, params.validated_data, *args, **kwargs)
13891410

13901411
@staticmethod
13911412
def _search_with_indexer(indexer, request, params):
@@ -1419,29 +1440,31 @@ def _title_search(self, request, validated_data, *args, **kwargs):
14191440
Fallback search method when no indexer is configured.
14201441
Only searches in the title field of documents.
14211442
"""
1422-
request.GET = request.GET.copy()
1423-
request.GET["title"] = validated_data["q"]
1424-
1425-
if "path" not in validated_data or not validated_data["path"]:
1443+
if not validated_data.get("path"):
14261444
return self.list(request, *args, **kwargs)
14271445

1428-
return self._list_descendants(request)
1446+
return self._list_descendants(request, validated_data)
14291447

1430-
def _list_descendants(self, request):
1448+
def _list_descendants(self, request, validated_data):
14311449
"""
14321450
List all documents whose path starts with the provided path parameter.
14331451
Includes the parent document itself.
14341452
Used internally by the search endpoint when path filtering is requested.
14351453
"""
14361454
# Get parent document without access filtering
1437-
parent_path = request.GET["path"]
1455+
parent_path = validated_data["path"]
14381456
try:
1439-
parent = models.Document.objects.get(path=parent_path)
1457+
parent = models.Document.objects.annotate_user_roles(request.user).get(
1458+
path=parent_path
1459+
)
14401460
except models.Document.DoesNotExist as exc:
14411461
raise drf.exceptions.NotFound("Document not found from path.") from exc
14421462

1443-
# Check object-level permissions using DocumentPermission logic
1444-
self.check_object_permissions(request, parent)
1463+
abilities = parent.get_abilities(request.user)
1464+
if not abilities.get("search"):
1465+
raise drf.exceptions.PermissionDenied(
1466+
"You do not have permission to search within this document."
1467+
)
14451468

14461469
# Get descendants and include the parent, ordered by path
14471470
queryset = (

src/backend/core/tests/documents/test_api_documents_list_filters.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@
1616
pytestmark = pytest.mark.django_db
1717

1818

19-
def test_api_documents_list_filter_and_access_rights():
19+
@pytest.mark.parametrize(
20+
"title_search_field",
21+
# for integration with indexer search we must have
22+
# the same filtering behaviour with "q" and "title" parameters
23+
[
24+
("title"),
25+
("q"),
26+
],
27+
)
28+
def test_api_documents_list_filter_and_access_rights(title_search_field):
2029
"""Filtering on querystring parameters should respect access rights."""
2130
user = factories.UserFactory()
2231
client = APIClient()
@@ -76,7 +85,7 @@ def random_favorited_by():
7685

7786
filters = {
7887
"link_reach": random.choice([None, *models.LinkReachChoices.values]),
79-
"title": random.choice([None, *word_list]),
88+
title_search_field: random.choice([None, *word_list]),
8089
"favorite": random.choice([None, True, False]),
8190
"creator": random.choice([None, user, other_user]),
8291
"ordering": random.choice(

src/backend/core/tests/documents/test_api_documents_search.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def test_api_documents_search_fall_back_on_search_list(mock_list, indexer_settin
8484
response = client.get("/api/v1.0/documents/search/", data={"q": q})
8585

8686
assert mock_list.call_count == 1
87-
assert mock_list.call_args[0][0].GET.get("title") == q
87+
assert mock_list.call_args[0][0].GET.get("q") == q
8888
assert response.json() == mocked_response
8989

9090

@@ -119,7 +119,7 @@ def test_api_documents_search_fallback_on_search_list_sub_docs(
119119
)
120120

121121
assert mock_list_descendants.call_count == 1
122-
assert mock_list_descendants.call_args[0][0].GET.get("title") == q
122+
assert mock_list_descendants.call_args[0][0].GET.get("q") == q
123123
assert mock_list_descendants.call_args[0][0].GET.get("path") == parent.path
124124
assert response.json() == mocked_response
125125

src/backend/core/tests/documents/test_api_documents_search_descendants.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,9 @@ def test_api_documents_descendants_list_anonymous_restricted_or_authenticated(re
286286
"/api/v1.0/documents/search/", data={"q": "child", "path": document.path}
287287
)
288288

289-
assert response.status_code == 401
289+
assert response.status_code == 403
290290
assert response.json() == {
291-
"detail": "Authentication credentials were not provided."
291+
"detail": "You do not have permission to search within this document."
292292
}
293293

294294

@@ -528,7 +528,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_restricted():
528528

529529
assert response.status_code == 403
530530
assert response.json() == {
531-
"detail": "You do not have permission to perform this action."
531+
"detail": "You do not have permission to search within this document."
532532
}
533533

534534

@@ -767,7 +767,7 @@ def test_api_documents_descendants_list_authenticated_related_child():
767767
)
768768
assert response.status_code == 403
769769
assert response.json() == {
770-
"detail": "You do not have permission to perform this action."
770+
"detail": "You do not have permission to search within this document."
771771
}
772772

773773

@@ -795,7 +795,7 @@ def test_api_documents_descendants_list_authenticated_related_team_none(
795795

796796
assert response.status_code == 403
797797
assert response.json() == {
798-
"detail": "You do not have permission to perform this action."
798+
"detail": "You do not have permission to search within this document."
799799
}
800800

801801

0 commit comments

Comments
 (0)