""" Helper functions for mapping model fields to a dictionary of default keyword arguments that should be used for their equivalent serializer fields. """ import inspect from django.core import validators from django.db import models from django.utils.text import capfirst from rest_framework.compat import postgres_fields from rest_framework.validators import UniqueValidator NUMERIC_FIELD_TYPES = ( models.IntegerField, models.FloatField, models.DecimalField, models.DurationField, ) class ClassLookupDict: """ Takes a dictionary with classes as keys. Lookups against this object will traverses the object's inheritance hierarchy in method resolution order, and returns the first matching value from the dictionary or raises a KeyError if nothing matches. """ def __init__(self, mapping): self.mapping = mapping def __getitem__(self, key): if hasattr(key, '_proxy_class'): # Deal with proxy classes. Ie. BoundField behaves as if it # is a Field instance when using ClassLookupDict. base_class = key._proxy_class else: base_class = key.__class__ for cls in inspect.getmro(base_class): if cls in self.mapping: return self.mapping[cls] raise KeyError('Class %s not found in lookup.' % base_class.__name__) def __setitem__(self, key, value): self.mapping[key] = value def needs_label(model_field, field_name): """ Returns `True` if the label based on the model's verbose name is not equal to the default label it would have based on it's field name. """ default_label = field_name.replace('_', ' ').capitalize() return capfirst(model_field.verbose_name) != default_label def get_detail_view_name(model): """ Given a model class, return the view name to use for URL relationships that refer to instances of the model. """ return '%(model_name)s-detail' % { 'model_name': model._meta.object_name.lower() } def get_unique_validators(field_name, model_field): """ Returns a list of UniqueValidators that should be applied to the field. """ field_set = set([field_name]) conditions = { c.condition for c in model_field.model._meta.constraints if isinstance(c, models.UniqueConstraint) and set(c.fields) == field_set } if getattr(model_field, 'unique', False): conditions.add(None) if not conditions: return unique_error_message = get_unique_error_message(model_field) queryset = model_field.model._default_manager for condition in conditions: yield UniqueValidator( queryset=queryset if condition is None else queryset.filter(condition), message=unique_error_message ) def get_field_kwargs(field_name, model_field): """ Creates a default instance of a basic non-relational field. """ kwargs = {} validator_kwarg = list(model_field.validators) # The following will only be used by ModelField classes. # Gets removed for everything else. kwargs['model_field'] = model_field if model_field.verbose_name and needs_label(model_field, field_name): kwargs['label'] = capfirst(model_field.verbose_name) if model_field.help_text: kwargs['help_text'] = model_field.help_text max_digits = getattr(model_field, 'max_digits', None) if max_digits is not None: kwargs['max_digits'] = max_digits decimal_places = getattr(model_field, 'decimal_places', None) if decimal_places is not None: kwargs['decimal_places'] = decimal_places if isinstance(model_field, models.SlugField): kwargs['allow_unicode'] = model_field.allow_unicode if isinstance(model_field, models.TextField) and not model_field.choices or \ (postgres_fields and isinstance(model_field, postgres_fields.JSONField)) or \ (hasattr(models, 'JSONField') and isinstance(model_field, models.JSONField)): kwargs['style'] = {'base_template': 'textarea.html'} if model_field.null: kwargs['allow_null'] = True if isinstance(model_field, models.AutoField) or not model_field.editable: # If this field is read-only, then return early. # Further keyword arguments are not valid. kwargs['read_only'] = True return kwargs if model_field.has_default() or model_field.blank or model_field.null: kwargs['required'] = False if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))): kwargs['allow_blank'] = True if not model_field.blank and (postgres_fields and isinstance(model_field, postgres_fields.ArrayField)): kwargs['allow_empty'] = False if isinstance(model_field, models.FilePathField): kwargs['path'] = model_field.path if model_field.match is not None: kwargs['match'] = model_field.match if model_field.recursive is not False: kwargs['recursive'] = model_field.recursive if model_field.allow_files is not True: kwargs['allow_files'] = model_field.allow_files if model_field.allow_folders is not False: kwargs['allow_folders'] = model_field.allow_folders if model_field.choices: kwargs['choices'] = model_field.choices else: # Ensure that max_value is passed explicitly as a keyword arg, # rather than as a validator. max_value = next(( validator.limit_value for validator in validator_kwarg if isinstance(validator, validators.MaxValueValidator) ), None) if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES): kwargs['max_value'] = max_value validator_kwarg = [ validator for validator in validator_kwarg if not isinstance(validator, validators.MaxValueValidator) ] # Ensure that min_value is passed explicitly as a keyword arg, # rather than as a validator. min_value = next(( validator.limit_value for validator in validator_kwarg if isinstance(validator, validators.MinValueValidator) ), None) if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES): kwargs['min_value'] = min_value validator_kwarg = [ validator for validator in validator_kwarg if not isinstance(validator, validators.MinValueValidator) ] # URLField does not need to include the URLValidator argument, # as it is explicitly added in. if isinstance(model_field, models.URLField): validator_kwarg = [ validator for validator in validator_kwarg if not isinstance(validator, validators.URLValidator) ] # EmailField does not need to include the validate_email argument, # as it is explicitly added in. if isinstance(model_field, models.EmailField): validator_kwarg = [ validator for validator in validator_kwarg if validator is not validators.validate_email ] # SlugField do not need to include the 'validate_slug' argument, if isinstance(model_field, models.SlugField): validator_kwarg = [ validator for validator in validator_kwarg if validator is not validators.validate_slug ] # IPAddressField do not need to include the 'validate_ipv46_address' argument, if isinstance(model_field, models.GenericIPAddressField): validator_kwarg = [ validator for validator in validator_kwarg if validator is not validators.validate_ipv46_address ] # Our decimal validation is handled in the field code, not validator code. if isinstance(model_field, models.DecimalField): validator_kwarg = [ validator for validator in validator_kwarg if not isinstance(validator, validators.DecimalValidator) ] # Ensure that max_length is passed explicitly as a keyword arg, # rather than as a validator. max_length = getattr(model_field, 'max_length', None) if max_length is not None and (isinstance(model_field, (models.CharField, models.TextField, models.FileField))): kwargs['max_length'] = max_length validator_kwarg = [ validator for validator in validator_kwarg if not isinstance(validator, validators.MaxLengthValidator) ] # Ensure that min_length is passed explicitly as a keyword arg, # rather than as a validator. min_length = next(( validator.limit_value for validator in validator_kwarg if isinstance(validator, validators.MinLengthValidator) ), None) if min_length is not None and isinstance(model_field, models.CharField): kwargs['min_length'] = min_length validator_kwarg = [ validator for validator in validator_kwarg if not isinstance(validator, validators.MinLengthValidator) ] validator_kwarg += get_unique_validators(field_name, model_field) if validator_kwarg: kwargs['validators'] = validator_kwarg return kwargs def get_relation_kwargs(field_name, relation_info): """ Creates a default instance of a flat relational field. """ model_field, related_model, to_many, to_field, has_through_model, reverse = relation_info kwargs = { 'queryset': related_model._default_manager, 'view_name': get_detail_view_name(related_model) } if to_many: kwargs['many'] = True if to_field: kwargs['to_field'] = to_field limit_choices_to = model_field and model_field.get_limit_choices_to() if limit_choices_to: if not isinstance(limit_choices_to, models.Q): limit_choices_to = models.Q(**limit_choices_to) kwargs['queryset'] = kwargs['queryset'].filter(limit_choices_to) if has_through_model: kwargs['read_only'] = True kwargs.pop('queryset', None) if model_field: if model_field.verbose_name and needs_label(model_field, field_name): kwargs['label'] = capfirst(model_field.verbose_name) help_text = model_field.help_text if help_text: kwargs['help_text'] = help_text if not model_field.editable: kwargs['read_only'] = True kwargs.pop('queryset', None) if model_field.null: kwargs['allow_null'] = True if kwargs.get('read_only', False): # If this field is read-only, then return early. # No further keyword arguments are valid. return kwargs if model_field.has_default() or model_field.blank or model_field.null: kwargs['required'] = False if model_field.validators: kwargs['validators'] = model_field.validators if getattr(model_field, 'unique', False): validator = UniqueValidator( queryset=model_field.model._default_manager, message=get_unique_error_message(model_field)) kwargs['validators'] = kwargs.get('validators', []) + [validator] if to_many and not model_field.blank: kwargs['allow_empty'] = False return kwargs def get_nested_relation_kwargs(relation_info): kwargs = {'read_only': True} if relation_info.to_many: kwargs['many'] = True return kwargs def get_url_kwargs(model_field): return { 'view_name': get_detail_view_name(model_field) } def get_unique_error_message(model_field): unique_error_message = model_field.error_messages.get('unique', None) if unique_error_message: unique_error_message = unique_error_message % { 'model_name': model_field.model._meta.verbose_name, 'field_label': model_field.verbose_name } return unique_error_message