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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""JSON serializers for plugin app."""
3from django.core.exceptions import ValidationError
4from django.utils.translation import gettext_lazy as _
6from drf_spectacular.types import OpenApiTypes
7from drf_spectacular.utils import extend_schema_field
8from rest_framework import serializers
10from common.serializers import GenericReferencedSettingSerializer
11from plugin.models import PluginConfig, PluginSetting, PluginUserSetting
14class MetadataSerializer(serializers.ModelSerializer):
15 """Serializer class for model metadata API access."""
17 metadata = serializers.JSONField(required=True)
19 class Meta:
20 """Metaclass options."""
22 fields = ['metadata']
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)
29 def update(self, instance, data):
30 """Perform update on the metadata field.
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
42 return super().update(instance, data)
45class PluginConfigSerializer(serializers.ModelSerializer):
46 """Serializer for a PluginConfig."""
48 class Meta:
49 """Meta for serializer."""
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 ]
67 read_only_fields = ['key', 'is_builtin', 'is_sample', 'is_installed']
69 meta = serializers.DictField(read_only=True)
70 mixins = serializers.DictField(read_only=True)
71 is_mandatory = serializers.BooleanField(read_only=True)
74class PluginAdminDetailSerializer(serializers.ModelSerializer):
75 """Serializer for a PluginConfig with admin details."""
77 class Meta:
78 """Metaclass options for serializer."""
80 model = PluginConfig
82 fields = ['source', 'context']
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 )
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 )
99class PluginConfigInstallSerializer(serializers.Serializer):
100 """Serializer for installing a new plugin."""
102 class Meta:
103 """Meta for serializer."""
105 fields = ['url', 'packagename', 'confirm']
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 )
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 )
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 )
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 )
141 def validate(self, data):
142 """Validate inputs.
144 Make sure both confirm and url are provided.
145 """
146 super().validate(data)
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})
155 return data
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
161 data = self.validated_data
163 packagename = data.get('packagename', '')
164 url = data.get('url', '')
165 version = data.get('version', None)
166 user = self.context['request'].user
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'))
171 return install_plugin(
172 url=url, packagename=packagename, version=version, user=user
173 )
176class PluginConfigEmptySerializer(serializers.Serializer):
177 """Serializer for a PluginConfig."""
180class PluginReloadSerializer(serializers.Serializer):
181 """Serializer for remotely forcing plugin registry reload."""
183 class Meta:
184 """Meta for serializer."""
186 fields = ['full_reload', 'force_reload', 'collect_plugins']
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 )
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 )
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 )
211 def save(self):
212 """Reload the plugin registry."""
213 from plugin.registry import registry
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 )
223class PluginActivateSerializer(serializers.Serializer):
224 """Serializer for activating or deactivating a plugin."""
226 model = PluginConfig
228 class Meta:
229 """Metaclass for serializer."""
231 fields = ['active']
233 active = serializers.BooleanField(
234 required=False,
235 default=True,
236 label=_('Activate Plugin'),
237 help_text=_('Activate this plugin'),
238 )
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)
244 if not active and instance.is_mandatory():
245 raise ValidationError(
246 'INVE-E10: ' + _('Mandatory plugin cannot be deactivated')
247 )
249 instance.activate(active)
250 return instance
253class PluginUninstallSerializer(serializers.Serializer):
254 """Serializer for uninstalling a plugin."""
256 class Meta:
257 """Metaclass for serializer."""
259 fields = ['delete_config']
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 )
268 def update(self, instance, validated_data):
269 """Uninstall the specified plugin."""
270 from plugin.installer import uninstall_plugin
272 user = self.context['request'].user
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'))
277 return uninstall_plugin(
278 instance, user=user, delete_config=validated_data.get('delete_config', True)
279 )
282class PluginSettingSerializer(GenericReferencedSettingSerializer):
283 """Serializer for the PluginSetting model."""
285 MODEL = PluginSetting
286 EXTRA_FIELDS = ['plugin', 'read_only']
288 plugin = serializers.CharField(source='plugin.key', read_only=True)
290 read_only = serializers.SerializerMethodField(
291 read_only=True,
292 help_text=_('Indicates if the setting is overridden by configuration'),
293 label=_('Override'),
294 )
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
300 overrides = plugin_setting_overrides(obj.plugin.key)
301 return obj.key in overrides
304class PluginUserSettingSerializer(GenericReferencedSettingSerializer):
305 """Serializer for the PluginUserSetting model."""
307 MODEL = PluginUserSetting
308 EXTRA_FIELDS = ['plugin', 'user']
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 )
316class PluginRegistryErrorSerializer(serializers.Serializer):
317 """Serializer for a plugin registry error."""
319 class Meta:
320 """Meta for serializer."""
322 fields = ['stage', 'name', 'message']
324 stage = serializers.CharField()
325 name = serializers.CharField()
326 message = serializers.CharField()
329class PluginRegistryStatusSerializer(serializers.Serializer):
330 """Serializer for plugin registry status."""
332 class Meta:
333 """Meta for serializer."""
335 fields = ['active_plugins', 'registry_errors']
337 active_plugins = serializers.IntegerField(read_only=True)
338 registry_errors = serializers.ListField(child=PluginRegistryErrorSerializer())
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."""
345 def __init__(self, **kwargs):
346 """Custom init routine for the serializer."""
347 kwargs['pk_field'] = 'key'
348 kwargs['queryset'] = PluginConfig.objects.all()
350 super().__init__(**kwargs)
352 def use_pk_only_optimization(self):
353 """Disable the PK optimization."""
354 return False
356 def to_internal_value(self, data):
357 """Lookup the PluginConfig object based on the slug."""
358 return PluginConfig.objects.filter(key=data).first()
360 def to_representation(self, value):
361 """Return the 'key' of the PluginConfig object."""
362 return value.key