mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-10 06:54:15 +00:00
[bug] Data import fix (#9962)
* Permission fix for data importer endpoint * Add playwright tests * Bump API version
This commit is contained in:
@ -1,12 +1,15 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
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
|
v363 -> 2025-07-04 : https://github.com/inventree/InvenTree/pull/9954
|
||||||
- Adds "user_detail" field to the ApiToken serializer
|
- Adds "user_detail" field to the ApiToken serializer
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class DataImporterPermission(permissions.BasePermission):
|
|||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
"""Class level permission checks are handled via InvenTree.permissions.IsAuthenticatedOrReadScope."""
|
"""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):
|
def has_object_permission(self, request, view, obj):
|
||||||
"""Check if the user has permission to access the imported object."""
|
"""Check if the user has permission to access the imported object."""
|
||||||
@ -91,25 +91,25 @@ class DataImporterModelList(APIView):
|
|||||||
return Response(models)
|
return Response(models)
|
||||||
|
|
||||||
|
|
||||||
class DataImportSessionList(DataImporterPermission, BulkDeleteMixin, ListCreateAPI):
|
class DataImportSessionMixin:
|
||||||
"""API endpoint for accessing a list of DataImportSession objects."""
|
"""Mixin class for DataImportSession API views."""
|
||||||
|
|
||||||
queryset = importer.models.DataImportSession.objects.all()
|
queryset = importer.models.DataImportSession.objects.all()
|
||||||
serializer_class = importer.serializers.DataImportSessionSerializer
|
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']
|
ordering_fields = ['timestamp', 'status', 'model_type']
|
||||||
|
|
||||||
|
|
||||||
class DataImportSessionDetail(DataImporterPermission, RetrieveUpdateDestroyAPI):
|
class DataImportSessionDetail(DataImportSessionMixin, RetrieveUpdateDestroyAPI):
|
||||||
"""Detail endpoint for a single DataImportSession object."""
|
"""Detail endpoint for a single DataImportSession object."""
|
||||||
|
|
||||||
queryset = importer.models.DataImportSession.objects.all()
|
|
||||||
serializer_class = importer.serializers.DataImportSessionSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class DataImportSessionAcceptFields(APIView):
|
class DataImportSessionAcceptFields(APIView):
|
||||||
"""API endpoint to accept the field mapping for a DataImportSession."""
|
"""API endpoint to accept the field mapping for a DataImportSession."""
|
||||||
|
@ -179,7 +179,7 @@ export function ApiFormField({
|
|||||||
radius='lg'
|
radius='lg'
|
||||||
size='sm'
|
size='sm'
|
||||||
error={definition.error ?? error?.message}
|
error={definition.error ?? error?.message}
|
||||||
onChange={(event) => onChange(event.currentTarget.checked)}
|
onChange={(event: any) => onChange(event.currentTarget.checked)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'date':
|
case 'date':
|
||||||
@ -217,6 +217,7 @@ export function ApiFormField({
|
|||||||
return (
|
return (
|
||||||
<FileInput
|
<FileInput
|
||||||
{...reducedDefinition}
|
{...reducedDefinition}
|
||||||
|
aria-label={`file-field-${fieldName}`}
|
||||||
id={fieldId}
|
id={fieldId}
|
||||||
ref={field.ref}
|
ref={field.ref}
|
||||||
radius='sm'
|
radius='sm'
|
||||||
|
5
src/frontend/tests/fixtures/bom_data.csv
vendored
Normal file
5
src/frontend/tests/fixtures/bom_data.csv
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Assembly,Component,Reference,Quantity,Overage,Allow Variants,Gets inherited,Optional,Consumable,Note,ID,Pricing min,Pricing max,Pricing min total,Pricing max total,Pricing updated,Component.Ipn,Component.Name,Component.Description,Validated,Available Stock,Available substitute stock,Available variant stock,External stock,On Order,In Production,Can Build
|
||||||
|
87,66,Screws,4.0,,False,False,False,False,,16,0.28,0.648622,1.12,2.594488,2024-08-08 06:55,,M3x8 Torx,"Torx head screw, M3 thread, 8.0mm",True,485.0,0.0,0.0,0.0,0.0,0.0,121.25
|
||||||
|
87,67,Large screw,1.0,,False,False,False,False,,17,0.574802,0.574802,0.574802,0.574802,2024-07-27 05:13,,M3x10 Torx,"Torx head screw, M3 thread, 10.0mm",True,1450.0,0.0,0.0,0.0,0.0,0.0,1450.0
|
||||||
|
87,82,Enclosure,1.0,,False,False,False,False,,15,,,,,2024-07-27 05:08,,1551ABK,"Small plastic enclosure, black",True,165.0,223.0,0.0,0.0,0.0,0.0,388.0
|
||||||
|
87,88,PCBA,1.0,,True,False,False,False,Assembled board,23,80.431083,129.328176,80.431083,129.328176,2024-12-27 23:14,002.01-PCBA,Widget Board (assembled),Assembled PCB for converting electricity into magic smoke,True,55.0,0.0,0.0,0.0,0.0,0.0,55.0
|
|
4
src/frontend/tests/fixtures/po_data.csv
vendored
Normal file
4
src/frontend/tests/fixtures/po_data.csv
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
ID,Supplier Part,Quantity,Reference,Notes,Order,Build Order,Overdue,Received,Purchase price,Currency,Auto Pricing,Destination,Target Date,Total price,Link,SKU,MPN,Internal Part Number,Internal Part,Internal Part Name
|
||||||
|
27,1034,100.0,,,11,,False,100.0,2.5,USD,True,,,250.0,,WE-10AWG-WT,,,897,Silicon Wire 10AWG White
|
||||||
|
28,1033,10.0,,,11,,False,10.0,135.0,USD,True,,,1350.0,,WE-12AWG-BK-1000,,,899,Silicon Wire 12AWG Black
|
||||||
|
29,1028,123.0,,,11,,False,123.0,1.0,USD,True,,,123.0,,WC-12AWG-WT,,,901,Silicon Wire 12AWG White
|
|
@ -114,7 +114,10 @@ export const doCachedLogin = async (
|
|||||||
await page.context().storageState({ path: fn });
|
await page.context().storageState({ path: fn });
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
await navigate(page, url, { baseUrl: options?.baseUrl });
|
await navigate(page, url, {
|
||||||
|
baseUrl: options?.baseUrl,
|
||||||
|
waitUntil: 'networkidle'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
|
79
src/frontend/tests/pui_importing.spec.ts
Normal file
79
src/frontend/tests/pui_importing.spec.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import test from '@playwright/test';
|
||||||
|
import { doCachedLogin } from './login';
|
||||||
|
|
||||||
|
test('Importing - Admin Center', async ({ browser }) => {
|
||||||
|
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();
|
||||||
|
});
|
Reference in New Issue
Block a user