Coverage for src/backend/InvenTree/common/serializers.py: 84%
400 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"""API serializers for common components."""
3from django.contrib.contenttypes.models import ContentType
4from django.db.models import Count, OuterRef, Subquery
5from django.urls import reverse
6from django.utils.translation import gettext_lazy as _
8import django_q.models
9from drf_spectacular.types import OpenApiTypes
10from drf_spectacular.utils import extend_schema_field
11from error_report.models import Error
12from flags.state import flag_state
13from rest_framework import serializers
14from rest_framework.exceptions import PermissionDenied
15from taggit.models import Tag
17import common.filters
18import common.models as common_models
19import common.validators
20import generic.states.custom
21from importer.registry import register_importer
22from InvenTree.helpers import get_objectreference
23from InvenTree.helpers_model import construct_absolute_url
24from InvenTree.mixins import DataImportExportSerializerMixin
25from InvenTree.models import InvenTreeParameterMixin
26from InvenTree.serializers import (
27 ContentTypeField,
28 FilterableSerializerMixin,
29 InvenTreeAttachmentSerializerField,
30 InvenTreeImageSerializerField,
31 InvenTreeModelSerializer,
32 InvenTreeTaggitSerializer,
33 OptionalField,
34)
35from plugin import registry as plugin_registry
36from users.serializers import OwnerSerializer, UserSerializer
39@extend_schema_field(OpenApiTypes.STR)
40class SettingsValueField(serializers.Field):
41 """Custom serializer field for a settings value."""
43 def get_attribute(self, instance):
44 """Return the object instance, not the attribute value."""
45 return instance
47 def to_representation(self, instance: common_models.InvenTreeSetting):
48 """Return the value of the setting.
50 Protected settings are returned as '***'
51 """
52 if instance.protected: 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 return '***'
54 else:
55 value = instance.value
57 if value is None: 57 ↛ 58line 57 didn't jump to line 58 because the condition on line 57 was never true
58 value = ''
60 # Attempt to coerce the value to a native type
61 if instance.is_int():
62 value = instance.as_int()
64 elif instance.is_float(): 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true
65 value = instance.as_float()
67 elif instance.is_bool():
68 value = instance.as_bool()
70 return value
72 def to_internal_value(self, data) -> str:
73 """Return the internal value of the setting."""
74 if data is None:
75 return ''
76 else:
77 return str(data)
80class SettingsSerializer(InvenTreeModelSerializer):
81 """Base serializer for a settings object."""
83 key = serializers.CharField(read_only=True)
85 name = serializers.CharField(read_only=True)
87 description = serializers.CharField(read_only=True)
89 type = serializers.CharField(source='setting_type', read_only=True)
91 choices = serializers.SerializerMethodField()
93 def get_choices(self, obj) -> list:
94 """Returns the choices available for a given item."""
95 results = []
97 choices = obj.choices()
99 if choices:
100 for choice in choices:
101 results.append({'value': choice[0], 'display_name': choice[1]})
103 return results
105 model_name = serializers.CharField(read_only=True, allow_null=True)
107 model_filters = serializers.DictField(read_only=True)
109 api_url = serializers.CharField(read_only=True, allow_null=True)
111 value = SettingsValueField(allow_null=True)
113 def validate_value(self, value):
114 """Validate the value of the setting."""
115 if value is None:
116 return ''
117 return str(value)
119 units = serializers.CharField(read_only=True)
121 required = serializers.BooleanField(read_only=True)
123 typ = serializers.CharField(read_only=True)
125 confirm = serializers.BooleanField(
126 read_only=True,
127 help_text=_('Indicates if changing this setting requires confirmation'),
128 )
130 confirm_text = serializers.CharField(read_only=True)
132 def is_valid(self, *, raise_exception=False):
133 """Validate the setting, including confirmation if required."""
134 ret = super().is_valid(raise_exception=raise_exception)
135 # Check if confirmation was provided if required
136 if self.instance.confirm():
137 req_data = self.context['request'].data
138 if not 'manual_confirm' in req_data or not req_data['manual_confirm']:
139 raise serializers.ValidationError({
140 'manual_confirm': _(
141 'This setting requires confirmation before changing. Please confirm the change.'
142 )
143 })
144 return ret
147class GlobalSettingsSerializer(SettingsSerializer):
148 """Serializer for the InvenTreeSetting model."""
150 class Meta:
151 """Meta options for GlobalSettingsSerializer."""
153 model = common_models.InvenTreeSetting
154 fields = [
155 'pk',
156 'key',
157 'value',
158 'name',
159 'description',
160 'type',
161 'units',
162 'choices',
163 'model_name',
164 'api_url',
165 'typ',
166 'read_only',
167 'confirm',
168 'confirm_text',
169 ]
171 read_only = serializers.SerializerMethodField(
172 read_only=True,
173 help_text=_(
174 'Indicates if the setting is overridden by an environment variable'
175 ),
176 label=_('Override'),
177 )
179 def get_read_only(self, obj) -> bool:
180 """Return True if the setting 'read_only' (cannot be edited).
182 A setting may be "read-only" if:
184 - It is overridden by an environment variable.
185 """
186 from common.settings import global_setting_overrides
188 overrides = global_setting_overrides()
189 return obj.key in overrides
192class UserSettingsSerializer(SettingsSerializer):
193 """Serializer for the InvenTreeUserSetting model."""
195 class Meta:
196 """Meta options for UserSettingsSerializer."""
198 model = common_models.InvenTreeUserSetting
199 fields = [
200 'pk',
201 'key',
202 'value',
203 'name',
204 'description',
205 'user',
206 'type',
207 'units',
208 'choices',
209 'model_name',
210 'api_url',
211 'typ',
212 'confirm',
213 'confirm_text',
214 ]
216 user = serializers.PrimaryKeyRelatedField(read_only=True)
219class CurrencyExchangeSerializer(serializers.Serializer):
220 """Serializer for a Currency Exchange request.
222 It's only purpose is describing the results correctly in the API schema right now.
223 """
225 base_currency = serializers.CharField(read_only=True)
226 exchange_rates = serializers.DictField(child=serializers.FloatField())
227 updated = serializers.DateTimeField(read_only=True)
230class GenericReferencedSettingSerializer(SettingsSerializer):
231 """Serializer for a GenericReferencedSetting model.
233 Args:
234 MODEL: model class for the serializer
235 EXTRA_FIELDS: fields that need to be appended to the serializer
236 field must also be defined in the custom class
237 """
239 MODEL = None
240 EXTRA_FIELDS = None
242 def __init__(self, *args, **kwargs):
243 """Init overrides the Meta class to make it dynamic."""
245 class CustomMeta:
246 """Scaffold for custom Meta class."""
248 fields = [
249 'pk',
250 'key',
251 'value',
252 'name',
253 'description',
254 'type',
255 'choices',
256 'model_name',
257 'model_filters',
258 'api_url',
259 'typ',
260 'units',
261 'required',
262 'confirm',
263 'confirm_text',
264 ]
266 # set Meta class
267 self.Meta = CustomMeta
268 self.Meta.model = self.MODEL
269 # extend the fields
270 self.Meta.fields.extend(self.EXTRA_FIELDS)
272 # resume operations
273 super().__init__(*args, **kwargs)
275 def validate_value(self, value):
276 """Validate the value of the setting."""
277 if value is None:
278 return ''
279 return str(value)
282class NotificationMessageSerializer(InvenTreeModelSerializer):
283 """Serializer for the InvenTreeUserSetting model."""
285 class Meta:
286 """Meta options for NotificationMessageSerializer."""
288 model = common_models.NotificationMessage
289 fields = [
290 'pk',
291 'target',
292 'source',
293 'user',
294 'category',
295 'name',
296 'message',
297 'creation',
298 'age',
299 'age_human',
300 'read',
301 ]
303 read_only_fields = [
304 'category',
305 'name',
306 'message',
307 'creation',
308 'age',
309 'age_human',
310 ]
312 target = serializers.SerializerMethodField(read_only=True)
313 source = serializers.SerializerMethodField(read_only=True)
314 user = serializers.PrimaryKeyRelatedField(read_only=True)
315 read = serializers.BooleanField()
317 def get_target(self, obj) -> dict:
318 """Function to resolve generic object reference to target."""
319 target = get_objectreference(obj, 'target_content_type', 'target_object_id')
321 if target and 'link' not in target: 321 ↛ 342line 321 didn't jump to line 342 because the condition on line 321 was always true
322 # Check if object has an absolute_url function
323 if hasattr(obj.target_object, 'get_absolute_url'): 323 ↛ 324line 323 didn't jump to line 324 because the condition on line 323 was never true
324 target['link'] = obj.target_object.get_absolute_url()
325 else:
326 # check if user is staff - link to admin
327 request = self.context.get('request')
328 if request and request.user and request.user.is_staff: 328 ↛ 342line 328 didn't jump to line 342 because the condition on line 328 was always true
329 meta = obj.target_object._meta
331 try:
332 target['link'] = construct_absolute_url(
333 reverse(
334 f'admin:{meta.db_table}_change',
335 kwargs={'object_id': obj.target_object_id},
336 )
337 )
338 except Exception:
339 # Do not crash if the reverse lookup fails
340 pass
342 return target
344 def get_source(self, obj) -> dict:
345 """Function to resolve generic object reference to source."""
346 return get_objectreference(obj, 'source_content_type', 'source_object_id')
349class NewsFeedEntrySerializer(InvenTreeModelSerializer):
350 """Serializer for the NewsFeedEntry model."""
352 class Meta:
353 """Meta options for NewsFeedEntrySerializer."""
355 model = common_models.NewsFeedEntry
356 fields = [
357 'pk',
358 'feed_id',
359 'title',
360 'link',
361 'published',
362 'author',
363 'summary',
364 'read',
365 ]
367 read = serializers.BooleanField()
370class ConfigSerializer(serializers.Serializer):
371 """Serializer for the InvenTree configuration.
373 This is a read-only serializer.
374 """
376 key = serializers.CharField(read_only=True)
377 env_var = serializers.CharField(read_only=True, allow_null=True)
378 config_key = serializers.CharField(read_only=True, allow_null=True)
379 source = serializers.CharField(read_only=True)
380 accessed = serializers.DateTimeField(read_only=True)
382 def to_representation(self, instance):
383 """Return the configuration data as a dictionary."""
384 if not isinstance(instance, str):
385 instance = list(instance.keys())[0]
387 data = {'key': instance}
389 for k, v in self.instance.get(instance, {}).items():
390 if k == 'default_value':
391 # Skip sensitive default values
392 continue
393 data[k] = v
395 return data
398class NotesImageSerializer(InvenTreeModelSerializer):
399 """Serializer for the NotesImage model."""
401 class Meta:
402 """Meta options for NotesImageSerializer."""
404 model = common_models.NotesImage
405 fields = ['pk', 'image', 'user', 'date', 'model_type', 'model_id']
407 read_only_fields = ['date', 'user']
409 image = InvenTreeImageSerializerField(required=True)
412@register_importer()
413class ProjectCodeSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
414 """Serializer for the ProjectCode model."""
416 class Meta:
417 """Meta options for ProjectCodeSerializer."""
419 model = common_models.ProjectCode
420 fields = ['pk', 'code', 'description', 'responsible', 'responsible_detail']
422 responsible_detail = OwnerSerializer(
423 source='responsible', read_only=True, allow_null=True
424 )
427@register_importer()
428class TagSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
429 """Serializer for the Tag model."""
431 class Meta:
432 """Meta options for TagSerializer."""
434 model = Tag
435 fields = ['pk', 'name', 'slug']
436 read_only_fields = ['pk', 'slug']
438 def validate(self, data):
439 """Slugify the received name to generate the slug."""
440 from django.utils.text import slugify
442 name = data.get('name', None)
444 if name is not None:
445 data['slug'] = slugify(name)
447 return data
450@register_importer()
451class CustomStateSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
452 """Serializer for the custom state model."""
454 class Meta:
455 """Meta options for CustomStateSerializer."""
457 model = common_models.InvenTreeCustomUserStateModel
458 fields = [
459 'pk',
460 'key',
461 'name',
462 'label',
463 'color',
464 'logical_key',
465 'model',
466 'model_name',
467 'reference_status',
468 ]
470 model_name = serializers.CharField(read_only=True, source='model.name')
472 reference_status = serializers.ChoiceField(
473 choices=generic.states.custom.state_reference_mappings()
474 )
477class FlagSerializer(serializers.Serializer):
478 """Serializer for feature flags."""
480 key = serializers.CharField(read_only=True)
481 state = serializers.CharField(read_only=True)
482 conditions = serializers.ListField(
483 child=serializers.DictField(), read_only=True, allow_null=True
484 )
486 def to_representation(self, instance):
487 """Return the configuration data as a dictionary."""
488 request = self.context.get('request')
489 if not isinstance(instance, str):
490 instance = list(instance.keys())[0]
491 data = {'key': instance, 'state': flag_state(instance, request=request)}
493 if request and request.user.is_superuser:
494 data['conditions'] = self.instance.get(instance)
496 return data
499class ContentTypeSerializer(serializers.Serializer):
500 """Serializer for ContentType models."""
502 pk = serializers.IntegerField(read_only=True)
503 app_label = serializers.CharField(read_only=True)
504 model = serializers.CharField(read_only=True)
505 app_labeled_name = serializers.CharField(read_only=True)
506 is_plugin = serializers.SerializerMethodField('get_is_plugin', read_only=True)
508 class Meta:
509 """Meta options for ContentTypeSerializer."""
511 model = ContentType
512 fields = ['pk', 'app_label', 'model', 'app_labeled_name', 'is_plugin']
514 def get_is_plugin(self, obj) -> bool:
515 """Return True if the model is a plugin model."""
516 return obj.app_label in plugin_registry.installed_apps
519@register_importer()
520class CustomUnitSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
521 """DRF serializer for CustomUnit model."""
523 class Meta:
524 """Meta options for CustomUnitSerializer."""
526 model = common_models.CustomUnit
527 fields = ['pk', 'name', 'symbol', 'definition']
530class AllUnitListResponseSerializer(serializers.Serializer):
531 """Serializer for the AllUnitList."""
533 class Unit(serializers.Serializer):
534 """Serializer for the AllUnitListResponseSerializer."""
536 name = serializers.CharField()
537 is_alias = serializers.BooleanField()
538 compatible_units = serializers.ListField(child=serializers.CharField())
539 isdimensionless = serializers.BooleanField()
541 default_system = serializers.CharField()
542 available_systems = serializers.ListField(child=serializers.CharField())
543 available_units = serializers.DictField(child=Unit())
546class ErrorMessageSerializer(InvenTreeModelSerializer):
547 """DRF serializer for server error messages."""
549 class Meta:
550 """Metaclass options for ErrorMessageSerializer."""
552 model = Error
554 fields = ['when', 'info', 'data', 'path', 'pk']
556 read_only_fields = ['when', 'info', 'data', 'path', 'pk']
559class TaskDetailSerializer(serializers.Serializer):
560 """Serializer for a background task detail."""
562 task_id = serializers.CharField(read_only=True)
563 exists = serializers.BooleanField(read_only=True)
564 pending = serializers.BooleanField(read_only=True)
565 complete = serializers.BooleanField(read_only=True)
566 success = serializers.BooleanField(read_only=True)
567 http_status = serializers.IntegerField(read_only=True)
569 @classmethod
570 def from_task(cls, task_id: str | bool | None) -> 'TaskDetailSerializer':
571 """Create a TaskDetailSerializer instance from a django_q Task.
573 Arguments:
574 task_id: The ID of the task to retrieve details for.
576 Returns:
577 An instance of TaskDetailSerializer with the task details.
579 Notes:
580 - If the provided task_id is None, the task has not been run, or has errored out
581 - If the provided task_id is a boolean, the task has been run synchronously, and the boolean value indicates success or failure
582 - If the provided task_id is a string, the task has been offloaded to the background worker, and the details can be from the database
584 """
585 from InvenTree.tasks import get_queued_task
587 if task_id is None or type(task_id) is bool:
588 # If the task_id is a boolean, the task has been run synchronously
589 return cls({
590 'task_id': '',
591 'exists': False,
592 'pending': False,
593 'complete': task_id is not None,
594 'success': False if task_id is None else bool(task_id),
595 'http_status': 404 if task_id is None else 200,
596 })
598 # A non-boolean result indicates that the task has been offloaded to the background worker
599 success = django_q.models.Success.objects.filter(id=task_id).first()
600 failure = django_q.models.Failure.objects.filter(id=task_id).first()
601 task = (
602 success
603 or failure
604 or django_q.models.Task.objects.filter(id=task_id).first()
605 )
606 queued = False
608 exists = bool(success or failure or task)
610 if not exists: 610 ↛ 614line 610 didn't jump to line 614 because the condition on line 610 was always true
611 # If the task has not been started yet, it may be present in the queue
612 queued = bool(get_queued_task(task_id))
614 complete = bool(success) or bool(failure)
616 # Determine the http_status code for the task
617 # - 200: Task exists and has been completed
618 # - 404: Task does not exist
619 http_status = 200 if exists or queued else 404
621 return cls({
622 'task_id': task_id,
623 'exists': exists or queued,
624 'pending': queued,
625 'complete': complete,
626 'success': bool(success),
627 'http_status': http_status,
628 })
631class TaskOverviewSerializer(serializers.Serializer):
632 """Serializer for background task overview."""
634 is_running = serializers.BooleanField(
635 label=_('Is Running'),
636 help_text='Boolean value to indicate if the background worker process is running.',
637 read_only=True,
638 )
640 pending_tasks = serializers.IntegerField(
641 label=_('Pending Tasks'),
642 help_text='Number of active background tasks',
643 read_only=True,
644 )
646 scheduled_tasks = serializers.IntegerField(
647 label=_('Scheduled Tasks'),
648 help_text='Number of scheduled background tasks',
649 read_only=True,
650 )
652 failed_tasks = serializers.IntegerField(
653 label=_('Failed Tasks'),
654 help_text='Number of failed background tasks',
655 read_only=True,
656 )
659class PendingTaskSerializer(InvenTreeModelSerializer):
660 """Serializer for an individual pending task object."""
662 class Meta:
663 """Metaclass options for the serializer."""
665 model = django_q.models.OrmQ
666 fields = ['pk', 'key', 'lock', 'task_id', 'name', 'func', 'args', 'kwargs']
668 task_id = serializers.CharField(label=_('Task ID'), help_text=_('Unique task ID'))
670 lock = serializers.DateTimeField(label=_('Lock'), help_text=_('Lock time'))
672 name = serializers.CharField(label=_('Name'), help_text=_('Task name'))
674 func = serializers.CharField(label=_('Function'), help_text=_('Function name'))
676 args = serializers.CharField(label=_('Arguments'), help_text=_('Task arguments'))
678 kwargs = serializers.CharField(
679 label=_('Keyword Arguments'), help_text=_('Task keyword arguments')
680 )
683class ScheduledTaskSerializer(InvenTreeModelSerializer):
684 """Serializer for an individual scheduled task object."""
686 class Meta:
687 """Metaclass options for the serializer."""
689 model = django_q.models.Schedule
690 fields = [
691 'pk',
692 'name',
693 'func',
694 'args',
695 'kwargs',
696 'schedule_type',
697 'repeats',
698 'last_run',
699 'next_run',
700 'success',
701 'task',
702 ]
704 last_run = serializers.DateTimeField()
705 success = serializers.BooleanField()
707 @staticmethod
708 def annotate_queryset(queryset):
709 """Add custom annotations to the queryset.
711 - last_run: The last time the task was run
712 - success: The outcome status of the last run
713 """
714 task = django_q.models.Task.objects.filter(id=OuterRef('task'))
716 queryset = queryset.annotate(
717 last_run=Subquery(task.values('started')[:1]),
718 success=Subquery(task.values('success')[:1]),
719 )
721 return queryset
724class FailedTaskSerializer(InvenTreeModelSerializer):
725 """Serializer for an individual failed task object."""
727 class Meta:
728 """Metaclass options for the serializer."""
730 model = django_q.models.Failure
731 fields = [
732 'pk',
733 'name',
734 'func',
735 'args',
736 'kwargs',
737 'started',
738 'stopped',
739 'attempt_count',
740 'result',
741 ]
743 pk = serializers.CharField(source='id', read_only=True)
745 result = serializers.CharField()
748class AttachmentSerializer(
749 FilterableSerializerMixin, InvenTreeTaggitSerializer, InvenTreeModelSerializer
750):
751 """Serializer class for the Attachment model."""
753 class Meta:
754 """Serializer metaclass."""
756 model = common_models.Attachment
757 fields = [
758 'pk',
759 'attachment',
760 'thumbnail',
761 'filename',
762 'link',
763 'comment',
764 'is_image',
765 'upload_date',
766 'upload_user',
767 'user_detail',
768 'file_size',
769 'model_type',
770 'model_id',
771 'tags',
772 ]
774 read_only_fields = [
775 'pk',
776 'file_size',
777 'upload_date',
778 'upload_user',
779 'filename',
780 'is_image',
781 ]
783 def __init__(self, *args, **kwargs):
784 """Override the model_type field to provide dynamic choices."""
785 super().__init__(*args, **kwargs)
787 if len(self.fields['model_type'].choices) == 0: 787 ↛ exitline 787 didn't return from function '__init__' because the condition on line 787 was always true
788 self.fields[
789 'model_type'
790 ].choices = common.validators.attachment_model_options()
792 tags = common.filters.enable_tags_filter()
794 user_detail = UserSerializer(source='upload_user', read_only=True, many=False)
796 attachment = InvenTreeAttachmentSerializerField(required=False, allow_null=True)
798 thumbnail = InvenTreeImageSerializerField(read_only=True, allow_null=True)
800 # The 'filename' field must be present in the serializer
801 filename = serializers.CharField(
802 label=_('Filename'), required=False, source='basename', allow_blank=False
803 )
805 upload_date = serializers.DateField(read_only=True)
807 # Note: The choices are overridden at run-time on class initialization
808 model_type = serializers.ChoiceField(
809 label=_('Model Type'),
810 choices=common.validators.attachment_model_options(),
811 required=True,
812 allow_blank=False,
813 allow_null=False,
814 )
816 def save(self, **kwargs):
817 """Override the save method to handle the model_type field."""
818 from InvenTree.models import InvenTreeAttachmentMixin
819 from users.permissions import check_user_permission
821 model_type = self.validated_data.get('model_type', None)
823 if model_type is None and self.instance: 823 ↛ 824line 823 didn't jump to line 824 because the condition on line 823 was never true
824 model_type = self.instance.model_type
826 # Ensure that the user has permission to attach files to the specified model
827 user = self.context.get('request').user
829 target_model_class = common.validators.attachment_model_class_from_label(
830 model_type
831 )
833 if not issubclass(target_model_class, InvenTreeAttachmentMixin): 833 ↛ 834line 833 didn't jump to line 834 because the condition on line 833 was never true
834 raise PermissionDenied(_('Invalid model type specified for attachment'))
836 permission_error_msg = _(
837 'User does not have permission to create or edit attachments for this model'
838 )
840 if not check_user_permission(user, target_model_class, 'change'): 840 ↛ 841line 840 didn't jump to line 841 because the condition on line 840 was never true
841 raise PermissionDenied(permission_error_msg)
843 # Check that the user has the required permissions to attach files to the target model
844 if not target_model_class.check_related_permission('change', user): 844 ↛ 845line 844 didn't jump to line 845 because the condition on line 844 was never true
845 raise PermissionDenied(permission_error_msg)
847 return super().save(**kwargs)
850@register_importer()
851class ParameterTemplateSerializer(
852 DataImportExportSerializerMixin, InvenTreeModelSerializer
853):
854 """Serializer for the ParameterTemplate model."""
856 class Meta:
857 """Meta options for ParameterTemplateSerializer."""
859 model = common_models.ParameterTemplate
860 fields = [
861 'pk',
862 'name',
863 'units',
864 'description',
865 'model_type',
866 'checkbox',
867 'choices',
868 'selectionlist',
869 'enabled',
870 ]
872 # Note: The choices are overridden at run-time on class initialization
873 model_type = ContentTypeField(
874 mixin_class=InvenTreeParameterMixin,
875 choices=common.validators.parameter_template_model_options,
876 label=_('Model Type'),
877 default='',
878 required=False,
879 allow_null=True,
880 )
882 def validate_model_type(self, model_type):
883 """Convert an empty string to None for the model_type field."""
884 return model_type or None
887@register_importer()
888class ParameterSerializer(
889 FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer
890):
891 """Serializer for the Parameter model."""
893 class Meta:
894 """Meta options for ParameterSerializer."""
896 model = common_models.Parameter
897 fields = [
898 'pk',
899 'template',
900 'model_type',
901 'model_id',
902 'data',
903 'data_numeric',
904 'note',
905 'updated',
906 'updated_by',
907 # Optional fields
908 'template_detail',
909 'updated_by_detail',
910 ]
912 read_only_fields = ['updated', 'updated_by']
914 def save(self, **kwargs):
915 """Save the Parameter instance."""
916 from InvenTree.models import InvenTreeParameterMixin
917 from users.permissions import check_user_permission
919 model_type = self.validated_data.get('model_type', None)
921 if model_type is None and self.instance:
922 model_type = self.instance.model_type
924 # Ensure that the user has permission to modify parameters for the specified model
925 user = self.context.get('request').user
927 target_model_class = model_type.model_class()
929 if not issubclass(target_model_class, InvenTreeParameterMixin):
930 raise PermissionDenied(_('Invalid model type specified for parameter'))
932 permission_error_msg = _(
933 'User does not have permission to create or edit parameters for this model'
934 )
936 if not check_user_permission(user, target_model_class, 'change'):
937 raise PermissionDenied(permission_error_msg)
939 if not target_model_class.check_related_permission('change', user):
940 raise PermissionDenied(permission_error_msg)
942 instance = super().save(**kwargs)
943 instance.updated_by = user
944 instance.save()
946 return instance
948 # Note: The choices are overridden at run-time on class initialization
949 model_type = ContentTypeField(
950 mixin_class=InvenTreeParameterMixin,
951 choices=common.validators.parameter_model_options,
952 label=_('Model Type'),
953 default='',
954 allow_null=False,
955 )
957 updated_by_detail = OptionalField(
958 serializer_class=UserSerializer,
959 serializer_kwargs={
960 'source': 'updated_by',
961 'read_only': True,
962 'allow_null': True,
963 'many': False,
964 },
965 default_include=True,
966 prefetch_fields=['updated_by'],
967 )
969 template_detail = OptionalField(
970 serializer_class=ParameterTemplateSerializer,
971 serializer_kwargs={'source': 'template', 'read_only': True, 'many': False},
972 default_include=True,
973 prefetch_fields=['template', 'template__model_type'],
974 )
977class IconSerializer(serializers.Serializer):
978 """Serializer for an icon."""
980 name = serializers.CharField()
981 category = serializers.CharField()
982 tags = serializers.ListField(child=serializers.CharField())
983 variants = serializers.DictField(child=serializers.CharField())
986class IconPackageSerializer(serializers.Serializer):
987 """Serializer for a list of icons."""
989 name = serializers.CharField()
990 prefix = serializers.CharField()
991 fonts = serializers.DictField(child=serializers.CharField())
992 icons = serializers.DictField(child=IconSerializer())
995class SelectionEntrySerializer(InvenTreeModelSerializer):
996 """Serializer for a selection entry."""
998 class Meta:
999 """Meta options for SelectionEntrySerializer."""
1001 model = common_models.SelectionListEntry
1002 fields = '__all__'
1004 def validate(self, attrs):
1005 """Ensure that the selection list is not locked."""
1006 ret = super().validate(attrs)
1007 list_obj = attrs.get('list') or (self.instance and self.instance.list)
1008 if list_obj and list_obj.locked: 1008 ↛ 1009line 1008 didn't jump to line 1009 because the condition on line 1008 was never true
1009 raise serializers.ValidationError({'list': _('Selection list is locked')})
1010 return ret
1013class SelectionListSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
1014 """Serializer for a selection list."""
1016 _choices_validated: dict = {}
1017 _choices_provided: bool = False
1019 class Meta:
1020 """Meta options for SelectionListSerializer."""
1022 model = common_models.SelectionList
1023 fields = [
1024 'pk',
1025 'name',
1026 'description',
1027 'active',
1028 'locked',
1029 'source_plugin',
1030 'source_string',
1031 'default',
1032 'created',
1033 'last_updated',
1034 'entry_count',
1035 'choices',
1036 ]
1038 default = SelectionEntrySerializer(read_only=True, allow_null=True, many=False)
1039 entry_count = serializers.IntegerField(read_only=True)
1041 choices = OptionalField(
1042 serializer_class=SelectionEntrySerializer,
1043 serializer_kwargs={'source': 'entries', 'many': True, 'read_only': True},
1044 prefetch_fields=['entries'],
1045 default_include=True,
1046 )
1048 @staticmethod
1049 def annotate_queryset(queryset):
1050 """Add count of entries for each selection list."""
1051 return queryset.annotate(entry_count=Count('entries'))
1053 def validate(self, attrs):
1054 """Ensure that the selection list is not locked."""
1055 ret = super().validate(attrs)
1056 if self.instance and self.instance.locked: 1056 ↛ 1057line 1056 didn't jump to line 1057 because the condition on line 1056 was never true
1057 raise serializers.ValidationError({'locked': _('Selection list is locked')})
1058 return ret
1061class DataOutputSerializer(InvenTreeModelSerializer):
1062 """Serializer for the DataOutput model."""
1064 class Meta:
1065 """Meta options for DataOutputSerializer."""
1067 model = common_models.DataOutput
1068 fields = [
1069 'pk',
1070 'created',
1071 'user',
1072 'user_detail',
1073 'total',
1074 'progress',
1075 'complete',
1076 'output_type',
1077 'template_name',
1078 'plugin',
1079 'output',
1080 'errors',
1081 ]
1083 user_detail = UserSerializer(source='user', read_only=True, many=False)
1085 output = InvenTreeAttachmentSerializerField(allow_null=True, read_only=True)
1088class EmailMessageSerializer(InvenTreeModelSerializer):
1089 """Serializer for the EmailMessage model."""
1091 class Meta:
1092 """Meta options for EmailMessageSerializer."""
1094 model = common_models.EmailMessage
1095 fields = [
1096 'pk',
1097 'global_id',
1098 'message_id_key',
1099 'thread_id_key',
1100 'thread',
1101 'subject',
1102 'body',
1103 'to',
1104 'sender',
1105 'status',
1106 'timestamp',
1107 'headers',
1108 'full_message',
1109 'direction',
1110 'priority',
1111 'error_code',
1112 'error_message',
1113 'error_timestamp',
1114 'delivery_options',
1115 ]
1118class TestEmailSerializer(serializers.Serializer):
1119 """Serializer to send a test email."""
1121 class Meta:
1122 """Meta options for TestEmailSerializer."""
1124 fields = ['email']
1126 email = serializers.EmailField(required=True)