diff options
| author | Andrew Hankinson | 2013-01-01 11:36:23 -0500 | 
|---|---|---|
| committer | Andrew Hankinson | 2013-01-01 11:36:23 -0500 | 
| commit | 389ca3b3b1faa90ea4624f495115d83024fdc151 (patch) | |
| tree | 24f1877904d2d1e40d62d52562ac9940175d7b5b | |
| parent | c6f212238c238561749574a54aec3b1b1fd8df61 (diff) | |
| parent | eff833b39d2f41c9eb773214f5b45c3d991e1511 (diff) | |
| download | django-rest-framework-389ca3b3b1faa90ea4624f495115d83024fdc151.tar.bz2 | |
Merge branch 'master' of git://github.com/tomchristie/django-rest-framework into patch-support
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | docs/api-guide/fields.md | 179 | ||||
| -rw-r--r-- | docs/api-guide/relations.md | 139 | ||||
| -rw-r--r-- | docs/index.md | 2 | ||||
| -rw-r--r-- | docs/template.html | 1 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 9 | ||||
| -rw-r--r-- | rest_framework/__init__.py | 2 | ||||
| -rw-r--r-- | rest_framework/fields.py | 443 | ||||
| -rw-r--r-- | rest_framework/relations.py | 446 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 2 | 
10 files changed, 638 insertions, 597 deletions
| @@ -81,6 +81,18 @@ To run the tests.  # Changelog +### 2.1.14 + +**Date**: 31st Dec 2012 + +* Bugfix: ModelSerializers now include reverse FK fields on creation. +* Bugfix: Model fields with `blank=True` are now `required=False` by default. +* Bugfix: Nested serializers now support nullable relationships. + +**Note**: From 2.1.14 onwards, relational fields move out of the `fields.py` module and into the new `relations.py` module, in order to seperate them from regular data type fields, such as `CharField` and `IntegerField`. + +This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and refering to fields using the style `serializers.PrimaryKeyRelatedField`. +  ### 2.1.13  **Date**: 28th Dec 2012 diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 50a09701..5bc8f7f7 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -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 diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md new file mode 100644 index 00000000..351b5e09 --- /dev/null +++ b/docs/api-guide/relations.md @@ -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/ diff --git a/docs/index.md b/docs/index.md index 69d972d0..4d50e5d6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/docs/template.html b/docs/template.html index 676a4807..d789cc58 100644 --- a/docs/template.html +++ b/docs/template.html @@ -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> diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 3ca3e6b3..c93eebac 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -16,12 +16,19 @@ Major version numbers (x.0.0) are reserved for project milestones.  No major poi  ## 2.1.x series -### Master +### 2.1.14 + +**Date**: 31st Dec 2012  * Bugfix: ModelSerializers now include reverse FK fields on creation.  * Bugfix: Model fields with `blank=True` are now `required=False` by default.  * Bugfix: Nested serializers now support nullable relationships. +**Note**: From 2.1.14 onwards, relational fields move out of the `fields.py` module and into the new `relations.py` module, in order to seperate them from regular data type fields, such as `CharField` and `IntegerField`. + +This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and refering to fields using the style `serializers.PrimaryKeyRelatedField`. + +  ### 2.1.13  **Date**: 28th Dec 2012 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2e38d863..151ba832 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.13' +__version__ = '2.1.14'  VERSION = __version__  # synonym diff --git a/rest_framework/fields.py b/rest_framework/fields.py index dd90c3f8..d8b82e5f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -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 ##### diff --git a/rest_framework/relations.py b/rest_framework/relations.py new file mode 100644 index 00000000..9b3a7790 --- /dev/null +++ b/rest_framework/relations.py @@ -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) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e8e6735a..ed173d85 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -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 * | 
