mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 04:55:44 +00:00
Fix machine request pickeling and settings (#6772)
* Fix machine request pickeling * fix precommit * fix: shared state between workers and main thread for machine registry * remove last usage of legacy PUI form framework to fix machine edit/delete modal * reset cache before initialization * update documentation * fix: invalidating cache * implement machine registry hash to check if a reload is required * trigger: ci * fix: request bug * fix: test * trigger: ci * add clear errors and improve restart hook * auto initialize not initialized machines when changing active state * fix: tests
This commit is contained in:
@ -2,8 +2,10 @@
|
||||
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
from plugin import registry as plg_registry
|
||||
|
||||
@ -104,3 +106,37 @@ class ClassProviderMixin:
|
||||
except ValueError:
|
||||
# Path(...).relative_to throws an ValueError if its not relative to the InvenTree source base dir
|
||||
return False
|
||||
|
||||
|
||||
def get_shared_class_instance_state_mixin(get_state_key: Callable[[type], str]):
|
||||
"""Get a mixin class that provides shared state for classes across the main application and worker.
|
||||
|
||||
Arguments:
|
||||
get_state_key: A function that returns the key for the shared state when given a class instance.
|
||||
"""
|
||||
|
||||
class SharedClassStateMixinClass:
|
||||
"""Mixin to provide shared state for classes across the main application and worker."""
|
||||
|
||||
def set_shared_state(self, key: str, value: Any):
|
||||
"""Set a shared state value for this machine.
|
||||
|
||||
Arguments:
|
||||
key: The key for the shared state
|
||||
value: The value to set
|
||||
"""
|
||||
cache.set(self._get_key(key), value, timeout=None)
|
||||
|
||||
def get_shared_state(self, key: str, default=None):
|
||||
"""Get a shared state value for this machine.
|
||||
|
||||
Arguments:
|
||||
key: The key for the shared state
|
||||
"""
|
||||
return cache.get(self._get_key(key)) or default
|
||||
|
||||
def _get_key(self, key: str):
|
||||
"""Get the key for this class instance."""
|
||||
return f'{get_state_key(self)}:{key}'
|
||||
|
||||
return SharedClassStateMixinClass
|
||||
|
@ -26,7 +26,6 @@ class MachineConfig(AppConfig):
|
||||
if (
|
||||
not canAppAccessDatabase(allow_test=True)
|
||||
or not isPluginRegistryLoaded()
|
||||
or not isInMainThread()
|
||||
or isRunningMigrations()
|
||||
or isImportingData()
|
||||
):
|
||||
@ -37,7 +36,7 @@ class MachineConfig(AppConfig):
|
||||
|
||||
try:
|
||||
logger.info('Loading InvenTree machines')
|
||||
registry.initialize()
|
||||
registry.initialize(main=isInMainThread())
|
||||
except (OperationalError, ProgrammingError):
|
||||
# Database might not yet be ready
|
||||
logger.warn('Database was not ready for initializing machines')
|
||||
|
@ -3,7 +3,11 @@
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union
|
||||
|
||||
from generic.states import StatusCode
|
||||
from InvenTree.helpers_mixin import ClassProviderMixin, ClassValidationMixin
|
||||
from InvenTree.helpers_mixin import (
|
||||
ClassProviderMixin,
|
||||
ClassValidationMixin,
|
||||
get_shared_class_instance_state_mixin,
|
||||
)
|
||||
|
||||
# Import only for typechecking, otherwise this throws cyclic import errors
|
||||
if TYPE_CHECKING:
|
||||
@ -44,7 +48,11 @@ class MachineStatus(StatusCode):
|
||||
"""
|
||||
|
||||
|
||||
class BaseDriver(ClassValidationMixin, ClassProviderMixin):
|
||||
class BaseDriver(
|
||||
ClassValidationMixin,
|
||||
ClassProviderMixin,
|
||||
get_shared_class_instance_state_mixin(lambda x: f'machine:driver:{x.SLUG}'),
|
||||
):
|
||||
"""Base class for all machine drivers.
|
||||
|
||||
Attributes:
|
||||
@ -69,8 +77,6 @@ class BaseDriver(ClassValidationMixin, ClassProviderMixin):
|
||||
"""Base driver __init__ method."""
|
||||
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.
|
||||
|
||||
@ -133,10 +139,20 @@ class BaseDriver(ClassValidationMixin, ClassProviderMixin):
|
||||
Arguments:
|
||||
error: Exception or string
|
||||
"""
|
||||
self.errors.append(error)
|
||||
self.set_shared_state('errors', self.errors + [error])
|
||||
|
||||
# --- state getters/setters
|
||||
@property
|
||||
def errors(self) -> list[Union[str, Exception]]:
|
||||
"""List of driver errors."""
|
||||
return self.get_shared_state('errors', [])
|
||||
|
||||
|
||||
class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
|
||||
class BaseMachineType(
|
||||
ClassValidationMixin,
|
||||
ClassProviderMixin,
|
||||
get_shared_class_instance_state_mixin(lambda x: f'machine:machine:{x.pk}'),
|
||||
):
|
||||
"""Base class for machine types.
|
||||
|
||||
Attributes:
|
||||
@ -178,12 +194,6 @@ class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
|
||||
from machine import registry
|
||||
from machine.models import MachineSetting
|
||||
|
||||
self.errors: list[Union[str, Exception]] = []
|
||||
self.initialized = False
|
||||
|
||||
self.status = self.default_machine_status
|
||||
self.status_text: str = ''
|
||||
|
||||
self.pk = machine_config.pk
|
||||
self.driver = registry.get_driver_instance(machine_config.driver)
|
||||
|
||||
@ -208,8 +218,6 @@ class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
|
||||
(self.driver_settings, MachineSetting.ConfigType.DRIVER),
|
||||
]
|
||||
|
||||
self.restart_required = False
|
||||
|
||||
def __str__(self):
|
||||
"""String representation of a machine."""
|
||||
return f'{self.name}'
|
||||
@ -272,16 +280,32 @@ class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
|
||||
|
||||
try:
|
||||
self.driver.update_machine(old_state, self)
|
||||
|
||||
# check if the active state has changed and initialize the machine if necessary
|
||||
if old_state['active'] != self.active:
|
||||
if self.initialized is False and self.active is True:
|
||||
self.initialize()
|
||||
elif self.initialized is True and self.active is False:
|
||||
self.initialized = False
|
||||
except Exception as e:
|
||||
self.handle_error(e)
|
||||
|
||||
def restart(self):
|
||||
"""Machine restart function, can be used to manually restart the machine from the admin ui."""
|
||||
"""Machine restart function, can be used to manually restart the machine from the admin ui.
|
||||
|
||||
This will first reset the machines state (errors, status, status_text) and then call the drivers restart function.
|
||||
"""
|
||||
if self.driver is None:
|
||||
return
|
||||
|
||||
try:
|
||||
# reset the machine state
|
||||
self.restart_required = False
|
||||
self.reset_errors()
|
||||
self.set_status(self.default_machine_status)
|
||||
self.set_status_text('')
|
||||
|
||||
# call the driver restart function
|
||||
self.driver.restart_machine(self)
|
||||
except Exception as e:
|
||||
self.handle_error(e)
|
||||
@ -293,7 +317,11 @@ class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
|
||||
Arguments:
|
||||
error: Exception or string
|
||||
"""
|
||||
self.errors.append(error)
|
||||
self.set_shared_state('errors', self.errors + [error])
|
||||
|
||||
def reset_errors(self):
|
||||
"""Helper function for resetting the error list for a machine."""
|
||||
self.set_shared_state('errors', [])
|
||||
|
||||
def get_setting(
|
||||
self, key: str, config_type_str: Literal['M', 'D'], cache: bool = False
|
||||
@ -364,7 +392,7 @@ class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
|
||||
Arguments:
|
||||
status: The new MachineStatus code to set
|
||||
"""
|
||||
self.status = status
|
||||
self.set_shared_state('status', status.value)
|
||||
|
||||
def set_status_text(self, status_text: str):
|
||||
"""Set the machine status text. It can be any arbitrary text.
|
||||
@ -372,4 +400,39 @@ class BaseMachineType(ClassValidationMixin, ClassProviderMixin):
|
||||
Arguments:
|
||||
status_text: The new status text to set
|
||||
"""
|
||||
self.status_text = status_text
|
||||
self.set_shared_state('status_text', status_text)
|
||||
|
||||
# --- state getters/setters
|
||||
@property
|
||||
def initialized(self) -> bool:
|
||||
"""Initialized state of the machine."""
|
||||
return self.get_shared_state('initialized', False)
|
||||
|
||||
@initialized.setter
|
||||
def initialized(self, value: bool):
|
||||
self.set_shared_state('initialized', value)
|
||||
|
||||
@property
|
||||
def restart_required(self) -> bool:
|
||||
"""Restart required state of the machine."""
|
||||
return self.get_shared_state('restart_required', False)
|
||||
|
||||
@restart_required.setter
|
||||
def restart_required(self, value: bool):
|
||||
self.set_shared_state('restart_required', value)
|
||||
|
||||
@property
|
||||
def errors(self) -> list[Union[str, Exception]]:
|
||||
"""List of machine errors."""
|
||||
return self.get_shared_state('errors', [])
|
||||
|
||||
@property
|
||||
def status(self) -> MachineStatus:
|
||||
"""Machine status code."""
|
||||
status_code = self.get_shared_state('status', self.default_machine_status.value)
|
||||
return self.MACHINE_STATUS(status_code)
|
||||
|
||||
@property
|
||||
def status_text(self) -> str:
|
||||
"""Machine status text."""
|
||||
return self.get_shared_state('status_text', '')
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
from typing import Union, cast
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.db import models
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from PIL.Image import Image
|
||||
@ -34,7 +35,6 @@ class LabelPrinterBaseDriver(BaseDriver):
|
||||
machine: 'LabelPrinterMachine',
|
||||
label: LabelTemplate,
|
||||
item: models.Model,
|
||||
request: Request,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Print a single label with the provided template and item.
|
||||
@ -43,7 +43,6 @@ class LabelPrinterBaseDriver(BaseDriver):
|
||||
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 (dict): The printing options set for this print job defined in the PrintingOptionsSerializer
|
||||
@ -57,8 +56,7 @@ class LabelPrinterBaseDriver(BaseDriver):
|
||||
self,
|
||||
machine: 'LabelPrinterMachine',
|
||||
label: LabelTemplate,
|
||||
items: QuerySet,
|
||||
request: Request,
|
||||
items: QuerySet[models.Model],
|
||||
**kwargs,
|
||||
) -> Union[None, JsonResponse]:
|
||||
"""Print one or more labels with the provided template and items.
|
||||
@ -67,7 +65,6 @@ class LabelPrinterBaseDriver(BaseDriver):
|
||||
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 (dict): The printing options set for this print job defined in the PrintingOptionsSerializer
|
||||
@ -81,7 +78,7 @@ class LabelPrinterBaseDriver(BaseDriver):
|
||||
but this can be overridden by the particular driver.
|
||||
"""
|
||||
for item in items:
|
||||
self.print_label(machine, label, item, request, **kwargs)
|
||||
self.print_label(machine, label, item, **kwargs)
|
||||
|
||||
def get_printers(
|
||||
self, label: LabelTemplate, items: QuerySet, **kwargs
|
||||
@ -123,56 +120,50 @@ class LabelPrinterBaseDriver(BaseDriver):
|
||||
return cast(LabelPrintingMixin, plg)
|
||||
|
||||
def render_to_pdf(
|
||||
self, label: LabelTemplate, item: models.Model, request: Request, **kwargs
|
||||
self, label: LabelTemplate, item: models.Model, **kwargs
|
||||
) -> HttpResponse:
|
||||
"""Helper method to render a label to PDF format for a specific item.
|
||||
|
||||
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
|
||||
"""
|
||||
response = self.machine_plugin.render_to_pdf(label, item, request, **kwargs)
|
||||
return response
|
||||
request = self._get_dummy_request()
|
||||
return self.machine_plugin.render_to_pdf(label, item, request, **kwargs)
|
||||
|
||||
def render_to_pdf_data(
|
||||
self, label: LabelTemplate, item: models.Model, request: Request, **kwargs
|
||||
self, label: LabelTemplate, item: models.Model, **kwargs
|
||||
) -> 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
|
||||
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)
|
||||
self.render_to_pdf(label, item, **kwargs)
|
||||
.get_document() # type: ignore
|
||||
.write_pdf()
|
||||
)
|
||||
|
||||
def render_to_html(
|
||||
self, label: LabelTemplate, item: models.Model, request: Request, **kwargs
|
||||
) -> str:
|
||||
def render_to_html(self, label: LabelTemplate, item: models.Model, **kwargs) -> str:
|
||||
"""Helper method to render a label to HTML format for a specific item.
|
||||
|
||||
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
|
||||
"""
|
||||
html = self.machine_plugin.render_to_html(label, item, request, **kwargs)
|
||||
return html
|
||||
request = self._get_dummy_request()
|
||||
return self.machine_plugin.render_to_html(label, item, request, **kwargs)
|
||||
|
||||
def render_to_png(
|
||||
self, label: LabelTemplate, item: models.Model, request: Request, **kwargs
|
||||
) -> Image:
|
||||
self, label: LabelTemplate, item: models.Model, **kwargs
|
||||
) -> Union[Image, None]:
|
||||
"""Helper method to render a label to PNG format for a specific item.
|
||||
|
||||
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 (bytes): The pdf document as bytes (optional)
|
||||
@ -182,8 +173,20 @@ class LabelPrinterBaseDriver(BaseDriver):
|
||||
pdf2image_kwargs (dict): Additional keyword arguments to pass to the
|
||||
[`pdf2image.convert_from_bytes`](https://pdf2image.readthedocs.io/en/latest/reference.html#pdf2image.pdf2image.convert_from_bytes) method (optional)
|
||||
"""
|
||||
png = self.machine_plugin.render_to_png(label, item, request, **kwargs)
|
||||
return png
|
||||
request = self._get_dummy_request()
|
||||
return self.machine_plugin.render_to_png(label, item, request, **kwargs)
|
||||
|
||||
def _get_dummy_request(self):
|
||||
"""Return a dummy request object to it work with legacy code.
|
||||
|
||||
Note: this is a private method and can be removed at anytime
|
||||
"""
|
||||
r = HttpRequest()
|
||||
r.META['SERVER_PORT'] = '80'
|
||||
r.META['SERVER_NAME'] = 'localhost'
|
||||
r.user = AnonymousUser()
|
||||
|
||||
return r
|
||||
|
||||
required_overrides = [[print_label, print_labels]]
|
||||
|
||||
@ -229,6 +232,7 @@ class LabelPrinterStatus(MachineStatus):
|
||||
UNKNOWN = 101, _('Unknown'), 'secondary'
|
||||
PRINTING = 110, _('Printing'), 'primary'
|
||||
NO_MEDIA = 301, _('No media'), 'warning'
|
||||
PAPER_JAM = 302, _('Paper jam'), 'warning'
|
||||
DISCONNECTED = 400, _('Disconnected'), 'danger'
|
||||
|
||||
|
||||
|
@ -1,15 +1,20 @@
|
||||
"""Machine registry."""
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
from django.core.cache import cache
|
||||
|
||||
from InvenTree.helpers_mixin import get_shared_class_instance_state_mixin
|
||||
from machine.machine_type import BaseDriver, BaseMachineType
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class MachineRegistry:
|
||||
class MachineRegistry(
|
||||
get_shared_class_instance_state_mixin(lambda _x: f'machine:registry')
|
||||
):
|
||||
"""Machine registry class."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
@ -23,17 +28,27 @@ class MachineRegistry:
|
||||
self.machines: dict[str, BaseMachineType] = {}
|
||||
|
||||
self.base_drivers: list[type[BaseDriver]] = []
|
||||
self.errors: list[Union[str, Exception]] = []
|
||||
|
||||
self._hash = None
|
||||
|
||||
@property
|
||||
def errors(self) -> list[Union[str, Exception]]:
|
||||
"""List of registry errors."""
|
||||
return cast(list[Union[str, Exception]], self.get_shared_state('errors', []))
|
||||
|
||||
def handle_error(self, error: Union[Exception, str]):
|
||||
"""Helper function for capturing errors with the machine registry."""
|
||||
self.errors.append(error)
|
||||
self.set_shared_state('errors', self.errors + [error])
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self, main: bool = False):
|
||||
"""Initialize the machine registry."""
|
||||
# clear cache for machines (only needed for global redis cache)
|
||||
if main and hasattr(cache, 'delete_pattern'): # pragma: no cover
|
||||
cache.delete_pattern(f'machine:*')
|
||||
|
||||
self.discover_machine_types()
|
||||
self.discover_drivers()
|
||||
self.load_machines()
|
||||
self.load_machines(main=main)
|
||||
|
||||
def discover_machine_types(self):
|
||||
"""Discovers all machine types by inferring all classes that inherit the BaseMachineType class."""
|
||||
@ -113,26 +128,39 @@ class MachineRegistry:
|
||||
|
||||
return self.driver_instances.get(slug, None)
|
||||
|
||||
def load_machines(self):
|
||||
def load_machines(self, main: bool = False):
|
||||
"""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
|
||||
|
||||
for machine_config in MachineConfig.objects.all():
|
||||
self.add_machine(machine_config, initialize=False)
|
||||
self.add_machine(
|
||||
machine_config, initialize=False, update_registry_hash=False
|
||||
)
|
||||
|
||||
# initialize drivers
|
||||
for driver in self.driver_instances.values():
|
||||
driver.init_driver()
|
||||
# initialize machines only in main thread
|
||||
if main:
|
||||
# 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:
|
||||
machine.initialize()
|
||||
# initialize machines after all machine instances were created
|
||||
for machine in self.machines.values():
|
||||
if machine.active:
|
||||
machine.initialize()
|
||||
|
||||
logger.info('Initialized %s machines', len(self.machines.keys()))
|
||||
self._update_registry_hash()
|
||||
logger.info('Initialized %s machines', len(self.machines.keys()))
|
||||
else:
|
||||
self._hash = None # reset hash to force reload hash
|
||||
logger.info('Loaded %s machines', len(self.machines.keys()))
|
||||
|
||||
def add_machine(self, machine_config, initialize=True):
|
||||
def reload_machines(self):
|
||||
"""Reload all machines from the database."""
|
||||
self.machines = {}
|
||||
self.load_machines()
|
||||
|
||||
def add_machine(self, machine_config, initialize=True, update_registry_hash=True):
|
||||
"""Add a machine to the machine registry."""
|
||||
machine_type = self.machine_types.get(machine_config.machine_type, None)
|
||||
if machine_type is None:
|
||||
@ -145,11 +173,19 @@ class MachineRegistry:
|
||||
if initialize and machine.active:
|
||||
machine.initialize()
|
||||
|
||||
def update_machine(self, old_machine_state, machine_config):
|
||||
if update_registry_hash:
|
||||
self._update_registry_hash()
|
||||
|
||||
def update_machine(
|
||||
self, old_machine_state, machine_config, update_registry_hash=True
|
||||
):
|
||||
"""Notify the machine about an update."""
|
||||
if machine := machine_config.machine:
|
||||
machine.update(old_machine_state)
|
||||
|
||||
if update_registry_hash:
|
||||
self._update_registry_hash()
|
||||
|
||||
def restart_machine(self, machine):
|
||||
"""Restart a machine."""
|
||||
machine.restart()
|
||||
@ -157,6 +193,7 @@ class MachineRegistry:
|
||||
def remove_machine(self, machine: BaseMachineType):
|
||||
"""Remove a machine from the registry."""
|
||||
self.machines.pop(str(machine.pk), None)
|
||||
self._update_registry_hash()
|
||||
|
||||
def get_machines(self, **kwargs):
|
||||
"""Get loaded machines from registry (By default only initialized machines).
|
||||
@ -169,6 +206,8 @@ class MachineRegistry:
|
||||
active: (bool)
|
||||
base_driver: base driver (class)
|
||||
"""
|
||||
self._check_reload()
|
||||
|
||||
allowed_fields = [
|
||||
'name',
|
||||
'machine_type',
|
||||
@ -212,6 +251,7 @@ class MachineRegistry:
|
||||
|
||||
def get_machine(self, pk: Union[str, UUID]):
|
||||
"""Get machine from registry by pk."""
|
||||
self._check_reload()
|
||||
return self.machines.get(str(pk), None)
|
||||
|
||||
def get_drivers(self, machine_type: str):
|
||||
@ -222,5 +262,37 @@ class MachineRegistry:
|
||||
if driver.machine_type == machine_type
|
||||
]
|
||||
|
||||
def _calculate_registry_hash(self):
|
||||
"""Calculate a hash of the machine registry state."""
|
||||
from hashlib import md5
|
||||
|
||||
data = md5()
|
||||
|
||||
for pk, machine in self.machines.items():
|
||||
data.update(str(pk).encode())
|
||||
try:
|
||||
data.update(str(machine.machine_config.active).encode())
|
||||
except:
|
||||
# machine does not exist anymore, hash will be different
|
||||
pass
|
||||
|
||||
return str(data.hexdigest())
|
||||
|
||||
def _check_reload(self):
|
||||
"""Check if the registry needs to be reloaded, and reload it."""
|
||||
if not self._hash:
|
||||
self._hash = self._calculate_registry_hash()
|
||||
|
||||
last_hash = self.get_shared_state('hash', None)
|
||||
|
||||
if last_hash and last_hash != self._hash:
|
||||
logger.info('Machine registry has changed - reloading machines')
|
||||
self.reload_machines()
|
||||
|
||||
def _update_registry_hash(self):
|
||||
"""Save the current registry hash."""
|
||||
self._hash = self._calculate_registry_hash()
|
||||
self.set_shared_state('hash', self._hash)
|
||||
|
||||
|
||||
registry: MachineRegistry = MachineRegistry()
|
||||
|
@ -32,7 +32,7 @@ class TestMachineRegistryMixin(TestCase):
|
||||
registry.driver_instances = {}
|
||||
registry.machines = {}
|
||||
registry.base_drivers = []
|
||||
registry.errors = []
|
||||
registry.set_shared_state('errors', [])
|
||||
|
||||
return super().tearDown()
|
||||
|
||||
@ -111,7 +111,7 @@ class TestDriverMachineInterface(TestMachineRegistryMixin, TestCase):
|
||||
self.machines = [self.machine1, self.machine2, self.machine3]
|
||||
|
||||
# init registry
|
||||
registry.initialize()
|
||||
registry.initialize(main=True)
|
||||
|
||||
# mock machine implementation
|
||||
self.machine_mocks = {
|
||||
@ -230,7 +230,7 @@ class TestLabelPrinterMachineType(TestMachineRegistryMixin, InvenTreeAPITestCase
|
||||
active=True,
|
||||
)
|
||||
|
||||
registry.initialize()
|
||||
registry.initialize(main=True)
|
||||
driver_instance = cast(
|
||||
TestingLabelPrinterDriver,
|
||||
registry.get_driver_instance('testing-label-printer'),
|
||||
|
@ -93,11 +93,9 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, InvenTreePlugin):
|
||||
|
||||
# execute the print job
|
||||
if driver.USE_BACKGROUND_WORKER is False:
|
||||
return driver.print_labels(machine, label, items, request, **print_kwargs)
|
||||
return driver.print_labels(machine, label, items, **print_kwargs)
|
||||
|
||||
offload_task(
|
||||
driver.print_labels, machine, label, items, request, **print_kwargs
|
||||
)
|
||||
offload_task(driver.print_labels, machine, label, items, **print_kwargs)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
|
@ -1,20 +1,12 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Divider, Stack } from '@mantine/core';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
import { api } from '../App';
|
||||
import { ApiForm, ApiFormProps } from '../components/forms/ApiForm';
|
||||
import {
|
||||
ApiFormFieldSet,
|
||||
ApiFormFieldType
|
||||
} from '../components/forms/fields/ApiFormField';
|
||||
import { StylishText } from '../components/items/StylishText';
|
||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||
import { PathParams, apiUrl } from '../states/ApiState';
|
||||
import { invalidResponse, permissionDenied } from './notifications';
|
||||
import { generateUniqueId } from './uid';
|
||||
|
||||
/**
|
||||
* Construct an API url from the provided ApiFormProps object
|
||||
@ -180,143 +172,3 @@ export function constructField({
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
export interface OpenApiFormProps extends ApiFormProps {
|
||||
title: string;
|
||||
cancelText?: string;
|
||||
cancelColor?: string;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/*
|
||||
* Construct and open a modal form
|
||||
* @param title :
|
||||
*/
|
||||
export function openModalApiForm(props: OpenApiFormProps) {
|
||||
// method property *must* be supplied
|
||||
if (!props.method) {
|
||||
notifications.show({
|
||||
title: t`Invalid Form`,
|
||||
message: t`method parameter not supplied`,
|
||||
color: 'red'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate a random modal ID for controller
|
||||
let modalId: string =
|
||||
`modal-${props.title}-${props.url}-${props.method}` + generateUniqueId();
|
||||
|
||||
props.actions = [
|
||||
...(props.actions || []),
|
||||
{
|
||||
text: props.cancelText ?? t`Cancel`,
|
||||
color: props.cancelColor ?? 'blue',
|
||||
onClick: () => {
|
||||
modals.close(modalId);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const oldFormSuccess = props.onFormSuccess;
|
||||
props.onFormSuccess = (data) => {
|
||||
oldFormSuccess?.(data);
|
||||
modals.close(modalId);
|
||||
};
|
||||
|
||||
let url = constructFormUrl(props.url, props.pk, props.pathParams);
|
||||
|
||||
// Make OPTIONS request first
|
||||
api
|
||||
.options(url)
|
||||
.then((response) => {
|
||||
// Extract available fields from the OPTIONS response (and handle any errors)
|
||||
|
||||
let fields: Record<string, ApiFormFieldType> | null = {};
|
||||
|
||||
if (!props.ignorePermissionCheck) {
|
||||
fields = extractAvailableFields(response, props.method);
|
||||
|
||||
if (fields == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const _props = { ...props };
|
||||
|
||||
if (_props.fields) {
|
||||
for (const [k, v] of Object.entries(_props.fields)) {
|
||||
_props.fields[k] = constructField({
|
||||
field: v,
|
||||
definition: fields?.[k]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
modals.open({
|
||||
title: <StylishText size="xl">{props.title}</StylishText>,
|
||||
modalId: modalId,
|
||||
size: 'xl',
|
||||
onClose: () => {
|
||||
props.onClose ? props.onClose() : null;
|
||||
},
|
||||
children: (
|
||||
<Stack gap={'xs'}>
|
||||
<Divider />
|
||||
<ApiForm id={modalId} props={props} optionsLoading={false} />
|
||||
</Stack>
|
||||
)
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
invalidResponse(error.response.status);
|
||||
} else {
|
||||
notifications.show({
|
||||
title: t`Form Error`,
|
||||
message: error.message,
|
||||
color: 'red'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a modal form to create a new model instance
|
||||
*/
|
||||
export function openCreateApiForm(props: OpenApiFormProps) {
|
||||
let createProps: OpenApiFormProps = {
|
||||
...props,
|
||||
method: 'POST'
|
||||
};
|
||||
|
||||
openModalApiForm(createProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a modal form to edit a model instance
|
||||
*/
|
||||
export function openEditApiForm(props: OpenApiFormProps) {
|
||||
let editProps: OpenApiFormProps = {
|
||||
...props,
|
||||
fetchInitialData: props.fetchInitialData ?? true,
|
||||
method: 'PATCH'
|
||||
};
|
||||
|
||||
openModalApiForm(editProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a modal form to delete a model instancel
|
||||
*/
|
||||
export function openDeleteApiForm(props: OpenApiFormProps) {
|
||||
let deleteProps: OpenApiFormProps = {
|
||||
...props,
|
||||
method: 'DELETE',
|
||||
submitText: t`Delete`,
|
||||
submitColor: 'red',
|
||||
fields: {}
|
||||
};
|
||||
|
||||
openModalApiForm(deleteProps);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import '@mantine/carousel/styles.css';
|
||||
import '@mantine/charts/styles.css';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import '@mantine/spotlight/styles.css';
|
||||
import * as Sentry from '@sentry/react';
|
||||
|
@ -41,8 +41,11 @@ import {
|
||||
} from '../../components/render/StatusRenderer';
|
||||
import { MachineSettingList } from '../../components/settings/SettingList';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { openDeleteApiForm, openEditApiForm } from '../../functions/forms';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { TableColumn } from '../Column';
|
||||
@ -205,8 +208,39 @@ function MachineDrawer({
|
||||
[refreshAll]
|
||||
);
|
||||
|
||||
const machineEditModal = useEditApiFormModal({
|
||||
title: t`Edit machine`,
|
||||
url: ApiEndpoints.machine_list,
|
||||
pk: machinePk,
|
||||
fields: useMemo(
|
||||
() => ({
|
||||
name: {},
|
||||
active: {}
|
||||
}),
|
||||
[]
|
||||
),
|
||||
onClose: () => refreshAll()
|
||||
});
|
||||
|
||||
const machineDeleteModal = useDeleteApiFormModal({
|
||||
title: t`Delete machine`,
|
||||
successMessage: t`Machine successfully deleted.`,
|
||||
url: ApiEndpoints.machine_list,
|
||||
pk: machinePk,
|
||||
preFormContent: (
|
||||
<Text>{t`Are you sure you want to remove the machine "${machine?.name}"?`}</Text>
|
||||
),
|
||||
onFormSuccess: () => {
|
||||
refreshTable();
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
{machineEditModal.modal}
|
||||
{machineDeleteModal.modal}
|
||||
|
||||
<Group justify="space-between">
|
||||
<Box></Box>
|
||||
|
||||
@ -227,36 +261,11 @@ function MachineDrawer({
|
||||
actions={[
|
||||
EditItemAction({
|
||||
tooltip: t`Edit machine`,
|
||||
onClick: () => {
|
||||
openEditApiForm({
|
||||
title: t`Edit machine`,
|
||||
url: ApiEndpoints.machine_list,
|
||||
pk: machinePk,
|
||||
fields: {
|
||||
name: {},
|
||||
active: {}
|
||||
},
|
||||
onClose: () => refreshAll()
|
||||
});
|
||||
}
|
||||
onClick: machineEditModal.open
|
||||
}),
|
||||
DeleteItemAction({
|
||||
tooltip: t`Delete machine`,
|
||||
onClick: () => {
|
||||
openDeleteApiForm({
|
||||
title: t`Delete machine`,
|
||||
successMessage: t`Machine successfully deleted.`,
|
||||
url: ApiEndpoints.machine_list,
|
||||
pk: machinePk,
|
||||
preFormContent: (
|
||||
<Text>{t`Are you sure you want to remove the machine "${machine?.name}"?`}</Text>
|
||||
),
|
||||
onFormSuccess: () => {
|
||||
refreshTable();
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
onClick: machineDeleteModal.open
|
||||
}),
|
||||
{
|
||||
icon: <IconRefresh />,
|
||||
|
Reference in New Issue
Block a user