fields.py 6.7 KB

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