fields.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. from collections import defaultdict
  2. from django.contrib.contenttypes.fields import GenericForeignKey
  3. from django.db import models
  4. from utilities.ordering import naturalize
  5. from .forms.widgets import ColorSelect
  6. from .validators import ColorValidator
  7. __all__ = (
  8. 'ColorField',
  9. 'NaturalOrderingField',
  10. 'NullableCharField',
  11. 'RestrictedGenericForeignKey',
  12. )
  13. # Deprecated: Retained only to ensure successful migration from early releases
  14. # Use models.CharField(null=True) instead
  15. # TODO: Remove in v4.0
  16. class NullableCharField(models.CharField):
  17. description = "Stores empty values as NULL rather than ''"
  18. def to_python(self, value):
  19. if isinstance(value, models.CharField):
  20. return value
  21. return value or ''
  22. def get_prep_value(self, value):
  23. return value or None
  24. class ColorField(models.CharField):
  25. default_validators = [ColorValidator]
  26. description = "A hexadecimal RGB color code"
  27. def __init__(self, *args, **kwargs):
  28. kwargs['max_length'] = 6
  29. super().__init__(*args, **kwargs)
  30. def formfield(self, **kwargs):
  31. kwargs['widget'] = ColorSelect
  32. return super().formfield(**kwargs)
  33. class NaturalOrderingField(models.CharField):
  34. """
  35. A field which stores a naturalized representation of its target field, to be used for ordering its parent model.
  36. :param target_field: Name of the field of the parent model to be naturalized
  37. :param naturalize_function: The function used to generate a naturalized value (optional)
  38. """
  39. description = "Stores a representation of its target field suitable for natural ordering"
  40. def __init__(self, target_field, naturalize_function=naturalize, *args, **kwargs):
  41. self.target_field = target_field
  42. self.naturalize_function = naturalize_function
  43. super().__init__(*args, **kwargs)
  44. def pre_save(self, model_instance, add):
  45. """
  46. Generate a naturalized value from the target field
  47. """
  48. original_value = getattr(model_instance, self.target_field)
  49. naturalized_value = self.naturalize_function(original_value, max_length=self.max_length)
  50. setattr(model_instance, self.attname, naturalized_value)
  51. return naturalized_value
  52. def deconstruct(self):
  53. kwargs = super().deconstruct()[3] # Pass kwargs from CharField
  54. kwargs['naturalize_function'] = self.naturalize_function
  55. return (
  56. self.name,
  57. 'utilities.fields.NaturalOrderingField',
  58. [self.target_field],
  59. kwargs,
  60. )
  61. class RestrictedGenericForeignKey(GenericForeignKey):
  62. # Replicated largely from GenericForeignKey. Changes include:
  63. # 1. Capture restrict_params from RestrictedPrefetch (hack)
  64. # 2. If restrict_params is set, call restrict() on the queryset for
  65. # the related model
  66. def get_prefetch_queryset(self, instances, queryset=None):
  67. restrict_params = {}
  68. # Compensate for the hack in RestrictedPrefetch
  69. if type(queryset) is dict:
  70. restrict_params = queryset
  71. elif queryset is not None:
  72. raise ValueError("Custom queryset can't be used for this lookup.")
  73. # For efficiency, group the instances by content type and then do one
  74. # query per model
  75. fk_dict = defaultdict(set)
  76. # We need one instance for each group in order to get the right db:
  77. instance_dict = {}
  78. ct_attname = self.model._meta.get_field(self.ct_field).get_attname()
  79. for instance in instances:
  80. # We avoid looking for values if either ct_id or fkey value is None
  81. ct_id = getattr(instance, ct_attname)
  82. if ct_id is not None:
  83. fk_val = getattr(instance, self.fk_field)
  84. if fk_val is not None:
  85. fk_dict[ct_id].add(fk_val)
  86. instance_dict[ct_id] = instance
  87. ret_val = []
  88. for ct_id, fkeys in fk_dict.items():
  89. instance = instance_dict[ct_id]
  90. ct = self.get_content_type(id=ct_id, using=instance._state.db)
  91. if restrict_params:
  92. # Override the default behavior to call restrict() on each model's queryset
  93. qs = ct.model_class().objects.filter(pk__in=fkeys).restrict(**restrict_params)
  94. ret_val.extend(qs)
  95. else:
  96. # Default behavior
  97. ret_val.extend(ct.get_all_objects_for_this_type(pk__in=fkeys))
  98. # For doing the join in Python, we have to match both the FK val and the
  99. # content type, so we use a callable that returns a (fk, class) pair.
  100. def gfk_key(obj):
  101. ct_id = getattr(obj, ct_attname)
  102. if ct_id is None:
  103. return None
  104. else:
  105. model = self.get_content_type(
  106. id=ct_id, using=obj._state.db
  107. ).model_class()
  108. return (
  109. model._meta.pk.get_prep_value(getattr(obj, self.fk_field)),
  110. model,
  111. )
  112. return (
  113. ret_val,
  114. lambda obj: (obj.pk, obj.__class__),
  115. gfk_key,
  116. True,
  117. self.name,
  118. False,
  119. )