mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-09 15:10:54 +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
|
||||
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
|
||||
|
||||
|
@ -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]
|
||||
|
||||
|
||||
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."""
|
||||
|
@ -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 (
|
||||
<FileInput
|
||||
{...reducedDefinition}
|
||||
aria-label={`file-field-${fieldName}`}
|
||||
id={fieldId}
|
||||
ref={field.ref}
|
||||
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 });
|
||||
|
||||
if (url) {
|
||||
await navigate(page, url, { baseUrl: options?.baseUrl });
|
||||
await navigate(page, url, {
|
||||
baseUrl: options?.baseUrl,
|
||||
waitUntil: 'networkidle'
|
||||
});
|
||||
}
|
||||
|
||||
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