field_mapping.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. """
  2. Helper functions for mapping model fields to a dictionary of default
  3. keyword arguments that should be used for their equivalent serializer fields.
  4. """
  5. import inspect
  6. from django.core import validators
  7. from django.db import models
  8. from django.utils.text import capfirst
  9. from rest_framework.compat import postgres_fields
  10. from rest_framework.validators import UniqueValidator
  11. NUMERIC_FIELD_TYPES = (
  12. models.IntegerField, models.FloatField, models.DecimalField, models.DurationField,
  13. )
  14. class ClassLookupDict:
  15. """
  16. Takes a dictionary with classes as keys.
  17. Lookups against this object will traverses the object's inheritance
  18. hierarchy in method resolution order, and returns the first matching value
  19. from the dictionary or raises a KeyError if nothing matches.
  20. """
  21. def __init__(self, mapping):
  22. self.mapping = mapping
  23. def __getitem__(self, key):
  24. if hasattr(key, '_proxy_class'):
  25. # Deal with proxy classes. Ie. BoundField behaves as if it
  26. # is a Field instance when using ClassLookupDict.
  27. base_class = key._proxy_class
  28. else:
  29. base_class = key.__class__
  30. for cls in inspect.getmro(base_class):
  31. if cls in self.mapping:
  32. return self.mapping[cls]
  33. raise KeyError('Class %s not found in lookup.' % base_class.__name__)
  34. def __setitem__(self, key, value):
  35. self.mapping[key] = value
  36. def needs_label(model_field, field_name):
  37. """
  38. Returns `True` if the label based on the model's verbose name
  39. is not equal to the default label it would have based on it's field name.
  40. """
  41. default_label = field_name.replace('_', ' ').capitalize()
  42. return capfirst(model_field.verbose_name) != default_label
  43. def get_detail_view_name(model):
  44. """
  45. Given a model class, return the view name to use for URL relationships
  46. that refer to instances of the model.
  47. """
  48. return '%(model_name)s-detail' % {
  49. 'model_name': model._meta.object_name.lower()
  50. }
  51. def get_field_kwargs(field_name, model_field):
  52. """
  53. Creates a default instance of a basic non-relational field.
  54. """
  55. kwargs = {}
  56. validator_kwarg = list(model_field.validators)
  57. # The following will only be used by ModelField classes.
  58. # Gets removed for everything else.
  59. kwargs['model_field'] = model_field
  60. if model_field.verbose_name and needs_label(model_field, field_name):
  61. kwargs['label'] = capfirst(model_field.verbose_name)
  62. if model_field.help_text:
  63. kwargs['help_text'] = model_field.help_text
  64. max_digits = getattr(model_field, 'max_digits', None)
  65. if max_digits is not None:
  66. kwargs['max_digits'] = max_digits
  67. decimal_places = getattr(model_field, 'decimal_places', None)
  68. if decimal_places is not None:
  69. kwargs['decimal_places'] = decimal_places
  70. if isinstance(model_field, models.SlugField):
  71. kwargs['allow_unicode'] = model_field.allow_unicode
  72. if isinstance(model_field, models.TextField) and not model_field.choices or \
  73. (postgres_fields and isinstance(model_field, postgres_fields.JSONField)) or \
  74. (hasattr(models, 'JSONField') and isinstance(model_field, models.JSONField)):
  75. kwargs['style'] = {'base_template': 'textarea.html'}
  76. if isinstance(model_field, models.AutoField) or not model_field.editable:
  77. # If this field is read-only, then return early.
  78. # Further keyword arguments are not valid.
  79. kwargs['read_only'] = True
  80. return kwargs
  81. if model_field.has_default() or model_field.blank or model_field.null:
  82. kwargs['required'] = False
  83. if model_field.null:
  84. kwargs['allow_null'] = True
  85. if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))):
  86. kwargs['allow_blank'] = True
  87. if not model_field.blank and (postgres_fields and isinstance(model_field, postgres_fields.ArrayField)):
  88. kwargs['allow_empty'] = False
  89. if isinstance(model_field, models.FilePathField):
  90. kwargs['path'] = model_field.path
  91. if model_field.match is not None:
  92. kwargs['match'] = model_field.match
  93. if model_field.recursive is not False:
  94. kwargs['recursive'] = model_field.recursive
  95. if model_field.allow_files is not True:
  96. kwargs['allow_files'] = model_field.allow_files
  97. if model_field.allow_folders is not False:
  98. kwargs['allow_folders'] = model_field.allow_folders
  99. if model_field.choices:
  100. kwargs['choices'] = model_field.choices
  101. else:
  102. # Ensure that max_value is passed explicitly as a keyword arg,
  103. # rather than as a validator.
  104. max_value = next((
  105. validator.limit_value for validator in validator_kwarg
  106. if isinstance(validator, validators.MaxValueValidator)
  107. ), None)
  108. if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
  109. kwargs['max_value'] = max_value
  110. validator_kwarg = [
  111. validator for validator in validator_kwarg
  112. if not isinstance(validator, validators.MaxValueValidator)
  113. ]
  114. # Ensure that min_value is passed explicitly as a keyword arg,
  115. # rather than as a validator.
  116. min_value = next((
  117. validator.limit_value for validator in validator_kwarg
  118. if isinstance(validator, validators.MinValueValidator)
  119. ), None)
  120. if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
  121. kwargs['min_value'] = min_value
  122. validator_kwarg = [
  123. validator for validator in validator_kwarg
  124. if not isinstance(validator, validators.MinValueValidator)
  125. ]
  126. # URLField does not need to include the URLValidator argument,
  127. # as it is explicitly added in.
  128. if isinstance(model_field, models.URLField):
  129. validator_kwarg = [
  130. validator for validator in validator_kwarg
  131. if not isinstance(validator, validators.URLValidator)
  132. ]
  133. # EmailField does not need to include the validate_email argument,
  134. # as it is explicitly added in.
  135. if isinstance(model_field, models.EmailField):
  136. validator_kwarg = [
  137. validator for validator in validator_kwarg
  138. if validator is not validators.validate_email
  139. ]
  140. # SlugField do not need to include the 'validate_slug' argument,
  141. if isinstance(model_field, models.SlugField):
  142. validator_kwarg = [
  143. validator for validator in validator_kwarg
  144. if validator is not validators.validate_slug
  145. ]
  146. # IPAddressField do not need to include the 'validate_ipv46_address' argument,
  147. if isinstance(model_field, models.GenericIPAddressField):
  148. validator_kwarg = [
  149. validator for validator in validator_kwarg
  150. if validator is not validators.validate_ipv46_address
  151. ]
  152. # Our decimal validation is handled in the field code, not validator code.
  153. if isinstance(model_field, models.DecimalField):
  154. validator_kwarg = [
  155. validator for validator in validator_kwarg
  156. if not isinstance(validator, validators.DecimalValidator)
  157. ]
  158. # Ensure that max_length is passed explicitly as a keyword arg,
  159. # rather than as a validator.
  160. max_length = getattr(model_field, 'max_length', None)
  161. if max_length is not None and (isinstance(model_field, (models.CharField, models.TextField, models.FileField))):
  162. kwargs['max_length'] = max_length
  163. validator_kwarg = [
  164. validator for validator in validator_kwarg
  165. if not isinstance(validator, validators.MaxLengthValidator)
  166. ]
  167. # Ensure that min_length is passed explicitly as a keyword arg,
  168. # rather than as a validator.
  169. min_length = next((
  170. validator.limit_value for validator in validator_kwarg
  171. if isinstance(validator, validators.MinLengthValidator)
  172. ), None)
  173. if min_length is not None and isinstance(model_field, models.CharField):
  174. kwargs['min_length'] = min_length
  175. validator_kwarg = [
  176. validator for validator in validator_kwarg
  177. if not isinstance(validator, validators.MinLengthValidator)
  178. ]
  179. if getattr(model_field, 'unique', False):
  180. unique_error_message = model_field.error_messages.get('unique', None)
  181. if unique_error_message:
  182. unique_error_message = unique_error_message % {
  183. 'model_name': model_field.model._meta.verbose_name,
  184. 'field_label': model_field.verbose_name
  185. }
  186. validator = UniqueValidator(
  187. queryset=model_field.model._default_manager,
  188. message=unique_error_message)
  189. validator_kwarg.append(validator)
  190. if validator_kwarg:
  191. kwargs['validators'] = validator_kwarg
  192. return kwargs
  193. def get_relation_kwargs(field_name, relation_info):
  194. """
  195. Creates a default instance of a flat relational field.
  196. """
  197. model_field, related_model, to_many, to_field, has_through_model, reverse = relation_info
  198. kwargs = {
  199. 'queryset': related_model._default_manager,
  200. 'view_name': get_detail_view_name(related_model)
  201. }
  202. if to_many:
  203. kwargs['many'] = True
  204. if to_field:
  205. kwargs['to_field'] = to_field
  206. limit_choices_to = model_field and model_field.get_limit_choices_to()
  207. if limit_choices_to:
  208. if not isinstance(limit_choices_to, models.Q):
  209. limit_choices_to = models.Q(**limit_choices_to)
  210. kwargs['queryset'] = kwargs['queryset'].filter(limit_choices_to)
  211. if has_through_model:
  212. kwargs['read_only'] = True
  213. kwargs.pop('queryset', None)
  214. if model_field:
  215. if model_field.verbose_name and needs_label(model_field, field_name):
  216. kwargs['label'] = capfirst(model_field.verbose_name)
  217. help_text = model_field.help_text
  218. if help_text:
  219. kwargs['help_text'] = help_text
  220. if not model_field.editable:
  221. kwargs['read_only'] = True
  222. kwargs.pop('queryset', None)
  223. if kwargs.get('read_only', False):
  224. # If this field is read-only, then return early.
  225. # No further keyword arguments are valid.
  226. return kwargs
  227. if model_field.has_default() or model_field.blank or model_field.null:
  228. kwargs['required'] = False
  229. if model_field.null:
  230. kwargs['allow_null'] = True
  231. if model_field.validators:
  232. kwargs['validators'] = model_field.validators
  233. if getattr(model_field, 'unique', False):
  234. validator = UniqueValidator(queryset=model_field.model._default_manager)
  235. kwargs['validators'] = kwargs.get('validators', []) + [validator]
  236. if to_many and not model_field.blank:
  237. kwargs['allow_empty'] = False
  238. return kwargs
  239. def get_nested_relation_kwargs(relation_info):
  240. kwargs = {'read_only': True}
  241. if relation_info.to_many:
  242. kwargs['many'] = True
  243. return kwargs
  244. def get_url_kwargs(model_field):
  245. return {
  246. 'view_name': get_detail_view_name(model_field)
  247. }