From 042039754bd144b02c41150badd69ced4794b805 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 6 Jul 2025 18:22:37 +1000 Subject: [PATCH] [bug] Data import fix (#9962) * Permission fix for data importer endpoint * Add playwright tests * Bump API version --- .../InvenTree/InvenTree/api_version.py | 5 +- src/backend/InvenTree/importer/api.py | 18 ++--- .../components/forms/fields/ApiFormField.tsx | 3 +- src/frontend/tests/fixtures/bom_data.csv | 5 ++ src/frontend/tests/fixtures/po_data.csv | 4 + src/frontend/tests/login.ts | 5 +- src/frontend/tests/pui_importing.spec.ts | 79 +++++++++++++++++++ 7 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 src/frontend/tests/fixtures/bom_data.csv create mode 100644 src/frontend/tests/fixtures/po_data.csv create mode 100644 src/frontend/tests/pui_importing.spec.ts diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 18ee59224a..fd4c051027 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 363 +INVENTREE_API_VERSION = 364 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v364 -> 2025-07-06 : https://github.com/inventree/InvenTree/pull/9962 + - Fix permissions for the DataImportSession API endpoints + v363 -> 2025-07-04 : https://github.com/inventree/InvenTree/pull/9954 - Adds "user_detail" field to the ApiToken serializer diff --git a/src/backend/InvenTree/importer/api.py b/src/backend/InvenTree/importer/api.py index 45bda7e430..cc00b7635c 100644 --- a/src/backend/InvenTree/importer/api.py +++ b/src/backend/InvenTree/importer/api.py @@ -30,7 +30,7 @@ class DataImporterPermission(permissions.BasePermission): def has_permission(self, request, view): """Class level permission checks are handled via InvenTree.permissions.IsAuthenticatedOrReadScope.""" - return True + return request.user and request.user.is_authenticated def has_object_permission(self, request, view, obj): """Check if the user has permission to access the imported object.""" @@ -91,25 +91,25 @@ class DataImporterModelList(APIView): return Response(models) -class DataImportSessionList(DataImporterPermission, BulkDeleteMixin, ListCreateAPI): - """API endpoint for accessing a list of DataImportSession objects.""" +class DataImportSessionMixin: + """Mixin class for DataImportSession API views.""" queryset = importer.models.DataImportSession.objects.all() serializer_class = importer.serializers.DataImportSessionSerializer + permission_classes = [DataImporterPermission] - filter_backends = SEARCH_ORDER_FILTER - filterset_fields = ['model_type', 'status', 'user'] +class DataImportSessionList(BulkDeleteMixin, DataImportSessionMixin, ListCreateAPI): + """API endpoint for accessing a list of DataImportSession objects.""" + filter_backends = SEARCH_ORDER_FILTER + filterset_fields = ['model_type', 'status', 'user'] ordering_fields = ['timestamp', 'status', 'model_type'] -class DataImportSessionDetail(DataImporterPermission, RetrieveUpdateDestroyAPI): +class DataImportSessionDetail(DataImportSessionMixin, RetrieveUpdateDestroyAPI): """Detail endpoint for a single DataImportSession object.""" - queryset = importer.models.DataImportSession.objects.all() - serializer_class = importer.serializers.DataImportSessionSerializer - class DataImportSessionAcceptFields(APIView): """API endpoint to accept the field mapping for a DataImportSession.""" diff --git a/src/frontend/src/components/forms/fields/ApiFormField.tsx b/src/frontend/src/components/forms/fields/ApiFormField.tsx index 98735d1fd8..017e3ca264 100644 --- a/src/frontend/src/components/forms/fields/ApiFormField.tsx +++ b/src/frontend/src/components/forms/fields/ApiFormField.tsx @@ -179,7 +179,7 @@ export function ApiFormField({ radius='lg' size='sm' error={definition.error ?? error?.message} - onChange={(event) => onChange(event.currentTarget.checked)} + onChange={(event: any) => onChange(event.currentTarget.checked)} /> ); case 'date': @@ -217,6 +217,7 @@ export function ApiFormField({ return ( { + const page = await doCachedLogin(browser, { + username: 'steven', + password: 'wizardstaff', + url: 'settings/admin/import' + }); + + await page + .getByRole('button', { name: 'action-button-create-import-' }) + .click(); + + const fileInput = await page.locator('input[type="file"]'); + await fileInput.setInputFiles('./tests/fixtures/bom_data.csv'); + await page.getByRole('button', { name: 'Submit' }).click(); + + // Submitting without selecting model type, should show error + await page.getByText('This field is required.').waitFor(); + await page.getByText('Errors exist for one or more').waitFor(); + + await page + .getByRole('textbox', { name: 'choice-field-model_type' }) + .fill('Cat'); + await page + .getByRole('option', { name: 'Part Category', exact: true }) + .click(); + await page.getByRole('button', { name: 'Submit' }).click(); + + await page.getByText('Description (optional)').waitFor(); + await page.getByText('Parent Category').waitFor(); +}); + +test('Importing - BOM', async ({ browser }) => { + const page = await doCachedLogin(browser, { + username: 'steven', + password: 'wizardstaff', + url: 'part/87/bom' + }); + + await page + .getByRole('button', { name: 'action-button-import-bom-data' }) + .click(); + + // Select BOM file fixture for import + const fileInput = await page.locator('input[type="file"]'); + await fileInput.setInputFiles('./tests/fixtures/bom_data.csv'); + await page.getByRole('button', { name: 'Submit' }).click(); + + await page.getByText('Mapping data columns to database fields').waitFor(); + await page.getByRole('button', { name: 'Accept Column Mapping' }).click(); + + await page.getByText('Processing Data').waitFor(); + await page.getByText('0 / 4').waitFor(); + await page + .getByLabel('Importing DataUpload FileMap') + .getByText('002.01-PCBA | Widget Board') + .waitFor(); +}); + +test('Importing - Purchase Order', async ({ browser }) => { + const page = await doCachedLogin(browser, { + username: 'steven', + password: 'wizardstaff', + url: 'purchasing/purchase-order/15/line-items' + }); + + await page + .getByRole('button', { name: 'action-button-import-line-' }) + .click(); + + const fileInput = await page.locator('input[type="file"]'); + await fileInput.setInputFiles('./tests/fixtures/po_data.csv'); + await page.getByRole('button', { name: 'Submit' }).click(); + + await page.getByRole('cell', { name: 'Database Field' }).waitFor(); + await page.getByRole('cell', { name: 'Field Description' }).waitFor(); +});