|
10 | 10 | import uuid |
11 | 11 | from collections import defaultdict |
12 | 12 | from urllib.parse import unquote, urlencode, urlparse |
| 13 | +from urllib.request import Request |
13 | 14 |
|
14 | 15 | from django.conf import settings |
15 | 16 | from django.contrib.postgres.aggregates import ArrayAgg |
|
64 | 65 | from core.utils import extract_attachments, filter_descendants |
65 | 66 |
|
66 | 67 | from . import permissions, serializers, utils |
67 | | -from .filters import DocumentFilter, ListDocumentFilter, UserSearchFilter |
| 68 | +from .filters import DocumentFilter, ListDocumentFilter, SubDocumentFilter, UserSearchFilter |
68 | 69 | from .throttling import ( |
69 | 70 | DocumentThrottle, |
70 | 71 | UserListThrottleBurst, |
@@ -459,21 +460,21 @@ def list(self, request, *args, **kwargs): |
459 | 460 | It performs early filtering on model fields, annotates user roles, and removes |
460 | 461 | descendant documents to keep only the highest ancestors readable by the current user. |
461 | 462 | """ |
462 | | - user = self.request.user |
| 463 | + user = request.user |
463 | 464 |
|
464 | 465 | # Not calling filter_queryset. We do our own cooking. |
465 | 466 | queryset = self.get_queryset() |
466 | 467 |
|
467 | 468 | filterset = ListDocumentFilter( |
468 | | - self.request.GET, queryset=queryset, request=self.request |
| 469 | + request.GET, queryset=queryset, request=request |
469 | 470 | ) |
470 | 471 | if not filterset.is_valid(): |
471 | 472 | raise drf.exceptions.ValidationError(filterset.errors) |
472 | 473 | filter_data = filterset.form.cleaned_data |
473 | 474 |
|
474 | 475 | # Filter as early as possible on fields that are available on the model |
475 | | - field = "is_creator_me" |
476 | | - queryset = filterset.filters[field].filter(queryset, filter_data[field]) |
| 476 | + for field in ["is_creator_me", "title"]: |
| 477 | + queryset = filterset.filters[field].filter(queryset, filter_data[field]) |
477 | 478 |
|
478 | 479 | queryset = queryset.annotate_user_roles(user) |
479 | 480 |
|
@@ -956,27 +957,6 @@ def all(self, request, *args, **kwargs): |
956 | 957 |
|
957 | 958 | return self.get_response_for_queryset(queryset) |
958 | 959 |
|
959 | | - @drf.decorators.action( |
960 | | - detail=True, |
961 | | - methods=["get"], |
962 | | - ordering=["path"], |
963 | | - ) |
964 | | - def descendants(self, request, *args, **kwargs): |
965 | | - """Handle listing descendants of a document""" |
966 | | - # TODO: remove ? might be dead code |
967 | | - document = self.get_object() |
968 | | - |
969 | | - queryset = document.get_descendants().filter(ancestors_deleted_at__isnull=True) |
970 | | - queryset = self.filter_queryset(queryset) |
971 | | - |
972 | | - filterset = DocumentFilter(request.GET, queryset=queryset) |
973 | | - if not filterset.is_valid(): |
974 | | - raise drf.exceptions.ValidationError(filterset.errors) |
975 | | - |
976 | | - queryset = filterset.qs |
977 | | - |
978 | | - return self.get_response_for_queryset(queryset) |
979 | | - |
980 | 960 | @drf.decorators.action( |
981 | 961 | detail=True, |
982 | 962 | methods=["get"], |
@@ -1184,25 +1164,30 @@ def duplicate(self, request, *args, **kwargs): |
1184 | 1164 | {"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED |
1185 | 1165 | ) |
1186 | 1166 |
|
1187 | | - def _search_simple(self, request, text): |
| 1167 | + |
| 1168 | + @drf.decorators.action(detail=False, methods=["get"], url_path="search") |
| 1169 | + @method_decorator(refresh_oidc_access_token) |
| 1170 | + def search(self, request, *args, **kwargs): |
1188 | 1171 | """ |
1189 | | - Returns a queryset filtered by the content of the document title |
| 1172 | + Returns a DRF response containing the filtered, annotated and ordered document list. |
| 1173 | +
|
| 1174 | + Applies filtering based on request parameter 'q' from `SearchDocumentSerializer`. |
| 1175 | + Depending of the configuration it can be: |
| 1176 | + - A fulltext search through the opensearch indexation app "find" if the backend is |
| 1177 | + enabled (see SEARCH_INDEXER_CLASS) |
| 1178 | + - A filtering by the model field 'title'. |
| 1179 | +
|
| 1180 | + The ordering is always by the most recent first. |
1190 | 1181 | """ |
1191 | | - # As the 'list' view we get a prefiltered queryset (deleted docs are excluded) |
1192 | | - queryset = models.Document.objects.all() |
1193 | | - filterset = DocumentFilter({"title": text}, queryset=queryset) |
1194 | | - # TODO: make sure parent in included when searching in sub-docs |
1195 | | - if not filterset.is_valid(): |
1196 | | - raise drf.exceptions.ValidationError(filterset.errors) |
| 1182 | + params = serializers.SearchDocumentSerializer(data=request.query_params) |
| 1183 | + params.is_valid(raise_exception=True) |
1197 | 1184 |
|
1198 | | - queryset = filterset.filter_queryset(queryset) |
| 1185 | + indexer = get_document_indexer() |
| 1186 | + if indexer: |
| 1187 | + return self._search_with_indexer(indexer, request, params=params) |
1199 | 1188 |
|
1200 | | - return self.get_response_for_queryset( |
1201 | | - queryset.order_by("-updated_at"), |
1202 | | - context={ |
1203 | | - "request": request, |
1204 | | - }, |
1205 | | - ) |
| 1189 | + # The indexer is not configured, we fallback on title search |
| 1190 | + return self.title_search(request, params.validated_data, *args, **kwargs) |
1206 | 1191 |
|
1207 | 1192 | @staticmethod |
1208 | 1193 | def _search_with_indexer(indexer, request, params): |
@@ -1231,30 +1216,28 @@ def _search_with_indexer(indexer, request, params): |
1231 | 1216 | } |
1232 | 1217 | ) |
1233 | 1218 |
|
1234 | | - @drf.decorators.action(detail=False, methods=["get"], url_path="search") |
1235 | | - @method_decorator(refresh_oidc_access_token) |
1236 | | - def search(self, request, *args, **kwargs): |
1237 | | - """ |
1238 | | - Returns a DRF response containing the filtered, annotated and ordered document list. |
1239 | | -
|
1240 | | - Applies filtering based on request parameter 'q' from `SearchDocumentSerializer`. |
1241 | | - Depending of the configuration it can be: |
1242 | | - - A fulltext search through the opensearch indexation app "find" if the backend is |
1243 | | - enabled (see SEARCH_INDEXER_CLASS) |
1244 | | - - A filtering by the model field 'title'. |
| 1219 | + def title_search(self, request, validated_data, *args, **kwargs): |
| 1220 | + request.GET = request.GET.copy() |
| 1221 | + request.GET['title'] = validated_data['q'] |
| 1222 | + if not "path" in validated_data or not validated_data["path"]: |
| 1223 | + return self.list(request, *args, **kwargs) |
| 1224 | + else: |
| 1225 | + return self._list_sub_docs(request) |
1245 | 1226 |
|
1246 | | - The ordering is always by the most recent first. |
| 1227 | + def _list_sub_docs(self, request): |
1247 | 1228 | """ |
1248 | | - params = serializers.SearchDocumentSerializer(data=request.query_params) |
1249 | | - params.is_valid(raise_exception=True) |
| 1229 | + List all documents whose path starts with the provided path parameter. |
| 1230 | + Used internally by the search endpoint when path filtering is requested. |
| 1231 | + """ |
| 1232 | + queryset = self.get_queryset() |
1250 | 1233 |
|
1251 | | - indexer = get_document_indexer() |
1252 | | - if indexer: |
1253 | | - return self._search_with_indexer(indexer, request, params=params) |
| 1234 | + filterset = SubDocumentFilter(request.GET, queryset=queryset) |
| 1235 | + if not filterset.is_valid(): |
| 1236 | + raise drf.exceptions.ValidationError(filterset.errors) |
| 1237 | + |
| 1238 | + queryset = filterset.qs |
| 1239 | + return self.get_response_for_queryset(queryset) |
1254 | 1240 |
|
1255 | | - # The indexer is not configured, we fallback on a simple icontains filter by the |
1256 | | - # model field 'title'. |
1257 | | - return self._search_simple(request, text=params.validated_data["q"]) |
1258 | 1241 |
|
1259 | 1242 | @drf.decorators.action(detail=True, methods=["get"], url_path="versions") |
1260 | 1243 | def versions_list(self, request, *args, **kwargs): |
|
0 commit comments