PageRenderTime 61ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/rest_framework/relations.py

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