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

1"""Models for the machine app.""" 

2 

3import uuid 

4from typing import Literal, Optional 

5 

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 _ 

11 

12import common.models 

13from machine import registry 

14from machine.machine_type import BaseMachineType 

15 

16 

17class MachineConfig(models.Model): 

18 """A Machine objects represents a physical machine.""" 

19 

20 id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 

21 

22 name = models.CharField( 

23 unique=True, 

24 max_length=255, 

25 verbose_name=_('Name'), 

26 help_text=_('Name of machine'), 

27 ) 

28 

29 machine_type = models.CharField( 

30 max_length=255, verbose_name=_('Machine Type'), help_text=_('Type of machine') 

31 ) 

32 

33 driver = models.CharField( 

34 max_length=255, 

35 verbose_name=_('Driver'), 

36 help_text=_('Driver used for the machine'), 

37 ) 

38 

39 active = models.BooleanField( 

40 default=True, verbose_name=_('Active'), help_text=_('Machines can be disabled') 

41 ) 

42 

43 def __str__(self) -> str: 

44 """String representation of a machine.""" 

45 return f'{self.name}' 

46 

47 def save(self, *args, **kwargs) -> None: 

48 """Custom save function to capture creates/updates to notify the registry.""" 

49 created = self._state.adding 

50 

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() 

58 

59 super().save(*args, **kwargs) 

60 

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) 

69 

70 def delete(self, *args, **kwargs): 

71 """Remove machine from registry first.""" 

72 if self.machine: 

73 registry.remove_machine(self.machine) 

74 

75 return super().delete(*args, **kwargs) 

76 

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 

85 

86 @property 

87 def machine(self) -> Optional[BaseMachineType]: 

88 """Machine instance getter.""" 

89 return registry.get_machine(self.pk) 

90 

91 @property 

92 def errors(self): 

93 """Machine errors getter.""" 

94 return getattr(self.machine, 'errors', []) 

95 

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 

100 

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 

105 

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) 

110 

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>') 

117 

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 

123 

124 out = mark_safe(self.machine.status.render(self.machine.status)) 

125 

126 if self.machine.status_text: 

127 out += escape(f' ({self.machine.status_text})') 

128 

129 return out 

130 

131 

132class MachineSetting(common.models.BaseInvenTreeSetting): 

133 """This models represents settings for individual machines.""" 

134 

135 typ = 'machine_config' 

136 extra_unique_fields = ['machine_config', 'config_type'] 

137 

138 class Meta: 

139 """Meta for MachineSetting.""" 

140 

141 unique_together = [('machine_config', 'config_type', 'key')] 

142 

143 class ConfigType(models.TextChoices): 

144 """Machine setting config type enum.""" 

145 

146 MACHINE = 'M', _('Machine') 

147 DRIVER = 'D', _('Driver') 

148 

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 ) 

154 

155 machine_config = models.ForeignKey( 

156 MachineConfig, 

157 related_name='settings', 

158 verbose_name=_('Machine Config'), 

159 on_delete=models.CASCADE, 

160 ) 

161 

162 config_type = models.CharField( 

163 verbose_name=_('Config type'), max_length=1, choices=ConfigType.choices 

164 ) 

165 

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() 

169 

170 super().save(*args, **kwargs) 

171 

172 registry.update_machine(old_machine, self.machine_config) 

173 

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 

181 

182 @classmethod 

183 def get_setting_definition(cls, key, **kwargs): 

184 """In the BaseInvenTreeSetting class, we have a class attribute named 'SETTINGS'. 

185 

186 which is a dict object that fully defines all the setting parameters. 

187 

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). 

190 

191 Settings can be provided by the caller, as kwargs['settings']. 

192 

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 

203 

204 return super().get_setting_definition(key, **kwargs)