mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 03:56:43 +00:00
[PUI] Add view of all defined units to Admin center (#8040)
* Add API endpoint for all defined units Fixes #7858 * render all units in API * bump API * Add display for all units * remove logging * fix types * ignore favicon errors * fix for new pint version * add tests * prove against units that are not defined * append trailing slash to url * make pagination disableable again
This commit is contained in:
parent
c5e3ea537d
commit
83d2624a45
@ -1,13 +1,16 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 251
|
INVENTREE_API_VERSION = 252
|
||||||
|
|
||||||
"""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 = """
|
||||||
|
|
||||||
|
v252 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8035
|
||||||
|
- Add endpoint for listing all known units
|
||||||
|
|
||||||
v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
|
v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
|
||||||
- Adds "attach_to_model" field to the ReporTemplate model
|
- Adds "attach_to_model" field to the ReporTemplate model
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Provides a JSON API for common components."""
|
"""Provides a JSON API for common components."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@ -19,6 +20,7 @@ from django_q.tasks import async_task
|
|||||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from error_report.models import Error
|
from error_report.models import Error
|
||||||
|
from pint._typing import UnitLike
|
||||||
from rest_framework import permissions, serializers
|
from rest_framework import permissions, serializers
|
||||||
from rest_framework.exceptions import NotAcceptable, NotFound, PermissionDenied
|
from rest_framework.exceptions import NotAcceptable, NotFound, PermissionDenied
|
||||||
from rest_framework.permissions import IsAdminUser
|
from rest_framework.permissions import IsAdminUser
|
||||||
@ -27,6 +29,7 @@ from rest_framework.views import APIView
|
|||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
import common.serializers
|
import common.serializers
|
||||||
|
import InvenTree.conversion
|
||||||
from common.icons import get_icon_packs
|
from common.icons import get_icon_packs
|
||||||
from common.settings import get_global_setting
|
from common.settings import get_global_setting
|
||||||
from generic.states.api import urlpattern as generic_states_api_urls
|
from generic.states.api import urlpattern as generic_states_api_urls
|
||||||
@ -533,6 +536,36 @@ class CustomUnitDetail(RetrieveUpdateDestroyAPI):
|
|||||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||||
|
|
||||||
|
|
||||||
|
class AllUnitList(ListAPI):
|
||||||
|
"""List of all defined units."""
|
||||||
|
|
||||||
|
serializer_class = common.serializers.AllUnitListResponseSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""Return a list of all available units."""
|
||||||
|
reg = InvenTree.conversion.get_unit_registry()
|
||||||
|
all_units = {k: self.get_unit(reg, k) for k in reg}
|
||||||
|
data = {
|
||||||
|
'default_system': reg.default_system,
|
||||||
|
'available_systems': dir(reg.sys),
|
||||||
|
'available_units': {k: v for k, v in all_units.items() if v},
|
||||||
|
}
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
def get_unit(self, reg, k):
|
||||||
|
"""Parse a unit from the registry."""
|
||||||
|
if not hasattr(reg, k):
|
||||||
|
return None
|
||||||
|
unit: Type[UnitLike] = getattr(reg, k)
|
||||||
|
return {
|
||||||
|
'name': k,
|
||||||
|
'is_alias': reg.get_name(k) == k,
|
||||||
|
'compatible_units': [str(a) for a in unit.compatible_units()],
|
||||||
|
'isdimensionless': unit.dimensionless,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ErrorMessageList(BulkDeleteMixin, ListAPI):
|
class ErrorMessageList(BulkDeleteMixin, ListAPI):
|
||||||
"""List view for server error messages."""
|
"""List view for server error messages."""
|
||||||
|
|
||||||
@ -900,6 +933,7 @@ common_api_urls = [
|
|||||||
path('', CustomUnitDetail.as_view(), name='api-custom-unit-detail')
|
path('', CustomUnitDetail.as_view(), name='api-custom-unit-detail')
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
path('all/', AllUnitList.as_view(), name='api-all-unit-list'),
|
||||||
path('', CustomUnitList.as_view(), name='api-custom-unit-list'),
|
path('', CustomUnitList.as_view(), name='api-custom-unit-list'),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
@ -382,6 +382,22 @@ class CustomUnitSerializer(DataImportExportSerializerMixin, InvenTreeModelSerial
|
|||||||
fields = ['pk', 'name', 'symbol', 'definition']
|
fields = ['pk', 'name', 'symbol', 'definition']
|
||||||
|
|
||||||
|
|
||||||
|
class AllUnitListResponseSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for the AllUnitList."""
|
||||||
|
|
||||||
|
class Unit(serializers.Serializer):
|
||||||
|
"""Serializer for the AllUnitListResponseSerializer."""
|
||||||
|
|
||||||
|
name = serializers.CharField()
|
||||||
|
is_alias = serializers.BooleanField()
|
||||||
|
compatible_units = serializers.ListField(child=serializers.CharField())
|
||||||
|
isdimensionless = serializers.BooleanField()
|
||||||
|
|
||||||
|
default_system = serializers.CharField()
|
||||||
|
available_systems = serializers.ListField(child=serializers.CharField())
|
||||||
|
available_units = Unit(many=True)
|
||||||
|
|
||||||
|
|
||||||
class ErrorMessageSerializer(InvenTreeModelSerializer):
|
class ErrorMessageSerializer(InvenTreeModelSerializer):
|
||||||
"""DRF serializer for server error messages."""
|
"""DRF serializer for server error messages."""
|
||||||
|
|
||||||
|
@ -1495,6 +1495,14 @@ class CustomUnitAPITest(InvenTreeAPITestCase):
|
|||||||
for name in invalid_name_values:
|
for name in invalid_name_values:
|
||||||
self.patch(url, {'name': name}, expected_code=400)
|
self.patch(url, {'name': name}, expected_code=400)
|
||||||
|
|
||||||
|
def test_api(self):
|
||||||
|
"""Test the CustomUnit API."""
|
||||||
|
response = self.get(reverse('api-all-unit-list'))
|
||||||
|
self.assertIn('default_system', response.data)
|
||||||
|
self.assertIn('available_systems', response.data)
|
||||||
|
self.assertIn('available_units', response.data)
|
||||||
|
self.assertEqual(len(response.data['available_units']) > 100, True)
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeAPITest(InvenTreeAPITestCase):
|
class ContentTypeAPITest(InvenTreeAPITestCase):
|
||||||
"""Unit tests for the ContentType API."""
|
"""Unit tests for the ContentType API."""
|
||||||
|
@ -31,6 +31,7 @@ export enum ApiEndpoints {
|
|||||||
// Generic API endpoints
|
// Generic API endpoints
|
||||||
currency_list = 'currency/exchange/',
|
currency_list = 'currency/exchange/',
|
||||||
currency_refresh = 'currency/refresh/',
|
currency_refresh = 'currency/refresh/',
|
||||||
|
all_units = 'units/all/',
|
||||||
task_overview = 'background-task/',
|
task_overview = 'background-task/',
|
||||||
task_pending_list = 'background-task/pending/',
|
task_pending_list = 'background-task/pending/',
|
||||||
task_scheduled_list = 'background-task/scheduled/',
|
task_scheduled_list = 'background-task/scheduled/',
|
||||||
|
@ -52,6 +52,8 @@ const CurrencyManagmentPanel = Loadable(
|
|||||||
lazy(() => import('./CurrencyManagmentPanel'))
|
lazy(() => import('./CurrencyManagmentPanel'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const UnitManagmentPanel = Loadable(lazy(() => import('./UnitManagmentPanel')));
|
||||||
|
|
||||||
const PluginManagementPanel = Loadable(
|
const PluginManagementPanel = Loadable(
|
||||||
lazy(() => import('./PluginManagementPanel'))
|
lazy(() => import('./PluginManagementPanel'))
|
||||||
);
|
);
|
||||||
@ -76,10 +78,6 @@ const CustomStateTable = Loadable(
|
|||||||
lazy(() => import('../../../../tables/settings/CustomStateTable'))
|
lazy(() => import('../../../../tables/settings/CustomStateTable'))
|
||||||
);
|
);
|
||||||
|
|
||||||
const CustomUnitsTable = Loadable(
|
|
||||||
lazy(() => import('../../../../tables/settings/CustomUnitsTable'))
|
|
||||||
);
|
|
||||||
|
|
||||||
const PartParameterTemplateTable = Loadable(
|
const PartParameterTemplateTable = Loadable(
|
||||||
lazy(() => import('../../../../tables/part/PartParameterTemplateTable'))
|
lazy(() => import('../../../../tables/part/PartParameterTemplateTable'))
|
||||||
);
|
);
|
||||||
@ -149,7 +147,7 @@ export default function AdminCenter() {
|
|||||||
name: 'customunits',
|
name: 'customunits',
|
||||||
label: t`Custom Units`,
|
label: t`Custom Units`,
|
||||||
icon: <IconScale />,
|
icon: <IconScale />,
|
||||||
content: <CustomUnitsTable />
|
content: <UnitManagmentPanel />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'part-parameters',
|
name: 'part-parameters',
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Accordion, Stack } from '@mantine/core';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { StylishText } from '../../../../components/items/StylishText';
|
||||||
|
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
|
||||||
|
import { useTable } from '../../../../hooks/UseTable';
|
||||||
|
import { apiUrl } from '../../../../states/ApiState';
|
||||||
|
import { BooleanColumn } from '../../../../tables/ColumnRenderers';
|
||||||
|
import { InvenTreeTable } from '../../../../tables/InvenTreeTable';
|
||||||
|
import CustomUnitsTable from '../../../../tables/settings/CustomUnitsTable';
|
||||||
|
|
||||||
|
function AllUnitTable() {
|
||||||
|
const table = useTable('all-units');
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessor: 'name',
|
||||||
|
title: t`Name`
|
||||||
|
},
|
||||||
|
BooleanColumn({ accessor: 'is_alias', title: t`Alias` }),
|
||||||
|
BooleanColumn({ accessor: 'isdimensionless', title: t`Dimensionless` })
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InvenTreeTable
|
||||||
|
url={apiUrl(ApiEndpoints.all_units)}
|
||||||
|
tableState={table}
|
||||||
|
columns={columns}
|
||||||
|
props={{
|
||||||
|
idAccessor: 'name',
|
||||||
|
enableSearch: false,
|
||||||
|
enablePagination: false,
|
||||||
|
enableColumnSwitching: false,
|
||||||
|
dataFormatter: (data: any) => {
|
||||||
|
let units = data.available_units ?? {};
|
||||||
|
return Object.entries(units).map(([key, values]: [string, any]) => {
|
||||||
|
return {
|
||||||
|
name: values.name,
|
||||||
|
is_alias: values.is_alias,
|
||||||
|
compatible_units: values.compatible_units,
|
||||||
|
isdimensionless: values.isdimensionless
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UnitManagmentPanel() {
|
||||||
|
return (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Accordion defaultValue="custom">
|
||||||
|
<Accordion.Item value="custom" key="custom">
|
||||||
|
<Accordion.Control>
|
||||||
|
<StylishText size="lg">{t`Custom Units`}</StylishText>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<CustomUnitsTable />
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
<Accordion.Item value="all" key="all">
|
||||||
|
<Accordion.Control>
|
||||||
|
<StylishText size="lg">{t`All units`}</StylishText>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<AllUnitTable />
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
@ -517,6 +517,11 @@ export function InvenTreeTable<T = any>({
|
|||||||
// Update tableState.records when new data received
|
// Update tableState.records when new data received
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tableState.setRecords(data ?? []);
|
tableState.setRecords(data ?? []);
|
||||||
|
|
||||||
|
// set pagesize to length if pagination is disabled
|
||||||
|
if (!tableProps.enablePagination) {
|
||||||
|
tableState.setPageSize(data?.length ?? defaultPageSize);
|
||||||
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const deleteRecords = useDeleteApiFormModal({
|
const deleteRecords = useDeleteApiFormModal({
|
||||||
@ -594,6 +599,15 @@ export function InvenTreeTable<T = any>({
|
|||||||
tableState.refreshTable();
|
tableState.refreshTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const optionalParams = useMemo(() => {
|
||||||
|
let optionalParamsa: Record<string, any> = {};
|
||||||
|
if (tableProps.enablePagination) {
|
||||||
|
optionalParamsa['recordsPerPageOptions'] = PAGE_SIZES;
|
||||||
|
optionalParamsa['onRecordsPerPageChange'] = updatePageSize;
|
||||||
|
}
|
||||||
|
return optionalParamsa;
|
||||||
|
}, [tableProps.enablePagination]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{deleteRecords.modal}
|
{deleteRecords.modal}
|
||||||
@ -739,8 +753,7 @@ export function InvenTreeTable<T = any>({
|
|||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
recordsPerPageOptions={PAGE_SIZES}
|
{...optionalParams}
|
||||||
onRecordsPerPageChange={updatePageSize}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -71,6 +71,7 @@ export const test = baseTest.extend({
|
|||||||
url != 'http://localhost:8000/api/user/token/' &&
|
url != 'http://localhost:8000/api/user/token/' &&
|
||||||
url != 'http://localhost:8000/api/barcode/' &&
|
url != 'http://localhost:8000/api/barcode/' &&
|
||||||
url != 'https://docs.inventree.org/en/versions.json' &&
|
url != 'https://docs.inventree.org/en/versions.json' &&
|
||||||
|
url != 'http://localhost:5173/favicon.ico' &&
|
||||||
!url.startsWith('http://localhost:8000/api/news/') &&
|
!url.startsWith('http://localhost:8000/api/news/') &&
|
||||||
!url.startsWith('http://localhost:8000/api/notifications/') &&
|
!url.startsWith('http://localhost:8000/api/notifications/') &&
|
||||||
!url.startsWith('chrome://') &&
|
!url.startsWith('chrome://') &&
|
||||||
|
Loading…
x
Reference in New Issue
Block a user