Coverage for src/backend/InvenTree/plugin/serializers.py: 93%

127 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 17:47 +0000

1"""JSON serializers for plugin app.""" 

2 

3from django.core.exceptions import ValidationError 

4from django.utils.translation import gettext_lazy as _ 

5 

6from drf_spectacular.types import OpenApiTypes 

7from drf_spectacular.utils import extend_schema_field 

8from rest_framework import serializers 

9 

10from common.serializers import GenericReferencedSettingSerializer 

11from plugin.models import PluginConfig, PluginSetting, PluginUserSetting 

12 

13 

14class MetadataSerializer(serializers.ModelSerializer): 

15 """Serializer class for model metadata API access.""" 

16 

17 metadata = serializers.JSONField(required=True) 

18 

19 class Meta: 

20 """Metaclass options.""" 

21 

22 fields = ['metadata'] 

23 

24 def __init__(self, model_type, *args, **kwargs): 

25 """Initialize the metadata serializer with information on the model type.""" 

26 self.Meta.model = model_type 

27 super().__init__(*args, **kwargs) 

28 

29 def update(self, instance, data): 

30 """Perform update on the metadata field. 

31 

32 Rules: 

33 - If this is a partial (PATCH) update, try to 'merge' data in 

34 - Else, if it is a PUT update, overwrite any existing metadata 

35 """ 

36 if self.partial: 36 ↛ 42line 36 didn't jump to line 42 because the condition on line 36 was always true

37 # Default behavior is to "merge" new data in 

38 metadata = instance.metadata.copy() if instance.metadata else {} 

39 metadata.update(data['metadata']) 

40 data['metadata'] = metadata 

41 

42 return super().update(instance, data) 

43 

44 

45class PluginConfigSerializer(serializers.ModelSerializer): 

46 """Serializer for a PluginConfig.""" 

47 

48 class Meta: 

49 """Meta for serializer.""" 

50 

51 model = PluginConfig 

52 fields = [ 

53 'pk', 

54 'key', 

55 'name', 

56 'package_name', 

57 'active', 

58 'meta', 

59 'mixins', 

60 'is_builtin', 

61 'is_sample', 

62 'is_installed', 

63 'is_package', 

64 'is_mandatory', 

65 ] 

66 

67 read_only_fields = ['key', 'is_builtin', 'is_sample', 'is_installed'] 

68 

69 meta = serializers.DictField(read_only=True) 

70 mixins = serializers.DictField(read_only=True) 

71 is_mandatory = serializers.BooleanField(read_only=True) 

72 

73 

74class PluginAdminDetailSerializer(serializers.ModelSerializer): 

75 """Serializer for a PluginConfig with admin details.""" 

76 

77 class Meta: 

78 """Metaclass options for serializer.""" 

79 

80 model = PluginConfig 

81 

82 fields = ['source', 'context'] 

83 

84 source = serializers.CharField( 

85 allow_null=True, 

86 label=_('Source File'), 

87 help_text=_('Path to the source file for admin integration'), 

88 source='admin_source', 

89 ) 

90 

91 context = serializers.JSONField( 

92 allow_null=True, 

93 label=_('Context'), 

94 help_text=_('Optional context data for the admin integration'), 

95 source='admin_context', 

96 ) 

97 

98 

99class PluginConfigInstallSerializer(serializers.Serializer): 

100 """Serializer for installing a new plugin.""" 

101 

102 class Meta: 

103 """Meta for serializer.""" 

104 

105 fields = ['url', 'packagename', 'confirm'] 

106 

107 url = serializers.CharField( 

108 required=False, 

109 allow_blank=True, 

110 label=_('Source URL'), 

111 help_text=_( 

112 'Source for the package - this can be a custom registry or a VCS path' 

113 ), 

114 ) 

115 

116 packagename = serializers.CharField( 

117 required=False, 

118 allow_blank=True, 

119 label=_('Package Name'), 

120 help_text=_( 

121 'Name for the Plugin Package - can also contain a version indicator' 

122 ), 

123 ) 

124 

125 version = serializers.CharField( 

126 required=False, 

127 allow_blank=True, 

128 label=_('Version'), 

129 help_text=_( 

130 'Version specifier for the plugin. Leave blank for latest version.' 

131 ), 

132 ) 

133 

134 confirm = serializers.BooleanField( 

135 label=_('Confirm plugin installation'), 

136 help_text=_( 

137 'This will install this plugin now into the current instance. The instance will go into maintenance.' 

138 ), 

139 ) 

140 

141 def validate(self, data): 

142 """Validate inputs. 

143 

144 Make sure both confirm and url are provided. 

145 """ 

146 super().validate(data) 

147 

148 # check the base requirements are met 

149 if not data.get('confirm'): 

150 raise ValidationError({'confirm': _('Installation not confirmed')}) 

151 if (not data.get('url')) and (not data.get('packagename')): 

152 msg = _('Either packagename or URL must be provided') 

153 raise ValidationError({'url': msg, 'packagename': msg}) 

154 

155 return data 

156 

157 def save(self): 

158 """Install a plugin from a package registry and set operational results as instance data.""" 

159 from plugin.installer import install_plugin 

160 

161 data = self.validated_data 

162 

163 packagename = data.get('packagename', '') 

164 url = data.get('url', '') 

165 version = data.get('version', None) 

166 user = self.context['request'].user 

167 

168 if not user or not user.is_superuser: 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true

169 raise ValidationError(_('Only superuser accounts can administer plugins')) 

170 

171 return install_plugin( 

172 url=url, packagename=packagename, version=version, user=user 

173 ) 

174 

175 

176class PluginConfigEmptySerializer(serializers.Serializer): 

177 """Serializer for a PluginConfig.""" 

178 

179 

180class PluginReloadSerializer(serializers.Serializer): 

181 """Serializer for remotely forcing plugin registry reload.""" 

182 

183 class Meta: 

184 """Meta for serializer.""" 

185 

186 fields = ['full_reload', 'force_reload', 'collect_plugins'] 

187 

188 full_reload = serializers.BooleanField( 

189 required=False, 

190 default=False, 

191 label=_('Full reload'), 

192 help_text=_('Perform a full reload of the plugin registry'), 

193 ) 

194 

195 force_reload = serializers.BooleanField( 

196 required=False, 

197 default=False, 

198 label=_('Force reload'), 

199 help_text=_( 

200 'Force a reload of the plugin registry, even if it is already loaded' 

201 ), 

202 ) 

203 

204 collect_plugins = serializers.BooleanField( 

205 required=False, 

206 default=False, 

207 label=_('Collect plugins'), 

208 help_text=_('Collect plugins and add them to the registry'), 

209 ) 

210 

211 def save(self): 

212 """Reload the plugin registry.""" 

213 from plugin.registry import registry 

214 

215 registry.reload_plugins( 

216 full_reload=self.validated_data.get('full_reload', False), 

217 force_reload=self.validated_data.get('force_reload', False), 

218 collect=self.validated_data.get('collect_plugins', False), 

219 clear_errors=True, 

220 ) 

221 

222 

223class PluginActivateSerializer(serializers.Serializer): 

224 """Serializer for activating or deactivating a plugin.""" 

225 

226 model = PluginConfig 

227 

228 class Meta: 

229 """Metaclass for serializer.""" 

230 

231 fields = ['active'] 

232 

233 active = serializers.BooleanField( 

234 required=False, 

235 default=True, 

236 label=_('Activate Plugin'), 

237 help_text=_('Activate this plugin'), 

238 ) 

239 

240 def update(self, instance: PluginConfig, validated_data): 

241 """Apply the new 'active' value to the plugin instance.""" 

242 active = validated_data.get('active', True) 

243 

244 if not active and instance.is_mandatory(): 

245 raise ValidationError( 

246 'INVE-E10: ' + _('Mandatory plugin cannot be deactivated') 

247 ) 

248 

249 instance.activate(active) 

250 return instance 

251 

252 

253class PluginUninstallSerializer(serializers.Serializer): 

254 """Serializer for uninstalling a plugin.""" 

255 

256 class Meta: 

257 """Metaclass for serializer.""" 

258 

259 fields = ['delete_config'] 

260 

261 delete_config = serializers.BooleanField( 

262 required=False, 

263 default=True, 

264 label=_('Delete configuration'), 

265 help_text=_('Delete the plugin configuration from the database'), 

266 ) 

267 

268 def update(self, instance, validated_data): 

269 """Uninstall the specified plugin.""" 

270 from plugin.installer import uninstall_plugin 

271 

272 user = self.context['request'].user 

273 

274 if not user or not user.is_superuser: 274 ↛ 275line 274 didn't jump to line 275 because the condition on line 274 was never true

275 raise ValidationError(_('Only superuser accounts can administer plugins')) 

276 

277 return uninstall_plugin( 

278 instance, user=user, delete_config=validated_data.get('delete_config', True) 

279 ) 

280 

281 

282class PluginSettingSerializer(GenericReferencedSettingSerializer): 

283 """Serializer for the PluginSetting model.""" 

284 

285 MODEL = PluginSetting 

286 EXTRA_FIELDS = ['plugin', 'read_only'] 

287 

288 plugin = serializers.CharField(source='plugin.key', read_only=True) 

289 

290 read_only = serializers.SerializerMethodField( 

291 read_only=True, 

292 help_text=_('Indicates if the setting is overridden by configuration'), 

293 label=_('Override'), 

294 ) 

295 

296 def get_read_only(self, obj) -> bool: 

297 """Return True if this plugin setting is locked by configuration.""" 

298 from common.settings import plugin_setting_overrides 

299 

300 overrides = plugin_setting_overrides(obj.plugin.key) 

301 return obj.key in overrides 

302 

303 

304class PluginUserSettingSerializer(GenericReferencedSettingSerializer): 

305 """Serializer for the PluginUserSetting model.""" 

306 

307 MODEL = PluginUserSetting 

308 EXTRA_FIELDS = ['plugin', 'user'] 

309 

310 plugin = serializers.CharField(source='plugin.key', read_only=True) 

311 user = serializers.PrimaryKeyRelatedField( 

312 read_only=True, help_text=_('The user for which this setting applies') 

313 ) 

314 

315 

316class PluginRegistryErrorSerializer(serializers.Serializer): 

317 """Serializer for a plugin registry error.""" 

318 

319 class Meta: 

320 """Meta for serializer.""" 

321 

322 fields = ['stage', 'name', 'message'] 

323 

324 stage = serializers.CharField() 

325 name = serializers.CharField() 

326 message = serializers.CharField() 

327 

328 

329class PluginRegistryStatusSerializer(serializers.Serializer): 

330 """Serializer for plugin registry status.""" 

331 

332 class Meta: 

333 """Meta for serializer.""" 

334 

335 fields = ['active_plugins', 'registry_errors'] 

336 

337 active_plugins = serializers.IntegerField(read_only=True) 

338 registry_errors = serializers.ListField(child=PluginRegistryErrorSerializer()) 

339 

340 

341@extend_schema_field(OpenApiTypes.STR) 

342class PluginRelationSerializer(serializers.PrimaryKeyRelatedField): 

343 """Serializer for a plugin field. Uses the 'slug' of the plugin as the lookup.""" 

344 

345 def __init__(self, **kwargs): 

346 """Custom init routine for the serializer.""" 

347 kwargs['pk_field'] = 'key' 

348 kwargs['queryset'] = PluginConfig.objects.all() 

349 

350 super().__init__(**kwargs) 

351 

352 def use_pk_only_optimization(self): 

353 """Disable the PK optimization.""" 

354 return False 

355 

356 def to_internal_value(self, data): 

357 """Lookup the PluginConfig object based on the slug.""" 

358 return PluginConfig.objects.filter(key=data).first() 

359 

360 def to_representation(self, value): 

361 """Return the 'key' of the PluginConfig object.""" 

362 return value.key