{setting ? (
) : (
diff --git a/src/frontend/src/components/tables/Column.tsx b/src/frontend/src/components/tables/Column.tsx
index 460386c2dc9..4917689b996 100644
--- a/src/frontend/src/components/tables/Column.tsx
+++ b/src/frontend/src/components/tables/Column.tsx
@@ -1,14 +1,14 @@
/**
* Interface for the table column definition
*/
-export type TableColumn = {
+export type TableColumn
= {
accessor: string; // The key in the record to access
ordering?: string; // The key in the record to sort by (defaults to accessor)
title: string; // The title of the column
sortable?: boolean; // Whether the column is sortable
switchable?: boolean; // Whether the column is switchable
hidden?: boolean; // Whether the column is hidden
- render?: (record: any) => any; // A custom render function
+ render?: (record: T) => any; // A custom render function
filter?: any; // A custom filter function
filtering?: boolean; // Whether the column is filterable
width?: number; // The width of the column
diff --git a/src/frontend/src/components/tables/InvenTreeTable.tsx b/src/frontend/src/components/tables/InvenTreeTable.tsx
index 51cd0ae6eba..536d9092adf 100644
--- a/src/frontend/src/components/tables/InvenTreeTable.tsx
+++ b/src/frontend/src/components/tables/InvenTreeTable.tsx
@@ -44,7 +44,7 @@ const defaultPageSize: number = 25;
* @param rowActions : (record: any) => RowAction[] - Callback function to generate row actions
* @param onRowClick : (record: any, index: number, event: any) => void - Callback function when a row is clicked
*/
-export type InvenTreeTableProps = {
+export type InvenTreeTableProps = {
params?: any;
defaultSortColumn?: string;
noRecordsText?: string;
@@ -60,9 +60,9 @@ export type InvenTreeTableProps = {
customActionGroups?: any[];
printingActions?: any[];
idAccessor?: string;
- dataFormatter?: (data: any) => any;
- rowActions?: (record: any) => RowAction[];
- onRowClick?: (record: any, index: number, event: any) => void;
+ dataFormatter?: (data: T) => any;
+ rowActions?: (record: T) => RowAction[];
+ onRowClick?: (record: T, index: number, event: any) => void;
};
/**
@@ -90,7 +90,7 @@ const defaultInvenTreeTableProps: InvenTreeTableProps = {
/**
* Table Component which extends DataTable with custom InvenTree functionality
*/
-export function InvenTreeTable({
+export function InvenTreeTable({
url,
tableKey,
columns,
@@ -98,8 +98,8 @@ export function InvenTreeTable({
}: {
url: string;
tableKey: string;
- columns: TableColumn[];
- props: InvenTreeTableProps;
+ columns: TableColumn[];
+ props: InvenTreeTableProps;
}) {
// Use the first part of the table key as the table name
const tableName: string = useMemo(() => {
diff --git a/src/frontend/src/components/tables/machine/MachineListTable.tsx b/src/frontend/src/components/tables/machine/MachineListTable.tsx
index 0989dfbbdb2..9323ab094e3 100644
--- a/src/frontend/src/components/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/components/tables/machine/MachineListTable.tsx
@@ -1,11 +1,58 @@
-import { t } from '@lingui/macro';
-import { Button, Container, Divider, Group, Text } from '@mantine/core';
-import { useCallback, useMemo, useState } from 'react';
+import { Trans, t } from '@lingui/macro';
+import {
+ ActionIcon,
+ Badge,
+ Box,
+ Button,
+ Card,
+ CheckIcon,
+ Code,
+ Container,
+ Divider,
+ Flex,
+ Group,
+ Indicator,
+ List,
+ LoadingOverlay,
+ Space,
+ Stack,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import {
+ IconAlertCircle,
+ IconChevronLeft,
+ IconDots,
+ IconPlus,
+ IconRefresh
+} from '@tabler/icons-react';
+import { useQuery } from '@tanstack/react-query';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { api } from '../../../App';
+import {
+ openCreateApiForm,
+ openDeleteApiForm,
+ openEditApiForm
+} from '../../../functions/forms';
import { notYetImplemented } from '../../../functions/notifications';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
-import { TableStatusRenderer } from '../../renderers/StatusRenderer';
+import { AddItemButton } from '../../buttons/AddItemButton';
+import { ButtonMenu } from '../../buttons/ButtonMenu';
+import { ApiFormProps } from '../../forms/ApiForm';
+import {
+ ActionDropdown,
+ DeleteItemAction,
+ EditItemAction
+} from '../../items/ActionDropdown';
+import { UnavailableIndicator } from '../../items/UnavailableIndicator';
+import { YesNoButton } from '../../items/YesNoButton';
+import {
+ StatusRenderer,
+ TableStatusRenderer
+} from '../../renderers/StatusRenderer';
import { MachineSettingList } from '../../settings/SettingList';
import { TableColumn } from '../Column';
import { BooleanColumn } from '../ColumnRenderers';
@@ -26,24 +73,240 @@ interface MachineI {
is_driver_available: boolean;
}
+interface MachineTypeI {
+ slug: string;
+ name: string;
+ description: string;
+ provider_file: string;
+ provider_plugin: string;
+ is_builtin: boolean;
+}
+
+interface MachineDriverI {
+ slug: string;
+ name: string;
+ description: string;
+ provider_file: string;
+ provider_plugin: string;
+ is_builtin: boolean;
+ machine_type: string;
+}
+
+function MachineStatusIndicator({ machine }: { machine: MachineI }) {
+ const sx = { marginLeft: '4px' };
+
+ // machine is not active, show a gray dot
+ if (!machine.active) {
+ return (
+
+
+
+ );
+ }
+
+ // determine the status color
+ let color = 'green';
+ const hasErrors =
+ machine.machine_errors.length > 0 || !machine.is_driver_available;
+
+ if (hasErrors || machine.status >= 300) color = 'red';
+ else if (machine.status >= 200) color = 'orange';
+
+ // determine if the machine is running
+ const processing =
+ machine.initialized && machine.status > 0 && machine.status < 300;
+
+ return (
+
+
+
+ );
+}
+
function MachineDetail({
- machine,
+ machinePk,
goBack
}: {
- machine: MachineI;
+ machinePk: string;
goBack: () => void;
}) {
+ const {
+ data: machine,
+ refetch,
+ isFetching: isMachineFetching
+ } = useQuery({
+ enabled: true,
+ queryKey: ['machine-detail', machinePk],
+ queryFn: () =>
+ api.get(apiUrl(ApiPaths.machine_list, machinePk)).then((res) => res.data)
+ });
+ const { data: machineTypes, isFetching: isMachineTypesFetching } = useQuery<
+ MachineTypeI[]
+ >({
+ queryKey: ['machine-types'],
+ queryFn: () =>
+ api.get(apiUrl(ApiPaths.machine_types_list)).then((res) => res.data),
+ staleTime: 10 * 1000
+ });
+
+ const isFetching = isMachineFetching || isMachineTypesFetching;
+
+ function InfoItem({
+ name,
+ children
+ }: {
+ name: string;
+ children: React.ReactNode;
+ }) {
+ return (
+
+
+ {name}:
+
+ {children}
+
+ );
+ }
+
return (
-
-
- {machine.name}
-
- Machine Settings
-
-
- Driver Settings
-
-
+
+
+
+
+
+
+
+
+
+ {machine && }
+ {machine?.name}
+
+
+ }
+ actions={[
+ EditItemAction({
+ tooltip: t`Edit machine`,
+ onClick: () => {
+ openEditApiForm({
+ title: t`Edit machine`,
+ url: ApiPaths.machine_list,
+ pk: machinePk,
+ fields: {
+ name: {},
+ active: {}
+ },
+ onClose: () => refetch()
+ });
+ }
+ }),
+ DeleteItemAction({
+ tooltip: t`Delete machine`,
+ onClick: () => {
+ openDeleteApiForm({
+ title: t`Delete machine`,
+ successMessage: t`Machine successfully deleted.`,
+ url: ApiPaths.machine_list,
+ pk: machinePk,
+ preFormContent: (
+ {t`Are you sure you want to remove the machine "${machine?.name}"?`}
+ ),
+ onFormSuccess: () => goBack()
+ });
+ }
+ })
+ ]}
+ />
+
+
+
+
+
+
+ Info
+
+ refetch()}>
+
+
+
+
+
+
+
+ {machine?.machine_type}
+ {machine &&
+ machineTypes &&
+ machineTypes.findIndex(
+ (m) => m.slug === machine.machine_type
+ ) === -1 && }
+
+
+
+
+ {machine?.driver}
+ {!machine?.is_driver_available && }
+
+
+
+
+
+
+
+
+
+
+ {machine?.status === -1 ? (
+ No status
+ ) : (
+ StatusRenderer({
+ status: `${machine?.status || -1}`,
+ type: `MachineStatus__${machine?.status_model}` as any
+ })
+ )}
+ {machine?.status_text}
+
+
+
+
+ Errors:
+
+ {machine && machine?.machine_errors.length > 0 ? (
+
+ {machine?.machine_errors.length}
+
+ ) : (
+
+ No errors reported
+
+ )}
+
+ {machine?.machine_errors.map((error) => (
+
+ {error}
+
+ ))}
+
+
+
+
+
+
+
+ {machine?.is_driver_available && (
+ <>
+
+ Machine Settings
+
+
+
+
+ Driver Settings
+
+
+ >
+ )}
+
);
}
@@ -51,19 +314,31 @@ function MachineDetail({
* Table displaying list of available plugins
*/
export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
+ const { data: machineTypes } = useQuery({
+ queryKey: ['machine-types'],
+ queryFn: () =>
+ api.get(apiUrl(ApiPaths.machine_types_list)).then((res) => res.data),
+ staleTime: 10 * 1000
+ });
+ const { data: machineDrivers } = useQuery({
+ queryKey: ['machine-drivers'],
+ queryFn: () =>
+ api.get(apiUrl(ApiPaths.machine_driver_list)).then((res) => res.data),
+ staleTime: 10 * 1000
+ });
+
const { tableKey, refreshTable } = useTableRefresh('machine');
- const machineTableColumns: TableColumn[] = useMemo(
+ const machineTableColumns = useMemo[]>(
() => [
{
accessor: 'name',
title: t`Machine`,
sortable: true,
- render: function (record: any) {
- // TODO: Add link to machine detail page
- // TODO: Add custom icon
+ render: function (record) {
return (
+
{record.name}
);
@@ -73,13 +348,30 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
accessor: 'machine_type',
title: t`Machine Type`,
sortable: true,
- filtering: true
+ render: (record) => {
+ return (
+
+ {record.machine_type}
+ {machineTypes &&
+ machineTypes.findIndex(
+ (m: any) => m.slug === record.machine_type
+ ) === -1 && }
+
+ );
+ }
},
{
accessor: 'driver',
title: t`Machine Driver`,
sortable: true,
- filtering: true
+ render: (record) => {
+ return (
+
+ {record.driver}
+ {!record.is_driver_available && }
+
+ );
+ }
},
BooleanColumn({
accessor: 'initialized',
@@ -101,44 +393,59 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
return renderer(record);
}
}
- },
- BooleanColumn({
- accessor: 'is_driver_available',
- title: t`Driver available`
- })
+ }
],
- []
+ [machineTypes]
);
- // Determine available actions for a given plugin
- function rowActions(record: any): RowAction[] {
- let actions: RowAction[] = [];
+ const [createFormMachineType, setCreateFormMachineType] = useState<
+ null | string
+ >(null);
+ const createFormDriverOptions = useMemo(() => {
+ if (!machineDrivers) return [];
- if (record.active) {
- actions.push({
- title: t`Deactivate`,
- color: 'red',
- onClick: () => {
- notYetImplemented();
- }
- });
- } else {
- actions.push({
- title: t`Activate`,
- onClick: () => {
- notYetImplemented();
- }
- });
- }
+ return machineDrivers
+ .filter((d) => d.machine_type === createFormMachineType)
+ .map((d) => ({
+ value: d.slug,
+ display_name: `${d.name} (${d.description})`
+ }));
+ }, [machineDrivers, createFormMachineType]);
- return actions;
- }
+ const createMachineForm = useMemo(() => {
+ return {
+ title: t`Create machine`,
+ url: ApiPaths.machine_list,
+ fields: {
+ name: {},
+ machine_type: {
+ field_type: 'choice',
+ choices: machineTypes
+ ? machineTypes.map((t) => ({
+ value: t.slug,
+ display_name: `${t.name} (${t.description})`
+ }))
+ : [],
+ onValueChange: ({ value }) => setCreateFormMachineType(value)
+ },
+ driver: {
+ field_type: 'choice',
+ disabled: !createFormMachineType,
+ choices: createFormDriverOptions
+ },
+ active: {}
+ },
+ onClose: () => {
+ setCreateFormMachineType(null);
+ }
+ };
+ }, [createFormMachineType, createFormDriverOptions, machineTypes]);
- const [machineDetail, setMachineDetail] = useState(null);
- const goBack = useCallback(() => setMachineDetail(null), []);
+ const [currentMachinePk, setCurrentMachinePk] = useState(null);
+ const goBack = useCallback(() => setCurrentMachinePk(null), []);
- if (machineDetail) {
- return ;
+ if (currentMachinePk) {
+ return ;
}
return (
@@ -149,19 +456,43 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
props={{
...props,
enableDownload: false,
- onRowClick: (record) => setMachineDetail(record),
+ onRowClick: (record) => setCurrentMachinePk(record.pk),
+ customActionGroups: [
+ {
+ setCreateFormMachineType(null);
+ openCreateApiForm(createMachineForm);
+ }}
+ />
+ ],
params: {
...props.params
},
- rowActions: rowActions,
customFilters: [
{
name: 'active',
label: t`Active`,
type: 'boolean'
+ },
+ {
+ name: 'machine_type',
+ label: t`Machine Type`,
+ type: 'choice',
+ choiceFunction: () =>
+ machineTypes
+ ? machineTypes.map((t) => ({ value: t.slug, label: t.name }))
+ : []
+ },
+ {
+ name: 'driver',
+ label: t`Machine Driver`,
+ type: 'choice',
+ choiceFunction: () =>
+ machineDrivers
+ ? machineDrivers.map((d) => ({ value: d.slug, label: d.name }))
+ : []
}
- // TODO: add machine_type choices filter
- // TODO: add driver choices filter
]
}}
/>
From 44d277df6393c65a0bc0adabdc8bc56c935dfbc1 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Fri, 26 Jan 2024 15:23:31 +0000
Subject: [PATCH 37/86] Fix merge issues
---
InvenTree/InvenTree/helpers.py | 2 +-
InvenTree/generic/states/api.py | 4 +--
InvenTree/machine/api.py | 16 ++++++------
.../tables/machine/MachineListTable.tsx | 25 ++++++++++---------
src/frontend/src/enums/ApiEndpoints.tsx | 10 +++++++-
.../Index/Settings/AdminCenter/Index.tsx | 13 +++++++++-
.../AdminCenter/MachineManagementPanel.tsx | 11 ++++++++
src/frontend/src/states/SettingsState.tsx | 7 ++++++
8 files changed, 63 insertions(+), 25 deletions(-)
create mode 100644 src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx
diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index 9a9e4281976..2d38efa376d 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -841,7 +841,7 @@ def get_target(self, obj):
return {'name': str(item), 'model': str(model_cls._meta.verbose_name), **ret}
-Inheritors_T = TypeVar("Inheritors_T")
+Inheritors_T = TypeVar('Inheritors_T')
def inheritors(cls: Type[Inheritors_T]) -> Set[Type[Inheritors_T]]:
diff --git a/InvenTree/generic/states/api.py b/InvenTree/generic/states/api.py
index 683bb79ae80..e71261ebb73 100644
--- a/InvenTree/generic/states/api.py
+++ b/InvenTree/generic/states/api.py
@@ -58,10 +58,10 @@ def get(self, request, *args, **kwargs):
"""Perform a GET request to learn information about status codes."""
data = {}
- def discover_status_codes(parent_status_class, prefix=[]):
+ def discover_status_codes(parent_status_class, prefix=None):
"""Recursively discover status classes."""
for status_class in parent_status_class.__subclasses__():
- name = "__".join([*prefix, status_class.__name__])
+ name = '__'.join([*(prefix or []), status_class.__name__])
data[name] = {
'class': status_class.__name__,
'values': status_class.dict(),
diff --git a/InvenTree/machine/api.py b/InvenTree/machine/api.py
index 66ada4378a5..8f2f7e80ab2 100644
--- a/InvenTree/machine/api.py
+++ b/InvenTree/machine/api.py
@@ -193,24 +193,24 @@ def get(self, request):
machine_api_urls = [
# machine types
- re_path(r"^types/", MachineTypesList.as_view(), name="api-machine-types"),
+ path("types/", MachineTypesList.as_view(), name="api-machine-types"),
# machine drivers
- re_path(r"^drivers/", MachineDriverList.as_view(), name="api-machine-drivers"),
+ path("drivers/", MachineDriverList.as_view(), name="api-machine-drivers"),
# registry status
- re_path(r"^status/", RegistryStatusView.as_view(), name="api-machine-registry-status"),
+ path("status/", RegistryStatusView.as_view(), name="api-machine-registry-status"),
# detail views for a single Machine
- path(r"/", include([
- re_path(r"^settings/", include([
+ path("/", include([
+ path("settings/", include([
re_path(r"^(?PM|D)/(?P\w+)/", MachineSettingDetail.as_view(), name="api-machine-settings-detail"),
- re_path(r"^.*$", MachineSettingList.as_view(), name="api-machine-settings"),
+ path("", MachineSettingList.as_view(), name="api-machine-settings"),
])),
- re_path(r"^.*$", MachineDetail.as_view(), name="api-machine-detail"),
+ path("", MachineDetail.as_view(), name="api-machine-detail"),
])),
# machine list and create
- re_path(r"^.*$", MachineList.as_view(), name="api-machine-list"),
+ path("", MachineList.as_view(), name="api-machine-list"),
]
diff --git a/src/frontend/src/components/tables/machine/MachineListTable.tsx b/src/frontend/src/components/tables/machine/MachineListTable.tsx
index 9323ab094e3..bcb450ded50 100644
--- a/src/frontend/src/components/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/components/tables/machine/MachineListTable.tsx
@@ -32,13 +32,13 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { api } from '../../../App';
import {
+ OpenApiFormProps,
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../../../functions/forms';
import { notYetImplemented } from '../../../functions/notifications';
-import { useTableRefresh } from '../../../hooks/TableRefresh';
-import { ApiPaths, apiUrl } from '../../../states/ApiState';
+import { apiUrl } from '../../../states/ApiState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { ButtonMenu } from '../../buttons/ButtonMenu';
import { ApiFormProps } from '../../forms/ApiForm';
@@ -52,12 +52,13 @@ import { YesNoButton } from '../../items/YesNoButton';
import {
StatusRenderer,
TableStatusRenderer
-} from '../../renderers/StatusRenderer';
+} from '../../render/StatusRenderer';
import { MachineSettingList } from '../../settings/SettingList';
import { TableColumn } from '../Column';
import { BooleanColumn } from '../ColumnRenderers';
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
-import { RowAction } from '../RowActions';
+import { ApiPaths } from "../../../enums/ApiEndpoints";
+import { useTable } from "../../../hooks/UseTable";
interface MachineI {
pk: string;
@@ -327,7 +328,7 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
staleTime: 10 * 1000
});
- const { tableKey, refreshTable } = useTableRefresh('machine');
+ const table = useTable('machine');
const machineTableColumns = useMemo[]>(
() => [
@@ -412,7 +413,7 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
}));
}, [machineDrivers, createFormMachineType]);
- const createMachineForm = useMemo(() => {
+ const createMachineForm = useMemo(() => {
return {
title: t`Create machine`,
url: ApiPaths.machine_list,
@@ -422,9 +423,9 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
field_type: 'choice',
choices: machineTypes
? machineTypes.map((t) => ({
- value: t.slug,
- display_name: `${t.name} (${t.description})`
- }))
+ value: t.slug,
+ display_name: `${t.name} (${t.description})`
+ }))
: [],
onValueChange: ({ value }) => setCreateFormMachineType(value)
},
@@ -451,13 +452,13 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
return (
setCurrentMachinePk(record.pk),
- customActionGroups: [
+ tableActions: [
{
@@ -469,7 +470,7 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
params: {
...props.params
},
- customFilters: [
+ tableFilters: [
{
name: 'active',
label: t`Active`,
diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx
index 0dc5b5368c5..6f8ce50a8ea 100644
--- a/src/frontend/src/enums/ApiEndpoints.tsx
+++ b/src/frontend/src/enums/ApiEndpoints.tsx
@@ -100,5 +100,13 @@ export enum ApiPaths {
error_report_list = 'api-error-report-list',
project_code_list = 'api-project-code-list',
- custom_unit_list = 'api-custom-unit-list'
+ custom_unit_list = 'api-custom-unit-list',
+
+ // Machine URLs
+ machine_types_list = 'api-machine-types',
+ machine_driver_list = 'api-machine-drivers',
+ machine_registry_status = 'api-machine-registry-status',
+ machine_list = 'api-machine-list',
+ machine_setting_list = 'api-machine-settings',
+ machine_setting_detail = 'api-machine-settings-detail',
}
diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
index 3aa646ee229..93eb9fc79c0 100644
--- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
+++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
@@ -7,7 +7,8 @@ import {
IconListDetails,
IconPlugConnected,
IconScale,
- IconUsersGroup
+ IconUsersGroup,
+ IconDevicesPc,
} from '@tabler/icons-react';
import { lazy, useMemo } from 'react';
@@ -29,6 +30,10 @@ const PluginManagementPanel = Loadable(
lazy(() => import('./PluginManagementPanel'))
);
+const MachineManagementPanel = Loadable(
+ lazy(() => import("./MachineManagementPanel"))
+);
+
const ErrorReportTable = Loadable(
lazy(() => import('../../../../components/tables/settings/ErrorTable'))
);
@@ -98,6 +103,12 @@ export default function AdminCenter() {
label: t`Plugins`,
icon: ,
content:
+ },
+ {
+ name: 'machine',
+ label: t`Machines`,
+ icon: ,
+ content:
}
];
}, []);
diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx
new file mode 100644
index 00000000000..4f39bebe435
--- /dev/null
+++ b/src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx
@@ -0,0 +1,11 @@
+import { Stack } from '@mantine/core';
+
+import { MachineListTable } from "../../../../components/tables/machine/MachineListTable";
+
+export default function MachineManagementPanel() {
+ return (
+
+
+
+ );
+}
diff --git a/src/frontend/src/states/SettingsState.tsx b/src/frontend/src/states/SettingsState.tsx
index f301bcd8ed7..63d375c8668 100644
--- a/src/frontend/src/states/SettingsState.tsx
+++ b/src/frontend/src/states/SettingsState.tsx
@@ -160,6 +160,13 @@ export const createMachineSettingsState = ({
error
);
});
+ },
+ getSetting: (key: string, default_value?: string) => {
+ return get().lookup[key] ?? default_value ?? '';
+ },
+ isSet: (key: string, default_value?: boolean) => {
+ let value = get().lookup[key] ?? default_value ?? 'false';
+ return isTrue(value);
}
}));
};
From a1602e422126fac053097761e338cabdfda7893e Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Fri, 26 Jan 2024 15:27:32 +0000
Subject: [PATCH 38/86] Fix style issues
---
InvenTree/machine/api.py | 112 ++++++++++--------
.../tables/machine/MachineListTable.tsx | 27 ++---
src/frontend/src/enums/ApiEndpoints.tsx | 2 +-
.../Index/Settings/AdminCenter/Index.tsx | 6 +-
.../AdminCenter/MachineManagementPanel.tsx | 2 +-
5 files changed, 73 insertions(+), 76 deletions(-)
diff --git a/InvenTree/machine/api.py b/InvenTree/machine/api.py
index 8f2f7e80ab2..bb1c7c41151 100644
--- a/InvenTree/machine/api.py
+++ b/InvenTree/machine/api.py
@@ -8,8 +8,7 @@
import machine.serializers as MachineSerializers
from InvenTree.filters import SEARCH_ORDER_FILTER
-from InvenTree.mixins import (ListCreateAPI, RetrieveUpdateAPI,
- RetrieveUpdateDestroyAPI)
+from InvenTree.mixins import ListCreateAPI, RetrieveUpdateAPI, RetrieveUpdateDestroyAPI
from machine import registry
from machine.models import MachineConfig, MachineSetting
@@ -26,33 +25,19 @@ class MachineList(ListCreateAPI):
def get_serializer_class(self):
# allow driver, machine_type fields on creation
- if self.request.method == "POST":
+ if self.request.method == 'POST':
return MachineSerializers.MachineConfigCreateSerializer
return super().get_serializer_class()
filter_backends = SEARCH_ORDER_FILTER
- filterset_fields = [
- "machine_type",
- "driver",
- "active",
- ]
+ filterset_fields = ['machine_type', 'driver', 'active']
- ordering_fields = [
- "name",
- "machine_type",
- "driver",
- "active",
- ]
+ ordering_fields = ['name', 'machine_type', 'driver', 'active']
- ordering = [
- "-active",
- "machine_type",
- ]
+ ordering = ['-active', 'machine_type']
- search_fields = [
- "name"
- ]
+ search_fields = ['name']
class MachineDetail(RetrieveUpdateDestroyAPI):
@@ -96,17 +81,25 @@ class MachineSettingList(APIView):
permission_classes = [permissions.IsAuthenticated]
- @extend_schema(responses={200: MachineSerializers.MachineSettingSerializer(many=True)})
+ @extend_schema(
+ responses={200: MachineSerializers.MachineSettingSerializer(many=True)}
+ )
def get(self, request, pk):
machine = get_machine(pk)
all_settings = []
for settings, config_type in machine.setting_types:
- settings_dict = MachineSetting.all_settings(settings_definition=settings, machine_config=machine.machine_config, config_type=config_type)
+ settings_dict = MachineSetting.all_settings(
+ settings_definition=settings,
+ machine_config=machine.machine_config,
+ config_type=config_type,
+ )
all_settings.extend(list(settings_dict.values()))
- results = MachineSerializers.MachineSettingSerializer(all_settings, many=True).data
+ results = MachineSerializers.MachineSettingSerializer(
+ all_settings, many=True
+ ).data
return Response(results)
@@ -126,17 +119,21 @@ class MachineSettingDetail(RetrieveUpdateAPI):
def get_object(self):
"""Lookup machine setting object, based on the URL."""
- pk = self.kwargs["pk"]
- key = self.kwargs["key"]
- config_type = MachineSetting.get_config_type(self.kwargs["config_type"])
+ pk = self.kwargs['pk']
+ key = self.kwargs['key']
+ config_type = MachineSetting.get_config_type(self.kwargs['config_type'])
machine = get_machine(pk)
setting_map = dict((d, s) for s, d in machine.setting_types)
if key.upper() not in setting_map[config_type]:
- raise NotFound(detail=f"Machine '{machine.name}' has no {config_type.name} setting matching '{key.upper()}'")
+ raise NotFound(
+ detail=f"Machine '{machine.name}' has no {config_type.name} setting matching '{key.upper()}'"
+ )
- return MachineSetting.get_setting_object(key, machine_config=machine.machine_config, config_type=config_type)
+ return MachineSetting.get_setting_object(
+ key, machine_config=machine.machine_config, config_type=config_type
+ )
class MachineTypesList(APIView):
@@ -150,7 +147,9 @@ class MachineTypesList(APIView):
@extend_schema(responses={200: MachineSerializers.MachineTypeSerializer(many=True)})
def get(self, request):
machine_types = list(registry.machine_types.values())
- results = MachineSerializers.MachineTypeSerializer(machine_types, many=True).data
+ results = MachineSerializers.MachineTypeSerializer(
+ machine_types, many=True
+ ).data
return Response(results)
@@ -162,13 +161,17 @@ class MachineDriverList(APIView):
permission_classes = [permissions.IsAuthenticated]
- @extend_schema(responses={200: MachineSerializers.MachineDriverSerializer(many=True)})
+ @extend_schema(
+ responses={200: MachineSerializers.MachineDriverSerializer(many=True)}
+ )
def get(self, request):
drivers = registry.drivers.values()
- if machine_type := request.query_params.get("machine_type", None):
+ if machine_type := request.query_params.get('machine_type', None):
drivers = filter(lambda d: d.machine_type == machine_type, drivers)
- results = MachineSerializers.MachineDriverSerializer(list(drivers), many=True).data
+ results = MachineSerializers.MachineDriverSerializer(
+ list(drivers), many=True
+ ).data
return Response(results)
@@ -182,10 +185,12 @@ class RegistryStatusView(APIView):
serializer_class = MachineSerializers.MachineRegistryStatusSerializer
- @extend_schema(responses={200: MachineSerializers.MachineRegistryStatusSerializer()})
+ @extend_schema(
+ responses={200: MachineSerializers.MachineRegistryStatusSerializer()}
+ )
def get(self, request):
result = MachineSerializers.MachineRegistryStatusSerializer({
- "registry_errors": list(map(str, registry.errors))
+ 'registry_errors': list(map(str, registry.errors))
}).data
return Response(result)
@@ -193,24 +198,29 @@ def get(self, request):
machine_api_urls = [
# machine types
- path("types/", MachineTypesList.as_view(), name="api-machine-types"),
-
+ path('types/', MachineTypesList.as_view(), name='api-machine-types'),
# machine drivers
- path("drivers/", MachineDriverList.as_view(), name="api-machine-drivers"),
-
+ path('drivers/', MachineDriverList.as_view(), name='api-machine-drivers'),
# registry status
- path("status/", RegistryStatusView.as_view(), name="api-machine-registry-status"),
-
+ path('status/', RegistryStatusView.as_view(), name='api-machine-registry-status'),
# detail views for a single Machine
- path("/", include([
- path("settings/", include([
- re_path(r"^(?PM|D)/(?P\w+)/", MachineSettingDetail.as_view(), name="api-machine-settings-detail"),
- path("", MachineSettingList.as_view(), name="api-machine-settings"),
- ])),
-
- path("", MachineDetail.as_view(), name="api-machine-detail"),
- ])),
-
+ path(
+ '/',
+ include([
+ path(
+ 'settings/',
+ include([
+ re_path(
+ r'^(?PM|D)/(?P\w+)/',
+ MachineSettingDetail.as_view(),
+ name='api-machine-settings-detail',
+ ),
+ path('', MachineSettingList.as_view(), name='api-machine-settings'),
+ ]),
+ ),
+ path('', MachineDetail.as_view(), name='api-machine-detail'),
+ ]),
+ ),
# machine list and create
- path("", MachineList.as_view(), name="api-machine-list"),
+ path('', MachineList.as_view(), name='api-machine-list'),
]
diff --git a/src/frontend/src/components/tables/machine/MachineListTable.tsx b/src/frontend/src/components/tables/machine/MachineListTable.tsx
index bcb450ded50..d0bfa310e89 100644
--- a/src/frontend/src/components/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/components/tables/machine/MachineListTable.tsx
@@ -3,12 +3,8 @@ import {
ActionIcon,
Badge,
Box,
- Button,
Card,
- CheckIcon,
Code,
- Container,
- Divider,
Flex,
Group,
Indicator,
@@ -20,28 +16,21 @@ import {
Title,
Tooltip
} from '@mantine/core';
-import {
- IconAlertCircle,
- IconChevronLeft,
- IconDots,
- IconPlus,
- IconRefresh
-} from '@tabler/icons-react';
+import { IconChevronLeft, IconDots, IconRefresh } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
import { api } from '../../../App';
+import { ApiPaths } from '../../../enums/ApiEndpoints';
import {
OpenApiFormProps,
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../../../functions/forms';
-import { notYetImplemented } from '../../../functions/notifications';
+import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { AddItemButton } from '../../buttons/AddItemButton';
-import { ButtonMenu } from '../../buttons/ButtonMenu';
-import { ApiFormProps } from '../../forms/ApiForm';
import {
ActionDropdown,
DeleteItemAction,
@@ -57,8 +46,6 @@ import { MachineSettingList } from '../../settings/SettingList';
import { TableColumn } from '../Column';
import { BooleanColumn } from '../ColumnRenderers';
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
-import { ApiPaths } from "../../../enums/ApiEndpoints";
-import { useTable } from "../../../hooks/UseTable";
interface MachineI {
pk: string;
@@ -423,9 +410,9 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
field_type: 'choice',
choices: machineTypes
? machineTypes.map((t) => ({
- value: t.slug,
- display_name: `${t.name} (${t.description})`
- }))
+ value: t.slug,
+ display_name: `${t.name} (${t.description})`
+ }))
: [],
onValueChange: ({ value }) => setCreateFormMachineType(value)
},
diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx
index 6f8ce50a8ea..03177aaff32 100644
--- a/src/frontend/src/enums/ApiEndpoints.tsx
+++ b/src/frontend/src/enums/ApiEndpoints.tsx
@@ -108,5 +108,5 @@ export enum ApiPaths {
machine_registry_status = 'api-machine-registry-status',
machine_list = 'api-machine-list',
machine_setting_list = 'api-machine-settings',
- machine_setting_detail = 'api-machine-settings-detail',
+ machine_setting_detail = 'api-machine-settings-detail'
}
diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
index 93eb9fc79c0..22b98afd7ed 100644
--- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
+++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
@@ -2,13 +2,13 @@ import { Trans, t } from '@lingui/macro';
import { Divider, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import {
IconCpu,
+ IconDevicesPc,
IconExclamationCircle,
IconList,
IconListDetails,
IconPlugConnected,
IconScale,
- IconUsersGroup,
- IconDevicesPc,
+ IconUsersGroup
} from '@tabler/icons-react';
import { lazy, useMemo } from 'react';
@@ -31,7 +31,7 @@ const PluginManagementPanel = Loadable(
);
const MachineManagementPanel = Loadable(
- lazy(() => import("./MachineManagementPanel"))
+ lazy(() => import('./MachineManagementPanel'))
);
const ErrorReportTable = Loadable(
diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx
index 4f39bebe435..892588dba1f 100644
--- a/src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx
+++ b/src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx
@@ -1,6 +1,6 @@
import { Stack } from '@mantine/core';
-import { MachineListTable } from "../../../../components/tables/machine/MachineListTable";
+import { MachineListTable } from '../../../../components/tables/machine/MachineListTable';
export default function MachineManagementPanel() {
return (
From 2430af87e1922361fe58b077e2c11edf2d06511c Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Fri, 26 Jan 2024 22:47:49 +0000
Subject: [PATCH 39/86] Added machine type,machine driver,error stack tables
---
InvenTree/machine/api.py | 4 +-
InvenTree/machine/serializers.py | 17 +-
.../components/forms/fields/ChoiceField.tsx | 36 +-
.../src/components/items/InfoItem.tsx | 39 +-
.../src/components/nav/DetailDrawer.tsx | 2 +-
.../src/components/settings/SettingList.tsx | 6 +
.../tables/machine/MachineListTable.tsx | 386 ++++++++++--------
.../tables/machine/MachineTypeTable.tsx | 321 +++++++++++++++
.../AdminCenter/MachineManagementPanel.tsx | 67 ++-
9 files changed, 671 insertions(+), 207 deletions(-)
create mode 100644 src/frontend/src/components/tables/machine/MachineTypeTable.tsx
diff --git a/InvenTree/machine/api.py b/InvenTree/machine/api.py
index bb1c7c41151..72c2e9c3559 100644
--- a/InvenTree/machine/api.py
+++ b/InvenTree/machine/api.py
@@ -190,7 +190,9 @@ class RegistryStatusView(APIView):
)
def get(self, request):
result = MachineSerializers.MachineRegistryStatusSerializer({
- 'registry_errors': list(map(str, registry.errors))
+ 'registry_errors': list(
+ {'message': str(error)} for error in registry.errors
+ )
}).data
return Response(result)
diff --git a/InvenTree/machine/serializers.py b/InvenTree/machine/serializers.py
index 40d8c1704bf..fc22f6d8152 100644
--- a/InvenTree/machine/serializers.py
+++ b/InvenTree/machine/serializers.py
@@ -117,10 +117,10 @@ class Meta:
def get_provider_file(self, obj: ClassProviderMixin) -> str:
return obj.get_provider_file()
- def get_provider_plugin(self, obj: ClassProviderMixin) -> Union[str, None]:
+ def get_provider_plugin(self, obj: ClassProviderMixin) -> Union[dict, None]:
plugin = obj.get_provider_plugin()
if plugin:
- return plugin.slug
+ return {"slug": plugin.slug, "name": plugin.human_name, "pk": getattr(plugin.plugin_config(), "pk", None)}
return None
def get_is_builtin(self, obj: ClassProviderMixin) -> bool:
@@ -150,6 +150,17 @@ class Meta(BaseMachineClassSerializer.Meta):
machine_type = serializers.SlugField(read_only=True)
+class MachineRegistryErrorSerializer(serializers.Serializer):
+ """Serializer for a machine registry error."""
+
+ class Meta:
+ fields = [
+ "message"
+ ]
+
+ message = serializers.CharField()
+
+
class MachineRegistryStatusSerializer(serializers.Serializer):
"""Serializer for machine registry status."""
@@ -158,4 +169,4 @@ class Meta:
"registry_errors",
]
- registry_errors = serializers.ListField(child=serializers.CharField())
+ registry_errors = serializers.ListField(child=MachineRegistryErrorSerializer())
diff --git a/src/frontend/src/components/forms/fields/ChoiceField.tsx b/src/frontend/src/components/forms/fields/ChoiceField.tsx
index 6c5762bd32a..63e1f3ef4ea 100644
--- a/src/frontend/src/components/forms/fields/ChoiceField.tsx
+++ b/src/frontend/src/components/forms/fields/ChoiceField.tsx
@@ -1,4 +1,4 @@
-import { Select } from '@mantine/core';
+import { Input, Select } from '@mantine/core';
import { useId } from '@mantine/hooks';
import { useCallback } from 'react';
import { useMemo } from 'react';
@@ -49,16 +49,30 @@ export function ChoiceField({
[field.onChange, definition]
);
+ /* Construct a "cut-down" version of the definition,
+ * which does not include any attributes that the lower components do not recognize
+ */
+ const fieldDefinition = useMemo(() => {
+ return {
+ ...definition,
+ onValueChange: undefined,
+ adjustFilters: undefined,
+ read_only: undefined
+ };
+ }, [definition]);
+
return (
-
+
+
+
);
}
diff --git a/src/frontend/src/components/items/InfoItem.tsx b/src/frontend/src/components/items/InfoItem.tsx
index e77a302bb98..aeb5c5110f8 100644
--- a/src/frontend/src/components/items/InfoItem.tsx
+++ b/src/frontend/src/components/items/InfoItem.tsx
@@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro';
-import { Flex, Group, Text } from '@mantine/core';
+import { Code, Flex, Group, Text } from '@mantine/core';
+import { Link, To } from 'react-router-dom';
import { YesNoButton } from './YesNoButton';
@@ -7,13 +8,37 @@ export function InfoItem({
name,
children,
type,
- value
+ value,
+ link
}: {
name: string;
children?: React.ReactNode;
- type?: 'text' | 'boolean';
+ type?: 'text' | 'boolean' | 'code';
value?: any;
+ link?: To;
}) {
+ function renderComponent() {
+ if (value === undefined) return null;
+
+ if (type === 'text') {
+ return {value || None};
+ }
+
+ if (type === 'boolean') {
+ return ;
+ }
+
+ if (type === 'code') {
+ return (
+
+ {value}
+
+ );
+ }
+
+ return null;
+ }
+
return (
@@ -21,13 +46,7 @@ export function InfoItem({
{children}
- {value !== undefined && type === 'text' ? (
- {value || None}
- ) : type === 'boolean' ? (
-
- ) : (
- ''
- )}
+ {link ? {renderComponent()} : renderComponent()}
);
diff --git a/src/frontend/src/components/nav/DetailDrawer.tsx b/src/frontend/src/components/nav/DetailDrawer.tsx
index fcb19180362..20ef06f21ca 100644
--- a/src/frontend/src/components/nav/DetailDrawer.tsx
+++ b/src/frontend/src/components/nav/DetailDrawer.tsx
@@ -31,7 +31,7 @@ function DetailDrawerComponent({
return (
navigate('../')}
+ onClose={() => navigate(-1)}
position={position}
size={size}
title={
diff --git a/src/frontend/src/components/settings/SettingList.tsx b/src/frontend/src/components/settings/SettingList.tsx
index 6f1ce65dd71..8e0f1c2dc98 100644
--- a/src/frontend/src/components/settings/SettingList.tsx
+++ b/src/frontend/src/components/settings/SettingList.tsx
@@ -1,3 +1,4 @@
+import { Trans } from '@lingui/macro';
import { Stack, Text } from '@mantine/core';
import React, { useEffect, useMemo, useRef } from 'react';
import { useStore } from 'zustand';
@@ -54,6 +55,11 @@ export function SettingList({
);
})}
+ {(keys || allKeys).length === 0 && (
+
+ No settings specified
+
+ )}
>
);
diff --git a/src/frontend/src/components/tables/machine/MachineListTable.tsx b/src/frontend/src/components/tables/machine/MachineListTable.tsx
index d0bfa310e89..70262a3d0dd 100644
--- a/src/frontend/src/components/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/components/tables/machine/MachineListTable.tsx
@@ -13,21 +13,17 @@ import {
Space,
Stack,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
-import { IconChevronLeft, IconDots, IconRefresh } from '@tabler/icons-react';
+import { IconDots, IconRefresh } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
-import React, { useCallback, useMemo, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import { api } from '../../../App';
import { ApiPaths } from '../../../enums/ApiEndpoints';
-import {
- OpenApiFormProps,
- openCreateApiForm,
- openDeleteApiForm,
- openEditApiForm
-} from '../../../functions/forms';
+import { openDeleteApiForm, openEditApiForm } from '../../../functions/forms';
+import { useCreateApiFormModal } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { AddItemButton } from '../../buttons/AddItemButton';
@@ -36,8 +32,10 @@ import {
DeleteItemAction,
EditItemAction
} from '../../items/ActionDropdown';
+import { InfoItem } from '../../items/InfoItem';
import { UnavailableIndicator } from '../../items/UnavailableIndicator';
import { YesNoButton } from '../../items/YesNoButton';
+import { DetailDrawer } from '../../nav/DetailDrawer';
import {
StatusRenderer,
TableStatusRenderer
@@ -46,6 +44,7 @@ import { MachineSettingList } from '../../settings/SettingList';
import { TableColumn } from '../Column';
import { BooleanColumn } from '../ColumnRenderers';
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
+import { MachineDriverI, MachineTypeI } from './MachineTypeTable';
interface MachineI {
pk: string;
@@ -61,25 +60,6 @@ interface MachineI {
is_driver_available: boolean;
}
-interface MachineTypeI {
- slug: string;
- name: string;
- description: string;
- provider_file: string;
- provider_plugin: string;
- is_builtin: boolean;
-}
-
-interface MachineDriverI {
- slug: string;
- name: string;
- description: string;
- provider_file: string;
- provider_plugin: string;
- is_builtin: boolean;
- machine_type: string;
-}
-
function MachineStatusIndicator({ machine }: { machine: MachineI }) {
const sx = { marginLeft: '4px' };
@@ -111,13 +91,54 @@ function MachineStatusIndicator({ machine }: { machine: MachineI }) {
);
}
-function MachineDetail({
+export function useMachineTypeDriver({
+ includeTypes = true,
+ includeDrivers = true
+}: { includeTypes?: boolean; includeDrivers?: boolean } = {}) {
+ const {
+ data: machineTypes,
+ isFetching: isMachineTypesFetching,
+ refetch: refreshMachineTypes
+ } = useQuery({
+ enabled: includeTypes,
+ queryKey: ['machine-types'],
+ queryFn: () =>
+ api.get(apiUrl(ApiPaths.machine_types_list)).then((res) => res.data),
+ staleTime: 10 * 1000
+ });
+ const {
+ data: machineDrivers,
+ isFetching: isMachineDriversFetching,
+ refetch: refreshDrivers
+ } = useQuery({
+ enabled: includeDrivers,
+ queryKey: ['machine-drivers'],
+ queryFn: () =>
+ api.get(apiUrl(ApiPaths.machine_driver_list)).then((res) => res.data),
+ staleTime: 10 * 1000
+ });
+
+ const refresh = useCallback(() => {
+ refreshMachineTypes();
+ refreshDrivers();
+ }, [refreshDrivers, refreshMachineTypes]);
+
+ return {
+ machineTypes,
+ machineDrivers,
+ isFetching: isMachineTypesFetching || isMachineDriversFetching,
+ refresh
+ };
+}
+
+function MachineDrawer({
machinePk,
- goBack
+ refreshTable
}: {
machinePk: string;
- goBack: () => void;
+ refreshTable: () => void;
}) {
+ const navigate = useNavigate();
const {
data: machine,
refetch,
@@ -128,42 +149,34 @@ function MachineDetail({
queryFn: () =>
api.get(apiUrl(ApiPaths.machine_list, machinePk)).then((res) => res.data)
});
- const { data: machineTypes, isFetching: isMachineTypesFetching } = useQuery<
- MachineTypeI[]
- >({
- queryKey: ['machine-types'],
- queryFn: () =>
- api.get(apiUrl(ApiPaths.machine_types_list)).then((res) => res.data),
- staleTime: 10 * 1000
- });
+ const {
+ machineTypes,
+ machineDrivers,
+ isFetching: isMachineTypeDriverFetching
+ } = useMachineTypeDriver();
- const isFetching = isMachineFetching || isMachineTypesFetching;
+ const isFetching = isMachineFetching || isMachineTypeDriverFetching;
- function InfoItem({
- name,
- children
- }: {
- name: string;
- children: React.ReactNode;
- }) {
- return (
-
-
- {name}:
-
- {children}
-
- );
- }
+ const machineType = useMemo(
+ () =>
+ machineTypes && machine
+ ? machineTypes.find((t) => t.slug === machine.machine_type)
+ : undefined,
+ [machine?.machine_type, machineTypes]
+ );
+
+ const machineDriver = useMemo(
+ () =>
+ machineDrivers && machine
+ ? machineDrivers.find((d) => d.slug === machine.driver)
+ : undefined,
+ [machine?.driver, machineDrivers]
+ );
return (
-
-
-
-
-
+
{machine && }
@@ -200,7 +213,7 @@ function MachineDetail({
preFormContent: (
{t`Are you sure you want to remove the machine "${machine?.name}"?`}
),
- onFormSuccess: () => goBack()
+ onFormSuccess: () => navigate(-1)
});
}
})
@@ -208,11 +221,11 @@ function MachineDetail({
/>
-
+
- Info
+ Machine information
refetch()}>
@@ -222,17 +235,17 @@ function MachineDetail({
- {machine?.machine_type}
- {machine &&
- machineTypes &&
- machineTypes.findIndex(
- (m) => m.slug === machine.machine_type
- ) === -1 && }
+
+ {machineType ? machineType.name : machine?.machine_type}
+
+ {machine && !machineType && }
- {machine?.driver}
+
+ {machineDriver ? machineDriver.name : machine?.driver}
+
{!machine?.is_driver_available && }
@@ -269,8 +282,8 @@ function MachineDetail({
)}
- {machine?.machine_errors.map((error) => (
-
+ {machine?.machine_errors.map((error, i) => (
+
{error}
))}
@@ -283,15 +296,19 @@ function MachineDetail({
{machine?.is_driver_available && (
<>
-
- Machine Settings
-
-
+
+
+ Machine Settings
+
+
+
-
- Driver Settings
-
-
+
+
+ Driver Settings
+
+
+
>
)}
@@ -302,20 +319,10 @@ function MachineDetail({
* Table displaying list of available plugins
*/
export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
- const { data: machineTypes } = useQuery({
- queryKey: ['machine-types'],
- queryFn: () =>
- api.get(apiUrl(ApiPaths.machine_types_list)).then((res) => res.data),
- staleTime: 10 * 1000
- });
- const { data: machineDrivers } = useQuery({
- queryKey: ['machine-drivers'],
- queryFn: () =>
- api.get(apiUrl(ApiPaths.machine_driver_list)).then((res) => res.data),
- staleTime: 10 * 1000
- });
+ const { machineTypes, machineDrivers } = useMachineTypeDriver();
const table = useTable('machine');
+ const navigate = useNavigate();
const machineTableColumns = useMemo[]>(
() => [
@@ -337,13 +344,15 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
title: t`Machine Type`,
sortable: true,
render: (record) => {
+ const machineType = machineTypes?.find(
+ (m) => m.slug === record.machine_type
+ );
return (
- {record.machine_type}
- {machineTypes &&
- machineTypes.findIndex(
- (m: any) => m.slug === record.machine_type
- ) === -1 && }
+
+ {machineType ? machineType.name : record.machine_type}
+
+ {machineTypes && !machineType && }
);
}
@@ -353,9 +362,10 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
title: t`Machine Driver`,
sortable: true,
render: (record) => {
+ const driver = machineDrivers?.find((d) => d.slug === record.driver);
return (
- {record.driver}
+ {driver ? driver.name : record.driver}
{!record.is_driver_available && }
);
@@ -400,89 +410,107 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
}));
}, [machineDrivers, createFormMachineType]);
- const createMachineForm = useMemo(() => {
- return {
- title: t`Create machine`,
- url: ApiPaths.machine_list,
- fields: {
- name: {},
- machine_type: {
- field_type: 'choice',
- choices: machineTypes
- ? machineTypes.map((t) => ({
- value: t.slug,
- display_name: `${t.name} (${t.description})`
- }))
- : [],
- onValueChange: ({ value }) => setCreateFormMachineType(value)
- },
- driver: {
- field_type: 'choice',
- disabled: !createFormMachineType,
- choices: createFormDriverOptions
- },
- active: {}
+ const createMachineForm = useCreateApiFormModal({
+ title: t`Create machine`,
+ url: ApiPaths.machine_list,
+ fields: {
+ name: {},
+ machine_type: {
+ field_type: 'choice',
+ choices: machineTypes
+ ? machineTypes.map((t) => ({
+ value: t.slug,
+ display_name: `${t.name} (${t.description})`
+ }))
+ : [],
+ onValueChange: (value) => setCreateFormMachineType(value)
},
- onClose: () => {
- setCreateFormMachineType(null);
- }
- };
- }, [createFormMachineType, createFormDriverOptions, machineTypes]);
-
- const [currentMachinePk, setCurrentMachinePk] = useState(null);
- const goBack = useCallback(() => setCurrentMachinePk(null), []);
+ driver: {
+ field_type: 'choice',
+ disabled: !createFormMachineType,
+ choices: createFormDriverOptions
+ },
+ active: {}
+ },
+ onFormSuccess: (data) => {
+ table.refreshTable();
+ navigate(`machine-${data.pk}/`);
+ },
+ onClose: () => {
+ setCreateFormMachineType(null);
+ }
+ });
- if (currentMachinePk) {
- return ;
- }
+ const tableActions = useMemo(() => {
+ return [
+ {
+ setCreateFormMachineType(null);
+ createMachineForm.open();
+ }}
+ />
+ ];
+ }, [createMachineForm.open]);
return (
- setCurrentMachinePk(record.pk),
- tableActions: [
- {
- setCreateFormMachineType(null);
- openCreateApiForm(createMachineForm);
- }}
- />
- ],
- params: {
- ...props.params
- },
- tableFilters: [
- {
- name: 'active',
- label: t`Active`,
- type: 'boolean'
- },
- {
- name: 'machine_type',
- label: t`Machine Type`,
- type: 'choice',
- choiceFunction: () =>
- machineTypes
- ? machineTypes.map((t) => ({ value: t.slug, label: t.name }))
- : []
+ <>
+ {createMachineForm.modal}
+ {
+ if (!id || !id.startsWith('machine-')) return false;
+ return (
+
+ );
+ }}
+ />
+ navigate(`machine-${machine.pk}/`),
+ tableActions,
+ params: {
+ ...props.params
},
- {
- name: 'driver',
- label: t`Machine Driver`,
- type: 'choice',
- choiceFunction: () =>
- machineDrivers
- ? machineDrivers.map((d) => ({ value: d.slug, label: d.name }))
- : []
- }
- ]
- }}
- />
+ tableFilters: [
+ {
+ name: 'active',
+ label: t`Active`,
+ type: 'boolean'
+ },
+ {
+ name: 'machine_type',
+ label: t`Machine Type`,
+ type: 'choice',
+ choiceFunction: () =>
+ machineTypes
+ ? machineTypes.map((t) => ({ value: t.slug, label: t.name }))
+ : []
+ },
+ {
+ name: 'driver',
+ label: t`Machine Driver`,
+ type: 'choice',
+ choiceFunction: () =>
+ machineDrivers
+ ? machineDrivers.map((d) => ({
+ value: d.slug,
+ label: d.name
+ }))
+ : []
+ }
+ ]
+ }}
+ />
+ >
);
}
diff --git a/src/frontend/src/components/tables/machine/MachineTypeTable.tsx b/src/frontend/src/components/tables/machine/MachineTypeTable.tsx
new file mode 100644
index 00000000000..dcb8aca6dc0
--- /dev/null
+++ b/src/frontend/src/components/tables/machine/MachineTypeTable.tsx
@@ -0,0 +1,321 @@
+import { Trans, t } from '@lingui/macro';
+import {
+ ActionIcon,
+ Card,
+ Group,
+ LoadingOverlay,
+ Stack,
+ Text,
+ Title
+} from '@mantine/core';
+import { IconRefresh } from '@tabler/icons-react';
+import { useMemo } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { ApiPaths } from '../../../enums/ApiEndpoints';
+import { useTable } from '../../../hooks/UseTable';
+import { apiUrl } from '../../../states/ApiState';
+import { InfoItem } from '../../items/InfoItem';
+import { DetailDrawer } from '../../nav/DetailDrawer';
+import { TableColumn } from '../Column';
+import { BooleanColumn } from '../ColumnRenderers';
+import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
+import { useMachineTypeDriver } from './MachineListTable';
+
+export interface MachineTypeI {
+ slug: string;
+ name: string;
+ description: string;
+ provider_file: string;
+ provider_plugin: { slug: string; name: string; pk: number | null } | null;
+ is_builtin: boolean;
+}
+
+export interface MachineDriverI {
+ slug: string;
+ name: string;
+ description: string;
+ provider_file: string;
+ provider_plugin: { slug: string; name: string; pk: number | null } | null;
+ is_builtin: boolean;
+ machine_type: string;
+}
+
+function MachineTypeDrawer({ machineTypeSlug }: { machineTypeSlug: string }) {
+ const navigate = useNavigate();
+
+ const { machineTypes, refresh, isFetching } = useMachineTypeDriver({
+ includeDrivers: false
+ });
+ const machineType = useMemo(
+ () => machineTypes?.find((m) => m.slug === machineTypeSlug),
+ [machineTypes, machineTypeSlug]
+ );
+
+ const table = useTable('machineDrivers');
+
+ const machineDriverTableColumns = useMemo[]>(
+ () => [
+ {
+ accessor: 'name',
+ title: t`Name`
+ },
+ {
+ accessor: 'slug',
+ title: t`Slug`
+ },
+ {
+ accessor: 'description',
+ title: t`Description`
+ },
+ BooleanColumn({
+ accessor: 'is_builtin',
+ title: t`Builtin driver`
+ })
+ ],
+ []
+ );
+
+ return (
+
+
+
+ {machineType ? machineType.name : machineTypeSlug}
+
+
+
+ {!machineType && (
+
+ Machine type not found.
+
+ )}
+
+
+
+
+
+ Machine type information
+
+ refresh()}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Available drivers
+
+
+ {
+ return data.filter(
+ (d: any) => d.machine_type === machineTypeSlug
+ );
+ },
+ enableDownload: false,
+ enableSearch: false,
+ onRowClick: (machine) => navigate(`../driver-${machine.slug}/`)
+ }}
+ />
+
+
+
+ );
+}
+
+function MachineDriverDrawer({
+ machineDriverSlug
+}: {
+ machineDriverSlug: string;
+}) {
+ const { machineDrivers, machineTypes, refresh, isFetching } =
+ useMachineTypeDriver();
+ const machineDriver = useMemo(
+ () => machineDrivers?.find((d) => d.slug === machineDriverSlug),
+ [machineDrivers, machineDriverSlug]
+ );
+ const machineType = useMemo(
+ () => machineTypes?.find((t) => t.slug === machineDriver?.machine_type),
+ [machineDrivers, machineTypes]
+ );
+
+ return (
+
+
+
+ {machineDriver ? machineDriver.name : machineDriverSlug}
+
+
+
+ {!machineDriver && (
+
+ Machine driver not found.
+
+ )}
+
+
+
+
+
+ Machine driver information
+
+ refresh()}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+/**
+ * Table displaying list of available machine types
+ */
+export function MachineTypeListTable({
+ props
+}: {
+ props: InvenTreeTableProps;
+}) {
+ const table = useTable('machineTypes');
+ const navigate = useNavigate();
+
+ const machineTypeTableColumns = useMemo[]>(
+ () => [
+ {
+ accessor: 'name',
+ title: t`Name`
+ },
+ {
+ accessor: 'slug',
+ title: t`Slug`
+ },
+ {
+ accessor: 'description',
+ title: t`Description`
+ },
+ BooleanColumn({
+ accessor: 'is_builtin',
+ title: t`Builtin type`
+ })
+ ],
+ []
+ );
+
+ return (
+ <>
+ {
+ if (!id || !id.startsWith('type-')) return false;
+ return (
+
+ );
+ }}
+ />
+ {
+ if (!id || !id.startsWith('driver-')) return false;
+ return (
+
+ );
+ }}
+ />
+ navigate(`type-${machine.slug}/`),
+ params: {
+ ...props.params
+ }
+ }}
+ />
+ >
+ );
+}
diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx
index 892588dba1f..58a1e393333 100644
--- a/src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx
+++ b/src/frontend/src/pages/Index/Settings/AdminCenter/MachineManagementPanel.tsx
@@ -1,11 +1,74 @@
-import { Stack } from '@mantine/core';
+import { Trans } from '@lingui/macro';
+import {
+ ActionIcon,
+ Code,
+ Group,
+ List,
+ Space,
+ Stack,
+ Text,
+ Title
+} from '@mantine/core';
+import { IconRefresh } from '@tabler/icons-react';
+import { useQuery } from '@tanstack/react-query';
+import { api } from '../../../../App';
import { MachineListTable } from '../../../../components/tables/machine/MachineListTable';
+import { MachineTypeListTable } from '../../../../components/tables/machine/MachineTypeTable';
+import { ApiPaths } from '../../../../enums/ApiEndpoints';
+import { apiUrl } from '../../../../states/ApiState';
+
+interface MachineRegistryStatusI {
+ registry_errors: { message: string }[];
+}
export default function MachineManagementPanel() {
+ const { data: registryStatus, refetch } = useQuery({
+ queryKey: ['machine-registry-status'],
+ queryFn: () =>
+ api.get(apiUrl(ApiPaths.machine_registry_status)).then((res) => res.data),
+ staleTime: 10 * 1000
+ });
+
return (
-
+
+
+
+
+
+
+ Machine types
+
+
+
+
+
+
+
+
+
+ Machine Error Stack
+
+ refetch()}>
+
+
+
+ {registryStatus?.registry_errors &&
+ registryStatus.registry_errors.length === 0 ? (
+
+ There are no machine registry errors.
+
+ ) : (
+
+ {registryStatus?.registry_errors?.map((error, i) => (
+
+ {error.message}
+
+ ))}
+
+ )}
+
);
}
From 758deb5b8a2940e5e959f72aa05c02ff6b7490ce Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Fri, 26 Jan 2024 22:48:27 +0000
Subject: [PATCH 40/86] Fix style in machine/serializers.py
---
InvenTree/machine/serializers.py | 110 +++++++++++++++----------------
1 file changed, 54 insertions(+), 56 deletions(-)
diff --git a/InvenTree/machine/serializers.py b/InvenTree/machine/serializers.py
index fc22f6d8152..52f3293504f 100644
--- a/InvenTree/machine/serializers.py
+++ b/InvenTree/machine/serializers.py
@@ -12,38 +12,36 @@ class MachineConfigSerializer(serializers.ModelSerializer):
class Meta:
"""Meta for serializer."""
+
model = MachineConfig
fields = [
- "pk",
- "name",
- "machine_type",
- "driver",
- "initialized",
- "active",
- "status",
- "status_model",
- "status_text",
- "machine_errors",
- "is_driver_available",
+ 'pk',
+ 'name',
+ 'machine_type',
+ 'driver',
+ 'initialized',
+ 'active',
+ 'status',
+ 'status_model',
+ 'status_text',
+ 'machine_errors',
+ 'is_driver_available',
]
- read_only_fields = [
- "machine_type",
- "driver",
- ]
+ read_only_fields = ['machine_type', 'driver']
- initialized = serializers.SerializerMethodField("get_initialized")
- status = serializers.SerializerMethodField("get_status")
- status_model = serializers.SerializerMethodField("get_status_model")
- status_text = serializers.SerializerMethodField("get_status_text")
- machine_errors = serializers.SerializerMethodField("get_errors")
- is_driver_available = serializers.SerializerMethodField("get_is_driver_available")
+ initialized = serializers.SerializerMethodField('get_initialized')
+ status = serializers.SerializerMethodField('get_status')
+ status_model = serializers.SerializerMethodField('get_status_model')
+ status_text = serializers.SerializerMethodField('get_status_text')
+ machine_errors = serializers.SerializerMethodField('get_errors')
+ is_driver_available = serializers.SerializerMethodField('get_is_driver_available')
def get_initialized(self, obj: MachineConfig) -> bool:
- return getattr(obj.machine, "initialized", False)
+ return getattr(obj.machine, 'initialized', False)
def get_status(self, obj: MachineConfig) -> int:
- status = getattr(obj.machine, "status", None)
+ status = getattr(obj.machine, 'status', None)
if status is not None:
return status.value
return -1
@@ -54,7 +52,7 @@ def get_status_model(self, obj: MachineConfig) -> Union[str, None]:
return None
def get_status_text(self, obj: MachineConfig) -> str:
- return getattr(obj.machine, "status_text", "")
+ return getattr(obj.machine, 'status_text', '')
def get_errors(self, obj: MachineConfig) -> List[str]:
return [str(err) for err in obj.errors]
@@ -68,27 +66,29 @@ class MachineConfigCreateSerializer(MachineConfigSerializer):
class Meta(MachineConfigSerializer.Meta):
"""Meta for serializer."""
- read_only_fields = list(set(MachineConfigSerializer.Meta.read_only_fields) - set(["machine_type", "driver"]))
+
+ read_only_fields = list(
+ set(MachineConfigSerializer.Meta.read_only_fields)
+ - set(['machine_type', 'driver'])
+ )
class MachineSettingSerializer(GenericReferencedSettingSerializer):
"""Serializer for the MachineSetting model."""
MODEL = MachineSetting
- EXTRA_FIELDS = [
- "config_type",
- ]
+ EXTRA_FIELDS = ['config_type']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# remove unwanted fields
- unwanted_fields = ["pk", "model_name", "api_url", "typ"]
+ unwanted_fields = ['pk', 'model_name', 'api_url', 'typ']
for f in unwanted_fields:
if f in self.Meta.fields:
self.Meta.fields.remove(f)
- setattr(self.Meta, "read_only_fields", ["config_type"])
+ self.Meta.read_only_fields = ['config_type']
class BaseMachineClassSerializer(serializers.Serializer):
@@ -96,23 +96,24 @@ class BaseMachineClassSerializer(serializers.Serializer):
class Meta:
"""Meta for a serializer."""
+
fields = [
- "slug",
- "name",
- "description",
- "provider_file",
- "provider_plugin",
- "is_builtin",
+ 'slug',
+ 'name',
+ 'description',
+ 'provider_file',
+ 'provider_plugin',
+ 'is_builtin',
]
read_only_fields = fields
- slug = serializers.SlugField(source="SLUG")
- name = serializers.CharField(source="NAME")
- description = serializers.CharField(source="DESCRIPTION")
- provider_file = serializers.SerializerMethodField("get_provider_file")
- provider_plugin = serializers.SerializerMethodField("get_provider_plugin")
- is_builtin = serializers.SerializerMethodField("get_is_builtin")
+ slug = serializers.SlugField(source='SLUG')
+ name = serializers.CharField(source='NAME')
+ description = serializers.CharField(source='DESCRIPTION')
+ provider_file = serializers.SerializerMethodField('get_provider_file')
+ provider_plugin = serializers.SerializerMethodField('get_provider_plugin')
+ is_builtin = serializers.SerializerMethodField('get_is_builtin')
def get_provider_file(self, obj: ClassProviderMixin) -> str:
return obj.get_provider_file()
@@ -120,7 +121,11 @@ def get_provider_file(self, obj: ClassProviderMixin) -> str:
def get_provider_plugin(self, obj: ClassProviderMixin) -> Union[dict, None]:
plugin = obj.get_provider_plugin()
if plugin:
- return {"slug": plugin.slug, "name": plugin.human_name, "pk": getattr(plugin.plugin_config(), "pk", None)}
+ return {
+ 'slug': plugin.slug,
+ 'name': plugin.human_name,
+ 'pk': getattr(plugin.plugin_config(), 'pk', None),
+ }
return None
def get_is_builtin(self, obj: ClassProviderMixin) -> bool:
@@ -132,9 +137,8 @@ class MachineTypeSerializer(BaseMachineClassSerializer):
class Meta(BaseMachineClassSerializer.Meta):
"""Meta for a serializer."""
- fields = [
- *BaseMachineClassSerializer.Meta.fields
- ]
+
+ fields = [*BaseMachineClassSerializer.Meta.fields]
class MachineDriverSerializer(BaseMachineClassSerializer):
@@ -142,10 +146,8 @@ class MachineDriverSerializer(BaseMachineClassSerializer):
class Meta(BaseMachineClassSerializer.Meta):
"""Meta for a serializer."""
- fields = [
- *BaseMachineClassSerializer.Meta.fields,
- "machine_type",
- ]
+
+ fields = [*BaseMachineClassSerializer.Meta.fields, 'machine_type']
machine_type = serializers.SlugField(read_only=True)
@@ -154,9 +156,7 @@ class MachineRegistryErrorSerializer(serializers.Serializer):
"""Serializer for a machine registry error."""
class Meta:
- fields = [
- "message"
- ]
+ fields = ['message']
message = serializers.CharField()
@@ -165,8 +165,6 @@ class MachineRegistryStatusSerializer(serializers.Serializer):
"""Serializer for machine registry status."""
class Meta:
- fields = [
- "registry_errors",
- ]
+ fields = ['registry_errors']
registry_errors = serializers.ListField(child=MachineRegistryErrorSerializer())
From c87a9f0aa331824952d306cf0f895cf7e8be330a Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Fri, 26 Jan 2024 23:04:57 +0000
Subject: [PATCH 41/86] Added pui link from machine to machine type/driver
drawer
---
.../tables/machine/MachineListTable.tsx | 21 +++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/src/frontend/src/components/tables/machine/MachineListTable.tsx b/src/frontend/src/components/tables/machine/MachineListTable.tsx
index 70262a3d0dd..dfb9586986f 100644
--- a/src/frontend/src/components/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/components/tables/machine/MachineListTable.tsx
@@ -19,6 +19,7 @@ import { IconDots, IconRefresh } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
+import { Link } from 'react-router-dom';
import { api } from '../../../App';
import { ApiPaths } from '../../../enums/ApiEndpoints';
@@ -235,17 +236,25 @@ function MachineDrawer({
-
- {machineType ? machineType.name : machine?.machine_type}
-
+ {machineType ? (
+
+ {machineType.name}
+
+ ) : (
+ {machine?.machine_type}
+ )}
{machine && !machineType && }
-
- {machineDriver ? machineDriver.name : machine?.driver}
-
+ {machineDriver ? (
+
+ {machineDriver.name}
+
+ ) : (
+ {machine?.driver}
+ )}
{!machine?.is_driver_available && }
From 8a59c90c09966cb1c4358b2efa325c4833d68573 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sat, 27 Jan 2024 09:33:50 +0000
Subject: [PATCH 42/86] Removed only partially working django admin in favor of
the PUI admin center implementation
---
InvenTree/machine/admin.py | 74 +++++--------------
.../machine_types/LabelPrintingMachineType.py | 20 ++---
2 files changed, 30 insertions(+), 64 deletions(-)
diff --git a/InvenTree/machine/admin.py b/InvenTree/machine/admin.py
index c23147348c3..5730415a517 100755
--- a/InvenTree/machine/admin.py
+++ b/InvenTree/machine/admin.py
@@ -1,25 +1,7 @@
-from django import forms
from django.contrib import admin
-from django.utils.translation import gettext_lazy as _
+from django.http.request import HttpRequest
from machine import models
-from machine.registry import registry
-
-# Note: Most of this code here is only for developing as there is no UI for machines *yet*.
-
-
-class MachineConfigAdminForm(forms.ModelForm):
- def get_machine_type_choices():
- return [(machine_type.SLUG, machine_type.NAME) for machine_type in registry.machine_types.values()]
-
- def get_driver_choices():
- return [(driver.SLUG, driver.NAME) for driver in registry.drivers.values()]
-
- # TODO: add conditional choices like shown here
- # Ref: https://www.reddit.com/r/django/comments/18cj55/conditional_choices_for_model_field_based_on/
- # Ref: https://gist.github.com/blackrobot/4956070
- driver = forms.ChoiceField(label=_("Driver"), choices=get_driver_choices)
- machine_type = forms.ChoiceField(label=_("Machine Type"), choices=get_machine_type_choices)
class MachineSettingInline(admin.TabularInline):
@@ -27,54 +9,38 @@ class MachineSettingInline(admin.TabularInline):
model = models.MachineSetting
- read_only_fields = [
- 'key',
- 'config_type'
- ]
-
- def get_extra(self, request, obj, **kwargs):
- if getattr(obj, 'machine', None) is not None:
- # TODO: improve this mechanism
- machine_settings = getattr(obj.machine, "MACHINE_SETTINGS", {})
- driver_settings = getattr(obj.machine.driver, "MACHINE_SETTINGS", {})
- count = len(machine_settings.keys()) + len(driver_settings.keys())
- if obj.settings.count() != count:
- return count
- return 0
+ read_only_fields = ['key', 'config_type']
def has_add_permission(self, request, obj):
"""The machine settings should not be meddled with manually."""
- return True # TODO: change back
+ return False
@admin.register(models.MachineConfig)
class MachineConfigAdmin(admin.ModelAdmin):
"""Custom admin with restricted id fields."""
- form = MachineConfigAdminForm
- list_filter = ["active"]
- list_display = ["name", "machine_type", "driver", "initialized", "active", "no_errors", "get_machine_status"]
- readonly_fields = ["initialized", "is_driver_available", "get_admin_errors", "get_machine_status"]
+ list_filter = ['active']
+ list_display = [
+ 'name',
+ 'machine_type',
+ 'driver',
+ 'initialized',
+ 'active',
+ 'no_errors',
+ 'get_machine_status',
+ ]
+ readonly_fields = [
+ 'initialized',
+ 'is_driver_available',
+ 'get_admin_errors',
+ 'get_machine_status',
+ ]
inlines = [MachineSettingInline]
def get_readonly_fields(self, request, obj):
# if update, don't allow changes on machine_type and driver
if obj is not None:
- return ["machine_type", "driver", *self.readonly_fields]
+ return ['machine_type', 'driver', *self.readonly_fields]
return self.readonly_fields
-
- def get_inline_formsets(self, request, formsets, inline_instances, obj):
- formsets = super().get_inline_formsets(request, formsets, inline_instances, obj)
-
- if getattr(obj, 'machine', None) is not None:
- machine_settings = getattr(obj.machine, "MACHINE_SETTINGS", {})
- driver_settings = getattr(obj.machine.driver, "MACHINE_SETTINGS", {})
- settings = [(s, models.MachineSetting.ConfigType.MACHINE) for s in machine_settings] + [(s, models.MachineSetting.ConfigType.DRIVER) for s in driver_settings]
- for form, (setting, typ) in zip(formsets[0].forms, settings):
- if form.fields["key"].initial is None:
- form.fields["key"].initial = setting
- if form.fields["config_type"].initial is None:
- form.fields["config_type"].initial = typ
-
- return formsets
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index 20f47babe29..79c3108df44 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -6,31 +6,31 @@
class BaseLabelPrintingDriver(BaseDriver):
"""Base label printing driver."""
- machine_type = "label_printer"
+ machine_type = 'label_printer'
def print_label(self):
"""This function must be overridden."""
- raise NotImplementedError("The `print_label` function must be overridden!")
+ raise NotImplementedError('The `print_label` function must be overridden!')
def print_labels(self):
"""This function must be overridden."""
- raise NotImplementedError("The `print_labels` function must be overridden!")
+ raise NotImplementedError('The `print_labels` function must be overridden!')
requires_override = [print_label]
class LabelPrintingMachineType(BaseMachineType):
- SLUG = "label_printer"
- NAME = _("Label Printer")
- DESCRIPTION = _("Device used to print labels")
+ SLUG = 'label_printer'
+ NAME = _('Label Printer')
+ DESCRIPTION = _('Directly print labels for various items.')
base_driver = BaseLabelPrintingDriver
class LabelPrinterStatus(MachineStatus):
- CONNECTED = 100, _("Connected"), "success"
- PRINTING = 101, _("Printing"), "primary"
- PAPER_MISSING = 301, _("Paper missing"), "warning"
- DISCONNECTED = 400, _("Disconnected"), "danger"
+ CONNECTED = 100, _('Connected'), 'success'
+ PRINTING = 101, _('Printing'), 'primary'
+ PAPER_MISSING = 301, _('Paper missing'), 'warning'
+ DISCONNECTED = 400, _('Disconnected'), 'danger'
MACHINE_STATUS = LabelPrinterStatus
From 1c227f8d235624f553d8e171b3bc40ec4529d1f5 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sat, 27 Jan 2024 10:32:16 +0000
Subject: [PATCH 43/86] Added required field to settings item
---
InvenTree/common/serializers.py | 3 +++
src/frontend/src/components/settings/SettingItem.tsx | 5 ++++-
src/frontend/src/states/states.tsx | 1 +
3 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py
index 8b6dcb70c26..572b96d9974 100644
--- a/InvenTree/common/serializers.py
+++ b/InvenTree/common/serializers.py
@@ -59,6 +59,8 @@ class SettingsSerializer(InvenTreeModelSerializer):
units = serializers.CharField(read_only=True)
+ required = serializers.BooleanField(read_only=True)
+
def get_choices(self, obj):
"""Returns the choices available for a given item."""
results = []
@@ -148,6 +150,7 @@ class CustomMeta:
'model_name',
'api_url',
'typ',
+ 'required',
]
# set Meta class
diff --git a/src/frontend/src/components/settings/SettingItem.tsx b/src/frontend/src/components/settings/SettingItem.tsx
index 4fce3cc1458..28deffc3a10 100644
--- a/src/frontend/src/components/settings/SettingItem.tsx
+++ b/src/frontend/src/components/settings/SettingItem.tsx
@@ -174,7 +174,10 @@ export function SettingItem({
- {setting.name}
+
+ {setting.name}
+ {setting.required ? ' *' : ''}
+
{setting.description}
diff --git a/src/frontend/src/states/states.tsx b/src/frontend/src/states/states.tsx
index 27db2fc39aa..2ac57b8291a 100644
--- a/src/frontend/src/states/states.tsx
+++ b/src/frontend/src/states/states.tsx
@@ -73,6 +73,7 @@ export interface Setting {
typ: SettingTyp;
plugin?: string;
method?: string;
+ required?: boolean;
}
export interface SettingChoice {
From 0e141c0103e057d4c265d0e1a1278c4e5329c782 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sat, 27 Jan 2024 15:13:55 +0000
Subject: [PATCH 44/86] Added machine restart function
---
InvenTree/machine/api.py | 21 ++++
InvenTree/machine/machine_type.py | 117 ++++++++++++++----
.../machine_types/LabelPrintingMachineType.py | 5 +-
InvenTree/machine/models.py | 87 +++++++------
InvenTree/machine/registry.py | 59 ++++++---
InvenTree/machine/serializers.py | 14 +++
.../src/components/items/ActionDropdown.tsx | 60 +++++----
.../src/components/settings/SettingItem.tsx | 16 ++-
.../src/components/settings/SettingList.tsx | 11 +-
.../tables/machine/MachineListTable.tsx | 49 +++++++-
src/frontend/src/enums/ApiEndpoints.tsx | 1 +
src/frontend/src/states/ApiState.tsx | 2 +
12 files changed, 327 insertions(+), 115 deletions(-)
diff --git a/InvenTree/machine/api.py b/InvenTree/machine/api.py
index 72c2e9c3559..d6842ffeae5 100644
--- a/InvenTree/machine/api.py
+++ b/InvenTree/machine/api.py
@@ -136,6 +136,23 @@ def get_object(self):
)
+class MachineRestart(APIView):
+ """Endpoint for performing a machine restart.
+
+ - POST: restart machine
+ """
+
+ permission_classes = [permissions.IsAuthenticated]
+
+ @extend_schema(responses={200: MachineSerializers.MachineRestartSerializer()})
+ def post(self, request, pk):
+ machine = get_machine(pk)
+ registry.restart_machine(machine)
+
+ result = MachineSerializers.MachineRestartSerializer({'ok': True}).data
+ return Response(result)
+
+
class MachineTypesList(APIView):
"""List API Endpoint for all discovered machine types.
@@ -209,6 +226,7 @@ def get(self, request):
path(
'/',
include([
+ # settings
path(
'settings/',
include([
@@ -220,6 +238,9 @@ def get(self, request):
path('', MachineSettingList.as_view(), name='api-machine-settings'),
]),
),
+ # restart
+ path('restart/', MachineRestart.as_view(), name='api-machine-restart'),
+ # detail
path('', MachineDetail.as_view(), name='api-machine-detail'),
]),
),
diff --git a/InvenTree/machine/machine_type.py b/InvenTree/machine/machine_type.py
index 94d04c792aa..332b0141a98 100644
--- a/InvenTree/machine/machine_type.py
+++ b/InvenTree/machine/machine_type.py
@@ -8,6 +8,7 @@
from common.models import SettingsKeyType
from machine.models import MachineConfig
else: # pragma: no cover
+
class MachineConfig:
pass
@@ -34,6 +35,7 @@ class MachineStatus(StatusCode):
4XX - Something wrong with the driver (e.g. cannot connect to the machine)
5XX - Unknown issues
"""
+
pass
@@ -56,9 +58,9 @@ class BaseDriver(ClassValidationMixin, ClassProviderMixin):
machine_type: str
- required_attributes = ["SLUG", "NAME", "DESCRIPTION", "machine_type"]
+ required_attributes = ['SLUG', 'NAME', 'DESCRIPTION', 'machine_type']
- def init_machine(self, machine: "BaseMachineType"):
+ def init_machine(self, machine: 'BaseMachineType'):
"""This method gets called for each active machine using that driver while initialization.
If this function raises an Exception, it gets added to the machine.errors
@@ -69,11 +71,13 @@ def init_machine(self, machine: "BaseMachineType"):
"""
pass
- def update_machine(self, old_machine_state: Dict[str, Any], machine: "BaseMachineType"):
+ def update_machine(
+ self, old_machine_state: Dict[str, Any], machine: 'BaseMachineType'
+ ):
"""This method gets called for each update of a machine
- TODO: this function gets called even the settings are not stored yet when edited through the admin dashboard
- TODO: test also if API is done, that this function gets called for settings changes
+ Note:
+ machine.restart_required can be set to True here if the machine needs a manual restart to apply the changes
Arguments:
old_machine_state: Dict holding the old machine state before update
@@ -81,6 +85,17 @@ def update_machine(self, old_machine_state: Dict[str, Any], machine: "BaseMachin
"""
pass
+ def restart_machine(self, machine: 'BaseMachineType'):
+ """This method gets called on manual machine restart e.g. by using the restart machine action in the Admin Center.
+
+ Note:
+ machine.restart_required gets set to False again
+
+ Arguments:
+ machine: Machine instance
+ """
+ pass
+
def get_machines(self, **kwargs):
"""Return all machines using this driver. (By default only initialized machines)"""
from machine import registry
@@ -116,7 +131,14 @@ class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
default_machine_status: MachineStatus
# used by the ClassValidationMixin
- required_attributes = ["SLUG", "NAME", "DESCRIPTION", "base_driver", "MACHINE_STATUS", "default_machine_status"]
+ required_attributes = [
+ 'SLUG',
+ 'NAME',
+ 'DESCRIPTION',
+ 'base_driver',
+ 'MACHINE_STATUS',
+ 'default_machine_status',
+ ]
def __init__(self, machine_config: MachineConfig) -> None:
from machine import registry
@@ -126,7 +148,7 @@ def __init__(self, machine_config: MachineConfig) -> None:
self.initialized = False
self.status = self.default_machine_status
- self.status_text = ""
+ self.status_text = ''
self.pk = machine_config.pk
self.driver = registry.get_driver_instance(machine_config.driver)
@@ -134,29 +156,40 @@ def __init__(self, machine_config: MachineConfig) -> None:
if not self.driver:
self.errors.append(f"Driver '{machine_config.driver}' not found")
if self.driver and not isinstance(self.driver, self.base_driver):
- self.errors.append(f"'{self.driver.NAME}' is incompatible with machine type '{self.NAME}'")
-
- self.machine_settings: Dict[str, SettingsKeyType] = getattr(self, "MACHINE_SETTINGS", {})
- self.driver_settings: Dict[str, SettingsKeyType] = getattr(self.driver, "MACHINE_SETTINGS", {})
-
- self.setting_types: List[Tuple[Dict[str, SettingsKeyType], MachineSetting.ConfigType]] = [
+ self.errors.append(
+ f"'{self.driver.NAME}' is incompatible with machine type '{self.NAME}'"
+ )
+
+ self.machine_settings: Dict[str, SettingsKeyType] = getattr(
+ self, 'MACHINE_SETTINGS', {}
+ )
+ self.driver_settings: Dict[str, SettingsKeyType] = getattr(
+ self.driver, 'MACHINE_SETTINGS', {}
+ )
+
+ self.setting_types: List[
+ Tuple[Dict[str, SettingsKeyType], MachineSetting.ConfigType]
+ ] = [
(self.machine_settings, MachineSetting.ConfigType.MACHINE),
(self.driver_settings, MachineSetting.ConfigType.DRIVER),
]
+ self.restart_required = False
+
if len(self.errors) > 0:
return
# TODO: add further init stuff here
def __str__(self):
- return f"{self.name}"
+ return f'{self.name}'
# --- properties
@property
def machine_config(self):
# always fetch the machine_config if needed to ensure we get the newest reference
from .models import MachineConfig
+
return MachineConfig.objects.get(pk=self.pk)
@property
@@ -179,7 +212,9 @@ def initialize(self):
error_parts = []
for config_type, missing in missing_settings.items():
if len(missing) > 0:
- error_parts.append(f"{config_type.name} settings: " + ", ".join(missing))
+ error_parts.append(
+ f'{config_type.name} settings: ' + ', '.join(missing)
+ )
self.errors.append(f"Missing {' and '.join(error_parts)}")
return
@@ -189,8 +224,29 @@ def initialize(self):
except Exception as e:
self.errors.append(e)
+ def update(self, old_state: dict[str, Any]):
+ """Machine update function, gets called if the machine itself changes or their settings."""
+ if self.driver is None:
+ return
+
+ try:
+ self.driver.update_machine(old_state, self)
+ except Exception as e:
+ self.errors.append(e)
+
+ def restart(self):
+ """Machine restart function, can be used to manually restart the machine from the admin ui."""
+ if self.driver is None:
+ return
+
+ try:
+ self.restart_required = False
+ self.driver.restart_machine(self)
+ except Exception as e:
+ self.errors.append(e)
+
# --- helper functions
- def get_setting(self, key, config_type_str: Literal["M", "D"], cache=False):
+ def get_setting(self, key, config_type_str: Literal['M', 'D'], cache=False):
"""Return the 'value' of the setting associated with this machine.
Arguments:
@@ -201,9 +257,14 @@ def get_setting(self, key, config_type_str: Literal["M", "D"], cache=False):
from machine.models import MachineSetting
config_type = MachineSetting.get_config_type(config_type_str)
- return MachineSetting.get_setting(key, machine_config=self.machine_config, config_type=config_type, cache=cache)
-
- def set_setting(self, key, config_type_str: Literal["M", "D"], value):
+ return MachineSetting.get_setting(
+ key,
+ machine_config=self.machine_config,
+ config_type=config_type,
+ cache=cache,
+ )
+
+ def set_setting(self, key, config_type_str: Literal['M', 'D'], value):
"""Set plugin setting value by key.
Arguments:
@@ -214,7 +275,13 @@ def set_setting(self, key, config_type_str: Literal["M", "D"], value):
from machine.models import MachineSetting
config_type = MachineSetting.get_config_type(config_type_str)
- MachineSetting.set_setting(key, value, None, machine_config=self.machine_config, config_type=config_type)
+ MachineSetting.set_setting(
+ key,
+ value,
+ None,
+ machine_config=self.machine_config,
+ config_type=config_type,
+ )
def check_settings(self):
"""Check if all required settings for this machine are defined.
@@ -227,10 +294,16 @@ def check_settings(self):
missing_settings: Dict[MachineSetting.ConfigType, List[str]] = {}
for settings, config_type in self.setting_types:
- is_valid, missing = MachineSetting.check_all_settings(settings_definition=settings, machine_config=self.machine_config, config_type=config_type)
+ is_valid, missing = MachineSetting.check_all_settings(
+ settings_definition=settings,
+ machine_config=self.machine_config,
+ config_type=config_type,
+ )
missing_settings[config_type] = missing
- return all(len(missing) == 0 for missing in missing_settings.values()), missing_settings
+ return all(
+ len(missing) == 0 for missing in missing_settings.values()
+ ), missing_settings
def set_status(self, status: MachineStatus):
"""Set the machine status code. There are predefined ones for each MachineType.
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index 79c3108df44..e1160e01c8b 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -28,8 +28,9 @@ class LabelPrintingMachineType(BaseMachineType):
class LabelPrinterStatus(MachineStatus):
CONNECTED = 100, _('Connected'), 'success'
- PRINTING = 101, _('Printing'), 'primary'
- PAPER_MISSING = 301, _('Paper missing'), 'warning'
+ STANDBY = 101, _('Standby'), 'success'
+ PRINTING = 110, _('Printing'), 'primary'
+ LABEL_SPOOL_EMPTY = 301, _('Label spool empty'), 'warning'
DISCONNECTED = 400, _('Disconnected'), 'danger'
MACHINE_STATUS = LabelPrinterStatus
diff --git a/InvenTree/machine/models.py b/InvenTree/machine/models.py
index a0293581411..a33e1c9b35a 100755
--- a/InvenTree/machine/models.py
+++ b/InvenTree/machine/models.py
@@ -19,37 +19,37 @@ class MachineConfig(models.Model):
name = models.CharField(
unique=True,
max_length=255,
- verbose_name=_("Name"),
- help_text=_("Name of machine")
+ verbose_name=_('Name'),
+ help_text=_('Name of machine'),
)
machine_type = models.CharField(
- max_length=255,
- verbose_name=_("Machine Type"),
- help_text=_("Type of machine"),
+ max_length=255, verbose_name=_('Machine Type'), help_text=_('Type of machine')
)
driver = models.CharField(
max_length=255,
- verbose_name=_("Driver"),
- help_text=_("Driver used for the machine")
+ verbose_name=_('Driver'),
+ help_text=_('Driver used for the machine'),
)
active = models.BooleanField(
- default=True,
- verbose_name=_("Active"),
- help_text=_("Machines can be disabled")
+ default=True, verbose_name=_('Active'), help_text=_('Machines can be disabled')
)
def __str__(self) -> str:
"""String representation of a machine."""
- return f"{self.name}"
+ return f'{self.name}'
def save(self, *args, **kwargs) -> None:
created = self._state.adding
old_machine = None
- if not created and self.pk and (old_machine := MachineConfig.objects.get(pk=self.pk)):
+ if (
+ not created
+ and self.pk
+ and (old_machine := MachineConfig.objects.get(pk=self.pk))
+ ):
old_machine = old_machine.to_dict()
super().save(*args, **kwargs)
@@ -72,7 +72,10 @@ def delete(self, *args, **kwargs):
def to_dict(self):
machine = {f.name: f.value_to_string(self) for f in self._meta.fields}
- machine['settings'] = {setting.key: (setting.value, setting.config_type) for setting in MachineSetting.objects.filter(machine_config=self)}
+ machine['settings'] = {
+ (setting.config_type, setting.key): setting.value
+ for setting in MachineSetting.objects.filter(machine_config=self)
+ }
return machine
@property
@@ -81,28 +84,30 @@ def machine(self):
@property
def errors(self):
- return getattr(self.machine, "errors", [])
+ return getattr(self.machine, 'errors', [])
- @admin.display(boolean=True, description=_("Driver available"))
+ @admin.display(boolean=True, description=_('Driver available'))
def is_driver_available(self) -> bool:
"""Status if driver for machine is available"""
return self.machine is not None and self.machine.driver is not None
- @admin.display(boolean=True, description=_("No errors"))
+ @admin.display(boolean=True, description=_('No errors'))
def no_errors(self) -> bool:
"""Status if machine has errors"""
return len(self.errors) == 0
- @admin.display(boolean=True, description=_("Initialized"))
+ @admin.display(boolean=True, description=_('Initialized'))
def initialized(self) -> bool:
"""Status if machine is initialized"""
- return getattr(self.machine, "initialized", False)
+ return getattr(self.machine, 'initialized', False)
- @admin.display(description=_("Errors"))
+ @admin.display(description=_('Errors'))
def get_admin_errors(self):
- return format_html_join(mark_safe("
"), "{}", ((str(error),) for error in self.errors)) or mark_safe(f"{_('No errors')}")
+ return format_html_join(
+ mark_safe('
'), '{}', ((str(error),) for error in self.errors)
+ ) or mark_safe(f"{_('No errors')}")
- @admin.display(description=_("Machine status"))
+ @admin.display(description=_('Machine status'))
def get_machine_status(self):
if self.machine is None:
return None
@@ -110,7 +115,7 @@ def get_machine_status(self):
out = mark_safe(self.machine.status.render(self.machine.status))
if self.machine.status_text:
- out += escape(f" ({self.machine.status_text})")
+ out += escape(f' ({self.machine.status_text})')
return out
@@ -118,37 +123,41 @@ def get_machine_status(self):
class MachineSetting(common.models.BaseInvenTreeSetting):
"""This models represents settings for individual machines."""
- typ = "machine_config"
- extra_unique_fields = ["machine_config", "config_type"]
+ typ = 'machine_config'
+ extra_unique_fields = ['machine_config', 'config_type']
class Meta:
"""Meta for MachineSetting."""
- unique_together = [
- ("machine_config", "config_type", "key")
- ]
+
+ unique_together = [('machine_config', 'config_type', 'key')]
class ConfigType(models.TextChoices):
- MACHINE = "M", _("Machine")
- DRIVER = "D", _("Driver")
+ MACHINE = 'M', _('Machine')
+ DRIVER = 'D', _('Driver')
machine_config = models.ForeignKey(
MachineConfig,
- related_name="settings",
- verbose_name=_("Machine Config"),
- on_delete=models.CASCADE
+ related_name='settings',
+ verbose_name=_('Machine Config'),
+ on_delete=models.CASCADE,
)
config_type = models.CharField(
- verbose_name=_("Config type"),
- max_length=1,
- choices=ConfigType.choices,
+ verbose_name=_('Config type'), max_length=1, choices=ConfigType.choices
)
+ def save(self, *args, **kwargs) -> None:
+ old_machine = self.machine_config.to_dict()
+
+ super().save(*args, **kwargs)
+
+ registry.update_machine(old_machine, self.machine_config)
+
@classmethod
- def get_config_type(cls, config_type_str: Literal["M", "D"]):
- if config_type_str == "M":
+ def get_config_type(cls, config_type_str: Literal['M', 'D']):
+ if config_type_str == 'M':
return cls.ConfigType.MACHINE
- elif config_type_str == "D":
+ elif config_type_str == 'D':
return cls.ConfigType.DRIVER
@classmethod
@@ -166,7 +175,7 @@ def get_setting_definition(cls, key, **kwargs):
if 'settings' not in kwargs:
machine_config: MachineConfig = kwargs.pop('machine_config', None)
if machine_config and machine_config.machine:
- config_type = kwargs.get("config_type", None)
+ config_type = kwargs.get('config_type', None)
if config_type == cls.ConfigType.DRIVER:
kwargs['settings'] = machine_config.machine.driver_settings
elif config_type == cls.ConfigType.MACHINE:
diff --git a/InvenTree/machine/registry.py b/InvenTree/machine/registry.py
index fcd3454224b..cd725f16996 100644
--- a/InvenTree/machine/registry.py
+++ b/InvenTree/machine/registry.py
@@ -7,13 +7,12 @@
logger = logging.getLogger('inventree')
-class MachinesRegistry:
+class MachineRegistry:
def __init__(self) -> None:
"""Initialize machine registry
Set up all needed references for internal and external states.
"""
-
self.machine_types: Dict[str, Type[BaseMachineType]] = {}
self.drivers: Dict[str, Type[BaseDriver]] = {}
self.driver_instances: Dict[str, BaseDriver] = {}
@@ -23,7 +22,7 @@ def __init__(self) -> None:
self.errors = []
def initialize(self):
- print("INITIALIZE") # TODO: remove debug statement
+ print('INITIALIZE') # TODO: remove debug statement
self.discover_machine_types()
self.discover_drivers()
self.load_machines()
@@ -31,12 +30,14 @@ def initialize(self):
def discover_machine_types(self):
import InvenTree.helpers
- logger.debug("Collecting machine types")
+ logger.debug('Collecting machine types')
machine_types: Dict[str, Type[BaseMachineType]] = {}
base_drivers: List[Type[BaseDriver]] = []
- discovered_machine_types: Set[Type[BaseMachineType]] = InvenTree.helpers.inheritors(BaseMachineType)
+ discovered_machine_types: Set[Type[BaseMachineType]] = (
+ InvenTree.helpers.inheritors(BaseMachineType)
+ )
for machine_type in discovered_machine_types:
try:
machine_type.validate()
@@ -45,7 +46,9 @@ def discover_machine_types(self):
continue
if machine_type.SLUG in machine_types:
- self.errors.append(ValueError(f"Cannot re-register machine type '{machine_type.SLUG}'"))
+ self.errors.append(
+ ValueError(f"Cannot re-register machine type '{machine_type.SLUG}'")
+ )
continue
machine_types[machine_type.SLUG] = machine_type
@@ -54,16 +57,18 @@ def discover_machine_types(self):
self.machine_types = machine_types
self.base_drivers = base_drivers
- logger.debug(f"Found {len(self.machine_types.keys())} machine types")
+ logger.debug(f'Found {len(self.machine_types.keys())} machine types')
def discover_drivers(self):
import InvenTree.helpers
- logger.debug("Collecting machine drivers")
+ logger.debug('Collecting machine drivers')
drivers: Dict[str, Type[BaseDriver]] = {}
- discovered_drivers: Set[Type[BaseDriver]] = InvenTree.helpers.inheritors(BaseDriver)
+ discovered_drivers: Set[Type[BaseDriver]] = InvenTree.helpers.inheritors(
+ BaseDriver
+ )
for driver in discovered_drivers:
# skip discovered drivers that define a base driver for a machine type
if driver in self.base_drivers:
@@ -76,14 +81,16 @@ def discover_drivers(self):
continue
if driver.SLUG in drivers:
- self.errors.append(ValueError(f"Cannot re-register driver '{driver.SLUG}'"))
+ self.errors.append(
+ ValueError(f"Cannot re-register driver '{driver.SLUG}'")
+ )
continue
drivers[driver.SLUG] = driver
self.drivers = drivers
- logger.debug(f"Found {len(self.drivers.keys())} machine drivers")
+ logger.debug(f'Found {len(self.drivers.keys())} machine drivers')
def get_driver_instance(self, slug: str):
if slug not in self.driver_instances:
@@ -110,7 +117,9 @@ def load_machines(self):
def add_machine(self, machine_config, initialize=True):
machine_type = self.machine_types.get(machine_config.machine_type, None)
if machine_type is None:
- self.errors.append(f"Machine type '{machine_config.machine_type}' not found")
+ self.errors.append(
+ f"Machine type '{machine_config.machine_type}' not found"
+ )
return
machine: BaseMachineType = machine_type(machine_config)
@@ -120,8 +129,11 @@ def add_machine(self, machine_config, initialize=True):
machine.initialize()
def update_machine(self, old_machine_state, machine_config):
- if (machine := machine_config.machine) and machine.driver:
- machine.driver.update_machine(old_machine_state, machine)
+ if machine := machine_config.machine:
+ machine.update(old_machine_state)
+
+ def restart_machine(self, machine):
+ machine.restart()
def remove_machine(self, machine: BaseMachineType):
self.machines.pop(str(machine.pk), None)
@@ -137,7 +149,14 @@ def get_machines(self, **kwargs):
active: (bool)
base_driver: base driver (class)
"""
- allowed_fields = ["name", "machine_type", "driver", "initialized", "active", "base_driver"]
+ allowed_fields = [
+ 'name',
+ 'machine_type',
+ 'driver',
+ 'initialized',
+ 'active',
+ 'base_driver',
+ ]
kwargs = {'initialized': True, **kwargs}
@@ -147,12 +166,14 @@ def filter_machine(machine: BaseMachineType):
continue
# check if current driver is subclass from base_driver
- if key == "base_driver":
- if machine.driver and not issubclass(machine.driver.__class__, value):
+ if key == 'base_driver':
+ if machine.driver and not issubclass(
+ machine.driver.__class__, value
+ ):
return False
# check if current machine is subclass from machine_type
- elif key == "machine_type":
+ elif key == 'machine_type':
if issubclass(machine.__class__, value):
return False
@@ -169,4 +190,4 @@ def get_machine(self, pk: Union[str, UUID]):
return self.machines.get(str(pk), None)
-registry: MachinesRegistry = MachinesRegistry()
+registry: MachineRegistry = MachineRegistry()
diff --git a/InvenTree/machine/serializers.py b/InvenTree/machine/serializers.py
index 52f3293504f..5a763439c66 100644
--- a/InvenTree/machine/serializers.py
+++ b/InvenTree/machine/serializers.py
@@ -26,6 +26,7 @@ class Meta:
'status_text',
'machine_errors',
'is_driver_available',
+ 'restart_required',
]
read_only_fields = ['machine_type', 'driver']
@@ -36,6 +37,7 @@ class Meta:
status_text = serializers.SerializerMethodField('get_status_text')
machine_errors = serializers.SerializerMethodField('get_errors')
is_driver_available = serializers.SerializerMethodField('get_is_driver_available')
+ restart_required = serializers.SerializerMethodField('get_restart_required')
def get_initialized(self, obj: MachineConfig) -> bool:
return getattr(obj.machine, 'initialized', False)
@@ -60,6 +62,9 @@ def get_errors(self, obj: MachineConfig) -> List[str]:
def get_is_driver_available(self, obj: MachineConfig) -> bool:
return obj.is_driver_available()
+ def get_restart_required(self, obj: MachineConfig) -> bool:
+ return getattr(obj.machine, 'restart_required', False)
+
class MachineConfigCreateSerializer(MachineConfigSerializer):
"""Serializer for creating a MachineConfig."""
@@ -168,3 +173,12 @@ class Meta:
fields = ['registry_errors']
registry_errors = serializers.ListField(child=MachineRegistryErrorSerializer())
+
+
+class MachineRestartSerializer(serializers.Serializer):
+ """Serializer for the machine restart response."""
+
+ class Meta:
+ fields = ['ok']
+
+ ok = serializers.BooleanField()
diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx
index 32854f5e537..f26408c6e1e 100644
--- a/src/frontend/src/components/items/ActionDropdown.tsx
+++ b/src/frontend/src/components/items/ActionDropdown.tsx
@@ -1,5 +1,11 @@
import { t } from '@lingui/macro';
-import { ActionIcon, Menu, Tooltip } from '@mantine/core';
+import {
+ ActionIcon,
+ Indicator,
+ IndicatorProps,
+ Menu,
+ Tooltip
+} from '@mantine/core';
import {
IconCopy,
IconEdit,
@@ -18,6 +24,7 @@ export type ActionDropdownItem = {
tooltip?: string;
disabled?: boolean;
onClick?: () => void;
+ indicator?: Omit;
};
/**
@@ -37,34 +44,41 @@ export function ActionDropdown({
const hasActions = useMemo(() => {
return actions.some((action) => !action.disabled);
}, [actions]);
+ const indicatorProps = useMemo(() => {
+ return actions.find((action) => action.indicator);
+ }, [actions]);
return hasActions ? (
-
+
);
diff --git a/src/frontend/src/components/settings/SettingList.tsx b/src/frontend/src/components/settings/SettingList.tsx
index 8e0f1c2dc98..b2691a519bf 100644
--- a/src/frontend/src/components/settings/SettingList.tsx
+++ b/src/frontend/src/components/settings/SettingList.tsx
@@ -17,10 +17,12 @@ import { SettingItem } from './SettingItem';
*/
export function SettingList({
settingsState,
- keys
+ keys,
+ onChange
}: {
settingsState: SettingsStateProps;
keys?: string[];
+ onChange?: () => void;
}) {
useEffect(() => {
settingsState.fetchSettings();
@@ -46,6 +48,7 @@ export function SettingList({
settingsState={settingsState}
setting={setting}
shaded={i % 2 === 0}
+ onChange={onChange}
/>
) : (
@@ -88,10 +91,12 @@ export function PluginSettingList({ pluginPk }: { pluginPk: string }) {
export function MachineSettingList({
machinePk,
- configType
+ configType,
+ onChange
}: {
machinePk: string;
configType: 'M' | 'D';
+ onChange?: () => void;
}) {
const machineSettingsStore = useRef(
createMachineSettingsState({
@@ -101,5 +106,5 @@ export function MachineSettingList({
).current;
const machineSettings = useStore(machineSettingsStore);
- return ;
+ return ;
}
diff --git a/src/frontend/src/components/tables/machine/MachineListTable.tsx b/src/frontend/src/components/tables/machine/MachineListTable.tsx
index dfb9586986f..e81752a58ae 100644
--- a/src/frontend/src/components/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/components/tables/machine/MachineListTable.tsx
@@ -15,7 +15,8 @@ import {
Text,
Title
} from '@mantine/core';
-import { IconDots, IconRefresh } from '@tabler/icons-react';
+import { notifications } from '@mantine/notifications';
+import { IconCheck, IconDots, IconRefresh } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -59,6 +60,7 @@ interface MachineI {
status_text: string;
machine_errors: string[];
is_driver_available: boolean;
+ restart_required: boolean;
}
function MachineStatusIndicator({ machine }: { machine: MachineI }) {
@@ -174,6 +176,24 @@ function MachineDrawer({
[machine?.driver, machineDrivers]
);
+ const restartMachine = useCallback(
+ (machinePk: string) => {
+ api
+ .post(
+ apiUrl(ApiPaths.machine_restart, undefined, { machine: machinePk })
+ )
+ .then(() => {
+ refetch();
+ notifications.show({
+ message: t`Machine restarted`,
+ color: 'green',
+ icon:
+ });
+ });
+ },
+ [refetch]
+ );
+
return (
@@ -217,7 +237,20 @@ function MachineDrawer({
onFormSuccess: () => navigate(-1)
});
}
- })
+ }),
+ {
+ icon: ,
+ name: t`Restart`,
+ tooltip:
+ t`Restart machine` +
+ (machine?.restart_required
+ ? ' (' + t`manual restart required` + ')'
+ : ''),
+ indicator: machine?.restart_required
+ ? { color: 'red' }
+ : undefined,
+ onClick: () => machine && restartMachine(machine?.pk)
+ }
]}
/>
@@ -309,14 +342,22 @@ function MachineDrawer({
Machine Settings
-
+
Driver Settings
-
+
>
)}
diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx
index 03177aaff32..b2a322fecff 100644
--- a/src/frontend/src/enums/ApiEndpoints.tsx
+++ b/src/frontend/src/enums/ApiEndpoints.tsx
@@ -107,6 +107,7 @@ export enum ApiPaths {
machine_driver_list = 'api-machine-drivers',
machine_registry_status = 'api-machine-registry-status',
machine_list = 'api-machine-list',
+ machine_restart = 'api-machine-restart',
machine_setting_list = 'api-machine-settings',
machine_setting_detail = 'api-machine-settings-detail'
}
diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx
index 4fc2b3e8827..52da8fb79ae 100644
--- a/src/frontend/src/states/ApiState.tsx
+++ b/src/frontend/src/states/ApiState.tsx
@@ -229,6 +229,8 @@ export function apiEndpoint(path: ApiPaths): string {
return 'machine/status/';
case ApiPaths.machine_list:
return 'machine/';
+ case ApiPaths.machine_restart:
+ return 'machine/:machine/restart/';
case ApiPaths.machine_setting_list:
return 'machine/:machine/settings/';
case ApiPaths.machine_setting_detail:
From b454a489cff53c9476d9b3fa1f79cb7345e5ad8b Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sat, 27 Jan 2024 15:55:06 +0000
Subject: [PATCH 45/86] Added restart requird badge to machine table/drawer
---
.../tables/machine/MachineListTable.tsx | 121 ++++++++++--------
1 file changed, 69 insertions(+), 52 deletions(-)
diff --git a/src/frontend/src/components/tables/machine/MachineListTable.tsx b/src/frontend/src/components/tables/machine/MachineListTable.tsx
index e81752a58ae..3ccd93ddb1b 100644
--- a/src/frontend/src/components/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/components/tables/machine/MachineListTable.tsx
@@ -176,6 +176,11 @@ function MachineDrawer({
[machine?.driver, machineDrivers]
);
+ const refreshAll = useCallback(() => {
+ refetch();
+ refreshTable();
+ }, [refetch, refreshTable]);
+
const restartMachine = useCallback(
(machinePk: string) => {
api
@@ -183,7 +188,7 @@ function MachineDrawer({
apiUrl(ApiPaths.machine_restart, undefined, { machine: machinePk })
)
.then(() => {
- refetch();
+ refreshAll();
notifications.show({
message: t`Machine restarted`,
color: 'green',
@@ -191,7 +196,7 @@ function MachineDrawer({
});
});
},
- [refetch]
+ [refreshAll]
);
return (
@@ -204,55 +209,62 @@ function MachineDrawer({
{machine?.name}
- }
- actions={[
- EditItemAction({
- tooltip: t`Edit machine`,
- onClick: () => {
- openEditApiForm({
- title: t`Edit machine`,
- url: ApiPaths.machine_list,
- pk: machinePk,
- fields: {
- name: {},
- active: {}
- },
- onClose: () => refetch()
- });
- }
- }),
- DeleteItemAction({
- tooltip: t`Delete machine`,
- onClick: () => {
- openDeleteApiForm({
- title: t`Delete machine`,
- successMessage: t`Machine successfully deleted.`,
- url: ApiPaths.machine_list,
- pk: machinePk,
- preFormContent: (
- {t`Are you sure you want to remove the machine "${machine?.name}"?`}
- ),
- onFormSuccess: () => navigate(-1)
- });
+
+ {machine?.restart_required && (
+
+ Restart required
+
+ )}
+ }
+ actions={[
+ EditItemAction({
+ tooltip: t`Edit machine`,
+ onClick: () => {
+ openEditApiForm({
+ title: t`Edit machine`,
+ url: ApiPaths.machine_list,
+ pk: machinePk,
+ fields: {
+ name: {},
+ active: {}
+ },
+ onClose: () => refreshAll()
+ });
+ }
+ }),
+ DeleteItemAction({
+ tooltip: t`Delete machine`,
+ onClick: () => {
+ openDeleteApiForm({
+ title: t`Delete machine`,
+ successMessage: t`Machine successfully deleted.`,
+ url: ApiPaths.machine_list,
+ pk: machinePk,
+ preFormContent: (
+ {t`Are you sure you want to remove the machine "${machine?.name}"?`}
+ ),
+ onFormSuccess: () => navigate(-1)
+ });
+ }
+ }),
+ {
+ icon: ,
+ name: t`Restart`,
+ tooltip:
+ t`Restart machine` +
+ (machine?.restart_required
+ ? ' (' + t`manual restart required` + ')'
+ : ''),
+ indicator: machine?.restart_required
+ ? { color: 'red' }
+ : undefined,
+ onClick: () => machine && restartMachine(machine?.pk)
}
- }),
- {
- icon: ,
- name: t`Restart`,
- tooltip:
- t`Restart machine` +
- (machine?.restart_required
- ? ' (' + t`manual restart required` + ')'
- : ''),
- indicator: machine?.restart_required
- ? { color: 'red' }
- : undefined,
- onClick: () => machine && restartMachine(machine?.pk)
- }
- ]}
- />
+ ]}
+ />
+
@@ -345,7 +357,7 @@ function MachineDrawer({
@@ -356,7 +368,7 @@ function MachineDrawer({
>
@@ -385,6 +397,11 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
{record.name}
+ {record.restart_required && (
+
+ Restart required
+
+ )}
);
}
From d321d5ef1dc82a152b04fbde17e0509f4c8462f6 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sat, 27 Jan 2024 17:23:04 +0000
Subject: [PATCH 46/86] Added driver init function
---
InvenTree/machine/machine_type.py | 8 ++++++++
InvenTree/machine/registry.py | 4 ++++
2 files changed, 12 insertions(+)
diff --git a/InvenTree/machine/machine_type.py b/InvenTree/machine/machine_type.py
index 332b0141a98..b65418235e7 100644
--- a/InvenTree/machine/machine_type.py
+++ b/InvenTree/machine/machine_type.py
@@ -60,6 +60,14 @@ class BaseDriver(ClassValidationMixin, ClassProviderMixin):
required_attributes = ['SLUG', 'NAME', 'DESCRIPTION', 'machine_type']
+ def init_driver(self):
+ """This method gets called after all machines are created and can be used to initialize the driver.
+
+ After the driver is initialized, the self.init_machine function is
+ called for each machine associated with that driver.
+ """
+ pass
+
def init_machine(self, machine: 'BaseMachineType'):
"""This method gets called for each active machine using that driver while initialization.
diff --git a/InvenTree/machine/registry.py b/InvenTree/machine/registry.py
index cd725f16996..c5a8d38d0b6 100644
--- a/InvenTree/machine/registry.py
+++ b/InvenTree/machine/registry.py
@@ -109,6 +109,10 @@ def load_machines(self):
for machine_config in MachineConfig.objects.all():
self.add_machine(machine_config, initialize=False)
+ # initialize drivers
+ for driver in self.driver_instances.values():
+ driver.init_driver()
+
# initialize machines after all machine instances were created
for machine in self.machines.values():
if machine.active:
From fe2ba8e1435a6a15bd9f9f833389885f49bc85e7 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sun, 28 Jan 2024 10:23:17 +0000
Subject: [PATCH 47/86] handle error functions for machines and registry
---
InvenTree/machine/machine_type.py | 27 +++++++++++++--------------
InvenTree/machine/registry.py | 17 +++++++++--------
2 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/InvenTree/machine/machine_type.py b/InvenTree/machine/machine_type.py
index b65418235e7..e1b953c8615 100644
--- a/InvenTree/machine/machine_type.py
+++ b/InvenTree/machine/machine_type.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Any, Dict, List, Literal, Tuple, Type
+from typing import TYPE_CHECKING, Any, Dict, List, Literal, Tuple, Type, Union
from generic.states import StatusCode
from InvenTree.helpers_mixin import ClassProviderMixin, ClassValidationMixin
@@ -162,9 +162,9 @@ def __init__(self, machine_config: MachineConfig) -> None:
self.driver = registry.get_driver_instance(machine_config.driver)
if not self.driver:
- self.errors.append(f"Driver '{machine_config.driver}' not found")
+ self.handle_error(f"Driver '{machine_config.driver}' not found")
if self.driver and not isinstance(self.driver, self.base_driver):
- self.errors.append(
+ self.handle_error(
f"'{self.driver.NAME}' is incompatible with machine type '{self.NAME}'"
)
@@ -184,11 +184,6 @@ def __init__(self, machine_config: MachineConfig) -> None:
self.restart_required = False
- if len(self.errors) > 0:
- return
-
- # TODO: add further init stuff here
-
def __str__(self):
return f'{self.name}'
@@ -223,14 +218,14 @@ def initialize(self):
error_parts.append(
f'{config_type.name} settings: ' + ', '.join(missing)
)
- self.errors.append(f"Missing {' and '.join(error_parts)}")
+ self.handle_error(f"Missing {' and '.join(error_parts)}")
return
try:
self.driver.init_machine(self)
self.initialized = True
except Exception as e:
- self.errors.append(e)
+ self.handle_error(e)
def update(self, old_state: dict[str, Any]):
"""Machine update function, gets called if the machine itself changes or their settings."""
@@ -240,7 +235,7 @@ def update(self, old_state: dict[str, Any]):
try:
self.driver.update_machine(old_state, self)
except Exception as e:
- self.errors.append(e)
+ self.handle_error(e)
def restart(self):
"""Machine restart function, can be used to manually restart the machine from the admin ui."""
@@ -251,10 +246,14 @@ def restart(self):
self.restart_required = False
self.driver.restart_machine(self)
except Exception as e:
- self.errors.append(e)
+ self.handle_error(e)
# --- helper functions
- def get_setting(self, key, config_type_str: Literal['M', 'D'], cache=False):
+ def handle_error(self, error: Union[Exception, str]):
+ """Helper function for capturing errors with the machine."""
+ self.errors.append(error)
+
+ def get_setting(self, key: str, config_type_str: Literal['M', 'D'], cache=False):
"""Return the 'value' of the setting associated with this machine.
Arguments:
@@ -272,7 +271,7 @@ def get_setting(self, key, config_type_str: Literal['M', 'D'], cache=False):
cache=cache,
)
- def set_setting(self, key, config_type_str: Literal['M', 'D'], value):
+ def set_setting(self, key: str, config_type_str: Literal['M', 'D'], value):
"""Set plugin setting value by key.
Arguments:
diff --git a/InvenTree/machine/registry.py b/InvenTree/machine/registry.py
index c5a8d38d0b6..cad44fe4c20 100644
--- a/InvenTree/machine/registry.py
+++ b/InvenTree/machine/registry.py
@@ -21,8 +21,11 @@ def __init__(self) -> None:
self.base_drivers: List[Type[BaseDriver]] = []
self.errors = []
+ def handle_error(self, error: Union[Exception, str]):
+ """Helper function for capturing errors with the machine registry."""
+ self.errors.append(error)
+
def initialize(self):
- print('INITIALIZE') # TODO: remove debug statement
self.discover_machine_types()
self.discover_drivers()
self.load_machines()
@@ -42,11 +45,11 @@ def discover_machine_types(self):
try:
machine_type.validate()
except NotImplementedError as error:
- self.errors.append(error)
+ self.handle_error(error)
continue
if machine_type.SLUG in machine_types:
- self.errors.append(
+ self.handle_error(
ValueError(f"Cannot re-register machine type '{machine_type.SLUG}'")
)
continue
@@ -77,11 +80,11 @@ def discover_drivers(self):
try:
driver.validate()
except NotImplementedError as error:
- self.errors.append(error)
+ self.handle_error(error)
continue
if driver.SLUG in drivers:
- self.errors.append(
+ self.handle_error(
ValueError(f"Cannot re-register driver '{driver.SLUG}'")
)
continue
@@ -121,9 +124,7 @@ def load_machines(self):
def add_machine(self, machine_config, initialize=True):
machine_type = self.machine_types.get(machine_config.machine_type, None)
if machine_type is None:
- self.errors.append(
- f"Machine type '{machine_config.machine_type}' not found"
- )
+ self.handle_error(f"Machine type '{machine_config.machine_type}' not found")
return
machine: BaseMachineType = machine_type(machine_config)
From be0abc0a975440951d3899492b70ecebe83f3999 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sun, 28 Jan 2024 10:45:41 +0000
Subject: [PATCH 48/86] Added driver errors
---
InvenTree/machine/machine_type.py | 12 +++++++--
InvenTree/machine/registry.py | 2 +-
InvenTree/machine/serializers.py | 11 +++++++-
.../tables/machine/MachineTypeTable.tsx | 25 +++++++++++++++++++
4 files changed, 46 insertions(+), 4 deletions(-)
diff --git a/InvenTree/machine/machine_type.py b/InvenTree/machine/machine_type.py
index e1b953c8615..b9a90b08f1a 100644
--- a/InvenTree/machine/machine_type.py
+++ b/InvenTree/machine/machine_type.py
@@ -60,6 +60,11 @@ class BaseDriver(ClassValidationMixin, ClassProviderMixin):
required_attributes = ['SLUG', 'NAME', 'DESCRIPTION', 'machine_type']
+ def __init__(self) -> None:
+ super().__init__()
+
+ self.errors: list[Union[str, Exception]] = []
+
def init_driver(self):
"""This method gets called after all machines are created and can be used to initialize the driver.
@@ -110,6 +115,9 @@ def get_machines(self, **kwargs):
return registry.get_machines(driver=self, **kwargs)
+ def handle_error(self, error: Union[Exception, str]):
+ self.errors.append(error)
+
class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
"""Base class for machine types
@@ -152,11 +160,11 @@ def __init__(self, machine_config: MachineConfig) -> None:
from machine import registry
from machine.models import MachineSetting
- self.errors = []
+ self.errors: list[Union[str, Exception]] = []
self.initialized = False
self.status = self.default_machine_status
- self.status_text = ''
+ self.status_text: str = ''
self.pk = machine_config.pk
self.driver = registry.get_driver_instance(machine_config.driver)
diff --git a/InvenTree/machine/registry.py b/InvenTree/machine/registry.py
index cad44fe4c20..4b308e77d33 100644
--- a/InvenTree/machine/registry.py
+++ b/InvenTree/machine/registry.py
@@ -19,7 +19,7 @@ def __init__(self) -> None:
self.machines: Dict[str, BaseMachineType] = {}
self.base_drivers: List[Type[BaseDriver]] = []
- self.errors = []
+ self.errors: list[Union[str, Exception]] = []
def handle_error(self, error: Union[Exception, str]):
"""Helper function for capturing errors with the machine registry."""
diff --git a/InvenTree/machine/serializers.py b/InvenTree/machine/serializers.py
index 5a763439c66..ba5983ee1c9 100644
--- a/InvenTree/machine/serializers.py
+++ b/InvenTree/machine/serializers.py
@@ -4,6 +4,7 @@
from common.serializers import GenericReferencedSettingSerializer
from InvenTree.helpers_mixin import ClassProviderMixin
+from machine import registry
from machine.models import MachineConfig, MachineSetting
@@ -152,10 +153,18 @@ class MachineDriverSerializer(BaseMachineClassSerializer):
class Meta(BaseMachineClassSerializer.Meta):
"""Meta for a serializer."""
- fields = [*BaseMachineClassSerializer.Meta.fields, 'machine_type']
+ fields = [*BaseMachineClassSerializer.Meta.fields, 'machine_type', 'errors']
machine_type = serializers.SlugField(read_only=True)
+ driver_errors = serializers.SerializerMethodField('get_errors')
+
+ def get_errors(self, obj) -> List[str]:
+ driver_instance = registry.driver_instances.get(obj.SLUG, None)
+ if driver_instance is None:
+ return []
+ return [str(err) for err in driver_instance.errors]
+
class MachineRegistryErrorSerializer(serializers.Serializer):
"""Serializer for a machine registry error."""
diff --git a/src/frontend/src/components/tables/machine/MachineTypeTable.tsx b/src/frontend/src/components/tables/machine/MachineTypeTable.tsx
index dcb8aca6dc0..9449c0c1289 100644
--- a/src/frontend/src/components/tables/machine/MachineTypeTable.tsx
+++ b/src/frontend/src/components/tables/machine/MachineTypeTable.tsx
@@ -1,8 +1,11 @@
import { Trans, t } from '@lingui/macro';
import {
ActionIcon,
+ Badge,
Card,
+ Code,
Group,
+ List,
LoadingOverlay,
Stack,
Text,
@@ -39,6 +42,7 @@ export interface MachineDriverI {
provider_plugin: { slug: string; name: string; pk: number | null } | null;
is_builtin: boolean;
machine_type: string;
+ driver_errors: string[];
}
function MachineTypeDrawer({ machineTypeSlug }: { machineTypeSlug: string }) {
@@ -238,6 +242,27 @@ function MachineDriverDrawer({
value={machineDriver?.is_builtin}
type="boolean"
/>
+
+
+ Errors:
+
+ {machineDriver && machineDriver?.driver_errors.length > 0 ? (
+
+ {machineDriver.driver_errors.length}
+
+ ) : (
+
+ No errors reported
+
+ )}
+
+ {machineDriver?.driver_errors.map((error, i) => (
+
+ {error}
+
+ ))}
+
+
From 0424cff3bc3fa06c568db1ac154a6506015fa1d5 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sun, 28 Jan 2024 11:11:22 +0000
Subject: [PATCH 49/86] Added machine table to driver drawer
---
.../tables/machine/MachineListTable.tsx | 55 +++++++++++++------
.../tables/machine/MachineTypeTable.tsx | 19 ++++++-
2 files changed, 57 insertions(+), 17 deletions(-)
diff --git a/src/frontend/src/components/tables/machine/MachineListTable.tsx b/src/frontend/src/components/tables/machine/MachineListTable.tsx
index 3ccd93ddb1b..aef75cfd6ff 100644
--- a/src/frontend/src/components/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/components/tables/machine/MachineListTable.tsx
@@ -380,7 +380,15 @@ function MachineDrawer({
/**
* Table displaying list of available plugins
*/
-export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
+export function MachineListTable({
+ props,
+ renderMachineDrawer = true,
+ createProps
+}: {
+ props: InvenTreeTableProps;
+ renderMachineDrawer?: boolean;
+ createProps?: { machine_type?: string; driver?: string };
+}) {
const { machineTypes, machineDrivers } = useMachineTypeDriver();
const table = useTable('machine');
@@ -483,6 +491,10 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
fields: {
name: {},
machine_type: {
+ hidden: !!createProps?.machine_type,
+ ...(createProps?.machine_type
+ ? { value: createProps.machine_type }
+ : {}),
field_type: 'choice',
choices: machineTypes
? machineTypes.map((t) => ({
@@ -493,6 +505,8 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
onValueChange: (value) => setCreateFormMachineType(value)
},
driver: {
+ hidden: !!createProps?.driver,
+ ...(createProps?.driver ? { value: createProps.driver } : {}),
field_type: 'choice',
disabled: !createFormMachineType,
choices: createFormDriverOptions
@@ -501,7 +515,9 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
},
onFormSuccess: (data) => {
table.refreshTable();
- navigate(`machine-${data.pk}/`);
+ navigate(
+ renderMachineDrawer ? `machine-${data.pk}/` : `../machine-${data.pk}/`
+ );
},
onClose: () => {
setCreateFormMachineType(null);
@@ -523,19 +539,21 @@ export function MachineListTable({ props }: { props: InvenTreeTableProps }) {
return (
<>
{createMachineForm.modal}
- {
- if (!id || !id.startsWith('machine-')) return false;
- return (
-
- );
- }}
- />
+ {renderMachineDrawer && (
+ {
+ if (!id || !id.startsWith('machine-')) return false;
+ return (
+
+ );
+ }}
+ />
+ )}
navigate(`machine-${machine.pk}/`),
+ onRowClick: (machine) =>
+ navigate(
+ renderMachineDrawer
+ ? `machine-${machine.pk}/`
+ : `../machine-${machine.pk}/`
+ ),
tableActions,
params: {
...props.params
diff --git a/src/frontend/src/components/tables/machine/MachineTypeTable.tsx b/src/frontend/src/components/tables/machine/MachineTypeTable.tsx
index 9449c0c1289..b4684c93a27 100644
--- a/src/frontend/src/components/tables/machine/MachineTypeTable.tsx
+++ b/src/frontend/src/components/tables/machine/MachineTypeTable.tsx
@@ -23,7 +23,7 @@ import { DetailDrawer } from '../../nav/DetailDrawer';
import { TableColumn } from '../Column';
import { BooleanColumn } from '../ColumnRenderers';
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
-import { useMachineTypeDriver } from './MachineListTable';
+import { MachineListTable, useMachineTypeDriver } from './MachineListTable';
export interface MachineTypeI {
slug: string;
@@ -266,6 +266,23 @@ function MachineDriverDrawer({
+
+
+
+
+ Machines
+
+
+
+
+
);
}
From badbb1debac69f1c662e476a46602bdd7b72df25 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sun, 28 Jan 2024 11:22:30 +0000
Subject: [PATCH 50/86] Added back button to detail drawer component
---
.../src/components/nav/DetailDrawer.tsx | 24 +++++++++++++++----
1 file changed, 19 insertions(+), 5 deletions(-)
diff --git a/src/frontend/src/components/nav/DetailDrawer.tsx b/src/frontend/src/components/nav/DetailDrawer.tsx
index 20ef06f21ca..8c341ef1ef5 100644
--- a/src/frontend/src/components/nav/DetailDrawer.tsx
+++ b/src/frontend/src/components/nav/DetailDrawer.tsx
@@ -1,4 +1,13 @@
-import { Divider, Drawer, MantineNumberSize, Stack, Text } from '@mantine/core';
+import {
+ ActionIcon,
+ Divider,
+ Drawer,
+ Group,
+ MantineNumberSize,
+ Stack,
+ Text
+} from '@mantine/core';
+import { IconChevronLeft } from '@tabler/icons-react';
import { useMemo } from 'react';
import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
@@ -31,13 +40,18 @@ function DetailDrawerComponent({
return (
navigate(-1)}
+ onClose={() => navigate('../')}
position={position}
size={size}
title={
-
- {title}
-
+
+ navigate(-1)}>
+
+
+
+ {title}
+
+
}
overlayProps={{ opacity: 0.5, blur: 4 }}
>
From 07c89b711390803397be35308d413419acc7bf07 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sun, 28 Jan 2024 11:41:55 +0000
Subject: [PATCH 51/86] Fix auto formatable pre-commit
---
InvenTree/InvenTree/helpers_mixin.py | 16 +++++++++++-----
InvenTree/machine/__init__.py | 8 +-------
InvenTree/machine/apps.py | 13 ++++++++-----
InvenTree/machine/machine_types/__init__.py | 9 +++++----
InvenTree/plugin/machine/__init__.py | 13 ++++++-------
5 files changed, 31 insertions(+), 28 deletions(-)
diff --git a/InvenTree/InvenTree/helpers_mixin.py b/InvenTree/InvenTree/helpers_mixin.py
index 7b13f10707e..12e0911c67c 100644
--- a/InvenTree/InvenTree/helpers_mixin.py
+++ b/InvenTree/InvenTree/helpers_mixin.py
@@ -22,10 +22,12 @@ class ClassValidationMixin:
@classmethod
def validate(cls):
def attribute_missing(key):
- return not hasattr(cls, key) or getattr(cls, key) == ""
+ return not hasattr(cls, key) or getattr(cls, key) == ''
def override_missing(base_implementation):
- return base_implementation == getattr(cls, base_implementation.__name__, None)
+ return base_implementation == getattr(
+ cls, base_implementation.__name__, None
+ )
missing_attributes = list(filter(attribute_missing, cls.required_attributes))
missing_overrides = list(filter(override_missing, cls.required_overrides))
@@ -33,12 +35,16 @@ def override_missing(base_implementation):
errors = []
if len(missing_attributes) > 0:
- errors.append(f"did not provide the following attributes: {', '.join(missing_attributes)}")
+ errors.append(
+ f"did not provide the following attributes: {', '.join(missing_attributes)}"
+ )
if len(missing_overrides) > 0:
- errors.append(f"did not override the required attributes: {', '.join(map(lambda attr: attr.__name__, missing_overrides))}")
+ errors.append(
+ f"did not override the required attributes: {', '.join(map(lambda attr: attr.__name__, missing_overrides))}"
+ )
if len(errors) > 0:
- raise NotImplementedError(f"'{cls}' " + " and ".join(errors))
+ raise NotImplementedError(f"'{cls}' " + ' and '.join(errors))
class ClassProviderMixin:
diff --git a/InvenTree/machine/__init__.py b/InvenTree/machine/__init__.py
index 2487c28c2df..719efa14e9f 100755
--- a/InvenTree/machine/__init__.py
+++ b/InvenTree/machine/__init__.py
@@ -1,10 +1,4 @@
from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
from machine.registry import registry
-__all__ = [
- "registry",
-
- "BaseMachineType",
- "BaseDriver",
- "MachineStatus"
-]
+__all__ = ['registry', 'BaseMachineType', 'BaseDriver', 'MachineStatus']
diff --git a/InvenTree/machine/apps.py b/InvenTree/machine/apps.py
index 72c8338773d..052df1b15a5 100755
--- a/InvenTree/machine/apps.py
+++ b/InvenTree/machine/apps.py
@@ -2,8 +2,7 @@
from django.apps import AppConfig
-from InvenTree.ready import (canAppAccessDatabase, isInMainThread,
- isPluginRegistryLoaded)
+from InvenTree.ready import canAppAccessDatabase, isInMainThread, isPluginRegistryLoaded
logger = logging.getLogger('inventree')
@@ -13,11 +12,15 @@ class MachineConfig(AppConfig):
def ready(self) -> None:
"""Initialization method for the Machine app."""
- if not canAppAccessDatabase(allow_test=True) or not isPluginRegistryLoaded() or not isInMainThread():
- logger.debug("Machine app: Skipping machine loading sequence")
+ if (
+ not canAppAccessDatabase(allow_test=True)
+ or not isPluginRegistryLoaded()
+ or not isInMainThread()
+ ):
+ logger.debug('Machine app: Skipping machine loading sequence')
return
from machine import registry
- logger.info("Loading InvenTree machines")
+ logger.info('Loading InvenTree machines')
registry.initialize()
diff --git a/InvenTree/machine/machine_types/__init__.py b/InvenTree/machine/machine_types/__init__.py
index 263e2060ef9..f59f4adb4b8 100644
--- a/InvenTree/machine/machine_types/__init__.py
+++ b/InvenTree/machine/machine_types/__init__.py
@@ -1,10 +1,11 @@
from machine.machine_types.LabelPrintingMachineType import (
- BaseLabelPrintingDriver, LabelPrintingMachineType)
+ BaseLabelPrintingDriver,
+ LabelPrintingMachineType,
+)
__all__ = [
# machine types
- "LabelPrintingMachineType",
-
+ 'LabelPrintingMachineType',
# base drivers
- "BaseLabelPrintingDriver",
+ 'BaseLabelPrintingDriver',
]
diff --git a/InvenTree/plugin/machine/__init__.py b/InvenTree/plugin/machine/__init__.py
index b6c92a2dff1..df104dd6929 100644
--- a/InvenTree/plugin/machine/__init__.py
+++ b/InvenTree/plugin/machine/__init__.py
@@ -1,11 +1,10 @@
-from machine import (BaseDriver, BaseMachineType, MachineStatus, machine_types,
- registry)
+from machine import BaseDriver, BaseMachineType, MachineStatus, machine_types, registry
from machine.machine_types import * # noqa: F403, F401
__all__ = [
- "registry",
- "BaseDriver",
- "BaseMachineType",
- "MachineStatus",
- *machine_types.__all__
+ 'registry',
+ 'BaseDriver',
+ 'BaseMachineType',
+ 'MachineStatus',
+ *machine_types.__all__,
]
From 34d730febf68dc5e0851837955797e1866042f19 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sun, 28 Jan 2024 12:17:40 +0000
Subject: [PATCH 52/86] fix: style
---
InvenTree/InvenTree/helpers_mixin.py | 23 ++++++++++++-
InvenTree/machine/admin.py | 4 ++-
InvenTree/machine/api.py | 20 ++++++------
InvenTree/machine/apps.py | 6 +++-
InvenTree/machine/machine_type.py | 32 ++++++++++++++++---
.../machine_types/LabelPrintingMachineType.py | 6 ++++
InvenTree/machine/models.py | 25 +++++++++++----
InvenTree/machine/registry.py | 17 ++++++++--
InvenTree/machine/serializers.py | 22 ++++++++++++-
InvenTree/machine/tests.py | 2 ++
InvenTree/machine/views.py | 3 --
11 files changed, 131 insertions(+), 29 deletions(-)
delete mode 100755 InvenTree/machine/views.py
diff --git a/InvenTree/InvenTree/helpers_mixin.py b/InvenTree/InvenTree/helpers_mixin.py
index 12e0911c67c..bdf8adf7224 100644
--- a/InvenTree/InvenTree/helpers_mixin.py
+++ b/InvenTree/InvenTree/helpers_mixin.py
@@ -14,6 +14,23 @@ class ClassValidationMixin:
Class attributes:
required_attributes: List of class attributes that need to be defined
required_overrides: List of functions that need override
+
+ Example:
+ ```py
+ class Parent(ClassValidationMixin):
+ NAME: str
+ def test(self):
+ pass
+
+ required_attributes = ["NAME"]
+ required_overrides = [test]
+
+ class MyClass(Parent):
+ pass
+
+ myClass = MyClass()
+ myClass.validate() # raises NotImplementedError
+ ```
"""
required_attributes = []
@@ -21,10 +38,14 @@ class ClassValidationMixin:
@classmethod
def validate(cls):
+ """Validate the class against the required attributes/overrides."""
+
def attribute_missing(key):
+ """Check if attribute is missing."""
return not hasattr(cls, key) or getattr(cls, key) == ''
def override_missing(base_implementation):
+ """Check if override is missing."""
return base_implementation == getattr(
cls, base_implementation.__name__, None
)
@@ -40,7 +61,7 @@ def override_missing(base_implementation):
)
if len(missing_overrides) > 0:
errors.append(
- f"did not override the required attributes: {', '.join(map(lambda attr: attr.__name__, missing_overrides))}"
+ f"did not override the required attributes: {', '.join(attr.__name__ for attr in missing_overrides)}"
)
if len(errors) > 0:
diff --git a/InvenTree/machine/admin.py b/InvenTree/machine/admin.py
index 5730415a517..3437c7a38ab 100755
--- a/InvenTree/machine/admin.py
+++ b/InvenTree/machine/admin.py
@@ -1,3 +1,5 @@
+"""Django admin interface for the machine app."""
+
from django.contrib import admin
from django.http.request import HttpRequest
@@ -39,7 +41,7 @@ class MachineConfigAdmin(admin.ModelAdmin):
inlines = [MachineSettingInline]
def get_readonly_fields(self, request, obj):
- # if update, don't allow changes on machine_type and driver
+ """If update, don't allow changes on machine_type and driver."""
if obj is not None:
return ['machine_type', 'driver', *self.readonly_fields]
diff --git a/InvenTree/machine/api.py b/InvenTree/machine/api.py
index d6842ffeae5..9448d2ea993 100644
--- a/InvenTree/machine/api.py
+++ b/InvenTree/machine/api.py
@@ -1,3 +1,5 @@
+"""JSON API for the machine app."""
+
from django.urls import include, path, re_path
from drf_spectacular.utils import extend_schema
@@ -24,7 +26,7 @@ class MachineList(ListCreateAPI):
serializer_class = MachineSerializers.MachineConfigSerializer
def get_serializer_class(self):
- # allow driver, machine_type fields on creation
+ """Allow driver, machine_type fields on creation."""
if self.request.method == 'POST':
return MachineSerializers.MachineConfigCreateSerializer
return super().get_serializer_class()
@@ -52,9 +54,6 @@ class MachineDetail(RetrieveUpdateDestroyAPI):
queryset = MachineConfig.objects.all()
serializer_class = MachineSerializers.MachineConfigSerializer
- def update(self, request, *args, **kwargs):
- return super().update(request, *args, **kwargs)
-
def get_machine(machine_pk):
"""Get machine by pk.
@@ -85,6 +84,7 @@ class MachineSettingList(APIView):
responses={200: MachineSerializers.MachineSettingSerializer(many=True)}
)
def get(self, request, pk):
+ """Return all settings for a machine config."""
machine = get_machine(pk)
all_settings = []
@@ -125,7 +125,7 @@ def get_object(self):
machine = get_machine(pk)
- setting_map = dict((d, s) for s, d in machine.setting_types)
+ setting_map = {d: s for s, d in machine.setting_types}
if key.upper() not in setting_map[config_type]:
raise NotFound(
detail=f"Machine '{machine.name}' has no {config_type.name} setting matching '{key.upper()}'"
@@ -139,13 +139,14 @@ def get_object(self):
class MachineRestart(APIView):
"""Endpoint for performing a machine restart.
- - POST: restart machine
+ - POST: restart machine by pk
"""
permission_classes = [permissions.IsAuthenticated]
@extend_schema(responses={200: MachineSerializers.MachineRestartSerializer()})
def post(self, request, pk):
+ """Restart machine by pk."""
machine = get_machine(pk)
registry.restart_machine(machine)
@@ -163,6 +164,7 @@ class MachineTypesList(APIView):
@extend_schema(responses={200: MachineSerializers.MachineTypeSerializer(many=True)})
def get(self, request):
+ """List all machine types."""
machine_types = list(registry.machine_types.values())
results = MachineSerializers.MachineTypeSerializer(
machine_types, many=True
@@ -182,6 +184,7 @@ class MachineDriverList(APIView):
responses={200: MachineSerializers.MachineDriverSerializer(many=True)}
)
def get(self, request):
+ """List all machine drivers."""
drivers = registry.drivers.values()
if machine_type := request.query_params.get('machine_type', None):
drivers = filter(lambda d: d.machine_type == machine_type, drivers)
@@ -206,10 +209,9 @@ class RegistryStatusView(APIView):
responses={200: MachineSerializers.MachineRegistryStatusSerializer()}
)
def get(self, request):
+ """Provide status data for the machine registry."""
result = MachineSerializers.MachineRegistryStatusSerializer({
- 'registry_errors': list(
- {'message': str(error)} for error in registry.errors
- )
+ 'registry_errors': [{'message': str(error)} for error in registry.errors]
}).data
return Response(result)
diff --git a/InvenTree/machine/apps.py b/InvenTree/machine/apps.py
index 052df1b15a5..602c2500b02 100755
--- a/InvenTree/machine/apps.py
+++ b/InvenTree/machine/apps.py
@@ -1,3 +1,5 @@
+"""Django machine app config."""
+
import logging
from django.apps import AppConfig
@@ -8,10 +10,12 @@
class MachineConfig(AppConfig):
+ """AppConfig class for the machine app."""
+
name = 'machine'
def ready(self) -> None:
- """Initialization method for the Machine app."""
+ """Initialization method for the machine app."""
if (
not canAppAccessDatabase(allow_test=True)
or not isPluginRegistryLoaded()
diff --git a/InvenTree/machine/machine_type.py b/InvenTree/machine/machine_type.py
index b9a90b08f1a..eb864ac18d9 100644
--- a/InvenTree/machine/machine_type.py
+++ b/InvenTree/machine/machine_type.py
@@ -1,3 +1,5 @@
+"""Base machine type/base driver."""
+
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Tuple, Type, Union
from generic.states import StatusCode
@@ -10,9 +12,13 @@
else: # pragma: no cover
class MachineConfig:
+ """Only used if not typechecking currently."""
+
pass
class SettingsKeyType:
+ """Only used if not typechecking currently."""
+
pass
@@ -40,7 +46,7 @@ class MachineStatus(StatusCode):
class BaseDriver(ClassValidationMixin, ClassProviderMixin):
- """Base class for machine drivers
+ """Base class for machine drivers.
Attributes:
SLUG: Slug string for identifying a machine
@@ -61,6 +67,7 @@ class BaseDriver(ClassValidationMixin, ClassProviderMixin):
required_attributes = ['SLUG', 'NAME', 'DESCRIPTION', 'machine_type']
def __init__(self) -> None:
+ """Base driver __init__ method."""
super().__init__()
self.errors: list[Union[str, Exception]] = []
@@ -87,7 +94,7 @@ def init_machine(self, machine: 'BaseMachineType'):
def update_machine(
self, old_machine_state: Dict[str, Any], machine: 'BaseMachineType'
):
- """This method gets called for each update of a machine
+ """This method gets called for each update of a machine.
Note:
machine.restart_required can be set to True here if the machine needs a manual restart to apply the changes
@@ -110,17 +117,27 @@ def restart_machine(self, machine: 'BaseMachineType'):
pass
def get_machines(self, **kwargs):
- """Return all machines using this driver. (By default only initialized machines)"""
+ """Return all machines using this driver (By default only initialized machines).
+
+ Kwargs:
+ name: Machine name
+ machine_type: Machine type definition (class)
+ driver: Machine driver (class)
+ initialized: (bool, default: True)
+ active: (bool)
+ base_driver: base driver (class)
+ """
from machine import registry
return registry.get_machines(driver=self, **kwargs)
def handle_error(self, error: Union[Exception, str]):
+ """Handle driver error."""
self.errors.append(error)
class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
- """Base class for machine types
+ """Base class for machine types.
Attributes:
SLUG: Slug string for identifying a machine type
@@ -157,6 +174,7 @@ class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
]
def __init__(self, machine_config: MachineConfig) -> None:
+ """Base machine type __init__ function."""
from machine import registry
from machine.models import MachineSetting
@@ -193,11 +211,13 @@ def __init__(self, machine_config: MachineConfig) -> None:
self.restart_required = False
def __str__(self):
+ """String representation of a machine."""
return f'{self.name}'
# --- properties
@property
def machine_config(self):
+ """Machine_config property."""
# always fetch the machine_config if needed to ensure we get the newest reference
from .models import MachineConfig
@@ -205,15 +225,17 @@ def machine_config(self):
@property
def name(self):
+ """Name property."""
return self.machine_config.name
@property
def active(self):
+ """Active property."""
return self.machine_config.active
# --- hook functions
def initialize(self):
- """Machine initialization function, gets called after all machines are loaded"""
+ """Machine initialization function, gets called after all machines are loaded."""
if self.driver is None:
return
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index e1160e01c8b..0b44e8dca4d 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -1,3 +1,5 @@
+"""Label printing machine type."""
+
from django.utils.translation import gettext_lazy as _
from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
@@ -20,6 +22,8 @@ def print_labels(self):
class LabelPrintingMachineType(BaseMachineType):
+ """Label printer machine type, is a direct integration to print labels for various items."""
+
SLUG = 'label_printer'
NAME = _('Label Printer')
DESCRIPTION = _('Directly print labels for various items.')
@@ -27,6 +31,8 @@ class LabelPrintingMachineType(BaseMachineType):
base_driver = BaseLabelPrintingDriver
class LabelPrinterStatus(MachineStatus):
+ """Label printer status codes."""
+
CONNECTED = 100, _('Connected'), 'success'
STANDBY = 101, _('Standby'), 'success'
PRINTING = 110, _('Printing'), 'primary'
diff --git a/InvenTree/machine/models.py b/InvenTree/machine/models.py
index a33e1c9b35a..d8f570d2fb6 100755
--- a/InvenTree/machine/models.py
+++ b/InvenTree/machine/models.py
@@ -1,3 +1,5 @@
+"""Models for the machine app."""
+
import uuid
from typing import Literal
@@ -42,6 +44,7 @@ def __str__(self) -> str:
return f'{self.name}'
def save(self, *args, **kwargs) -> None:
+ """Custom save function to capture creates/updates to notify the registry."""
created = self._state.adding
old_machine = None
@@ -64,13 +67,14 @@ def save(self, *args, **kwargs) -> None:
registry.update_machine(old_machine, self)
def delete(self, *args, **kwargs):
- # remove machine from registry first
+ """Remove machine from registry first."""
if self.machine:
registry.remove_machine(self.machine)
return super().delete(*args, **kwargs)
def to_dict(self):
+ """Serialize a machine config to a dict including setting."""
machine = {f.name: f.value_to_string(self) for f in self._meta.fields}
machine['settings'] = {
(setting.config_type, setting.key): setting.value
@@ -80,35 +84,39 @@ def to_dict(self):
@property
def machine(self):
+ """Machine instance getter."""
return registry.get_machine(self.pk)
@property
def errors(self):
+ """Machine errors getter."""
return getattr(self.machine, 'errors', [])
@admin.display(boolean=True, description=_('Driver available'))
def is_driver_available(self) -> bool:
- """Status if driver for machine is available"""
+ """Status if driver for machine is available."""
return self.machine is not None and self.machine.driver is not None
@admin.display(boolean=True, description=_('No errors'))
def no_errors(self) -> bool:
- """Status if machine has errors"""
+ """Status if machine has errors."""
return len(self.errors) == 0
@admin.display(boolean=True, description=_('Initialized'))
def initialized(self) -> bool:
- """Status if machine is initialized"""
+ """Status if machine is initialized."""
return getattr(self.machine, 'initialized', False)
@admin.display(description=_('Errors'))
def get_admin_errors(self):
+ """Get machine errors for django admin interface."""
return format_html_join(
mark_safe('
'), '{}', ((str(error),) for error in self.errors)
) or mark_safe(f"{_('No errors')}")
@admin.display(description=_('Machine status'))
def get_machine_status(self):
+ """Get machine status for django admin interface."""
if self.machine is None:
return None
@@ -132,6 +140,8 @@ class Meta:
unique_together = [('machine_config', 'config_type', 'key')]
class ConfigType(models.TextChoices):
+ """Machine setting config type enum."""
+
MACHINE = 'M', _('Machine')
DRIVER = 'D', _('Driver')
@@ -147,6 +157,7 @@ class ConfigType(models.TextChoices):
)
def save(self, *args, **kwargs) -> None:
+ """Custom save method to notify the registry on changes."""
old_machine = self.machine_config.to_dict()
super().save(*args, **kwargs)
@@ -155,6 +166,7 @@ def save(self, *args, **kwargs) -> None:
@classmethod
def get_config_type(cls, config_type_str: Literal['M', 'D']):
+ """Helper method to get the correct enum value for easier usage with literal strings."""
if config_type_str == 'M':
return cls.ConfigType.MACHINE
elif config_type_str == 'D':
@@ -162,8 +174,9 @@ def get_config_type(cls, config_type_str: Literal['M', 'D']):
@classmethod
def get_setting_definition(cls, key, **kwargs):
- """In the BaseInvenTreeSetting class, we have a class attribute named 'SETTINGS', which
- is a dict object that fully defines all the setting parameters.
+ """In the BaseInvenTreeSetting class, we have a class attribute named 'SETTINGS'.
+
+ which is a dict object that fully defines all the setting parameters.
Here, unlike the BaseInvenTreeSetting, we do not know the definitions of all settings
'ahead of time' (as they are defined externally in the machine driver).
diff --git a/InvenTree/machine/registry.py b/InvenTree/machine/registry.py
index 4b308e77d33..4798e31b450 100644
--- a/InvenTree/machine/registry.py
+++ b/InvenTree/machine/registry.py
@@ -1,3 +1,5 @@
+"""Machine registry."""
+
import logging
from typing import Dict, List, Set, Type, Union
from uuid import UUID
@@ -8,8 +10,10 @@
class MachineRegistry:
+ """Machine registry class."""
+
def __init__(self) -> None:
- """Initialize machine registry
+ """Initialize machine registry.
Set up all needed references for internal and external states.
"""
@@ -26,11 +30,13 @@ def handle_error(self, error: Union[Exception, str]):
self.errors.append(error)
def initialize(self):
+ """Initialize the machine registry."""
self.discover_machine_types()
self.discover_drivers()
self.load_machines()
def discover_machine_types(self):
+ """Discovers all machine types by inferring all classes that inherit the BaseMachineType class."""
import InvenTree.helpers
logger.debug('Collecting machine types')
@@ -63,6 +69,7 @@ def discover_machine_types(self):
logger.debug(f'Found {len(self.machine_types.keys())} machine types')
def discover_drivers(self):
+ """Discovers all machine drivers by inferring all classes that inherit the BaseDriver class."""
import InvenTree.helpers
logger.debug('Collecting machine drivers')
@@ -96,6 +103,7 @@ def discover_drivers(self):
logger.debug(f'Found {len(self.drivers.keys())} machine drivers')
def get_driver_instance(self, slug: str):
+ """Return or create a driver instance if needed."""
if slug not in self.driver_instances:
driver = self.drivers.get(slug, None)
if driver is None:
@@ -106,6 +114,7 @@ def get_driver_instance(self, slug: str):
return self.driver_instances.get(slug, None)
def load_machines(self):
+ """Load all machines defined in the database into the machine registry."""
# Imports need to be in this level to prevent early db model imports
from machine.models import MachineConfig
@@ -122,6 +131,7 @@ def load_machines(self):
machine.initialize()
def add_machine(self, machine_config, initialize=True):
+ """Add a machine to the machine registry."""
machine_type = self.machine_types.get(machine_config.machine_type, None)
if machine_type is None:
self.handle_error(f"Machine type '{machine_config.machine_type}' not found")
@@ -134,17 +144,20 @@ def add_machine(self, machine_config, initialize=True):
machine.initialize()
def update_machine(self, old_machine_state, machine_config):
+ """Notify the machine about an update."""
if machine := machine_config.machine:
machine.update(old_machine_state)
def restart_machine(self, machine):
+ """Restart a machine."""
machine.restart()
def remove_machine(self, machine: BaseMachineType):
+ """Remove a machine from the registry."""
self.machines.pop(str(machine.pk), None)
def get_machines(self, **kwargs):
- """Get loaded machines from registry. (By default only initialized machines)
+ """Get loaded machines from registry (By default only initialized machines).
Kwargs:
name: Machine name
diff --git a/InvenTree/machine/serializers.py b/InvenTree/machine/serializers.py
index ba5983ee1c9..02df3f6caa9 100644
--- a/InvenTree/machine/serializers.py
+++ b/InvenTree/machine/serializers.py
@@ -1,3 +1,5 @@
+"""Serializers for the machine app."""
+
from typing import List, Union
from rest_framework import serializers
@@ -41,29 +43,36 @@ class Meta:
restart_required = serializers.SerializerMethodField('get_restart_required')
def get_initialized(self, obj: MachineConfig) -> bool:
+ """Serializer method for the initialized field."""
return getattr(obj.machine, 'initialized', False)
def get_status(self, obj: MachineConfig) -> int:
+ """Serializer method for the status field."""
status = getattr(obj.machine, 'status', None)
if status is not None:
return status.value
return -1
def get_status_model(self, obj: MachineConfig) -> Union[str, None]:
+ """Serializer method for the status model field."""
if obj.machine and obj.machine.MACHINE_STATUS:
return obj.machine.MACHINE_STATUS.__name__
return None
def get_status_text(self, obj: MachineConfig) -> str:
+ """Serializer method for the status text field."""
return getattr(obj.machine, 'status_text', '')
def get_errors(self, obj: MachineConfig) -> List[str]:
+ """Serializer method for the errors field."""
return [str(err) for err in obj.errors]
def get_is_driver_available(self, obj: MachineConfig) -> bool:
+ """Serializer method for the is_driver_available field."""
return obj.is_driver_available()
def get_restart_required(self, obj: MachineConfig) -> bool:
+ """Serializer method for the restart_required field."""
return getattr(obj.machine, 'restart_required', False)
@@ -75,7 +84,7 @@ class Meta(MachineConfigSerializer.Meta):
read_only_fields = list(
set(MachineConfigSerializer.Meta.read_only_fields)
- - set(['machine_type', 'driver'])
+ - {'machine_type', 'driver'}
)
@@ -86,6 +95,7 @@ class MachineSettingSerializer(GenericReferencedSettingSerializer):
EXTRA_FIELDS = ['config_type']
def __init__(self, *args, **kwargs):
+ """Custom init method to remove unwanted fields."""
super().__init__(*args, **kwargs)
# remove unwanted fields
@@ -122,9 +132,11 @@ class Meta:
is_builtin = serializers.SerializerMethodField('get_is_builtin')
def get_provider_file(self, obj: ClassProviderMixin) -> str:
+ """Serializer method for the provider_file field."""
return obj.get_provider_file()
def get_provider_plugin(self, obj: ClassProviderMixin) -> Union[dict, None]:
+ """Serializer method for the provider_plugin field."""
plugin = obj.get_provider_plugin()
if plugin:
return {
@@ -135,6 +147,7 @@ def get_provider_plugin(self, obj: ClassProviderMixin) -> Union[dict, None]:
return None
def get_is_builtin(self, obj: ClassProviderMixin) -> bool:
+ """Serializer method for the is_builtin field."""
return obj.get_is_builtin()
@@ -160,6 +173,7 @@ class Meta(BaseMachineClassSerializer.Meta):
driver_errors = serializers.SerializerMethodField('get_errors')
def get_errors(self, obj) -> List[str]:
+ """Serializer method for the errors field."""
driver_instance = registry.driver_instances.get(obj.SLUG, None)
if driver_instance is None:
return []
@@ -170,6 +184,8 @@ class MachineRegistryErrorSerializer(serializers.Serializer):
"""Serializer for a machine registry error."""
class Meta:
+ """Meta for a serializer."""
+
fields = ['message']
message = serializers.CharField()
@@ -179,6 +195,8 @@ class MachineRegistryStatusSerializer(serializers.Serializer):
"""Serializer for machine registry status."""
class Meta:
+ """Meta for a serializer."""
+
fields = ['registry_errors']
registry_errors = serializers.ListField(child=MachineRegistryErrorSerializer())
@@ -188,6 +206,8 @@ class MachineRestartSerializer(serializers.Serializer):
"""Serializer for the machine restart response."""
class Meta:
+ """Meta for a serializer."""
+
fields = ['ok']
ok = serializers.BooleanField()
diff --git a/InvenTree/machine/tests.py b/InvenTree/machine/tests.py
index a79ca8be565..413d0d4bc7e 100755
--- a/InvenTree/machine/tests.py
+++ b/InvenTree/machine/tests.py
@@ -1,3 +1,5 @@
+"""Machine app tests."""
+
# from django.test import TestCase
# Create your tests here.
diff --git a/InvenTree/machine/views.py b/InvenTree/machine/views.py
deleted file mode 100755
index fd0e0449559..00000000000
--- a/InvenTree/machine/views.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# from django.shortcuts import render
-
-# Create your views here.
From e36451cb786076b510a238b350ce2bffaaaa4700 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sun, 28 Jan 2024 12:26:10 +0000
Subject: [PATCH 53/86] Fix deepsource
---
InvenTree/machine/registry.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/InvenTree/machine/registry.py b/InvenTree/machine/registry.py
index 4798e31b450..76f4142f104 100644
--- a/InvenTree/machine/registry.py
+++ b/InvenTree/machine/registry.py
@@ -66,7 +66,7 @@ def discover_machine_types(self):
self.machine_types = machine_types
self.base_drivers = base_drivers
- logger.debug(f'Found {len(self.machine_types.keys())} machine types')
+ logger.debug('Found %s machine types', len(self.machine_types.keys()))
def discover_drivers(self):
"""Discovers all machine drivers by inferring all classes that inherit the BaseDriver class."""
@@ -100,7 +100,7 @@ def discover_drivers(self):
self.drivers = drivers
- logger.debug(f'Found {len(self.drivers.keys())} machine drivers')
+ logger.debug('Found %s machine drivers', len(self.drivers.keys()))
def get_driver_instance(self, slug: str):
"""Return or create a driver instance if needed."""
@@ -130,6 +130,8 @@ def load_machines(self):
if machine.active:
machine.initialize()
+ logger.info('Initialized %s machines', len(self.machines.keys()))
+
def add_machine(self, machine_config, initialize=True):
"""Add a machine to the machine registry."""
machine_type = self.machine_types.get(machine_config.machine_type, None)
From 9bf51fe67cb7672cb609bcf84c136bdb5f4694ce Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sun, 28 Jan 2024 17:35:43 +0000
Subject: [PATCH 54/86] Removed slug field from table, added more links between
drawers, remove detail drawer blur
---
.../src/components/nav/DetailDrawer.tsx | 1 -
.../tables/machine/MachineTypeTable.tsx | 51 ++++++++++---------
2 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/src/frontend/src/components/nav/DetailDrawer.tsx b/src/frontend/src/components/nav/DetailDrawer.tsx
index 8c341ef1ef5..80c4e2e3b72 100644
--- a/src/frontend/src/components/nav/DetailDrawer.tsx
+++ b/src/frontend/src/components/nav/DetailDrawer.tsx
@@ -53,7 +53,6 @@ function DetailDrawerComponent({
}
- overlayProps={{ opacity: 0.5, blur: 4 }}
>
diff --git a/src/frontend/src/components/tables/machine/MachineTypeTable.tsx b/src/frontend/src/components/tables/machine/MachineTypeTable.tsx
index b4684c93a27..f5ca4363106 100644
--- a/src/frontend/src/components/tables/machine/MachineTypeTable.tsx
+++ b/src/frontend/src/components/tables/machine/MachineTypeTable.tsx
@@ -64,10 +64,6 @@ function MachineTypeDrawer({ machineTypeSlug }: { machineTypeSlug: string }) {
accessor: 'name',
title: t`Name`
},
- {
- accessor: 'slug',
- title: t`Slug`
- },
{
accessor: 'description',
title: t`Description`
@@ -114,16 +110,18 @@ function MachineTypeDrawer({ machineTypeSlug }: { machineTypeSlug: string }) {
value={machineType?.description}
type="text"
/>
-
+ {!machineType?.is_builtin && (
+
+ )}
-
+ {!machineDriver?.is_builtin && (
+
+ )}
Date: Sun, 28 Jan 2024 20:56:22 +0000
Subject: [PATCH 55/86] Added initial docs
---
docs/docs/extend/plugins/machines.md | 204 +++++++++++++++++++++++++++
docs/mkdocs.yml | 1 +
2 files changed, 205 insertions(+)
create mode 100644 docs/docs/extend/plugins/machines.md
diff --git a/docs/docs/extend/plugins/machines.md b/docs/docs/extend/plugins/machines.md
new file mode 100644
index 00000000000..32db0666f38
--- /dev/null
+++ b/docs/docs/extend/plugins/machines.md
@@ -0,0 +1,204 @@
+---
+title: Machines
+---
+
+## Machines
+
+InvenTree has a builtin machine registry. There are different machine types available where each type can have different drivers. Drivers and even custom machine types can be provided by plugins.
+
+### Registry
+
+The machine registry is the main component which gets initialized on server start and manages all configured machines.
+
+#### Initialization process
+
+The machine registry initialization process can be divided into three stages as described in this diagram:
+
+```mermaid
+flowchart LR
+ A["`**Server start**`"] --> B
+ B["`**Stage 1: Discover machine types**
+ by looking for classes that inherit the BaseMachineType class`"] --> C
+ C["`**Stage 2: Discover drivers**
+ by looking for classes that inherit the BaseDriver class (and are not referenced as base driver for any discovered machine type)`"] --> MA
+ subgraph MA["Stage 3: Machine loading"]
+ direction TB
+ D["`For each MachineConfig in database instantiate the MachineType class (drivers get instantiated here as needed and passed to the machine class. But only one instance of the driver class is maintained along the registry)`"] --> E
+ E["`The driver.init_driver function is called for each used driver`"] --> F
+ F["`The machine.initialize function is called for each machine, which calls the driver.init_machine function for each machine, then the machine.initialized state is set to true`"]
+ end
+ MA --> X["`**Done**`"]
+```
+
+### Machine types
+
+Each machine type can provide a different type of connection functionality between inventree and a physical machine. These machine types are already built into InvenTree.
+The machine type class gets instantiated for each machine on server startup and the reference is stored in the machine registry.
+
+#### Built in types
+
+| Name | Description |
+| ------------------------------- | ---------------------------------------- |
+| [Label printer](#label-printer) | Directly print labels for various items. |
+
+##### Label printer
+
+Label printer machines can directly print labels for various items in InvenTree. They replace standard [`LabelPrintingMixin`](../plugins/label.md) plugins that are used to connect to physical printers. Using machines rather than a standard `LabelPrintingMixin` plugin has the advantage that machines can be created multiple times using different settings but the same driver. That way multiple label printers of the same brand can be connected.
+
+TODO
+
+#### Available attributes
+
+| Name | Description |
+| ------------------------ | --------------------------------------------------------------------------------------------------------------------- |
+| `SLUG` | A slug for the machine type needs to be set (short identifier string that satisfies the following format `/[a-z-]+/`) |
+| `NAME` | A name for the machine type needs to be set |
+| `DESCRIPTION` | A description for the machine type needs to be set |
+| `base_driver` | Reference to the base driver class |
+| `MACHINE_SETTINGS` | Machine settings dict, see [settings](#settings) |
+| `MACHINE_STATUS` | Machine status enum, see [status](#machine-status) |
+| `default_machine_status` | default machine status, see [status](#machine-status) |
+
+#### Available methods
+
+| Name | Description |
+| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
+| `initialize(self)` | gets called on initialization |
+| `update(self, old_machine_state: Dict[str, Any])` | gets called if any changes on a machine or their settings occur. The `machine.restart_required` flag can be set here. |
+| `restart(self, machine: 'BaseMachineType')` | gets called if the admin manually presses the restart button on the admin interface |
+| `handle_error(self, error: Union[Exception, str])` | helper function to capture errors and show them in the admin ui |
+| `get_setting(self, key: str, config_type_str: Literal['M', 'D'], cache=False)` | get a setting for a machine |
+| `set_setting(self, key: str, config_type_str: Literal['M', 'D'], value)` | set a setting for a machine |
+| `check_settings(self)` | check that all required settings are set |
+| `set_status(self, status: MachineStatus)` | set a machine status code |
+| `set_status_text(self, status_text: str)` | set a machine status text |
+
+#### Example machine type
+
+If you want to create your own machine type, please also take a look at the already existing machine types.
+
+```py
+from django.utils.translation import ugettext_lazy as _
+from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
+
+class BaseXYZDriver(BaseDriver):
+ """Base xyz driver."""
+
+ machine_type = 'xyz'
+
+ def my_custom_required_method(self):
+ """This function must be overridden."""
+ raise NotImplementedError('The `my_custom_required_method` function must be overridden!')
+
+ def my_custom_method(self):
+ """This function must be overridden."""
+ raise NotImplementedError('The `my_custom_method` function must be overridden!')
+
+ requires_override = [my_custom_required_method]
+
+class XYZMachineType(BaseMachineType):
+ SLUG = 'xyz'
+ NAME = _('XYZ')
+ DESCRIPTION = _('This is an awesome machine type for xyz.')
+
+ base_driver = BaseXYZDriver
+
+ class XYZStatus(MachineStatus):
+ CONNECTED = 100, _('Connected'), 'success'
+ STANDBY = 101, _('Standby'), 'success'
+ PRINTING = 110, _('Printing'), 'primary'
+
+ MACHINE_STATUS = XYZStatus
+
+ default_machine_status = XYZStatus.DISCONNECTED
+```
+
+### Drivers
+
+Drivers provide the connection layer between physical machines and inventree. There can be multiple drivers defined for the same machine type. Drivers are provided by plugins that are enabled and extend the corresponding base driver for the particular machine type. Each machine type already provides a base driver that needs to be inherited.
+
+#### Available attributes
+
+| Name | Description |
+| ------------------ | --------------------------------------------------------------------------------------------------------------- |
+| `SLUG` | A slug for the driver needs to be set (short identifier string that satisfies the following format `/[a-z-]+/`) |
+| `NAME` | A name for the driver needs to be set |
+| `DESCRIPTION` | A description for the driver needs to be set |
+| `MACHINE_SETTINGS` | Machine settings dict, see [settings](#settings) (optional) |
+| (`machine_type`) | Already set to the machine type slug by the base driver |
+
+#### Available methods
+
+| Name | Description |
+| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `init_driver(self)` | gets called on initialization before each individual machine gets initialized |
+| `init_machine(self, machine: 'BaseMachineType')` | gets called for each machine |
+| `update_machine(self, old_machine_state: Dict[str, Any], machine: 'BaseMachineType')` | gets called if any changes on a machine or their settings occur. The `machine.restart_required` flag can be set here. |
+| `restart_machine(self, machine: 'BaseMachineType')` | gets called if the admin manually presses the restart button on the admin interface |
+| `get_machines(self, *, name, machine_type, driver, initialized, active, base_driver)` | helper function to get all machine, by default only initialized machines that also use the current driver are returned |
+| `handle_error(self, error: Union[Exception, str])` | helper function to capture errors and show them in the admin ui |
+
+#### Example driver
+
+A basic driver only needs to specify the basic attributes like `SLUG`, `NAME`, `DESCRIPTION`. The others are given by the used base driver, so take a look at [Machine types](#machine-types). The class will be discovered if it is provided by an **installed & activated** plugin just like this:
+
+```py
+from plugin import InvenTreePlugin
+from plugin.machine import BaseXYZDriver
+
+class CupsLabelPlugin(InvenTreePlugin):
+ NAME = "CupsLabels"
+ SLUG = "cups"
+ TITLE = "Cups Label Printer"
+ # ...
+
+class MyXYZDriver(BaseXYZDriver):
+ SLUG = 'my-abc-driver'
+ NAME = 'My ABC driver'
+ DESCRIPTION = 'This is an awesome driver for ABC'
+```
+
+### Settings
+
+Each machine can have different settings configured. There are machine settings that are specific to that machine type and driver settings that are specific to the driver, but both can be specified individually for each machine. Define them by adding a `MACHINE_SETTINGS` dictionary attribute to either the driver or the machine type. The format follows the same pattern as the `SETTINGS` for normal plugins documented on the [`SettingsMixin`](../plugins/settings.md)
+
+```py
+class MyXYZDriver(BaseXYZDriver):
+ MACHINE_SETTINGS = {
+ 'SERVER': {
+ 'name': _('Server'),
+ 'description': _('IP/Hostname to connect to the cups server'),
+ 'default': 'localhost',
+ 'required': True,
+ }
+ }
+```
+
+Settings can even marked as `'required': True` which prevents the machine from starting if the setting is not defined.
+
+### Machine status
+
+Each machine type has a set of status codes defined that can be set for each machine by the driver. There also needs to be a default status code defined.
+
+```py
+class XYZMachineType(BaseMachineType):
+ # ...
+ class XYZStatus(MachineStatus):
+ CONNECTED = 100, _('Connected'), 'success'
+ STANDBY = 101, _('Standby'), 'success'
+ DISCONNECTED = 400, _('Disconnected'), 'danger'
+
+ MACHINE_STATUS = XYZStatus
+ default_machine_status = XYZStatus.DISCONNECTED
+```
+
+And to set a status code for a machine by the driver. There can also be a free text status code defined.
+
+```py
+class MyXYZDriver(BaseXYZDriver):
+ # ...
+ def init_machine(self, machine):
+ # ... do some init stuff here
+ machine.set_status(XYZMachineType.MACHINE_STATUS.CONNECTED)
+ machine.set_status_text("Paper missing")
+```
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 2afbf527ae6..4ad2dffab96 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -201,6 +201,7 @@ nav:
- Developing a Plugin: extend/how_to_plugin.md
- Model Metadata: extend/plugins/metadata.md
- Tags: extend/plugins/tags.md
+ - Machines: extend/plugins/machines.md
- Plugin Mixins:
- Action Mixin: extend/plugins/action.md
- API Mixin: extend/plugins/api.md
From d27ac8db62824ed18ddfa24c42034699c4295a3c Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Fri, 2 Feb 2024 09:21:53 +0000
Subject: [PATCH 56/86] Removed description from driver/machine type select and
fixed disabled driver select if no machine type is selected
---
src/frontend/src/components/forms/fields/ChoiceField.tsx | 1 +
src/frontend/src/tables/machine/MachineListTable.tsx | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/frontend/src/components/forms/fields/ChoiceField.tsx b/src/frontend/src/components/forms/fields/ChoiceField.tsx
index 8eedfb2bd13..d0137d9d3b0 100644
--- a/src/frontend/src/components/forms/fields/ChoiceField.tsx
+++ b/src/frontend/src/components/forms/fields/ChoiceField.tsx
@@ -62,6 +62,7 @@ export function ChoiceField({
description={definition.description}
placeholder={definition.placeholder}
required={definition.required}
+ disabled={definition.disabled}
icon={definition.icon}
withinPortal={true}
/>
diff --git a/src/frontend/src/tables/machine/MachineListTable.tsx b/src/frontend/src/tables/machine/MachineListTable.tsx
index b6efbfb8182..ea8fed09629 100644
--- a/src/frontend/src/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/tables/machine/MachineListTable.tsx
@@ -485,7 +485,7 @@ export function MachineListTable({
.filter((d) => d.machine_type === createFormMachineType)
.map((d) => ({
value: d.slug,
- display_name: `${d.name} (${d.description})`
+ display_name: d.name
}));
}, [machineDrivers, createFormMachineType]);
@@ -503,7 +503,7 @@ export function MachineListTable({
choices: machineTypes
? machineTypes.map((t) => ({
value: t.slug,
- display_name: `${t.name} (${t.description})`
+ display_name: t.name
}))
: [],
onValueChange: (value) => setCreateFormMachineType(value)
From 24b4987222683c105609c0c32c2b0472d4a67e01 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Fri, 2 Feb 2024 15:44:06 +0000
Subject: [PATCH 57/86] Added basic label printing implementation
---
InvenTree/InvenTree/helpers_mixin.py | 16 +-
InvenTree/machine/machine_type.py | 3 +-
.../machine_types/LabelPrintingMachineType.py | 180 +++++++++++++++++-
InvenTree/machine/registry.py | 8 +
InvenTree/plugin/base/label/mixins.py | 9 +-
.../builtin/labels/inventree_machine.py | 112 +++++++++++
6 files changed, 314 insertions(+), 14 deletions(-)
create mode 100644 InvenTree/plugin/builtin/labels/inventree_machine.py
diff --git a/InvenTree/InvenTree/helpers_mixin.py b/InvenTree/InvenTree/helpers_mixin.py
index bdf8adf7224..59c88785ed6 100644
--- a/InvenTree/InvenTree/helpers_mixin.py
+++ b/InvenTree/InvenTree/helpers_mixin.py
@@ -13,7 +13,7 @@ class ClassValidationMixin:
Class attributes:
required_attributes: List of class attributes that need to be defined
- required_overrides: List of functions that need override
+ required_overrides: List of functions that need override, a nested list mean either one of them needs an override
Example:
```py
@@ -46,6 +46,9 @@ def attribute_missing(key):
def override_missing(base_implementation):
"""Check if override is missing."""
+ if isinstance(base_implementation, list):
+ return all(override_missing(x) for x in base_implementation)
+
return base_implementation == getattr(
cls, base_implementation.__name__, None
)
@@ -60,8 +63,17 @@ def override_missing(base_implementation):
f"did not provide the following attributes: {', '.join(missing_attributes)}"
)
if len(missing_overrides) > 0:
+ missing_overrides_list = []
+ for base_implementation in missing_overrides:
+ if isinstance(base_implementation, list):
+ missing_overrides_list.append(
+ 'one of '
+ + ' or '.join(attr.__name__ for attr in base_implementation)
+ )
+ else:
+ missing_overrides_list.append(base_implementation.__name__)
errors.append(
- f"did not override the required attributes: {', '.join(attr.__name__ for attr in missing_overrides)}"
+ f"did not override the required attributes: {', '.join(missing_overrides_list)}"
)
if len(errors) > 0:
diff --git a/InvenTree/machine/machine_type.py b/InvenTree/machine/machine_type.py
index eb864ac18d9..a217c952efa 100644
--- a/InvenTree/machine/machine_type.py
+++ b/InvenTree/machine/machine_type.py
@@ -122,13 +122,14 @@ def get_machines(self, **kwargs):
Kwargs:
name: Machine name
machine_type: Machine type definition (class)
- driver: Machine driver (class)
initialized: (bool, default: True)
active: (bool)
base_driver: base driver (class)
"""
from machine import registry
+ kwargs.pop('driver', None)
+
return registry.get_machines(driver=self, **kwargs)
def handle_error(self, error: Union[Exception, str]):
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index 0b44e8dca4d..873b1fe7d66 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -1,8 +1,19 @@
"""Label printing machine type."""
+from typing import Union, cast
+
+from django.db.models.query import QuerySet
+from django.http import HttpResponse, JsonResponse
from django.utils.translation import gettext_lazy as _
+from PIL.Image import Image
+from rest_framework import serializers
+from rest_framework.request import Request
+
+from label.models import LabelTemplate
from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
+from plugin import registry as plg_registry
+from plugin.base.label.mixins import LabelItemType, LabelPrintingMixin
class BaseLabelPrintingDriver(BaseDriver):
@@ -10,15 +21,166 @@ class BaseLabelPrintingDriver(BaseDriver):
machine_type = 'label_printer'
- def print_label(self):
- """This function must be overridden."""
- raise NotImplementedError('The `print_label` function must be overridden!')
-
- def print_labels(self):
- """This function must be overridden."""
- raise NotImplementedError('The `print_labels` function must be overridden!')
-
- requires_override = [print_label]
+ # Run printing functions by default in a background worker.
+ USE_BACKGROUND_WORKER = True
+
+ def print_label(
+ self,
+ machine: 'LabelPrintingMachineType',
+ label: LabelTemplate,
+ item: LabelItemType,
+ request: Request,
+ **kwargs,
+ ) -> None:
+ """Print a single label with the provided template and item.
+
+ Arguments:
+ machine: The LabelPrintingMachine instance that should be used for printing
+ label: The LabelTemplate object to use for printing
+ item: The database item to print (e.g. StockItem instance)
+ request: The HTTP request object which triggered this print job
+
+ Keyword Arguments:
+ printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
+
+ Note that the supplied args/kwargs may be different if the driver overrides the print_labels() method.
+ """
+ pass
+
+ def print_labels(
+ self,
+ machine: 'LabelPrintingMachineType',
+ label: LabelTemplate,
+ items: QuerySet[LabelItemType],
+ request: Request,
+ **kwargs,
+ ) -> Union[None, JsonResponse]:
+ """Print one or more labels with the provided template and items.
+
+ Arguments:
+ machine: The LabelPrintingMachine instance that should be used for printing
+ label: The LabelTemplate object to use for printing
+ items: The list of database items to print (e.g. StockItem instances)
+ request: The HTTP request object which triggered this print job
+
+ Keyword Arguments:
+ printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
+
+ Returns:
+ If USE_BACKGROUND_WORKER=False, a JsonResponse object which indicates outcome to the user, otherwise None
+
+ The default implementation simply calls print_label() for each label, producing multiple single label output "jobs"
+ but this can be overridden by the particular driver.
+ """
+ for item in items:
+ self.print_label(machine, label, item, request, **kwargs)
+
+ def get_printers(
+ self, label: LabelTemplate, items: QuerySet[LabelItemType], **kwargs
+ ) -> list[BaseMachineType]:
+ """Get all printers that would be available to print this job.
+
+ By default all printers that are initialized using this driver are returned.
+
+ Arguments:
+ label: The LabelTemplate object to use for printing
+ items: The lost of database items to print (e.g. StockItem instances)
+ """
+ return self.get_machines()
+
+ def get_printing_options_serializer(
+ self, request: Request, *args, **kwargs
+ ) -> Union[serializers.Serializer, None]:
+ """Return a serializer class instance with dynamic printing options.
+
+ Arguments:
+ request: The request made to print a label or interfering the available serializer fields via an OPTIONS request
+ *args, **kwargs: need to be passed to the serializer instance
+
+ Returns:
+ A class instance of a DRF serializer class, by default this an instance of
+ self.PrintingOptionsSerializer using the *args, **kwargs if existing for this driver
+ """
+ serializer = getattr(self, 'PrintingOptionsSerializer', None)
+
+ if not serializer:
+ return None
+
+ return serializer(*args, **kwargs)
+
+ # --- helper functions
+ @property
+ def machine_plugin(self) -> LabelPrintingMixin:
+ """Returns the builtin machine label printing plugin."""
+ plg = plg_registry.get_plugin('inventreelabelmachine')
+ return cast(LabelPrintingMixin, plg)
+
+ def render_to_pdf(
+ self, label: LabelTemplate, item: LabelItemType, request: Request, **kwargs
+ ) -> HttpResponse:
+ """Render this label to PDF format.
+
+ Arguments:
+ label: The LabelTemplate object to render
+ item: The item to render the label with
+ request: The HTTP request object which triggered this print job
+ """
+ label.object_to_print = item
+ response = self.machine_plugin.render_to_pdf(label, request, **kwargs)
+ label.object_to_print = None
+ return response
+
+ def render_to_pdf_data(
+ self, label: LabelTemplate, item: LabelItemType, request: Request, **kwargs
+ ) -> bytes:
+ """Render this label to PDF and return it as bytes.
+
+ Arguments:
+ label: The LabelTemplate object to render
+ item: The item to render the label with
+ request: The HTTP request object which triggered this print job
+ """
+ return (
+ self.render_to_pdf(label, item, request, **kwargs)
+ .get_document() # type: ignore
+ .write_pdf()
+ )
+
+ def render_to_html(
+ self, label: LabelTemplate, item: LabelItemType, request: Request, **kwargs
+ ) -> str:
+ """Render this label to HTML format.
+
+ Arguments:
+ label: The LabelTemplate object to render
+ item: The item to render the label with
+ request: The HTTP request object which triggered this print job
+ """
+ label.object_to_print = item
+ html = self.machine_plugin.render_to_html(label, request, **kwargs)
+ label.object_to_print = None
+ return html
+
+ def render_to_png(
+ self, label: LabelTemplate, item: LabelItemType, request: Request, **kwargs
+ ) -> Image:
+ """Render this label to PNG format.
+
+ Arguments:
+ label: The LabelTemplate object to render
+ item: The item to render the label with
+ request: The HTTP request object which triggered this print job
+
+ Keyword Arguments:
+ pdf_data: The pdf document as bytes (optional)
+ dpi: The dpi used to render the image (optional)
+ """
+ label.object_to_print = item
+ png = self.machine_plugin.render_to_png(label, request, **kwargs)
+ label.object_to_print = None
+ return png
+
+ required_overrides = [[print_label, print_labels]]
class LabelPrintingMachineType(BaseMachineType):
diff --git a/InvenTree/machine/registry.py b/InvenTree/machine/registry.py
index 76f4142f104..f949f299b88 100644
--- a/InvenTree/machine/registry.py
+++ b/InvenTree/machine/registry.py
@@ -209,5 +209,13 @@ def get_machine(self, pk: Union[str, UUID]):
"""Get machine from registry by pk."""
return self.machines.get(str(pk), None)
+ def get_drivers(self, machine_type: str):
+ """Get all drivers for a specific machine type."""
+ return [
+ driver
+ for driver in self.driver_instances.values()
+ if driver.machine_type == machine_type
+ ]
+
registry: MachineRegistry = MachineRegistry()
diff --git a/InvenTree/plugin/base/label/mixins.py b/InvenTree/plugin/base/label/mixins.py
index 975cf669857..daef8d590c2 100644
--- a/InvenTree/plugin/base/label/mixins.py
+++ b/InvenTree/plugin/base/label/mixins.py
@@ -2,17 +2,23 @@
from typing import Union
+from django.db.models.query import QuerySet
from django.http import JsonResponse
import pdf2image
from rest_framework import serializers
from rest_framework.request import Request
+from build.models import BuildLine
from common.models import InvenTreeSetting
from InvenTree.tasks import offload_task
from label.models import LabelTemplate
+from part.models import Part
from plugin.base.label import label as plugin_label
from plugin.helpers import MixinNotImplementedError
+from stock.models import StockItem, StockLocation
+
+LabelItemType = Union[StockItem, StockLocation, Part, BuildLine]
class LabelPrintingMixin:
@@ -77,9 +83,8 @@ def render_to_png(self, label: LabelTemplate, request=None, **kwargs):
def print_labels(
self,
label: LabelTemplate,
- items: list,
+ items: QuerySet[LabelItemType],
request: Request,
- printing_options: dict,
**kwargs,
):
"""Print one or more labels with the provided template and items.
diff --git a/InvenTree/plugin/builtin/labels/inventree_machine.py b/InvenTree/plugin/builtin/labels/inventree_machine.py
new file mode 100644
index 00000000000..02ae4779b84
--- /dev/null
+++ b/InvenTree/plugin/builtin/labels/inventree_machine.py
@@ -0,0 +1,112 @@
+"""Label printing plugin that provides support for printing using a label printer machine."""
+
+from typing import cast
+
+from django.http import JsonResponse
+from django.utils.translation import gettext_lazy as _
+
+from rest_framework import serializers
+
+from InvenTree.serializers import DependentField
+from InvenTree.tasks import offload_task
+from label.models import LabelTemplate
+from machine.machine_type import BaseMachineType
+from machine.machine_types import BaseLabelPrintingDriver, LabelPrintingMachineType
+from plugin import InvenTreePlugin
+from plugin.machine import registry
+from plugin.mixins import LabelPrintingMixin
+
+
+def get_machine_and_driver(machine_pk: str):
+ """Get the driver by machine pk and ensure that it is a label printing driver."""
+ machine = registry.get_machine(machine_pk)
+
+ if machine is None:
+ return None, None
+
+ if machine.SLUG != 'label_printer':
+ return None, None
+
+ machine = cast(LabelPrintingMachineType, machine)
+ driver = machine.driver
+
+ if driver is None:
+ return machine, None
+
+ return machine, cast(BaseLabelPrintingDriver, driver)
+
+
+class InvenTreeLabelPlugin(LabelPrintingMixin, InvenTreePlugin):
+ """Builtin plugin for machine label printing.
+
+ This enables machines to print labels.
+ """
+
+ NAME = 'InvenTreeLabelMachine'
+ TITLE = _('InvenTree machine label printer')
+ DESCRIPTION = _('Provides support for printing using a machine')
+ VERSION = '1.0.0'
+ AUTHOR = _('InvenTree contributors')
+
+ def print_labels(self, label: LabelTemplate, items, request, **kwargs):
+ """Print labels implementation that calls the correct machine driver print_labels method."""
+ machine, driver = get_machine_and_driver(
+ kwargs['printing_options'].get('machine', '')
+ )
+
+ if driver is None or machine is None:
+ return None
+
+ print_kwargs = {
+ **kwargs,
+ 'printing_options': kwargs['printing_options'].get('driver_options', {}),
+ }
+
+ if driver.USE_BACKGROUND_WORKER is False:
+ return driver.print_labels(machine, label, items, request, **print_kwargs)
+
+ offload_task(
+ driver.print_labels, machine, label, items, request, **print_kwargs
+ )
+
+ return JsonResponse({
+ 'success': True,
+ 'message': f'{len(items)} labels printed',
+ })
+
+ class PrintingOptionsSerializer(serializers.Serializer):
+ """Printing options serializer that adds a machine select and the machines options."""
+
+ def __init__(self, *args, **kwargs):
+ """Custom __init__ method to dynamically override the machine choices based on the request."""
+ super().__init__(*args, **kwargs)
+
+ view = kwargs['context']['view']
+ template = view.get_object()
+ items_to_print = view.get_items()
+
+ machines: list[BaseMachineType] = []
+ for driver in cast(
+ list[BaseLabelPrintingDriver], registry.get_drivers('label_printer')
+ ):
+ machines.extend(driver.get_printers(template, items_to_print))
+ self.fields['machine'].choices = [(m.pk, m.name) for m in machines]
+
+ machine = serializers.ChoiceField(choices=[])
+
+ driver_options = DependentField(
+ depends_on=['machine'],
+ field_serializer='get_driver_options',
+ required=False,
+ )
+
+ def get_driver_options(self, fields):
+ """Returns the selected machines serializer."""
+ _, driver = get_machine_and_driver(fields['machine'])
+
+ if driver is None:
+ return None
+
+ return driver.get_printing_options_serializer(
+ self.context['request'], context=self.context
+ )
From 58ad80b4597036600567d9793034764a73a56710 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Fri, 2 Feb 2024 15:55:44 +0000
Subject: [PATCH 58/86] Remove translated column names because they are now
retrieved from the api
---
.../src/tables/machine/MachineListTable.tsx | 13 ++-----------
1 file changed, 2 insertions(+), 11 deletions(-)
diff --git a/src/frontend/src/tables/machine/MachineListTable.tsx b/src/frontend/src/tables/machine/MachineListTable.tsx
index ea8fed09629..390e493d91a 100644
--- a/src/frontend/src/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/tables/machine/MachineListTable.tsx
@@ -402,7 +402,6 @@ export function MachineListTable({
() => [
{
accessor: 'name',
- title: t`Machine`,
sortable: true,
render: function (record) {
return (
@@ -420,7 +419,6 @@ export function MachineListTable({
},
{
accessor: 'machine_type',
- title: t`Machine Type`,
sortable: true,
render: (record) => {
const machineType = machineTypes?.find(
@@ -438,7 +436,6 @@ export function MachineListTable({
},
{
accessor: 'driver',
- title: t`Machine Driver`,
sortable: true,
render: (record) => {
const driver = machineDrivers?.find((d) => d.slug === record.driver);
@@ -451,16 +448,13 @@ export function MachineListTable({
}
},
BooleanColumn({
- accessor: 'initialized',
- title: t`Initialized`
+ accessor: 'initialized'
}),
BooleanColumn({
- accessor: 'active',
- title: t`Active`
+ accessor: 'active'
}),
{
accessor: 'status',
- title: t`Status`,
sortable: false,
render: (record) => {
const renderer = TableStatusRenderer(
@@ -578,12 +572,10 @@ export function MachineListTable({
tableFilters: [
{
name: 'active',
- label: t`Active`,
type: 'boolean'
},
{
name: 'machine_type',
- label: t`Machine Type`,
type: 'choice',
choiceFunction: () =>
machineTypes
@@ -592,7 +584,6 @@ export function MachineListTable({
},
{
name: 'driver',
- label: t`Machine Driver`,
type: 'choice',
choiceFunction: () =>
machineDrivers
From d4ed13e6a660ba8f6d8f668fd895d6b0cb5473fc Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sat, 3 Feb 2024 17:41:15 +0000
Subject: [PATCH 59/86] Added printer location setting
---
InvenTree/InvenTree/serializers.py | 11 ++++++--
.../machine_types/LabelPrintingMachineType.py | 26 +++++++++++++++++--
InvenTree/machine/serializers.py | 10 ++-----
.../builtin/labels/inventree_machine.py | 23 +++++++++++++---
.../src/components/items/ActionDropdown.tsx | 8 ++++--
5 files changed, 60 insertions(+), 18 deletions(-)
diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py
index 4f13ec9face..e0e6d4d0b30 100644
--- a/InvenTree/InvenTree/serializers.py
+++ b/InvenTree/InvenTree/serializers.py
@@ -155,8 +155,15 @@ def visit_parent(node):
# check if the request data contains the dependent fields, otherwise skip getting the child
for f in self.depends_on:
- if not data.get(f, None):
- return
+ if data.get(f, None) is None:
+ if (
+ self.parent
+ and (v := getattr(self.parent.fields[f], 'default', None))
+ is not None
+ ):
+ data[f] = v
+ else:
+ return
# partially validate the data for options requests that set raise_exception while calling .get_child(...)
if raise_exception:
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index 873b1fe7d66..2491c49a612 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -14,6 +14,7 @@
from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
from plugin import registry as plg_registry
from plugin.base.label.mixins import LabelItemType, LabelPrintingMixin
+from stock.models import StockLocation
class BaseLabelPrintingDriver(BaseDriver):
@@ -77,7 +78,7 @@ def print_labels(
def get_printers(
self, label: LabelTemplate, items: QuerySet[LabelItemType], **kwargs
- ) -> list[BaseMachineType]:
+ ) -> list['LabelPrintingMachineType']:
"""Get all printers that would be available to print this job.
By default all printers that are initialized using this driver are returned.
@@ -85,8 +86,11 @@ def get_printers(
Arguments:
label: The LabelTemplate object to use for printing
items: The lost of database items to print (e.g. StockItem instances)
+
+ Keyword Arguments:
+ request: The django request used to make the get printers request
"""
- return self.get_machines()
+ return cast(list['LabelPrintingMachineType'], self.get_machines())
def get_printing_options_serializer(
self, request: Request, *args, **kwargs
@@ -192,6 +196,14 @@ class LabelPrintingMachineType(BaseMachineType):
base_driver = BaseLabelPrintingDriver
+ MACHINE_SETTINGS = {
+ 'LOCATION': {
+ 'name': _('Printer Location'),
+ 'description': _('Scope the printer to a specific location'),
+ 'model': 'stock.stocklocation',
+ }
+ }
+
class LabelPrinterStatus(MachineStatus):
"""Label printer status codes."""
@@ -204,3 +216,13 @@ class LabelPrinterStatus(MachineStatus):
MACHINE_STATUS = LabelPrinterStatus
default_machine_status = LabelPrinterStatus.DISCONNECTED
+
+ @property
+ def location(self):
+ """Access the machines location instance using this property."""
+ location_pk = self.get_setting('LOCATION', 'M')
+
+ if not location_pk:
+ return None
+
+ return StockLocation.objects.get(pk=location_pk)
diff --git a/InvenTree/machine/serializers.py b/InvenTree/machine/serializers.py
index 02df3f6caa9..16e024aec07 100644
--- a/InvenTree/machine/serializers.py
+++ b/InvenTree/machine/serializers.py
@@ -95,16 +95,10 @@ class MachineSettingSerializer(GenericReferencedSettingSerializer):
EXTRA_FIELDS = ['config_type']
def __init__(self, *args, **kwargs):
- """Custom init method to remove unwanted fields."""
+ """Custom init method to make the config_type field read only."""
super().__init__(*args, **kwargs)
- # remove unwanted fields
- unwanted_fields = ['pk', 'model_name', 'api_url', 'typ']
- for f in unwanted_fields:
- if f in self.Meta.fields:
- self.Meta.fields.remove(f)
-
- self.Meta.read_only_fields = ['config_type']
+ self.Meta.read_only_fields = ['config_type'] # type: ignore
class BaseMachineClassSerializer(serializers.Serializer):
diff --git a/InvenTree/plugin/builtin/labels/inventree_machine.py b/InvenTree/plugin/builtin/labels/inventree_machine.py
index 02ae4779b84..9aa55977f15 100644
--- a/InvenTree/plugin/builtin/labels/inventree_machine.py
+++ b/InvenTree/plugin/builtin/labels/inventree_machine.py
@@ -10,7 +10,6 @@
from InvenTree.serializers import DependentField
from InvenTree.tasks import offload_task
from label.models import LabelTemplate
-from machine.machine_type import BaseMachineType
from machine.machine_types import BaseLabelPrintingDriver, LabelPrintingMachineType
from plugin import InvenTreePlugin
from plugin.machine import registry
@@ -85,12 +84,28 @@ def __init__(self, *args, **kwargs):
template = view.get_object()
items_to_print = view.get_items()
- machines: list[BaseMachineType] = []
+ machines: list[LabelPrintingMachineType] = []
for driver in cast(
list[BaseLabelPrintingDriver], registry.get_drivers('label_printer')
):
- machines.extend(driver.get_printers(template, items_to_print))
- self.fields['machine'].choices = [(m.pk, m.name) for m in machines]
+ machines.extend(
+ driver.get_printers(
+ template, items_to_print, request=kwargs['context']['request']
+ )
+ )
+ choices = [(m.pk, self.get_printer_name(m)) for m in machines]
+ self.fields['machine'].choices = choices
+ if len(choices) > 0:
+ self.fields['machine'].default = choices[0][0]
+
+ def get_printer_name(self, machine: LabelPrintingMachineType):
+ """Construct the printers name."""
+ name = machine.name
+
+ if machine.location:
+ name += f' @ {machine.location.name}'
+
+ return name
machine = serializers.ChoiceField(choices=[])
diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx
index f26408c6e1e..833273ca647 100644
--- a/src/frontend/src/components/items/ActionDropdown.tsx
+++ b/src/frontend/src/components/items/ActionDropdown.tsx
@@ -62,8 +62,12 @@ export function ActionDropdown({
{actions.map((action) =>
action.disabled ? null : (
-
-
+
+
{
From a9808c64911b287ee56368b10b74d9d8b0dde576 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sat, 3 Feb 2024 19:11:31 +0000
Subject: [PATCH 60/86] Save last 10 used printer machine per user and sort
them in the printing dialog
---
InvenTree/common/models.py | 5 ++
InvenTree/machine/machine_type.py | 4 ++
.../builtin/labels/inventree_machine.py | 56 ++++++++++++++++++-
3 files changed, 63 insertions(+), 2 deletions(-)
diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py
index 84d02cba269..8b0fc88035f 100644
--- a/InvenTree/common/models.py
+++ b/InvenTree/common/models.py
@@ -2339,6 +2339,11 @@ class Meta:
'default': True,
'validator': bool,
},
+ 'LAST_USED_PRINTING_MACHINES': {
+ 'name': _('Last used printing machines'),
+ 'description': _('Save the last used printing machines for a user'),
+ 'default': '',
+ },
}
typ = 'user'
diff --git a/InvenTree/machine/machine_type.py b/InvenTree/machine/machine_type.py
index a217c952efa..501565001d7 100644
--- a/InvenTree/machine/machine_type.py
+++ b/InvenTree/machine/machine_type.py
@@ -215,6 +215,10 @@ def __str__(self):
"""String representation of a machine."""
return f'{self.name}'
+ def __repr__(self):
+ """Python representation of a machine."""
+ return f'<{self.__class__.__name__}: {self.name}>'
+
# --- properties
@property
def machine_config(self):
diff --git a/InvenTree/plugin/builtin/labels/inventree_machine.py b/InvenTree/plugin/builtin/labels/inventree_machine.py
index 9aa55977f15..eae8a1c9906 100644
--- a/InvenTree/plugin/builtin/labels/inventree_machine.py
+++ b/InvenTree/plugin/builtin/labels/inventree_machine.py
@@ -7,6 +7,7 @@
from rest_framework import serializers
+from common.models import InvenTreeUserSetting
from InvenTree.serializers import DependentField
from InvenTree.tasks import offload_task
from label.models import LabelTemplate
@@ -35,6 +36,20 @@ def get_machine_and_driver(machine_pk: str):
return machine, cast(BaseLabelPrintingDriver, driver)
+def get_last_used_printers(user):
+ """Get the last used printers for a specific user."""
+ return [
+ printer
+ for printer in cast(
+ str,
+ InvenTreeUserSetting.get_setting(
+ 'LAST_USED_PRINTING_MACHINES', '', user=user
+ ),
+ ).split(',')
+ if printer
+ ]
+
+
class InvenTreeLabelPlugin(LabelPrintingMixin, InvenTreePlugin):
"""Builtin plugin for machine label printing.
@@ -61,6 +76,20 @@ def print_labels(self, label: LabelTemplate, items, request, **kwargs):
'printing_options': kwargs['printing_options'].get('driver_options', {}),
}
+ # save the current used printer as last used printer
+ # only the last ten used printers are saved so that this list doesn't grow infinitely
+ last_used_printers = get_last_used_printers(request.user)
+ machine_pk = str(machine.pk)
+ if machine_pk in last_used_printers:
+ last_used_printers.remove(machine_pk)
+ last_used_printers.insert(0, machine_pk)
+ InvenTreeUserSetting.set_setting(
+ 'LAST_USED_PRINTING_MACHINES',
+ ','.join(last_used_printers[:10]),
+ user=request.user,
+ )
+
+ # execute the print job
if driver.USE_BACKGROUND_WORKER is False:
return driver.print_labels(machine, label, items, request, **print_kwargs)
@@ -84,6 +113,7 @@ def __init__(self, *args, **kwargs):
template = view.get_object()
items_to_print = view.get_items()
+ # get all available printers for each driver
machines: list[LabelPrintingMachineType] = []
for driver in cast(
list[BaseLabelPrintingDriver], registry.get_drivers('label_printer')
@@ -93,11 +123,33 @@ def __init__(self, *args, **kwargs):
template, items_to_print, request=kwargs['context']['request']
)
)
- choices = [(m.pk, self.get_printer_name(m)) for m in machines]
- self.fields['machine'].choices = choices
+
+ # sort the last used printers for the user to the top
+ user = kwargs['context']['request'].user
+ last_used_printers = get_last_used_printers(user)[::-1]
+ machines = sorted(
+ machines,
+ key=lambda m: last_used_printers.index(str(m.pk))
+ if str(m.pk) in last_used_printers
+ else -1,
+ reverse=True,
+ )
+
+ choices = [(str(m.pk), self.get_printer_name(m)) for m in machines]
+
+ # if there are choices available, use the first as default
if len(choices) > 0:
self.fields['machine'].default = choices[0][0]
+ # add 'last used' flag to the first choice
+ if choices[0][0] in last_used_printers:
+ choices[0] = (
+ choices[0][0],
+ choices[0][1] + ' (' + _('last used') + ')',
+ )
+
+ self.fields['machine'].choices = choices
+
def get_printer_name(self, machine: LabelPrintingMachineType):
"""Construct the printers name."""
name = machine.name
From d4c188481cc6f57f1abecf84f8518653c641c8c1 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sat, 3 Feb 2024 19:46:52 +0000
Subject: [PATCH 61/86] Added BasePrintingOptionsSerializer for common options
---
.../machine_types/LabelPrintingMachineType.py | 19 +++++++++++++++++--
.../builtin/labels/inventree_machine.py | 1 +
2 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index 2491c49a612..df22ca78306 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -43,6 +43,8 @@ def print_label(
Keyword Arguments:
printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
+ by default the following options are available:
+ - copies: number of copies to print for the label
Note that the supplied args/kwargs may be different if the driver overrides the print_labels() method.
"""
@@ -66,6 +68,8 @@ def print_labels(
Keyword Arguments:
printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
+ by default the following options are available:
+ - copies: number of copies to print for each label
Returns:
If USE_BACKGROUND_WORKER=False, a JsonResponse object which indicates outcome to the user, otherwise None
@@ -94,7 +98,7 @@ def get_printers(
def get_printing_options_serializer(
self, request: Request, *args, **kwargs
- ) -> Union[serializers.Serializer, None]:
+ ) -> 'BaseLabelPrintingDriver.BasePrintingOptionsSerializer':
"""Return a serializer class instance with dynamic printing options.
Arguments:
@@ -108,7 +112,9 @@ def get_printing_options_serializer(
serializer = getattr(self, 'PrintingOptionsSerializer', None)
if not serializer:
- return None
+ return BaseLabelPrintingDriver.BasePrintingOptionsSerializer(
+ *args, **kwargs
+ )
return serializer(*args, **kwargs)
@@ -186,6 +192,15 @@ def render_to_png(
required_overrides = [[print_label, print_labels]]
+ class BasePrintingOptionsSerializer(serializers.Serializer):
+ """Printing options base serializer that implements common options."""
+
+ copies = serializers.IntegerField(
+ default=1,
+ label=_('Copies'),
+ help_text=_('Number of copies to print for each label'),
+ )
+
class LabelPrintingMachineType(BaseMachineType):
"""Label printer machine type, is a direct integration to print labels for various items."""
diff --git a/InvenTree/plugin/builtin/labels/inventree_machine.py b/InvenTree/plugin/builtin/labels/inventree_machine.py
index eae8a1c9906..826564dad9f 100644
--- a/InvenTree/plugin/builtin/labels/inventree_machine.py
+++ b/InvenTree/plugin/builtin/labels/inventree_machine.py
@@ -162,6 +162,7 @@ def get_printer_name(self, machine: LabelPrintingMachineType):
machine = serializers.ChoiceField(choices=[])
driver_options = DependentField(
+ label=_('Options'),
depends_on=['machine'],
field_serializer='get_driver_options',
required=False,
From 8e4ad5c2c70c4e2db09ec22366c95b9e8c1e30f3 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sat, 3 Feb 2024 20:38:36 +0000
Subject: [PATCH 62/86] Fix not printing_options are not properly casted to its
internal value
---
InvenTree/label/api.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py
index 694e3b9d906..f9b42e70693 100644
--- a/InvenTree/label/api.py
+++ b/InvenTree/label/api.py
@@ -233,7 +233,10 @@ def print(self, request, items_to_print):
# The plugin is responsible for handling the request and returning a response.
result = plugin.print_labels(
- label, items_to_print, request, printing_options=request.data
+ label,
+ items_to_print,
+ request,
+ printing_options=(serializer.data if serializer else {}),
)
if isinstance(result, JsonResponse):
From da1aabb3d9b572a381a1244f2577ec200ae53b95 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sat, 3 Feb 2024 21:51:07 +0000
Subject: [PATCH 63/86] Fix type
---
InvenTree/machine/machine_types/LabelPrintingMachineType.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index df22ca78306..c7630ea9a7c 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -114,7 +114,7 @@ def get_printing_options_serializer(
if not serializer:
return BaseLabelPrintingDriver.BasePrintingOptionsSerializer(
*args, **kwargs
- )
+ ) # type: ignore
return serializer(*args, **kwargs)
From e4b55696fe031eef51269e1365055ed1e9950760 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sun, 4 Feb 2024 15:36:30 +0000
Subject: [PATCH 64/86] Improved machine docs
---
InvenTree/machine/machine_type.py | 75 ++++---
.../machine_types/LabelPrintingMachineType.py | 95 ++++----
InvenTree/plugin/machine/__init__.py | 11 +-
InvenTree/plugin/machine/machine_types.py | 3 +
docs/docs/extend/machines/label_printer.md | 35 +++
docs/docs/extend/machines/overview.md | 202 +++++++++++++++++
docs/docs/extend/plugins/machines.md | 204 ------------------
docs/mkdocs.yml | 11 +-
docs/requirements.txt | 1 +
9 files changed, 361 insertions(+), 276 deletions(-)
create mode 100644 InvenTree/plugin/machine/machine_types.py
create mode 100644 docs/docs/extend/machines/label_printer.md
create mode 100644 docs/docs/extend/machines/overview.md
delete mode 100644 docs/docs/extend/plugins/machines.md
diff --git a/InvenTree/machine/machine_type.py b/InvenTree/machine/machine_type.py
index 501565001d7..00fa66ba14b 100644
--- a/InvenTree/machine/machine_type.py
+++ b/InvenTree/machine/machine_type.py
@@ -35,25 +35,27 @@ class MachineStatus(StatusCode):
Additionally there are helpers to access all additional attributes `text`, `label`, `color`.
Status code ranges:
+ ```
1XX - Everything fine
2XX - Warnings (e.g. ink is about to become empty)
3XX - Something wrong with the machine (e.g. no labels are remaining on the spool)
4XX - Something wrong with the driver (e.g. cannot connect to the machine)
5XX - Unknown issues
+ ```
"""
pass
class BaseDriver(ClassValidationMixin, ClassProviderMixin):
- """Base class for machine drivers.
+ """Base class for all machine drivers.
Attributes:
- SLUG: Slug string for identifying a machine
- NAME: User friendly name for displaying
- DESCRIPTION: Description of what this driver does
+ SLUG: Slug string for identifying the driver in format /[a-z-]+/ (required)
+ NAME: User friendly name for displaying (required)
+ DESCRIPTION: Description of what this driver does (required)
- MACHINE_SETTINGS: Driver specific settings dict (optional)
+ MACHINE_SETTINGS: Driver specific settings dict
"""
SLUG: str
@@ -109,7 +111,7 @@ def restart_machine(self, machine: 'BaseMachineType'):
"""This method gets called on manual machine restart e.g. by using the restart machine action in the Admin Center.
Note:
- machine.restart_required gets set to False again
+ `machine.restart_required` gets set to False again before this function is called
Arguments:
machine: Machine instance
@@ -119,12 +121,12 @@ def restart_machine(self, machine: 'BaseMachineType'):
def get_machines(self, **kwargs):
"""Return all machines using this driver (By default only initialized machines).
- Kwargs:
- name: Machine name
- machine_type: Machine type definition (class)
- initialized: (bool, default: True)
- active: (bool)
- base_driver: base driver (class)
+ Keyword Arguments:
+ name (str): Machine name
+ machine_type (BaseMachineType): Machine type definition (class)
+ initialized (bool): default: True
+ active (bool): machine needs to be active
+ base_driver (BaseDriver): base driver (class)
"""
from machine import registry
@@ -133,7 +135,11 @@ def get_machines(self, **kwargs):
return registry.get_machines(driver=self, **kwargs)
def handle_error(self, error: Union[Exception, str]):
- """Handle driver error."""
+ """Handle driver error.
+
+ Arguments:
+ error: Exception or string
+ """
self.errors.append(error)
@@ -141,9 +147,9 @@ class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
"""Base class for machine types.
Attributes:
- SLUG: Slug string for identifying a machine type
- NAME: User friendly name for displaying
- DESCRIPTION: Description of what this machine type can do (default: "")
+ SLUG: Slug string for identifying the machine type in format /[a-z-]+/ (required)
+ NAME: User friendly name for displaying (required)
+ DESCRIPTION: Description of what this machine type can do (required)
base_driver: Reference to the base driver for this machine type
@@ -222,7 +228,7 @@ def __repr__(self):
# --- properties
@property
def machine_config(self):
- """Machine_config property."""
+ """Machine_config property which is a reference to the database entry."""
# always fetch the machine_config if needed to ensure we get the newest reference
from .models import MachineConfig
@@ -230,12 +236,12 @@ def machine_config(self):
@property
def name(self):
- """Name property."""
+ """The machines name."""
return self.machine_config.name
@property
def active(self):
- """Active property."""
+ """The machines active status."""
return self.machine_config.active
# --- hook functions
@@ -263,7 +269,11 @@ def initialize(self):
self.handle_error(e)
def update(self, old_state: dict[str, Any]):
- """Machine update function, gets called if the machine itself changes or their settings."""
+ """Machine update function, gets called if the machine itself changes or their settings.
+
+ Arguments:
+ old_state: Dict holding the old machine state before update
+ """
if self.driver is None:
return
@@ -285,15 +295,21 @@ def restart(self):
# --- helper functions
def handle_error(self, error: Union[Exception, str]):
- """Helper function for capturing errors with the machine."""
+ """Helper function for capturing errors with the machine.
+
+ Arguments:
+ error: Exception or string
+ """
self.errors.append(error)
- def get_setting(self, key: str, config_type_str: Literal['M', 'D'], cache=False):
+ def get_setting(
+ self, key: str, config_type_str: Literal['M', 'D'], cache: bool = False
+ ):
"""Return the 'value' of the setting associated with this machine.
Arguments:
key: The 'name' of the setting value to be retrieved
- config_type: Either "M" (machine scoped settings) or "D" (driver scoped settings)
+ config_type_str: Either "M" (machine scoped settings) or "D" (driver scoped settings)
cache: Whether to use RAM cached value (default = False)
"""
from machine.models import MachineSetting
@@ -306,12 +322,12 @@ def get_setting(self, key: str, config_type_str: Literal['M', 'D'], cache=False)
cache=cache,
)
- def set_setting(self, key: str, config_type_str: Literal['M', 'D'], value):
+ def set_setting(self, key: str, config_type_str: Literal['M', 'D'], value: Any):
"""Set plugin setting value by key.
Arguments:
key: The 'name' of the setting to set
- config_type: Either "M" (machine scoped settings) or "D" (driver scoped settings)
+ config_type_str: Either "M" (machine scoped settings) or "D" (driver scoped settings)
value: The 'value' of the setting
"""
from machine.models import MachineSetting
@@ -351,9 +367,16 @@ def set_status(self, status: MachineStatus):
"""Set the machine status code. There are predefined ones for each MachineType.
Import the MachineType to access it's `MACHINE_STATUS` enum.
+
+ Arguments:
+ status: The new MachineStatus code to set
"""
self.status = status
def set_status_text(self, status_text: str):
- """Set the machine status text. It can be any arbitrary text."""
+ """Set the machine status text. It can be any arbitrary text.
+
+ Arguments:
+ status_text: The new status text to set
+ """
self.status_text = status_text
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index c7630ea9a7c..936430f4d9d 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -18,11 +18,14 @@
class BaseLabelPrintingDriver(BaseDriver):
- """Base label printing driver."""
+ """Base label printing driver.
+
+ Attributes:
+ USE_BACKGROUND_WORKER (bool): If True, the `print_label()` and `print_labels()` methods will be run in a background worker (default: True)
+ """
machine_type = 'label_printer'
- # Run printing functions by default in a background worker.
USE_BACKGROUND_WORKER = True
def print_label(
@@ -42,7 +45,7 @@ def print_label(
request: The HTTP request object which triggered this print job
Keyword Arguments:
- printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
+ printing_options (dict): The printing options set for this print job defined in the PrintingOptionsSerializer
by default the following options are available:
- copies: number of copies to print for the label
@@ -67,12 +70,12 @@ def print_labels(
request: The HTTP request object which triggered this print job
Keyword Arguments:
- printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
+ printing_options (dict): The printing options set for this print job defined in the PrintingOptionsSerializer
by default the following options are available:
- copies: number of copies to print for each label
Returns:
- If USE_BACKGROUND_WORKER=False, a JsonResponse object which indicates outcome to the user, otherwise None
+ If `USE_BACKGROUND_WORKER=False`, a JsonResponse object which indicates outcome to the user, otherwise None
The default implementation simply calls print_label() for each label, producing multiple single label output "jobs"
but this can be overridden by the particular driver.
@@ -92,43 +95,37 @@ def get_printers(
items: The lost of database items to print (e.g. StockItem instances)
Keyword Arguments:
- request: The django request used to make the get printers request
+ request (Request): The django request used to make the get printers request
"""
return cast(list['LabelPrintingMachineType'], self.get_machines())
def get_printing_options_serializer(
self, request: Request, *args, **kwargs
- ) -> 'BaseLabelPrintingDriver.BasePrintingOptionsSerializer':
+ ) -> 'BaseLabelPrintingDriver.PrintingOptionsSerializer':
"""Return a serializer class instance with dynamic printing options.
Arguments:
request: The request made to print a label or interfering the available serializer fields via an OPTIONS request
- *args, **kwargs: need to be passed to the serializer instance
+
+ Note:
+ `*args`, `**kwargs` needs to be passed to the serializer instance
Returns:
- A class instance of a DRF serializer class, by default this an instance of
- self.PrintingOptionsSerializer using the *args, **kwargs if existing for this driver
+ A class instance of a DRF serializer class, by default this an instance of self.PrintingOptionsSerializer using the *args, **kwargs if existing for this driver
"""
- serializer = getattr(self, 'PrintingOptionsSerializer', None)
-
- if not serializer:
- return BaseLabelPrintingDriver.BasePrintingOptionsSerializer(
- *args, **kwargs
- ) # type: ignore
-
- return serializer(*args, **kwargs)
+ return self.PrintingOptionsSerializer(*args, **kwargs)
# --- helper functions
@property
def machine_plugin(self) -> LabelPrintingMixin:
- """Returns the builtin machine label printing plugin."""
+ """Returns the builtin machine label printing plugin that manages printing through machines."""
plg = plg_registry.get_plugin('inventreelabelmachine')
return cast(LabelPrintingMixin, plg)
def render_to_pdf(
self, label: LabelTemplate, item: LabelItemType, request: Request, **kwargs
) -> HttpResponse:
- """Render this label to PDF format.
+ """Helper method to render a label to PDF format for a specific item.
Arguments:
label: The LabelTemplate object to render
@@ -143,7 +140,7 @@ def render_to_pdf(
def render_to_pdf_data(
self, label: LabelTemplate, item: LabelItemType, request: Request, **kwargs
) -> bytes:
- """Render this label to PDF and return it as bytes.
+ """Helper method to render a label to PDF and return it as bytes for a specific item.
Arguments:
label: The LabelTemplate object to render
@@ -159,7 +156,7 @@ def render_to_pdf_data(
def render_to_html(
self, label: LabelTemplate, item: LabelItemType, request: Request, **kwargs
) -> str:
- """Render this label to HTML format.
+ """Helper method to render a label to HTML format for a specific item.
Arguments:
label: The LabelTemplate object to render
@@ -174,7 +171,7 @@ def render_to_html(
def render_to_png(
self, label: LabelTemplate, item: LabelItemType, request: Request, **kwargs
) -> Image:
- """Render this label to PNG format.
+ """Helper method to render a label to PNG format for a specific item.
Arguments:
label: The LabelTemplate object to render
@@ -182,8 +179,8 @@ def render_to_png(
request: The HTTP request object which triggered this print job
Keyword Arguments:
- pdf_data: The pdf document as bytes (optional)
- dpi: The dpi used to render the image (optional)
+ pdf_data (bytes): The pdf document as bytes (optional)
+ dpi (int): The dpi used to render the image (optional)
"""
label.object_to_print = item
png = self.machine_plugin.render_to_png(label, request, **kwargs)
@@ -192,8 +189,25 @@ def render_to_png(
required_overrides = [[print_label, print_labels]]
- class BasePrintingOptionsSerializer(serializers.Serializer):
- """Printing options base serializer that implements common options."""
+ class PrintingOptionsSerializer(serializers.Serializer):
+ """Printing options serializer that implements common options.
+
+ This can be overridden by the driver to implement custom options, but the driver should always extend this class.
+
+ Example:
+ This example shows how to extend the default serializer and add a new option:
+ ```py
+ class MyDriver(BaseLabelPrintingDriver):
+ # ...
+
+ class PrintingOptionsSerializer(BaseLabelPrintingDriver.PrintingOptionsSerializer):
+ auto_cut = serializers.BooleanField(
+ default=True,
+ label=_('Auto cut'),
+ help_text=_('Automatically cut the label after printing'),
+ )
+ ```
+ """
copies = serializers.IntegerField(
default=1,
@@ -202,6 +216,24 @@ class BasePrintingOptionsSerializer(serializers.Serializer):
)
+class LabelPrinterStatus(MachineStatus):
+ """Label printer status codes.
+
+ Attributes:
+ CONNECTED: The printer is connected and ready to print
+ STANDBY: The printers connection is in standby mode, but will be ready to print at any time
+ PRINTING: The printer is currently printing a label
+ NO_MEDIA: The printer is out of media (e.g. the label spool is empty)
+ DISCONNECTED: The driver cannot establish a connection to the printer
+ """
+
+ CONNECTED = 100, _('Connected'), 'success'
+ UNKNOWN = 101, _('Standby'), 'success'
+ PRINTING = 110, _('Printing'), 'primary'
+ NO_MEDIA = 301, _('No media'), 'warning'
+ DISCONNECTED = 400, _('Disconnected'), 'danger'
+
+
class LabelPrintingMachineType(BaseMachineType):
"""Label printer machine type, is a direct integration to print labels for various items."""
@@ -219,15 +251,6 @@ class LabelPrintingMachineType(BaseMachineType):
}
}
- class LabelPrinterStatus(MachineStatus):
- """Label printer status codes."""
-
- CONNECTED = 100, _('Connected'), 'success'
- STANDBY = 101, _('Standby'), 'success'
- PRINTING = 110, _('Printing'), 'primary'
- LABEL_SPOOL_EMPTY = 301, _('Label spool empty'), 'warning'
- DISCONNECTED = 400, _('Disconnected'), 'danger'
-
MACHINE_STATUS = LabelPrinterStatus
default_machine_status = LabelPrinterStatus.DISCONNECTED
diff --git a/InvenTree/plugin/machine/__init__.py b/InvenTree/plugin/machine/__init__.py
index df104dd6929..617df0762b3 100644
--- a/InvenTree/plugin/machine/__init__.py
+++ b/InvenTree/plugin/machine/__init__.py
@@ -1,10 +1,3 @@
-from machine import BaseDriver, BaseMachineType, MachineStatus, machine_types, registry
-from machine.machine_types import * # noqa: F403, F401
+from machine import BaseDriver, BaseMachineType, MachineStatus, registry
-__all__ = [
- 'registry',
- 'BaseDriver',
- 'BaseMachineType',
- 'MachineStatus',
- *machine_types.__all__,
-]
+__all__ = ['registry', 'BaseDriver', 'BaseMachineType', 'MachineStatus']
diff --git a/InvenTree/plugin/machine/machine_types.py b/InvenTree/plugin/machine/machine_types.py
new file mode 100644
index 00000000000..f83dea24703
--- /dev/null
+++ b/InvenTree/plugin/machine/machine_types.py
@@ -0,0 +1,3 @@
+"""just re-export the machine types from the plugin InvenTree app."""
+
+from machine.machine_types import * # noqa: F403, F401
diff --git a/docs/docs/extend/machines/label_printer.md b/docs/docs/extend/machines/label_printer.md
new file mode 100644
index 00000000000..ea49dadb9d7
--- /dev/null
+++ b/docs/docs/extend/machines/label_printer.md
@@ -0,0 +1,35 @@
+## Label printer
+
+Label printer machines can directly print labels for various items in InvenTree. They replace standard [`LabelPrintingMixin`](../plugins/label.md) plugins that are used to connect to physical printers. Using machines rather than a standard `LabelPrintingMixin` plugin has the advantage that machines can be created multiple times using different settings but the same driver. That way multiple label printers of the same brand can be connected.
+
+### Writing your own printing driver
+
+Take a look at the most basic required code for a driver in this [example](./overview.md#example-driver). Next either implement the [`print_label`](#machine.machine_types.BaseLabelPrintingDriver.print_label) or [`print_labels`](#machine.machine_types.BaseLabelPrintingDriver.print_labels) function.
+
+### Label printer status
+
+There are a couple of predefined status codes for label printers. By default the Disconnected status code is set.
+
+::: machine.machine_types.LabelPrintingMachineType.LabelPrinterStatus
+ options:
+ heading_level: 4
+ show_bases: false
+ show_docstring_description: false
+
+### LabelPrintingDriver API
+
+::: machine.machine_types.BaseLabelPrintingDriver
+ options:
+ heading_level: 4
+ show_bases: false
+ members:
+ - print_label
+ - print_labels
+ - get_printers
+ - PrintingOptionsSerializer
+ - get_printing_options_serializer
+ - machine_plugin
+ - render_to_pdf
+ - render_to_pdf_data
+ - render_to_html
+ - render_to_png
diff --git a/docs/docs/extend/machines/overview.md b/docs/docs/extend/machines/overview.md
new file mode 100644
index 00000000000..4e1c29fabff
--- /dev/null
+++ b/docs/docs/extend/machines/overview.md
@@ -0,0 +1,202 @@
+---
+title: Machines
+---
+
+## Machines
+
+InvenTree has a builtin machine registry. There are different machine types available where each type can have different drivers. Drivers and even custom machine types can be provided by plugins.
+
+### Registry
+
+The machine registry is the main component which gets initialized on server start and manages all configured machines.
+
+#### Initialization process
+
+The machine registry initialization process can be divided into three stages:
+
+- **Stage 1: Discover machine types:** by looking for classes that inherit the BaseMachineType class
+- **Stage 2: Discover drivers:** by looking for classes that inherit the BaseDriver class (and are not referenced as base driver for any discovered machine type)
+- **Stage 3: Machine loading:**
+ 1. For each MachineConfig in database instantiate the MachineType class (drivers get instantiated here as needed and passed to the machine class. But only one instance of the driver class is maintained along the registry)
+ 2. The driver.init_driver function is called for each used driver
+ 3. The machine.initialize function is called for each machine, which calls the driver.init_machine function for each machine, then the machine.initialized state is set to true
+
+### Machine types
+
+Each machine type can provide a different type of connection functionality between inventree and a physical machine. These machine types are already built into InvenTree.
+
+#### Built-in types
+
+| Name | Description |
+| --- | --- |
+| [Label printer](./label_printer.md) | Directly print labels for various items. |
+
+#### Example machine type
+
+If you want to create your own machine type, please also take a look at the already existing machine types in `machines/machine_types/*.py`. The following example creates a machine type called `abc`.
+
+```py
+from django.utils.translation import ugettext_lazy as _
+from plugin.machine import BaseDriver, BaseMachineType, MachineStatus
+
+class BaseABCDriver(BaseDriver):
+ """Base xyz driver."""
+
+ machine_type = 'abc'
+
+ def my_custom_required_method(self):
+ """This function must be overridden."""
+ raise NotImplementedError('The `my_custom_required_method` function must be overridden!')
+
+ def my_custom_method(self):
+ """This function can be overridden."""
+ raise NotImplementedError('The `my_custom_method` function can be overridden!')
+
+ required_overrides = [my_custom_required_method]
+
+class ABCMachineType(BaseMachineType):
+ SLUG = 'abc'
+ NAME = _('ABC')
+ DESCRIPTION = _('This is an awesome machine type for ABC.')
+
+ base_driver = BaseABCDriver
+
+ class ABCStatus(MachineStatus):
+ CONNECTED = 100, _('Connected'), 'success'
+ STANDBY = 101, _('Standby'), 'success'
+ PRINTING = 110, _('Printing'), 'primary'
+
+ MACHINE_STATUS = ABCStatus
+
+ default_machine_status = ABCStatus.DISCONNECTED
+```
+
+#### Machine Type API
+
+The machine type class gets instantiated for each machine on server startup and the reference is stored in the machine registry. (Therefore `machine.NAME` is the machine type name and `machine.name` links to the machine instances user defined name)
+
+::: machine.BaseMachineType
+ options:
+ heading_level: 5
+ show_bases: false
+ members:
+ - machine_config
+ - name
+ - active
+ - initialize
+ - update
+ - restart
+ - handle_error
+ - get_setting
+ - set_setting
+ - check_setting
+ - set_status
+ - set_status_text
+
+### Drivers
+
+Drivers provide the connection layer between physical machines and inventree. There can be multiple drivers defined for the same machine type. Drivers are provided by plugins that are enabled and extend the corresponding base driver for the particular machine type. Each machine type already provides a base driver that needs to be inherited.
+
+#### Example driver
+
+A basic driver only needs to specify the basic attributes like `SLUG`, `NAME`, `DESCRIPTION`. The others are given by the used base driver, so take a look at [Machine types](#machine-types). The following example will create an driver called `abc` for the `xyz` machine type. The class will be discovered if it is provided by an **installed & activated** plugin just like this:
+
+```py
+from plugin import InvenTreePlugin
+from plugin.machine.machine_types import BaseXYZDriver
+
+class MyABCXYZDriverPlugin(InvenTreePlugin):
+ NAME = "ABCXYZDriver"
+ SLUG = "abc-driver"
+ TITLE = "ABC XYZ Driver"
+ # ...
+
+class MyXYZDriver(BaseXYZDriver):
+ SLUG = 'my-abc-driver'
+ NAME = 'My ABC driver'
+ DESCRIPTION = 'This is an awesome driver for ABC'
+```
+
+#### Driver API
+
+::: machine.BaseDriver
+ options:
+ heading_level: 5
+ show_bases: false
+ members:
+ - init_driver
+ - init_machine
+ - update_machine
+ - restart_machine
+ - get_machines
+ - handle_error
+
+### Settings
+
+Each machine can have different settings configured. There are machine settings that are specific to that machine type and driver settings that are specific to the driver, but both can be specified individually for each machine. Define them by adding a `MACHINE_SETTINGS` dictionary attribute to either the driver or the machine type. The format follows the same pattern as the `SETTINGS` for normal plugins documented on the [`SettingsMixin`](../plugins/settings.md)
+
+```py
+class MyXYZDriver(BaseXYZDriver):
+ MACHINE_SETTINGS = {
+ 'SERVER': {
+ 'name': _('Server'),
+ 'description': _('IP/Hostname to connect to the cups server'),
+ 'default': 'localhost',
+ 'required': True,
+ }
+ }
+```
+
+Settings can even marked as `'required': True` which prevents the machine from starting if the setting is not defined.
+
+### Machine status
+
+Machine status can be used to report the machine status to the users. They can be set by the driver for each machine, but get lost on a server restart.
+
+#### Codes
+
+Each machine type has a set of status codes defined that can be set for each machine by the driver. There also needs to be a default status code defined.
+
+```py
+from plugin.machine import MachineStatus, BaseMachineType
+
+class XYZStatus(MachineStatus):
+ CONNECTED = 100, _('Connected'), 'success'
+ STANDBY = 101, _('Standby'), 'success'
+ DISCONNECTED = 400, _('Disconnected'), 'danger'
+
+class XYZMachineType(BaseMachineType):
+ # ...
+
+ MACHINE_STATUS = XYZStatus
+ default_machine_status = XYZStatus.DISCONNECTED
+```
+
+And to set a status code for a machine by the driver.
+
+```py
+class MyXYZDriver(BaseXYZDriver):
+ # ...
+ def init_machine(self, machine):
+ # ... do some init stuff here
+ machine.set_status(XYZMachineType.MACHINE_STATUS.CONNECTED)
+```
+
+**`MachineStatus` API**
+
+::: machine.machine_type.MachineStatus
+ options:
+ heading_level: 5
+ show_bases: false
+
+#### Free text
+
+There can also be a free text status code defined.
+
+```py
+class MyXYZDriver(BaseXYZDriver):
+ # ...
+ def init_machine(self, machine):
+ # ... do some init stuff here
+ machine.set_status_text("Paper missing")
+```
diff --git a/docs/docs/extend/plugins/machines.md b/docs/docs/extend/plugins/machines.md
deleted file mode 100644
index 32db0666f38..00000000000
--- a/docs/docs/extend/plugins/machines.md
+++ /dev/null
@@ -1,204 +0,0 @@
----
-title: Machines
----
-
-## Machines
-
-InvenTree has a builtin machine registry. There are different machine types available where each type can have different drivers. Drivers and even custom machine types can be provided by plugins.
-
-### Registry
-
-The machine registry is the main component which gets initialized on server start and manages all configured machines.
-
-#### Initialization process
-
-The machine registry initialization process can be divided into three stages as described in this diagram:
-
-```mermaid
-flowchart LR
- A["`**Server start**`"] --> B
- B["`**Stage 1: Discover machine types**
- by looking for classes that inherit the BaseMachineType class`"] --> C
- C["`**Stage 2: Discover drivers**
- by looking for classes that inherit the BaseDriver class (and are not referenced as base driver for any discovered machine type)`"] --> MA
- subgraph MA["Stage 3: Machine loading"]
- direction TB
- D["`For each MachineConfig in database instantiate the MachineType class (drivers get instantiated here as needed and passed to the machine class. But only one instance of the driver class is maintained along the registry)`"] --> E
- E["`The driver.init_driver function is called for each used driver`"] --> F
- F["`The machine.initialize function is called for each machine, which calls the driver.init_machine function for each machine, then the machine.initialized state is set to true`"]
- end
- MA --> X["`**Done**`"]
-```
-
-### Machine types
-
-Each machine type can provide a different type of connection functionality between inventree and a physical machine. These machine types are already built into InvenTree.
-The machine type class gets instantiated for each machine on server startup and the reference is stored in the machine registry.
-
-#### Built in types
-
-| Name | Description |
-| ------------------------------- | ---------------------------------------- |
-| [Label printer](#label-printer) | Directly print labels for various items. |
-
-##### Label printer
-
-Label printer machines can directly print labels for various items in InvenTree. They replace standard [`LabelPrintingMixin`](../plugins/label.md) plugins that are used to connect to physical printers. Using machines rather than a standard `LabelPrintingMixin` plugin has the advantage that machines can be created multiple times using different settings but the same driver. That way multiple label printers of the same brand can be connected.
-
-TODO
-
-#### Available attributes
-
-| Name | Description |
-| ------------------------ | --------------------------------------------------------------------------------------------------------------------- |
-| `SLUG` | A slug for the machine type needs to be set (short identifier string that satisfies the following format `/[a-z-]+/`) |
-| `NAME` | A name for the machine type needs to be set |
-| `DESCRIPTION` | A description for the machine type needs to be set |
-| `base_driver` | Reference to the base driver class |
-| `MACHINE_SETTINGS` | Machine settings dict, see [settings](#settings) |
-| `MACHINE_STATUS` | Machine status enum, see [status](#machine-status) |
-| `default_machine_status` | default machine status, see [status](#machine-status) |
-
-#### Available methods
-
-| Name | Description |
-| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
-| `initialize(self)` | gets called on initialization |
-| `update(self, old_machine_state: Dict[str, Any])` | gets called if any changes on a machine or their settings occur. The `machine.restart_required` flag can be set here. |
-| `restart(self, machine: 'BaseMachineType')` | gets called if the admin manually presses the restart button on the admin interface |
-| `handle_error(self, error: Union[Exception, str])` | helper function to capture errors and show them in the admin ui |
-| `get_setting(self, key: str, config_type_str: Literal['M', 'D'], cache=False)` | get a setting for a machine |
-| `set_setting(self, key: str, config_type_str: Literal['M', 'D'], value)` | set a setting for a machine |
-| `check_settings(self)` | check that all required settings are set |
-| `set_status(self, status: MachineStatus)` | set a machine status code |
-| `set_status_text(self, status_text: str)` | set a machine status text |
-
-#### Example machine type
-
-If you want to create your own machine type, please also take a look at the already existing machine types.
-
-```py
-from django.utils.translation import ugettext_lazy as _
-from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
-
-class BaseXYZDriver(BaseDriver):
- """Base xyz driver."""
-
- machine_type = 'xyz'
-
- def my_custom_required_method(self):
- """This function must be overridden."""
- raise NotImplementedError('The `my_custom_required_method` function must be overridden!')
-
- def my_custom_method(self):
- """This function must be overridden."""
- raise NotImplementedError('The `my_custom_method` function must be overridden!')
-
- requires_override = [my_custom_required_method]
-
-class XYZMachineType(BaseMachineType):
- SLUG = 'xyz'
- NAME = _('XYZ')
- DESCRIPTION = _('This is an awesome machine type for xyz.')
-
- base_driver = BaseXYZDriver
-
- class XYZStatus(MachineStatus):
- CONNECTED = 100, _('Connected'), 'success'
- STANDBY = 101, _('Standby'), 'success'
- PRINTING = 110, _('Printing'), 'primary'
-
- MACHINE_STATUS = XYZStatus
-
- default_machine_status = XYZStatus.DISCONNECTED
-```
-
-### Drivers
-
-Drivers provide the connection layer between physical machines and inventree. There can be multiple drivers defined for the same machine type. Drivers are provided by plugins that are enabled and extend the corresponding base driver for the particular machine type. Each machine type already provides a base driver that needs to be inherited.
-
-#### Available attributes
-
-| Name | Description |
-| ------------------ | --------------------------------------------------------------------------------------------------------------- |
-| `SLUG` | A slug for the driver needs to be set (short identifier string that satisfies the following format `/[a-z-]+/`) |
-| `NAME` | A name for the driver needs to be set |
-| `DESCRIPTION` | A description for the driver needs to be set |
-| `MACHINE_SETTINGS` | Machine settings dict, see [settings](#settings) (optional) |
-| (`machine_type`) | Already set to the machine type slug by the base driver |
-
-#### Available methods
-
-| Name | Description |
-| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
-| `init_driver(self)` | gets called on initialization before each individual machine gets initialized |
-| `init_machine(self, machine: 'BaseMachineType')` | gets called for each machine |
-| `update_machine(self, old_machine_state: Dict[str, Any], machine: 'BaseMachineType')` | gets called if any changes on a machine or their settings occur. The `machine.restart_required` flag can be set here. |
-| `restart_machine(self, machine: 'BaseMachineType')` | gets called if the admin manually presses the restart button on the admin interface |
-| `get_machines(self, *, name, machine_type, driver, initialized, active, base_driver)` | helper function to get all machine, by default only initialized machines that also use the current driver are returned |
-| `handle_error(self, error: Union[Exception, str])` | helper function to capture errors and show them in the admin ui |
-
-#### Example driver
-
-A basic driver only needs to specify the basic attributes like `SLUG`, `NAME`, `DESCRIPTION`. The others are given by the used base driver, so take a look at [Machine types](#machine-types). The class will be discovered if it is provided by an **installed & activated** plugin just like this:
-
-```py
-from plugin import InvenTreePlugin
-from plugin.machine import BaseXYZDriver
-
-class CupsLabelPlugin(InvenTreePlugin):
- NAME = "CupsLabels"
- SLUG = "cups"
- TITLE = "Cups Label Printer"
- # ...
-
-class MyXYZDriver(BaseXYZDriver):
- SLUG = 'my-abc-driver'
- NAME = 'My ABC driver'
- DESCRIPTION = 'This is an awesome driver for ABC'
-```
-
-### Settings
-
-Each machine can have different settings configured. There are machine settings that are specific to that machine type and driver settings that are specific to the driver, but both can be specified individually for each machine. Define them by adding a `MACHINE_SETTINGS` dictionary attribute to either the driver or the machine type. The format follows the same pattern as the `SETTINGS` for normal plugins documented on the [`SettingsMixin`](../plugins/settings.md)
-
-```py
-class MyXYZDriver(BaseXYZDriver):
- MACHINE_SETTINGS = {
- 'SERVER': {
- 'name': _('Server'),
- 'description': _('IP/Hostname to connect to the cups server'),
- 'default': 'localhost',
- 'required': True,
- }
- }
-```
-
-Settings can even marked as `'required': True` which prevents the machine from starting if the setting is not defined.
-
-### Machine status
-
-Each machine type has a set of status codes defined that can be set for each machine by the driver. There also needs to be a default status code defined.
-
-```py
-class XYZMachineType(BaseMachineType):
- # ...
- class XYZStatus(MachineStatus):
- CONNECTED = 100, _('Connected'), 'success'
- STANDBY = 101, _('Standby'), 'success'
- DISCONNECTED = 400, _('Disconnected'), 'danger'
-
- MACHINE_STATUS = XYZStatus
- default_machine_status = XYZStatus.DISCONNECTED
-```
-
-And to set a status code for a machine by the driver. There can also be a free text status code defined.
-
-```py
-class MyXYZDriver(BaseXYZDriver):
- # ...
- def init_machine(self, machine):
- # ... do some init stuff here
- machine.set_status(XYZMachineType.MACHINE_STATUS.CONNECTED)
- machine.set_status_text("Paper missing")
-```
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 4ad2dffab96..57b31a786a3 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -201,7 +201,6 @@ nav:
- Developing a Plugin: extend/how_to_plugin.md
- Model Metadata: extend/plugins/metadata.md
- Tags: extend/plugins/tags.md
- - Machines: extend/plugins/machines.md
- Plugin Mixins:
- Action Mixin: extend/plugins/action.md
- API Mixin: extend/plugins/api.md
@@ -218,6 +217,9 @@ nav:
- Settings Mixin: extend/plugins/settings.md
- URL Mixin: extend/plugins/urls.md
- Validation Mixin: extend/plugins/validation.md
+ - Machines:
+ - Overview: extend/machines/overview.md
+ - Label Printer: extend/machines/label_printer.md
- Themes: extend/themes.md
- Third-Party: extend/integrate.md
@@ -233,6 +235,13 @@ plugins:
on_config: "docs.docs.hooks:on_config"
- macros:
include_dir: docs/_includes
+ - mkdocstrings:
+ default_handler: python
+ handlers:
+ python:
+ options:
+ show_symbol_type_heading: true
+ show_symbol_type_toc: true
# Extensions
markdown_extensions:
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 2bfb42c832a..ad768a9899a 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -4,3 +4,4 @@ mkdocs-material>=9.0,<10.0
mkdocs-git-revision-date-localized-plugin>=1.1,<2.0
mkdocs-simple-hooks>=0.1,<1.0
mkdocs-include-markdown-plugin
+mkdocstrings[python]>=0.24.0
From b8090b96a7da2a358126bd884618525318d9c59e Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Sun, 4 Feb 2024 15:53:32 +0000
Subject: [PATCH 65/86] Fix docs
---
docs/mkdocs.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 57b31a786a3..3562e9a570a 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -239,6 +239,8 @@ plugins:
default_handler: python
handlers:
python:
+ paths:
+ - ../InvenTree
options:
show_symbol_type_heading: true
show_symbol_type_toc: true
From ff0f02d06a7426bde6fb48b4e8c87c2ccad0fd4f Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Mon, 5 Feb 2024 08:35:00 +0000
Subject: [PATCH 66/86] Added UNKNOWN status code to label printer status
---
InvenTree/machine/machine_type.py | 3 +++
InvenTree/machine/machine_types/LabelPrintingMachineType.py | 6 +++---
docs/docs/extend/machines/label_printer.md | 2 +-
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/InvenTree/machine/machine_type.py b/InvenTree/machine/machine_type.py
index 00fa66ba14b..fd1e052d262 100644
--- a/InvenTree/machine/machine_type.py
+++ b/InvenTree/machine/machine_type.py
@@ -34,6 +34,9 @@ class MachineStatus(StatusCode):
Additionally there are helpers to access all additional attributes `text`, `label`, `color`.
+ Available colors:
+ primary, secondary, warning, danger, success, warning, info
+
Status code ranges:
```
1XX - Everything fine
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index 936430f4d9d..4d8679eb960 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -221,14 +221,14 @@ class LabelPrinterStatus(MachineStatus):
Attributes:
CONNECTED: The printer is connected and ready to print
- STANDBY: The printers connection is in standby mode, but will be ready to print at any time
+ UNKNOWN: The printer status is unknown (e.g. there is no active connection to the printer)
PRINTING: The printer is currently printing a label
NO_MEDIA: The printer is out of media (e.g. the label spool is empty)
DISCONNECTED: The driver cannot establish a connection to the printer
"""
CONNECTED = 100, _('Connected'), 'success'
- UNKNOWN = 101, _('Standby'), 'success'
+ UNKNOWN = 101, _('Unknown'), 'secondary'
PRINTING = 110, _('Printing'), 'primary'
NO_MEDIA = 301, _('No media'), 'warning'
DISCONNECTED = 400, _('Disconnected'), 'danger'
@@ -253,7 +253,7 @@ class LabelPrintingMachineType(BaseMachineType):
MACHINE_STATUS = LabelPrinterStatus
- default_machine_status = LabelPrinterStatus.DISCONNECTED
+ default_machine_status = LabelPrinterStatus.UNKNOWN
@property
def location(self):
diff --git a/docs/docs/extend/machines/label_printer.md b/docs/docs/extend/machines/label_printer.md
index ea49dadb9d7..00e3bef00f0 100644
--- a/docs/docs/extend/machines/label_printer.md
+++ b/docs/docs/extend/machines/label_printer.md
@@ -8,7 +8,7 @@ Take a look at the most basic required code for a driver in this [example](./ove
### Label printer status
-There are a couple of predefined status codes for label printers. By default the Disconnected status code is set.
+There are a couple of predefined status codes for label printers. By default the `UNKNOWN` status code is set for each machine, but they can be changed at any time by the driver. For more info about status code see [Machine status codes](./overview.md#machine-status).
::: machine.machine_types.LabelPrintingMachineType.LabelPrinterStatus
options:
From 71761cdfd12e7c2f6dd6c15024f08bdf94e59e35 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Mon, 5 Feb 2024 08:54:51 +0000
Subject: [PATCH 67/86] Skip machine loading when running migrations
---
InvenTree/machine/apps.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/InvenTree/machine/apps.py b/InvenTree/machine/apps.py
index 602c2500b02..217afdb0927 100755
--- a/InvenTree/machine/apps.py
+++ b/InvenTree/machine/apps.py
@@ -4,7 +4,12 @@
from django.apps import AppConfig
-from InvenTree.ready import canAppAccessDatabase, isInMainThread, isPluginRegistryLoaded
+from InvenTree.ready import (
+ canAppAccessDatabase,
+ isInMainThread,
+ isPluginRegistryLoaded,
+ isRunningMigrations,
+)
logger = logging.getLogger('inventree')
@@ -20,6 +25,7 @@ def ready(self) -> None:
not canAppAccessDatabase(allow_test=True)
or not isPluginRegistryLoaded()
or not isInMainThread()
+ or isRunningMigrations()
):
logger.debug('Machine app: Skipping machine loading sequence')
return
From 60fbb85b01506cf67d92e29cc7dd371df7148231 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Mon, 5 Feb 2024 09:10:13 +0000
Subject: [PATCH 68/86] Fix testing?
---
InvenTree/machine/apps.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/InvenTree/machine/apps.py b/InvenTree/machine/apps.py
index 217afdb0927..3264ac6319c 100755
--- a/InvenTree/machine/apps.py
+++ b/InvenTree/machine/apps.py
@@ -3,9 +3,11 @@
import logging
from django.apps import AppConfig
+from django.db.utils import OperationalError, ProgrammingError
from InvenTree.ready import (
canAppAccessDatabase,
+ isImportingData,
isInMainThread,
isPluginRegistryLoaded,
isRunningMigrations,
@@ -26,11 +28,16 @@ def ready(self) -> None:
or not isPluginRegistryLoaded()
or not isInMainThread()
or isRunningMigrations()
+ or isImportingData()
):
logger.debug('Machine app: Skipping machine loading sequence')
return
from machine import registry
- logger.info('Loading InvenTree machines')
- registry.initialize()
+ try:
+ logger.info('Loading InvenTree machines')
+ registry.initialize()
+ except (OperationalError, ProgrammingError):
+ # Database might not yet be ready
+ logger.warn('Database was not ready for initializing machines')
From 25a147e6446db5e315f047311c6914eff28c5dcd Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Mon, 5 Feb 2024 10:27:31 +0000
Subject: [PATCH 69/86] Fix: tests?
---
InvenTree/plugin/base/label/mixins.py | 2 +-
InvenTree/plugin/base/label/test_label_mixin.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/InvenTree/plugin/base/label/mixins.py b/InvenTree/plugin/base/label/mixins.py
index daef8d590c2..ff2e292dd59 100644
--- a/InvenTree/plugin/base/label/mixins.py
+++ b/InvenTree/plugin/base/label/mixins.py
@@ -126,7 +126,7 @@ def print_labels(
'user': user,
'width': label.width,
'height': label.height,
- 'printing_options': printing_options,
+ 'printing_options': kwargs['printing_options'],
}
if self.BLOCKING_PRINT:
diff --git a/InvenTree/plugin/base/label/test_label_mixin.py b/InvenTree/plugin/base/label/test_label_mixin.py
index dbb73713f52..9dfb2f6dc3c 100644
--- a/InvenTree/plugin/base/label/test_label_mixin.py
+++ b/InvenTree/plugin/base/label/test_label_mixin.py
@@ -82,7 +82,7 @@ def test_installed(self):
"""Test that the sample printing plugin is installed."""
# Get all label plugins
plugins = registry.with_mixin('labels')
- self.assertEqual(len(plugins), 3)
+ self.assertEqual(len(plugins), 4)
# But, it is not 'active'
plugins = registry.with_mixin('labels', active=True)
@@ -110,7 +110,7 @@ def test_api(self):
# Should be available via the API now
response = self.client.get(url, {'mixin': 'labels', 'active': True})
- self.assertEqual(len(response.data), 3)
+ self.assertEqual(len(response.data), 4)
labels = [item['key'] for item in response.data]
From 568f76af05a2e03074c5dd23cf3b8fa4005b481f Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Mon, 5 Feb 2024 10:52:09 +0000
Subject: [PATCH 70/86] Fix: tests?
---
InvenTree/plugin/base/label/test_label_mixin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/InvenTree/plugin/base/label/test_label_mixin.py b/InvenTree/plugin/base/label/test_label_mixin.py
index 9dfb2f6dc3c..240ec409d54 100644
--- a/InvenTree/plugin/base/label/test_label_mixin.py
+++ b/InvenTree/plugin/base/label/test_label_mixin.py
@@ -86,7 +86,7 @@ def test_installed(self):
# But, it is not 'active'
plugins = registry.with_mixin('labels', active=True)
- self.assertEqual(len(plugins), 2)
+ self.assertEqual(len(plugins), 3)
def test_api(self):
"""Test that we can filter the API endpoint by mixin."""
From dbdd364289f7b7f17f6b7a11f6ad5c47e5ca4a29 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Mon, 5 Feb 2024 11:30:28 +0000
Subject: [PATCH 71/86] Disable docs check precommit
---
docs/ci/check_mkdocs_config.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/ci/check_mkdocs_config.py b/docs/ci/check_mkdocs_config.py
index 93a3afe0c80..6658b74e0af 100644
--- a/docs/ci/check_mkdocs_config.py
+++ b/docs/ci/check_mkdocs_config.py
@@ -1,5 +1,6 @@
"""Check mkdocs.yml config file for errors."""
+exit(0)
import os
import yaml
From a3966c22b43b6ab138520a3245d6006e8f73b453 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Mon, 5 Feb 2024 11:34:14 +0000
Subject: [PATCH 72/86] Disable docs check precommit
---
.pre-commit-config.yaml | 2 +-
docs/ci/check_mkdocs_config.py | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 764691ede0c..418349912b1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -13,7 +13,7 @@ repos:
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- - id: check-yaml
+ # - id: check-yaml
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.11
diff --git a/docs/ci/check_mkdocs_config.py b/docs/ci/check_mkdocs_config.py
index 6658b74e0af..93a3afe0c80 100644
--- a/docs/ci/check_mkdocs_config.py
+++ b/docs/ci/check_mkdocs_config.py
@@ -1,6 +1,5 @@
"""Check mkdocs.yml config file for errors."""
-exit(0)
import os
import yaml
From 55508b88ecb67b9e56b12214242af0a607feb70e Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Mon, 5 Feb 2024 14:25:59 +0000
Subject: [PATCH 73/86] First draft for tests
---
.../machine_types/LabelPrintingMachineType.py | 4 +-
InvenTree/machine/test_api.py | 67 +++++++++++++++++++
2 files changed, 69 insertions(+), 2 deletions(-)
create mode 100644 InvenTree/machine/test_api.py
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index 4d8679eb960..fcedea92a04 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -24,7 +24,7 @@ class BaseLabelPrintingDriver(BaseDriver):
USE_BACKGROUND_WORKER (bool): If True, the `print_label()` and `print_labels()` methods will be run in a background worker (default: True)
"""
- machine_type = 'label_printer'
+ machine_type = 'label-printer'
USE_BACKGROUND_WORKER = True
@@ -237,7 +237,7 @@ class LabelPrinterStatus(MachineStatus):
class LabelPrintingMachineType(BaseMachineType):
"""Label printer machine type, is a direct integration to print labels for various items."""
- SLUG = 'label_printer'
+ SLUG = 'label-printer'
NAME = _('Label Printer')
DESCRIPTION = _('Directly print labels for various items.')
diff --git a/InvenTree/machine/test_api.py b/InvenTree/machine/test_api.py
new file mode 100644
index 00000000000..e5950375657
--- /dev/null
+++ b/InvenTree/machine/test_api.py
@@ -0,0 +1,67 @@
+"""Machine API tests."""
+
+from django.urls import reverse
+
+from InvenTree.unit_test import InvenTreeAPITestCase
+from machine import registry
+from machine.machine_types import BaseLabelPrintingDriver
+
+
+class MachineAPITest(InvenTreeAPITestCase):
+ """Class for unit testing machine API endpoints."""
+
+ @classmethod
+ def setUpTestData(cls):
+ """Create a test driver."""
+ super().setUpTestData()
+
+ class TestingLabelPrinterDriver(BaseLabelPrintingDriver):
+ """Test driver for label printing."""
+
+ SLUG = 'test-label-printer'
+ NAME = 'Test label printer'
+ DESCRIPTION = 'This is a test label printer driver for testing.'
+
+ def print_label(self, *args, **kwargs) -> None:
+ """Override print_label."""
+ pass
+
+ id(TestingLabelPrinterDriver) # just to be sure that this class really exists
+ registry.initialize()
+
+ def test_machine_type_list(self):
+ """Test machine types list API endpoint."""
+ response = self.get(reverse('api-machine-types'))
+ self.assertEqual(len(response.data), 1)
+ self.assertDictContainsSubset(
+ {
+ 'slug': 'label-printer',
+ 'name': 'Label Printer',
+ 'description': 'Directly print labels for various items.',
+ 'provider_plugin': None,
+ 'is_builtin': True,
+ },
+ response.data[0],
+ )
+ self.assertTrue(
+ response.data[0]['provider_file'].endswith(
+ 'machine/machine_types/LabelPrintingMachineType.py'
+ )
+ )
+
+ def test_machine_driver_list(self):
+ """Test machine driver list API endpoint."""
+ response = self.get(reverse('api-machine-drivers'))
+ self.assertEqual(len(response.data), 1)
+ self.assertDictContainsSubset(
+ {
+ 'slug': 'test-label-printer',
+ 'name': 'Test label printer',
+ 'description': 'This is a test label printer driver for testing.',
+ 'provider_plugin': None,
+ 'is_builtin': True,
+ 'machine_type': 'label-printer',
+ 'errors': [],
+ },
+ response.data[0],
+ )
From 422a51fbdf06c1caefff8912d918992a5f4cd896 Mon Sep 17 00:00:00 2001
From: Matthias Mair
Date: Mon, 5 Feb 2024 20:50:45 +0100
Subject: [PATCH 74/86] fix test
---
InvenTree/machine/test_api.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/InvenTree/machine/test_api.py b/InvenTree/machine/test_api.py
index e5950375657..fa825dc8548 100644
--- a/InvenTree/machine/test_api.py
+++ b/InvenTree/machine/test_api.py
@@ -61,7 +61,7 @@ def test_machine_driver_list(self):
'provider_plugin': None,
'is_builtin': True,
'machine_type': 'label-printer',
- 'errors': [],
+ 'driver_errors': [],
},
response.data[0],
)
From 138a1542c8f1114a664f12e8debf05a6cf4c2b56 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Mon, 5 Feb 2024 20:21:48 +0000
Subject: [PATCH 75/86] Add type ignore
---
InvenTree/machine/machine_types/LabelPrintingMachineType.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index fcedea92a04..9e2bfcffbfb 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -113,7 +113,7 @@ def get_printing_options_serializer(
Returns:
A class instance of a DRF serializer class, by default this an instance of self.PrintingOptionsSerializer using the *args, **kwargs if existing for this driver
"""
- return self.PrintingOptionsSerializer(*args, **kwargs)
+ return self.PrintingOptionsSerializer(*args, **kwargs) # type: ignore
# --- helper functions
@property
From 8ffd865a9cdad9a371a2504bf3034a372e974596 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Tue, 6 Feb 2024 12:33:41 +0000
Subject: [PATCH 76/86] Added API tests
---
InvenTree/InvenTree/tests.py | 96 +++++++++
InvenTree/machine/test_api.py | 195 +++++++++++++++++-
.../src/tables/machine/MachineListTable.tsx | 2 +-
3 files changed, 288 insertions(+), 5 deletions(-)
diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index 0cda36703b6..fc417be9da3 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -28,6 +28,7 @@
import InvenTree.tasks
from common.models import CustomUnit, InvenTreeSetting
from common.settings import currency_codes
+from InvenTree.helpers_mixin import ClassProviderMixin, ClassValidationMixin
from InvenTree.sanitizer import sanitize_svg
from InvenTree.unit_test import InvenTreeTestCase
from part.models import Part, PartCategory
@@ -1264,3 +1265,98 @@ def test_generation(self):
self.assertEqual(resp.url, '/api/auth/login-redirect/')
# And we should be logged in again
self.assertEqual(resp.wsgi_request.user, self.user)
+
+
+class ClassValidationMixinTest(TestCase):
+ """Tests for the ClassValidationMixin class."""
+
+ class BaseTestClass(ClassValidationMixin):
+ """A valid class that inherits from ClassValidationMixin."""
+
+ NAME: str
+
+ def test(self):
+ """Test function."""
+ pass
+
+ def test1(self):
+ """Test function."""
+ pass
+
+ def test2(self):
+ """Test function."""
+ pass
+
+ required_attributes = ['NAME']
+ required_overrides = [test, [test1, test2]]
+
+ class InvalidClass:
+ """An invalid class that does not inherit from ClassValidationMixin."""
+
+ pass
+
+ def test_valid_class(self):
+ """Test that a valid class passes the validation."""
+
+ class TestClass(self.BaseTestClass):
+ """A valid class that inherits from BaseTestClass."""
+
+ NAME = 'Test'
+
+ def test(self):
+ """Test function."""
+ pass
+
+ def test2(self):
+ """Test function."""
+ pass
+
+ TestClass.validate()
+
+ def test_invalid_class(self):
+ """Test that an invalid class fails the validation."""
+
+ class TestClass1(self.BaseTestClass):
+ """A bad class that inherits from BaseTestClass."""
+
+ with self.assertRaisesRegex(
+ NotImplementedError,
+ r'\'<.*TestClass1\'>\' did not provide the following attributes: NAME and did not override the required attributes: test, one of test1 or test2',
+ ):
+ TestClass1.validate()
+
+ class TestClass2(self.BaseTestClass):
+ """A bad class that inherits from BaseTestClass."""
+
+ NAME = 'Test'
+
+ def test2(self):
+ """Test function."""
+ pass
+
+ with self.assertRaisesRegex(
+ NotImplementedError,
+ r'\'<.*TestClass2\'>\' did not override the required attributes: test',
+ ):
+ TestClass2.validate()
+
+
+class ClassProviderMixinTest(TestCase):
+ """Tests for the ClassProviderMixin class."""
+
+ class TestClass(ClassProviderMixin):
+ """This class is a dummy class to test the ClassProviderMixin."""
+
+ pass
+
+ def test_get_provider_file(self):
+ """Test the get_provider_file function."""
+ self.assertEqual(self.TestClass.get_provider_file(), __file__)
+
+ def test_provider_plugin(self):
+ """Test the provider_plugin function."""
+ self.assertEqual(self.TestClass.get_provider_plugin(), None)
+
+ def test_get_is_builtin(self):
+ """Test the get_is_builtin function."""
+ self.assertTrue(self.TestClass.get_is_builtin())
diff --git a/InvenTree/machine/test_api.py b/InvenTree/machine/test_api.py
index fa825dc8548..261d7728f6f 100644
--- a/InvenTree/machine/test_api.py
+++ b/InvenTree/machine/test_api.py
@@ -4,16 +4,20 @@
from InvenTree.unit_test import InvenTreeAPITestCase
from machine import registry
+from machine.machine_type import BaseMachineType
from machine.machine_types import BaseLabelPrintingDriver
+from machine.models import MachineConfig
+from stock.models import StockLocation
class MachineAPITest(InvenTreeAPITestCase):
"""Class for unit testing machine API endpoints."""
+ roles = ['admin.add', 'admin.view', 'admin.change', 'admin.delete']
+
@classmethod
- def setUpTestData(cls):
- """Create a test driver."""
- super().setUpTestData()
+ def setUpClass(cls):
+ """Setup some testing drivers/machines."""
class TestingLabelPrinterDriver(BaseLabelPrintingDriver):
"""Test driver for label printing."""
@@ -22,13 +26,43 @@ class TestingLabelPrinterDriver(BaseLabelPrintingDriver):
NAME = 'Test label printer'
DESCRIPTION = 'This is a test label printer driver for testing.'
+ MACHINE_SETTINGS = {
+ 'TEST_SETTING': {
+ 'name': 'Test setting',
+ 'description': 'This is a test setting',
+ }
+ }
+
+ def restart_machine(self, machine: BaseMachineType):
+ """Override restart_machine."""
+ machine.set_status_text('Restarting...')
+
+ def print_label(self, *args, **kwargs) -> None:
+ """Override print_label."""
+ pass
+
+ class CopyTestingLabelPrinterDriver(BaseLabelPrintingDriver):
+ """Test driver for label printing."""
+
+ SLUG = 'test-label-printer'
+ NAME = 'Test label printer'
+ DESCRIPTION = 'This is a test label printer driver for testing.'
+
+ MACHINE_SETTINGS = {
+ 'TEST_SETTING': {
+ 'name': 'Test setting',
+ 'description': 'This is a test setting',
+ }
+ }
+
def print_label(self, *args, **kwargs) -> None:
"""Override print_label."""
pass
- id(TestingLabelPrinterDriver) # just to be sure that this class really exists
registry.initialize()
+ super().setUpClass()
+
def test_machine_type_list(self):
"""Test machine types list API endpoint."""
response = self.get(reverse('api-machine-types'))
@@ -65,3 +99,156 @@ def test_machine_driver_list(self):
},
response.data[0],
)
+
+ def test_machine_status(self):
+ """Test machine status API endpoint."""
+ response = self.get(reverse('api-machine-registry-status'))
+ self.assertIn(
+ "Cannot re-register driver 'test-label-printer'",
+ [e['message'] for e in response.data['registry_errors']],
+ )
+
+ def test_machine_list(self):
+ """Test machine list API endpoint."""
+ response = self.get(reverse('api-machine-list'))
+ self.assertEqual(len(response.data), 0)
+
+ MachineConfig.objects.create(
+ machine_type='label-printer',
+ driver='test-label-printer',
+ name='Test Machine',
+ active=True,
+ )
+
+ response = self.get(reverse('api-machine-list'))
+ self.assertEqual(len(response.data), 1)
+ self.assertDictContainsSubset(
+ {
+ 'name': 'Test Machine',
+ 'machine_type': 'label-printer',
+ 'driver': 'test-label-printer',
+ 'initialized': True,
+ 'active': True,
+ 'status': 101,
+ 'status_model': 'LabelPrinterStatus',
+ 'status_text': '',
+ 'is_driver_available': True,
+ },
+ response.data[0],
+ )
+
+ def test_machine_detail(self):
+ """Test machine detail API endpoint."""
+ placeholder_uuid = '00000000-0000-0000-0000-000000000000'
+ self.assertFalse(len(MachineConfig.objects.all()), 0)
+ self.get(
+ reverse('api-machine-detail', kwargs={'pk': placeholder_uuid}),
+ expected_code=404,
+ )
+
+ machine_data = {
+ 'machine_type': 'label-printer',
+ 'driver': 'test-label-printer',
+ 'name': 'Test Machine',
+ 'active': True,
+ }
+
+ # Create a machine
+ response = self.post(reverse('api-machine-list'), machine_data)
+ self.assertDictContainsSubset(machine_data, response.data)
+ pk = response.data['pk']
+
+ # Retrieve the machine
+ response = self.get(reverse('api-machine-detail', kwargs={'pk': pk}))
+ self.assertDictContainsSubset(machine_data, response.data)
+
+ # Update the machine
+ response = self.patch(
+ reverse('api-machine-detail', kwargs={'pk': pk}),
+ {'name': 'Updated Machine'},
+ )
+ self.assertDictContainsSubset({'name': 'Updated Machine'}, response.data)
+ self.assertEqual(MachineConfig.objects.get(pk=pk).name, 'Updated Machine')
+
+ # Delete the machine
+ response = self.delete(
+ reverse('api-machine-detail', kwargs={'pk': pk}), expected_code=204
+ )
+ self.assertFalse(len(MachineConfig.objects.all()), 0)
+
+ # Create machine where the driver does not exist
+ machine_data['driver'] = 'non-existent-driver'
+ machine_data['name'] = 'Machine with non-existent driver'
+ response = self.post(reverse('api-machine-list'), machine_data)
+ self.assertIn(
+ "Driver 'non-existent-driver' not found", response.data['machine_errors']
+ )
+ self.assertFalse(response.data['initialized'])
+ self.assertFalse(response.data['is_driver_available'])
+
+ def test_machine_detail_settings(self):
+ """Test machine detail settings API endpoint."""
+ machine = MachineConfig.objects.create(
+ machine_type='label-printer',
+ driver='test-label-printer',
+ name='Test Machine with settings',
+ active=True,
+ )
+ machine_setting_url = reverse(
+ 'api-machine-settings-detail',
+ kwargs={'pk': machine.pk, 'config_type': 'M', 'key': 'LOCATION'},
+ )
+ driver_setting_url = reverse(
+ 'api-machine-settings-detail',
+ kwargs={'pk': machine.pk, 'config_type': 'D', 'key': 'TEST_SETTING'},
+ )
+
+ # Get settings
+ response = self.get(machine_setting_url)
+ self.assertEqual(response.data['value'], '')
+
+ response = self.get(driver_setting_url)
+ self.assertEqual(response.data['value'], '')
+
+ # Update machine setting
+ location = StockLocation.objects.create(name='Test Location')
+ response = self.patch(machine_setting_url, {'value': str(location.pk)})
+ self.assertEqual(response.data['value'], str(location.pk))
+
+ response = self.get(machine_setting_url)
+ self.assertEqual(response.data['value'], str(location.pk))
+
+ # Update driver setting
+ response = self.patch(driver_setting_url, {'value': 'test value'})
+ self.assertEqual(response.data['value'], 'test value')
+
+ response = self.get(driver_setting_url)
+ self.assertEqual(response.data['value'], 'test value')
+
+ # Get list of all settings for a machine
+ settings_url = reverse('api-machine-settings', kwargs={'pk': machine.pk})
+ response = self.get(settings_url)
+ self.assertEqual(len(response.data), 2)
+ self.assertEqual(
+ [('M', 'LOCATION'), ('D', 'TEST_SETTING')],
+ [(s['config_type'], s['key']) for s in response.data],
+ )
+
+ def test_machine_restart(self):
+ """Test machine restart API endpoint."""
+ machine = MachineConfig.objects.create(
+ machine_type='label-printer',
+ driver='test-label-printer',
+ name='Test Machine',
+ active=True,
+ )
+
+ response = self.get(reverse('api-machine-detail', kwargs={'pk': machine.pk}))
+ self.assertEqual(response.data['status_text'], '')
+
+ response = self.post(
+ reverse('api-machine-restart', kwargs={'pk': machine.pk}), expected_code=200
+ )
+
+ response = self.get(reverse('api-machine-detail', kwargs={'pk': machine.pk}))
+ self.assertEqual(response.data['status_text'], 'Restarting...')
diff --git a/src/frontend/src/tables/machine/MachineListTable.tsx b/src/frontend/src/tables/machine/MachineListTable.tsx
index 390e493d91a..813d7842553 100644
--- a/src/frontend/src/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/tables/machine/MachineListTable.tsx
@@ -405,7 +405,7 @@ export function MachineListTable({
sortable: true,
render: function (record) {
return (
-
+
{record.name}
{record.restart_required && (
From 32dc782beb42d887847d924d4e06e07f18600df2 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Tue, 6 Feb 2024 13:33:16 +0000
Subject: [PATCH 77/86] Test ci?
---
InvenTree/machine/test_api.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/InvenTree/machine/test_api.py b/InvenTree/machine/test_api.py
index 261d7728f6f..c195a795c28 100644
--- a/InvenTree/machine/test_api.py
+++ b/InvenTree/machine/test_api.py
@@ -243,12 +243,15 @@ def test_machine_restart(self):
active=True,
)
+ # verify machine status before restart
response = self.get(reverse('api-machine-detail', kwargs={'pk': machine.pk}))
self.assertEqual(response.data['status_text'], '')
+ # restart the machine
response = self.post(
reverse('api-machine-restart', kwargs={'pk': machine.pk}), expected_code=200
)
+ # verify machine status after restart
response = self.get(reverse('api-machine-detail', kwargs={'pk': machine.pk}))
self.assertEqual(response.data['status_text'], 'Restarting...')
From 7cfe1e83ed3ec0c1774fc7a17c92a3f0a5a102e8 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Tue, 6 Feb 2024 21:11:08 +0000
Subject: [PATCH 78/86] Add more tests
---
InvenTree/machine/test_api.py | 71 +++++++++-----
InvenTree/machine/tests.py | 174 +++++++++++++++++++++++++++++++++-
2 files changed, 218 insertions(+), 27 deletions(-)
diff --git a/InvenTree/machine/test_api.py b/InvenTree/machine/test_api.py
index c195a795c28..0cad560e639 100644
--- a/InvenTree/machine/test_api.py
+++ b/InvenTree/machine/test_api.py
@@ -15,14 +15,14 @@ class MachineAPITest(InvenTreeAPITestCase):
roles = ['admin.add', 'admin.view', 'admin.change', 'admin.delete']
- @classmethod
- def setUpClass(cls):
+ # @classmethod
+ def setUp(self):
"""Setup some testing drivers/machines."""
class TestingLabelPrinterDriver(BaseLabelPrintingDriver):
"""Test driver for label printing."""
- SLUG = 'test-label-printer'
+ SLUG = 'test-label-printer-api'
NAME = 'Test label printer'
DESCRIPTION = 'This is a test label printer driver for testing.'
@@ -41,19 +41,23 @@ def print_label(self, *args, **kwargs) -> None:
"""Override print_label."""
pass
- class CopyTestingLabelPrinterDriver(BaseLabelPrintingDriver):
+ class TestingLabelPrinterDriverError1(BaseLabelPrintingDriver):
"""Test driver for label printing."""
- SLUG = 'test-label-printer'
- NAME = 'Test label printer'
+ SLUG = 'test-label-printer-error'
+ NAME = 'Test label printer error'
DESCRIPTION = 'This is a test label printer driver for testing.'
- MACHINE_SETTINGS = {
- 'TEST_SETTING': {
- 'name': 'Test setting',
- 'description': 'This is a test setting',
- }
- }
+ def print_label(self, *args, **kwargs) -> None:
+ """Override print_label."""
+ pass
+
+ class TestingLabelPrinterDriverError2(BaseLabelPrintingDriver):
+ """Test driver for label printing."""
+
+ SLUG = 'test-label-printer-error'
+ NAME = 'Test label printer error'
+ DESCRIPTION = 'This is a test label printer driver for testing.'
def print_label(self, *args, **kwargs) -> None:
"""Override print_label."""
@@ -61,12 +65,26 @@ def print_label(self, *args, **kwargs) -> None:
registry.initialize()
- super().setUpClass()
+ super().setUp()
+
+ # @classmethod
+ def tearDown(self) -> None:
+ """Clean up after testing."""
+ registry.machine_types = {}
+ registry.drivers = {}
+ registry.driver_instances = {}
+ registry.machines = {}
+ registry.base_drivers = []
+ registry.errors = []
+
+ return super().tearDown()
def test_machine_type_list(self):
"""Test machine types list API endpoint."""
response = self.get(reverse('api-machine-types'))
- self.assertEqual(len(response.data), 1)
+ machine_type = [t for t in response.data if t['slug'] == 'label-printer']
+ self.assertEqual(len(machine_type), 1)
+ machine_type = machine_type[0]
self.assertDictContainsSubset(
{
'slug': 'label-printer',
@@ -75,10 +93,10 @@ def test_machine_type_list(self):
'provider_plugin': None,
'is_builtin': True,
},
- response.data[0],
+ machine_type,
)
self.assertTrue(
- response.data[0]['provider_file'].endswith(
+ machine_type['provider_file'].endswith(
'machine/machine_types/LabelPrintingMachineType.py'
)
)
@@ -86,10 +104,12 @@ def test_machine_type_list(self):
def test_machine_driver_list(self):
"""Test machine driver list API endpoint."""
response = self.get(reverse('api-machine-drivers'))
- self.assertEqual(len(response.data), 1)
+ driver = [a for a in response.data if a['slug'] == 'test-label-printer-api']
+ self.assertEqual(len(driver), 1)
+ driver = driver[0]
self.assertDictContainsSubset(
{
- 'slug': 'test-label-printer',
+ 'slug': 'test-label-printer-api',
'name': 'Test label printer',
'description': 'This is a test label printer driver for testing.',
'provider_plugin': None,
@@ -97,14 +117,15 @@ def test_machine_driver_list(self):
'machine_type': 'label-printer',
'driver_errors': [],
},
- response.data[0],
+ driver,
)
+ self.assertEqual(driver['provider_file'], __file__)
def test_machine_status(self):
"""Test machine status API endpoint."""
response = self.get(reverse('api-machine-registry-status'))
self.assertIn(
- "Cannot re-register driver 'test-label-printer'",
+ "Cannot re-register driver 'test-label-printer-error'",
[e['message'] for e in response.data['registry_errors']],
)
@@ -115,7 +136,7 @@ def test_machine_list(self):
MachineConfig.objects.create(
machine_type='label-printer',
- driver='test-label-printer',
+ driver='test-label-printer-api',
name='Test Machine',
active=True,
)
@@ -126,7 +147,7 @@ def test_machine_list(self):
{
'name': 'Test Machine',
'machine_type': 'label-printer',
- 'driver': 'test-label-printer',
+ 'driver': 'test-label-printer-api',
'initialized': True,
'active': True,
'status': 101,
@@ -148,7 +169,7 @@ def test_machine_detail(self):
machine_data = {
'machine_type': 'label-printer',
- 'driver': 'test-label-printer',
+ 'driver': 'test-label-printer-api',
'name': 'Test Machine',
'active': True,
}
@@ -190,7 +211,7 @@ def test_machine_detail_settings(self):
"""Test machine detail settings API endpoint."""
machine = MachineConfig.objects.create(
machine_type='label-printer',
- driver='test-label-printer',
+ driver='test-label-printer-api',
name='Test Machine with settings',
active=True,
)
@@ -238,7 +259,7 @@ def test_machine_restart(self):
"""Test machine restart API endpoint."""
machine = MachineConfig.objects.create(
machine_type='label-printer',
- driver='test-label-printer',
+ driver='test-label-printer-api',
name='Test Machine',
active=True,
)
diff --git a/InvenTree/machine/tests.py b/InvenTree/machine/tests.py
index 413d0d4bc7e..971d1bbf908 100755
--- a/InvenTree/machine/tests.py
+++ b/InvenTree/machine/tests.py
@@ -1,5 +1,175 @@
"""Machine app tests."""
-# from django.test import TestCase
+from unittest.mock import MagicMock, Mock
-# Create your tests here.
+from django.test import TestCase
+
+from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
+from machine.models import MachineConfig
+from machine.registry import registry
+
+
+class TestDriverMachineInterface(TestCase):
+ """Test the machine registry."""
+
+ def setUp(self):
+ """Setup some testing drivers/machines."""
+
+ class TestingMachineBaseDriver(BaseDriver):
+ """Test base driver for testing machines."""
+
+ machine_type = 'testing-type'
+
+ class TestingMachineType(BaseMachineType):
+ """Test machine type for testing."""
+
+ SLUG = 'testing-type'
+ NAME = 'Testing machine type'
+ DESCRIPTION = 'This is a test machine type for testing.'
+
+ base_driver = TestingMachineBaseDriver
+
+ class TestingMachineTypeStatus(MachineStatus):
+ """Test machine status."""
+
+ UNKNOWN = 100, 'Unknown', 'secondary'
+
+ MACHINE_STATUS = TestingMachineTypeStatus
+ default_machine_status = MACHINE_STATUS.UNKNOWN
+
+ class TestingDriver(TestingMachineBaseDriver):
+ """Test driver for testing machines."""
+
+ SLUG = 'test-driver'
+ NAME = 'Test Driver'
+ DESCRIPTION = 'This is a test driver for testing.'
+
+ MACHINE_SETTINGS = {
+ 'TEST_SETTING': {'name': 'Test Setting', 'description': 'Test setting'}
+ }
+
+ self.machine1 = MachineConfig.objects.create(
+ name='Test Machine 1',
+ machine_type='testing-type',
+ driver='test-driver',
+ active=True,
+ )
+ self.machine2 = MachineConfig.objects.create(
+ name='Test Machine 2',
+ machine_type='testing-type',
+ driver='test-driver',
+ active=True,
+ )
+ self.machine3 = MachineConfig.objects.create(
+ name='Test Machine 3',
+ machine_type='testing-type',
+ driver='test-driver',
+ active=False,
+ )
+ self.machines = [self.machine1, self.machine2, self.machine3]
+
+ # mock driver implementation
+ self.driver_mocks = {
+ k: Mock()
+ for k in [
+ 'init_driver',
+ 'init_machine',
+ 'update_machine',
+ 'restart_machine',
+ ]
+ }
+ for key, value in self.driver_mocks.items():
+ setattr(TestingDriver, key, value)
+
+ # save machines
+ for m in self.machines:
+ m.save()
+
+ # init registry
+ registry.initialize()
+
+ # mock machine implementation
+ self.machine_mocks = {
+ m: {k: MagicMock() for k in ['update', 'restart']} for m in self.machines
+ }
+ for machine_config, mock_dict in self.machine_mocks.items():
+ for key, mock in mock_dict.items():
+ mock.side_effect = getattr(machine_config.machine, key)
+ setattr(machine_config.machine, key, mock)
+
+ super().setUp()
+
+ def tearDown(self) -> None:
+ """Clean up after testing."""
+ registry.machine_types = {}
+ registry.drivers = {}
+ registry.driver_instances = {}
+ registry.machines = {}
+ registry.base_drivers = []
+ registry.errors = []
+
+ return super().tearDown()
+
+ def test_machine_lifecycle(self):
+ """Test the machine registry."""
+ # test that the registry is initialized correctly
+ self.assertEqual(len(registry.machines), 3)
+ self.assertEqual(len(registry.driver_instances), 1)
+
+ # test get_machines
+ self.assertEqual(len(registry.get_machines()), 2)
+ self.assertEqual(len(registry.get_machines(active=False, initialized=False)), 1)
+ self.assertEqual(len(registry.get_machines(name='Test Machine 1')), 1)
+ self.assertEqual(
+ len(registry.get_machines(name='Test Machine 1', active=False)), 0
+ )
+ self.assertEqual(
+ len(registry.get_machines(name='Test Machine 1', active=True)), 1
+ )
+
+ # test get_machine
+ self.assertEqual(registry.get_machine(self.machine1.pk), self.machine1.machine)
+
+ # test get_drivers
+ self.assertEqual(len(registry.get_drivers('testing-type')), 1)
+ self.assertEqual(registry.get_drivers('testing-type')[0].SLUG, 'test-driver')
+
+ # test that init hooks where called correctly
+ self.driver_mocks['init_driver'].assert_called_once()
+ self.assertEqual(self.driver_mocks['init_machine'].call_count, 2)
+
+ # Test machine restart hook
+ registry.restart_machine(self.machine1.machine)
+ self.driver_mocks['restart_machine'].assert_called_once_with(
+ self.machine1.machine
+ )
+ self.assertEqual(self.machine_mocks[self.machine1]['restart'].call_count, 1)
+
+ # Test machine update hook
+ self.machine1.name = 'Test Machine 1 - Updated'
+ self.machine1.save()
+ self.driver_mocks['update_machine'].assert_called_once()
+ self.assertEqual(self.machine_mocks[self.machine1]['update'].call_count, 1)
+ old_machine_state, machine = self.driver_mocks['update_machine'].call_args.args
+ self.assertEqual(old_machine_state['name'], 'Test Machine 1')
+ self.assertEqual(machine.name, 'Test Machine 1 - Updated')
+ self.assertEqual(self.machine1.machine, machine)
+ self.machine_mocks[self.machine1]['update'].reset_mock()
+
+ # get ref to machine 1
+ machine1: BaseMachineType = self.machine1.machine # type: ignore
+ self.assertIsNotNone(machine1)
+
+ # Test machine setting update hook
+ self.assertEqual(machine1.get_setting('TEST_SETTING', 'D'), '')
+ machine1.set_setting('TEST_SETTING', 'D', 'test-value')
+ self.assertEqual(self.machine_mocks[self.machine1]['update'].call_count, 2)
+ old_machine_state, machine = self.driver_mocks['update_machine'].call_args.args
+ self.assertEqual(old_machine_state['settings']['D', 'TEST_SETTING'], '')
+ self.assertEqual(machine1.get_setting('TEST_SETTING', 'D'), 'test-value')
+ self.assertEqual(self.machine1.machine, machine)
+
+ # Test remove machine
+ self.assertEqual(len(registry.get_machines()), 2)
+ registry.remove_machine(machine1)
+ self.assertEqual(len(registry.get_machines()), 1)
From be6eb54d7337b022552a6452e9ea6385bcfbce42 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Wed, 7 Feb 2024 10:44:18 +0000
Subject: [PATCH 79/86] Added more tests
---
.pre-commit-config.yaml | 2 +-
InvenTree/machine/machine_type.py | 10 ---
.../machine_types/LabelPrintingMachineType.py | 1 -
InvenTree/machine/registry.py | 4 +-
InvenTree/machine/serializers.py | 1 -
InvenTree/machine/test_api.py | 77 +++++++++++++------
InvenTree/machine/tests.py | 68 ++++++++--------
7 files changed, 96 insertions(+), 67 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 790aaf5df58..8c359ac93d6 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -13,7 +13,7 @@ repos:
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- # - id: check-yaml
+ - id: check-yaml
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.1
diff --git a/InvenTree/machine/machine_type.py b/InvenTree/machine/machine_type.py
index fd1e052d262..f928d65735e 100644
--- a/InvenTree/machine/machine_type.py
+++ b/InvenTree/machine/machine_type.py
@@ -14,13 +14,9 @@
class MachineConfig:
"""Only used if not typechecking currently."""
- pass
-
class SettingsKeyType:
"""Only used if not typechecking currently."""
- pass
-
class MachineStatus(StatusCode):
"""Base class for representing a set of machine status codes.
@@ -47,8 +43,6 @@ class MachineStatus(StatusCode):
```
"""
- pass
-
class BaseDriver(ClassValidationMixin, ClassProviderMixin):
"""Base class for all machine drivers.
@@ -83,7 +77,6 @@ def init_driver(self):
After the driver is initialized, the self.init_machine function is
called for each machine associated with that driver.
"""
- pass
def init_machine(self, machine: 'BaseMachineType'):
"""This method gets called for each active machine using that driver while initialization.
@@ -94,7 +87,6 @@ def init_machine(self, machine: 'BaseMachineType'):
Arguments:
machine: Machine instance
"""
- pass
def update_machine(
self, old_machine_state: Dict[str, Any], machine: 'BaseMachineType'
@@ -108,7 +100,6 @@ def update_machine(
old_machine_state: Dict holding the old machine state before update
machine: Machine instance with the new state
"""
- pass
def restart_machine(self, machine: 'BaseMachineType'):
"""This method gets called on manual machine restart e.g. by using the restart machine action in the Admin Center.
@@ -119,7 +110,6 @@ def restart_machine(self, machine: 'BaseMachineType'):
Arguments:
machine: Machine instance
"""
- pass
def get_machines(self, **kwargs):
"""Return all machines using this driver (By default only initialized machines).
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
index 9e2bfcffbfb..b0a78cb484b 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/LabelPrintingMachineType.py
@@ -51,7 +51,6 @@ def print_label(
Note that the supplied args/kwargs may be different if the driver overrides the print_labels() method.
"""
- pass
def print_labels(
self,
diff --git a/InvenTree/machine/registry.py b/InvenTree/machine/registry.py
index f949f299b88..3caa5e3178c 100644
--- a/InvenTree/machine/registry.py
+++ b/InvenTree/machine/registry.py
@@ -183,7 +183,9 @@ def get_machines(self, **kwargs):
def filter_machine(machine: BaseMachineType):
for key, value in kwargs.items():
if key not in allowed_fields:
- continue
+ raise ValueError(
+ f"'{key}' is not a valid filter field for registry.get_machines."
+ )
# check if current driver is subclass from base_driver
if key == 'base_driver':
diff --git a/InvenTree/machine/serializers.py b/InvenTree/machine/serializers.py
index 16e024aec07..88e87d2d181 100644
--- a/InvenTree/machine/serializers.py
+++ b/InvenTree/machine/serializers.py
@@ -57,7 +57,6 @@ def get_status_model(self, obj: MachineConfig) -> Union[str, None]:
"""Serializer method for the status model field."""
if obj.machine and obj.machine.MACHINE_STATUS:
return obj.machine.MACHINE_STATUS.__name__
- return None
def get_status_text(self, obj: MachineConfig) -> str:
"""Serializer method for the status text field."""
diff --git a/InvenTree/machine/test_api.py b/InvenTree/machine/test_api.py
index 0cad560e639..c87f43c9c6f 100644
--- a/InvenTree/machine/test_api.py
+++ b/InvenTree/machine/test_api.py
@@ -1,21 +1,26 @@
"""Machine API tests."""
+import re
+from typing import cast
+
from django.urls import reverse
from InvenTree.unit_test import InvenTreeAPITestCase
from machine import registry
-from machine.machine_type import BaseMachineType
+from machine.machine_type import BaseDriver, BaseMachineType
from machine.machine_types import BaseLabelPrintingDriver
from machine.models import MachineConfig
+from machine.tests import TestMachineRegistryMixin
from stock.models import StockLocation
-class MachineAPITest(InvenTreeAPITestCase):
+class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
"""Class for unit testing machine API endpoints."""
roles = ['admin.add', 'admin.view', 'admin.change', 'admin.delete']
- # @classmethod
+ placeholder_uuid = '00000000-0000-0000-0000-000000000000'
+
def setUp(self):
"""Setup some testing drivers/machines."""
@@ -39,7 +44,6 @@ def restart_machine(self, machine: BaseMachineType):
def print_label(self, *args, **kwargs) -> None:
"""Override print_label."""
- pass
class TestingLabelPrinterDriverError1(BaseLabelPrintingDriver):
"""Test driver for label printing."""
@@ -50,7 +54,6 @@ class TestingLabelPrinterDriverError1(BaseLabelPrintingDriver):
def print_label(self, *args, **kwargs) -> None:
"""Override print_label."""
- pass
class TestingLabelPrinterDriverError2(BaseLabelPrintingDriver):
"""Test driver for label printing."""
@@ -61,23 +64,17 @@ class TestingLabelPrinterDriverError2(BaseLabelPrintingDriver):
def print_label(self, *args, **kwargs) -> None:
"""Override print_label."""
- pass
- registry.initialize()
+ class TestingLabelPrinterDriverNotImplemented(BaseLabelPrintingDriver):
+ """Test driver for label printing."""
- super().setUp()
+ SLUG = 'test-label-printer-not-implemented'
+ NAME = 'Test label printer error not implemented'
+ DESCRIPTION = 'This is a test label printer driver for testing.'
- # @classmethod
- def tearDown(self) -> None:
- """Clean up after testing."""
- registry.machine_types = {}
- registry.drivers = {}
- registry.driver_instances = {}
- registry.machines = {}
- registry.base_drivers = []
- registry.errors = []
+ registry.initialize()
- return super().tearDown()
+ super().setUp()
def test_machine_type_list(self):
"""Test machine types list API endpoint."""
@@ -121,13 +118,38 @@ def test_machine_driver_list(self):
)
self.assertEqual(driver['provider_file'], __file__)
+ # Test driver with errors
+ driver_instance = cast(
+ BaseDriver, registry.get_driver_instance('test-label-printer-api')
+ )
+ self.assertIsNotNone(driver_instance)
+ driver_instance.handle_error('Test error')
+
+ response = self.get(reverse('api-machine-drivers'))
+ driver = [a for a in response.data if a['slug'] == 'test-label-printer-api']
+ self.assertEqual(len(driver), 1)
+ driver = driver[0]
+ self.assertEqual(driver['driver_errors'], ['Test error'])
+
def test_machine_status(self):
"""Test machine status API endpoint."""
response = self.get(reverse('api-machine-registry-status'))
- self.assertIn(
+ errors_msgs = [e['message'] for e in response.data['registry_errors']]
+
+ required_patterns = [
+ r'\'\' did not override the required attributes: one of print_label or print_labels',
"Cannot re-register driver 'test-label-printer-error'",
- [e['message'] for e in response.data['registry_errors']],
- )
+ ]
+
+ for pattern in required_patterns:
+ for error in errors_msgs:
+ if re.match(pattern, error):
+ break
+ else:
+ errors_str = '\n'.join([f'- {e}' for e in errors_msgs])
+ self.fail(
+ f"""Error message matching pattern '{pattern}' not found in machine registry errors:\n{errors_str}"""
+ )
def test_machine_list(self):
"""Test machine list API endpoint."""
@@ -160,10 +182,9 @@ def test_machine_list(self):
def test_machine_detail(self):
"""Test machine detail API endpoint."""
- placeholder_uuid = '00000000-0000-0000-0000-000000000000'
self.assertFalse(len(MachineConfig.objects.all()), 0)
self.get(
- reverse('api-machine-detail', kwargs={'pk': placeholder_uuid}),
+ reverse('api-machine-detail', kwargs={'pk': self.placeholder_uuid}),
expected_code=404,
)
@@ -209,12 +230,22 @@ def test_machine_detail(self):
def test_machine_detail_settings(self):
"""Test machine detail settings API endpoint."""
+ machine_setting_url = reverse(
+ 'api-machine-settings-detail',
+ kwargs={'pk': self.placeholder_uuid, 'config_type': 'M', 'key': 'LOCATION'},
+ )
+
+ # Test machine settings for non-existent machine
+ self.get(machine_setting_url, expected_code=404)
+
+ # Create a machine
machine = MachineConfig.objects.create(
machine_type='label-printer',
driver='test-label-printer-api',
name='Test Machine with settings',
active=True,
)
+
machine_setting_url = reverse(
'api-machine-settings-detail',
kwargs={'pk': machine.pk, 'config_type': 'M', 'key': 'LOCATION'},
diff --git a/InvenTree/machine/tests.py b/InvenTree/machine/tests.py
index 971d1bbf908..51e9b377ed5 100755
--- a/InvenTree/machine/tests.py
+++ b/InvenTree/machine/tests.py
@@ -9,7 +9,22 @@
from machine.registry import registry
-class TestDriverMachineInterface(TestCase):
+class TestMachineRegistryMixin(TestCase):
+ """Machine registry test mixin to setup the registry between tests correctly."""
+
+ def tearDown(self) -> None:
+ """Clean up after testing."""
+ registry.machine_types = {}
+ registry.drivers = {}
+ registry.driver_instances = {}
+ registry.machines = {}
+ registry.base_drivers = []
+ registry.errors = []
+
+ return super().tearDown()
+
+
+class TestDriverMachineInterface(TestMachineRegistryMixin, TestCase):
"""Test the machine registry."""
def setUp(self):
@@ -48,6 +63,20 @@ class TestingDriver(TestingMachineBaseDriver):
'TEST_SETTING': {'name': 'Test Setting', 'description': 'Test setting'}
}
+ # mock driver implementation
+ self.driver_mocks = {
+ k: Mock()
+ for k in [
+ 'init_driver',
+ 'init_machine',
+ 'update_machine',
+ 'restart_machine',
+ ]
+ }
+
+ for key, value in self.driver_mocks.items():
+ setattr(TestingDriver, key, value)
+
self.machine1 = MachineConfig.objects.create(
name='Test Machine 1',
machine_type='testing-type',
@@ -68,23 +97,6 @@ class TestingDriver(TestingMachineBaseDriver):
)
self.machines = [self.machine1, self.machine2, self.machine3]
- # mock driver implementation
- self.driver_mocks = {
- k: Mock()
- for k in [
- 'init_driver',
- 'init_machine',
- 'update_machine',
- 'restart_machine',
- ]
- }
- for key, value in self.driver_mocks.items():
- setattr(TestingDriver, key, value)
-
- # save machines
- for m in self.machines:
- m.save()
-
# init registry
registry.initialize()
@@ -99,19 +111,8 @@ class TestingDriver(TestingMachineBaseDriver):
super().setUp()
- def tearDown(self) -> None:
- """Clean up after testing."""
- registry.machine_types = {}
- registry.drivers = {}
- registry.driver_instances = {}
- registry.machines = {}
- registry.base_drivers = []
- registry.errors = []
-
- return super().tearDown()
-
def test_machine_lifecycle(self):
- """Test the machine registry."""
+ """Test the machine lifecycle."""
# test that the registry is initialized correctly
self.assertEqual(len(registry.machines), 3)
self.assertEqual(len(registry.driver_instances), 1)
@@ -127,6 +128,13 @@ def test_machine_lifecycle(self):
len(registry.get_machines(name='Test Machine 1', active=True)), 1
)
+ # test get_machines with an unknown filter
+ with self.assertRaisesMessage(
+ ValueError,
+ "'unknown_filter' is not a valid filter field for registry.get_machines.",
+ ):
+ registry.get_machines(unknown_filter='test')
+
# test get_machine
self.assertEqual(registry.get_machine(self.machine1.pk), self.machine1.machine)
From 17f8bb474e55924ce477082889fd52bbe23d08f9 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Wed, 7 Feb 2024 11:03:57 +0000
Subject: [PATCH 80/86] Bump api version
---
InvenTree/InvenTree/api_version.py | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py
index 38d08a7bf79..41714859145 100644
--- a/InvenTree/InvenTree/api_version.py
+++ b/InvenTree/InvenTree/api_version.py
@@ -1,11 +1,20 @@
"""InvenTree API version information."""
# InvenTree API version
-INVENTREE_API_VERSION = 166
+INVENTREE_API_VERSION = 167
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
+v167 -> 2024-02-07 : https://github.com/inventree/InvenTree/pull/4824
+ - Adds machine CRUD API endpoints
+ - Adds machine settings API endpoints
+ - Adds machine restart API endpoint
+ - Adds machine types/drivers list API endpoints
+ - Adds machine registry status API endpoint
+ - Adds 'required' field to the global Settings API
+ - Discover sub-sub classes of the StatusCode API
+
v166 -> 2024-02-04 : https://github.com/inventree/InvenTree/pull/6400
- Adds package_name to plugin API
- Adds mechanism for uninstalling plugins via the API
From b4f6758350bfd367692d60655ebd41f793a31c3f Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Wed, 7 Feb 2024 12:03:10 +0000
Subject: [PATCH 81/86] Changed driver/base driver naming schema
---
InvenTree/machine/machine_types/__init__.py | 10 +++---
...rintingMachineType.py => label_printer.py} | 22 ++++++-------
InvenTree/machine/test_api.py | 12 +++----
.../builtin/labels/inventree_machine.py | 12 +++----
docs/docs/extend/machines/label_printer.md | 6 ++--
docs/docs/extend/machines/overview.md | 31 +++++++++----------
6 files changed, 46 insertions(+), 47 deletions(-)
rename InvenTree/machine/machine_types/{LabelPrintingMachineType.py => label_printer.py} (94%)
diff --git a/InvenTree/machine/machine_types/__init__.py b/InvenTree/machine/machine_types/__init__.py
index f59f4adb4b8..6fe810f21a5 100644
--- a/InvenTree/machine/machine_types/__init__.py
+++ b/InvenTree/machine/machine_types/__init__.py
@@ -1,11 +1,11 @@
-from machine.machine_types.LabelPrintingMachineType import (
- BaseLabelPrintingDriver,
- LabelPrintingMachineType,
+from machine.machine_types.label_printer import (
+ LabelPrinterBaseDriver,
+ LabelPrinterMachine,
)
__all__ = [
# machine types
- 'LabelPrintingMachineType',
+ 'LabelPrinterMachine',
# base drivers
- 'BaseLabelPrintingDriver',
+ 'LabelPrinterBaseDriver',
]
diff --git a/InvenTree/machine/machine_types/LabelPrintingMachineType.py b/InvenTree/machine/machine_types/label_printer.py
similarity index 94%
rename from InvenTree/machine/machine_types/LabelPrintingMachineType.py
rename to InvenTree/machine/machine_types/label_printer.py
index b0a78cb484b..73fa57f2d79 100644
--- a/InvenTree/machine/machine_types/LabelPrintingMachineType.py
+++ b/InvenTree/machine/machine_types/label_printer.py
@@ -17,8 +17,8 @@
from stock.models import StockLocation
-class BaseLabelPrintingDriver(BaseDriver):
- """Base label printing driver.
+class LabelPrinterBaseDriver(BaseDriver):
+ """Base driver for label printer machines.
Attributes:
USE_BACKGROUND_WORKER (bool): If True, the `print_label()` and `print_labels()` methods will be run in a background worker (default: True)
@@ -30,7 +30,7 @@ class BaseLabelPrintingDriver(BaseDriver):
def print_label(
self,
- machine: 'LabelPrintingMachineType',
+ machine: 'LabelPrinterMachine',
label: LabelTemplate,
item: LabelItemType,
request: Request,
@@ -54,7 +54,7 @@ def print_label(
def print_labels(
self,
- machine: 'LabelPrintingMachineType',
+ machine: 'LabelPrinterMachine',
label: LabelTemplate,
items: QuerySet[LabelItemType],
request: Request,
@@ -84,7 +84,7 @@ def print_labels(
def get_printers(
self, label: LabelTemplate, items: QuerySet[LabelItemType], **kwargs
- ) -> list['LabelPrintingMachineType']:
+ ) -> list['LabelPrinterMachine']:
"""Get all printers that would be available to print this job.
By default all printers that are initialized using this driver are returned.
@@ -96,11 +96,11 @@ def get_printers(
Keyword Arguments:
request (Request): The django request used to make the get printers request
"""
- return cast(list['LabelPrintingMachineType'], self.get_machines())
+ return cast(list['LabelPrinterMachine'], self.get_machines())
def get_printing_options_serializer(
self, request: Request, *args, **kwargs
- ) -> 'BaseLabelPrintingDriver.PrintingOptionsSerializer':
+ ) -> 'LabelPrinterBaseDriver.PrintingOptionsSerializer':
"""Return a serializer class instance with dynamic printing options.
Arguments:
@@ -196,10 +196,10 @@ class PrintingOptionsSerializer(serializers.Serializer):
Example:
This example shows how to extend the default serializer and add a new option:
```py
- class MyDriver(BaseLabelPrintingDriver):
+ class MyDriver(LabelPrinterBaseDriver):
# ...
- class PrintingOptionsSerializer(BaseLabelPrintingDriver.PrintingOptionsSerializer):
+ class PrintingOptionsSerializer(LabelPrinterBaseDriver.PrintingOptionsSerializer):
auto_cut = serializers.BooleanField(
default=True,
label=_('Auto cut'),
@@ -233,14 +233,14 @@ class LabelPrinterStatus(MachineStatus):
DISCONNECTED = 400, _('Disconnected'), 'danger'
-class LabelPrintingMachineType(BaseMachineType):
+class LabelPrinterMachine(BaseMachineType):
"""Label printer machine type, is a direct integration to print labels for various items."""
SLUG = 'label-printer'
NAME = _('Label Printer')
DESCRIPTION = _('Directly print labels for various items.')
- base_driver = BaseLabelPrintingDriver
+ base_driver = LabelPrinterBaseDriver
MACHINE_SETTINGS = {
'LOCATION': {
diff --git a/InvenTree/machine/test_api.py b/InvenTree/machine/test_api.py
index c87f43c9c6f..24deed5e833 100644
--- a/InvenTree/machine/test_api.py
+++ b/InvenTree/machine/test_api.py
@@ -8,7 +8,7 @@
from InvenTree.unit_test import InvenTreeAPITestCase
from machine import registry
from machine.machine_type import BaseDriver, BaseMachineType
-from machine.machine_types import BaseLabelPrintingDriver
+from machine.machine_types import LabelPrinterBaseDriver
from machine.models import MachineConfig
from machine.tests import TestMachineRegistryMixin
from stock.models import StockLocation
@@ -24,7 +24,7 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
def setUp(self):
"""Setup some testing drivers/machines."""
- class TestingLabelPrinterDriver(BaseLabelPrintingDriver):
+ class TestingLabelPrinterDriver(LabelPrinterBaseDriver):
"""Test driver for label printing."""
SLUG = 'test-label-printer-api'
@@ -45,7 +45,7 @@ def restart_machine(self, machine: BaseMachineType):
def print_label(self, *args, **kwargs) -> None:
"""Override print_label."""
- class TestingLabelPrinterDriverError1(BaseLabelPrintingDriver):
+ class TestingLabelPrinterDriverError1(LabelPrinterBaseDriver):
"""Test driver for label printing."""
SLUG = 'test-label-printer-error'
@@ -55,7 +55,7 @@ class TestingLabelPrinterDriverError1(BaseLabelPrintingDriver):
def print_label(self, *args, **kwargs) -> None:
"""Override print_label."""
- class TestingLabelPrinterDriverError2(BaseLabelPrintingDriver):
+ class TestingLabelPrinterDriverError2(LabelPrinterBaseDriver):
"""Test driver for label printing."""
SLUG = 'test-label-printer-error'
@@ -65,7 +65,7 @@ class TestingLabelPrinterDriverError2(BaseLabelPrintingDriver):
def print_label(self, *args, **kwargs) -> None:
"""Override print_label."""
- class TestingLabelPrinterDriverNotImplemented(BaseLabelPrintingDriver):
+ class TestingLabelPrinterDriverNotImplemented(LabelPrinterBaseDriver):
"""Test driver for label printing."""
SLUG = 'test-label-printer-not-implemented'
@@ -94,7 +94,7 @@ def test_machine_type_list(self):
)
self.assertTrue(
machine_type['provider_file'].endswith(
- 'machine/machine_types/LabelPrintingMachineType.py'
+ 'machine/machine_types/label_printer.py'
)
)
diff --git a/InvenTree/plugin/builtin/labels/inventree_machine.py b/InvenTree/plugin/builtin/labels/inventree_machine.py
index 826564dad9f..c2794aac978 100644
--- a/InvenTree/plugin/builtin/labels/inventree_machine.py
+++ b/InvenTree/plugin/builtin/labels/inventree_machine.py
@@ -11,7 +11,7 @@
from InvenTree.serializers import DependentField
from InvenTree.tasks import offload_task
from label.models import LabelTemplate
-from machine.machine_types import BaseLabelPrintingDriver, LabelPrintingMachineType
+from machine.machine_types import LabelPrinterBaseDriver, LabelPrinterMachine
from plugin import InvenTreePlugin
from plugin.machine import registry
from plugin.mixins import LabelPrintingMixin
@@ -27,13 +27,13 @@ def get_machine_and_driver(machine_pk: str):
if machine.SLUG != 'label_printer':
return None, None
- machine = cast(LabelPrintingMachineType, machine)
+ machine = cast(LabelPrinterMachine, machine)
driver = machine.driver
if driver is None:
return machine, None
- return machine, cast(BaseLabelPrintingDriver, driver)
+ return machine, cast(LabelPrinterBaseDriver, driver)
def get_last_used_printers(user):
@@ -114,9 +114,9 @@ def __init__(self, *args, **kwargs):
items_to_print = view.get_items()
# get all available printers for each driver
- machines: list[LabelPrintingMachineType] = []
+ machines: list[LabelPrinterMachine] = []
for driver in cast(
- list[BaseLabelPrintingDriver], registry.get_drivers('label_printer')
+ list[LabelPrinterBaseDriver], registry.get_drivers('label_printer')
):
machines.extend(
driver.get_printers(
@@ -150,7 +150,7 @@ def __init__(self, *args, **kwargs):
self.fields['machine'].choices = choices
- def get_printer_name(self, machine: LabelPrintingMachineType):
+ def get_printer_name(self, machine: LabelPrinterMachine):
"""Construct the printers name."""
name = machine.name
diff --git a/docs/docs/extend/machines/label_printer.md b/docs/docs/extend/machines/label_printer.md
index 00e3bef00f0..060bd3ec0de 100644
--- a/docs/docs/extend/machines/label_printer.md
+++ b/docs/docs/extend/machines/label_printer.md
@@ -4,13 +4,13 @@ Label printer machines can directly print labels for various items in InvenTree.
### Writing your own printing driver
-Take a look at the most basic required code for a driver in this [example](./overview.md#example-driver). Next either implement the [`print_label`](#machine.machine_types.BaseLabelPrintingDriver.print_label) or [`print_labels`](#machine.machine_types.BaseLabelPrintingDriver.print_labels) function.
+Take a look at the most basic required code for a driver in this [example](./overview.md#example-driver). Next either implement the [`print_label`](#machine.machine_types.LabelPrinterBaseDriver.print_label) or [`print_labels`](#machine.machine_types.LabelPrinterBaseDriver.print_labels) function.
### Label printer status
There are a couple of predefined status codes for label printers. By default the `UNKNOWN` status code is set for each machine, but they can be changed at any time by the driver. For more info about status code see [Machine status codes](./overview.md#machine-status).
-::: machine.machine_types.LabelPrintingMachineType.LabelPrinterStatus
+::: machine.machine_types.label_printer.LabelPrinterStatus
options:
heading_level: 4
show_bases: false
@@ -18,7 +18,7 @@ There are a couple of predefined status codes for label printers. By default the
### LabelPrintingDriver API
-::: machine.machine_types.BaseLabelPrintingDriver
+::: machine.machine_types.LabelPrinterBaseDriver
options:
heading_level: 4
show_bases: false
diff --git a/docs/docs/extend/machines/overview.md b/docs/docs/extend/machines/overview.md
index 4e1c29fabff..7ceb8e146cf 100644
--- a/docs/docs/extend/machines/overview.md
+++ b/docs/docs/extend/machines/overview.md
@@ -39,7 +39,7 @@ If you want to create your own machine type, please also take a look at the alre
from django.utils.translation import ugettext_lazy as _
from plugin.machine import BaseDriver, BaseMachineType, MachineStatus
-class BaseABCDriver(BaseDriver):
+class ABCBaseDriver(BaseDriver):
"""Base xyz driver."""
machine_type = 'abc'
@@ -54,12 +54,12 @@ class BaseABCDriver(BaseDriver):
required_overrides = [my_custom_required_method]
-class ABCMachineType(BaseMachineType):
+class ABCMachine(BaseMachineType):
SLUG = 'abc'
NAME = _('ABC')
DESCRIPTION = _('This is an awesome machine type for ABC.')
- base_driver = BaseABCDriver
+ base_driver = ABCBaseDriver
class ABCStatus(MachineStatus):
CONNECTED = 100, _('Connected'), 'success'
@@ -67,7 +67,6 @@ class ABCMachineType(BaseMachineType):
PRINTING = 110, _('Printing'), 'primary'
MACHINE_STATUS = ABCStatus
-
default_machine_status = ABCStatus.DISCONNECTED
```
@@ -103,18 +102,18 @@ A basic driver only needs to specify the basic attributes like `SLUG`, `NAME`, `
```py
from plugin import InvenTreePlugin
-from plugin.machine.machine_types import BaseXYZDriver
+from plugin.machine.machine_types import ABCBaseDriver
-class MyABCXYZDriverPlugin(InvenTreePlugin):
- NAME = "ABCXYZDriver"
- SLUG = "abc-driver"
- TITLE = "ABC XYZ Driver"
+class MyXyzAbcDriverPlugin(InvenTreePlugin):
+ NAME = "XyzAbcDriver"
+ SLUG = "xyz-driver"
+ TITLE = "Xyz Abc Driver"
# ...
-class MyXYZDriver(BaseXYZDriver):
- SLUG = 'my-abc-driver'
- NAME = 'My ABC driver'
- DESCRIPTION = 'This is an awesome driver for ABC'
+class XYZDriver(ABCBaseDriver):
+ SLUG = 'my-xyz-driver'
+ NAME = 'My XYZ driver'
+ DESCRIPTION = 'This is an awesome XYZ driver for a ABC machine'
```
#### Driver API
@@ -136,7 +135,7 @@ class MyXYZDriver(BaseXYZDriver):
Each machine can have different settings configured. There are machine settings that are specific to that machine type and driver settings that are specific to the driver, but both can be specified individually for each machine. Define them by adding a `MACHINE_SETTINGS` dictionary attribute to either the driver or the machine type. The format follows the same pattern as the `SETTINGS` for normal plugins documented on the [`SettingsMixin`](../plugins/settings.md)
```py
-class MyXYZDriver(BaseXYZDriver):
+class MyXYZDriver(ABCBaseDriver):
MACHINE_SETTINGS = {
'SERVER': {
'name': _('Server'),
@@ -175,7 +174,7 @@ class XYZMachineType(BaseMachineType):
And to set a status code for a machine by the driver.
```py
-class MyXYZDriver(BaseXYZDriver):
+class MyXYZDriver(ABCBaseDriver):
# ...
def init_machine(self, machine):
# ... do some init stuff here
@@ -194,7 +193,7 @@ class MyXYZDriver(BaseXYZDriver):
There can also be a free text status code defined.
```py
-class MyXYZDriver(BaseXYZDriver):
+class MyXYZDriver(ABCBaseDriver):
# ...
def init_machine(self, machine):
# ... do some init stuff here
From 502863fbabd2fa8c8c9648a9bf400a01636f6817 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Wed, 7 Feb 2024 14:09:43 +0000
Subject: [PATCH 82/86] Added more tests
---
InvenTree/machine/test_api.py | 2 -
InvenTree/machine/tests.py | 119 ++++++++++++++++++
.../builtin/labels/inventree_machine.py | 10 +-
docs/docs/extend/machines/overview.md | 2 +-
4 files changed, 126 insertions(+), 7 deletions(-)
diff --git a/InvenTree/machine/test_api.py b/InvenTree/machine/test_api.py
index 24deed5e833..1e603bd2c2a 100644
--- a/InvenTree/machine/test_api.py
+++ b/InvenTree/machine/test_api.py
@@ -19,8 +19,6 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
roles = ['admin.add', 'admin.view', 'admin.change', 'admin.delete']
- placeholder_uuid = '00000000-0000-0000-0000-000000000000'
-
def setUp(self):
"""Setup some testing drivers/machines."""
diff --git a/InvenTree/machine/tests.py b/InvenTree/machine/tests.py
index 51e9b377ed5..5bbc04fb5ac 100755
--- a/InvenTree/machine/tests.py
+++ b/InvenTree/machine/tests.py
@@ -1,17 +1,30 @@
"""Machine app tests."""
+from typing import cast
from unittest.mock import MagicMock, Mock
+from django.apps import apps
from django.test import TestCase
+from django.urls import reverse
+from rest_framework import serializers
+
+from InvenTree.unit_test import InvenTreeAPITestCase
+from label.models import PartLabel
from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
+from machine.machine_types.label_printer import LabelPrinterBaseDriver
from machine.models import MachineConfig
from machine.registry import registry
+from part.models import Part
+from plugin.models import PluginConfig
+from plugin.registry import registry as plg_registry
class TestMachineRegistryMixin(TestCase):
"""Machine registry test mixin to setup the registry between tests correctly."""
+ placeholder_uuid = '00000000-0000-0000-0000-000000000000'
+
def tearDown(self) -> None:
"""Clean up after testing."""
registry.machine_types = {}
@@ -181,3 +194,109 @@ def test_machine_lifecycle(self):
self.assertEqual(len(registry.get_machines()), 2)
registry.remove_machine(machine1)
self.assertEqual(len(registry.get_machines()), 1)
+
+
+class TestLabelPrinterMachineType(TestMachineRegistryMixin, InvenTreeAPITestCase):
+ """Test the label printer machine type."""
+
+ fixtures = ['category', 'part', 'location', 'stock']
+
+ def setUp(self):
+ """Setup the label printer machine type."""
+ super().setUp()
+
+ class TestingLabelPrinterDriver(LabelPrinterBaseDriver):
+ """Label printer driver for testing."""
+
+ SLUG = 'testing-label-printer'
+ NAME = 'Testing Label Printer'
+ DESCRIPTION = 'This is a test label printer driver for testing.'
+
+ class PrintingOptionsSerializer(
+ LabelPrinterBaseDriver.PrintingOptionsSerializer
+ ):
+ """Test printing options serializer."""
+
+ test_option = serializers.IntegerField()
+
+ def print_label(self, *args, **kwargs):
+ """Mock print label method so that there are no errors."""
+
+ self.machine = MachineConfig.objects.create(
+ name='Test Label Printer',
+ machine_type='label-printer',
+ driver='testing-label-printer',
+ active=True,
+ )
+
+ registry.initialize()
+ driver_instance = cast(
+ TestingLabelPrinterDriver,
+ registry.get_driver_instance('testing-label-printer'),
+ )
+
+ self.print_label = Mock()
+ driver_instance.print_label = self.print_label
+
+ self.print_labels = Mock(side_effect=driver_instance.print_labels)
+ driver_instance.print_labels = self.print_labels
+
+ def test_print_label(self):
+ """Test the print label method."""
+ plugin_ref = 'inventreelabelmachine'
+
+ # setup the label app
+ apps.get_app_config('label').create_labels() # type: ignore
+ plg_registry.reload_plugins()
+ config = cast(PluginConfig, plg_registry.get_plugin(plugin_ref).plugin_config()) # type: ignore
+ config.active = True
+ config.save()
+
+ parts = Part.objects.all()[:2]
+ label = cast(PartLabel, PartLabel.objects.first())
+
+ url = reverse('api-part-label-print', kwargs={'pk': label.pk})
+ url += f'/?plugin={plugin_ref}&part[]={parts[0].pk}&part[]={parts[1].pk}'
+
+ self.post(
+ url,
+ {
+ 'machine': str(self.machine.pk),
+ 'driver_options': {'copies': '1', 'test_option': '2'},
+ },
+ expected_code=200,
+ )
+
+ # test the print labels method call
+ self.print_labels.assert_called_once()
+ self.assertEqual(self.print_labels.call_args.args[0], self.machine.machine)
+ self.assertEqual(self.print_labels.call_args.args[1], label)
+ self.assertQuerySetEqual(
+ self.print_labels.call_args.args[2], parts, transform=lambda x: x
+ )
+ self.assertIn('printing_options', self.print_labels.call_args.kwargs)
+ self.assertEqual(
+ self.print_labels.call_args.kwargs['printing_options'],
+ {'copies': 1, 'test_option': 2},
+ )
+
+ # test the single print label method calls
+ self.assertEqual(self.print_label.call_count, 2)
+ self.assertEqual(self.print_label.call_args.args[0], self.machine.machine)
+ self.assertEqual(self.print_label.call_args.args[1], label)
+ self.assertEqual(self.print_label.call_args.args[2], parts[1])
+ self.assertIn('printing_options', self.print_labels.call_args.kwargs)
+ self.assertEqual(
+ self.print_labels.call_args.kwargs['printing_options'],
+ {'copies': 1, 'test_option': 2},
+ )
+
+ # test with non existing machine
+ self.post(
+ url,
+ {
+ 'machine': self.placeholder_uuid,
+ 'driver_options': {'copies': '1', 'test_option': '2'},
+ },
+ expected_code=400,
+ )
diff --git a/InvenTree/plugin/builtin/labels/inventree_machine.py b/InvenTree/plugin/builtin/labels/inventree_machine.py
index c2794aac978..86d6b361100 100644
--- a/InvenTree/plugin/builtin/labels/inventree_machine.py
+++ b/InvenTree/plugin/builtin/labels/inventree_machine.py
@@ -21,10 +21,11 @@ def get_machine_and_driver(machine_pk: str):
"""Get the driver by machine pk and ensure that it is a label printing driver."""
machine = registry.get_machine(machine_pk)
- if machine is None:
+ # machine should be valid due to the machine select field validator
+ if machine is None: # pragma: no cover
return None, None
- if machine.SLUG != 'label_printer':
+ if machine.SLUG != 'label-printer': # pragma: no cover
return None, None
machine = cast(LabelPrinterMachine, machine)
@@ -68,7 +69,8 @@ def print_labels(self, label: LabelTemplate, items, request, **kwargs):
kwargs['printing_options'].get('machine', '')
)
- if driver is None or machine is None:
+ # the driver and machine should be valid due to the machine select field validator
+ if driver is None or machine is None: # pragma: no cover
return None
print_kwargs = {
@@ -116,7 +118,7 @@ def __init__(self, *args, **kwargs):
# get all available printers for each driver
machines: list[LabelPrinterMachine] = []
for driver in cast(
- list[LabelPrinterBaseDriver], registry.get_drivers('label_printer')
+ list[LabelPrinterBaseDriver], registry.get_drivers('label-printer')
):
machines.extend(
driver.get_printers(
diff --git a/docs/docs/extend/machines/overview.md b/docs/docs/extend/machines/overview.md
index 7ceb8e146cf..e3c822425c6 100644
--- a/docs/docs/extend/machines/overview.md
+++ b/docs/docs/extend/machines/overview.md
@@ -36,7 +36,7 @@ Each machine type can provide a different type of connection functionality betwe
If you want to create your own machine type, please also take a look at the already existing machine types in `machines/machine_types/*.py`. The following example creates a machine type called `abc`.
```py
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from plugin.machine import BaseDriver, BaseMachineType, MachineStatus
class ABCBaseDriver(BaseDriver):
From 8f7b2478198d8955661104057ed1d0688d61594b Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Wed, 7 Feb 2024 14:39:09 +0000
Subject: [PATCH 83/86] Fix tests
---
InvenTree/plugin/builtin/labels/inventree_machine.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/InvenTree/plugin/builtin/labels/inventree_machine.py b/InvenTree/plugin/builtin/labels/inventree_machine.py
index 86d6b361100..13c91a35a9f 100644
--- a/InvenTree/plugin/builtin/labels/inventree_machine.py
+++ b/InvenTree/plugin/builtin/labels/inventree_machine.py
@@ -22,17 +22,17 @@ def get_machine_and_driver(machine_pk: str):
machine = registry.get_machine(machine_pk)
# machine should be valid due to the machine select field validator
- if machine is None: # pragma: no cover
- return None, None
+ if machine is None:
+ return None, None # pragma: no cover
- if machine.SLUG != 'label-printer': # pragma: no cover
- return None, None
+ if machine.SLUG != 'label-printer':
+ return None, None # pragma: no cover
machine = cast(LabelPrinterMachine, machine)
driver = machine.driver
if driver is None:
- return machine, None
+ return machine, None # pragma: no cover
return machine, cast(LabelPrinterBaseDriver, driver)
From 6571feae1fa982e445622934d722d518118221b3 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Wed, 7 Feb 2024 21:49:54 +0000
Subject: [PATCH 84/86] Added setting choice with kwargs and get_machines with
initialized=None
---
InvenTree/common/models.py | 6 +++++-
InvenTree/machine/machine_type.py | 2 +-
InvenTree/machine/registry.py | 7 +++++--
InvenTree/machine/tests.py | 1 +
4 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py
index a8cb9e5beda..9a13fda7e23 100644
--- a/InvenTree/common/models.py
+++ b/InvenTree/common/models.py
@@ -525,7 +525,11 @@ def get_setting_choices(cls, key, **kwargs):
if callable(choices):
# Evaluate the function (we expect it will return a list of tuples...)
- return choices()
+ try:
+ # Attempt to pass the kwargs to the function, if it doesn't expect them, ignore and call without
+ return choices(**kwargs)
+ except TypeError:
+ return choices()
return choices
diff --git a/InvenTree/machine/machine_type.py b/InvenTree/machine/machine_type.py
index f928d65735e..d92d0540d4e 100644
--- a/InvenTree/machine/machine_type.py
+++ b/InvenTree/machine/machine_type.py
@@ -117,7 +117,7 @@ def get_machines(self, **kwargs):
Keyword Arguments:
name (str): Machine name
machine_type (BaseMachineType): Machine type definition (class)
- initialized (bool): default: True
+ initialized (bool | None): use None to get all machines (default: True)
active (bool): machine needs to be active
base_driver (BaseDriver): base driver (class)
"""
diff --git a/InvenTree/machine/registry.py b/InvenTree/machine/registry.py
index 3caa5e3178c..fbd712fef28 100644
--- a/InvenTree/machine/registry.py
+++ b/InvenTree/machine/registry.py
@@ -165,7 +165,7 @@ def get_machines(self, **kwargs):
name: Machine name
machine_type: Machine type definition (class)
driver: Machine driver (class)
- initialized: (bool, default: True)
+ initialized (bool | None): use None to get all machines (default: True)
active: (bool)
base_driver: base driver (class)
"""
@@ -178,7 +178,10 @@ def get_machines(self, **kwargs):
'base_driver',
]
- kwargs = {'initialized': True, **kwargs}
+ if 'initialized' not in kwargs:
+ kwargs['initialized'] = True
+ if kwargs['initialized'] is None:
+ del kwargs['initialized']
def filter_machine(machine: BaseMachineType):
for key, value in kwargs.items():
diff --git a/InvenTree/machine/tests.py b/InvenTree/machine/tests.py
index 5bbc04fb5ac..03cbb746526 100755
--- a/InvenTree/machine/tests.py
+++ b/InvenTree/machine/tests.py
@@ -132,6 +132,7 @@ def test_machine_lifecycle(self):
# test get_machines
self.assertEqual(len(registry.get_machines()), 2)
+ self.assertEqual(len(registry.get_machines(initialized=None)), 3)
self.assertEqual(len(registry.get_machines(active=False, initialized=False)), 1)
self.assertEqual(len(registry.get_machines(name='Test Machine 1')), 1)
self.assertEqual(
From ec6b9728fc581f8b1982c50b62f5b25e2188eeb1 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Wed, 7 Feb 2024 21:56:30 +0000
Subject: [PATCH 85/86] Refetch table after deleting machine
---
src/frontend/src/tables/machine/MachineListTable.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/frontend/src/tables/machine/MachineListTable.tsx b/src/frontend/src/tables/machine/MachineListTable.tsx
index 813d7842553..e248c62d90d 100644
--- a/src/frontend/src/tables/machine/MachineListTable.tsx
+++ b/src/frontend/src/tables/machine/MachineListTable.tsx
@@ -249,7 +249,10 @@ function MachineDrawer({
preFormContent: (
{t`Are you sure you want to remove the machine "${machine?.name}"?`}
),
- onFormSuccess: () => navigate(-1)
+ onFormSuccess: () => {
+ refreshTable();
+ navigate(-1);
+ }
});
}
}),
From e6f88a0890a8bdabf9d2108acd2ee8bfd57e5bd1 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Wed, 7 Feb 2024 22:22:12 +0000
Subject: [PATCH 86/86] Fix test
---
InvenTree/plugin/builtin/labels/inventree_machine.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/InvenTree/plugin/builtin/labels/inventree_machine.py b/InvenTree/plugin/builtin/labels/inventree_machine.py
index 13c91a35a9f..1d5d8c6651b 100644
--- a/InvenTree/plugin/builtin/labels/inventree_machine.py
+++ b/InvenTree/plugin/builtin/labels/inventree_machine.py
@@ -22,17 +22,17 @@ def get_machine_and_driver(machine_pk: str):
machine = registry.get_machine(machine_pk)
# machine should be valid due to the machine select field validator
- if machine is None:
- return None, None # pragma: no cover
+ if machine is None: # pragma: no cover
+ return None, None
- if machine.SLUG != 'label-printer':
- return None, None # pragma: no cover
+ if machine.SLUG != 'label-printer': # pragma: no cover
+ return None, None
machine = cast(LabelPrinterMachine, machine)
driver = machine.driver
- if driver is None:
- return machine, None # pragma: no cover
+ if driver is None: # pragma: no cover
+ return machine, None
return machine, cast(LabelPrinterBaseDriver, driver)