customfields.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. from functools import cached_property
  2. from django.core.exceptions import ValidationError as DjangoValidationError
  3. from django.db.models import prefetch_related_objects
  4. from django.utils.translation import gettext as _
  5. from drf_spectacular.types import OpenApiTypes
  6. from drf_spectacular.utils import extend_schema_field
  7. from rest_framework.fields import Field
  8. from rest_framework.serializers import ListSerializer, ValidationError
  9. from extras.choices import CustomFieldTypeChoices
  10. from extras.constants import CUSTOMFIELD_EMPTY_VALUES
  11. from extras.models import CustomField
  12. from utilities.api import get_prefetches_for_serializer, get_serializer_for_model
  13. from utilities.forms.fields import LaxURLField
  14. #
  15. # Custom fields
  16. #
  17. class CustomFieldDefaultValues:
  18. """
  19. Return a dictionary of all CustomFields assigned to the parent model and their default values.
  20. """
  21. requires_context = True
  22. def __call__(self, serializer_field):
  23. self.model = serializer_field.parent.Meta.model
  24. # Populate the default value for each CustomField on the model
  25. value = {}
  26. for field in CustomField.objects.get_for_model(self.model):
  27. if field.default is not None:
  28. value[field.name] = field.default
  29. else:
  30. value[field.name] = None
  31. return value
  32. @extend_schema_field(OpenApiTypes.OBJECT)
  33. class CustomFieldsDataField(Field):
  34. def _get_custom_fields(self):
  35. """
  36. Cache CustomFields assigned to this model to avoid redundant database queries
  37. """
  38. if not hasattr(self, '_custom_fields'):
  39. self._custom_fields = CustomField.objects.get_for_model(self.parent.Meta.model)
  40. return self._custom_fields
  41. @cached_property
  42. def _nested_serializers(self):
  43. return {}
  44. def _get_nested_serializer(self, cf, many):
  45. """
  46. Return the nested serializer for the objects a custom field references, built once per field instance.
  47. """
  48. serializer_class = get_serializer_for_model(cf.related_object_type.model_class())
  49. if (serializer_class, many) not in self._nested_serializers:
  50. self._nested_serializers[(serializer_class, many)] = serializer_class(
  51. nested=True, many=many, context=self.parent.context
  52. )
  53. return self._nested_serializers[(serializer_class, many)]
  54. def to_representation(self, obj):
  55. data = {}
  56. cache = self.parent.context.get('cf_object_cache')
  57. for cf in self._get_custom_fields():
  58. if cache is not None and cf.type in (
  59. CustomFieldTypeChoices.TYPE_OBJECT,
  60. CustomFieldTypeChoices.TYPE_MULTIOBJECT,
  61. ):
  62. raw = obj.get(cf.name)
  63. if raw is None:
  64. value = None
  65. elif cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
  66. model = cf.related_object_type.model_class()
  67. value = cache.get((model, raw))
  68. else:
  69. model = cf.related_object_type.model_class()
  70. value = [cache[(model, pk)] for pk in raw if (model, pk) in cache] or None
  71. else:
  72. value = cf.deserialize(obj.get(cf.name))
  73. if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
  74. value = self._get_nested_serializer(cf, many=False).to_representation(value)
  75. elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
  76. value = self._get_nested_serializer(cf, many=True).to_representation(value)
  77. elif cf.type in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT):
  78. value = cf.resolve_selection_value(value)
  79. data[cf.name] = value
  80. return data
  81. def to_internal_value(self, data):
  82. if type(data) is not dict:
  83. raise ValidationError(
  84. "Invalid data format. Custom field data must be passed as a dictionary mapping field names to their "
  85. "values."
  86. )
  87. custom_fields = {cf.name: cf for cf in self._get_custom_fields()}
  88. # Reject any unknown custom field names
  89. invalid_fields = set(data) - set(custom_fields)
  90. if invalid_fields:
  91. raise ValidationError({
  92. field: _("Custom field '{name}' does not exist for this object type.").format(name=field)
  93. for field in sorted(invalid_fields)
  94. })
  95. # Serialize object and multi-object values
  96. for cf in custom_fields.values():
  97. if cf.name in data and data[cf.name] not in CUSTOMFIELD_EMPTY_VALUES and cf.type in (
  98. CustomFieldTypeChoices.TYPE_OBJECT,
  99. CustomFieldTypeChoices.TYPE_MULTIOBJECT
  100. ):
  101. serializer_class = get_serializer_for_model(cf.related_object_type.model_class())
  102. many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT
  103. serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context)
  104. if serializer.is_valid():
  105. data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id']
  106. else:
  107. raise ValidationError(_("Unknown related object(s): {name}").format(name=data[cf.name]))
  108. # Normalize URL values the same way the UI does (LaxURLField with assume_scheme='https'), so a
  109. # schemeless value (e.g. "example.com") is stored as an absolute URL ("https://example.com").
  110. # Malformed values are left untouched for CustomField.validate() to report.
  111. elif cf.type == CustomFieldTypeChoices.TYPE_URL and isinstance(data.get(cf.name), str) and data[cf.name]:
  112. try:
  113. data[cf.name] = LaxURLField(assume_scheme='https').to_python(data[cf.name])
  114. except DjangoValidationError:
  115. pass
  116. # If updating an existing instance, start with existing custom_field_data
  117. if self.parent.instance:
  118. data = {**self.parent.instance.custom_field_data, **data}
  119. return data
  120. class CustomFieldListSerializer(ListSerializer):
  121. """
  122. ListSerializer that pre-fetches all OBJECT/MULTIOBJECT custom field related objects
  123. in bulk before per-item serialization.
  124. """
  125. def to_representation(self, data):
  126. cf_field = self.child.fields.get('custom_fields')
  127. if isinstance(cf_field, CustomFieldsDataField):
  128. object_type_cfs = [
  129. cf for cf in cf_field._get_custom_fields()
  130. if cf.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT)
  131. ]
  132. pks_by_model = {}
  133. for cf in object_type_cfs:
  134. pks = pks_by_model.setdefault(cf.related_object_type.model_class(), set())
  135. for item in data:
  136. raw = item.custom_field_data.get(cf.name)
  137. if raw is not None:
  138. if cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
  139. pks.update(raw)
  140. else:
  141. pks.add(raw)
  142. cache = {}
  143. for model, pks in pks_by_model.items():
  144. objects = list(model.objects.filter(pk__in=pks))
  145. # A target model may lack a REST serializer, so plan only for found targets
  146. if objects:
  147. serializer_class = get_serializer_for_model(model)
  148. # Targets render with their nested (brief) representation
  149. fields = getattr(serializer_class.Meta, 'brief_fields', None)
  150. prefetch_related_objects(objects, *get_prefetches_for_serializer(serializer_class, fields=fields))
  151. for obj in objects:
  152. cache[(model, obj.pk)] = obj
  153. self.child.context['cf_object_cache'] = cache
  154. return super().to_representation(data)