655 lines
25 KiB
Python
655 lines
25 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (C) 2014-2017 Andrey Antukh <niwi@niwi.nz>
|
|
# Copyright (C) 2014-2017 Jesús Espino <jespinog@gmail.com>
|
|
# Copyright (C) 2014-2017 David Barragán <bameda@dbarragan.com>
|
|
# Copyright (C) 2014-2017 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
# The code is partially taken (and modified) from django rest framework
|
|
# that is licensed under the following terms:
|
|
#
|
|
# Copyright (c) 2011-2014, Tom Christie
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
#
|
|
# Redistributions of source code must retain the above copyright notice, this
|
|
# list of conditions and the following disclaimer.
|
|
# Redistributions in binary form must reproduce the above copyright notice, this
|
|
# list of conditions and the following disclaimer in the documentation and/or
|
|
# other materials provided with the distribution.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
"""
|
|
Serializer fields that deal with relationships.
|
|
|
|
These fields allow you to specify the style that should be used to represent
|
|
model relationships, including hyperlinks, primary keys, or slugs.
|
|
"""
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
|
|
from django import forms
|
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
|
from django.forms import widgets
|
|
from django.forms.models import ModelChoiceIterator
|
|
from django.utils.encoding import smart_text
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from .fields import Field, WritableField, get_component, is_simple_callable
|
|
from .reverse import reverse
|
|
from taiga.base.exceptions import ValidationError
|
|
|
|
import warnings
|
|
from urllib import parse as urlparse
|
|
|
|
|
|
|
|
|
|
##### Relational fields #####
|
|
|
|
|
|
# Not actually Writable, but subclasses may need to be.
|
|
class RelatedField(WritableField):
|
|
"""
|
|
Base class for related model fields.
|
|
|
|
This represents a relationship using the unicode representation of the target.
|
|
"""
|
|
widget = widgets.Select
|
|
many_widget = widgets.SelectMultiple
|
|
form_field_class = forms.ChoiceField
|
|
many_form_field_class = forms.MultipleChoiceField
|
|
null_values = (None, "", "None")
|
|
|
|
cache_choices = False
|
|
empty_label = None
|
|
read_only = True
|
|
many = False
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
# "null" is to be deprecated in favor of "required"
|
|
if "null" in kwargs:
|
|
warnings.warn("The `null` keyword argument is deprecated. "
|
|
"Use the `required` keyword argument instead.",
|
|
DeprecationWarning, stacklevel=2)
|
|
kwargs["required"] = not kwargs.pop("null")
|
|
|
|
queryset = kwargs.pop("queryset", None)
|
|
self.many = kwargs.pop("many", self.many)
|
|
if self.many:
|
|
self.widget = self.many_widget
|
|
self.form_field_class = self.many_form_field_class
|
|
|
|
kwargs["read_only"] = kwargs.pop("read_only", self.read_only)
|
|
super(RelatedField, self).__init__(*args, **kwargs)
|
|
|
|
if not self.required:
|
|
self.empty_label = BLANK_CHOICE_DASH[0][1]
|
|
|
|
self.queryset = queryset
|
|
|
|
def initialize(self, parent, field_name):
|
|
super(RelatedField, self).initialize(parent, field_name)
|
|
if self.queryset is None and not self.read_only:
|
|
manager = getattr(self.parent.opts.model, self.source or field_name)
|
|
if hasattr(manager, "related"): # Forward
|
|
self.queryset = manager.related.model._default_manager.all()
|
|
else: # Reverse
|
|
self.queryset = manager.field.rel.to._default_manager.all()
|
|
|
|
### We need this stuff to make form choices work...
|
|
|
|
def prepare_value(self, obj):
|
|
return self.to_native(obj)
|
|
|
|
def label_from_instance(self, obj):
|
|
"""
|
|
Return a readable representation for use with eg. select widgets.
|
|
"""
|
|
desc = smart_text(obj)
|
|
ident = smart_text(self.to_native(obj))
|
|
if desc == ident:
|
|
return desc
|
|
return "%s - %s" % (desc, ident)
|
|
|
|
def _get_queryset(self):
|
|
return self._queryset
|
|
|
|
def _set_queryset(self, queryset):
|
|
self._queryset = queryset
|
|
self.widget.choices = self.choices
|
|
|
|
queryset = property(_get_queryset, _set_queryset)
|
|
|
|
def _get_choices(self):
|
|
# If self._choices is set, then somebody must have manually set
|
|
# the property self.choices. In this case, just return self._choices.
|
|
if hasattr(self, "_choices"):
|
|
return self._choices
|
|
|
|
# Otherwise, execute the QuerySet in self.queryset to determine the
|
|
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
|
|
# consumed. Note that we"re instantiating a new ModelChoiceIterator *each*
|
|
# time _get_choices() is called (and, thus, each time self.choices is
|
|
# accessed) so that we can ensure the QuerySet has not been consumed. This
|
|
# construct might look complicated but it allows for lazy evaluation of
|
|
# the queryset.
|
|
return ModelChoiceIterator(self)
|
|
|
|
def _set_choices(self, value):
|
|
# Setting choices also sets the choices on the widget.
|
|
# choices can be any iterable, but we call list() on it because
|
|
# it will be consumed more than once.
|
|
self._choices = self.widget.choices = list(value)
|
|
|
|
choices = property(_get_choices, _set_choices)
|
|
|
|
### Default value handling
|
|
|
|
def get_default_value(self):
|
|
default = super(RelatedField, self).get_default_value()
|
|
if self.many and default is None:
|
|
return []
|
|
return default
|
|
|
|
### Regular serializer stuff...
|
|
|
|
def field_to_native(self, obj, field_name):
|
|
try:
|
|
if self.source == "*":
|
|
return self.to_native(obj)
|
|
|
|
source = self.source or field_name
|
|
value = obj
|
|
|
|
for component in source.split("."):
|
|
if value is None:
|
|
break
|
|
value = get_component(value, component)
|
|
except ObjectDoesNotExist:
|
|
return None
|
|
|
|
if value is None:
|
|
return None
|
|
|
|
if self.many:
|
|
if is_simple_callable(getattr(value, "all", None)):
|
|
return [self.to_native(item) for item in value.all()]
|
|
else:
|
|
# Also support non-queryset iterables.
|
|
# This allows us to also support plain lists of related items.
|
|
return [self.to_native(item) for item in value]
|
|
return self.to_native(value)
|
|
|
|
def field_from_native(self, data, files, field_name, into):
|
|
if self.read_only:
|
|
return
|
|
|
|
try:
|
|
if self.many:
|
|
try:
|
|
# Form data
|
|
value = data.getlist(field_name)
|
|
if value == [""] or value == []:
|
|
raise KeyError
|
|
except AttributeError:
|
|
# Non-form data
|
|
value = data[field_name]
|
|
else:
|
|
value = data[field_name]
|
|
except KeyError:
|
|
if self.partial:
|
|
return
|
|
value = self.get_default_value()
|
|
|
|
if value in self.null_values:
|
|
if self.required:
|
|
raise ValidationError(self.error_messages["required"])
|
|
into[(self.source or field_name)] = None
|
|
elif self.many:
|
|
into[(self.source or field_name)] = [self.from_native(item) for item in value]
|
|
else:
|
|
into[(self.source or field_name)] = self.from_native(value)
|
|
|
|
|
|
### PrimaryKey relationships
|
|
|
|
class PrimaryKeyRelatedField(RelatedField):
|
|
"""
|
|
Represents a relationship as a pk value.
|
|
"""
|
|
read_only = False
|
|
|
|
default_error_messages = {
|
|
"does_not_exist": _("Invalid pk '%s' - object does not exist."),
|
|
"incorrect_type": _("Incorrect type. Expected pk value, received %s."),
|
|
}
|
|
|
|
# TODO: Remove these field hacks...
|
|
def prepare_value(self, obj):
|
|
return self.to_native(obj.pk)
|
|
|
|
def label_from_instance(self, obj):
|
|
"""
|
|
Return a readable representation for use with eg. select widgets.
|
|
"""
|
|
desc = smart_text(obj)
|
|
ident = smart_text(self.to_native(obj.pk))
|
|
if desc == ident:
|
|
return desc
|
|
return "%s - %s" % (desc, ident)
|
|
|
|
# TODO: Possibly change this to just take `obj`, through prob less performant
|
|
def to_native(self, pk):
|
|
return pk
|
|
|
|
def from_native(self, data):
|
|
if self.queryset is None:
|
|
raise Exception("Writable related fields must include a `queryset` argument")
|
|
|
|
try:
|
|
return self.queryset.get(pk=data)
|
|
except ObjectDoesNotExist:
|
|
msg = self.error_messages["does_not_exist"] % smart_text(data)
|
|
raise ValidationError(msg)
|
|
except (TypeError, ValueError):
|
|
received = type(data).__name__
|
|
msg = self.error_messages["incorrect_type"] % received
|
|
raise ValidationError(msg)
|
|
|
|
def field_to_native(self, obj, field_name):
|
|
if self.many:
|
|
# To-many relationship
|
|
|
|
queryset = None
|
|
if not self.source:
|
|
# Prefer obj.serializable_value for performance reasons
|
|
try:
|
|
queryset = obj.serializable_value(field_name)
|
|
except AttributeError:
|
|
pass
|
|
if queryset is None:
|
|
# RelatedManager (reverse relationship)
|
|
source = self.source or field_name
|
|
queryset = obj
|
|
for component in source.split("."):
|
|
if queryset is None:
|
|
return []
|
|
queryset = get_component(queryset, component)
|
|
|
|
# Forward relationship
|
|
if is_simple_callable(getattr(queryset, "all", None)):
|
|
return [self.to_native(item.pk) for item in queryset.all()]
|
|
else:
|
|
# Also support non-queryset iterables.
|
|
# This allows us to also support plain lists of related items.
|
|
return [self.to_native(item.pk) for item in queryset]
|
|
|
|
# To-one relationship
|
|
try:
|
|
# Prefer obj.serializable_value for performance reasons
|
|
pk = obj.serializable_value(self.source or field_name)
|
|
except AttributeError:
|
|
# RelatedObject (reverse relationship)
|
|
try:
|
|
pk = getattr(obj, self.source or field_name).pk
|
|
except (ObjectDoesNotExist, AttributeError):
|
|
return None
|
|
|
|
# Forward relationship
|
|
return self.to_native(pk)
|
|
|
|
|
|
### Slug relationships
|
|
|
|
|
|
class SlugRelatedField(RelatedField):
|
|
"""
|
|
Represents a relationship using a unique field on the target.
|
|
"""
|
|
read_only = False
|
|
|
|
default_error_messages = {
|
|
"does_not_exist": _("Object with %s=%s does not exist."),
|
|
"invalid": _("Invalid value."),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.slug_field = kwargs.pop("slug_field", None)
|
|
assert self.slug_field, "slug_field is required"
|
|
super(SlugRelatedField, self).__init__(*args, **kwargs)
|
|
|
|
def to_native(self, obj):
|
|
return getattr(obj, self.slug_field)
|
|
|
|
def from_native(self, data):
|
|
if self.queryset is None:
|
|
raise Exception("Writable related fields must include a `queryset` argument")
|
|
|
|
try:
|
|
return self.queryset.get(**{self.slug_field: data})
|
|
except ObjectDoesNotExist:
|
|
raise ValidationError(self.error_messages["does_not_exist"] %
|
|
(self.slug_field, smart_text(data)))
|
|
except (TypeError, ValueError):
|
|
msg = self.error_messages["invalid"]
|
|
raise ValidationError(msg)
|
|
|
|
|
|
### Hyperlinked relationships
|
|
|
|
class HyperlinkedRelatedField(RelatedField):
|
|
"""
|
|
Represents a relationship using hyperlinking.
|
|
"""
|
|
read_only = False
|
|
lookup_field = "pk"
|
|
|
|
default_error_messages = {
|
|
"no_match": _("Invalid hyperlink - No URL match"),
|
|
"incorrect_match": _("Invalid hyperlink - Incorrect URL match"),
|
|
"configuration_error": _("Invalid hyperlink due to configuration error"),
|
|
"does_not_exist": _("Invalid hyperlink - object does not exist."),
|
|
"incorrect_type": _("Incorrect type. Expected url string, received %s."),
|
|
}
|
|
|
|
# These are all pending deprecation
|
|
pk_url_kwarg = "pk"
|
|
slug_field = "slug"
|
|
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
try:
|
|
self.view_name = kwargs.pop("view_name")
|
|
except KeyError:
|
|
raise ValueError("Hyperlinked field requires \"view_name\" kwarg")
|
|
|
|
self.lookup_field = kwargs.pop("lookup_field", self.lookup_field)
|
|
self.format = kwargs.pop("format", None)
|
|
|
|
# These are pending deprecation
|
|
if "pk_url_kwarg" in kwargs:
|
|
msg = "pk_url_kwarg is pending deprecation. Use lookup_field instead."
|
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
|
if "slug_url_kwarg" in kwargs:
|
|
msg = "slug_url_kwarg is pending deprecation. Use lookup_field instead."
|
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
|
if "slug_field" in kwargs:
|
|
msg = "slug_field is pending deprecation. Use lookup_field instead."
|
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
|
|
|
self.pk_url_kwarg = kwargs.pop("pk_url_kwarg", self.pk_url_kwarg)
|
|
self.slug_field = kwargs.pop("slug_field", self.slug_field)
|
|
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
|
self.slug_url_kwarg = kwargs.pop("slug_url_kwarg", default_slug_kwarg)
|
|
|
|
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
|
|
|
|
def get_url(self, obj, view_name, request, format):
|
|
"""
|
|
Given an object, return the URL that hyperlinks to the object.
|
|
|
|
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
|
attributes are not configured to correctly match the URL conf.
|
|
"""
|
|
lookup_field = getattr(obj, self.lookup_field)
|
|
kwargs = {self.lookup_field: lookup_field}
|
|
try:
|
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
except NoReverseMatch:
|
|
pass
|
|
|
|
if self.pk_url_kwarg != "pk":
|
|
# Only try pk if it has been explicitly set.
|
|
# Otherwise, the default `lookup_field = "pk"` has us covered.
|
|
pk = obj.pk
|
|
kwargs = {self.pk_url_kwarg: pk}
|
|
try:
|
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
except NoReverseMatch:
|
|
pass
|
|
|
|
slug = getattr(obj, self.slug_field, None)
|
|
if slug is not None:
|
|
# Only try slug if it corresponds to an attribute on the object.
|
|
kwargs = {self.slug_url_kwarg: slug}
|
|
try:
|
|
ret = reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
if self.slug_field == "slug" and self.slug_url_kwarg == "slug":
|
|
# If the lookup succeeds using the default slug params,
|
|
# then `slug_field` is being used implicitly, and we
|
|
# we need to warn about the pending deprecation.
|
|
msg = "Implicit slug field hyperlinked fields are pending deprecation." \
|
|
"You should set `lookup_field=slug` on the HyperlinkedRelatedField."
|
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
|
return ret
|
|
except NoReverseMatch:
|
|
pass
|
|
|
|
raise NoReverseMatch()
|
|
|
|
def get_object(self, queryset, view_name, view_args, view_kwargs):
|
|
"""
|
|
Return the object corresponding to a matched URL.
|
|
|
|
Takes the matched URL conf arguments, and the queryset, and should
|
|
return an object instance, or raise an `ObjectDoesNotExist` exception.
|
|
"""
|
|
lookup = view_kwargs.get(self.lookup_field, None)
|
|
pk = view_kwargs.get(self.pk_url_kwarg, None)
|
|
slug = view_kwargs.get(self.slug_url_kwarg, None)
|
|
|
|
if lookup is not None:
|
|
filter_kwargs = {self.lookup_field: lookup}
|
|
elif pk is not None:
|
|
filter_kwargs = {"pk": pk}
|
|
elif slug is not None:
|
|
filter_kwargs = {self.slug_field: slug}
|
|
else:
|
|
raise ObjectDoesNotExist()
|
|
|
|
return queryset.get(**filter_kwargs)
|
|
|
|
def to_native(self, obj):
|
|
view_name = self.view_name
|
|
request = self.context.get("request", None)
|
|
format = self.format or self.context.get("format", None)
|
|
|
|
if request is None:
|
|
msg = (
|
|
"Using `HyperlinkedRelatedField` without including the request "
|
|
"in the serializer context is deprecated. "
|
|
"Add `context={'request': request}` when instantiating "
|
|
"the serializer."
|
|
)
|
|
warnings.warn(msg, DeprecationWarning, stacklevel=4)
|
|
|
|
# If the object has not yet been saved then we cannot hyperlink to it.
|
|
if getattr(obj, "pk", None) is None:
|
|
return
|
|
|
|
# Return the hyperlink, or error if incorrectly configured.
|
|
try:
|
|
return self.get_url(obj, view_name, request, format)
|
|
except NoReverseMatch:
|
|
msg = (
|
|
"Could not resolve URL for hyperlinked relationship using "
|
|
"view name '%s'. You may have failed to include the related "
|
|
"model in your API, or incorrectly configured the "
|
|
"`lookup_field` attribute on this field."
|
|
)
|
|
raise Exception(msg % view_name)
|
|
|
|
def from_native(self, value):
|
|
# Convert URL -> model instance pk
|
|
# TODO: Use values_list
|
|
queryset = self.queryset
|
|
if queryset is None:
|
|
raise Exception("Writable related fields must include a `queryset` argument")
|
|
|
|
try:
|
|
http_prefix = value.startswith(("http:", "https:"))
|
|
except AttributeError:
|
|
msg = self.error_messages["incorrect_type"]
|
|
raise ValidationError(msg % type(value).__name__)
|
|
|
|
if http_prefix:
|
|
# If needed convert absolute URLs to relative path
|
|
value = urlparse.urlparse(value).path
|
|
prefix = get_script_prefix()
|
|
if value.startswith(prefix):
|
|
value = "/" + value[len(prefix):]
|
|
|
|
try:
|
|
match = resolve(value)
|
|
except Exception:
|
|
raise ValidationError(self.error_messages["no_match"])
|
|
|
|
if match.view_name != self.view_name:
|
|
raise ValidationError(self.error_messages["incorrect_match"])
|
|
|
|
try:
|
|
return self.get_object(queryset, match.view_name,
|
|
match.args, match.kwargs)
|
|
except (ObjectDoesNotExist, TypeError, ValueError):
|
|
raise ValidationError(self.error_messages["does_not_exist"])
|
|
|
|
|
|
class HyperlinkedIdentityField(Field):
|
|
"""
|
|
Represents the instance, or a property on the instance, using hyperlinking.
|
|
"""
|
|
lookup_field = "pk"
|
|
read_only = True
|
|
|
|
# These are all pending deprecation
|
|
pk_url_kwarg = "pk"
|
|
slug_field = "slug"
|
|
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
try:
|
|
self.view_name = kwargs.pop("view_name")
|
|
except KeyError:
|
|
msg = "HyperlinkedIdentityField requires \"view_name\" argument"
|
|
raise ValueError(msg)
|
|
|
|
self.format = kwargs.pop("format", None)
|
|
lookup_field = kwargs.pop("lookup_field", None)
|
|
self.lookup_field = lookup_field or self.lookup_field
|
|
|
|
# These are pending deprecation
|
|
if "pk_url_kwarg" in kwargs:
|
|
msg = "pk_url_kwarg is pending deprecation. Use lookup_field instead."
|
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
|
if "slug_url_kwarg" in kwargs:
|
|
msg = "slug_url_kwarg is pending deprecation. Use lookup_field instead."
|
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
|
if "slug_field" in kwargs:
|
|
msg = "slug_field is pending deprecation. Use lookup_field instead."
|
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
|
|
|
self.slug_field = kwargs.pop("slug_field", self.slug_field)
|
|
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
|
self.pk_url_kwarg = kwargs.pop("pk_url_kwarg", self.pk_url_kwarg)
|
|
self.slug_url_kwarg = kwargs.pop("slug_url_kwarg", default_slug_kwarg)
|
|
|
|
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
|
|
|
|
def field_to_native(self, obj, field_name):
|
|
request = self.context.get("request", None)
|
|
format = self.context.get("format", None)
|
|
view_name = self.view_name
|
|
|
|
if request is None:
|
|
warnings.warn("Using `HyperlinkedIdentityField` without including the "
|
|
"request in the serializer context is deprecated. "
|
|
"Add `context={'request': request}` when instantiating the serializer.",
|
|
DeprecationWarning, stacklevel=4)
|
|
|
|
# By default use whatever format is given for the current context
|
|
# unless the target is a different type to the source.
|
|
#
|
|
# Eg. Consider a HyperlinkedIdentityField pointing from a json
|
|
# representation to an html property of that representation...
|
|
#
|
|
# "/snippets/1/" should link to "/snippets/1/highlight/"
|
|
# ...but...
|
|
# "/snippets/1/.json" should link to "/snippets/1/highlight/.html"
|
|
if format and self.format and self.format != format:
|
|
format = self.format
|
|
|
|
# Return the hyperlink, or error if incorrectly configured.
|
|
try:
|
|
return self.get_url(obj, view_name, request, format)
|
|
except NoReverseMatch:
|
|
msg = (
|
|
"Could not resolve URL for hyperlinked relationship using "
|
|
"view name '%s'. You may have failed to include the related "
|
|
"model in your API, or incorrectly configured the "
|
|
"`lookup_field` attribute on this field."
|
|
)
|
|
raise Exception(msg % view_name)
|
|
|
|
def get_url(self, obj, view_name, request, format):
|
|
"""
|
|
Given an object, return the URL that hyperlinks to the object.
|
|
|
|
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
|
attributes are not configured to correctly match the URL conf.
|
|
"""
|
|
lookup_field = getattr(obj, self.lookup_field, None)
|
|
kwargs = {self.lookup_field: lookup_field}
|
|
|
|
# Handle unsaved object case
|
|
if lookup_field is None:
|
|
return None
|
|
|
|
try:
|
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
except NoReverseMatch:
|
|
pass
|
|
|
|
if self.pk_url_kwarg != "pk":
|
|
# Only try pk lookup if it has been explicitly set.
|
|
# Otherwise, the default `lookup_field = "pk"` has us covered.
|
|
kwargs = {self.pk_url_kwarg: obj.pk}
|
|
try:
|
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
except NoReverseMatch:
|
|
pass
|
|
|
|
slug = getattr(obj, self.slug_field, None)
|
|
if slug:
|
|
# Only use slug lookup if a slug field exists on the model
|
|
kwargs = {self.slug_url_kwarg: slug}
|
|
try:
|
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
except NoReverseMatch:
|
|
pass
|
|
|
|
raise NoReverseMatch()
|