Conversation
Changed Files
|
There was a problem hiding this comment.
Pull request overview
This pull request implements a "Quick Add Capture(s) to Dataset" feature that allows users to add one or multiple captures to a dataset through two workflows: (1) a selection mode in the captures table where users can select multiple captures via checkboxes, and (2) planned support for single-capture quick-add from dropdown menus (though the UI for this is not yet implemented in the templates).
Changes:
- Added backend API endpoints for quick-adding captures to datasets and retrieving eligible datasets
- Implemented frontend selection mode with checkbox-based multi-select in the captures table
- Created a modal interface for selecting the target dataset and confirming the operation
- Added support for automatic multi-channel capture grouping during addition
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| gateway/sds_gateway/users/views.py | Added QuickAddCapturesToDatasetView for adding captures and UserDatasetsForQuickAddView for listing eligible datasets with proper permission checks |
| gateway/sds_gateway/users/urls.py | Registered two new URL patterns for the quick-add API endpoints |
| gateway/sds_gateway/templates/users/partials/quick_add_to_dataset_modal.html | New modal template for dataset selection with accessible form controls |
| gateway/sds_gateway/templates/users/partials/captures_page_table.html | Added selection column with checkboxes (hidden by default, shown in selection mode) |
| gateway/sds_gateway/templates/users/file_list.html | Integrated selection mode buttons and quick-add modal into the file list page |
| gateway/sds_gateway/static/js/file-list.js | Implemented selection mode toggle, checkbox state management, and row click handling |
| gateway/sds_gateway/static/js/components.js | Updated row click handler to ignore clicks on selection checkboxes |
| gateway/sds_gateway/static/js/actions/QuickAddToDatasetManager.js | New manager class handling modal interactions, dataset loading, and API calls for single/multi capture addition |
| gateway/sds_gateway/static/css/file-list.css | Added CSS rules to show/hide selection column based on selection mode state |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| this.showMessage(msg, hasErrors ? "warning" : "success"); | ||
| if (window.showAlert) | ||
| window.showAlert(msg, hasErrors ? "warning" : "success"); | ||
| if (hasSuccess || !hasErrors) { | ||
| setTimeout(() => { | ||
| window.bootstrap?.Modal?.getInstance(this.modalEl)?.hide(); | ||
| }, 1500); | ||
| } else if (this.confirmBtn) { | ||
| this.confirmBtn.disabled = false; | ||
| } |
There was a problem hiding this comment.
After successfully adding captures to a dataset, the modal closes but selection mode remains active with checkboxes still checked. The modal should exit selection mode after a successful add operation by calling exitSelectionMode (or exposing it globally so the manager can access it) to provide a clean user experience.
| <th scope="col" | ||
| class="capture-select-column" | ||
| id="select-header" | ||
| aria-label="Select">Select</th> |
There was a problem hiding this comment.
The captures table includes a "Select" column header with the ID "select-header", but this column header lacks an accessible label explaining its purpose. While the aria-label attribute is present, for better accessibility the visible text "Select" should be retained within the th element, or a screen reader-only text should be provided using Bootstrap's visually-hidden class.
| if (addBtn) { | ||
| addBtn.addEventListener("click", () => { | ||
| const modal = document.getElementById("quickAddToDatasetModal"); | ||
| if (modal) { | ||
| const ids = Array.from(this.tableManager?.selectedCaptureIds ?? []); | ||
| modal.dataset.captureUuids = JSON.stringify(ids); | ||
| const bsModal = bootstrap.Modal.getOrCreateInstance(modal); | ||
| bsModal.show(); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
When clicking the "Add" button with no captures selected, the modal still opens. The add button should be disabled or the code should validate that at least one capture is selected before opening the modal, with an appropriate message displayed if none are selected.
| shared_perms = UserSharePermission.objects.filter( | ||
| shared_with=user, | ||
| item_type=ItemType.DATASET, | ||
| is_deleted=False, | ||
| is_enabled=True, | ||
| permission_level__in=[ | ||
| PermissionLevel.CO_OWNER, | ||
| PermissionLevel.CONTRIBUTOR, | ||
| ], | ||
| ) | ||
| shared_uuids = [p.item_uuid for p in shared_perms] |
There was a problem hiding this comment.
Iterating over shared_perms to extract item_uuid values results in fetching all UserSharePermission objects into Python memory when only UUIDs are needed. This can be optimized by using values_list to extract UUIDs directly in the database query: shared_uuids = list(UserSharePermission.objects.filter(...).values_list('item_uuid', flat=True))
| shared_perms = UserSharePermission.objects.filter( | |
| shared_with=user, | |
| item_type=ItemType.DATASET, | |
| is_deleted=False, | |
| is_enabled=True, | |
| permission_level__in=[ | |
| PermissionLevel.CO_OWNER, | |
| PermissionLevel.CONTRIBUTOR, | |
| ], | |
| ) | |
| shared_uuids = [p.item_uuid for p in shared_perms] | |
| shared_uuids = list( | |
| UserSharePermission.objects.filter( | |
| shared_with=user, | |
| item_type=ItemType.DATASET, | |
| is_deleted=False, | |
| is_enabled=True, | |
| permission_level__in=[ | |
| PermissionLevel.CO_OWNER, | |
| PermissionLevel.CONTRIBUTOR, | |
| ], | |
| ).values_list("item_uuid", flat=True) | |
| ) |
| const text = | ||
| typeof firstErrorMessage === "object" | ||
| ? (firstErrorMessage.message ?? | ||
| firstErrorMessage.detail ?? | ||
| String(firstErrorMessage)) | ||
| : String(firstErrorMessage); |
There was a problem hiding this comment.
The formatQuickAddSummary function treats the first error message as either a string or an object, but the errors array from the API contains strings (per line 1827: errors.append(f"{c.uuid}: {e}")). The complex object handling (firstErrorMessage.message, firstErrorMessage.detail) is unnecessary and may never execute. Either simplify the code to expect strings only, or document why object error messages might occur.
| const text = | |
| typeof firstErrorMessage === "object" | |
| ? (firstErrorMessage.message ?? | |
| firstErrorMessage.detail ?? | |
| String(firstErrorMessage)) | |
| : String(firstErrorMessage); | |
| const text = String(firstErrorMessage); |
| class QuickAddCapturesToDatasetView(Auth0LoginRequiredMixin, View): | ||
| """ | ||
| Quick-add view: add a single capture (and all its channels if multi-channel) | ||
| owned by the user to a dataset the user can modify. | ||
| Expects POST with JSON body: | ||
| {"dataset_uuid": "<uuid>", "capture_uuid": "<uuid>"} | ||
| Returns error if capture_uuid or dataset_uuid is missing/invalid. | ||
| Returns error if user does not have permission to add captures to the dataset. | ||
| On success returns added/skipped UUIDs and errors (skipped = already in dataset). | ||
| """ | ||
|
|
||
| def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> JsonResponse: | ||
| data, error = self._parse_json(request) | ||
| if error: | ||
| return error | ||
| assert data is not None # type narrowing: success path only | ||
|
|
||
| # Validate both UUIDs before any ORM access | ||
| dataset_uuid, capture_uuid, err = self._validate_quick_add_uuids(data) | ||
| if err: | ||
| return err | ||
| assert dataset_uuid is not None | ||
| assert capture_uuid is not None | ||
|
|
||
| # Fetch capture: must be owned by user and not deleted | ||
| capture = Capture.objects.filter( | ||
| uuid=capture_uuid, | ||
| owner=request.user, | ||
| is_deleted=False, | ||
| ).first() | ||
| if not capture: | ||
| return JsonResponse( | ||
| {"error": "Capture not found"}, | ||
| status=status.HTTP_404_NOT_FOUND, | ||
| ) | ||
|
|
||
| # Dataset: must exist, not deleted, private (cannot change public datasets) | ||
| dataset = Dataset.objects.filter( | ||
| uuid=dataset_uuid, | ||
| is_deleted=False, | ||
| is_public=False, | ||
| ).first() | ||
| if not dataset: | ||
| return JsonResponse( | ||
| {"error": "Dataset not found or cannot be modified"}, | ||
| status=status.HTTP_404_NOT_FOUND, | ||
| ) | ||
| # User must have add permission (owner, co-owner, or contributor; not viewer) | ||
| if not UserSharePermission.user_can_add_assets( | ||
| cast("User", request.user), dataset.uuid, ItemType.DATASET | ||
| ): | ||
| return JsonResponse( | ||
| {"error": "You do not have permission to edit this dataset."}, | ||
| status=status.HTTP_403_FORBIDDEN, | ||
| ) | ||
|
|
||
| report = self._add_capture_to_dataset_with_report( | ||
| capture=capture, | ||
| dataset=dataset, | ||
| user=cast("User", request.user), | ||
| ) | ||
| return JsonResponse( | ||
| { | ||
| "success": True, | ||
| "added": [str(u) for u in report["added"]], | ||
| "skipped": [str(u) for u in report["skipped"]], | ||
| "errors": report["errors"], | ||
| }, | ||
| status=status.HTTP_200_OK, | ||
| ) |
There was a problem hiding this comment.
The new QuickAddCapturesToDatasetView and UserDatasetsForQuickAddView endpoints lack test coverage. Based on the comprehensive test patterns in test_views.py and test_drf_views.py, tests should be added to cover scenarios including: valid single/multi-capture additions, permission checks (owner/co-owner/contributor/viewer), invalid UUIDs, non-existent captures/datasets, public dataset rejection, multi-channel capture grouping, and error handling.
There was a problem hiding this comment.
Seconding this. Generate the tests after refactoring so you're not testing functions that end up getting merged or deleted.
| class QuickAddCapturesToDatasetView(Auth0LoginRequiredMixin, View): | ||
| """ | ||
| Quick-add view: add a single capture (and all its channels if multi-channel) | ||
| owned by the user to a dataset the user can modify. | ||
| Expects POST with JSON body: | ||
| {"dataset_uuid": "<uuid>", "capture_uuid": "<uuid>"} | ||
| Returns error if capture_uuid or dataset_uuid is missing/invalid. | ||
| Returns error if user does not have permission to add captures to the dataset. | ||
| On success returns added/skipped UUIDs and errors (skipped = already in dataset). | ||
| """ | ||
|
|
||
| def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> JsonResponse: | ||
| data, error = self._parse_json(request) | ||
| if error: | ||
| return error | ||
| assert data is not None # type narrowing: success path only | ||
|
|
||
| # Validate both UUIDs before any ORM access | ||
| dataset_uuid, capture_uuid, err = self._validate_quick_add_uuids(data) | ||
| if err: | ||
| return err | ||
| assert dataset_uuid is not None | ||
| assert capture_uuid is not None | ||
|
|
||
| # Fetch capture: must be owned by user and not deleted | ||
| capture = Capture.objects.filter( | ||
| uuid=capture_uuid, | ||
| owner=request.user, | ||
| is_deleted=False, | ||
| ).first() | ||
| if not capture: | ||
| return JsonResponse( | ||
| {"error": "Capture not found"}, | ||
| status=status.HTTP_404_NOT_FOUND, | ||
| ) | ||
|
|
||
| # Dataset: must exist, not deleted, private (cannot change public datasets) | ||
| dataset = Dataset.objects.filter( | ||
| uuid=dataset_uuid, | ||
| is_deleted=False, | ||
| is_public=False, | ||
| ).first() | ||
| if not dataset: | ||
| return JsonResponse( | ||
| {"error": "Dataset not found or cannot be modified"}, | ||
| status=status.HTTP_404_NOT_FOUND, | ||
| ) | ||
| # User must have add permission (owner, co-owner, or contributor; not viewer) | ||
| if not UserSharePermission.user_can_add_assets( | ||
| cast("User", request.user), dataset.uuid, ItemType.DATASET | ||
| ): | ||
| return JsonResponse( | ||
| {"error": "You do not have permission to edit this dataset."}, | ||
| status=status.HTTP_403_FORBIDDEN, | ||
| ) | ||
|
|
||
| report = self._add_capture_to_dataset_with_report( | ||
| capture=capture, | ||
| dataset=dataset, | ||
| user=cast("User", request.user), | ||
| ) | ||
| return JsonResponse( | ||
| { | ||
| "success": True, | ||
| "added": [str(u) for u in report["added"]], | ||
| "skipped": [str(u) for u in report["skipped"]], | ||
| "errors": report["errors"], | ||
| }, | ||
| status=status.HTTP_200_OK, | ||
| ) | ||
|
|
||
| def _validate_quick_add_uuids( | ||
| self, data: dict[str, Any] | ||
| ) -> tuple[UUID | None, UUID | None, JsonResponse | None]: | ||
| """ | ||
| Validate dataset_uuid and capture_uuid from request data. | ||
| Returns (dataset_uuid, capture_uuid, None) on success, | ||
| (None, None, error_response) on validation failure. | ||
| """ | ||
| dataset_uuid, err = self._validate_uuid(data.get("dataset_uuid")) | ||
| if err: | ||
| return None, None, err | ||
| capture_uuid, err = self._validate_uuid(data.get("capture_uuid")) | ||
| if err: | ||
| return None, None, err | ||
| return dataset_uuid, capture_uuid, None | ||
|
|
||
| def _add_capture_to_dataset_with_report( | ||
| self, | ||
| capture: "Capture", | ||
| dataset: "Dataset", | ||
| user: "User", | ||
| ) -> dict[str, Any]: | ||
| """ | ||
| Add a single capture (and its multi-channel siblings if applicable) to a | ||
| dataset. Skips captures already in the dataset. Internal use only. | ||
|
|
||
| Returns a dict with keys: "added" (list of UUIDs), "skipped" (list of | ||
| UUIDs), "errors" (list of str). | ||
| """ | ||
| added: list[UUID] = [] | ||
| skipped: list[UUID] = [] | ||
| errors: list[str] = [] | ||
|
|
||
| if capture.is_multi_channel: | ||
| candidates = list( | ||
| Capture.objects.filter( | ||
| top_level_dir=capture.top_level_dir, | ||
| owner=user, | ||
| is_deleted=False, | ||
| ) | ||
| ) | ||
| else: | ||
| candidates = [capture] | ||
|
|
||
| existing_pks = set( | ||
| dataset.captures.filter(pk__in=[c.pk for c in candidates]).values_list( | ||
| "pk", flat=True | ||
| ) | ||
| ) | ||
|
|
||
| for c in candidates: | ||
| if c.pk in existing_pks: | ||
| skipped.append(c.uuid) | ||
| continue | ||
| try: | ||
| dataset.captures.add(c) | ||
| added.append(c.uuid) | ||
| except OperationalError as e: | ||
| errors.append(f"{c.uuid}: {e}") | ||
| except IntegrityError as e: | ||
| errors.append(f"{c.uuid}: {e}") | ||
| except Exception as e: # noqa: BLE001 - catch-all for unexpected errors | ||
| errors.append(f"{c.uuid}: {e}") | ||
|
|
||
| return {"added": added, "skipped": skipped, "errors": errors} | ||
|
|
||
| def _parse_json( | ||
| self, request: HttpRequest | ||
| ) -> tuple[dict[str, Any] | None, JsonResponse | None]: | ||
| """ | ||
| Parse request body as JSON and require it to be an object. | ||
| Returns (data, None) on success, (None, error_response) on error. | ||
| Caller should: data, err = self._parse_json(request); if err: return err | ||
| """ | ||
| if not request.content_type or "application/json" not in request.content_type: | ||
| return None, JsonResponse( | ||
| {"error": "Content-Type must be application/json"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| if not request.body: | ||
| return None, JsonResponse( | ||
| {"error": "Invalid JSON body"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| try: | ||
| data = json.loads(request.body) | ||
| except json.JSONDecodeError: | ||
| return None, JsonResponse( | ||
| {"error": "Invalid JSON body"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| if not isinstance(data, dict): | ||
| return None, JsonResponse( | ||
| {"error": "Request body must be a JSON object"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| return data, None | ||
|
|
||
| def _validate_uuid(self, value: Any) -> tuple[UUID | None, JsonResponse | None]: | ||
| """ | ||
| Validate capture_uuid or dataset_uuid from request data. | ||
| Returns (uuid, None) on success, | ||
| (None, error_response) on validation failure. | ||
| """ | ||
| if value is None: | ||
| return None, JsonResponse( | ||
| {"error": "uuid is required"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| if not isinstance(value, str): | ||
| return None, JsonResponse( | ||
| {"error": "uuid must be a string"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| s = value.strip() | ||
| if not s: | ||
| return None, JsonResponse( | ||
| {"error": "uuid cannot be empty"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| max_length = 64 | ||
| if len(s) > max_length: | ||
| return None, JsonResponse( | ||
| {"error": "Invalid uuid"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| try: | ||
| return UUID(s), None | ||
| except ValueError: | ||
| return None, JsonResponse( | ||
| {"error": "Invalid uuid"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
|
|
||
|
|
||
| quick_add_capture_to_dataset_view = QuickAddCapturesToDatasetView.as_view() |
There was a problem hiding this comment.
The class is named QuickAddCapturesToDatasetView (plural "Captures") but the exported view instance is named quick_add_capture_to_dataset_view (singular "Capture"). While the view can handle both single and multiple captures, the inconsistency is confusing. Consider renaming either the class to use singular form to match the export, or update the export to use plural form.
| async handleAdd() { | ||
| const datasetUuid = this.selectEl?.value; | ||
| if (!datasetUuid) return; | ||
| const isMulti = | ||
| Array.isArray(this.currentCaptureUuids) && | ||
| this.currentCaptureUuids.length > 0; | ||
| const isSingle = this.currentCaptureUuid && this.quickAddUrl; | ||
| if (!isMulti && !isSingle) return; | ||
| if (this.confirmBtn) this.confirmBtn.disabled = true; | ||
| this.resetMessage(); | ||
| if (isMulti) { | ||
| await this.handleMultiAdd(datasetUuid); | ||
| } else { | ||
| await this.handleSingleAdd(datasetUuid); | ||
| } |
There was a problem hiding this comment.
When multi-add mode is triggered with an empty capture list (currentCaptureUuids is an empty array), the code proceeds silently without providing feedback. The handleAdd function should validate that currentCaptureUuids contains at least one capture and show an appropriate error message if empty, similar to how isSingle and isMulti are checked.
| setupSelectionCheckboxHandler() { | ||
| document.addEventListener("change", (e) => { | ||
| if (!e.target.matches(".capture-select-checkbox")) return; | ||
| const uuid = e.target.getAttribute("data-capture-uuid"); | ||
| if (!uuid) return; | ||
| if (e.target.checked) { | ||
| this.selectedCaptureIds.add(uuid); | ||
| } else { | ||
| this.selectedCaptureIds.delete(uuid); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
The setupSelectionCheckboxHandler method attaches a document-level event listener that is never removed, which could lead to memory leaks if FileListCapturesTableManager is instantiated multiple times (e.g., during testing or dynamic page updates). While this may not be a practical issue in production since the table is typically initialized once, consider storing the listener reference and providing a cleanup method for proper resource management.
| table.classList.remove("selection-mode-active"); | ||
| mainBtn.classList.remove("d-none"); | ||
| mainBtn.setAttribute("aria-pressed", "false"); | ||
| if (modeButtonsWrap) modeButtonsWrap.classList.add("d-none"); |
There was a problem hiding this comment.
When exiting selection mode, the selected checkboxes remain checked visually and in the selectedCaptureIds set. The exitSelectionMode function should clear all selections by unchecking all checkboxes and clearing the selectedCaptureIds set to prevent stale selections from persisting when the user re-enters selection mode later.
| if (modeButtonsWrap) modeButtonsWrap.classList.add("d-none"); | |
| if (modeButtonsWrap) modeButtonsWrap.classList.add("d-none"); | |
| // Clear all selection checkboxes in the table | |
| const checkboxes = table.querySelectorAll('input[type="checkbox"]'); | |
| checkboxes.forEach((checkbox) => { | |
| checkbox.checked = false; | |
| checkbox.indeterminate = false; | |
| }); | |
| // Clear the selected capture IDs set/collection, if present | |
| const selectedCaptureIds = this.tableManager?.selectedCaptureIds; | |
| if (selectedCaptureIds) { | |
| if (typeof selectedCaptureIds.clear === "function") { | |
| selectedCaptureIds.clear(); | |
| } else if (Array.isArray(selectedCaptureIds)) { | |
| selectedCaptureIds.length = 0; | |
| } | |
| } |
| def _validate_quick_add_uuids( | ||
| self, data: dict[str, Any] | ||
| ) -> tuple[UUID | None, UUID | None, JsonResponse | None]: | ||
| """ | ||
| Validate dataset_uuid and capture_uuid from request data. | ||
| Returns (dataset_uuid, capture_uuid, None) on success, | ||
| (None, None, error_response) on validation failure. | ||
| """ | ||
| dataset_uuid, err = self._validate_uuid(data.get("dataset_uuid")) | ||
| if err: | ||
| return None, None, err | ||
| capture_uuid, err = self._validate_uuid(data.get("capture_uuid")) | ||
| if err: | ||
| return None, None, err | ||
| return dataset_uuid, capture_uuid, None |
There was a problem hiding this comment.
merge this with _validate_uuid
| # Fetch capture: must be owned by user and not deleted | ||
| capture = Capture.objects.filter( | ||
| uuid=capture_uuid, | ||
| owner=request.user, | ||
| is_deleted=False, | ||
| ).first() | ||
| if not capture: | ||
| return JsonResponse( | ||
| {"error": "Capture not found"}, | ||
| status=status.HTTP_404_NOT_FOUND, | ||
| ) | ||
|
|
||
| # Dataset: must exist, not deleted, private (cannot change public datasets) | ||
| dataset = Dataset.objects.filter( | ||
| uuid=dataset_uuid, | ||
| is_deleted=False, | ||
| is_public=False, | ||
| ).first() | ||
| if not dataset: | ||
| return JsonResponse( | ||
| {"error": "Dataset not found or cannot be modified"}, | ||
| status=status.HTTP_404_NOT_FOUND, | ||
| ) | ||
| # User must have add permission (owner, co-owner, or contributor; not viewer) | ||
| if not UserSharePermission.user_can_add_assets( | ||
| cast("User", request.user), dataset.uuid, ItemType.DATASET | ||
| ): | ||
| return JsonResponse( | ||
| {"error": "You do not have permission to edit this dataset."}, | ||
| status=status.HTTP_403_FORBIDDEN, | ||
| ) | ||
|
|
There was a problem hiding this comment.
That first filter will return a subset of datasets: you're filtering by ownership when co-owners can also edit a dataset. These co-owned datasets will be excluded from that filter.
This whole block can likely be replaced by a call to _validate_dataset_edit_permissions(). If there's undesired behavior in this function, refactor it into something both places can use, because it's better than having code related to permission checks duplicated.
| except Exception as e: # noqa: BLE001 - catch-all for unexpected errors | ||
| errors.append(f"{c.uuid}: {e}") | ||
|
|
||
| return {"added": added, "skipped": skipped, "errors": errors} |
There was a problem hiding this comment.
[nit] dicts lack type validation and may raise KeyErrors at runtime; use a dataclass or namedtuple instead
| def _parse_json( | ||
| self, request: HttpRequest | ||
| ) -> tuple[dict[str, Any] | None, JsonResponse | None]: | ||
| """ | ||
| Parse request body as JSON and require it to be an object. | ||
| Returns (data, None) on success, (None, error_response) on error. | ||
| Caller should: data, err = self._parse_json(request); if err: return err | ||
| """ | ||
| if not request.content_type or "application/json" not in request.content_type: | ||
| return None, JsonResponse( | ||
| {"error": "Content-Type must be application/json"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| if not request.body: | ||
| return None, JsonResponse( | ||
| {"error": "Invalid JSON body"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| try: | ||
| data = json.loads(request.body) | ||
| except json.JSONDecodeError: | ||
| return None, JsonResponse( | ||
| {"error": "Invalid JSON body"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| if not isinstance(data, dict): | ||
| return None, JsonResponse( | ||
| {"error": "Request body must be a JSON object"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
| return data, None |
There was a problem hiding this comment.
May be unnecessary. See if we can let json.loads raise in the parent function instead. Also add the parser to the class, if requiring a JSON on every endpoint of this view:
from rest_framework.parsers import JSONParser
class ExampleView(APIView):
...
parser_classes = [JSONParser]see https://www.django-rest-framework.org/api-guide/parsers/
| ) | ||
| datasets = [ | ||
| {"uuid": str(d["uuid"]), "name": (d["name"] or "Unnamed")} | ||
| for d in list(owned) + list(shared) |
There was a problem hiding this comment.
Conversion to list is not needed. Use itertools.chain to avoid consuming the iterables into memory at once.
No description provided.