customfields.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. from django.utils.translation import gettext as _
  2. from drf_spectacular.types import OpenApiTypes
  3. from drf_spectacular.utils import extend_schema_field
  4. from rest_framework.fields import Field
  5. from rest_framework.serializers import ListSerializer, ValidationError
  6. from extras.choices import CustomFieldTypeChoices
  7. from extras.constants import CUSTOMFIELD_EMPTY_VALUES
  8. from extras.models import CustomField
  9. from utilities.api import get_serializer_for_model
  10. #
  11. # Custom fields
  12. #
  13. class CustomFieldDefaultValues:
  14. """
  15. Return a dictionary of all CustomFields assigned to the parent model and their default values.
  16. """
  17. requires_context = True
  18. def __call__(self, serializer_field):
  19. self.model = serializer_field.parent.Meta.model
  20. # Populate the default value for each CustomField on the model
  21. value = {}
  22. for field in CustomField.objects.get_for_model(self.model):
  23. if field.default is not None:
  24. value[field.name] = field.default
  25. else:
  26. value[field.name] = None
  27. return value
  28. @extend_schema_field(OpenApiTypes.OBJECT)
  29. class CustomFieldsDataField(Field):
  30. def _get_custom_fields(self):
  31. """
  32. Cache CustomFields assigned to this model to avoid redundant database queries
  33. """
  34. if not hasattr(self, '_custom_fields'):
  35. self._custom_fields = CustomField.objects.get_for_model(self.parent.Meta.model)
  36. return self._custom_fields
  37. def to_representation(self, obj):
  38. # TODO: Fix circular import
  39. from utilities.api import get_serializer_for_model
  40. data = {}
  41. cache = self.parent.context.get('cf_object_cache')
  42. for cf in self._get_custom_fields():
  43. if cache is not None and cf.type in (
  44. CustomFieldTypeChoices.TYPE_OBJECT,
  45. CustomFieldTypeChoices.TYPE_MULTIOBJECT,
  46. ):
  47. raw = obj.get(cf.name)
  48. if raw is None:
  49. value = None
  50. elif cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
  51. model = cf.related_object_type.model_class()
  52. value = cache.get((model, raw))
  53. else:
  54. model = cf.related_object_type.model_class()
  55. value = [cache[(model, pk)] for pk in raw if (model, pk) in cache] or None
  56. else:
  57. value = cf.deserialize(obj.get(cf.name))
  58. if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
  59. serializer = get_serializer_for_model(cf.related_object_type.model_class())
  60. value = serializer(value, nested=True, context=self.parent.context).data
  61. elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
  62. serializer = get_serializer_for_model(cf.related_object_type.model_class())
  63. value = serializer(value, nested=True, many=True, context=self.parent.context).data
  64. elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_SELECT:
  65. # Represent the selected choice as an object with its value and resolved label
  66. value = {
  67. 'value': value,
  68. 'label': cf.get_choice_label(value),
  69. }
  70. elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
  71. # Represent each selected choice as an object with its value and resolved label
  72. value = [
  73. {'value': v, 'label': cf.get_choice_label(v)} for v in value
  74. ]
  75. data[cf.name] = value
  76. return data
  77. def to_internal_value(self, data):
  78. if type(data) is not dict:
  79. raise ValidationError(
  80. "Invalid data format. Custom field data must be passed as a dictionary mapping field names to their "
  81. "values."
  82. )
  83. custom_fields = {cf.name: cf for cf in self._get_custom_fields()}
  84. # Reject any unknown custom field names
  85. invalid_fields = set(data) - set(custom_fields)
  86. if invalid_fields:
  87. raise ValidationError({
  88. field: _("Custom field '{name}' does not exist for this object type.").format(name=field)
  89. for field in sorted(invalid_fields)
  90. })
  91. # Serialize object and multi-object values
  92. for cf in custom_fields.values():
  93. if cf.name in data and data[cf.name] not in CUSTOMFIELD_EMPTY_VALUES and cf.type in (
  94. CustomFieldTypeChoices.TYPE_OBJECT,
  95. CustomFieldTypeChoices.TYPE_MULTIOBJECT
  96. ):
  97. serializer_class = get_serializer_for_model(cf.related_object_type.model_class())
  98. many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT
  99. serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context)
  100. if serializer.is_valid():
  101. data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id']
  102. else:
  103. raise ValidationError(_("Unknown related object(s): {name}").format(name=data[cf.name]))
  104. # If updating an existing instance, start with existing custom_field_data
  105. if self.parent.instance:
  106. data = {**self.parent.instance.custom_field_data, **data}
  107. return data
  108. class CustomFieldListSerializer(ListSerializer):
  109. """
  110. ListSerializer that pre-fetches all OBJECT/MULTIOBJECT custom field related objects
  111. in bulk before per-item serialization.
  112. """
  113. def to_representation(self, data):
  114. cf_field = self.child.fields.get('custom_fields')
  115. if isinstance(cf_field, CustomFieldsDataField):
  116. object_type_cfs = [
  117. cf for cf in cf_field._get_custom_fields()
  118. if cf.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT)
  119. ]
  120. cache = {}
  121. for cf in object_type_cfs:
  122. model = cf.related_object_type.model_class()
  123. pks = set()
  124. for item in data:
  125. raw = item.custom_field_data.get(cf.name)
  126. if raw is not None:
  127. if cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
  128. pks.update(raw)
  129. else:
  130. pks.add(raw)
  131. for obj in model.objects.filter(pk__in=pks):
  132. cache[(model, obj.pk)] = obj
  133. self.child.context['cf_object_cache'] = cache
  134. return super().to_representation(data)