| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- from collections import defaultdict
- from django.contrib.contenttypes.fields import GenericForeignKey
- from django.db import models
- from utilities.ordering import naturalize
- from .forms.widgets import ColorSelect
- from .validators import ColorValidator
- __all__ = (
- 'ColorField',
- 'NaturalOrderingField',
- 'NullableCharField',
- 'RestrictedGenericForeignKey',
- )
- # Deprecated: Retained only to ensure successful migration from early releases
- # Use models.CharField(null=True) instead
- # TODO: Remove in v4.0
- class NullableCharField(models.CharField):
- description = "Stores empty values as NULL rather than ''"
- def to_python(self, value):
- if isinstance(value, models.CharField):
- return value
- return value or ''
- def get_prep_value(self, value):
- return value or None
- class ColorField(models.CharField):
- default_validators = [ColorValidator]
- description = "A hexadecimal RGB color code"
- def __init__(self, *args, **kwargs):
- kwargs['max_length'] = 6
- super().__init__(*args, **kwargs)
- def formfield(self, **kwargs):
- kwargs['widget'] = ColorSelect
- return super().formfield(**kwargs)
- class NaturalOrderingField(models.CharField):
- """
- A field which stores a naturalized representation of its target field, to be used for ordering its parent model.
- :param target_field: Name of the field of the parent model to be naturalized
- :param naturalize_function: The function used to generate a naturalized value (optional)
- """
- description = "Stores a representation of its target field suitable for natural ordering"
- def __init__(self, target_field, naturalize_function=naturalize, *args, **kwargs):
- self.target_field = target_field
- self.naturalize_function = naturalize_function
- super().__init__(*args, **kwargs)
- def pre_save(self, model_instance, add):
- """
- Generate a naturalized value from the target field
- """
- original_value = getattr(model_instance, self.target_field)
- naturalized_value = self.naturalize_function(original_value, max_length=self.max_length)
- setattr(model_instance, self.attname, naturalized_value)
- return naturalized_value
- def deconstruct(self):
- kwargs = super().deconstruct()[3] # Pass kwargs from CharField
- kwargs['naturalize_function'] = self.naturalize_function
- return (
- self.name,
- 'utilities.fields.NaturalOrderingField',
- [self.target_field],
- kwargs,
- )
- class RestrictedGenericForeignKey(GenericForeignKey):
- # Replicated largely from GenericForeignKey. Changes include:
- # 1. Capture restrict_params from RestrictedPrefetch (hack)
- # 2. If restrict_params is set, call restrict() on the queryset for
- # the related model
- def get_prefetch_queryset(self, instances, queryset=None):
- restrict_params = {}
- # Compensate for the hack in RestrictedPrefetch
- if type(queryset) is dict:
- restrict_params = queryset
- elif queryset is not None:
- raise ValueError("Custom queryset can't be used for this lookup.")
- # For efficiency, group the instances by content type and then do one
- # query per model
- fk_dict = defaultdict(set)
- # We need one instance for each group in order to get the right db:
- instance_dict = {}
- ct_attname = self.model._meta.get_field(self.ct_field).get_attname()
- for instance in instances:
- # We avoid looking for values if either ct_id or fkey value is None
- ct_id = getattr(instance, ct_attname)
- if ct_id is not None:
- fk_val = getattr(instance, self.fk_field)
- if fk_val is not None:
- fk_dict[ct_id].add(fk_val)
- instance_dict[ct_id] = instance
- ret_val = []
- for ct_id, fkeys in fk_dict.items():
- instance = instance_dict[ct_id]
- ct = self.get_content_type(id=ct_id, using=instance._state.db)
- if restrict_params:
- # Override the default behavior to call restrict() on each model's queryset
- qs = ct.model_class().objects.filter(pk__in=fkeys).restrict(**restrict_params)
- ret_val.extend(qs)
- else:
- # Default behavior
- ret_val.extend(ct.get_all_objects_for_this_type(pk__in=fkeys))
- # For doing the join in Python, we have to match both the FK val and the
- # content type, so we use a callable that returns a (fk, class) pair.
- def gfk_key(obj):
- ct_id = getattr(obj, ct_attname)
- if ct_id is None:
- return None
- else:
- model = self.get_content_type(
- id=ct_id, using=obj._state.db
- ).model_class()
- return (
- model._meta.pk.get_prep_value(getattr(obj, self.fk_field)),
- model,
- )
- return (
- ret_val,
- lambda obj: (obj.pk, obj.__class__),
- gfk_key,
- True,
- self.name,
- False,
- )
|