mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-23 15:54:16 +03:00
Relation fields move into relations.py
This commit is contained in:
parent
33580c82b3
commit
8fad0a727a
|
@ -2,11 +2,11 @@
|
|||
|
||||
# Serializer fields
|
||||
|
||||
> Flat is better than nested.
|
||||
> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it -- normalizing it to a consistent format.
|
||||
>
|
||||
> — [The Zen of Python][cite]
|
||||
> — [Django documentation][cite]
|
||||
|
||||
Serializer fields handle converting between primative values and internal datatypes. They also deal with validating input values, as well as retrieving and setting the values from their parent objects.
|
||||
Serializer fields handle converting between primitive values and internal datatypes. They also deal with validating input values, as well as retrieving and setting the values from their parent objects.
|
||||
|
||||
---
|
||||
|
||||
|
@ -28,7 +28,7 @@ Defaults to the name of the field.
|
|||
|
||||
### `read_only`
|
||||
|
||||
Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance dureing deserialization.
|
||||
Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance during deserialization.
|
||||
|
||||
Defaults to `False`
|
||||
|
||||
|
@ -41,7 +41,7 @@ Defaults to `True`.
|
|||
|
||||
### `default`
|
||||
|
||||
If set, this gives the default value that will be used for the field if none is supplied. If not set the default behaviour is to not populate the attribute at all.
|
||||
If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all.
|
||||
|
||||
### `validators`
|
||||
|
||||
|
@ -96,9 +96,9 @@ Would produce output similar to:
|
|||
'expired': True
|
||||
}
|
||||
|
||||
By default, the `Field` class will perform a basic translation of the source value into primative datatypes, falling back to unicode representations of complex datatypes when necessary.
|
||||
By default, the `Field` class will perform a basic translation of the source value into primitive datatypes, falling back to unicode representations of complex datatypes when necessary.
|
||||
|
||||
You can customize this behaviour by overriding the `.to_native(self, value)` method.
|
||||
You can customize this behavior by overriding the `.to_native(self, value)` method.
|
||||
|
||||
## WritableField
|
||||
|
||||
|
@ -110,6 +110,24 @@ A generic field that can be tied to any arbitrary model field. The `ModelField`
|
|||
|
||||
**Signature:** `ModelField(model_field=<Django ModelField class>)`
|
||||
|
||||
## SerializerMethodField
|
||||
|
||||
This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.timezone import now
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
days_since_joined = serializers.SerializerMethodField('get_days_since_joined')
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
def get_days_since_joined(self, obj):
|
||||
return (now() - obj.date_joined).days
|
||||
|
||||
---
|
||||
|
||||
# Typed Fields
|
||||
|
@ -211,151 +229,8 @@ Signature and validation is the same as with `FileField`.
|
|||
|
||||
---
|
||||
|
||||
**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since eg json doesn't support file uploads.
|
||||
**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since e.g. json doesn't support file uploads.
|
||||
Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
|
||||
|
||||
---
|
||||
|
||||
# Relational Fields
|
||||
|
||||
Relational fields are used to represent model relationships. They can be applied to `ForeignKey`, `ManyToManyField` and `OneToOneField` relationships, as well as to reverse relationships, and custom relationships such as `GenericForeignKey`.
|
||||
|
||||
## RelatedField
|
||||
|
||||
This field can be applied to any of the following:
|
||||
|
||||
* A `ForeignKey` field.
|
||||
* A `OneToOneField` field.
|
||||
* A reverse OneToOne relationship
|
||||
* Any other "to-one" relationship.
|
||||
|
||||
By default `RelatedField` will represent the target of the field using it's `__unicode__` method.
|
||||
|
||||
You can customise this behaviour by subclassing `ManyRelatedField`, and overriding the `.to_native(self, value)` method.
|
||||
|
||||
## ManyRelatedField
|
||||
|
||||
This field can be applied to any of the following:
|
||||
|
||||
* A `ManyToManyField` field.
|
||||
* A reverse ManyToMany relationship.
|
||||
* A reverse ForeignKey relationship
|
||||
* Any other "to-many" relationship.
|
||||
|
||||
By default `ManyRelatedField` will represent the targets of the field using their `__unicode__` method.
|
||||
|
||||
For example, given the following models:
|
||||
|
||||
class TaggedItem(models.Model):
|
||||
"""
|
||||
Tags arbitrary model instances using a generic relation.
|
||||
|
||||
See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/
|
||||
"""
|
||||
tag = models.SlugField()
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.tag
|
||||
|
||||
|
||||
class Bookmark(models.Model):
|
||||
"""
|
||||
A bookmark consists of a URL, and 0 or more descriptive tags.
|
||||
"""
|
||||
url = models.URLField()
|
||||
tags = GenericRelation(TaggedItem)
|
||||
|
||||
And a model serializer defined like this:
|
||||
|
||||
class BookmarkSerializer(serializers.ModelSerializer):
|
||||
tags = serializers.ManyRelatedField(source='tags')
|
||||
|
||||
class Meta:
|
||||
model = Bookmark
|
||||
exclude = ('id',)
|
||||
|
||||
Then an example output format for a Bookmark instance would be:
|
||||
|
||||
{
|
||||
'tags': [u'django', u'python'],
|
||||
'url': u'https://www.djangoproject.com/'
|
||||
}
|
||||
|
||||
## PrimaryKeyRelatedField / ManyPrimaryKeyRelatedField
|
||||
|
||||
`PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField` will represent the target of the relationship using it's primary key.
|
||||
|
||||
By default these fields are read-write, although you can change this behaviour using the `read_only` flag.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
||||
* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships.
|
||||
|
||||
## SlugRelatedField / ManySlugRelatedField
|
||||
|
||||
`SlugRelatedField` and `ManySlugRelatedField` will represent the target of the relationship using a unique slug.
|
||||
|
||||
By default these fields read-write, although you can change this behaviour using the `read_only` flag.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`.
|
||||
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
||||
* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships.
|
||||
|
||||
## HyperlinkedRelatedField / ManyHyperlinkedRelatedField
|
||||
|
||||
`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink.
|
||||
|
||||
By default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
* `view_name` - The view name that should be used as the target of the relationship. **required**.
|
||||
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
||||
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
||||
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
|
||||
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
|
||||
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
|
||||
* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships.
|
||||
|
||||
## HyperLinkedIdentityField
|
||||
|
||||
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer.
|
||||
|
||||
This field is always read-only.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
* `view_name` - The view name that should be used as the target of the relationship. **required**.
|
||||
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
||||
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
|
||||
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
|
||||
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
|
||||
|
||||
# Other Fields
|
||||
|
||||
## SerializerMethodField
|
||||
|
||||
This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.timezone import now
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
days_since_joined = serializers.SerializerMethodField('get_days_since_joined')
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
def get_days_since_joined(self, obj):
|
||||
return (now() - obj.date_joined).days
|
||||
|
||||
[cite]: http://www.python.org/dev/peps/pep-0020/
|
||||
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
|
||||
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
|
||||
|
|
139
docs/api-guide/relations.md
Normal file
139
docs/api-guide/relations.md
Normal file
|
@ -0,0 +1,139 @@
|
|||
<a class="github" href="relations.py"></a>
|
||||
|
||||
# Serializer relations
|
||||
|
||||
> Bad programmers worry about the code.
|
||||
> Good programmers worry about data structures and their relationships.
|
||||
>
|
||||
> — [Linus Torvalds][cite]
|
||||
|
||||
|
||||
Relational fields are used to represent model relationships. They can be applied to `ForeignKey`, `ManyToManyField` and `OneToOneField` relationships, as well as to reverse relationships, and custom relationships such as `GenericForeignKey`.
|
||||
|
||||
---
|
||||
|
||||
**Note:** The relational fields are declared in `relations.py`, but by convention you should import them using `from rest_framework import serializers` and refer to fields as `serializers.<FieldName>`.
|
||||
|
||||
---
|
||||
|
||||
## RelatedField
|
||||
|
||||
This field can be applied to any of the following:
|
||||
|
||||
* A `ForeignKey` field.
|
||||
* A `OneToOneField` field.
|
||||
* A reverse OneToOne relationship
|
||||
* Any other "to-one" relationship.
|
||||
|
||||
By default `RelatedField` will represent the target of the field using it's `__unicode__` method.
|
||||
|
||||
You can customize this behavior by subclassing `ManyRelatedField`, and overriding the `.to_native(self, value)` method.
|
||||
|
||||
## ManyRelatedField
|
||||
|
||||
This field can be applied to any of the following:
|
||||
|
||||
* A `ManyToManyField` field.
|
||||
* A reverse ManyToMany relationship.
|
||||
* A reverse ForeignKey relationship
|
||||
* Any other "to-many" relationship.
|
||||
|
||||
By default `ManyRelatedField` will represent the targets of the field using their `__unicode__` method.
|
||||
|
||||
For example, given the following models:
|
||||
|
||||
class TaggedItem(models.Model):
|
||||
"""
|
||||
Tags arbitrary model instances using a generic relation.
|
||||
|
||||
See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/
|
||||
"""
|
||||
tag = models.SlugField()
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.tag
|
||||
|
||||
|
||||
class Bookmark(models.Model):
|
||||
"""
|
||||
A bookmark consists of a URL, and 0 or more descriptive tags.
|
||||
"""
|
||||
url = models.URLField()
|
||||
tags = GenericRelation(TaggedItem)
|
||||
|
||||
And a model serializer defined like this:
|
||||
|
||||
class BookmarkSerializer(serializers.ModelSerializer):
|
||||
tags = serializers.ManyRelatedField(source='tags')
|
||||
|
||||
class Meta:
|
||||
model = Bookmark
|
||||
exclude = ('id',)
|
||||
|
||||
Then an example output format for a Bookmark instance would be:
|
||||
|
||||
{
|
||||
'tags': [u'django', u'python'],
|
||||
'url': u'https://www.djangoproject.com/'
|
||||
}
|
||||
|
||||
## PrimaryKeyRelatedField
|
||||
## ManyPrimaryKeyRelatedField
|
||||
|
||||
`PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField` will represent the target of the relationship using it's primary key.
|
||||
|
||||
By default these fields are read-write, although you can change this behavior using the `read_only` flag.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
||||
* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships.
|
||||
|
||||
## SlugRelatedField
|
||||
## ManySlugRelatedField
|
||||
|
||||
`SlugRelatedField` and `ManySlugRelatedField` will represent the target of the relationship using a unique slug.
|
||||
|
||||
By default these fields read-write, although you can change this behavior using the `read_only` flag.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`.
|
||||
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
||||
* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships.
|
||||
|
||||
## HyperlinkedRelatedField
|
||||
## ManyHyperlinkedRelatedField
|
||||
|
||||
`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink.
|
||||
|
||||
By default, `HyperlinkedRelatedField` is read-write, although you can change this behavior using the `read_only` flag.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
* `view_name` - The view name that should be used as the target of the relationship. **required**.
|
||||
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
||||
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
||||
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
|
||||
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
|
||||
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
|
||||
* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships.
|
||||
|
||||
## HyperLinkedIdentityField
|
||||
|
||||
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer.
|
||||
|
||||
This field is always read-only.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
* `view_name` - The view name that should be used as the target of the relationship. **required**.
|
||||
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
||||
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
|
||||
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
|
||||
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
|
||||
|
||||
[cite]: http://lwn.net/Articles/193245/
|
|
@ -94,6 +94,7 @@ The API guide is your complete reference manual to all the functionality provide
|
|||
* [Renderers][renderers]
|
||||
* [Serializers][serializers]
|
||||
* [Serializer fields][fields]
|
||||
* [Serializer relations][relations]
|
||||
* [Authentication][authentication]
|
||||
* [Permissions][permissions]
|
||||
* [Throttling][throttling]
|
||||
|
@ -185,6 +186,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[renderers]: api-guide/renderers.md
|
||||
[serializers]: api-guide/serializers.md
|
||||
[fields]: api-guide/fields.md
|
||||
[relations]: api-guide/relations.md
|
||||
[authentication]: api-guide/authentication.md
|
||||
[permissions]: api-guide/permissions.md
|
||||
[throttling]: api-guide/throttling.md
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
<li><a href="{{ base_url }}/api-guide/renderers{{ suffix }}">Renderers</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/serializers{{ suffix }}">Serializers</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/fields{{ suffix }}">Serializer fields</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/relations{{ suffix }}">Serializer relations</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/authentication{{ suffix }}">Authentication</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/permissions{{ suffix }}">Permissions</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/throttling{{ suffix }}">Throttling</a></li>
|
||||
|
|
|
@ -7,18 +7,14 @@ import warnings
|
|||
from io import BytesIO
|
||||
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.urlresolvers import resolve, get_script_prefix
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.conf import settings
|
||||
from django import forms
|
||||
from django.forms import widgets
|
||||
from django.forms.models import ModelChoiceIterator
|
||||
from django.utils.encoding import is_protected_type, smart_unicode
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.compat import parse_date, parse_datetime
|
||||
from rest_framework.compat import timezone
|
||||
from urlparse import urlparse
|
||||
|
||||
|
||||
def is_simple_callable(obj):
|
||||
|
@ -252,443 +248,6 @@ class ModelField(WritableField):
|
|||
"type": self.model_field.get_internal_type()
|
||||
}
|
||||
|
||||
##### Relational fields #####
|
||||
|
||||
|
||||
# Not actually Writable, but subclasses may need to be.
|
||||
class RelatedField(WritableField):
|
||||
"""
|
||||
Base class for related model fields.
|
||||
|
||||
If not overridden, this represents a to-one relationship, using the unicode
|
||||
representation of the target.
|
||||
"""
|
||||
widget = widgets.Select
|
||||
cache_choices = False
|
||||
empty_label = None
|
||||
default_read_only = True # TODO: Remove this
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.queryset = kwargs.pop('queryset', None)
|
||||
self.null = kwargs.pop('null', False)
|
||||
super(RelatedField, self).__init__(*args, **kwargs)
|
||||
self.read_only = kwargs.pop('read_only', self.default_read_only)
|
||||
|
||||
def initialize(self, parent, field_name):
|
||||
super(RelatedField, self).initialize(parent, field_name)
|
||||
if self.queryset is None and not self.read_only:
|
||||
try:
|
||||
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()
|
||||
except:
|
||||
raise
|
||||
msg = ('Serializer related fields must include a `queryset`' +
|
||||
' argument or set `read_only=True')
|
||||
raise Exception(msg)
|
||||
|
||||
### We need this stuff to make form choices work...
|
||||
|
||||
# def __deepcopy__(self, memo):
|
||||
# result = super(RelatedField, self).__deepcopy__(memo)
|
||||
# result.queryset = result.queryset
|
||||
# return result
|
||||
|
||||
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_unicode(obj)
|
||||
ident = smart_unicode(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)
|
||||
|
||||
### Regular serializer stuff...
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = getattr(obj, self.source or field_name)
|
||||
return self.to_native(value)
|
||||
|
||||
def field_from_native(self, data, files, field_name, into):
|
||||
if self.read_only:
|
||||
return
|
||||
|
||||
try:
|
||||
value = data[field_name]
|
||||
except KeyError:
|
||||
if self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
return
|
||||
|
||||
if value in (None, '') and not self.null:
|
||||
raise ValidationError('Value may not be null')
|
||||
elif value in (None, '') and self.null:
|
||||
into[(self.source or field_name)] = None
|
||||
else:
|
||||
into[(self.source or field_name)] = self.from_native(value)
|
||||
|
||||
|
||||
class ManyRelatedMixin(object):
|
||||
"""
|
||||
Mixin to convert a related field to a many related field.
|
||||
"""
|
||||
widget = widgets.SelectMultiple
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = getattr(obj, self.source or field_name)
|
||||
return [self.to_native(item) for item in value.all()]
|
||||
|
||||
def field_from_native(self, data, files, field_name, into):
|
||||
if self.read_only:
|
||||
return
|
||||
|
||||
try:
|
||||
# Form data
|
||||
value = data.getlist(self.source or field_name)
|
||||
except:
|
||||
# Non-form data
|
||||
value = data.get(self.source or field_name)
|
||||
else:
|
||||
if value == ['']:
|
||||
value = []
|
||||
|
||||
into[field_name] = [self.from_native(item) for item in value]
|
||||
|
||||
|
||||
class ManyRelatedField(ManyRelatedMixin, RelatedField):
|
||||
"""
|
||||
Base class for related model managers.
|
||||
|
||||
If not overridden, this represents a to-many relationship, using the unicode
|
||||
representations of the target, and is read-only.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
### PrimaryKey relationships
|
||||
|
||||
class PrimaryKeyRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a to-one relationship as a pk value.
|
||||
"""
|
||||
default_read_only = False
|
||||
form_field_class = forms.ChoiceField
|
||||
|
||||
# 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_unicode(obj)
|
||||
ident = smart_unicode(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 = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
|
||||
raise ValidationError(msg)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
try:
|
||||
# Prefer obj.serializable_value for performance reasons
|
||||
pk = obj.serializable_value(self.source or field_name)
|
||||
except AttributeError:
|
||||
# RelatedObject (reverse relationship)
|
||||
obj = getattr(obj, self.source or field_name)
|
||||
return self.to_native(obj.pk)
|
||||
# Forward relationship
|
||||
return self.to_native(pk)
|
||||
|
||||
|
||||
class ManyPrimaryKeyRelatedField(ManyRelatedField):
|
||||
"""
|
||||
Represents a to-many relationship as a pk value.
|
||||
"""
|
||||
default_read_only = False
|
||||
form_field_class = forms.MultipleChoiceField
|
||||
|
||||
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_unicode(obj)
|
||||
ident = smart_unicode(self.to_native(obj.pk))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
def to_native(self, pk):
|
||||
return pk
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
try:
|
||||
# Prefer obj.serializable_value for performance reasons
|
||||
queryset = obj.serializable_value(self.source or field_name)
|
||||
except AttributeError:
|
||||
# RelatedManager (reverse relationship)
|
||||
queryset = getattr(obj, self.source or field_name)
|
||||
return [self.to_native(item.pk) for item in queryset.all()]
|
||||
# Forward relationship
|
||||
return [self.to_native(item.pk) for item in queryset.all()]
|
||||
|
||||
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 = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
|
||||
raise ValidationError(msg)
|
||||
|
||||
### Slug relationships
|
||||
|
||||
|
||||
class SlugRelatedField(RelatedField):
|
||||
default_read_only = False
|
||||
form_field_class = forms.ChoiceField
|
||||
|
||||
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('Object with %s=%s does not exist.' %
|
||||
(self.slug_field, unicode(data)))
|
||||
|
||||
|
||||
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
|
||||
form_field_class = forms.MultipleChoiceField
|
||||
|
||||
|
||||
### Hyperlinked relationships
|
||||
|
||||
class HyperlinkedRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a to-one relationship, using hyperlinking.
|
||||
"""
|
||||
pk_url_kwarg = 'pk'
|
||||
slug_field = 'slug'
|
||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||
default_read_only = False
|
||||
form_field_class = forms.ChoiceField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.view_name = kwargs.pop('view_name')
|
||||
except:
|
||||
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
|
||||
|
||||
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)
|
||||
|
||||
self.format = kwargs.pop('format', None)
|
||||
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_slug_field(self):
|
||||
"""
|
||||
Get the name of a slug field to be used to look up by slug.
|
||||
"""
|
||||
return self.slug_field
|
||||
|
||||
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)
|
||||
pk = getattr(obj, 'pk', None)
|
||||
if pk is None:
|
||||
return
|
||||
kwargs = {self.pk_url_kwarg: pk}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
slug = getattr(obj, self.slug_field, None)
|
||||
|
||||
if not slug:
|
||||
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
||||
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
||||
|
||||
def from_native(self, value):
|
||||
# Convert URL -> model instance pk
|
||||
# TODO: Use values_list
|
||||
if self.queryset is None:
|
||||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
if value.startswith('http:') or value.startswith('https:'):
|
||||
# If needed convert absolute URLs to relative path
|
||||
value = urlparse(value).path
|
||||
prefix = get_script_prefix()
|
||||
if value.startswith(prefix):
|
||||
value = '/' + value[len(prefix):]
|
||||
|
||||
try:
|
||||
match = resolve(value)
|
||||
except:
|
||||
raise ValidationError('Invalid hyperlink - No URL match')
|
||||
|
||||
if match.url_name != self.view_name:
|
||||
raise ValidationError('Invalid hyperlink - Incorrect URL match')
|
||||
|
||||
pk = match.kwargs.get(self.pk_url_kwarg, None)
|
||||
slug = match.kwargs.get(self.slug_url_kwarg, None)
|
||||
|
||||
# Try explicit primary key.
|
||||
if pk is not None:
|
||||
queryset = self.queryset.filter(pk=pk)
|
||||
# Next, try looking up by slug.
|
||||
elif slug is not None:
|
||||
slug_field = self.get_slug_field()
|
||||
queryset = self.queryset.filter(**{slug_field: slug})
|
||||
# If none of those are defined, it's an error.
|
||||
else:
|
||||
raise ValidationError('Invalid hyperlink')
|
||||
|
||||
try:
|
||||
obj = queryset.get()
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError('Invalid hyperlink - object does not exist.')
|
||||
return obj
|
||||
|
||||
|
||||
class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
|
||||
"""
|
||||
Represents a to-many relationship, using hyperlinking.
|
||||
"""
|
||||
form_field_class = forms.MultipleChoiceField
|
||||
|
||||
|
||||
class HyperlinkedIdentityField(Field):
|
||||
"""
|
||||
Represents the instance, or a property on the instance, using hyperlinking.
|
||||
"""
|
||||
pk_url_kwarg = 'pk'
|
||||
slug_field = 'slug'
|
||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# TODO: Make view_name mandatory, and have the
|
||||
# HyperlinkedModelSerializer set it on-the-fly
|
||||
self.view_name = kwargs.pop('view_name', None)
|
||||
self.format = kwargs.pop('format', None)
|
||||
|
||||
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.format or self.context.get('format', None)
|
||||
view_name = self.view_name or self.parent.opts.view_name
|
||||
kwargs = {self.pk_url_kwarg: obj.pk}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
slug = getattr(obj, self.slug_field, None)
|
||||
|
||||
if not slug:
|
||||
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
||||
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
||||
|
||||
|
||||
##### Typed Fields #####
|
||||
|
||||
|
|
446
rest_framework/relations.py
Normal file
446
rest_framework/relations.py
Normal file
|
@ -0,0 +1,446 @@
|
|||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.urlresolvers import resolve, get_script_prefix
|
||||
from django import forms
|
||||
from django.forms import widgets
|
||||
from django.forms.models import ModelChoiceIterator
|
||||
from django.utils.encoding import smart_unicode
|
||||
from rest_framework.fields import Field, WritableField
|
||||
from rest_framework.reverse import reverse
|
||||
from urlparse import urlparse
|
||||
|
||||
##### Relational fields #####
|
||||
|
||||
|
||||
# Not actually Writable, but subclasses may need to be.
|
||||
class RelatedField(WritableField):
|
||||
"""
|
||||
Base class for related model fields.
|
||||
|
||||
If not overridden, this represents a to-one relationship, using the unicode
|
||||
representation of the target.
|
||||
"""
|
||||
widget = widgets.Select
|
||||
cache_choices = False
|
||||
empty_label = None
|
||||
default_read_only = True # TODO: Remove this
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.queryset = kwargs.pop('queryset', None)
|
||||
self.null = kwargs.pop('null', False)
|
||||
super(RelatedField, self).__init__(*args, **kwargs)
|
||||
self.read_only = kwargs.pop('read_only', self.default_read_only)
|
||||
|
||||
def initialize(self, parent, field_name):
|
||||
super(RelatedField, self).initialize(parent, field_name)
|
||||
if self.queryset is None and not self.read_only:
|
||||
try:
|
||||
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()
|
||||
except:
|
||||
raise
|
||||
msg = ('Serializer related fields must include a `queryset`' +
|
||||
' argument or set `read_only=True')
|
||||
raise Exception(msg)
|
||||
|
||||
### We need this stuff to make form choices work...
|
||||
|
||||
# def __deepcopy__(self, memo):
|
||||
# result = super(RelatedField, self).__deepcopy__(memo)
|
||||
# result.queryset = result.queryset
|
||||
# return result
|
||||
|
||||
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_unicode(obj)
|
||||
ident = smart_unicode(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)
|
||||
|
||||
### Regular serializer stuff...
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = getattr(obj, self.source or field_name)
|
||||
return self.to_native(value)
|
||||
|
||||
def field_from_native(self, data, files, field_name, into):
|
||||
if self.read_only:
|
||||
return
|
||||
|
||||
try:
|
||||
value = data[field_name]
|
||||
except KeyError:
|
||||
if self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
return
|
||||
|
||||
if value in (None, '') and not self.null:
|
||||
raise ValidationError('Value may not be null')
|
||||
elif value in (None, '') and self.null:
|
||||
into[(self.source or field_name)] = None
|
||||
else:
|
||||
into[(self.source or field_name)] = self.from_native(value)
|
||||
|
||||
|
||||
class ManyRelatedMixin(object):
|
||||
"""
|
||||
Mixin to convert a related field to a many related field.
|
||||
"""
|
||||
widget = widgets.SelectMultiple
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = getattr(obj, self.source or field_name)
|
||||
return [self.to_native(item) for item in value.all()]
|
||||
|
||||
def field_from_native(self, data, files, field_name, into):
|
||||
if self.read_only:
|
||||
return
|
||||
|
||||
try:
|
||||
# Form data
|
||||
value = data.getlist(self.source or field_name)
|
||||
except:
|
||||
# Non-form data
|
||||
value = data.get(self.source or field_name)
|
||||
else:
|
||||
if value == ['']:
|
||||
value = []
|
||||
|
||||
into[field_name] = [self.from_native(item) for item in value]
|
||||
|
||||
|
||||
class ManyRelatedField(ManyRelatedMixin, RelatedField):
|
||||
"""
|
||||
Base class for related model managers.
|
||||
|
||||
If not overridden, this represents a to-many relationship, using the unicode
|
||||
representations of the target, and is read-only.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
### PrimaryKey relationships
|
||||
|
||||
class PrimaryKeyRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a to-one relationship as a pk value.
|
||||
"""
|
||||
default_read_only = False
|
||||
form_field_class = forms.ChoiceField
|
||||
|
||||
# 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_unicode(obj)
|
||||
ident = smart_unicode(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 = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
|
||||
raise ValidationError(msg)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
try:
|
||||
# Prefer obj.serializable_value for performance reasons
|
||||
pk = obj.serializable_value(self.source or field_name)
|
||||
except AttributeError:
|
||||
# RelatedObject (reverse relationship)
|
||||
obj = getattr(obj, self.source or field_name)
|
||||
return self.to_native(obj.pk)
|
||||
# Forward relationship
|
||||
return self.to_native(pk)
|
||||
|
||||
|
||||
class ManyPrimaryKeyRelatedField(ManyRelatedField):
|
||||
"""
|
||||
Represents a to-many relationship as a pk value.
|
||||
"""
|
||||
default_read_only = False
|
||||
form_field_class = forms.MultipleChoiceField
|
||||
|
||||
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_unicode(obj)
|
||||
ident = smart_unicode(self.to_native(obj.pk))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
def to_native(self, pk):
|
||||
return pk
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
try:
|
||||
# Prefer obj.serializable_value for performance reasons
|
||||
queryset = obj.serializable_value(self.source or field_name)
|
||||
except AttributeError:
|
||||
# RelatedManager (reverse relationship)
|
||||
queryset = getattr(obj, self.source or field_name)
|
||||
return [self.to_native(item.pk) for item in queryset.all()]
|
||||
# Forward relationship
|
||||
return [self.to_native(item.pk) for item in queryset.all()]
|
||||
|
||||
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 = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
|
||||
raise ValidationError(msg)
|
||||
|
||||
### Slug relationships
|
||||
|
||||
|
||||
class SlugRelatedField(RelatedField):
|
||||
default_read_only = False
|
||||
form_field_class = forms.ChoiceField
|
||||
|
||||
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('Object with %s=%s does not exist.' %
|
||||
(self.slug_field, unicode(data)))
|
||||
|
||||
|
||||
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
|
||||
form_field_class = forms.MultipleChoiceField
|
||||
|
||||
|
||||
### Hyperlinked relationships
|
||||
|
||||
class HyperlinkedRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a to-one relationship, using hyperlinking.
|
||||
"""
|
||||
pk_url_kwarg = 'pk'
|
||||
slug_field = 'slug'
|
||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||
default_read_only = False
|
||||
form_field_class = forms.ChoiceField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.view_name = kwargs.pop('view_name')
|
||||
except:
|
||||
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
|
||||
|
||||
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)
|
||||
|
||||
self.format = kwargs.pop('format', None)
|
||||
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_slug_field(self):
|
||||
"""
|
||||
Get the name of a slug field to be used to look up by slug.
|
||||
"""
|
||||
return self.slug_field
|
||||
|
||||
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)
|
||||
pk = getattr(obj, 'pk', None)
|
||||
if pk is None:
|
||||
return
|
||||
kwargs = {self.pk_url_kwarg: pk}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
slug = getattr(obj, self.slug_field, None)
|
||||
|
||||
if not slug:
|
||||
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
||||
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
||||
|
||||
def from_native(self, value):
|
||||
# Convert URL -> model instance pk
|
||||
# TODO: Use values_list
|
||||
if self.queryset is None:
|
||||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
if value.startswith('http:') or value.startswith('https:'):
|
||||
# If needed convert absolute URLs to relative path
|
||||
value = urlparse(value).path
|
||||
prefix = get_script_prefix()
|
||||
if value.startswith(prefix):
|
||||
value = '/' + value[len(prefix):]
|
||||
|
||||
try:
|
||||
match = resolve(value)
|
||||
except:
|
||||
raise ValidationError('Invalid hyperlink - No URL match')
|
||||
|
||||
if match.url_name != self.view_name:
|
||||
raise ValidationError('Invalid hyperlink - Incorrect URL match')
|
||||
|
||||
pk = match.kwargs.get(self.pk_url_kwarg, None)
|
||||
slug = match.kwargs.get(self.slug_url_kwarg, None)
|
||||
|
||||
# Try explicit primary key.
|
||||
if pk is not None:
|
||||
queryset = self.queryset.filter(pk=pk)
|
||||
# Next, try looking up by slug.
|
||||
elif slug is not None:
|
||||
slug_field = self.get_slug_field()
|
||||
queryset = self.queryset.filter(**{slug_field: slug})
|
||||
# If none of those are defined, it's an error.
|
||||
else:
|
||||
raise ValidationError('Invalid hyperlink')
|
||||
|
||||
try:
|
||||
obj = queryset.get()
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError('Invalid hyperlink - object does not exist.')
|
||||
return obj
|
||||
|
||||
|
||||
class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
|
||||
"""
|
||||
Represents a to-many relationship, using hyperlinking.
|
||||
"""
|
||||
form_field_class = forms.MultipleChoiceField
|
||||
|
||||
|
||||
class HyperlinkedIdentityField(Field):
|
||||
"""
|
||||
Represents the instance, or a property on the instance, using hyperlinking.
|
||||
"""
|
||||
pk_url_kwarg = 'pk'
|
||||
slug_field = 'slug'
|
||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# TODO: Make view_name mandatory, and have the
|
||||
# HyperlinkedModelSerializer set it on-the-fly
|
||||
self.view_name = kwargs.pop('view_name', None)
|
||||
self.format = kwargs.pop('format', None)
|
||||
|
||||
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.format or self.context.get('format', None)
|
||||
view_name = self.view_name or self.parent.opts.view_name
|
||||
kwargs = {self.pk_url_kwarg: obj.pk}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
slug = getattr(obj, self.slug_field, None)
|
||||
|
||||
if not slug:
|
||||
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
||||
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
|
||||
except:
|
||||
pass
|
||||
|
||||
raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
|
|
@ -14,7 +14,7 @@ from rest_framework.compat import get_concrete_model
|
|||
# This helps keep the seperation between model fields, form fields, and
|
||||
# serializer fields more explicit.
|
||||
|
||||
|
||||
from rest_framework.relations import *
|
||||
from rest_framework.fields import *
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user