PageRenderTime 67ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/rest_framework/relations.py

https://github.com/dekkers/django-rest-framework
Python | 646 lines | 605 code | 16 blank | 25 comment | 23 complexity | da3196e8f83f016a1a7d40c4a3750222 MD5 | raw file
  1. """
  2. Serializer fields that deal with relationships.
  3. These fields allow you to specify the style that should be used to represent
  4. model relationships, including hyperlinks, primary keys, or slugs.
  5. """
  6. from __future__ import unicode_literals
  7. from django.core.exceptions import ObjectDoesNotExist, ValidationError
  8. from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
  9. from django import forms
  10. from django.db.models.fields import BLANK_CHOICE_DASH
  11. from django.forms import widgets
  12. from django.forms.models import ModelChoiceIterator
  13. from django.utils.translation import ugettext_lazy as _
  14. from rest_framework.fields import Field, WritableField, get_component, is_simple_callable
  15. from rest_framework.reverse import reverse
  16. from rest_framework.compat import urlparse
  17. from rest_framework.compat import smart_text
  18. import warnings
  19. ##### Relational fields #####
  20. # Not actually Writable, but subclasses may need to be.
  21. class RelatedField(WritableField):
  22. """
  23. Base class for related model fields.
  24. This represents a relationship using the unicode representation of the target.
  25. """
  26. widget = widgets.Select
  27. many_widget = widgets.SelectMultiple
  28. form_field_class = forms.ChoiceField
  29. many_form_field_class = forms.MultipleChoiceField
  30. null_values = (None, '', 'None')
  31. cache_choices = False
  32. empty_label = None
  33. read_only = True
  34. many = False
  35. def __init__(self, *args, **kwargs):
  36. # 'null' is to be deprecated in favor of 'required'
  37. if 'null' in kwargs:
  38. warnings.warn('The `null` keyword argument is deprecated. '
  39. 'Use the `required` keyword argument instead.',
  40. DeprecationWarning, stacklevel=2)
  41. kwargs['required'] = not kwargs.pop('null')
  42. queryset = kwargs.pop('queryset', None)
  43. self.many = kwargs.pop('many', self.many)
  44. if self.many:
  45. self.widget = self.many_widget
  46. self.form_field_class = self.many_form_field_class
  47. kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
  48. super(RelatedField, self).__init__(*args, **kwargs)
  49. if not self.required:
  50. # Accessed in ModelChoiceIterator django/forms/models.py:1034
  51. # If set adds empty choice.
  52. self.empty_label = BLANK_CHOICE_DASH[0][1]
  53. self.queryset = queryset
  54. def initialize(self, parent, field_name):
  55. super(RelatedField, self).initialize(parent, field_name)
  56. if self.queryset is None and not self.read_only:
  57. manager = getattr(self.parent.opts.model, self.source or field_name)
  58. if hasattr(manager, 'related'): # Forward
  59. self.queryset = manager.related.model._default_manager.all()
  60. else: # Reverse
  61. self.queryset = manager.field.rel.to._default_manager.all()
  62. ### We need this stuff to make form choices work...
  63. def prepare_value(self, obj):
  64. return self.to_native(obj)
  65. def label_from_instance(self, obj):
  66. """
  67. Return a readable representation for use with eg. select widgets.
  68. """
  69. desc = smart_text(obj)
  70. ident = smart_text(self.to_native(obj))
  71. if desc == ident:
  72. return desc
  73. return "%s - %s" % (desc, ident)
  74. def _get_queryset(self):
  75. return self._queryset
  76. def _set_queryset(self, queryset):
  77. self._queryset = queryset
  78. self.widget.choices = self.choices
  79. queryset = property(_get_queryset, _set_queryset)
  80. def _get_choices(self):
  81. # If self._choices is set, then somebody must have manually set
  82. # the property self.choices. In this case, just return self._choices.
  83. if hasattr(self, '_choices'):
  84. return self._choices
  85. # Otherwise, execute the QuerySet in self.queryset to determine the
  86. # choices dynamically. Return a fresh ModelChoiceIterator that has not been
  87. # consumed. Note that we're instantiating a new ModelChoiceIterator *each*
  88. # time _get_choices() is called (and, thus, each time self.choices is
  89. # accessed) so that we can ensure the QuerySet has not been consumed. This
  90. # construct might look complicated but it allows for lazy evaluation of
  91. # the queryset.
  92. return ModelChoiceIterator(self)
  93. def _set_choices(self, value):
  94. # Setting choices also sets the choices on the widget.
  95. # choices can be any iterable, but we call list() on it because
  96. # it will be consumed more than once.
  97. self._choices = self.widget.choices = list(value)
  98. choices = property(_get_choices, _set_choices)
  99. ### Default value handling
  100. def get_default_value(self):
  101. default = super(RelatedField, self).get_default_value()
  102. if self.many and default is None:
  103. return []
  104. return default
  105. ### Regular serializer stuff...
  106. def field_to_native(self, obj, field_name):
  107. try:
  108. if self.source == '*':
  109. return self.to_native(obj)
  110. source = self.source or field_name
  111. value = obj
  112. for component in source.split('.'):
  113. if value is None:
  114. break
  115. value = get_component(value, component)
  116. except ObjectDoesNotExist:
  117. return None
  118. if value is None:
  119. return None
  120. if self.many:
  121. if is_simple_callable(getattr(value, 'all', None)):
  122. return [self.to_native(item) for item in value.all()]
  123. else:
  124. # Also support non-queryset iterables.
  125. # This allows us to also support plain lists of related items.
  126. return [self.to_native(item) for item in value]
  127. return self.to_native(value)
  128. def field_from_native(self, data, files, field_name, into):
  129. if self.read_only:
  130. return
  131. try:
  132. if self.many:
  133. try:
  134. # Form data
  135. value = data.getlist(field_name)
  136. if value == [''] or value == []:
  137. raise KeyError
  138. except AttributeError:
  139. # Non-form data
  140. value = data[field_name]
  141. else:
  142. value = data[field_name]
  143. except KeyError:
  144. if self.partial:
  145. return
  146. value = self.get_default_value()
  147. if value in self.null_values:
  148. if self.required:
  149. raise ValidationError(self.error_messages['required'])
  150. into[(self.source or field_name)] = None
  151. elif self.many:
  152. into[(self.source or field_name)] = [self.from_native(item) for item in value]
  153. else:
  154. into[(self.source or field_name)] = self.from_native(value)
  155. ### PrimaryKey relationships
  156. class PrimaryKeyRelatedField(RelatedField):
  157. """
  158. Represents a relationship as a pk value.
  159. """
  160. read_only = False
  161. default_error_messages = {
  162. 'does_not_exist': _("Invalid pk '%s' - object does not exist."),
  163. 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'),
  164. }
  165. # TODO: Remove these field hacks...
  166. def prepare_value(self, obj):
  167. return self.to_native(obj.pk)
  168. def label_from_instance(self, obj):
  169. """
  170. Return a readable representation for use with eg. select widgets.
  171. """
  172. desc = smart_text(obj)
  173. ident = smart_text(self.to_native(obj.pk))
  174. if desc == ident:
  175. return desc
  176. return "%s - %s" % (desc, ident)
  177. # TODO: Possibly change this to just take `obj`, through prob less performant
  178. def to_native(self, pk):
  179. return pk
  180. def from_native(self, data):
  181. if self.queryset is None:
  182. raise Exception('Writable related fields must include a `queryset` argument')
  183. try:
  184. return self.queryset.get(pk=data)
  185. except ObjectDoesNotExist:
  186. msg = self.error_messages['does_not_exist'] % smart_text(data)
  187. raise ValidationError(msg)
  188. except (TypeError, ValueError):
  189. received = type(data).__name__
  190. msg = self.error_messages['incorrect_type'] % received
  191. raise ValidationError(msg)
  192. def field_to_native(self, obj, field_name):
  193. if self.many:
  194. # To-many relationship
  195. queryset = None
  196. if not self.source:
  197. # Prefer obj.serializable_value for performance reasons
  198. try:
  199. queryset = obj.serializable_value(field_name)
  200. except AttributeError:
  201. pass
  202. if queryset is None:
  203. # RelatedManager (reverse relationship)
  204. source = self.source or field_name
  205. queryset = obj
  206. for component in source.split('.'):
  207. if queryset is None:
  208. return []
  209. queryset = get_component(queryset, component)
  210. # Forward relationship
  211. if is_simple_callable(getattr(queryset, 'all', None)):
  212. return [self.to_native(item.pk) for item in queryset.all()]
  213. else:
  214. # Also support non-queryset iterables.
  215. # This allows us to also support plain lists of related items.
  216. return [self.to_native(item.pk) for item in queryset]
  217. # To-one relationship
  218. try:
  219. # Prefer obj.serializable_value for performance reasons
  220. pk = obj.serializable_value(self.source or field_name)
  221. except AttributeError:
  222. # RelatedObject (reverse relationship)
  223. try:
  224. pk = getattr(obj, self.source or field_name).pk
  225. except (ObjectDoesNotExist, AttributeError):
  226. return None
  227. # Forward relationship
  228. return self.to_native(pk)
  229. ### Slug relationships
  230. class SlugRelatedField(RelatedField):
  231. """
  232. Represents a relationship using a unique field on the target.
  233. """
  234. read_only = False
  235. default_error_messages = {
  236. 'does_not_exist': _("Object with %s=%s does not exist."),
  237. 'invalid': _('Invalid value.'),
  238. }
  239. def __init__(self, *args, **kwargs):
  240. self.slug_field = kwargs.pop('slug_field', None)
  241. assert self.slug_field, 'slug_field is required'
  242. super(SlugRelatedField, self).__init__(*args, **kwargs)
  243. def to_native(self, obj):
  244. return getattr(obj, self.slug_field)
  245. def from_native(self, data):
  246. if self.queryset is None:
  247. raise Exception('Writable related fields must include a `queryset` argument')
  248. try:
  249. return self.queryset.get(**{self.slug_field: data})
  250. except ObjectDoesNotExist:
  251. raise ValidationError(self.error_messages['does_not_exist'] %
  252. (self.slug_field, smart_text(data)))
  253. except (TypeError, ValueError):
  254. msg = self.error_messages['invalid']
  255. raise ValidationError(msg)
  256. ### Hyperlinked relationships
  257. class HyperlinkedRelatedField(RelatedField):
  258. """
  259. Represents a relationship using hyperlinking.
  260. """
  261. read_only = False
  262. lookup_field = 'pk'
  263. default_error_messages = {
  264. 'no_match': _('Invalid hyperlink - No URL match'),
  265. 'incorrect_match': _('Invalid hyperlink - Incorrect URL match'),
  266. 'configuration_error': _('Invalid hyperlink due to configuration error'),
  267. 'does_not_exist': _("Invalid hyperlink - object does not exist."),
  268. 'incorrect_type': _('Incorrect type. Expected url string, received %s.'),
  269. }
  270. # These are all pending deprecation
  271. pk_url_kwarg = 'pk'
  272. slug_field = 'slug'
  273. slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
  274. def __init__(self, *args, **kwargs):
  275. try:
  276. self.view_name = kwargs.pop('view_name')
  277. except KeyError:
  278. raise ValueError("Hyperlinked field requires 'view_name' kwarg")
  279. self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
  280. self.format = kwargs.pop('format', None)
  281. # These are pending deprecation
  282. if 'pk_url_kwarg' in kwargs:
  283. msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.'
  284. warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
  285. if 'slug_url_kwarg' in kwargs:
  286. msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.'
  287. warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
  288. if 'slug_field' in kwargs:
  289. msg = 'slug_field is pending deprecation. Use lookup_field instead.'
  290. warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
  291. self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
  292. self.slug_field = kwargs.pop('slug_field', self.slug_field)
  293. default_slug_kwarg = self.slug_url_kwarg or self.slug_field
  294. self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
  295. super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
  296. def get_url(self, obj, view_name, request, format):
  297. """
  298. Given an object, return the URL that hyperlinks to the object.
  299. May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
  300. attributes are not configured to correctly match the URL conf.
  301. """
  302. lookup_field = getattr(obj, self.lookup_field)
  303. kwargs = {self.lookup_field: lookup_field}
  304. try:
  305. return reverse(view_name, kwargs=kwargs, request=request, format=format)
  306. except NoReverseMatch:
  307. pass
  308. if self.pk_url_kwarg != 'pk':
  309. # Only try pk if it has been explicitly set.
  310. # Otherwise, the default `lookup_field = 'pk'` has us covered.
  311. pk = obj.pk
  312. kwargs = {self.pk_url_kwarg: pk}
  313. try:
  314. return reverse(view_name, kwargs=kwargs, request=request, format=format)
  315. except NoReverseMatch:
  316. pass
  317. slug = getattr(obj, self.slug_field, None)
  318. if slug is not None:
  319. # Only try slug if it corresponds to an attribute on the object.
  320. kwargs = {self.slug_url_kwarg: slug}
  321. try:
  322. ret = reverse(view_name, kwargs=kwargs, request=request, format=format)
  323. if self.slug_field == 'slug' and self.slug_url_kwarg == 'slug':
  324. # If the lookup succeeds using the default slug params,
  325. # then `slug_field` is being used implicitly, and we
  326. # we need to warn about the pending deprecation.
  327. msg = 'Implicit slug field hyperlinked fields are pending deprecation.' \
  328. 'You should set `lookup_field=slug` on the HyperlinkedRelatedField.'
  329. warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
  330. return ret
  331. except NoReverseMatch:
  332. pass
  333. raise NoReverseMatch()
  334. def get_object(self, queryset, view_name, view_args, view_kwargs):
  335. """
  336. Return the object corresponding to a matched URL.
  337. Takes the matched URL conf arguments, and the queryset, and should
  338. return an object instance, or raise an `ObjectDoesNotExist` exception.
  339. """
  340. lookup = view_kwargs.get(self.lookup_field, None)
  341. pk = view_kwargs.get(self.pk_url_kwarg, None)
  342. slug = view_kwargs.get(self.slug_url_kwarg, None)
  343. if lookup is not None:
  344. filter_kwargs = {self.lookup_field: lookup}
  345. elif pk is not None:
  346. filter_kwargs = {'pk': pk}
  347. elif slug is not None:
  348. filter_kwargs = {self.slug_field: slug}
  349. else:
  350. raise ObjectDoesNotExist()
  351. return queryset.get(**filter_kwargs)
  352. def to_native(self, obj):
  353. view_name = self.view_name
  354. request = self.context.get('request', None)
  355. format = self.format or self.context.get('format', None)
  356. if request is None:
  357. msg = (
  358. "Using `HyperlinkedRelatedField` without including the request "
  359. "in the serializer context is deprecated. "
  360. "Add `context={'request': request}` when instantiating "
  361. "the serializer."
  362. )
  363. warnings.warn(msg, DeprecationWarning, stacklevel=4)
  364. # If the object has not yet been saved then we cannot hyperlink to it.
  365. if getattr(obj, 'pk', None) is None:
  366. return
  367. # Return the hyperlink, or error if incorrectly configured.
  368. try:
  369. return self.get_url(obj, view_name, request, format)
  370. except NoReverseMatch:
  371. msg = (
  372. 'Could not resolve URL for hyperlinked relationship using '
  373. 'view name "%s". You may have failed to include the related '
  374. 'model in your API, or incorrectly configured the '
  375. '`lookup_field` attribute on this field.'
  376. )
  377. raise Exception(msg % view_name)
  378. def from_native(self, value):
  379. # Convert URL -> model instance pk
  380. # TODO: Use values_list
  381. queryset = self.queryset
  382. if queryset is None:
  383. raise Exception('Writable related fields must include a `queryset` argument')
  384. try:
  385. http_prefix = value.startswith(('http:', 'https:'))
  386. except AttributeError:
  387. msg = self.error_messages['incorrect_type']
  388. raise ValidationError(msg % type(value).__name__)
  389. if http_prefix:
  390. # If needed convert absolute URLs to relative path
  391. value = urlparse.urlparse(value).path
  392. prefix = get_script_prefix()
  393. if value.startswith(prefix):
  394. value = '/' + value[len(prefix):]
  395. try:
  396. match = resolve(value)
  397. except Exception:
  398. raise ValidationError(self.error_messages['no_match'])
  399. if match.view_name != self.view_name:
  400. raise ValidationError(self.error_messages['incorrect_match'])
  401. try:
  402. return self.get_object(queryset, match.view_name,
  403. match.args, match.kwargs)
  404. except (ObjectDoesNotExist, TypeError, ValueError):
  405. raise ValidationError(self.error_messages['does_not_exist'])
  406. class HyperlinkedIdentityField(Field):
  407. """
  408. Represents the instance, or a property on the instance, using hyperlinking.
  409. """
  410. lookup_field = 'pk'
  411. read_only = True
  412. # These are all pending deprecation
  413. pk_url_kwarg = 'pk'
  414. slug_field = 'slug'
  415. slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
  416. def __init__(self, *args, **kwargs):
  417. try:
  418. self.view_name = kwargs.pop('view_name')
  419. except KeyError:
  420. msg = "HyperlinkedIdentityField requires 'view_name' argument"
  421. raise ValueError(msg)
  422. self.format = kwargs.pop('format', None)
  423. lookup_field = kwargs.pop('lookup_field', None)
  424. self.lookup_field = lookup_field or self.lookup_field
  425. # These are pending deprecation
  426. if 'pk_url_kwarg' in kwargs:
  427. msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.'
  428. warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
  429. if 'slug_url_kwarg' in kwargs:
  430. msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.'
  431. warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
  432. if 'slug_field' in kwargs:
  433. msg = 'slug_field is pending deprecation. Use lookup_field instead.'
  434. warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
  435. self.slug_field = kwargs.pop('slug_field', self.slug_field)
  436. default_slug_kwarg = self.slug_url_kwarg or self.slug_field
  437. self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
  438. self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
  439. super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
  440. def field_to_native(self, obj, field_name):
  441. request = self.context.get('request', None)
  442. format = self.context.get('format', None)
  443. view_name = self.view_name
  444. if request is None:
  445. warnings.warn("Using `HyperlinkedIdentityField` without including the "
  446. "request in the serializer context is deprecated. "
  447. "Add `context={'request': request}` when instantiating the serializer.",
  448. DeprecationWarning, stacklevel=4)
  449. # By default use whatever format is given for the current context
  450. # unless the target is a different type to the source.
  451. #
  452. # Eg. Consider a HyperlinkedIdentityField pointing from a json
  453. # representation to an html property of that representation...
  454. #
  455. # '/snippets/1/' should link to '/snippets/1/highlight/'
  456. # ...but...
  457. # '/snippets/1/.json' should link to '/snippets/1/highlight/.html'
  458. if format and self.format and self.format != format:
  459. format = self.format
  460. # Return the hyperlink, or error if incorrectly configured.
  461. try:
  462. return self.get_url(obj, view_name, request, format)
  463. except NoReverseMatch:
  464. msg = (
  465. 'Could not resolve URL for hyperlinked relationship using '
  466. 'view name "%s". You may have failed to include the related '
  467. 'model in your API, or incorrectly configured the '
  468. '`lookup_field` attribute on this field.'
  469. )
  470. raise Exception(msg % view_name)
  471. def get_url(self, obj, view_name, request, format):
  472. """
  473. Given an object, return the URL that hyperlinks to the object.
  474. May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
  475. attributes are not configured to correctly match the URL conf.
  476. """
  477. lookup_field = getattr(obj, self.lookup_field, None)
  478. kwargs = {self.lookup_field: lookup_field}
  479. # Handle unsaved object case
  480. if lookup_field is None:
  481. return None
  482. try:
  483. return reverse(view_name, kwargs=kwargs, request=request, format=format)
  484. except NoReverseMatch:
  485. pass
  486. if self.pk_url_kwarg != 'pk':
  487. # Only try pk lookup if it has been explicitly set.
  488. # Otherwise, the default `lookup_field = 'pk'` has us covered.
  489. kwargs = {self.pk_url_kwarg: obj.pk}
  490. try:
  491. return reverse(view_name, kwargs=kwargs, request=request, format=format)
  492. except NoReverseMatch:
  493. pass
  494. slug = getattr(obj, self.slug_field, None)
  495. if slug:
  496. # Only use slug lookup if a slug field exists on the model
  497. kwargs = {self.slug_url_kwarg: slug}
  498. try:
  499. return reverse(view_name, kwargs=kwargs, request=request, format=format)
  500. except NoReverseMatch:
  501. pass
  502. raise NoReverseMatch()
  503. ### Old-style many classes for backwards compat
  504. class ManyRelatedField(RelatedField):
  505. def __init__(self, *args, **kwargs):
  506. warnings.warn('`ManyRelatedField()` is deprecated. '
  507. 'Use `RelatedField(many=True)` instead.',
  508. DeprecationWarning, stacklevel=2)
  509. kwargs['many'] = True
  510. super(ManyRelatedField, self).__init__(*args, **kwargs)
  511. class ManyPrimaryKeyRelatedField(PrimaryKeyRelatedField):
  512. def __init__(self, *args, **kwargs):
  513. warnings.warn('`ManyPrimaryKeyRelatedField()` is deprecated. '
  514. 'Use `PrimaryKeyRelatedField(many=True)` instead.',
  515. DeprecationWarning, stacklevel=2)
  516. kwargs['many'] = True
  517. super(ManyPrimaryKeyRelatedField, self).__init__(*args, **kwargs)
  518. class ManySlugRelatedField(SlugRelatedField):
  519. def __init__(self, *args, **kwargs):
  520. warnings.warn('`ManySlugRelatedField()` is deprecated. '
  521. 'Use `SlugRelatedField(many=True)` instead.',
  522. DeprecationWarning, stacklevel=2)
  523. kwargs['many'] = True
  524. super(ManySlugRelatedField, self).__init__(*args, **kwargs)
  525. class ManyHyperlinkedRelatedField(HyperlinkedRelatedField):
  526. def __init__(self, *args, **kwargs):
  527. warnings.warn('`ManyHyperlinkedRelatedField()` is deprecated. '
  528. 'Use `HyperlinkedRelatedField(many=True)` instead.',
  529. DeprecationWarning, stacklevel=2)
  530. kwargs['many'] = True
  531. super(ManyHyperlinkedRelatedField, self).__init__(*args, **kwargs)