customfields.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. from datetime import datetime
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.db import transaction
  4. from rest_framework import serializers
  5. from rest_framework.exceptions import ValidationError
  6. from extras.choices import *
  7. from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
  8. from utilities.api import ValidatedModelSerializer
  9. #
  10. # Custom fields
  11. #
  12. class CustomFieldsSerializer(serializers.BaseSerializer):
  13. def to_representation(self, obj):
  14. return obj
  15. def to_internal_value(self, data):
  16. content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
  17. custom_fields = {
  18. field.name: field for field in CustomField.objects.filter(obj_type=content_type)
  19. }
  20. for field_name, value in data.items():
  21. try:
  22. cf = custom_fields[field_name]
  23. except KeyError:
  24. raise ValidationError(
  25. "Invalid custom field for {} objects: {}".format(content_type, field_name)
  26. )
  27. # Data validation
  28. if value not in [None, '']:
  29. # Validate integer
  30. if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
  31. try:
  32. int(value)
  33. except ValueError:
  34. raise ValidationError(
  35. "Invalid value for integer field {}: {}".format(field_name, value)
  36. )
  37. # Validate boolean
  38. if cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
  39. raise ValidationError(
  40. "Invalid value for boolean field {}: {}".format(field_name, value)
  41. )
  42. # Validate date
  43. if cf.type == CustomFieldTypeChoices.TYPE_DATE:
  44. try:
  45. datetime.strptime(value, '%Y-%m-%d')
  46. except ValueError:
  47. raise ValidationError(
  48. "Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(field_name, value)
  49. )
  50. # Validate selected choice
  51. if cf.type == CustomFieldTypeChoices.TYPE_SELECT:
  52. try:
  53. value = int(value)
  54. except ValueError:
  55. raise ValidationError(
  56. "{}: Choice selections must be passed as integers.".format(field_name)
  57. )
  58. valid_choices = [c.pk for c in cf.choices.all()]
  59. if value not in valid_choices:
  60. raise ValidationError(
  61. "Invalid choice for field {}: {}".format(field_name, value)
  62. )
  63. elif cf.required:
  64. raise ValidationError("Required field {} cannot be empty.".format(field_name))
  65. # Check for missing required fields
  66. missing_fields = []
  67. for field_name, field in custom_fields.items():
  68. if field.required and field_name not in data:
  69. missing_fields.append(field_name)
  70. if missing_fields:
  71. raise ValidationError("Missing required fields: {}".format(u", ".join(missing_fields)))
  72. return data
  73. class CustomFieldModelSerializer(ValidatedModelSerializer):
  74. """
  75. Extends ModelSerializer to render any CustomFields and their values associated with an object.
  76. """
  77. custom_fields = CustomFieldsSerializer(required=False)
  78. def __init__(self, *args, **kwargs):
  79. def _populate_custom_fields(instance, fields):
  80. instance.custom_fields = {}
  81. for field in fields:
  82. value = instance.cf.get(field.name)
  83. if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None:
  84. instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
  85. else:
  86. instance.custom_fields[field.name] = value
  87. super().__init__(*args, **kwargs)
  88. # Retrieve the set of CustomFields which apply to this type of object
  89. content_type = ContentType.objects.get_for_model(self.Meta.model)
  90. fields = CustomField.objects.filter(obj_type=content_type)
  91. if self.instance is not None:
  92. # Populate CustomFieldValues for each instance from database
  93. try:
  94. for obj in self.instance:
  95. _populate_custom_fields(obj, fields)
  96. except TypeError:
  97. _populate_custom_fields(self.instance, fields)
  98. else:
  99. if not hasattr(self, 'initial_data'):
  100. self.initial_data = {}
  101. # Populate default values
  102. if fields and 'custom_fields' not in self.initial_data:
  103. self.initial_data['custom_fields'] = {}
  104. # Populate initial data using custom field default values
  105. for field in fields:
  106. if field.name not in self.initial_data['custom_fields'] and field.default:
  107. if field.type == CustomFieldTypeChoices.TYPE_SELECT:
  108. field_value = field.choices.get(value=field.default).pk
  109. elif field.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
  110. field_value = bool(field.default)
  111. else:
  112. field_value = field.default
  113. self.initial_data['custom_fields'][field.name] = field_value
  114. def _save_custom_fields(self, instance, custom_fields):
  115. content_type = ContentType.objects.get_for_model(self.Meta.model)
  116. for field_name, value in custom_fields.items():
  117. custom_field = CustomField.objects.get(name=field_name)
  118. CustomFieldValue.objects.update_or_create(
  119. field=custom_field,
  120. obj_type=content_type,
  121. obj_id=instance.pk,
  122. defaults={'serialized_value': custom_field.serialize_value(value)},
  123. )
  124. def create(self, validated_data):
  125. custom_fields = validated_data.pop('custom_fields', None)
  126. with transaction.atomic():
  127. instance = super().create(validated_data)
  128. # Save custom fields
  129. if custom_fields is not None:
  130. self._save_custom_fields(instance, custom_fields)
  131. instance.custom_fields = custom_fields
  132. return instance
  133. def update(self, instance, validated_data):
  134. custom_fields = validated_data.pop('custom_fields', None)
  135. with transaction.atomic():
  136. instance = super().update(instance, validated_data)
  137. # Save custom fields
  138. if custom_fields is not None:
  139. self._save_custom_fields(instance, custom_fields)
  140. instance.custom_fields = custom_fields
  141. return instance
  142. class CustomFieldChoiceSerializer(serializers.ModelSerializer):
  143. """
  144. Imitate utilities.api.ChoiceFieldSerializer
  145. """
  146. value = serializers.IntegerField(source='pk')
  147. label = serializers.CharField(source='value')
  148. class Meta:
  149. model = CustomFieldChoice
  150. fields = ['value', 'label']