aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2012-11-01 23:04:13 +0000
committerTom Christie2012-11-01 23:04:13 +0000
commitd327c5f531b341ad980d20454211b02b87f34d0e (patch)
treea12f9fd53c73f7e9b4dd06785e3d357259e0a678
parent027c9079f62322fe933bdfd4438f23cf4848e3cc (diff)
downloaddjango-rest-framework-d327c5f531b341ad980d20454211b02b87f34d0e.tar.bz2
Relational field support in browseable API.
Add slug relational fields. Add quickstart.
-rw-r--r--djangorestframework.egg-info/PKG-INFO19
-rw-r--r--djangorestframework.egg-info/SOURCES.txt86
-rw-r--r--djangorestframework.egg-info/dependency_links.txt1
-rw-r--r--djangorestframework.egg-info/top_level.txt7
-rw-r--r--docs/index.md2
-rw-r--r--docs/template.html2
-rw-r--r--docs/tutorial/quickstart.md9
-rw-r--r--rest_framework/fields.py107
-rw-r--r--rest_framework/renderers.py25
-rw-r--r--rest_framework/static/rest_framework/css/default.css7
-rw-r--r--rest_framework/templates/rest_framework/base.html4
11 files changed, 244 insertions, 25 deletions
diff --git a/djangorestframework.egg-info/PKG-INFO b/djangorestframework.egg-info/PKG-INFO
new file mode 100644
index 00000000..04eafbde
--- /dev/null
+++ b/djangorestframework.egg-info/PKG-INFO
@@ -0,0 +1,19 @@
+Metadata-Version: 1.0
+Name: djangorestframework
+Version: 2.0.0
+Summary: A lightweight REST framework for Django.
+Home-page: http://django-rest-framework.org
+Author: Tom Christie
+Author-email: tom@tomchristie.com
+License: BSD
+Download-URL: http://pypi.python.org/pypi/rest_framework/
+Description: UNKNOWN
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Environment :: Web Environment
+Classifier: Framework :: Django
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: WWW/HTTP
diff --git a/djangorestframework.egg-info/SOURCES.txt b/djangorestframework.egg-info/SOURCES.txt
new file mode 100644
index 00000000..298d1c33
--- /dev/null
+++ b/djangorestframework.egg-info/SOURCES.txt
@@ -0,0 +1,86 @@
+MANIFEST.in
+setup.py
+djangorestframework.egg-info/PKG-INFO
+djangorestframework.egg-info/SOURCES.txt
+djangorestframework.egg-info/dependency_links.txt
+djangorestframework.egg-info/top_level.txt
+rest_framework/__init__.py
+rest_framework/authentication.py
+rest_framework/compat.py
+rest_framework/decorators.py
+rest_framework/exceptions.py
+rest_framework/fields.py
+rest_framework/generics.py
+rest_framework/mixins.py
+rest_framework/models.py
+rest_framework/negotiation.py
+rest_framework/pagination.py
+rest_framework/parsers.py
+rest_framework/permissions.py
+rest_framework/renderers.py
+rest_framework/request.py
+rest_framework/response.py
+rest_framework/reverse.py
+rest_framework/serializers.py
+rest_framework/settings.py
+rest_framework/status.py
+rest_framework/throttling.py
+rest_framework/urlpatterns.py
+rest_framework/urls.py
+rest_framework/views.py
+rest_framework/authtoken/__init__.py
+rest_framework/authtoken/models.py
+rest_framework/authtoken/views.py
+rest_framework/authtoken/migrations/0001_initial.py
+rest_framework/authtoken/migrations/__init__.py
+rest_framework/runtests/__init__.py
+rest_framework/runtests/runcoverage.py
+rest_framework/runtests/runtests.py
+rest_framework/runtests/settings.py
+rest_framework/runtests/urls.py
+rest_framework/static/rest_framework/css/bootstrap-tweaks.css
+rest_framework/static/rest_framework/css/bootstrap.min.css
+rest_framework/static/rest_framework/css/default.css
+rest_framework/static/rest_framework/css/prettify.css
+rest_framework/static/rest_framework/img/glyphicons-halflings-white.png
+rest_framework/static/rest_framework/img/glyphicons-halflings.png
+rest_framework/static/rest_framework/img/grid.png
+rest_framework/static/rest_framework/js/bootstrap.min.js
+rest_framework/static/rest_framework/js/default.js
+rest_framework/static/rest_framework/js/jquery-1.8.1-min.js
+rest_framework/static/rest_framework/js/prettify-min.js
+rest_framework/templates/rest_framework/api.html
+rest_framework/templates/rest_framework/base.html
+rest_framework/templates/rest_framework/login.html
+rest_framework/templatetags/__init__.py
+rest_framework/templatetags/rest_framework.py
+rest_framework/tests/__init__.py
+rest_framework/tests/authentication.py
+rest_framework/tests/breadcrumbs.py
+rest_framework/tests/decorators.py
+rest_framework/tests/description.py
+rest_framework/tests/files.py
+rest_framework/tests/genericrelations.py
+rest_framework/tests/generics.py
+rest_framework/tests/htmlrenderer.py
+rest_framework/tests/hyperlinkedserializers.py
+rest_framework/tests/models.py
+rest_framework/tests/modelviews.py
+rest_framework/tests/negotiation.py
+rest_framework/tests/pagination.py
+rest_framework/tests/parsers.py
+rest_framework/tests/renderers.py
+rest_framework/tests/request.py
+rest_framework/tests/response.py
+rest_framework/tests/reverse.py
+rest_framework/tests/serializer.py
+rest_framework/tests/status.py
+rest_framework/tests/testcases.py
+rest_framework/tests/tests.py
+rest_framework/tests/throttling.py
+rest_framework/tests/validators.py
+rest_framework/tests/views.py
+rest_framework/utils/__init__.py
+rest_framework/utils/breadcrumbs.py
+rest_framework/utils/encoders.py
+rest_framework/utils/mediatypes.py \ No newline at end of file
diff --git a/djangorestframework.egg-info/dependency_links.txt b/djangorestframework.egg-info/dependency_links.txt
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/djangorestframework.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/djangorestframework.egg-info/top_level.txt b/djangorestframework.egg-info/top_level.txt
new file mode 100644
index 00000000..7e18534d
--- /dev/null
+++ b/djangorestframework.egg-info/top_level.txt
@@ -0,0 +1,7 @@
+rest_framework/authtoken
+rest_framework/utils
+rest_framework/tests
+rest_framework/runtests
+rest_framework/templatetags
+rest_framework
+rest_framework/authtoken/migrations
diff --git a/docs/index.md b/docs/index.md
index 75a1cf6e..5e086872 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -66,11 +66,9 @@ If you're intending to use the browseable API you'll want to add REST framework'
Note that the URL path can be whatever you want, but you must include `rest_framework.urls` with the `rest_framework` namespace.
-<!--
## Quickstart
Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running with REST framework.
--->
## Tutorial
diff --git a/docs/template.html b/docs/template.html
index 94fc269f..c428dff3 100644
--- a/docs/template.html
+++ b/docs/template.html
@@ -53,7 +53,7 @@
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
<ul class="dropdown-menu">
- <!--<li><a href="{{ base_url }}/tutorial/quickstart{{ suffix }}">Quickstart</a></li>-->
+ <li><a href="{{ base_url }}/tutorial/quickstart{{ suffix }}">Quickstart</a></li>
<li><a href="{{ base_url }}/tutorial/1-serialization{{ suffix }}">1 - Serialization</a></li>
<li><a href="{{ base_url }}/tutorial/2-requests-and-responses{{ suffix }}">2 - Requests and responses</a></li>
<li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li>
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index 6bde725b..93da1a59 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -19,12 +19,19 @@ First up we're going to define some serializers in `quickstart/serializers.py` t
class GroupSerializer(serializers.HyperlinkedModelSerializer):
+ permissions = serializers.ManySlugRelatedField(
+ slug_field='codename',
+ queryset=Permission.objects.all()
+ )
+
class Meta:
model = Group
fields = ('url', 'name', 'permissions')
Notice that we're using hyperlinked relations in this case, with `HyperlinkedModelSerializer`. You can also use primary key and various other relationships, but hyperlinking is good RESTful design.
+We've also overridden the `permission` field on the `GroupSerializer`. In this case we don't want to use a hyperlinked representation, but instead use the list of permission codenames associated with the group, so we've used a `ManySlugRelatedField`, using the `codename` field for the representation.
+
## Views
Right, we'd better write some views then. Open `quickstart/views.py` and get typing.
@@ -152,7 +159,7 @@ We can now access our API, both from the command-line, using tools like `curl`..
},
{
"email": "tom@example.com",
- "groups": [],
+ "groups": [ ],
"url": "http://127.0.0.1:8000/users/2/",
"username": "tom"
}
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 1d6d760e..6d858087 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -8,6 +8,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix
from django.conf import settings
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
@@ -232,11 +233,72 @@ class ModelField(WritableField):
class RelatedField(WritableField):
"""
Base class for related model fields.
+
+ If not overridden, this represents a to-one relatinship, using the unicode
+ representation of the target.
"""
+ widget = widgets.Select
+ cache_choices = False
+ empty_label = None
+
def __init__(self, *args, **kwargs):
self.queryset = kwargs.pop('queryset', None)
super(RelatedField, self).__init__(*args, **kwargs)
+ ### 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 = 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 serializier stuff...
+
def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name)
return self.to_native(value)
@@ -253,6 +315,8 @@ 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()]
@@ -276,6 +340,9 @@ class ManyRelatedMixin(object):
class ManyRelatedField(ManyRelatedMixin, RelatedField):
"""
Base class for related model managers.
+
+ If not overridden, this represents a to-many relatinship, using the unicode
+ representations of the target, and is read-only.
"""
pass
@@ -284,7 +351,7 @@ class ManyRelatedField(ManyRelatedMixin, RelatedField):
class PrimaryKeyRelatedField(RelatedField):
"""
- Serializes a related field or related object to a pk value.
+ Represents a to-one relationship as a pk value.
"""
def to_native(self, pk):
@@ -313,7 +380,7 @@ class PrimaryKeyRelatedField(RelatedField):
class ManyPrimaryKeyRelatedField(ManyRelatedField):
"""
- Serializes a to-many related field or related manager to a pk value.
+ Represents a to-many relationship as a pk value.
"""
def to_native(self, pk):
return pk
@@ -329,10 +396,36 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
# Forward relationship
return [self.to_native(item.pk) for item in queryset.all()]
+### Slug relationships
+
+
+class SlugRelatedField(RelatedField):
+ 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):
+ 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):
+ pass
+
### Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
+ """
+ Represents a to-one relationship, using hyperlinking.
+ """
pk_url_kwarg = 'pk'
slug_url_kwarg = 'slug'
slug_field = 'slug'
@@ -417,16 +510,20 @@ class HyperlinkedRelatedField(RelatedField):
class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
+ """
+ Represents a to-many relationship, using hyperlinking.
+ """
pass
class HyperlinkedIdentityField(Field):
"""
- A field that represents the model's identity using a hyperlink.
+ Represents the instance, or a property on the instance, using hyperlinking.
"""
+
def __init__(self, *args, **kwargs):
- # TODO: Make this mandatory, and have the HyperlinkedModelSerializer
- # set it on-the-fly
+ # 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)
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 8dff0c77..63578b14 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -282,10 +282,12 @@ class BrowsableAPIRenderer(BaseRenderer):
serializers.EmailField: forms.EmailField,
serializers.CharField: forms.CharField,
serializers.BooleanField: forms.BooleanField,
- serializers.PrimaryKeyRelatedField: forms.ModelChoiceField,
- serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField,
- serializers.HyperlinkedRelatedField: forms.ModelChoiceField,
- serializers.ManyHyperlinkedRelatedField: forms.ModelMultipleChoiceField
+ serializers.PrimaryKeyRelatedField: forms.ChoiceField,
+ serializers.ManyPrimaryKeyRelatedField: forms.MultipleChoiceField,
+ serializers.SlugRelatedField: forms.ChoiceField,
+ serializers.ManySlugRelatedField: forms.MultipleChoiceField,
+ serializers.HyperlinkedRelatedField: forms.ChoiceField,
+ serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField
}
fields = {}
@@ -296,19 +298,14 @@ class BrowsableAPIRenderer(BaseRenderer):
kwargs = {}
kwargs['required'] = v.required
- if getattr(v, 'queryset', None):
- kwargs['queryset'] = v.queryset
+ #if getattr(v, 'queryset', None):
+ # kwargs['queryset'] = v.queryset
+
+ if getattr(v, 'choices', None) is not None:
+ kwargs['choices'] = v.choices
if getattr(v, 'widget', None):
widget = copy.deepcopy(v.widget)
- # If choices have friendly readable names,
- # then add in the identities too
- if getattr(widget, 'choices', None):
- choices = widget.choices
- if any([ident != desc for (ident, desc) in choices]):
- choices = [(ident, "%s (%s)" % (desc, ident))
- for (ident, desc) in choices]
- widget.choices = choices
kwargs['widget'] = widget
if getattr(v, 'default', None) is not None:
diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css
index e29da395..fdf45659 100644
--- a/rest_framework/static/rest_framework/css/default.css
+++ b/rest_framework/static/rest_framework/css/default.css
@@ -36,6 +36,13 @@ ul.breadcrumb {
margin: 58px 0 0 0;
}
+form select, form input {
+ width: 90%;
+}
+
+form select[multiple] {
+ height: 150px;
+}
/* To allow tooltips to work on disabled elements */
.disabled-tooltip-shield {
position: absolute;
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index e0f79481..9b0a0dca 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -134,7 +134,7 @@
<div class="control-group {% if field.errors %}error{% endif %}">
{{ field.label_tag|add_class:"control-label" }}
<div class="controls">
- {{ field|add_class:"input-xlarge" }}
+ {{ field }}
<span class="help-inline">{{ field.help_text }}</span>
{{ field.errors|add_class:"help-block" }}
</div>
@@ -159,7 +159,7 @@
<div class="control-group {% if field.errors %}error{% endif %}">
{{ field.label_tag|add_class:"control-label" }}
<div class="controls">
- {{ field|add_class:"input-xlarge" }}
+ {{ field }}
<span class='help-inline'>{{ field.help_text }}</span>
{{ field.errors|add_class:"help-block" }}
</div>