"""Models for the machine app.""" import uuid from typing import Literal from django.contrib import admin from django.db import models from django.utils.html import escape, format_html_join from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ import common.models from machine import registry class MachineConfig(models.Model): """A Machine objects represents a physical machine.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField( unique=True, max_length=255, verbose_name=_('Name'), help_text=_('Name of machine'), ) machine_type = models.CharField( 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'), ) active = models.BooleanField( default=True, verbose_name=_('Active'), help_text=_('Machines can be disabled') ) def __str__(self) -> str: """String representation of a machine.""" 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 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) if created: # machine was created, add it to the machine registry registry.add_machine(self, initialize=True) elif old_machine: # machine was updated, invoke update hook # elif acts just as a type gate, old_machine should be defined always # if machine is not created now which is already handled above registry.update_machine(old_machine, self) def delete(self, *args, **kwargs): """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 for setting in MachineSetting.objects.filter(machine_config=self) } return machine @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.""" 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.""" return len(self.errors) == 0 @admin.display(boolean=True, description=_('Initialized')) def initialized(self) -> bool: """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 out = mark_safe(self.machine.status.render(self.machine.status)) if self.machine.status_text: out += escape(f' ({self.machine.status_text})') return out class MachineSetting(common.models.BaseInvenTreeSetting): """This models represents settings for individual machines.""" typ = 'machine_config' extra_unique_fields = ['machine_config', 'config_type'] class Meta: """Meta for MachineSetting.""" unique_together = [('machine_config', 'config_type', 'key')] class ConfigType(models.TextChoices): """Machine setting config type enum.""" MACHINE = 'M', _('Machine') DRIVER = 'D', _('Driver') machine_config = models.ForeignKey( MachineConfig, 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 ) 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) registry.update_machine(old_machine, self.machine_config) @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': return cls.ConfigType.DRIVER @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. 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). Settings can be provided by the caller, as kwargs['settings']. If not provided, we'll look at the machine registry to see what settings this machine driver requires """ 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) if config_type == cls.ConfigType.DRIVER: kwargs['settings'] = machine_config.machine.driver_settings elif config_type == cls.ConfigType.MACHINE: kwargs['settings'] = machine_config.machine.machine_settings return super().get_setting_definition(key, **kwargs)