2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-12-16 17:28:11 +00:00

Add custom serializer field for ContentType with choices

This commit is contained in:
Oliver Walters
2025-11-24 13:14:36 +00:00
parent 019494420a
commit 6c76b309bf
2 changed files with 84 additions and 0 deletions

View File

@@ -281,6 +281,25 @@ def getModelsWithMixin(mixin_class) -> list:
return [x for x in db_models if x is not None and issubclass(x, mixin_class)]
def getModelChoicesWithMixin(
mixin_class, allow_null: bool = False
) -> list[tuple[str, str]]:
"""Return a list of model choices (app_label.model_name, verbose_name) for models that inherit from the given mixin class."""
choices = []
if allow_null:
choices.append((None, _('None')))
models = getModelsWithMixin(mixin_class)
for model in models:
label = f'{model._meta.app_label}.{model._meta.model_name}'
name = model._meta.verbose_name
choices.append((label, name))
return choices
def notify_responsible(
instance,
sender,

View File

@@ -28,6 +28,7 @@ import InvenTree.ready
from common.currency import currency_code_default, currency_code_mappings
from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
from InvenTree.helpers import str2bool
from InvenTree.helpers_model import getModelChoicesWithMixin
from .setting.storages import StorageBackends
@@ -716,3 +717,67 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
raise ValidationError(_('Failed to download image from remote URL'))
return url
class ContentTypeField(serializers.Field):
"""Serializer field which represents a ContentType as 'app_label.model_name'.
This field converts a ContentType instance to a string representation in the format 'app_label.model_name' during serialization, and vice versa during deserialization.
Additionally, a "mixin_class" can be supplied to the field, which will restrict the valid content types to only those models which inherit from the specified mixin.
"""
mixin_class = None
def __init__(self, *args, mixin_class=None, **kwargs):
"""Initialize the ContentTypeField.
Args:
mixin_class: Optional mixin class to restrict valid content types.
"""
self.mixin_class = mixin_class
super().__init__(*args, **kwargs)
# Override the 'choices' field, to limit to the appropriate models
if self.mixin_class is not None:
self.choices = getModelChoicesWithMixin(
self.mixin_class, allow_null=kwargs.get('allow_null', False)
)
def to_representation(self, value):
"""Convert ContentType instance to string representation."""
return f'{value.app_label}.{value.model}'
def to_internal_value(self, data):
"""Convert string representation back to ContentType instance."""
from django.contrib.contenttypes.models import ContentType
content_type = None
try:
app_label, model = data.split('.')
content_types = ContentType.objects.filter(app_label=app_label, model=model)
if content_types.exists() and content_types.count() == 1:
# Try exact match first
content_type = content_types.first()
else:
# Try lookup just on model name
content_types = ContentType.objects.filter(model=model)
if content_types.exists() and content_types.count() == 1:
content_type = content_types.first()
except Exception:
raise ValidationError(_('Invalid content type format'))
if content_type is None:
raise ValidationError(_('Content type not found'))
if self.mixin_class is not None:
model_class = content_type.model_class()
if not issubclass(model_class, self.mixin_class):
raise ValidationError(
_('Content type does not match required mixin class')
)
return content_type