Coverage for src/backend/InvenTree/machine/models.py: 57%
95 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Models for the machine app."""
3import uuid
4from typing import Literal, Optional
6from django.contrib import admin
7from django.db import models
8from django.utils.html import escape, format_html_join
9from django.utils.safestring import mark_safe
10from django.utils.translation import gettext_lazy as _
12import common.models
13from machine import registry
14from machine.machine_type import BaseMachineType
17class MachineConfig(models.Model):
18 """A Machine objects represents a physical machine."""
20 id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
22 name = models.CharField(
23 unique=True,
24 max_length=255,
25 verbose_name=_('Name'),
26 help_text=_('Name of machine'),
27 )
29 machine_type = models.CharField(
30 max_length=255, verbose_name=_('Machine Type'), help_text=_('Type of machine')
31 )
33 driver = models.CharField(
34 max_length=255,
35 verbose_name=_('Driver'),
36 help_text=_('Driver used for the machine'),
37 )
39 active = models.BooleanField(
40 default=True, verbose_name=_('Active'), help_text=_('Machines can be disabled')
41 )
43 def __str__(self) -> str:
44 """String representation of a machine."""
45 return f'{self.name}'
47 def save(self, *args, **kwargs) -> None:
48 """Custom save function to capture creates/updates to notify the registry."""
49 created = self._state.adding
51 old_machine = None
52 if ( 52 ↛ 57line 52 didn't jump to line 57 because the condition on line 52 was never true
53 not created
54 and self.pk
55 and (old_machine := MachineConfig.objects.get(pk=self.pk))
56 ):
57 old_machine = old_machine.to_dict()
59 super().save(*args, **kwargs)
61 if created: 61 ↛ 64line 61 didn't jump to line 64 because the condition on line 61 was always true
62 # machine was created, add it to the machine registry
63 registry.add_machine(self, initialize=True)
64 elif old_machine:
65 # machine was updated, invoke update hook
66 # elif acts just as a type gate, old_machine should be defined always
67 # if machine is not created now which is already handled above
68 registry.update_machine(old_machine, self)
70 def delete(self, *args, **kwargs):
71 """Remove machine from registry first."""
72 if self.machine:
73 registry.remove_machine(self.machine)
75 return super().delete(*args, **kwargs)
77 def to_dict(self):
78 """Serialize a machine config to a dict including setting."""
79 machine = {f.name: f.value_to_string(self) for f in self._meta.fields}
80 machine['settings'] = {
81 (setting.config_type, setting.key): setting.value
82 for setting in MachineSetting.objects.filter(machine_config=self)
83 }
84 return machine
86 @property
87 def machine(self) -> Optional[BaseMachineType]:
88 """Machine instance getter."""
89 return registry.get_machine(self.pk)
91 @property
92 def errors(self):
93 """Machine errors getter."""
94 return getattr(self.machine, 'errors', [])
96 @admin.display(boolean=True, description=_('Driver available'))
97 def is_driver_available(self) -> bool:
98 """Status if driver for machine is available."""
99 return self.machine is not None and self.machine.driver is not None
101 @admin.display(boolean=True, description=_('No errors'))
102 def no_errors(self) -> bool:
103 """Status if machine has errors."""
104 return len(self.errors) == 0
106 @admin.display(boolean=True, description=_('Initialized'))
107 def initialized(self) -> bool:
108 """Status if machine is initialized."""
109 return getattr(self.machine, 'initialized', False)
111 @admin.display(description=_('Errors'))
112 def get_admin_errors(self):
113 """Get machine errors for django admin interface."""
114 return format_html_join(
115 mark_safe('<br>'), '{}', ((str(error),) for error in self.errors)
116 ) or mark_safe(f'<i>{_("No errors")}</i>')
118 @admin.display(description=_('Machine status'))
119 def get_machine_status(self):
120 """Get machine status for django admin interface."""
121 if self.machine is None:
122 return None
124 out = mark_safe(self.machine.status.render(self.machine.status))
126 if self.machine.status_text:
127 out += escape(f' ({self.machine.status_text})')
129 return out
132class MachineSetting(common.models.BaseInvenTreeSetting):
133 """This models represents settings for individual machines."""
135 typ = 'machine_config'
136 extra_unique_fields = ['machine_config', 'config_type']
138 class Meta:
139 """Meta for MachineSetting."""
141 unique_together = [('machine_config', 'config_type', 'key')]
143 class ConfigType(models.TextChoices):
144 """Machine setting config type enum."""
146 MACHINE = 'M', _('Machine')
147 DRIVER = 'D', _('Driver')
149 def to_native_value(self):
150 """Return the 'native' value of this setting."""
151 return self.__class__.get_setting(
152 self.key, machine_config=self.machine_config, config_type=self.config_type
153 )
155 machine_config = models.ForeignKey(
156 MachineConfig,
157 related_name='settings',
158 verbose_name=_('Machine Config'),
159 on_delete=models.CASCADE,
160 )
162 config_type = models.CharField(
163 verbose_name=_('Config type'), max_length=1, choices=ConfigType.choices
164 )
166 def save(self, *args, **kwargs) -> None:
167 """Custom save method to notify the registry on changes."""
168 old_machine = self.machine_config.to_dict()
170 super().save(*args, **kwargs)
172 registry.update_machine(old_machine, self.machine_config)
174 @classmethod
175 def get_config_type(cls, config_type_str: Literal['M', 'D']):
176 """Helper method to get the correct enum value for easier usage with literal strings."""
177 if config_type_str == 'M':
178 return cls.ConfigType.MACHINE
179 elif config_type_str == 'D': 179 ↛ exitline 179 didn't return from function 'get_config_type' because the condition on line 179 was always true
180 return cls.ConfigType.DRIVER
182 @classmethod
183 def get_setting_definition(cls, key, **kwargs):
184 """In the BaseInvenTreeSetting class, we have a class attribute named 'SETTINGS'.
186 which is a dict object that fully defines all the setting parameters.
188 Here, unlike the BaseInvenTreeSetting, we do not know the definitions of all settings
189 'ahead of time' (as they are defined externally in the machine driver).
191 Settings can be provided by the caller, as kwargs['settings'].
193 If not provided, we'll look at the machine registry to see what settings this machine driver requires
194 """
195 if 'settings' not in kwargs:
196 machine_config: Optional[MachineConfig] = kwargs.pop('machine_config', None)
197 if machine_config and machine_config.machine:
198 config_type = kwargs.get('config_type')
199 if config_type == cls.ConfigType.DRIVER:
200 kwargs['settings'] = machine_config.machine.driver_settings
201 elif config_type == cls.ConfigType.MACHINE:
202 kwargs['settings'] = machine_config.machine.machine_settings
204 return super().get_setting_definition(key, **kwargs)