mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15: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:
		| @@ -1,13 +1,16 @@ | ||||
| """InvenTree API version information.""" | ||||
|  | ||||
| # 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.""" | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     - Adds "attach_to_model" field to the ReporTemplate model | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| """Provides a JSON API for common components.""" | ||||
|  | ||||
| import json | ||||
| from typing import Type | ||||
|  | ||||
| from django.conf import settings | ||||
| 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 drf_spectacular.utils import OpenApiResponse, extend_schema | ||||
| from error_report.models import Error | ||||
| from pint._typing import UnitLike | ||||
| from rest_framework import permissions, serializers | ||||
| from rest_framework.exceptions import NotAcceptable, NotFound, PermissionDenied | ||||
| from rest_framework.permissions import IsAdminUser | ||||
| @@ -27,6 +29,7 @@ from rest_framework.views import APIView | ||||
|  | ||||
| import common.models | ||||
| import common.serializers | ||||
| import InvenTree.conversion | ||||
| from common.icons import get_icon_packs | ||||
| from common.settings import get_global_setting | ||||
| from generic.states.api import urlpattern as generic_states_api_urls | ||||
| @@ -533,6 +536,36 @@ class CustomUnitDetail(RetrieveUpdateDestroyAPI): | ||||
|     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): | ||||
|     """List view for server error messages.""" | ||||
|  | ||||
| @@ -900,6 +933,7 @@ common_api_urls = [ | ||||
|                     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'), | ||||
|         ]), | ||||
|     ), | ||||
|   | ||||
| @@ -382,6 +382,22 @@ class CustomUnitSerializer(DataImportExportSerializerMixin, InvenTreeModelSerial | ||||
|         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): | ||||
|     """DRF serializer for server error messages.""" | ||||
|  | ||||
|   | ||||
| @@ -1495,6 +1495,14 @@ class CustomUnitAPITest(InvenTreeAPITestCase): | ||||
|         for name in invalid_name_values: | ||||
|             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): | ||||
|     """Unit tests for the ContentType API.""" | ||||
|   | ||||
| @@ -31,6 +31,7 @@ export enum ApiEndpoints { | ||||
|   // Generic API endpoints | ||||
|   currency_list = 'currency/exchange/', | ||||
|   currency_refresh = 'currency/refresh/', | ||||
|   all_units = 'units/all/', | ||||
|   task_overview = 'background-task/', | ||||
|   task_pending_list = 'background-task/pending/', | ||||
|   task_scheduled_list = 'background-task/scheduled/', | ||||
|   | ||||
| @@ -52,6 +52,8 @@ const CurrencyManagmentPanel = Loadable( | ||||
|   lazy(() => import('./CurrencyManagmentPanel')) | ||||
| ); | ||||
|  | ||||
| const UnitManagmentPanel = Loadable(lazy(() => import('./UnitManagmentPanel'))); | ||||
|  | ||||
| const PluginManagementPanel = Loadable( | ||||
|   lazy(() => import('./PluginManagementPanel')) | ||||
| ); | ||||
| @@ -76,10 +78,6 @@ const CustomStateTable = Loadable( | ||||
|   lazy(() => import('../../../../tables/settings/CustomStateTable')) | ||||
| ); | ||||
|  | ||||
| const CustomUnitsTable = Loadable( | ||||
|   lazy(() => import('../../../../tables/settings/CustomUnitsTable')) | ||||
| ); | ||||
|  | ||||
| const PartParameterTemplateTable = Loadable( | ||||
|   lazy(() => import('../../../../tables/part/PartParameterTemplateTable')) | ||||
| ); | ||||
| @@ -149,7 +147,7 @@ export default function AdminCenter() { | ||||
|         name: 'customunits', | ||||
|         label: t`Custom Units`, | ||||
|         icon: <IconScale />, | ||||
|         content: <CustomUnitsTable /> | ||||
|         content: <UnitManagmentPanel /> | ||||
|       }, | ||||
|       { | ||||
|         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 | ||||
|   useEffect(() => { | ||||
|     tableState.setRecords(data ?? []); | ||||
|  | ||||
|     // set pagesize to length if pagination is disabled | ||||
|     if (!tableProps.enablePagination) { | ||||
|       tableState.setPageSize(data?.length ?? defaultPageSize); | ||||
|     } | ||||
|   }, [data]); | ||||
|  | ||||
|   const deleteRecords = useDeleteApiFormModal({ | ||||
| @@ -594,6 +599,15 @@ export function InvenTreeTable<T = any>({ | ||||
|     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 ( | ||||
|     <> | ||||
|       {deleteRecords.modal} | ||||
| @@ -739,8 +753,7 @@ export function InvenTreeTable<T = any>({ | ||||
|                   overflow: 'hidden' | ||||
|                 }) | ||||
|               }} | ||||
|               recordsPerPageOptions={PAGE_SIZES} | ||||
|               onRecordsPerPageChange={updatePageSize} | ||||
|               {...optionalParams} | ||||
|             /> | ||||
|           </Box> | ||||
|         </Stack> | ||||
|   | ||||
| @@ -71,6 +71,7 @@ export const test = baseTest.extend({ | ||||
|         url != 'http://localhost:8000/api/user/token/' && | ||||
|         url != 'http://localhost:8000/api/barcode/' && | ||||
|         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/notifications/') && | ||||
|         !url.startsWith('chrome://') && | ||||
|   | ||||
		Reference in New Issue
	
	Block a user