customfields.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. from datetime import datetime
  2. from django.contrib.contenttypes.models import ContentType
  3. from rest_framework.exceptions import ValidationError
  4. from rest_framework.fields import CreateOnlyDefault, Field
  5. from extras.choices import *
  6. from extras.models import CustomField
  7. from utilities.api import ValidatedModelSerializer
  8. #
  9. # Custom fields
  10. #
  11. class CustomFieldDefaultValues:
  12. """
  13. Return a dictionary of all CustomFields assigned to the parent model and their default values.
  14. """
  15. requires_context = True
  16. def __call__(self, serializer_field):
  17. self.model = serializer_field.parent.Meta.model
  18. # Retrieve the CustomFields for the parent model
  19. content_type = ContentType.objects.get_for_model(self.model)
  20. fields = CustomField.objects.filter(obj_type=content_type)
  21. # Populate the default value for each CustomField
  22. value = {}
  23. for field in fields:
  24. if field.default:
  25. if field.type == CustomFieldTypeChoices.TYPE_INTEGER:
  26. field_value = int(field.default)
  27. elif field.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
  28. # TODO: Fix default value assignment for boolean custom fields
  29. field_value = False if field.default.lower() == 'false' else bool(field.default)
  30. else:
  31. field_value = field.default
  32. value[field.name] = field_value
  33. else:
  34. value[field.name] = None
  35. return value
  36. class CustomFieldsDataField(Field):
  37. def _get_custom_fields(self):
  38. """
  39. Cache CustomFields assigned to this model to avoid redundant database queries
  40. """
  41. if not hasattr(self, '_custom_fields'):
  42. content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
  43. self._custom_fields = CustomField.objects.filter(obj_type=content_type)
  44. return self._custom_fields
  45. def to_representation(self, obj):
  46. return {
  47. cf.name: obj.get(cf.name) for cf in self._get_custom_fields()
  48. }
  49. def to_internal_value(self, data):
  50. # If updating an existing instance, start with existing custom_field_data
  51. if self.parent.instance:
  52. data = {**self.parent.instance.custom_field_data, **data}
  53. custom_fields = {field.name: field for field in self._get_custom_fields()}
  54. for field_name, value in data.items():
  55. try:
  56. cf = custom_fields[field_name]
  57. except KeyError:
  58. raise ValidationError(f"Invalid custom field name: {field_name}")
  59. # Data validation
  60. if value not in [None, '']:
  61. # Validate integer
  62. if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
  63. try:
  64. int(value)
  65. except ValueError:
  66. raise ValidationError(f"Invalid value for integer field {field_name}: {value}")
  67. # Validate boolean
  68. if cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
  69. raise ValidationError(f"Invalid value for boolean field {field_name}: {value}")
  70. # Validate date
  71. if cf.type == CustomFieldTypeChoices.TYPE_DATE:
  72. try:
  73. datetime.strptime(value, '%Y-%m-%d')
  74. except ValueError:
  75. raise ValidationError(
  76. f"Invalid date for field {field_name}: {value}. (Required format is YYYY-MM-DD.)"
  77. )
  78. # Validate selected choice
  79. if cf.type == CustomFieldTypeChoices.TYPE_SELECT:
  80. if value not in cf.choices:
  81. raise ValidationError(f"Invalid choice for field {field_name}: {value}")
  82. elif cf.required:
  83. raise ValidationError(f"Required field {field_name} cannot be empty.")
  84. # Check for missing required fields
  85. missing_fields = []
  86. for field_name, field in custom_fields.items():
  87. if field.required and field_name not in data:
  88. missing_fields.append(field_name)
  89. if missing_fields:
  90. raise ValidationError("Missing required fields: {}".format(u", ".join(missing_fields)))
  91. return data
  92. class CustomFieldModelSerializer(ValidatedModelSerializer):
  93. """
  94. Extends ModelSerializer to render any CustomFields and their values associated with an object.
  95. """
  96. custom_fields = CustomFieldsDataField(
  97. source='custom_field_data',
  98. default=CreateOnlyDefault(CustomFieldDefaultValues())
  99. )
  100. def __init__(self, *args, **kwargs):
  101. super().__init__(*args, **kwargs)
  102. if self.instance is not None:
  103. # Retrieve the set of CustomFields which apply to this type of object
  104. content_type = ContentType.objects.get_for_model(self.Meta.model)
  105. fields = CustomField.objects.filter(obj_type=content_type)
  106. # Populate CustomFieldValues for each instance from database
  107. if type(self.instance) in (list, tuple):
  108. for obj in self.instance:
  109. self._populate_custom_fields(obj, fields)
  110. else:
  111. self._populate_custom_fields(self.instance, fields)
  112. def _populate_custom_fields(self, instance, custom_fields):
  113. instance.custom_fields = {}
  114. for field in custom_fields:
  115. instance.custom_fields[field.name] = instance.cf.get(field.name)